diff --git a/.ai/laravel-socialite/5/core.blade.php b/.ai/laravel-socialite/5/core.blade.php new file mode 100644 index 00000000..40b5e406 --- /dev/null +++ b/.ai/laravel-socialite/5/core.blade.php @@ -0,0 +1,42 @@ +# Laravel Socialite v5 Specific Guidelines + +## Version 5 Enhanced Features + +### Improved Error Handling +```php +use Laravel\Socialite\Two\InvalidStateException; + +try { + $user = Socialite::driver('github')->user(); +} catch (InvalidStateException $e) { + Log::warning('Invalid OAuth state', ['error' => $e->getMessage()]); + return redirect('/login')->with('error', 'Authentication session expired. Please try again.'); +} +``` + +### Type Hints Best Practice +```php +use Laravel\Socialite\Contracts\User as SocialiteUser; + +public function handleCallback(string $provider): RedirectResponse +{ + $socialiteUser = Socialite::driver($provider)->user(); + + $user = $this->findOrCreateUser($socialiteUser, $provider); + Auth::login($user); + + return redirect()->intended('/dashboard'); +} + +private function findOrCreateUser(SocialiteUser $socialiteUser, string $provider): User +{ + return User::updateOrCreate([ + 'provider_name' => $provider, + 'provider_id' => $socialiteUser->getId(), + ], [ + 'name' => $socialiteUser->getName(), + 'email' => $socialiteUser->getEmail(), + 'avatar' => $socialiteUser->getAvatar(), + ]); +} +``` \ No newline at end of file diff --git a/.ai/laravel-socialite/core.blade.php b/.ai/laravel-socialite/core.blade.php new file mode 100644 index 00000000..2958352f --- /dev/null +++ b/.ai/laravel-socialite/core.blade.php @@ -0,0 +1,302 @@ +# Laravel Socialite Guidelines + +Laravel Socialite provides a simple, convenient way to authenticate with OAuth providers like Facebook, Twitter, Google, LinkedIn, GitHub, GitLab, and Bitbucket. + +## Installation + +```bash +composer require laravel/socialite +``` + +## Configuration + +1. Add OAuth provider credentials to your `.env` file: + +```env +GOOGLE_CLIENT_ID=your-google-client-id +GOOGLE_CLIENT_SECRET=your-google-client-secret +GOOGLE_REDIRECT_URL=http://your-app.com/auth/callback/google + +GITHUB_CLIENT_ID=your-github-client-id +GITHUB_CLIENT_SECRET=your-github-client-secret +GITHUB_REDIRECT_URL=http://your-app.com/auth/callback/github +``` + +2. Add the provider configuration to `config/services.php`: + +```php +'github' => [ + 'client_id' => env('GITHUB_CLIENT_ID'), + 'client_secret' => env('GITHUB_CLIENT_SECRET'), + 'redirect' => env('GITHUB_REDIRECT_URL'), +], + +'google' => [ + 'client_id' => env('GOOGLE_CLIENT_ID'), + 'client_secret' => env('GOOGLE_CLIENT_SECRET'), + 'redirect' => env('GOOGLE_REDIRECT_URL'), +], +``` + +## Usage Patterns + +### Basic Authentication Flow + +1. **Redirect to Provider**: +```php +use Laravel\Socialite\Facades\Socialite; + +Route::get('/auth/redirect/{provider}', function (string $provider) { + return Socialite::driver($provider)->redirect(); +}); +``` + +2. **Handle Provider Callback**: +```php +Route::get('/auth/callback/{provider}', function (string $provider) { + $user = Socialite::driver($provider)->user(); + + // Find or create user + $authUser = User::updateOrCreate([ + 'email' => $user->getEmail(), + ], [ + 'name' => $user->getName(), + 'provider_id' => $user->getId(), + 'provider_name' => $provider, + 'avatar' => $user->getAvatar(), + ]); + + Auth::login($authUser); + + return redirect('/dashboard'); +}); +``` + +### User Data Access + +```php +$user = Socialite::driver('github')->user(); + +// Basic info +$user->getId(); +$user->getNickname(); +$user->getName(); +$user->getEmail(); +$user->getAvatar(); + +// Provider-specific data +$user->user; // Raw user object from provider +$user->token; // OAuth token +$user->refreshToken; // OAuth refresh token (if available) +$user->expiresIn; // Token expiration time +``` + +### Stateless Authentication + +For API usage, use stateless authentication: + +```php +$user = Socialite::driver('github')->stateless()->user(); +``` + +### Scopes and Parameters + +Request specific scopes or parameters: + +```php +return Socialite::driver('github') + ->scopes(['read:user', 'public_repo']) + ->with(['allow_signup' => 'false']) + ->redirect(); +``` + +## Best Practices + +### Database Schema + +Create a flexible user schema that supports multiple providers: + +```php +Schema::create('users', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password')->nullable(); // Nullable for social-only users + $table->string('provider_name')->nullable(); + $table->string('provider_id')->nullable(); + $table->string('avatar')->nullable(); + $table->rememberToken(); + $table->timestamps(); + + $table->unique(['provider_name', 'provider_id']); +}); +``` + +### User Model + +```php +class User extends Authenticatable +{ + protected $fillable = [ + 'name', + 'email', + 'password', + 'provider_name', + 'provider_id', + 'avatar', + ]; + + protected $hidden = [ + 'password', + 'remember_token', + ]; + + protected $casts = [ + 'email_verified_at' => 'datetime', + ]; + + public function isSocialUser(): bool + { + return !is_null($this->provider_name); + } +} +``` + +### Security Considerations + +1. **Validate Provider**: +```php +Route::get('/auth/redirect/{provider}', function (string $provider) { + if (!in_array($provider, ['github', 'google', 'twitter'])) { + abort(404); + } + + return Socialite::driver($provider)->redirect(); +}); +``` + +2. **Handle Existing Users**: +```php +Route::get('/auth/callback/{provider}', function (string $provider) { + $socialUser = Socialite::driver($provider)->user(); + + // Check if user exists with this email but different provider + $existingUser = User::where('email', $socialUser->getEmail())->first(); + + if ($existingUser && $existingUser->provider_name !== $provider) { + return redirect('/login')->with('error', 'This email is already registered with a different provider.'); + } + + // Your user creation logic here +}); +``` + +3. **Rate Limiting**: +```php +Route::get('/auth/redirect/{provider}', function (string $provider) { + return Socialite::driver($provider)->redirect(); +})->middleware('throttle:6,1'); +``` + +### Error Handling + +```php +use Laravel\Socialite\Facades\Socialite; +use Laravel\Socialite\Two\InvalidStateException; + +Route::get('/auth/callback/{provider}', function (string $provider) { + try { + $user = Socialite::driver($provider)->user(); + + // Handle user logic + + } catch (InvalidStateException $e) { + return redirect('/login')->with('error', 'Authentication failed. Please try again.'); + } catch (Exception $e) { + Log::error('Social login error: ' . $e->getMessage()); + return redirect('/login')->with('error', 'Something went wrong. Please try again.'); + } +}); +``` + +## Custom Providers + +For custom OAuth providers, extend the abstract provider: + +```php +use Laravel\Socialite\Two\AbstractProvider; +use Laravel\Socialite\Two\User; + +class CustomProvider extends AbstractProvider +{ + protected function getAuthUrl($state) + { + return $this->buildAuthUrlFromBase('https://example.com/oauth/authorize', $state); + } + + protected function getTokenUrl() + { + return 'https://example.com/oauth/token'; + } + + protected function getUserByToken($token) + { + $response = $this->getHttpClient()->get('https://example.com/api/user', [ + 'headers' => [ + 'Authorization' => 'Bearer ' . $token, + ], + ]); + + return json_decode($response->getBody(), true); + } + + protected function mapUserToObject(array $user) + { + return (new User)->setRaw($user)->map([ + 'id' => $user['id'], + 'nickname' => $user['username'], + 'name' => $user['name'], + 'email' => $user['email'], + 'avatar' => $user['avatar_url'], + ]); + } +} +``` + +## Testing + +### Feature Tests + +```php +use Laravel\Socialite\Facades\Socialite; +use Laravel\Socialite\Two\User; + +test('user can login with github', function () { + $githubUser = Mockery::mock(User::class); + $githubUser->shouldReceive('getId')->andReturn('123456'); + $githubUser->shouldReceive('getName')->andReturn('John Doe'); + $githubUser->shouldReceive('getEmail')->andReturn('john@example.com'); + $githubUser->shouldReceive('getAvatar')->andReturn('https://example.com/avatar.jpg'); + + Socialite::shouldReceive('driver->user')->andReturn($githubUser); + + $response = $this->get('/auth/callback/github'); + + $response->assertRedirect('/dashboard'); + $this->assertDatabaseHas('users', [ + 'email' => 'john@example.com', + 'provider_name' => 'github', + ]); +}); +``` + +## Common Pitfalls to Avoid + +1. **Don't store sensitive tokens in logs** +2. **Always validate the provider parameter** +3. **Handle email conflicts gracefully** +4. **Use HTTPS for redirect URLs in production** +5. **Keep provider credentials secure and out of version control** +6. **Handle cases where users deny permission** +7. **Implement proper session management for the OAuth flow** \ No newline at end of file diff --git a/README.md b/README.md index 9d560de4..1f1bde21 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ Laravel Boost includes AI guidelines for the following packages and frameworks. |---------|-------------------| | Core & Boost | core | | Laravel Framework | core, 10.x, 11.x, 12.x | +| Laravel Socialite | core, 5.x | | Livewire | core, 2.x, 3.x | | Filament | core, 4.x | | Flux UI | core, free, pro | diff --git a/src/Install/GuidelineComposer.php b/src/Install/GuidelineComposer.php index cf1b0a67..490b9f30 100644 --- a/src/Install/GuidelineComposer.php +++ b/src/Install/GuidelineComposer.php @@ -126,6 +126,12 @@ protected function find(): Collection ); } + // Add Laravel Socialite if detected + if ($this->isSocialiteInstalled()) { + $guidelines->put('laravel-socialite/core', $this->guideline('laravel-socialite/core')); + $guidelines->put('laravel-socialite/v5', $this->guideline('laravel-socialite/5/core')); + } + if ($this->config->enforceTests) { $guidelines->put('tests', $this->guideline('enforce-tests')); } @@ -233,4 +239,37 @@ private function processBoostSnippets(string $content): string return $placeholder; }, $content); } + + /** + * Check if Laravel Socialite is installed by looking at composer.lock. + */ + private function isSocialiteInstalled(): bool + { + $composerLockPath = base_path('composer.lock'); + + if (! file_exists($composerLockPath)) { + return false; + } + + $contents = file_get_contents($composerLockPath); + if ($contents === false) { + return false; + } + + $data = json_decode($contents, true); + if (json_last_error() !== JSON_ERROR_NONE || ! is_array($data)) { + return false; + } + + $packages = array_merge($data['packages'] ?? [], $data['packages-dev'] ?? []); + + foreach ($packages as $package) { + if (($package['name'] ?? '') === 'laravel/socialite') { + return true; + } + } + + return false; + } + } diff --git a/tests/Feature/Install/GuidelineComposerTest.php b/tests/Feature/Install/GuidelineComposerTest.php index f1e6846d..1d64596d 100644 --- a/tests/Feature/Install/GuidelineComposerTest.php +++ b/tests/Feature/Install/GuidelineComposerTest.php @@ -251,3 +251,88 @@ ->toContain('laravel/v11') ->toContain('pest/core'); }); + +test('includes Socialite guidelines when package is detected', function () { + $packages = new PackageCollection([ + new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), + ]); + + $this->roster->shouldReceive('packages')->andReturn($packages); + + + $composerLockContent = json_encode([ + 'packages' => [ + [ + 'name' => 'laravel/socialite', + 'version' => '5.14.0', + ], + ], + ]); + + $tempComposerLock = base_path('composer.lock.test'); + file_put_contents($tempComposerLock, $composerLockContent); + + $originalBasePath = base_path('composer.lock'); + if (file_exists($originalBasePath)) { + rename($originalBasePath, $originalBasePath.'.backup'); + } + file_put_contents($originalBasePath, $composerLockContent); + + $guidelines = $this->composer->compose(); + $used = $this->composer->used(); + + expect($guidelines) + ->toContain('=== laravel-socialite/core rules ===') + ->toContain('=== laravel-socialite/v5 rules ==='); + + expect($used) + ->toContain('laravel-socialite/core') + ->toContain('laravel-socialite/v5'); + + unlink($tempComposerLock); + if (file_exists($originalBasePath.'.backup')) { + rename($originalBasePath.'.backup', $originalBasePath); + } else { + unlink($originalBasePath); + } +}); + +test('excludes Socialite guidelines when package is not installed', function () { + $packages = new PackageCollection([ + new Package(Packages::LARAVEL, 'laravel/framework', '11.0.0'), + ]); + + $this->roster->shouldReceive('packages')->andReturn($packages); + + $composerLockContent = json_encode([ + 'packages' => [ + [ + 'name' => 'laravel/framework', + 'version' => '11.0.0', + ], + ], + ]); + + $originalBasePath = base_path('composer.lock'); + if (file_exists($originalBasePath)) { + rename($originalBasePath, $originalBasePath.'.backup'); + } + file_put_contents($originalBasePath, $composerLockContent); + + $guidelines = $this->composer->compose(); + $used = $this->composer->used(); + + expect($guidelines) + ->not->toContain('=== laravel-socialite/core rules ===') + ->not->toContain('=== laravel-socialite/v5 rules ==='); + + expect($used) + ->not->toContain('laravel-socialite/core') + ->not->toContain('laravel-socialite/v5'); + + if (file_exists($originalBasePath.'.backup')) { + rename($originalBasePath.'.backup', $originalBasePath); + } else { + unlink($originalBasePath); + } +});