diff --git a/.env.example b/.env.example index 35db1dd..8f660c7 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,14 @@ LOG_STACK=single LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug +GOOGLE_CLIENT_ID=test +GOOGLE_CLIENT_SECRET=test +GOOGLE_REDIRECT_URL=http://localhost/auth/google/callback +GITHUB_CLIENT_ID=test +GITHUB_CLIENT_SECRET=test +GITHUB_REDIRECT_URL=http://localhost/auth/github/callback + + DB_CONNECTION=sqlite # DB_HOST=127.0.0.1 # DB_PORT=3306 diff --git a/.gitignore b/.gitignore index 83ec5b7..05243f4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ /.nova /.phpunit.cache /.zed +/.pnpm-store /auth.json /node_modules /public/build @@ -26,3 +27,4 @@ components.d.ts resources/js/routes resources/js/actions resources/js/wayfinder + diff --git a/app/Http/Controllers/SocialProviderController.php b/app/Http/Controllers/SocialProviderController.php new file mode 100644 index 0000000..8a7ab58 --- /dev/null +++ b/app/Http/Controllers/SocialProviderController.php @@ -0,0 +1,116 @@ +redirect(); + } + + public function handleProviderCallback($provider, Request $request) + { + $socialUser = Socialite::driver($provider)->user(); + + try { + // If user is already authenticated, link the social account + if (Auth::check()) { + return $this->linkSocialAccount(Auth::user(), $provider, $socialUser); + } + + // Check if user already exists with this social provider + $existingSocialUser = User::where([ + 'provider' => $provider, + 'provider_id' => $socialUser->getId() + ])->first(); + + if ($existingSocialUser) { + // User exists with this social provider, log them in + $existingSocialUser->provider_token = $socialUser->token; + $existingSocialUser->save(); + + Auth::login($existingSocialUser); + return redirect()->route('dashboard'); + } + + // Check if email is already used by another account + $existingUser = User::where('email', $socialUser->getEmail())->first(); + + if ($existingUser) { + // Email exists but with different provider or username/password + if ($existingUser->provider && $existingUser->provider !== $provider) { + return redirect()->route('login')->withErrors([ + 'email' => 'Cet email est déjà associé à un compte ' . $existingUser->provider + ]); + } else if (!$existingUser->provider) { + return redirect()->route('login')->withErrors([ + 'email' => 'Cet email utilise une autre méthode de connexion (nom d\'utilisateur/mot de passe)' + ]); + } + } + + // Create new user account + $user = User::create([ + 'name' => $socialUser->getName(), + 'email' => $socialUser->getEmail(), + 'provider' => $provider, + 'provider_id' => $socialUser->getId(), + 'provider_token' => $socialUser->token, + 'email_verified_at' => now(), + 'profile_photo_path' => $socialUser->getAvatar(), + ]); + + Auth::login($user); + return redirect()->route('dashboard'); + + } catch (\Exception $e) { + return redirect()->route('login')->withErrors([ + 'email' => 'Une erreur est survenue lors de la connexion' + ]); + } + } + + /** + * Link a social account to an existing authenticated user + */ + private function linkSocialAccount(User $user, string $provider, $socialUser) + { + // Check if this social account is already linked to another user + $existingSocialUser = User::where([ + 'provider' => $provider, + 'provider_id' => $socialUser->getId() + ])->first(); + + if ($existingSocialUser && $existingSocialUser->id !== $user->id) { + return redirect()->route('user.profile.index')->withErrors([ + 'social' => 'Ce compte ' . $provider . ' est déjà lié à un autre utilisateur' + ]); + } + + // Check if user already has this provider linked + if ($user->provider === $provider) { + return redirect()->route('user.profile.index')->withErrors([ + 'social' => 'Ce compte ' . $provider . ' est déjà lié à votre profil' + ]); + } + + // Update user with social provider information + $user->update([ + 'provider' => $provider, + 'provider_id' => $socialUser->getId(), + 'provider_token' => $socialUser->token, + ]); + + return redirect()->route('user.profile.index')->with('success', + 'Compte ' . $provider . ' lié avec succès à votre profil' + ); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 8d3d0a0..0372635 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -23,6 +23,11 @@ class User extends Authenticatable implements MustVerifyEmail 'name', 'email', 'password', + 'provider', + 'provider_id', + 'provider_token', + 'email_verified_at', + 'profile_photo_path', ]; /** diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..1bdde98 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use Illuminate\Support\Facades\Route; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -19,6 +20,15 @@ public function register(): void */ public function boot(): void { - // + $this->loadAuthRoutes(); + } + + /** + * Load the auth routes. + */ + private function loadAuthRoutes(): void + { + Route::middleware('web') + ->group(base_path('routes/auth.php')); } } diff --git a/artisan b/artisan old mode 100755 new mode 100644 diff --git a/composer.json b/composer.json index 1a8ae63..25db0ee 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "inertiajs/inertia-laravel": "^2.0", "laravel/fortify": "^1.28", "laravel/framework": "^12.0", + "laravel/socialite": "^5.23", "laravel/tinker": "^2.10.1", "laravel/wayfinder": "^0.1.6" }, diff --git a/composer.lock b/composer.lock index 695c83d..f6e7802 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": "c7d01a894b29be9622b874f2d51920a8", + "content-hash": "5ab8c68bd17cbd549fd38d58c15db927", "packages": [ { "name": "bacon/bacon-qr-code", @@ -613,6 +613,69 @@ ], "time": "2025-03-06T22:45:56+00:00" }, + { + "name": "firebase/php-jwt", + "version": "v6.11.1", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66", + "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.11.1" + }, + "time": "2025-04-09T20:32:01+00:00" + }, { "name": "fruitcake/php-cors", "version": "v1.3.0", @@ -1628,6 +1691,78 @@ }, "time": "2025-03-19T13:51:03+00:00" }, + { + "name": "laravel/socialite", + "version": "v5.23.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/socialite.git", + "reference": "e9e0fc83b9d8d71c8385a5da20e5b95ca6234cf5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/socialite/zipball/e9e0fc83b9d8d71c8385a5da20e5b95ca6234cf5", + "reference": "e9e0fc83b9d8d71c8385a5da20e5b95ca6234cf5", + "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|^12.0", + "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "league/oauth1-client": "^1.11", + "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|^10.0", + "phpstan/phpstan": "^1.12.23", + "phpunit/phpunit": "^8.0|^9.3|^10.4|^11.5" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Socialite": "Laravel\\Socialite\\Facades\\Socialite" + }, + "providers": [ + "Laravel\\Socialite\\SocialiteServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "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": "2025-07-23T14:16:08+00:00" + }, { "name": "laravel/tinker", "version": "v2.10.1", @@ -2133,6 +2268,82 @@ ], "time": "2024-09-21T08:32:55+00:00" }, + { + "name": "league/oauth1-client", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth1-client.git", + "reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/f9c94b088837eb1aae1ad7c4f23eb65cc6993055", + "reference": "f9c94b088837eb1aae1ad7c4f23eb65cc6993055", + "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.11.0" + }, + "time": "2024-12-10T19:59:05+00:00" + }, { "name": "league/uri", "version": "7.5.1", @@ -2878,6 +3089,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": "phpoption/phpoption", "version": "1.9.3", @@ -2953,6 +3214,116 @@ ], "time": "2024-07-20T21:41:07+00:00" }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.46", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6", + "reference": "56483a7de62a6c2a6635e42e93b8a9e25d4f0ec6", + "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.46" + }, + "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": "2025-06-26T16:29:55+00:00" + }, { "name": "pragmarx/google2fa", "version": "v8.0.3", diff --git a/config/cors.php b/config/cors.php new file mode 100644 index 0000000..8a39e6d --- /dev/null +++ b/config/cors.php @@ -0,0 +1,34 @@ + ['api/*', 'sanctum/csrf-cookie'], + + 'allowed_methods' => ['*'], + + 'allowed_origins' => ['*'], + + 'allowed_origins_patterns' => [], + + 'allowed_headers' => ['*'], + + 'exposed_headers' => [], + + 'max_age' => 0, + + 'supports_credentials' => false, + +]; diff --git a/config/services.php b/config/services.php index 6182e4b..bea264f 100644 --- a/config/services.php +++ b/config/services.php @@ -34,5 +34,15 @@ 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), ], ], + 'google' => [ + 'client_id' => env('GOOGLE_CLIENT_ID'), + 'client_secret' => env('GOOGLE_CLIENT_SECRET'), + 'redirect' => env('GOOGLE_REDIRECT_URI'), + ], + 'github' => [ + 'client_id' => env('GITHUB_CLIENT_ID'), + 'client_secret' => env('GITHUB_CLIENT_SECRET'), + 'redirect' => env('GITHUB_REDIRECT_URI'), + ], ]; diff --git a/database/migrations/2025_08_17_164746_add_social_provider_column.php b/database/migrations/2025_08_17_164746_add_social_provider_column.php new file mode 100644 index 0000000..8c6b023 --- /dev/null +++ b/database/migrations/2025_08_17_164746_add_social_provider_column.php @@ -0,0 +1,34 @@ +string('provider')->nullable(); + $table->string('provider_id')->nullable(); + $table->string('provider_token')->nullable(); + $table->string('password')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('provider'); + $table->dropColumn('provider_id'); + $table->dropColumn('provider_token'); + $table->string('password')->change(); + }); + } +}; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..555b243 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,27 @@ +services: + laravel.test: + build: + context: './vendor/laravel/sail/runtimes/8.4' + dockerfile: Dockerfile + args: + WWWGROUP: '${WWWGROUP}' + image: 'sail-8.4/app' + extra_hosts: + - 'host.docker.internal:host-gateway' + ports: + - '${APP_PORT:-80}:80' + - '${VITE_PORT:-5173}:${VITE_PORT:-5173}' + environment: + WWWUSER: '${WWWUSER}' + LARAVEL_SAIL: 1 + XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}' + XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}' + IGNITION_LOCAL_SITES_PATH: '${PWD}' + volumes: + - '.:/var/www/html' + networks: + - sail + depends_on: { } +networks: + sail: + driver: bridge diff --git a/resources/js/pages/auth/login.vue b/resources/js/pages/auth/login.vue index 79eb8f9..45e5ea4 100644 --- a/resources/js/pages/auth/login.vue +++ b/resources/js/pages/auth/login.vue @@ -1,8 +1,9 @@ + + diff --git a/routes/auth.php b/routes/auth.php new file mode 100644 index 0000000..9ca3ebd --- /dev/null +++ b/routes/auth.php @@ -0,0 +1,12 @@ +group(function () { + Route::get('auth/{provider}/redirect', [SocialProviderController::class, 'redirectToProvider'])->name('auth.provider.redirect'); +}); + +// Allow both guest and authenticated users to access the callback +Route::get('auth/{provider}/callback', [SocialProviderController::class, 'handleProviderCallback'])->name('auth.provider.callback'); +