diff --git a/.env.example b/.env.example index ac57ff0..dcf0e6e 100644 --- a/.env.example +++ b/.env.example @@ -7,6 +7,17 @@ APP_URL=http://localhost ASSET_URL="${APP_URL}" APP_PORT=8000 SELF_HOSTED=true +REGISTRATION_ENABLED=true + +ENABLED_LOGIN_PROVIDERS= +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= +LINKEDIN_CLIENT_ID= +LINKEDIN_CLIENT_SECRET= +FACEBOOK_CLIENT_ID= +FACEBOOK_CLIENT_SECRET= APP_LOCALE=en APP_FALLBACK_LOCALE=en diff --git a/app/Http/Controllers/SocialLoginController.php b/app/Http/Controllers/SocialLoginController.php new file mode 100644 index 0000000..ddb6598 --- /dev/null +++ b/app/Http/Controllers/SocialLoginController.php @@ -0,0 +1,96 @@ +validateProvider($provider); + + return Socialite::driver($provider)->redirect(); + } + + /** + * Obtain the user information from GitHub. + * + */ + public function handleProviderCallback(string $provider) + { + $this->validateProvider($provider); + + $providerUser = Socialite::driver($provider)->user(); + + // check if this account is already linked + $connected_account = ConnectedAccount::firstOrNew([ + 'provider' => $provider, + 'provider_id' => $providerUser->id + ], [ + 'token' => $providerUser->token, + 'secret' => $providerUser->tokenSecret, + 'refresh_token' => $providerUser->refreshToken, + 'expires_at' => $providerUser->expiresIn + ]); + + // already linked, let's go login + if ($connected_account->exists) { + Auth::login($connected_account->user); + + return redirect(route('dashboard')); + } + + // new user, let's create one + if (!$user = User::where('email', $providerUser->email)->first()) { + + $user = User::create([ + 'name' => $providerUser->name, + 'email' => $providerUser->email, + 'email_verified_at' => now() + ]); + + $connected_account->user_id = $user->id; + $connected_account->save(); + + Auth::login($user); + + return redirect(route('dashboard')); + } + + // email exists already, send verification link + $verification = ConnectedAccountVerification::updateOrCreate([ + 'email' => $providerUser->email, + 'provider' => $provider, + 'verified_at' => null + ], [ + 'provider_id' => $providerUser->id, + 'connected_account' => $connected_account + ]); + + $user->notify(new VerifyConnectedAccountNotification($verification->id)); + + return redirect(route('login')) + ->with('status', __('Account already exists. Check your email for a link to login.')); + } + + protected function validateProvider($provider): void + { + if (!in_array($provider, explode(',', config('services.enabled_login_providers')))) { + + throw new Exception('Please provide a valid social provider.'); + } + } +} diff --git a/app/Http/Controllers/VerifyConnectedAccountController.php b/app/Http/Controllers/VerifyConnectedAccountController.php new file mode 100644 index 0000000..adafcd8 --- /dev/null +++ b/app/Http/Controllers/VerifyConnectedAccountController.php @@ -0,0 +1,44 @@ +verified_at) { + + // mark request as verified + $verification->verified_at = now(); + $verification->save(); + + // mark user as verified + $user = User::where('email', $verification->email)->firstOrFail(); + $user->email_verified_at = now(); + $user->save(); + + // add connected account + $user->connectedAccounts()->create([ + ...$verification->connected_account, + ...[ + 'provider' => $verification->provider, + 'provider_id' => $verification->provider_id, + ] + ]); + + Auth::login($user); + } + + return redirect(route('dashboard')); + } +} diff --git a/app/Models/ConnectedAccount.php b/app/Models/ConnectedAccount.php new file mode 100644 index 0000000..372544f --- /dev/null +++ b/app/Models/ConnectedAccount.php @@ -0,0 +1,55 @@ + + */ + protected function casts(): array + { + return [ + 'created_at' => 'datetime', + 'expires_at' => 'datetime', + ]; + } + + /** + * Get user of the connected account. + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} \ No newline at end of file diff --git a/app/Models/ConnectedAccountVerification.php b/app/Models/ConnectedAccountVerification.php new file mode 100644 index 0000000..d904e9a --- /dev/null +++ b/app/Models/ConnectedAccountVerification.php @@ -0,0 +1,29 @@ + 'datetime', + 'updated_at' => 'datetime', + 'verified_at' => 'datetime', + 'connected_account' => 'json' + ]; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 39c688e..ee14950 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -2,8 +2,7 @@ namespace App\Models; -// use Illuminate\Contracts\Auth\MustVerifyEmail; - +use App\Traits\HasConnectedAccounts; use Laravel\Sanctum\HasApiTokens; use Laravel\Jetstream\HasProfilePhoto; use Illuminate\Notifications\Notifiable; @@ -13,8 +12,9 @@ use Illuminate\Database\Eloquent\Concerns\HasUuids; use Staudenmeir\EloquentHasManyDeep\HasRelationships; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; +use Illuminate\Contracts\Auth\MustVerifyEmail; -class User extends Authenticatable +class User extends Authenticatable implements MustVerifyEmail { use HasApiTokens; use HasFactory; @@ -23,6 +23,7 @@ class User extends Authenticatable use TwoFactorAuthenticatable; use HasUuids; use HasRelationships; + use HasConnectedAccounts; protected $fillable = [ 'name', diff --git a/app/Notifications/VerifyConnectedAccountNotification.php b/app/Notifications/VerifyConnectedAccountNotification.php new file mode 100644 index 0000000..fd2d201 --- /dev/null +++ b/app/Notifications/VerifyConnectedAccountNotification.php @@ -0,0 +1,59 @@ + + */ + public function via(object $notifiable): array + { + return ['mail']; + } + + /** + * Get the mail representation of the notification. + */ + public function toMail(object $notifiable): MailMessage + { + $verification = ConnectedAccountVerification::find($this->verification_id); + $provider = config("services.$verification->provider.name"); + + return (new MailMessage) + ->greeting('Welcome back!') + ->subject("Connect your $provider account with Investbrain") + ->line("You recently attempted to log into an existing Investbrain account using $provider. To safeguard your Investbrain account, please confirm this was you by pressing the 'Connect $provider' button below:") + ->action("Connect $provider", route('verify_connected_account', ['verification_id' => $this->verification_id])) + ->line('If you do not recognize this activity, we recommend [changing your password]('.route('profile.show').') as soon as possible. Otherwise, you can disregard this message.'); + } + + /** + * Get the array representation of the notification. + * + * @return array + */ + public function toArray(object $notifiable): array + { + return [ + // + ]; + } +} diff --git a/app/Traits/HasConnectedAccounts.php b/app/Traits/HasConnectedAccounts.php new file mode 100644 index 0000000..4926d4e --- /dev/null +++ b/app/Traits/HasConnectedAccounts.php @@ -0,0 +1,66 @@ +id == optional($connectedAccount)->user_id; + } + + /** + * Determine if the user has a specific account type. + */ + public function hasTokenFor(string $provider): bool + { + return $this->connectedAccounts->contains('provider', Str::lower($provider)); + } + + /** + * Attempt to retrieve the token for a given provider. + */ + public function getTokenFor(string $provider, mixed $default = null): mixed + { + if ($this->hasTokenFor($provider)) { + return $this->connectedAccounts + ->where('provider', Str::lower($provider)) + ->first() + ->token; + } + + return $default; + } + + /** + * Attempt to find a connected account that belongs to the user, + * for the given provider and ID. + */ + public function getConnectedAccountFor(string $provider, string $id): mixed + { + return $this->connectedAccounts + ->where('provider', $provider) + ->where('provider_id', $id) + ->first(); + } + + /** + * Get all the connected accounts belonging to the user. + */ + public function connectedAccounts(): HasMany + { + return $this->hasMany(ConnectedAccount::class); + } +} \ No newline at end of file diff --git a/composer.json b/composer.json index d469610..ca15387 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,7 @@ "laravel/framework": "^11.9", "laravel/jetstream": "^5.1", "laravel/sanctum": "^4.0", + "laravel/socialite": "^5.16", "laravel/tinker": "^2.9", "livewire/livewire": "^3.5", "livewire/volt": "^1.6", diff --git a/composer.lock b/composer.lock index b1b8b22..ecd0f51 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0f0957a51f038034d44aa347d5b97635", + "content-hash": "7fcc707e9612c64b40469d52efdb75e6", "packages": [ { "name": "bacon/bacon-qr-code", @@ -968,6 +968,69 @@ }, "time": "2024-09-13T01:29:18+00:00" }, + { + "name": "firebase/php-jwt", + "version": "v6.10.1", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "500501c2ce893c824c801da135d02661199f60c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/500501c2ce893c824c801da135d02661199f60c5", + "reference": "500501c2ce893c824c801da135d02661199f60c5", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^2.0||^3.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v6.10.1" + }, + "time": "2024-05-18T18:05:11+00:00" + }, { "name": "fruitcake/php-cors", "version": "v1.3.0", @@ -2274,6 +2337,78 @@ }, "time": "2024-09-23T13:33:08+00:00" }, + { + "name": "laravel/socialite", + "version": "v5.16.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/socialite.git", + "reference": "40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/socialite/zipball/40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf", + "reference": "40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf", + "shasum": "" + }, + "require": { + "ext-json": "*", + "firebase/php-jwt": "^6.4", + "guzzlehttp/guzzle": "^6.0|^7.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "league/oauth1-client": "^1.10.1", + "php": "^7.2|^8.0", + "phpseclib/phpseclib": "^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.0|^9.3|^10.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Socialite\\SocialiteServiceProvider" + ], + "aliases": { + "Socialite": "Laravel\\Socialite\\Facades\\Socialite" + } + } + }, + "autoload": { + "psr-4": { + "Laravel\\Socialite\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel wrapper around OAuth 1 & OAuth 2 libraries.", + "homepage": "https://laravel.com", + "keywords": [ + "laravel", + "oauth" + ], + "support": { + "issues": "https://github.com/laravel/socialite/issues", + "source": "https://github.com/laravel/socialite" + }, + "time": "2024-09-03T09:46:57+00:00" + }, { "name": "laravel/tinker", "version": "v2.10.0", @@ -2716,6 +2851,82 @@ ], "time": "2024-09-21T08:32:55+00:00" }, + { + "name": "league/oauth1-client", + "version": "v1.10.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth1-client.git", + "reference": "d6365b901b5c287dd41f143033315e2f777e1167" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/d6365b901b5c287dd41f143033315e2f777e1167", + "reference": "d6365b901b5c287dd41f143033315e2f777e1167", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-openssl": "*", + "guzzlehttp/guzzle": "^6.0|^7.0", + "guzzlehttp/psr7": "^1.7|^2.0", + "php": ">=7.1||>=8.0" + }, + "require-dev": { + "ext-simplexml": "*", + "friendsofphp/php-cs-fixer": "^2.17", + "mockery/mockery": "^1.3.3", + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5||9.5" + }, + "suggest": { + "ext-simplexml": "For decoding XML-based responses." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev", + "dev-develop": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth1\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Corlett", + "email": "bencorlett@me.com", + "homepage": "http://www.webcomm.com.au", + "role": "Developer" + } + ], + "description": "OAuth 1.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "bitbucket", + "identity", + "idp", + "oauth", + "oauth1", + "single sign on", + "trello", + "tumblr", + "twitter" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth1-client/issues", + "source": "https://github.com/thephpleague/oauth1-client/tree/v1.10.1" + }, + "time": "2022-04-15T14:02:14+00:00" + }, { "name": "livewire/livewire", "version": "v3.5.11", @@ -3760,6 +3971,56 @@ }, "time": "2024-05-08T12:36:18+00:00" }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, { "name": "phpoffice/phpspreadsheet", "version": "1.29.2", @@ -3940,6 +4201,116 @@ ], "time": "2024-07-20T21:41:07+00:00" }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.42", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "db92f1b1987b12b13f248fe76c3a52cadb67bb98" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/db92f1b1987b12b13f248fe76c3a52cadb67bb98", + "reference": "db92f1b1987b12b13f248fe76c3a52cadb67bb98", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2|^3", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.42" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2024-09-16T03:06:04+00:00" + }, { "name": "pragmarx/google2fa", "version": "v8.0.3", diff --git a/config/fortify.php b/config/fortify.php index 0551d1d..a324748 100644 --- a/config/fortify.php +++ b/config/fortify.php @@ -144,7 +144,7 @@ return [ */ 'features' => [ - Features::registration(), + env('REGISTRATION_ENABLED', true) ? Features::registration() : null, Features::resetPasswords(), Features::emailVerification(), Features::updateProfileInformation(), diff --git a/config/services.php b/config/services.php index 27a3617..88669fc 100644 --- a/config/services.php +++ b/config/services.php @@ -35,4 +35,38 @@ return [ ], ], + 'github' => [ + 'client_id' => env('GITHUB_CLIENT_ID'), + 'client_secret' => env('GITHUB_CLIENT_SECRET'), + 'redirect' => '/auth/github/callback', + 'logo' => 'github-icon', + 'color' => '#393939', + 'name' => 'GitHub' + ], + + 'google' => [ + 'client_id' => env('GOOGLE_CLIENT_ID'), + 'client_secret' => env('GOOGLE_CLIENT_SECRET'), + 'redirect' => '/auth/google/callback', + 'color' => '#4285F4', + 'name' => 'Google' + ], + + 'facebook' => [ + 'client_id' => env('FACEBOOK_CLIENT_ID'), + 'client_secret' => env('FACEBOOK_CLIENT_SECRET'), + 'redirect' => '/auth/facebook/callback', + 'color' => '#0165E1', + 'name' => 'Facebook' + ], + + 'linkedin-openid' => [ + 'client_id' => env('LINKEDIN_CLIENT_ID'), + 'client_secret' => env('LINKEDIN_CLIENT_SECRET'), + 'redirect' => '/auth/linkedin-openid/callback', + 'color' => '#0a66c2', + 'name' => 'Linkedin' + ], + + 'enabled_login_providers' => env('ENABLED_LOGIN_PROVIDERS', []) ]; diff --git a/database/migrations/2024_10_19_155635_create_connected_accounts_table.php b/database/migrations/2024_10_19_155635_create_connected_accounts_table.php new file mode 100644 index 0000000..c9c2033 --- /dev/null +++ b/database/migrations/2024_10_19_155635_create_connected_accounts_table.php @@ -0,0 +1,55 @@ +string('password')->nullable()->change(); + }); + + Schema::create('connected_accounts', function (Blueprint $table) { + $table->uuid('id')->primary(); + $table->foreignIdFor(User::class, 'user_id')->constrained()->onDelete('cascade'); + $table->string('provider'); + $table->string('provider_id'); + $table->string('token', 1000); + $table->string('secret')->nullable(); // OAuth1 + $table->string('refresh_token', 1000)->nullable(); // OAuth2 + $table->dateTime('expires_at')->nullable(); // OAuth2 + $table->timestamps(); + + $table->index(['user_id', 'id']); + $table->index(['provider', 'provider_id']); + }); + + Schema::create('connected_account_verifications', function (Blueprint $table) { + $table->uuid('id')->primary(); + $table->string('email'); + $table->string('provider'); + $table->string('provider_id'); + $table->json('connected_account'); + $table->timestamps(); + $table->timestamp('verified_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + + Schema::dropIfExists('connected_account_verifications'); + + Schema::dropIfExists('connected_accounts'); + } +}; \ No newline at end of file diff --git a/lang/en.json b/lang/en.json index a42e1a6..4288fef 100644 --- a/lang/en.json +++ b/lang/en.json @@ -51,6 +51,7 @@ "Token Name": "Token Name", "Permissions": "Permissions", "Profile Information": "Profile Information", + "Account already exists. Check your email for a link to login.": "Account already exists. Check your email for a link to login.", "Update your account\\'s profile information and email address.": "Update your account\\'s profile information and email address.", "Photo": "Photo", "Select A New Photo": "Select A New Photo", @@ -80,7 +81,8 @@ "I agree to the :terms_of_service and :privacy_policy": "I agree to the :terms_of_service and :privacy_policy", "Terms of Service": "Terms of Service", "Privacy Policy": "Privacy Notice", - "Need to register?": "Need to register?", + "Sign up with email": "Sign up with email", + "Login with": "Login with", "Already registered?": "Already registered?", "Reset Password": "Reset Password", "Please confirm access to your account by entering the authentication code provided by your authenticator application.": "Please confirm access to your account by entering the authentication code provided by your authenticator application.", diff --git a/lang/es.json b/lang/es.json index 375bbeb..42e8d6a 100644 --- a/lang/es.json +++ b/lang/es.json @@ -51,6 +51,7 @@ "Token Name": "Nombre del Token", "Permissions": "Permisos", "Profile Information": "Información del Perfil", + "Account already exists. Check your email for a link to login.": "La cuenta ya existe. Revisa tu correo electrónico para un enlace de inicio de sesión.", "Update your account's profile information and email address.": "Actualiza la información de perfil y la dirección de correo electrónico de tu cuenta.", "Photo": "Foto", "Select A New Photo": "Seleccionar una Nueva Foto", @@ -80,7 +81,8 @@ "I agree to the :terms_of_service and :privacy_policy": "Acepto los :terms_of_service y la :privacy_policy", "Terms of Service": "Términos de Servicio", "Privacy Policy": "Aviso de Privacidad", - "Need to register?": "¿Necesitas registrarte?", + "Sign up with email": "Regístrate con correo electrónico", + "Login with": "Iniciar sesión con", "Already registered?": "¿Ya estás registrado?", "Reset Password": "Restablecer Contraseña", "Please confirm access to your account by entering the authentication code provided by your authenticator application.": "Por favor, confirma el acceso a tu cuenta ingresando el código de autenticación proporcionado por tu aplicación de autenticación.", diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index 1f1c3ee..c33ccb1 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -47,15 +47,34 @@ - + @if (\Laravel\Fortify\Features::enabled('registration')) -
- - - {{ __('Need to register?') }} - + -
+
+ + @foreach(explode(',', config('services.enabled_login_providers')) as $provider) + + @include("components.$provider-icon") + + {{ __('Login with') }} {{ config("services.$provider.name") }} + + @endforeach + + + {{ __('Sign up with email') }} + + +
+ @endif diff --git a/resources/views/components/facebook-icon.blade.php b/resources/views/components/facebook-icon.blade.php new file mode 100644 index 0000000..f400c6e --- /dev/null +++ b/resources/views/components/facebook-icon.blade.php @@ -0,0 +1,6 @@ + + diff --git a/resources/views/components/github-icon.blade.php b/resources/views/components/github-icon.blade.php index 50cbc10..1b47773 100644 --- a/resources/views/components/github-icon.blade.php +++ b/resources/views/components/github-icon.blade.php @@ -1 +1,3 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/resources/views/components/google-icon.blade.php b/resources/views/components/google-icon.blade.php new file mode 100644 index 0000000..6fe66f3 --- /dev/null +++ b/resources/views/components/google-icon.blade.php @@ -0,0 +1,6 @@ + + diff --git a/resources/views/components/linkedin-openid-icon.blade.php b/resources/views/components/linkedin-openid-icon.blade.php new file mode 100644 index 0000000..f904130 --- /dev/null +++ b/resources/views/components/linkedin-openid-icon.blade.php @@ -0,0 +1,3 @@ + diff --git a/routes/web.php b/routes/web.php index 3637157..49e8c49 100644 --- a/routes/web.php +++ b/routes/web.php @@ -4,7 +4,9 @@ use Illuminate\Support\Facades\Route; use App\Http\Controllers\HoldingController; use App\Http\Controllers\DashboardController; use App\Http\Controllers\PortfolioController; +use App\Http\Controllers\SocialLoginController; use App\Http\Controllers\TransactionController; +use App\Http\Controllers\VerifyConnectedAccountController; use Laravel\Jetstream\Http\Controllers\Livewire\PrivacyPolicyController; use Laravel\Jetstream\Http\Controllers\Livewire\TermsOfServiceController; @@ -21,10 +23,10 @@ Route::get('/test', function () { // }); -Route::middleware(['auth:sanctum', config('jetstream.auth_session'), 'verified'])->group(function () { +Route::middleware(['auth:sanctum', config('jetstream.auth_session')])->group(function () { Route::get('/dashboard', [DashboardController::class, 'show'])->name('dashboard'); - Route::view('/import-export', 'import-export')->name('import-export'); + Route::view('/import-export', 'import-export')->name('import-export')->middleware('verified'); Route::get('/portfolio/create', [PortfolioController::class, 'create'])->name('portfolio.create'); Route::get('/portfolio/{portfolio}', [PortfolioController::class, 'show'])->name('portfolio.show'); @@ -37,3 +39,9 @@ Route::middleware(['auth:sanctum', config('jetstream.auth_session'), 'verified'] // overwrites jetstream routes Route::get('/terms', [TermsOfServiceController::class, 'show'])->name('terms.show'); Route::get('/privacy', [PrivacyPolicyController::class, 'show'])->name('policy.show'); + +// social login routes +Route::get('auth/verify/{verification_id}', VerifyConnectedAccountController::class)->name('verify_connected_account'); + +Route::get('auth/{provider}', [SocialLoginController::class, 'redirectToProvider'])->name('oauth.redirect'); +Route::get('auth/{provider}/callback', [SocialLoginController::class, 'handleProviderCallback']);