diff --git a/composer.json b/composer.json index 3e25506..3640bbd 100644 --- a/composer.json +++ b/composer.json @@ -30,6 +30,7 @@ "illuminate/validation": "^12.0", "illuminate/view": "^12.0", "intonate/tinker-zero": "^1.2", + "laracord/socialite-discord": "dev-next", "laravel-zero/framework": "^12.0", "laravel/sanctum": "^4.0", "react/async": "^4.2", diff --git a/config/discord.php b/config/discord.php index 4de2fd3..bd30677 100644 --- a/config/discord.php +++ b/config/discord.php @@ -17,6 +17,20 @@ 'token' => env('DISCORD_TOKEN', ''), + /* + |-------------------------------------------------------------------------- + | Discord Client Secret + |-------------------------------------------------------------------------- + | + | This is the client secret associated with your Discord application. + | It is used during the OAuth2 authorization process in conjunction + | with the client ID. Keep this secret safe and never expose it + | publicly, as it could be used to impersonate your application. + | + */ + + 'secret' => env('DISCORD_CLIENT_SECRET', ''), + /* |-------------------------------------------------------------------------- | Gateway Intents diff --git a/src/Auth/DiscordAuthenticatable.php b/src/Auth/DiscordAuthenticatable.php new file mode 100644 index 0000000..6fca6cd --- /dev/null +++ b/src/Auth/DiscordAuthenticatable.php @@ -0,0 +1,83 @@ +discord_id; + } + + /** + * Get the name of the password attribute for the user. + * Since there are no passwords, we can return null or an empty string. + * + * @return string + */ + public function getAuthPasswordName() + { + return null; + } + + /** + * Get the password for the user. + * Since there are no passwords, return null to ensure no password-based auth. + * + * @return string|null + */ + public function getAuthPassword() + { + return null; + } + + /** + * Get the token value for the "remember me" session. + * Not using remember tokens for Discord-only auth. + * + * @return string|null + */ + public function getRememberToken() + { + return null; + } + + /** + * Set the token value for the "remember me" session. + * Not using remember tokens for Discord-only auth. + * + * @param string $value + * @return void + */ + public function setRememberToken($value) + { + // + } + + /** + * Get the column name for the "remember me" token. + * Not using remember tokens for Discord-only auth. + * + * @return string|null + */ + public function getRememberTokenName() + { + return null; + } +} \ No newline at end of file diff --git a/src/Bot/Concerns/HasDiscordAuth.php b/src/Bot/Concerns/HasDiscordAuth.php new file mode 100644 index 0000000..e9ba968 --- /dev/null +++ b/src/Bot/Concerns/HasDiscordAuth.php @@ -0,0 +1,101 @@ +discordAuthScopes = $discordAuthScopes; + + return $this; + } + + /** + * Get the discord Oauth2 scopes. + */ + public function getDiscordAuthScopes(): array + { + return $this->discordAuthScopes; + } + + /** + * Set the redirect URL after successful Discord authentication. + */ + public function withDiscordLoggedInRedirect(string $url): self + { + $this->discordLoggedInRedirect = $url; + + return $this; + } + + /** + * Get the redirect URL after successful Discord authentication. + */ + public function getDiscordLoggedInRedirect(): string + { + return $this->discordLoggedInRedirect; + } + + /** + * Set the redirect URL after failed Discord authentication. + */ + public function withDiscordLoginFailedRedirect(string $url): self + { + $this->discordLogInFailedRedirect = $url; + + return $this; + } + + /** + * Get the redirect URL after failed Discord authentication. + */ + public function getDiscordLoginFailedRedirect(): string + { + return $this->discordLogInFailedRedirect; + } + + /** + * Set the closure that should be executed after a successful Discord login. + */ + public function afterDiscordAuthCallback(?Closure $afterDiscordAuthCallback): self + { + $this->afterDiscordAuthCallback = $afterDiscordAuthCallback; + + return $this; + } + + /** + * Get the closure that should be executed after a successful Discord login. + */ + public function getAfterDiscordAuthCallback(): ?Closure + { + return $this->afterDiscordAuthCallback; + } +} diff --git a/src/Http/Controllers/DiscordCallbackController.php b/src/Http/Controllers/DiscordCallbackController.php new file mode 100644 index 0000000..96808a8 --- /dev/null +++ b/src/Http/Controllers/DiscordCallbackController.php @@ -0,0 +1,44 @@ +setRequest($request) + ->user(); + + $model = app('bot')->getUserModel(); + + if (! class_exists($model)) { + return redirect()->to(app('bot')->getDiscordLoginFailedRedirect()); + } + + $user = $model::updateOrCreate( + ['discord_id' => $discordUser->getId()], + [ + 'username' => $discordUser->getName() ?? $discordUser->getNickname(), + ] + ); + + $afterDiscordAuthCallback = app('bot')->getAfterDiscordAuthCallback(); + if (is_callable($afterDiscordAuthCallback)) { + $afterDiscordAuthCallback($user, $discordUser); + } + + auth()->login($user); + + return redirect()->to(app('bot')->getDiscordLoggedInRedirect()); + } catch (Exception) { + return redirect()->to(app('bot')->getDiscordLoginFailedRedirect()); + } + } +} diff --git a/src/Http/Controllers/DiscordLoginController.php b/src/Http/Controllers/DiscordLoginController.php new file mode 100644 index 0000000..6bd64c6 --- /dev/null +++ b/src/Http/Controllers/DiscordLoginController.php @@ -0,0 +1,17 @@ +setRequest($request) + ->scopes(app('bot')->getDiscordAuthScopes()) + ->redirect(); + } +} diff --git a/src/Http/Routing/UrlGenerator.php b/src/Http/Routing/UrlGenerator.php new file mode 100644 index 0000000..ec18e21 --- /dev/null +++ b/src/Http/Routing/UrlGenerator.php @@ -0,0 +1,21 @@ +registerLoop(); $this->registerConsole(); $this->registerLogger(); + $this->registerUrlGenerator(); $this->app->singleton(KernelContract::class, Kernel::class); $this->app->singleton(Middleware::class, fn () => new Middleware); @@ -116,7 +124,22 @@ public function register() $this->app->singleton(Message::class, fn () => Message::make($bot)); - return $this->bot($bot); + return $this->bot($bot) + ->registerHook(Hook::AFTER_HTTP_SERVER_START, function (Laracord $bot) { + config([ + 'services.discord' => [ + 'client_id' => $bot->discord()->id, + 'client_secret' => config('discord.secret'), + 'redirect' => route('oauth.discord.callback'), + ], + ]); + }) + ->withRoutes(function (Router $router) { + Route::middleware('web')->group(function() { + Route::get('/oauth/discord/login', DiscordLoginController::class)->name('oauth.discord.login'); + Route::get('/oauth/discord/callback', DiscordCallbackController::class)->name('oauth.discord.callback'); + }); + }); })); $this->app->alias(Laracord::class, 'bot'); @@ -236,6 +259,19 @@ protected function registerLogger(): void $this->app->booting(fn () => $this->app->register(LogProvider::class)); } + /** + * Register the URL generator. + */ + protected function registerUrlGenerator(): void + { + $this->app->bind(UrlGenerator::class, function ($app) { + $request = $app->has('request') ? $app->make('request') : null; + return new LaracordUrlGenerator($request); + }); + + $this->app->alias(UrlGenerator::class, 'url'); + } + /** * Register the default components. */