From 652181063d841690f7cd8791e94945e112de98c3 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Wed, 31 Jul 2024 01:55:45 +0330 Subject: [PATCH 01/20] support laravel passport --- composer.json | 1 + routes/inertia.php | 28 +- routes/livewire.php | 6 + src/Console/InstallCommand.php | 40 +- src/Features.php | 20 + src/HasTeams.php | 6 +- .../Inertia/OAuthAppController.php | 109 +++++ .../Inertia/OAuthConnectionController.php | 29 ++ .../Inertia/PassportApiTokenController.php | 73 ++++ .../Livewire/OAuthAppController.php | 18 + src/Http/Livewire/OAuthAppManager.php | 280 ++++++++++++ src/Http/Livewire/PassportApiTokenManager.php | 152 +++++++ src/Http/Middleware/ShareInertiaData.php | 1 + src/Jetstream.php | 10 + src/JetstreamServiceProvider.php | 12 +- stubs/config/jetstream.php | 1 + .../resources/js/Layouts/AppLayout.vue | 8 + .../resources/js/Pages/OAuth/Index.vue | 28 ++ .../Pages/OAuth/Partials/OAuthAppManager.vue | 403 ++++++++++++++++++ .../resources/js/Pages/PassportAPI/Index.vue | 30 ++ .../PassportAPI/Partials/ApiTokenManager.vue | 210 +++++++++ stubs/inertia/routes/web.php | 2 +- .../resources/views/navigation-menu.blade.php | 12 + .../resources/views/oauth/index.blade.php | 13 + .../views/oauth/oauth-app-manager.blade.php | 268 ++++++++++++ .../passport-api/api-token-manager.blade.php | 144 +++++++ .../views/passport-api/index.blade.php | 13 + 27 files changed, 1904 insertions(+), 13 deletions(-) create mode 100644 src/Http/Controllers/Inertia/OAuthAppController.php create mode 100644 src/Http/Controllers/Inertia/OAuthConnectionController.php create mode 100644 src/Http/Controllers/Inertia/PassportApiTokenController.php create mode 100644 src/Http/Controllers/Livewire/OAuthAppController.php create mode 100644 src/Http/Livewire/OAuthAppManager.php create mode 100644 src/Http/Livewire/PassportApiTokenManager.php create mode 100644 stubs/inertia/resources/js/Pages/OAuth/Index.vue create mode 100644 stubs/inertia/resources/js/Pages/OAuth/Partials/OAuthAppManager.vue create mode 100644 stubs/inertia/resources/js/Pages/PassportAPI/Index.vue create mode 100644 stubs/inertia/resources/js/Pages/PassportAPI/Partials/ApiTokenManager.vue create mode 100644 stubs/livewire/resources/views/oauth/index.blade.php create mode 100644 stubs/livewire/resources/views/oauth/oauth-app-manager.blade.php create mode 100644 stubs/livewire/resources/views/passport-api/api-token-manager.blade.php create mode 100644 stubs/livewire/resources/views/passport-api/index.blade.php diff --git a/composer.json b/composer.json index a3f08672a..e7b94b263 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ }, "require-dev": { "inertiajs/inertia-laravel": "^1.0", + "laravel/passport": "13.x-dev", "laravel/sanctum": "^4.0", "livewire/livewire": "^3.3", "mockery/mockery": "^1.0", diff --git a/routes/inertia.php b/routes/inertia.php index 00d13cfcf..0031dc182 100644 --- a/routes/inertia.php +++ b/routes/inertia.php @@ -2,9 +2,12 @@ use Illuminate\Support\Facades\Route; use Laravel\Jetstream\Http\Controllers\CurrentTeamController; -use Laravel\Jetstream\Http\Controllers\Inertia\ApiTokenController; +use Laravel\Jetstream\Http\Controllers\Inertia\ApiTokenController as SanctumApiTokenController; use Laravel\Jetstream\Http\Controllers\Inertia\CurrentUserController; +use Laravel\Jetstream\Http\Controllers\Inertia\OAuthAppController; +use Laravel\Jetstream\Http\Controllers\Inertia\OAuthConnectionController; use Laravel\Jetstream\Http\Controllers\Inertia\OtherBrowserSessionsController; +use Laravel\Jetstream\Http\Controllers\Inertia\PassportApiTokenController; use Laravel\Jetstream\Http\Controllers\Inertia\PrivacyPolicyController; use Laravel\Jetstream\Http\Controllers\Inertia\ProfilePhotoController; use Laravel\Jetstream\Http\Controllers\Inertia\TeamController; @@ -47,10 +50,25 @@ Route::group(['middleware' => 'verified'], function () { // API... if (Jetstream::hasApiFeatures()) { - Route::get('/user/api-tokens', [ApiTokenController::class, 'index'])->name('api-tokens.index'); - Route::post('/user/api-tokens', [ApiTokenController::class, 'store'])->name('api-tokens.store'); - Route::put('/user/api-tokens/{token}', [ApiTokenController::class, 'update'])->name('api-tokens.update'); - Route::delete('/user/api-tokens/{token}', [ApiTokenController::class, 'destroy'])->name('api-tokens.destroy'); + if (Jetstream::hasOAuthFeatures()) { + Route::get('/user/api-tokens', [SanctumApiTokenController::class, 'index'])->name('api-tokens.index'); + Route::post('/user/api-tokens', [SanctumApiTokenController::class, 'store'])->name('api-tokens.store'); + Route::delete('/user/api-tokens/{token}', [SanctumApiTokenController::class, 'destroy'])->name('api-tokens.destroy'); + } else { + Route::get('/user/api-tokens', [SanctumApiTokenController::class, 'index'])->name('api-tokens.index'); + Route::post('/user/api-tokens', [SanctumApiTokenController::class, 'store'])->name('api-tokens.store'); + Route::put('/user/api-tokens/{token}', [SanctumApiTokenController::class, 'update'])->name('api-tokens.update'); + Route::delete('/user/api-tokens/{token}', [SanctumApiTokenController::class, 'destroy'])->name('api-tokens.destroy'); + } + } + + // OAuth... + if (Jetstream::hasOAuthFeatures()) { + Route::get('/user/oauth-apps', [OAuthAppController::class, 'index'])->name('oauth-apps.index'); + Route::post('/user/oauth-apps', [OAuthAppController::class, 'store'])->name('oauth-apps.store'); + Route::put('/user/oauth-apps/{app}', [OAuthAppController::class, 'update'])->name('oauth-apps.update'); + Route::delete('/user/oauth-apps/{app}', [OAuthAppController::class, 'destroy'])->name('oauth-apps.destroy'); + Route::delete('/user/oauth-connections/{app}', [OAuthConnectionController::class, 'destroy'])->name('oauth-connections.destroy'); } // Teams... diff --git a/routes/livewire.php b/routes/livewire.php index 8de58fc95..7e88824f9 100644 --- a/routes/livewire.php +++ b/routes/livewire.php @@ -3,6 +3,7 @@ use Illuminate\Support\Facades\Route; use Laravel\Jetstream\Http\Controllers\CurrentTeamController; use Laravel\Jetstream\Http\Controllers\Livewire\ApiTokenController; +use Laravel\Jetstream\Http\Controllers\Livewire\OAuthAppController; use Laravel\Jetstream\Http\Controllers\Livewire\PrivacyPolicyController; use Laravel\Jetstream\Http\Controllers\Livewire\TeamController; use Laravel\Jetstream\Http\Controllers\Livewire\TermsOfServiceController; @@ -34,6 +35,11 @@ Route::get('/user/api-tokens', [ApiTokenController::class, 'index'])->name('api-tokens.index'); } + // OAuth... + if (Jetstream::hasOAuthFeatures()) { + Route::get('/user/oauth-apps', [OAuthAppController::class, 'index'])->name('oauth-apps.index'); + } + // Teams... if (Jetstream::hasTeamFeatures()) { Route::get('/teams/create', [TeamController::class, 'create'])->name('teams.create'); diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php index 41c4cb6dc..ce3aff12c 100644 --- a/src/Console/InstallCommand.php +++ b/src/Console/InstallCommand.php @@ -33,6 +33,7 @@ class InstallCommand extends Command implements PromptsForMissingInput {--dark : Indicate that dark mode support should be installed} {--teams : Indicates if team support should be installed} {--api : Indicates if API support should be installed} + {--oauth : Indicates if OAuth support via Laravel Passport should be installed} {--verification : Indicates if email verification support should be installed} {--pest : Indicates if Pest should be installed} {--ssr : Indicates if Inertia SSR support should be installed} @@ -87,6 +88,12 @@ public function handle() $this->replaceInFile('// Features::api(),', 'Features::api(),', config_path('jetstream.php')); } + // Configure OAuth... + if ($this->option('oauth')) { + $this->replaceInFile('// Features::oauth(),', 'Features::oauth(),', config_path('jetstream.php')); + $this->replaceInFile('sanctum', 'web', config_path('jetstream.php')); + } + // Configure Email Verification... if ($this->option('verification')) { $this->replaceInFile('// Features::emailVerification(),', 'Features::emailVerification(),', config_path('fortify.php')); @@ -156,6 +163,7 @@ protected function installLivewireStack() $this->call('install:api', [ '--without-migration-prompt' => true, + '--passport' => $this->option('oauth'), ]); // Update Configuration... @@ -202,6 +210,10 @@ protected function installLivewireStack() // Models... copy(__DIR__.'/../../stubs/app/Models/User.php', app_path('Models/User.php')); + if ($this->option('oauth')) { + $this->replaceInFile('Laravel\Sanctum\HasApiTokens', 'Laravel\Passport\HasApiTokens', app_path('Models/User.php')); + } + // Factories... copy(__DIR__.'/../../database/factories/UserFactory.php', base_path('database/factories/UserFactory.php')); @@ -227,9 +239,15 @@ protected function installLivewireStack() copy(__DIR__.'/../../stubs/livewire/resources/views/policy.blade.php', resource_path('views/policy.blade.php')); // Other Views... - (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/livewire/resources/views/api', resource_path('views/api')); (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/livewire/resources/views/profile', resource_path('views/profile')); (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/livewire/resources/views/auth', resource_path('views/auth')); + (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/livewire/resources/views/oauth', resource_path('views/oauth')); + + if ($this->option('oauth')) { + (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/livewire/resources/views/passport-api', resource_path('views/api')); + } else { + (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/livewire/resources/views/api', resource_path('views/api')); + } if (! Str::contains(file_get_contents(base_path('routes/web.php')), "'/dashboard'")) { (new Filesystem)->append(base_path('routes/web.php'), $this->livewireRouteDefinition()); @@ -316,7 +334,7 @@ protected function livewireRouteDefinition() return <<<'EOF' Route::middleware([ - 'auth:sanctum', + config('jetstream.guard') ? 'auth:'.config('jetstream.guard') : 'auth', config('jetstream.auth_session'), 'verified', ])->group(function () { @@ -342,6 +360,7 @@ protected function installInertiaStack() $this->call('install:api', [ '--without-migration-prompt' => true, + '--passport' => $this->option('oauth'), ]); // Install NPM packages... @@ -405,6 +424,10 @@ protected function installInertiaStack() // Models... copy(__DIR__.'/../../stubs/app/Models/User.php', app_path('Models/User.php')); + if ($this->option('oauth')) { + $this->replaceInFile('Laravel\Sanctum\HasApiTokens', 'Laravel\Passport\HasApiTokens', app_path('Models/User.php')); + } + // Factories... copy(__DIR__.'/../../database/factories/UserFactory.php', base_path('database/factories/UserFactory.php')); @@ -428,9 +451,15 @@ protected function installInertiaStack() (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia/resources/js/Components', resource_path('js/Components')); (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia/resources/js/Layouts', resource_path('js/Layouts')); - (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia/resources/js/Pages/API', resource_path('js/Pages/API')); (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia/resources/js/Pages/Auth', resource_path('js/Pages/Auth')); (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia/resources/js/Pages/Profile', resource_path('js/Pages/Profile')); + (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia/resources/js/Pages/OAuth', resource_path('js/Pages/OAuth')); + + if ($this->option('oauth')) { + (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia/resources/js/Pages/PassportAPI', resource_path('js/Pages/API')); + } else { + (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/inertia/resources/js/Pages/API', resource_path('js/Pages/API')); + } copy(__DIR__.'/../../stubs/inertia/routes/web.php', base_path('routes/web.php')); @@ -537,6 +566,10 @@ protected function ensureApplicationIsTeamCompatible() copy(__DIR__.'/../../stubs/app/Models/TeamInvitation.php', app_path('Models/TeamInvitation.php')); copy(__DIR__.'/../../stubs/app/Models/UserWithTeams.php', app_path('Models/User.php')); + if ($this->option('oauth')) { + $this->replaceInFile('Laravel\Sanctum\HasApiTokens', 'Laravel\Passport\HasApiTokens', app_path('Models/User.php')); + } + // Actions... copy(__DIR__.'/../../stubs/app/Actions/Jetstream/AddTeamMember.php', app_path('Actions/Jetstream/AddTeamMember.php')); copy(__DIR__.'/../../stubs/app/Actions/Jetstream/CreateTeam.php', app_path('Actions/Jetstream/CreateTeam.php')); @@ -854,6 +887,7 @@ protected function afterPromptingForMissingArguments(InputInterface $input, Outp options: collect([ 'teams' => 'Team support', 'api' => 'API support', + 'oauth' => 'OAuth support via Laravel Passport', 'verification' => 'Email verification', 'dark' => 'Dark mode', ])->when( diff --git a/src/Features.php b/src/Features.php index ee9c85ebe..14f2fc82e 100644 --- a/src/Features.php +++ b/src/Features.php @@ -48,6 +48,16 @@ public static function hasApiFeatures() return static::enabled(static::api()); } + /** + * Determine if the application is using any OAuth features. + * + * @return bool + */ + public static function hasOAuthFeatures() + { + return static::enabled(static::oauth()); + } + /** * Determine if the application is using any team features. * @@ -108,6 +118,16 @@ public static function api() return 'api'; } + /** + * Enable the OAuth feature. + * + * @return string + */ + public static function oauth() + { + return 'oauth'; + } + /** * Enable the teams feature. * diff --git a/src/HasTeams.php b/src/HasTeams.php index 6c7442736..89c2d2657 100644 --- a/src/HasTeams.php +++ b/src/HasTeams.php @@ -3,7 +3,8 @@ namespace Laravel\Jetstream; use Illuminate\Support\Str; -use Laravel\Sanctum\HasApiTokens; +use Laravel\Sanctum\HasApiTokens as SanctumHasApiTokens; +use Laravel\Passport\HasApiTokens as PassportHasApiTokens; trait HasTeams { @@ -207,7 +208,8 @@ public function hasTeamPermission($team, string $permission) return false; } - if (in_array(HasApiTokens::class, class_uses_recursive($this)) && + if ((in_array(SanctumHasApiTokens::class, $traits = class_uses_recursive($this)) || + in_array(PassportHasApiTokens::class, $traits)) && ! $this->tokenCan($permission) && $this->currentAccessToken() !== null) { return false; diff --git a/src/Http/Controllers/Inertia/OAuthAppController.php b/src/Http/Controllers/Inertia/OAuthAppController.php new file mode 100644 index 000000000..00306b453 --- /dev/null +++ b/src/Http/Controllers/Inertia/OAuthAppController.php @@ -0,0 +1,109 @@ +render($request, 'API/Index', [ + 'authorizedApps' => $request->user()->tokens() + ->with('client') + ->where('revoked', false) + ->where('expires_at', '>', Date::now()) + ->get() + ->reduce(function (Collection $apps, Token $token) { + if ($token->client->revoked || $token->client->personal_access_client) { + return $apps; + } + + $app = $apps->get($token->client_id); + + if ($app) { + $app['scopes'] = array_unique(array_merge($app['scopes'], $token->scopes)); + $app['tokens_count'] += 1; + + $apps->put($token->client_id, $app); + } else { + $apps->put($token->client_id, [ + 'client' => $token->client, + 'scopes' => $token->scopes, + 'tokens_count' => 1, + ]); + } + + return $apps; + }, collect()), + 'oauthApps' => $request->user()->clients() + ->where('revoked', false) + ->orderBy('name', 'asc') + ->get() + ->map(fn (Client $client) => $client->toArray() + [ + 'is_confidential' => $client->confidential(), + 'created_date' => $client->created_at->toFormattedDateString(), + ]), + ]); + } + + /** + * Create a new OAuth app. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function store(Request $request) + { + $client = app(CreatesClients::class)->create($request->all()); + + return back()->with('flash', [ + 'client_id' => $client->id, + 'client_secret' => $client->plainSecret, + ]); + } + + /** + * Update the given OAuth app. + * + * @param \Illuminate\Http\Request $request + * @param string $clientId + * @return \Illuminate\Http\RedirectResponse + */ + public function update(Request $request, $clientId) + { + $client = $request->user()->clients()->findOrFail($clientId); + + app(UpdatesClients::class)->update($client, $request->all()); + + return back(303); + } + + /** + * Delete the given OAuth App. + * + * @param \Illuminate\Http\Request $request + * @param string $clientId + * @return \Illuminate\Http\RedirectResponse + */ + public function destroy(Request $request, $clientId) + { + $request->user()->clients()->find($clientId)->delete(); + + return back(303); + } +} diff --git a/src/Http/Controllers/Inertia/OAuthConnectionController.php b/src/Http/Controllers/Inertia/OAuthConnectionController.php new file mode 100644 index 000000000..f460804de --- /dev/null +++ b/src/Http/Controllers/Inertia/OAuthConnectionController.php @@ -0,0 +1,29 @@ +user()->tokens() + ->where('client_id', $clientId) + ->each(function (Token $token) { + $token->refreshToken->revoke(); + $token->revoke(); + }); + + return back(303); + } +} diff --git a/src/Http/Controllers/Inertia/PassportApiTokenController.php b/src/Http/Controllers/Inertia/PassportApiTokenController.php new file mode 100644 index 000000000..308e9d200 --- /dev/null +++ b/src/Http/Controllers/Inertia/PassportApiTokenController.php @@ -0,0 +1,73 @@ +render($request, 'API/Index', [ + 'tokens' => $request->user()->tokens() + ->with('client') + ->where('revoked', false) + ->where('expires_at', '>', Date::now()) + ->get() + ->filter(fn (Token $token) => $token->client->personal_access_client) + ->map(fn (Token $token) => $token->toArray() + [ + 'issued_ago' => $token->created_at->diffForHumans(), + 'expires_in' => $token->expires_at->longAbsoluteDiffForHumans(), + ]), + 'availableScopes' => Passport::scopes(), + 'defaultScopes' => Passport::defaultScopes(), + ]); + } + + /** + * Create a new API token. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function store(Request $request) + { + $request->validate([ + 'name' => ['required', 'string', 'max:255'], + ]); + + $result = $request->user()->createToken( + $request->name, + Passport::validScopes($request->input('scopes', [])) + ); + + return back()->with('flash', [ + 'token' => $result->accessToken, + ]); + } + + /** + * Delete the given API token. + * + * @param \Illuminate\Http\Request $request + * @param string $tokenId + * @return \Illuminate\Http\RedirectResponse + */ + public function destroy(Request $request, $tokenId) + { + $request->user()->tokens()->find($tokenId)->revoke(); + + return back(303); + } +} diff --git a/src/Http/Controllers/Livewire/OAuthAppController.php b/src/Http/Controllers/Livewire/OAuthAppController.php new file mode 100644 index 000000000..d938c0b4d --- /dev/null +++ b/src/Http/Controllers/Livewire/OAuthAppController.php @@ -0,0 +1,18 @@ + + */ + public $authorizedApps; + + /** + * The user's OAuth apps. + * + * @var \Illuminate\Database\Eloquent\Collection + */ + public $oauthApps; + + /** + * Create OAuth app form state. + * + * @var array + */ + public array $createOAuthAppForm = [ + 'name' => '', + 'redirect_uri' => '', + 'confidential' => false, + ]; + + /** + * Indicates if the client credentials is being displayed to the user. + */ + public bool $displayingClientCredentials = false; + + /** + * The client credentials. + */ + public array $clientCredentials = [ + 'id' => null, + 'secret' => null, + ]; + + /** + * Indicates if the application is confirming if a authorized app should be revoked. + */ + public bool $confirmingAuthorizedAppRevocation = false; + + /** + * The ID of the authorized app being revoked. + */ + public string $authorizedAppIdBeingRevoked; + + /** + * Indicates if the user is currently managing an OAuth app. + * + * @var bool + */ + public bool $managingOAuthApp = false; + + /** + * The OAuth app that is currently being managed. + * + * @var \Laravel\Passport\Client|null + */ + public ?Client $oauthAppBeingManaged; + + /** + * The update OAuth app form state. + * + * @var array + */ + public array $updateOAuthAppForm = [ + 'name' => '', + 'redirect_uri' => '', + ]; + + /** + * Indicates if the application is confirming if a OAuth app should be deleted. + */ + public bool $confirmingOAuthAppDeletion = false; + + /** + * The ID of the OAuth app being deleted. + */ + public string $oauthAppIdBeingDeleted; + + /** + * Mount the component. + */ + public function mount(): void + { + $this->loadAuthorizedApps(); + $this->loadOAuthApps(); + } + + /** + * Render the component. + */ + public function render(): View + { + return view('oauth.oauth-app-manager'); + } + + /** + * Get the current user of the application. + * + * @return \Illuminate\Contracts\Auth\Authenticatable + */ + public function getUserProperty() + { + return Auth::user(); + } + + /** + * Load the user's authorized apps. + */ + #[On('authorized-app-revoked')] + public function loadAuthorizedApps(): void + { + $this->authorizedApps = $this->user->tokens() + ->with('client') + ->where('revoked', false) + ->where('expires_at', '>', Date::now()) + ->get() + ->reduce(function (Collection $apps, Token $token) { + if ($token->client->revoked || $token->client->personal_access_client) { + return $apps; + } + + $app = $apps->get($token->client_id); + + if ($app) { + $app['scopes'] = array_unique(array_merge($app['scopes'], $token->scopes)); + $app['tokens_count'] += 1; + + $apps->put($token->client_id, $app); + } else { + $apps->put($token->client_id, [ + 'client' => $token->client, + 'scopes' => $token->scopes, + 'tokens_count' => 1, + ]); + } + + return $apps; + }, collect()); + } + + /** + * Load the user's OAuth apps. + */ + #[On(['oauth-app-created', 'oauth-app-updated', 'oauth-app-deleted'])] + public function loadOAuthApps(): void + { + $this->oauthApps = $this->user->clients() + ->where('revoked', false) + ->orderBy('name', 'asc') + ->get(); + } + + /** + * Create a new OAuth Client. + */ + public function createOAuthApp(CreatesClients $creator): void + { + $this->resetErrorBag(); + + $this->displayClientCredentials( + $creator->create($this->createOAuthAppForm) + ); + + $this->createOAuthAppForm['name'] = ''; + $this->createOAuthAppForm['redirect_uri'] = ''; + $this->createOAuthAppForm['confidential'] = false; + + $this->dispatch('oauth-app-created'); + } + + /** + * Display the token value to the user. + */ + protected function displayClientCredentials(Client $client): void + { + $this->displayingClientCredentials = true; + + $this->clientCredentials = [ + 'id' => $client->id, + 'secret' => $client->plainSecret, + ]; + + $this->dispatch('client-credentials-displayed'); + } + + /** + * Allow the given OAuth app to be managed. + */ + public function manageOAuthApp(string $clientId): void + { + $this->managingOAuthApp = true; + + $this->oauthAppBeingManaged = $this->oauthApps->find($clientId); + + $this->updateOAuthAppForm['name'] = $this->oauthAppBeingManaged->name; + $this->updateOAuthAppForm['redirect_uri'] = $this->oauthAppBeingManaged->redirect; + } + + /** + * Update the OAuth app. + */ + public function updateOAuthApp(UpdatesClients $updater): void + { + $updater->update($this->oauthAppBeingManaged, $this->updateOAuthAppForm); + + $this->dispatch('oauth-app-updated'); + + $this->managingOAuthApp = false; + } + + /** + * Confirm that the given OAuth app should be deleted. + */ + public function confirmOAuthAppDeletion(string $clientId): void + { + $this->confirmingOAuthAppDeletion = true; + + $this->oauthAppIdBeingDeleted = $clientId; + } + + /** + * Delete the OAuth app. + */ + public function deleteOAuthApp(): void + { + $this->oauthApps->find($this->oauthAppIdBeingDeleted)->delete(); + + $this->dispatch('oauth-app-deleted'); + + $this->confirmingOAuthAppDeletion = false; + } + + /** + * Confirm that the given authorized app should be revoked. + */ + public function confirmAuthorizedAppRevocation(string $tokenId): void + { + $this->confirmingAuthorizedAppRevocation = true; + + $this->authorizedAppIdBeingRevoked = $tokenId; + } + + /** + * Revoke the authorized app. + */ + public function revokeAuthorizedApp(): void + { + $this->user->tokens() + ->where('client_id', $this->authorizedAppIdBeingRevoked) + ->each(function (Token $token) { + $token->refreshToken->revoke(); + $token->revoke(); + }); + + $this->dispatch('authorized-app-revoked'); + + $this->confirmingAuthorizedAppRevocation = false; + } +} diff --git a/src/Http/Livewire/PassportApiTokenManager.php b/src/Http/Livewire/PassportApiTokenManager.php new file mode 100644 index 000000000..4fefb6ce9 --- /dev/null +++ b/src/Http/Livewire/PassportApiTokenManager.php @@ -0,0 +1,152 @@ + + */ + public $tokens; + + /** + * Create API token form state. + * + * @var array + */ + public array $createApiTokenForm = [ + 'name' => '', + 'scopes' => [], + ]; + + /** + * Indicates if the token is being displayed to the user. + */ + public bool $displayingToken = false; + + /** + * The token value. + */ + public ?string $tokenValue; + + /** + * Indicates if the application is confirming if an API token should be deleted. + */ + public bool $confirmingApiTokenDeletion = false; + + /** + * The ID of the API token being deleted. + */ + public string $apiTokenIdBeingDeleted; + + /** + * Mount the component. + */ + public function mount(): void + { + $this->createApiTokenForm['scopes'] = Passport::defaultScopes(); + + $this->loadTokens(); + } + + /** + * Load the user's tokens. + */ + #[On(['token-created', 'token-deleted'])] + public function loadTokens(): void + { + $this->tokens = $this->user->tokens() + ->with('client') + ->where('revoked', false) + ->where('expires_at', '>', Date::now()) + ->get() + ->filter(fn ($token) => $token->client->personal_access_client); + } + + /** + * Create a new API token. + */ + public function createApiToken(): void + { + $this->resetErrorBag(); + + Validator::make([ + 'name' => $this->createApiTokenForm['name'], + ], [ + 'name' => ['required', 'string', 'max:255'], + ])->validateWithBag('createApiToken'); + + $this->displayTokenValue($this->user->createToken( + $this->createApiTokenForm['name'], + Passport::validScopes($this->createApiTokenForm['scopes']) + )); + + $this->createApiTokenForm['name'] = ''; + $this->createApiTokenForm['scopes'] = Passport::defaultScopes(); + + $this->dispatch('token-created'); + } + + /** + * Display the token value to the user. + */ + protected function displayTokenValue(PersonalAccessTokenResult $result): void + { + $this->displayingToken = true; + + $this->tokenValue = $result->accessToken; + + $this->dispatch('token-displayed'); + } + + /** + * Confirm that the given API token should be deleted. + */ + public function confirmApiTokenDeletion(string $tokenId): void + { + $this->confirmingApiTokenDeletion = true; + + $this->apiTokenIdBeingDeleted = $tokenId; + } + + /** + * Delete the API token. + */ + public function deleteApiToken(): void + { + $this->tokens->find($this->apiTokenIdBeingDeleted)->revoke(); + + $this->dispatch('token-deleted'); + + $this->confirmingApiTokenDeletion = false; + } + + /** + * Get the current user of the application. + * + * @return \Illuminate\Contracts\Auth\Authenticatable + */ + public function getUserProperty() + { + return Auth::user(); + } + + /** + * Render the component. + */ + public function render(): View + { + return view('api.api-token-manager'); + } +} diff --git a/src/Http/Middleware/ShareInertiaData.php b/src/Http/Middleware/ShareInertiaData.php index 80c266358..1c5497d9f 100644 --- a/src/Http/Middleware/ShareInertiaData.php +++ b/src/Http/Middleware/ShareInertiaData.php @@ -34,6 +34,7 @@ public function handle($request, $next) 'flash' => $request->session()->get('flash', []), 'hasAccountDeletionFeatures' => Jetstream::hasAccountDeletionFeatures(), 'hasApiFeatures' => Jetstream::hasApiFeatures(), + 'hasOAuthFeatures' => Jetstream::hasOAuthFeatures(), 'hasTeamFeatures' => Jetstream::hasTeamFeatures(), 'hasTermsAndPrivacyPolicyFeature' => Jetstream::hasTermsAndPrivacyPolicyFeature(), 'managesProfilePhotos' => Jetstream::managesProfilePhotos(), diff --git a/src/Jetstream.php b/src/Jetstream.php index 07e7d16df..08b3846ac 100644 --- a/src/Jetstream.php +++ b/src/Jetstream.php @@ -186,6 +186,16 @@ public static function hasApiFeatures() return Features::hasApiFeatures(); } + /** + * Determine if Jetstream is supporting OAuth features. + * + * @return bool + */ + public static function hasOAuthFeatures() + { + return Features::hasOAuthFeatures(); + } + /** * Determine if Jetstream is supporting team features. * diff --git a/src/JetstreamServiceProvider.php b/src/JetstreamServiceProvider.php index 720706409..3a737a629 100644 --- a/src/JetstreamServiceProvider.php +++ b/src/JetstreamServiceProvider.php @@ -14,12 +14,14 @@ use Inertia\Inertia; use Laravel\Fortify\Events\PasswordUpdatedViaController; use Laravel\Fortify\Fortify; -use Laravel\Jetstream\Http\Livewire\ApiTokenManager; +use Laravel\Jetstream\Http\Livewire\ApiTokenManager as SanctumApiTokenManager; use Laravel\Jetstream\Http\Livewire\CreateTeamForm; use Laravel\Jetstream\Http\Livewire\DeleteTeamForm; use Laravel\Jetstream\Http\Livewire\DeleteUserForm; use Laravel\Jetstream\Http\Livewire\LogoutOtherBrowserSessionsForm; use Laravel\Jetstream\Http\Livewire\NavigationMenu; +use Laravel\Jetstream\Http\Livewire\OAuthAppManager; +use Laravel\Jetstream\Http\Livewire\PassportApiTokenManager; use Laravel\Jetstream\Http\Livewire\TeamMemberManager; use Laravel\Jetstream\Http\Livewire\TwoFactorAuthenticationForm; use Laravel\Jetstream\Http\Livewire\UpdatePasswordForm; @@ -90,7 +92,13 @@ public function boot() Livewire::component('profile.delete-user-form', DeleteUserForm::class); if (Features::hasApiFeatures()) { - Livewire::component('api.api-token-manager', ApiTokenManager::class); + Livewire::component('api.api-token-manager', + Features::hasOAuthFeatures() ? PassportApiTokenManager::class : SanctumApiTokenManager::class + ); + } + + if (Features::hasOAuthFeatures()) { + Livewire::component('oauth.oauth-app-manager', OAuthAppManager::class); } if (Features::hasTeamFeatures()) { diff --git a/stubs/config/jetstream.php b/stubs/config/jetstream.php index a90b5a072..68fc7fd90 100644 --- a/stubs/config/jetstream.php +++ b/stubs/config/jetstream.php @@ -61,6 +61,7 @@ // Features::termsAndPrivacyPolicy(), // Features::profilePhotos(), // Features::api(), + // Features::oauth(), // Features::teams(['invitations' => true]), Features::accountDeletion(), ], diff --git a/stubs/inertia/resources/js/Layouts/AppLayout.vue b/stubs/inertia/resources/js/Layouts/AppLayout.vue index 654a6f793..b36f9a679 100644 --- a/stubs/inertia/resources/js/Layouts/AppLayout.vue +++ b/stubs/inertia/resources/js/Layouts/AppLayout.vue @@ -146,6 +146,10 @@ const logout = () => { API Tokens + + OAuth Apps + +
@@ -222,6 +226,10 @@ const logout = () => { API Tokens + + OAuth Apps + +
diff --git a/stubs/inertia/resources/js/Pages/OAuth/Index.vue b/stubs/inertia/resources/js/Pages/OAuth/Index.vue new file mode 100644 index 000000000..1f6066584 --- /dev/null +++ b/stubs/inertia/resources/js/Pages/OAuth/Index.vue @@ -0,0 +1,28 @@ + + + diff --git a/stubs/inertia/resources/js/Pages/OAuth/Partials/OAuthAppManager.vue b/stubs/inertia/resources/js/Pages/OAuth/Partials/OAuthAppManager.vue new file mode 100644 index 000000000..30c14d384 --- /dev/null +++ b/stubs/inertia/resources/js/Pages/OAuth/Partials/OAuthAppManager.vue @@ -0,0 +1,403 @@ + + + diff --git a/stubs/inertia/resources/js/Pages/PassportAPI/Index.vue b/stubs/inertia/resources/js/Pages/PassportAPI/Index.vue new file mode 100644 index 000000000..bbb8f698e --- /dev/null +++ b/stubs/inertia/resources/js/Pages/PassportAPI/Index.vue @@ -0,0 +1,30 @@ + + + diff --git a/stubs/inertia/resources/js/Pages/PassportAPI/Partials/ApiTokenManager.vue b/stubs/inertia/resources/js/Pages/PassportAPI/Partials/ApiTokenManager.vue new file mode 100644 index 000000000..da5663019 --- /dev/null +++ b/stubs/inertia/resources/js/Pages/PassportAPI/Partials/ApiTokenManager.vue @@ -0,0 +1,210 @@ + + + diff --git a/stubs/inertia/routes/web.php b/stubs/inertia/routes/web.php index 5bfb84299..7570b628a 100644 --- a/stubs/inertia/routes/web.php +++ b/stubs/inertia/routes/web.php @@ -14,7 +14,7 @@ }); Route::middleware([ - 'auth:sanctum', + config('jetstream.guard') ? 'auth:'.config('jetstream.guard') : 'auth', config('jetstream.auth_session'), 'verified', ])->group(function () { diff --git a/stubs/livewire/resources/views/navigation-menu.blade.php b/stubs/livewire/resources/views/navigation-menu.blade.php index fc5b22dbf..a6a0433bb 100644 --- a/stubs/livewire/resources/views/navigation-menu.blade.php +++ b/stubs/livewire/resources/views/navigation-menu.blade.php @@ -108,6 +108,12 @@ @endif + @if (Laravel\Jetstream\Jetstream::hasOAuthFeatures()) + + {{ __('OAuth Apps') }} + + @endif +
@@ -171,6 +177,12 @@ @endif + @if (Laravel\Jetstream\Jetstream::hasOAuthFeatures()) + + {{ __('OAuth Apps') }} + + @endif + @csrf diff --git a/stubs/livewire/resources/views/oauth/index.blade.php b/stubs/livewire/resources/views/oauth/index.blade.php new file mode 100644 index 000000000..12e86d994 --- /dev/null +++ b/stubs/livewire/resources/views/oauth/index.blade.php @@ -0,0 +1,13 @@ + + +

+ {{ __('OAuth Apps') }} +

+
+ +
+
+ @livewire('oauth.oauth-app-manager') +
+
+
diff --git a/stubs/livewire/resources/views/oauth/oauth-app-manager.blade.php b/stubs/livewire/resources/views/oauth/oauth-app-manager.blade.php new file mode 100644 index 000000000..e9f4412d2 --- /dev/null +++ b/stubs/livewire/resources/views/oauth/oauth-app-manager.blade.php @@ -0,0 +1,268 @@ +
+ @if ($this->authorizedApps->isNotEmpty()) + +
+ + + {{ __('Manage Authorized Apps') }} + + + + {{ __('Keep track of your connections to third-party apps and services.') }} + + + + +
+ @foreach ($this->authorizedApps as $id => $app) +
+
+
+ {{ $app['client']->name }} +
+
+ {{ implode(', ', $app['scopes']) }} +
+
+ +
+
+ {{ $app['tokens_count'] }} {{ __('Tokens') }} +
+ + +
+
+ @endforeach +
+
+
+
+ + + @endif + + + + + {{ __('Register OAuth App') }} + + + + {{ __('You may register an OAuth client to use our application\'s API.') }} + + + + +
+ + + +
+ + +
+ + + +
+ + +
+ + +
+
+ + + + {{ __('Registered.') }} + + + + {{ __('Register') }} + + +
+ + @if ($this->oauthApps->isNotEmpty()) + + + +
+ + + {{ __('Manage OAuth Apps') }} + + + + {{ __('You may delete any of your existing registered apps if they are no longer needed.') }} + + + + +
+ @foreach ($this->oauthApps as $app) +
+
+ {{ $app->name }} + + – {{ $app->confidential() ? __('Confidential') : __('Public') }} + +
+ +
+
+ {{ __('Created at') }} {{ $app->created_at->toFormattedDateString() }} +
+ + + + +
+
+ @endforeach +
+
+
+
+ @endif + + + + + {{ __('Client Credentials') }} + + + +
+
+ {{ __('Please copy your new client credentials.') }} + + @if ($clientCredentials['secret']) + {{ __('For your security, client secret won\'t be shown again.') }} + @endif +
+ +
+ + +
+ + @if ($clientCredentials['secret']) +
+ + +
+ @endif +
+
+ + + + {{ __('Close') }} + + +
+ + + + + {{ __('Delete OAuth App') }} + + + + {{ __('Are you sure you would like to delete this app?') }} + + + + + {{ __('Cancel') }} + + + + {{ __('Delete') }} + + + + + + + + {{ __('OAuth App Management') }} + + + +
+ +
+ + +
+ + +
+ + + +
+ + +
+ + + +
+
+
+ + + + {{ __('Cancel') }} + + + + {{ __('Save') }} + + +
+ + + + + {{ __('Revoke Authorized App') }} + + + + {{ __('Are you sure you would like to revoke this app?') }} + + + + + {{ __('Cancel') }} + + + + {{ __('Revoke') }} + + + +
diff --git a/stubs/livewire/resources/views/passport-api/api-token-manager.blade.php b/stubs/livewire/resources/views/passport-api/api-token-manager.blade.php new file mode 100644 index 000000000..af07b3a2b --- /dev/null +++ b/stubs/livewire/resources/views/passport-api/api-token-manager.blade.php @@ -0,0 +1,144 @@ +
+ + + + {{ __('Create API Token') }} + + + + {{ __('Personal access tokens allow secure authentication to our application\'s API for your personal use.') }} + + + + +
+ + + +
+ + + @if (count($scopes = Laravel\Passport\Passport::scopes()) > 0) +
+
+ {{ __('Scopes') }} + +
+ @foreach ($scopes as $scope) + + @endforeach +
+
+
+ @endif +
+ + + + {{ __('Created.') }} + + + + {{ __('Create') }} + + +
+ + @if ($this->tokens->isNotEmpty()) + + + +
+ + + {{ __('Manage API Tokens') }} + + + + {{ __('You may revoke any of your existing personal access tokens if they are no longer needed.') }} + + + + +
+ @foreach ($this->tokens as $token) +
+
+
+ {{ $token->name }} +
+
+ {{ implode(', ', $token->scopes) }} +
+
+ +
+
+ {{ __('Issued') }} {{ $token->created_at->diffForHumans() }} +
+ +
+ {{ __('Expires in') }} {{ $token->expires_at->longAbsoluteDiffForHumans() }} +
+ + +
+
+ @endforeach +
+
+
+
+ @endif + + + + + {{ __('Personal Access Token') }} + + + +
+ {{ __('Please copy your new personal access token. For your security, it won\'t be shown again.') }} +
+ + +
+ + + + {{ __('Close') }} + + +
+ + + + + {{ __('Revoke API Token') }} + + + + {{ __('Are you sure you would like to revoke this API token?') }} + + + + + {{ __('Cancel') }} + + + + {{ __('Revoke') }} + + + +
diff --git a/stubs/livewire/resources/views/passport-api/index.blade.php b/stubs/livewire/resources/views/passport-api/index.blade.php new file mode 100644 index 000000000..5f6be47ab --- /dev/null +++ b/stubs/livewire/resources/views/passport-api/index.blade.php @@ -0,0 +1,13 @@ + + +

+ {{ __('API Tokens') }} +

+
+ +
+
+ @livewire('api.api-token-manager') +
+
+
From 2293e2f96292df7446f5c591385a2a54c6c8f34a Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Wed, 31 Jul 2024 02:00:01 +0330 Subject: [PATCH 02/20] formatting --- src/HasTeams.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HasTeams.php b/src/HasTeams.php index 89c2d2657..7c847d79c 100644 --- a/src/HasTeams.php +++ b/src/HasTeams.php @@ -3,8 +3,8 @@ namespace Laravel\Jetstream; use Illuminate\Support\Str; -use Laravel\Sanctum\HasApiTokens as SanctumHasApiTokens; use Laravel\Passport\HasApiTokens as PassportHasApiTokens; +use Laravel\Sanctum\HasApiTokens as SanctumHasApiTokens; trait HasTeams { From dbe6f8726a69ff6d83c26db219d24cb2dbfbe496 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Thu, 1 Aug 2024 01:53:14 +0330 Subject: [PATCH 03/20] wip --- routes/inertia.php | 8 +++++--- src/Http/Controllers/Inertia/OAuthAppController.php | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/routes/inertia.php b/routes/inertia.php index 0031dc182..a9cd5fa1c 100644 --- a/routes/inertia.php +++ b/routes/inertia.php @@ -51,9 +51,9 @@ // API... if (Jetstream::hasApiFeatures()) { if (Jetstream::hasOAuthFeatures()) { - Route::get('/user/api-tokens', [SanctumApiTokenController::class, 'index'])->name('api-tokens.index'); - Route::post('/user/api-tokens', [SanctumApiTokenController::class, 'store'])->name('api-tokens.store'); - Route::delete('/user/api-tokens/{token}', [SanctumApiTokenController::class, 'destroy'])->name('api-tokens.destroy'); + Route::get('/user/api-tokens', [PassportApiTokenController::class, 'index'])->name('api-tokens.index'); + Route::post('/user/api-tokens', [PassportApiTokenController::class, 'store'])->name('api-tokens.store'); + Route::delete('/user/api-tokens/{token}', [PassportApiTokenController::class, 'destroy'])->name('api-tokens.destroy'); } else { Route::get('/user/api-tokens', [SanctumApiTokenController::class, 'index'])->name('api-tokens.index'); Route::post('/user/api-tokens', [SanctumApiTokenController::class, 'store'])->name('api-tokens.store'); @@ -68,6 +68,8 @@ Route::post('/user/oauth-apps', [OAuthAppController::class, 'store'])->name('oauth-apps.store'); Route::put('/user/oauth-apps/{app}', [OAuthAppController::class, 'update'])->name('oauth-apps.update'); Route::delete('/user/oauth-apps/{app}', [OAuthAppController::class, 'destroy'])->name('oauth-apps.destroy'); + + Route::get('/user/oauth-connections', [OAuthConnectionController::class, 'index'])->name('oauth-connections.index'); Route::delete('/user/oauth-connections/{app}', [OAuthConnectionController::class, 'destroy'])->name('oauth-connections.destroy'); } diff --git a/src/Http/Controllers/Inertia/OAuthAppController.php b/src/Http/Controllers/Inertia/OAuthAppController.php index 00306b453..d4e046cc2 100644 --- a/src/Http/Controllers/Inertia/OAuthAppController.php +++ b/src/Http/Controllers/Inertia/OAuthAppController.php @@ -22,7 +22,7 @@ class OAuthAppController extends Controller */ public function index(Request $request) { - return Jetstream::inertia()->render($request, 'API/Index', [ + return Jetstream::inertia()->render($request, 'OAuth/Index', [ 'authorizedApps' => $request->user()->tokens() ->with('client') ->where('revoked', false) From 03068c266fecce57618df6a884b4f368fb1304e3 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Thu, 1 Aug 2024 10:35:00 +0330 Subject: [PATCH 04/20] separate oauth managers --- routes/inertia.php | 1 - .../Inertia/OAuthAppController.php | 44 +++---- .../Inertia/PassportApiTokenController.php | 2 +- src/Http/Livewire/OAuthAppManager.php | 110 +++--------------- src/Http/Livewire/OAuthConnectionManager.php | 104 +++++++++++++++++ src/Http/Livewire/PassportApiTokenManager.php | 2 +- src/JetstreamServiceProvider.php | 2 + .../resources/js/Pages/OAuth/Index.vue | 12 +- .../Pages/OAuth/Partials/OAuthAppManager.vue | 91 +-------------- .../OAuth/Partials/OAuthConnectionManager.vue | 102 ++++++++++++++++ .../resources/views/oauth/index.blade.php | 2 + .../views/oauth/oauth-app-manager.blade.php | 72 +----------- .../oauth/oauth-connection-manager.blade.php | 67 +++++++++++ 13 files changed, 325 insertions(+), 286 deletions(-) create mode 100644 src/Http/Livewire/OAuthConnectionManager.php create mode 100644 stubs/inertia/resources/js/Pages/OAuth/Partials/OAuthConnectionManager.vue create mode 100644 stubs/livewire/resources/views/oauth/oauth-connection-manager.blade.php diff --git a/routes/inertia.php b/routes/inertia.php index a9cd5fa1c..2eb1bf237 100644 --- a/routes/inertia.php +++ b/routes/inertia.php @@ -69,7 +69,6 @@ Route::put('/user/oauth-apps/{app}', [OAuthAppController::class, 'update'])->name('oauth-apps.update'); Route::delete('/user/oauth-apps/{app}', [OAuthAppController::class, 'destroy'])->name('oauth-apps.destroy'); - Route::get('/user/oauth-connections', [OAuthConnectionController::class, 'index'])->name('oauth-connections.index'); Route::delete('/user/oauth-connections/{app}', [OAuthConnectionController::class, 'destroy'])->name('oauth-connections.destroy'); } diff --git a/src/Http/Controllers/Inertia/OAuthAppController.php b/src/Http/Controllers/Inertia/OAuthAppController.php index d4e046cc2..1fcfb1d85 100644 --- a/src/Http/Controllers/Inertia/OAuthAppController.php +++ b/src/Http/Controllers/Inertia/OAuthAppController.php @@ -4,7 +4,6 @@ use Illuminate\Http\Request; use Illuminate\Routing\Controller; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\Date; use Laravel\Jetstream\Jetstream; use Laravel\Passport\Client; @@ -23,36 +22,20 @@ class OAuthAppController extends Controller public function index(Request $request) { return Jetstream::inertia()->render($request, 'OAuth/Index', [ - 'authorizedApps' => $request->user()->tokens() + 'connections' => $request->user()->tokens() ->with('client') ->where('revoked', false) ->where('expires_at', '>', Date::now()) ->get() - ->reduce(function (Collection $apps, Token $token) { - if ($token->client->revoked || $token->client->personal_access_client) { - return $apps; - } - - $app = $apps->get($token->client_id); - - if ($app) { - $app['scopes'] = array_unique(array_merge($app['scopes'], $token->scopes)); - $app['tokens_count'] += 1; - - $apps->put($token->client_id, $app); - } else { - $apps->put($token->client_id, [ - 'client' => $token->client, - 'scopes' => $token->scopes, - 'tokens_count' => 1, - ]); - } - - return $apps; - }, collect()), - 'oauthApps' => $request->user()->clients() + ->reject(fn (Token $token) => $token->client->revoked || $token->client->hasGrantType('personal_access')) + ->groupBy('client_id') + ->map(fn ($tokens) => [ + 'client' => $tokens->first->client, + 'scopes' => $tokens->pluck('scopes')->flatten()->unique()->all(), + 'tokens_count' => $tokens->count(), + ]), + 'apps' => $request->user()->clients() ->where('revoked', false) - ->orderBy('name', 'asc') ->get() ->map(fn (Client $client) => $client->toArray() + [ 'is_confidential' => $client->confidential(), @@ -102,7 +85,14 @@ public function update(Request $request, $clientId) */ public function destroy(Request $request, $clientId) { - $request->user()->clients()->find($clientId)->delete(); + $client = $request->user()->clients()->find($clientId); + + $client->tokens()->each(function (Token $token) { + $token->refreshToken->revoke(); + $token->revoke(); + }); + + $client->revoke(); return back(303); } diff --git a/src/Http/Controllers/Inertia/PassportApiTokenController.php b/src/Http/Controllers/Inertia/PassportApiTokenController.php index 308e9d200..6d2695f32 100644 --- a/src/Http/Controllers/Inertia/PassportApiTokenController.php +++ b/src/Http/Controllers/Inertia/PassportApiTokenController.php @@ -25,7 +25,7 @@ public function index(Request $request) ->where('revoked', false) ->where('expires_at', '>', Date::now()) ->get() - ->filter(fn (Token $token) => $token->client->personal_access_client) + ->filter(fn (Token $token) => $token->client->hasGrantType('personal_access')) ->map(fn (Token $token) => $token->toArray() + [ 'issued_ago' => $token->created_at->diffForHumans(), 'expires_in' => $token->expires_at->longAbsoluteDiffForHumans(), diff --git a/src/Http/Livewire/OAuthAppManager.php b/src/Http/Livewire/OAuthAppManager.php index 24689234d..5366d5470 100644 --- a/src/Http/Livewire/OAuthAppManager.php +++ b/src/Http/Livewire/OAuthAppManager.php @@ -2,9 +2,7 @@ namespace Laravel\Jetstream\Http\Livewire; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Date; use Illuminate\View\View; use Laravel\Passport\Client; use Laravel\Passport\Contracts\CreatesClients; @@ -16,18 +14,11 @@ class OAuthAppManager extends Component { /** - * The user's authorized apps. - * - * @var \Illuminate\Database\Eloquent\Collection - */ - public $authorizedApps; - - /** - * The user's OAuth apps. + * The user's registered OAuth apps. * * @var \Illuminate\Database\Eloquent\Collection */ - public $oauthApps; + public $apps; /** * Create OAuth app form state. @@ -53,16 +44,6 @@ class OAuthAppManager extends Component 'secret' => null, ]; - /** - * Indicates if the application is confirming if a authorized app should be revoked. - */ - public bool $confirmingAuthorizedAppRevocation = false; - - /** - * The ID of the authorized app being revoked. - */ - public string $authorizedAppIdBeingRevoked; - /** * Indicates if the user is currently managing an OAuth app. * @@ -102,8 +83,7 @@ class OAuthAppManager extends Component */ public function mount(): void { - $this->loadAuthorizedApps(); - $this->loadOAuthApps(); + $this->loadApps(); } /** @@ -124,50 +104,14 @@ public function getUserProperty() return Auth::user(); } - /** - * Load the user's authorized apps. - */ - #[On('authorized-app-revoked')] - public function loadAuthorizedApps(): void - { - $this->authorizedApps = $this->user->tokens() - ->with('client') - ->where('revoked', false) - ->where('expires_at', '>', Date::now()) - ->get() - ->reduce(function (Collection $apps, Token $token) { - if ($token->client->revoked || $token->client->personal_access_client) { - return $apps; - } - - $app = $apps->get($token->client_id); - - if ($app) { - $app['scopes'] = array_unique(array_merge($app['scopes'], $token->scopes)); - $app['tokens_count'] += 1; - - $apps->put($token->client_id, $app); - } else { - $apps->put($token->client_id, [ - 'client' => $token->client, - 'scopes' => $token->scopes, - 'tokens_count' => 1, - ]); - } - - return $apps; - }, collect()); - } - /** * Load the user's OAuth apps. */ - #[On(['oauth-app-created', 'oauth-app-updated', 'oauth-app-deleted'])] - public function loadOAuthApps(): void + #[On(['app-created', 'app-updated', 'app-deleted'])] + public function loadApps(): void { - $this->oauthApps = $this->user->clients() + $this->apps = $this->user->clients() ->where('revoked', false) - ->orderBy('name', 'asc') ->get(); } @@ -186,7 +130,7 @@ public function createOAuthApp(CreatesClients $creator): void $this->createOAuthAppForm['redirect_uri'] = ''; $this->createOAuthAppForm['confidential'] = false; - $this->dispatch('oauth-app-created'); + $this->dispatch('app-created'); } /** @@ -211,7 +155,7 @@ public function manageOAuthApp(string $clientId): void { $this->managingOAuthApp = true; - $this->oauthAppBeingManaged = $this->oauthApps->find($clientId); + $this->oauthAppBeingManaged = $this->apps->find($clientId); $this->updateOAuthAppForm['name'] = $this->oauthAppBeingManaged->name; $this->updateOAuthAppForm['redirect_uri'] = $this->oauthAppBeingManaged->redirect; @@ -224,7 +168,7 @@ public function updateOAuthApp(UpdatesClients $updater): void { $updater->update($this->oauthAppBeingManaged, $this->updateOAuthAppForm); - $this->dispatch('oauth-app-updated'); + $this->dispatch('app-updated'); $this->managingOAuthApp = false; } @@ -244,37 +188,17 @@ public function confirmOAuthAppDeletion(string $clientId): void */ public function deleteOAuthApp(): void { - $this->oauthApps->find($this->oauthAppIdBeingDeleted)->delete(); - - $this->dispatch('oauth-app-deleted'); - - $this->confirmingOAuthAppDeletion = false; - } - - /** - * Confirm that the given authorized app should be revoked. - */ - public function confirmAuthorizedAppRevocation(string $tokenId): void - { - $this->confirmingAuthorizedAppRevocation = true; + $app = $this->apps->find($this->oauthAppIdBeingDeleted); - $this->authorizedAppIdBeingRevoked = $tokenId; - } + $app->tokens()->each(function (Token $token) { + $token->refreshToken->revoke(); + $token->revoke(); + }); - /** - * Revoke the authorized app. - */ - public function revokeAuthorizedApp(): void - { - $this->user->tokens() - ->where('client_id', $this->authorizedAppIdBeingRevoked) - ->each(function (Token $token) { - $token->refreshToken->revoke(); - $token->revoke(); - }); + $app->revoke(); - $this->dispatch('authorized-app-revoked'); + $this->dispatch('app-deleted'); - $this->confirmingAuthorizedAppRevocation = false; + $this->confirmingOAuthAppDeletion = false; } } diff --git a/src/Http/Livewire/OAuthConnectionManager.php b/src/Http/Livewire/OAuthConnectionManager.php new file mode 100644 index 000000000..b3f5d60c9 --- /dev/null +++ b/src/Http/Livewire/OAuthConnectionManager.php @@ -0,0 +1,104 @@ + + */ + public $connections; + + /** + * Indicates if the application is confirming if a connection should be deleted. + */ + public bool $confirmingConnectionDeletion = false; + + /** + * The ID of the client its connection being deleted. + */ + public ?string $connectionClientIdBeingDeleted; + + /** + * Mount the component. + */ + public function mount(): void + { + $this->loadConnections(); + } + + /** + * Render the component. + */ + public function render(): View + { + return view('oauth.oauth-connection-manager'); + } + + /** + * Get the current user of the application. + * + * @return \Illuminate\Contracts\Auth\Authenticatable + */ + public function getUserProperty() + { + return Auth::user(); + } + + /** + * Load the user's connections with OAuth apps. + */ + #[On('connection-deleted')] + public function loadConnections(): void + { + $this->connections = $this->user->tokens() + ->with('client') + ->where('revoked', false) + ->where('expires_at', '>', Date::now()) + ->get() + ->reject(fn (Token $token) => $token->client->revoked || $token->client->hasGrantType('personal_access')) + ->groupBy('client_id') + ->map(fn ($tokens) => [ + 'client' => $tokens->first->client, + 'scopes' => $tokens->pluck('scopes')->flatten()->unique()->all(), + 'tokens_count' => $tokens->count(), + ]); + } + + /** + * Confirm that the given connection should be deleted. + */ + public function confirmConnectionDeletion(string $clientId): void + { + $this->confirmingConnectionDeletion = true; + + $this->connectionClientIdBeingDeleted = $clientId; + } + + /** + * Delete the connection with the OAuth app. + */ + public function deleteConnection(): void + { + $this->user->tokens() + ->where('client_id', $this->connectionClientIdBeingDeleted) + ->each(function (Token $token) { + $token->refreshToken->revoke(); + $token->revoke(); + }); + + $this->dispatch('connection-deleted'); + + $this->confirmingConnectionDeletion = false; + $this->connectionClientIdBeingDeleted = null; + } +} diff --git a/src/Http/Livewire/PassportApiTokenManager.php b/src/Http/Livewire/PassportApiTokenManager.php index 4fefb6ce9..ece7acb81 100644 --- a/src/Http/Livewire/PassportApiTokenManager.php +++ b/src/Http/Livewire/PassportApiTokenManager.php @@ -71,7 +71,7 @@ public function loadTokens(): void ->where('revoked', false) ->where('expires_at', '>', Date::now()) ->get() - ->filter(fn ($token) => $token->client->personal_access_client); + ->filter(fn ($token) => $token->client->hasGrantType('personal_access')); } /** diff --git a/src/JetstreamServiceProvider.php b/src/JetstreamServiceProvider.php index 3a737a629..280bdc1f4 100644 --- a/src/JetstreamServiceProvider.php +++ b/src/JetstreamServiceProvider.php @@ -21,6 +21,7 @@ use Laravel\Jetstream\Http\Livewire\LogoutOtherBrowserSessionsForm; use Laravel\Jetstream\Http\Livewire\NavigationMenu; use Laravel\Jetstream\Http\Livewire\OAuthAppManager; +use Laravel\Jetstream\Http\Livewire\OAuthConnectionManager; use Laravel\Jetstream\Http\Livewire\PassportApiTokenManager; use Laravel\Jetstream\Http\Livewire\TeamMemberManager; use Laravel\Jetstream\Http\Livewire\TwoFactorAuthenticationForm; @@ -99,6 +100,7 @@ public function boot() if (Features::hasOAuthFeatures()) { Livewire::component('oauth.oauth-app-manager', OAuthAppManager::class); + Livewire::component('oauth.oauth-connection-manager', OAuthConnectionManager::class); } if (Features::hasTeamFeatures()) { diff --git a/stubs/inertia/resources/js/Pages/OAuth/Index.vue b/stubs/inertia/resources/js/Pages/OAuth/Index.vue index 1f6066584..b4723289a 100644 --- a/stubs/inertia/resources/js/Pages/OAuth/Index.vue +++ b/stubs/inertia/resources/js/Pages/OAuth/Index.vue @@ -1,10 +1,11 @@ @@ -18,10 +19,9 @@ defineProps({
- + + +
diff --git a/stubs/inertia/resources/js/Pages/OAuth/Partials/OAuthAppManager.vue b/stubs/inertia/resources/js/Pages/OAuth/Partials/OAuthAppManager.vue index 30c14d384..fe95682e3 100644 --- a/stubs/inertia/resources/js/Pages/OAuth/Partials/OAuthAppManager.vue +++ b/stubs/inertia/resources/js/Pages/OAuth/Partials/OAuthAppManager.vue @@ -16,8 +16,7 @@ import SectionBorder from '@/Components/SectionBorder.vue'; import TextInput from '@/Components/TextInput.vue'; const props = defineProps({ - authorizedApps: Array, - oauthApps: Array, + apps: Array, }); const createOAuthAppForm = useForm({ @@ -33,12 +32,9 @@ const updateOAuthAppForm = useForm({ const deleteOAuthAppForm = useForm({}); -const revokeAuthorizedAppForm = useForm({}); - const displayingClientCredentials = ref(false); const oauthAppBeingManaged = ref(null); const oauthAppBeingDeleted = ref(null); -const authorizedAppBeingRevoked = ref(null); const createOAuthApp = () => { createOAuthAppForm.post(route('oauth-apps.store'), { @@ -75,65 +71,10 @@ const deleteOAuthApp = () => { onSuccess: () => (oauthAppBeingDeleted.value = null), }); }; - -const confirmAuthorizedAppRevocation = (app) => { - authorizedAppBeingRevoked.value = app; -}; - -const revokeAuthorizedApp = () => { - revokeAuthorizedAppForm.delete(route('oauth-connections.destroy', authorizedAppBeingRevoked.value), { - preserveScroll: true, - preserveState: true, - onSuccess: () => (authorizedAppBeingRevoked.value = null), - }); -}; diff --git a/stubs/inertia/resources/js/Pages/OAuth/Partials/OAuthConnectionManager.vue b/stubs/inertia/resources/js/Pages/OAuth/Partials/OAuthConnectionManager.vue new file mode 100644 index 000000000..cdb4d1de8 --- /dev/null +++ b/stubs/inertia/resources/js/Pages/OAuth/Partials/OAuthConnectionManager.vue @@ -0,0 +1,102 @@ + + + diff --git a/stubs/livewire/resources/views/oauth/index.blade.php b/stubs/livewire/resources/views/oauth/index.blade.php index 12e86d994..88794827b 100644 --- a/stubs/livewire/resources/views/oauth/index.blade.php +++ b/stubs/livewire/resources/views/oauth/index.blade.php @@ -7,6 +7,8 @@
+ @livewire('oauth.oauth-connection-manager') + @livewire('oauth.oauth-app-manager')
diff --git a/stubs/livewire/resources/views/oauth/oauth-app-manager.blade.php b/stubs/livewire/resources/views/oauth/oauth-app-manager.blade.php index e9f4412d2..8ee1b257f 100644 --- a/stubs/livewire/resources/views/oauth/oauth-app-manager.blade.php +++ b/stubs/livewire/resources/views/oauth/oauth-app-manager.blade.php @@ -1,49 +1,4 @@
- @if ($this->authorizedApps->isNotEmpty()) - -
- - - {{ __('Manage Authorized Apps') }} - - - - {{ __('Keep track of your connections to third-party apps and services.') }} - - - - -
- @foreach ($this->authorizedApps as $id => $app) -
-
-
- {{ $app['client']->name }} -
-
- {{ implode(', ', $app['scopes']) }} -
-
- -
-
- {{ $app['tokens_count'] }} {{ __('Tokens') }} -
- - -
-
- @endforeach -
-
-
-
- - - @endif - @@ -80,7 +35,7 @@ - + {{ __('Registered.') }} @@ -90,7 +45,7 @@ - @if ($this->oauthApps->isNotEmpty()) + @if ($this->apps->isNotEmpty()) @@ -107,7 +62,7 @@
- @foreach ($this->oauthApps as $app) + @foreach ($this->apps as $app)
{{ $app->name }} @@ -244,25 +199,4 @@ class="mt-1 bg-gray-100 px-4 py-2 rounded font-mono text-sm text-gray-500 w-full - - - - - {{ __('Revoke Authorized App') }} - - - - {{ __('Are you sure you would like to revoke this app?') }} - - - - - {{ __('Cancel') }} - - - - {{ __('Revoke') }} - - -
diff --git a/stubs/livewire/resources/views/oauth/oauth-connection-manager.blade.php b/stubs/livewire/resources/views/oauth/oauth-connection-manager.blade.php new file mode 100644 index 000000000..915a3b32d --- /dev/null +++ b/stubs/livewire/resources/views/oauth/oauth-connection-manager.blade.php @@ -0,0 +1,67 @@ +
+ @if ($this->authorizedApps->isNotEmpty()) + +
+ + + {{ __('Manage Authorized Apps') }} + + + + {{ __('Keep track of your connections with third-party apps and services. You may delete the access you\'ve given to any of your existing authorized apps if they are no longer needed.') }} + + + + +
+ @foreach ($this->connections as $id => $connection) +
+
+
+ {{ $connection['client']->name }} +
+
+ {{ implode(', ', $connection['scopes']) }} +
+
+ +
+
+ {{ $connection['tokens_count'] }} {{ __('Tokens') }} +
+ + +
+
+ @endforeach +
+
+
+
+ + + @endif + + + + + {{ __('Delete OAuth Connection') }} + + + + {{ __('Are you sure you would like to delete all connections you have with this app?') }} + + + + + {{ __('Cancel') }} + + + + {{ __('Delete') }} + + + +
From a38a60db2badfdec713afca71f62d9b64f01ae5d Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Fri, 2 Aug 2024 01:51:01 +0330 Subject: [PATCH 05/20] wip --- src/Http/Controllers/Inertia/OAuthAppController.php | 2 +- src/Http/Livewire/OAuthConnectionManager.php | 2 +- .../resources/views/oauth/oauth-connection-manager.blade.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Http/Controllers/Inertia/OAuthAppController.php b/src/Http/Controllers/Inertia/OAuthAppController.php index 1fcfb1d85..bfe9ff520 100644 --- a/src/Http/Controllers/Inertia/OAuthAppController.php +++ b/src/Http/Controllers/Inertia/OAuthAppController.php @@ -30,7 +30,7 @@ public function index(Request $request) ->reject(fn (Token $token) => $token->client->revoked || $token->client->hasGrantType('personal_access')) ->groupBy('client_id') ->map(fn ($tokens) => [ - 'client' => $tokens->first->client, + 'client' => $tokens->first()->client, 'scopes' => $tokens->pluck('scopes')->flatten()->unique()->all(), 'tokens_count' => $tokens->count(), ]), diff --git a/src/Http/Livewire/OAuthConnectionManager.php b/src/Http/Livewire/OAuthConnectionManager.php index b3f5d60c9..bcc7ec3a8 100644 --- a/src/Http/Livewire/OAuthConnectionManager.php +++ b/src/Http/Livewire/OAuthConnectionManager.php @@ -68,7 +68,7 @@ public function loadConnections(): void ->reject(fn (Token $token) => $token->client->revoked || $token->client->hasGrantType('personal_access')) ->groupBy('client_id') ->map(fn ($tokens) => [ - 'client' => $tokens->first->client, + 'client' => $tokens->first()->client, 'scopes' => $tokens->pluck('scopes')->flatten()->unique()->all(), 'tokens_count' => $tokens->count(), ]); diff --git a/stubs/livewire/resources/views/oauth/oauth-connection-manager.blade.php b/stubs/livewire/resources/views/oauth/oauth-connection-manager.blade.php index 915a3b32d..6818b08e5 100644 --- a/stubs/livewire/resources/views/oauth/oauth-connection-manager.blade.php +++ b/stubs/livewire/resources/views/oauth/oauth-connection-manager.blade.php @@ -1,5 +1,5 @@
- @if ($this->authorizedApps->isNotEmpty()) + @if ($this->connections->isNotEmpty())
From 160ab84a2bbb01b37e6dcd8c6b74dbdb49ee701b Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Fri, 2 Aug 2024 06:30:56 +0330 Subject: [PATCH 06/20] wip --- src/Http/Livewire/OAuthAppManager.php | 8 ++++---- .../js/Pages/OAuth/Partials/OAuthAppManager.vue | 14 +++++++------- .../views/oauth/oauth-app-manager.blade.php | 8 ++++---- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Http/Livewire/OAuthAppManager.php b/src/Http/Livewire/OAuthAppManager.php index 5366d5470..5de00545e 100644 --- a/src/Http/Livewire/OAuthAppManager.php +++ b/src/Http/Livewire/OAuthAppManager.php @@ -27,7 +27,7 @@ class OAuthAppManager extends Component */ public array $createOAuthAppForm = [ 'name' => '', - 'redirect_uri' => '', + 'redirect_uris' => [], 'confidential' => false, ]; @@ -65,7 +65,7 @@ class OAuthAppManager extends Component */ public array $updateOAuthAppForm = [ 'name' => '', - 'redirect_uri' => '', + 'redirect_uris' => [], ]; /** @@ -127,7 +127,7 @@ public function createOAuthApp(CreatesClients $creator): void ); $this->createOAuthAppForm['name'] = ''; - $this->createOAuthAppForm['redirect_uri'] = ''; + $this->createOAuthAppForm['redirect_uris'] = []; $this->createOAuthAppForm['confidential'] = false; $this->dispatch('app-created'); @@ -158,7 +158,7 @@ public function manageOAuthApp(string $clientId): void $this->oauthAppBeingManaged = $this->apps->find($clientId); $this->updateOAuthAppForm['name'] = $this->oauthAppBeingManaged->name; - $this->updateOAuthAppForm['redirect_uri'] = $this->oauthAppBeingManaged->redirect; + $this->updateOAuthAppForm['redirect_uris'] = explode(',', $this->oauthAppBeingManaged->redirect); } /** diff --git a/stubs/inertia/resources/js/Pages/OAuth/Partials/OAuthAppManager.vue b/stubs/inertia/resources/js/Pages/OAuth/Partials/OAuthAppManager.vue index fe95682e3..f5218836b 100644 --- a/stubs/inertia/resources/js/Pages/OAuth/Partials/OAuthAppManager.vue +++ b/stubs/inertia/resources/js/Pages/OAuth/Partials/OAuthAppManager.vue @@ -21,13 +21,13 @@ const props = defineProps({ const createOAuthAppForm = useForm({ name: '', - redirect_uri: '', + redirect_uris: [], confidential: false, }); const updateOAuthAppForm = useForm({ name: '', - redirect_uri: '', + redirect_uris: [], }); const deleteOAuthAppForm = useForm({}); @@ -48,7 +48,7 @@ const createOAuthApp = () => { const manageOAuthApp = (app) => { updateOAuthAppForm.name = app.name; - updateOAuthAppForm.redirect_uri = app.redirect; + updateOAuthAppForm.redirect_uris = app.redirect.split(','); oauthAppBeingManaged.value = app; }; @@ -105,12 +105,12 @@ const deleteOAuthApp = () => { - +
@@ -263,12 +263,12 @@ const deleteOAuthApp = () => { - +
diff --git a/stubs/livewire/resources/views/oauth/oauth-app-manager.blade.php b/stubs/livewire/resources/views/oauth/oauth-app-manager.blade.php index 8ee1b257f..9f3ccea5e 100644 --- a/stubs/livewire/resources/views/oauth/oauth-app-manager.blade.php +++ b/stubs/livewire/resources/views/oauth/oauth-app-manager.blade.php @@ -20,8 +20,8 @@
- - + +
@@ -183,8 +183,8 @@ class="mt-1 bg-gray-100 px-4 py-2 rounded font-mono text-sm text-gray-500 w-full
- - + +
From bbd8f127f1f7c4eefa6f9e0ba7ccbbb344defd04 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Fri, 2 Aug 2024 11:13:49 +0330 Subject: [PATCH 07/20] add authorize view stubs --- .../js/Pages/Auth/OAuth/Authorize.vue | 80 +++++++++++++++++++ .../views/auth/oauth/authorize.blade.php | 59 ++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 stubs/inertia/resources/js/Pages/Auth/OAuth/Authorize.vue create mode 100644 stubs/livewire/resources/views/auth/oauth/authorize.blade.php diff --git a/stubs/inertia/resources/js/Pages/Auth/OAuth/Authorize.vue b/stubs/inertia/resources/js/Pages/Auth/OAuth/Authorize.vue new file mode 100644 index 000000000..8b6a7b105 --- /dev/null +++ b/stubs/inertia/resources/js/Pages/Auth/OAuth/Authorize.vue @@ -0,0 +1,80 @@ + + + diff --git a/stubs/livewire/resources/views/auth/oauth/authorize.blade.php b/stubs/livewire/resources/views/auth/oauth/authorize.blade.php new file mode 100644 index 000000000..186b5baec --- /dev/null +++ b/stubs/livewire/resources/views/auth/oauth/authorize.blade.php @@ -0,0 +1,59 @@ + + + + + + +
+

{{ $user->name }}

+

{{ $user->email }}

+
+ +
+ {{ __(':client is requesting permission to access your account.', ['client' => $client->name]) }} +
+ + @if (count($scopes) > 0) +
+

{{ __('This application will be able to:') }}

+ +
    + @foreach ($scopes as $scope) +
  • {{ $scope->description }}
  • + @endforeach +
+
+ @endif + +
+ + @csrf + + + + + + + {{ __('Authorize') }} + + + +
+ @csrf + @method('DELETE') + + + + + + + {{ __('Decline') }} + +
+ + + {{ __('Log into another account') }} + +
+
+
From 69326adec92df8ba8823fb6f639648a8689d0a3c Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Sat, 3 Aug 2024 07:49:08 +0330 Subject: [PATCH 08/20] wip --- src/JetstreamServiceProvider.php | 18 ++++++++++++++++++ .../js/Pages/Auth/OAuth/Authorize.vue | 14 ++++++-------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/JetstreamServiceProvider.php b/src/JetstreamServiceProvider.php index 280bdc1f4..5257db090 100644 --- a/src/JetstreamServiceProvider.php +++ b/src/JetstreamServiceProvider.php @@ -29,6 +29,7 @@ use Laravel\Jetstream\Http\Livewire\UpdateProfileInformationForm; use Laravel\Jetstream\Http\Livewire\UpdateTeamNameForm; use Laravel\Jetstream\Http\Middleware\ShareInertiaData; +use Laravel\Passport\Passport; use Livewire\Livewire; class JetstreamServiceProvider extends ServiceProvider @@ -52,6 +53,10 @@ public function boot() { Fortify::viewPrefix('auth.'); + if (class_exists(Passport::class)) { + Passport::viewPrefix('auth.oauth.'); + } + $this->configurePublishing(); $this->configureRoutes(); $this->configureCommands(); @@ -242,5 +247,18 @@ protected function bootInertia() Fortify::confirmPasswordView(function () { return Inertia::render('Auth/ConfirmPassword'); }); + + if (class_exists(Passport::class)) { + Passport::authorizationView(function ($params) { + return Inertia::render('Auth/OAuth/Authorize', [ + 'user' => $params['user'], + 'client' => $params['client'], + 'scopes' => $params['scopes'], + 'state' => $params['request']->state, + 'authToken' => $params['authToken'], + 'promptLoginUrl' => $params['request']->fullUrlWithQuery(['prompt' => 'login']), + ]); + }); + } } } diff --git a/stubs/inertia/resources/js/Pages/Auth/OAuth/Authorize.vue b/stubs/inertia/resources/js/Pages/Auth/OAuth/Authorize.vue index 8b6a7b105..421ffa74d 100644 --- a/stubs/inertia/resources/js/Pages/Auth/OAuth/Authorize.vue +++ b/stubs/inertia/resources/js/Pages/Auth/OAuth/Authorize.vue @@ -6,10 +6,8 @@ import PrimaryButton from '@/Components/PrimaryButton.vue'; import SecondaryButton from '@/Components/SecondaryButton.vue'; const props = defineProps({ - userName: String, - userEmail: String, - clientId: String, - clientName: String, + user: Object, + client: Object, scopes: Array, state: String, authToken: String, @@ -18,7 +16,7 @@ const props = defineProps({ const form = useForm({ state: props.state, - client_id: props.clientId, + client_id: props.client.id, auth_token: props.authToken, }); @@ -42,12 +40,12 @@ const deny = () => {
-

{{ userName }}

-

{{ userEmail }}

+

{{ user.name }}

+

{{ user.email }}

- {{ clientName }} is requesting permission to access your account. + {{ client.name }} is requesting permission to access your account.
From c7c214b940230f2b77e8158af5023e56c3b5f0c6 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Sun, 4 Aug 2024 20:46:17 +0330 Subject: [PATCH 09/20] wip --- src/Http/Controllers/Inertia/OAuthAppController.php | 6 +++--- src/Http/Controllers/Inertia/OAuthConnectionController.php | 4 ++-- src/Http/Livewire/OAuthAppManager.php | 6 +++--- src/Http/Livewire/OAuthConnectionManager.php | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Http/Controllers/Inertia/OAuthAppController.php b/src/Http/Controllers/Inertia/OAuthAppController.php index bfe9ff520..55c3a6c7a 100644 --- a/src/Http/Controllers/Inertia/OAuthAppController.php +++ b/src/Http/Controllers/Inertia/OAuthAppController.php @@ -88,11 +88,11 @@ public function destroy(Request $request, $clientId) $client = $request->user()->clients()->find($clientId); $client->tokens()->each(function (Token $token) { - $token->refreshToken->revoke(); - $token->revoke(); + $token->refreshToken()->delete(); + $token->delete(); }); - $client->revoke(); + $client->delete(); return back(303); } diff --git a/src/Http/Controllers/Inertia/OAuthConnectionController.php b/src/Http/Controllers/Inertia/OAuthConnectionController.php index f460804de..1f1b14a20 100644 --- a/src/Http/Controllers/Inertia/OAuthConnectionController.php +++ b/src/Http/Controllers/Inertia/OAuthConnectionController.php @@ -20,8 +20,8 @@ public function destroy(Request $request, $clientId) $request->user()->tokens() ->where('client_id', $clientId) ->each(function (Token $token) { - $token->refreshToken->revoke(); - $token->revoke(); + $token->refreshToken()->delete(); + $token->delete(); }); return back(303); diff --git a/src/Http/Livewire/OAuthAppManager.php b/src/Http/Livewire/OAuthAppManager.php index 5de00545e..14735a313 100644 --- a/src/Http/Livewire/OAuthAppManager.php +++ b/src/Http/Livewire/OAuthAppManager.php @@ -191,11 +191,11 @@ public function deleteOAuthApp(): void $app = $this->apps->find($this->oauthAppIdBeingDeleted); $app->tokens()->each(function (Token $token) { - $token->refreshToken->revoke(); - $token->revoke(); + $token->refreshToken()->delete(); + $token->delete(); }); - $app->revoke(); + $app->delete(); $this->dispatch('app-deleted'); diff --git a/src/Http/Livewire/OAuthConnectionManager.php b/src/Http/Livewire/OAuthConnectionManager.php index bcc7ec3a8..46cce596e 100644 --- a/src/Http/Livewire/OAuthConnectionManager.php +++ b/src/Http/Livewire/OAuthConnectionManager.php @@ -92,8 +92,8 @@ public function deleteConnection(): void $this->user->tokens() ->where('client_id', $this->connectionClientIdBeingDeleted) ->each(function (Token $token) { - $token->refreshToken->revoke(); - $token->revoke(); + $token->refreshToken()->delete(); + $token->delete(); }); $this->dispatch('connection-deleted'); From 140bbc011c4f4d4b4374990521f4360fd62590e2 Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Mon, 5 Aug 2024 01:21:02 +0330 Subject: [PATCH 10/20] wip --- .../Inertia/PassportApiTokenController.php | 2 +- src/Http/Livewire/PassportApiTokenManager.php | 2 +- .../Pages/PassportAPI/Partials/ApiTokenManager.vue | 12 ++++++------ .../views/passport-api/api-token-manager.blade.php | 12 ++++++------ 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Http/Controllers/Inertia/PassportApiTokenController.php b/src/Http/Controllers/Inertia/PassportApiTokenController.php index 6d2695f32..4fec48867 100644 --- a/src/Http/Controllers/Inertia/PassportApiTokenController.php +++ b/src/Http/Controllers/Inertia/PassportApiTokenController.php @@ -66,7 +66,7 @@ public function store(Request $request) */ public function destroy(Request $request, $tokenId) { - $request->user()->tokens()->find($tokenId)->revoke(); + $request->user()->tokens()->find($tokenId)->delete(); return back(303); } diff --git a/src/Http/Livewire/PassportApiTokenManager.php b/src/Http/Livewire/PassportApiTokenManager.php index ece7acb81..3f94980d4 100644 --- a/src/Http/Livewire/PassportApiTokenManager.php +++ b/src/Http/Livewire/PassportApiTokenManager.php @@ -125,7 +125,7 @@ public function confirmApiTokenDeletion(string $tokenId): void */ public function deleteApiToken(): void { - $this->tokens->find($this->apiTokenIdBeingDeleted)->revoke(); + $this->tokens->find($this->apiTokenIdBeingDeleted)->delete(); $this->dispatch('token-deleted'); diff --git a/stubs/inertia/resources/js/Pages/PassportAPI/Partials/ApiTokenManager.vue b/stubs/inertia/resources/js/Pages/PassportAPI/Partials/ApiTokenManager.vue index da5663019..546e268fd 100644 --- a/stubs/inertia/resources/js/Pages/PassportAPI/Partials/ApiTokenManager.vue +++ b/stubs/inertia/resources/js/Pages/PassportAPI/Partials/ApiTokenManager.vue @@ -122,7 +122,7 @@ const deleteApiToken = () => { @@ -148,7 +148,7 @@ const deleteApiToken = () => {
@@ -181,14 +181,14 @@ const deleteApiToken = () => { - + diff --git a/stubs/livewire/resources/views/passport-api/api-token-manager.blade.php b/stubs/livewire/resources/views/passport-api/api-token-manager.blade.php index af07b3a2b..36ab325d4 100644 --- a/stubs/livewire/resources/views/passport-api/api-token-manager.blade.php +++ b/stubs/livewire/resources/views/passport-api/api-token-manager.blade.php @@ -58,7 +58,7 @@ - {{ __('You may revoke any of your existing personal access tokens if they are no longer needed.') }} + {{ __('You may delete any of your existing personal access tokens if they are no longer needed.') }} @@ -85,7 +85,7 @@ @@ -121,14 +121,14 @@ class="mt-4 bg-gray-100 px-4 py-2 rounded font-mono text-sm text-gray-500 w-full - + - {{ __('Revoke API Token') }} + {{ __('Delete API Token') }} - {{ __('Are you sure you would like to revoke this API token?') }} + {{ __('Are you sure you would like to delete this API token?') }} @@ -137,7 +137,7 @@ class="mt-4 bg-gray-100 px-4 py-2 rounded font-mono text-sm text-gray-500 w-full - {{ __('Revoke') }} + {{ __('Delete') }} From 3322aea3aa941ba8bd1939c4b58fe1d170207f2b Mon Sep 17 00:00:00 2001 From: Hafez Divandari Date: Mon, 5 Aug 2024 16:07:33 +0330 Subject: [PATCH 11/20] wip --- src/Http/Controllers/Inertia/OAuthAppController.php | 5 +++-- src/Http/Livewire/OAuthConnectionManager.php | 5 +++-- .../js/Pages/OAuth/Partials/OAuthConnectionManager.vue | 2 +- .../views/oauth/oauth-connection-manager.blade.php | 6 +++--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Http/Controllers/Inertia/OAuthAppController.php b/src/Http/Controllers/Inertia/OAuthAppController.php index 55c3a6c7a..01718f496 100644 --- a/src/Http/Controllers/Inertia/OAuthAppController.php +++ b/src/Http/Controllers/Inertia/OAuthAppController.php @@ -31,9 +31,10 @@ public function index(Request $request) ->groupBy('client_id') ->map(fn ($tokens) => [ 'client' => $tokens->first()->client, - 'scopes' => $tokens->pluck('scopes')->flatten()->unique()->all(), + 'scopes' => $tokens->pluck('scopes')->flatten()->unique()->values()->all(), 'tokens_count' => $tokens->count(), - ]), + ]) + ->values(), 'apps' => $request->user()->clients() ->where('revoked', false) ->get() diff --git a/src/Http/Livewire/OAuthConnectionManager.php b/src/Http/Livewire/OAuthConnectionManager.php index 46cce596e..ad3caf232 100644 --- a/src/Http/Livewire/OAuthConnectionManager.php +++ b/src/Http/Livewire/OAuthConnectionManager.php @@ -69,9 +69,10 @@ public function loadConnections(): void ->groupBy('client_id') ->map(fn ($tokens) => [ 'client' => $tokens->first()->client, - 'scopes' => $tokens->pluck('scopes')->flatten()->unique()->all(), + 'scopes' => $tokens->pluck('scopes')->flatten()->unique()->values()->all(), 'tokens_count' => $tokens->count(), - ]); + ]) + ->values(); } /** diff --git a/stubs/inertia/resources/js/Pages/OAuth/Partials/OAuthConnectionManager.vue b/stubs/inertia/resources/js/Pages/OAuth/Partials/OAuthConnectionManager.vue index cdb4d1de8..b28c072f5 100644 --- a/stubs/inertia/resources/js/Pages/OAuth/Partials/OAuthConnectionManager.vue +++ b/stubs/inertia/resources/js/Pages/OAuth/Partials/OAuthConnectionManager.vue @@ -45,7 +45,7 @@ const deleteConnection = () => {