From e50e67e2abca0ee43d01d7f4246b8976f7450784 Mon Sep 17 00:00:00 2001 From: roble Date: Sat, 25 Apr 2026 21:58:37 +0100 Subject: [PATCH 01/12] feat(auth): implement authentication features including login, registration, password reset, and social login - Add LoginController for handling user login and session management. - Introduce MagicLinkController for magic link authentication. - Create PasswordController for updating user passwords. - Implement RegisterController for user registration and role assignment. - Add ReimpersonateController for re-impersonating users. - Create ResetPasswordController for handling password reset requests. - Implement SocialiteController for social authentication and account linking. - Add VerifyEmailController for email verification. - Create middleware to ensure magic link functionality is enabled. - Implement LoginRequest and RegisterRequest for request validation. - Add listeners for user registration, impersonation events, and last login updates. - Create models for MagicLinkToken and SocialAccount to manage tokens and social accounts. - Implement notifications for magic link and welcome messages. - Register AuthServiceProvider to share authentication data with Inertia. - Create SocialiteService for handling social authentication logic. - Add Sociable trait to manage user social accounts. --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 2 +- CLAUDE.md | 2 +- Taskfile.yml | 4 +- app/Providers/RouteServiceProvider.php | 17 --- composer.json | 13 +- database/seeders/DatabaseSeeder.php | 13 ++ module.json | 15 --- routes/api.php | 6 +- routes/web.php | 126 +++++++++--------- {app => src}/Exceptions/AuthException.php | 0 .../Exceptions/SocialiteException.php | 0 {app => src}/Filament/AuthPlugin.php | 0 .../Resources/Users/Pages/CreateUser.php | 0 .../Resources/Users/Pages/EditUser.php | 0 .../Resources/Users/Pages/ListUsers.php | 0 .../Resources/Users/Pages/ViewUser.php | 0 .../Resources/Users/Schemas/UserForm.php | 0 .../Resources/Users/Schemas/UserInfolist.php | 0 .../Resources/Users/Tables/UsersTable.php | 0 .../Filament/Resources/Users/UserResource.php | 0 {app => src}/Http/Controllers/Controller.php | 0 ...mailVerificationNotificationController.php | 0 .../EmailVerificationPromptController.php | 0 .../Controllers/ForgotPasswordController.php | 0 .../Http/Controllers/LoginController.php | 0 .../Http/Controllers/MagicLinkController.php | 0 .../Http/Controllers/PasswordController.php | 0 .../Http/Controllers/RegisterController.php | 0 .../Controllers/ReimpersonateController.php | 0 .../Controllers/ResetPasswordController.php | 0 .../Http/Controllers/SocialiteController.php | 0 .../Controllers/VerifyEmailController.php | 0 .../Middleware/EnsureMagicLinkEnabled.php | 0 {app => src}/Http/Requests/LoginRequest.php | 0 .../Http/Requests/RegisterRequest.php | 0 {app => src}/Listeners/AssignUserRole.php | 0 {app => src}/Listeners/Impersonation.php | 0 .../Listeners/SendWelcomeNotification.php | 0 .../Listeners/UpdateUserLastLogin.php | 0 {app => src}/Models/MagicLinkToken.php | 0 {app => src}/Models/SocialAccount.php | 0 .../Notifications/MagicLinkNotification.php | 0 .../Notifications/WelcomeNotification.php | 0 .../Providers/AuthServiceProvider.php | 15 +-- {app => src}/Services/SocialiteService.php | 0 {app => src}/Traits/Sociable.php | 0 47 files changed, 96 insertions(+), 119 deletions(-) delete mode 100644 app/Providers/RouteServiceProvider.php create mode 100644 database/seeders/DatabaseSeeder.php delete mode 100644 module.json rename {app => src}/Exceptions/AuthException.php (100%) rename {app => src}/Exceptions/SocialiteException.php (100%) rename {app => src}/Filament/AuthPlugin.php (100%) rename {app => src}/Filament/Resources/Users/Pages/CreateUser.php (100%) rename {app => src}/Filament/Resources/Users/Pages/EditUser.php (100%) rename {app => src}/Filament/Resources/Users/Pages/ListUsers.php (100%) rename {app => src}/Filament/Resources/Users/Pages/ViewUser.php (100%) rename {app => src}/Filament/Resources/Users/Schemas/UserForm.php (100%) rename {app => src}/Filament/Resources/Users/Schemas/UserInfolist.php (100%) rename {app => src}/Filament/Resources/Users/Tables/UsersTable.php (100%) rename {app => src}/Filament/Resources/Users/UserResource.php (100%) rename {app => src}/Http/Controllers/Controller.php (100%) rename {app => src}/Http/Controllers/EmailVerificationNotificationController.php (100%) rename {app => src}/Http/Controllers/EmailVerificationPromptController.php (100%) rename {app => src}/Http/Controllers/ForgotPasswordController.php (100%) rename {app => src}/Http/Controllers/LoginController.php (100%) rename {app => src}/Http/Controllers/MagicLinkController.php (100%) rename {app => src}/Http/Controllers/PasswordController.php (100%) rename {app => src}/Http/Controllers/RegisterController.php (100%) rename {app => src}/Http/Controllers/ReimpersonateController.php (100%) rename {app => src}/Http/Controllers/ResetPasswordController.php (100%) rename {app => src}/Http/Controllers/SocialiteController.php (100%) rename {app => src}/Http/Controllers/VerifyEmailController.php (100%) rename {app => src}/Http/Middleware/EnsureMagicLinkEnabled.php (100%) rename {app => src}/Http/Requests/LoginRequest.php (100%) rename {app => src}/Http/Requests/RegisterRequest.php (100%) rename {app => src}/Listeners/AssignUserRole.php (100%) rename {app => src}/Listeners/Impersonation.php (100%) rename {app => src}/Listeners/SendWelcomeNotification.php (100%) rename {app => src}/Listeners/UpdateUserLastLogin.php (100%) rename {app => src}/Models/MagicLinkToken.php (100%) rename {app => src}/Models/SocialAccount.php (100%) rename {app => src}/Notifications/MagicLinkNotification.php (100%) rename {app => src}/Notifications/WelcomeNotification.php (100%) rename {app => src}/Providers/AuthServiceProvider.php (91%) rename {app => src}/Services/SocialiteService.php (100%) rename {app => src}/Traits/Sociable.php (100%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e6d0709..160d817 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,7 @@ jobs: release: uses: saucebase-dev/saucebase/.github/workflows/semantic-release.yml@main with: - version_file: 'module.json' + version_file: 'composer.json' version_file_path: '.version' prerelease: false secrets: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9d842e7..b3ba07e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,4 +13,4 @@ jobs: test: uses: saucebase-dev/saucebase/.github/workflows/test-module.yml@main with: - module: Auth + module: auth diff --git a/CLAUDE.md b/CLAUDE.md index 4c2d686..c827c54 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,7 +74,7 @@ Redirect URIs default to `/auth/socialite/{provider}/callback`. Providers config ```bash php artisan test --testsuite=Modules --filter='^Modules\\Auth\\Tests' # PHPUnit -npx playwright test --project="@Auth*" # E2E +npx playwright test --project="@auth*" # E2E ``` **E2E coverage**: login (basic, errors, security/rate-limiting, social, logout), register (basic, errors), forgot-password (basic, errors), verify-email. Page objects in `tests/e2e/pages/`, fixtures in `tests/e2e/fixtures/users.ts`. diff --git a/Taskfile.yml b/Taskfile.yml index 27b2c63..3e54da6 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -9,11 +9,11 @@ tasks: test:e2e: desc: Run E2E tests for Auth module - cmd: npx playwright test --project="@Auth*" {{.CLI_ARGS}} + cmd: npx playwright test --project="@auth*" {{.CLI_ARGS}} interactive: true # ── Code Generation ──────────────────────────────────────────── types:generate: desc: Generate TypeScript types from PHP DTOs and enums - cmd: php artisan module:generate-types Auth + cmd: php artisan module:generate-types auth diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php deleted file mode 100644 index f5d5bef..0000000 --- a/app/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,17 +0,0 @@ -group(module_path('auth', '/routes/web.php')); - Route::middleware('api') - ->group(module_path('auth', '/routes/api.php')); - } -} diff --git a/composer.json b/composer.json index 1ec903a..a5bf0b8 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,9 @@ { "name": "saucebase/auth", - "description": "", + "description": "Authentication module for Saucebase", "type": "saucebase-module", + "license": "proprietary", + "version": "1.8.1", "authors": [ { "name": "Saucebase", @@ -10,13 +12,15 @@ ], "extra": { "laravel": { - "providers": [], + "providers": [ + "Modules\\Auth\\Providers\\AuthServiceProvider" + ], "aliases": {} } }, "autoload": { "psr-4": { - "Modules\\Auth\\": "app/", + "Modules\\Auth\\": "src/", "Modules\\Auth\\Database\\Factories\\": "database/factories/", "Modules\\Auth\\Database\\Seeders\\": "database/seeders/" } @@ -29,5 +33,6 @@ "require": { "laravel/socialite": "^5.21", "stechstudio/filament-impersonate": "^5.1" - } + }, + "minimum-stability": "stable" } diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..5aa8a7d --- /dev/null +++ b/database/seeders/DatabaseSeeder.php @@ -0,0 +1,13 @@ +call(AuthDatabaseSeeder::class); + } +} diff --git a/module.json b/module.json deleted file mode 100644 index a9a6b1f..0000000 --- a/module.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "Auth", - "alias": "auth", - "description": "Authentication module for Saucebase", - "author": "Saucebase", - "version": "v1.8.1", - "keywords": [ - "auth" - ], - "priority": 0, - "providers": [ - "Modules\\Auth\\Providers\\AuthServiceProvider" - ], - "files": [] -} diff --git a/routes/api.php b/routes/api.php index 5a7d39e..4d9a221 100644 --- a/routes/api.php +++ b/routes/api.php @@ -3,6 +3,8 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Route; -Route::middleware(['auth:sanctum'])->prefix('api/v1/auth')->group(function () { - Route::get('me', fn () => response()->json(Auth::user())); +Route::middleware('api')->group(function (): void { + Route::middleware(['auth:sanctum'])->prefix('api/v1/auth')->group(function (): void { + Route::get('me', fn () => response()->json(Auth::user())); + }); }); diff --git a/routes/web.php b/routes/web.php index 0b34f42..3215399 100644 --- a/routes/web.php +++ b/routes/web.php @@ -13,85 +13,87 @@ use Modules\Auth\Http\Controllers\SocialiteController; use Modules\Auth\Http\Controllers\VerifyEmailController; -Route::prefix('auth')->group(function () { - Route::middleware('guest')->group(function () { +Route::middleware('web')->group(function (): void { + Route::prefix('auth')->group(function (): void { + Route::middleware('guest')->group(function (): void { - Route::get('login', [LoginController::class, 'create']) - ->name('login'); + Route::get('login', [LoginController::class, 'create']) + ->name('login'); - Route::post('login', [LoginController::class, 'store']); + Route::post('login', [LoginController::class, 'store']); - Route::get('register', [RegisterController::class, 'create']) - ->name('register'); + Route::get('register', [RegisterController::class, 'create']) + ->name('register'); - Route::post('register', [RegisterController::class, 'store']); + Route::post('register', [RegisterController::class, 'store']); - Route::get('forgot-password', [ForgotPasswordController::class, 'create']) - ->name('password.request'); + Route::get('forgot-password', [ForgotPasswordController::class, 'create']) + ->name('password.request'); - Route::post('forgot-password', [ForgotPasswordController::class, 'store']) - ->name('password.email'); + Route::post('forgot-password', [ForgotPasswordController::class, 'store']) + ->name('password.email'); - Route::get('reset-password/{token}', [ResetPasswordController::class, 'create']) - ->name('password.reset'); + Route::get('reset-password/{token}', [ResetPasswordController::class, 'create']) + ->name('password.reset'); - Route::post('reset-password', [ResetPasswordController::class, 'store']) - ->middleware('throttle:6,1') - ->name('password.store'); + Route::post('reset-password', [ResetPasswordController::class, 'store']) + ->middleware('throttle:6,1') + ->name('password.store'); - Route::get('magic-link', [MagicLinkController::class, 'create']) - ->name('magic-link.create'); + Route::get('magic-link', [MagicLinkController::class, 'create']) + ->name('magic-link.create'); - Route::post('magic-link', [MagicLinkController::class, 'store']) - ->middleware('throttle:5,1') - ->name('magic-link.store'); - }); + Route::post('magic-link', [MagicLinkController::class, 'store']) + ->middleware('throttle:5,1') + ->name('magic-link.store'); + }); - Route::middleware('auth')->group(function () { + Route::middleware('auth')->group(function (): void { - Route::any('logout', [LoginController::class, 'destroy']) - ->name('logout'); + Route::any('logout', [LoginController::class, 'destroy']) + ->name('logout'); - Route::get('verify-email', EmailVerificationPromptController::class) - ->name('verification.notice'); + Route::get('verify-email', EmailVerificationPromptController::class) + ->name('verification.notice'); - Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) - ->middleware(['signed', 'throttle:6,1']) - ->name('verification.verify'); + Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) + ->middleware(['signed', 'throttle:6,1']) + ->name('verification.verify'); - Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) - ->middleware('throttle:6,1') - ->name('verification.send'); + Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) + ->middleware('throttle:6,1') + ->name('verification.send'); - Route::put('password', [PasswordController::class, 'update'])->name('password.update'); + Route::put('password', [PasswordController::class, 'update'])->name('password.update'); - Route::delete('socialite/{provider}', [SocialiteController::class, 'disconnect']) - ->name('auth.socialite.disconnect'); + Route::delete('socialite/{provider}', [SocialiteController::class, 'disconnect']) + ->name('auth.socialite.disconnect'); - Route::post('impersonate/{userId}', ReimpersonateController::class) - ->name('auth.impersonate.reimpersonate'); - }); + Route::post('impersonate/{userId}', ReimpersonateController::class) + ->name('auth.impersonate.reimpersonate'); + }); + + /** + * Socialite Routes + * + * These routes are placed outside the auth/guest middleware groups because: + * - Guests can use them for social login/registration + * - Authenticated users can use them to connect additional social providers + */ + Route::get('socialite/{provider}', [SocialiteController::class, 'redirect']) + ->name('auth.socialite.redirect'); - /** - * Socialite Routes - * - * These routes are placed outside the auth/guest middleware groups because: - * - Guests can use them for social login/registration - * - Authenticated users can use them to connect additional social providers - */ - Route::get('socialite/{provider}', [SocialiteController::class, 'redirect']) - ->name('auth.socialite.redirect'); - - Route::get('socialite/{provider}/callback', [SocialiteController::class, 'callback']) - ->name('auth.socialite.callback'); - - /** - * Magic Link Authentication - * - * Placed outside guest/auth groups because the user clicking the link - * is not yet authenticated (they are in their email client). - */ - Route::get('magic-link/{token}', [MagicLinkController::class, 'authenticate']) - ->middleware('throttle:10,1') - ->name('magic-link.authenticate'); + Route::get('socialite/{provider}/callback', [SocialiteController::class, 'callback']) + ->name('auth.socialite.callback'); + + /** + * Magic Link Authentication + * + * Placed outside guest/auth groups because the user clicking the link + * is not yet authenticated (they are in their email client). + */ + Route::get('magic-link/{token}', [MagicLinkController::class, 'authenticate']) + ->middleware('throttle:10,1') + ->name('magic-link.authenticate'); + }); }); diff --git a/app/Exceptions/AuthException.php b/src/Exceptions/AuthException.php similarity index 100% rename from app/Exceptions/AuthException.php rename to src/Exceptions/AuthException.php diff --git a/app/Exceptions/SocialiteException.php b/src/Exceptions/SocialiteException.php similarity index 100% rename from app/Exceptions/SocialiteException.php rename to src/Exceptions/SocialiteException.php diff --git a/app/Filament/AuthPlugin.php b/src/Filament/AuthPlugin.php similarity index 100% rename from app/Filament/AuthPlugin.php rename to src/Filament/AuthPlugin.php diff --git a/app/Filament/Resources/Users/Pages/CreateUser.php b/src/Filament/Resources/Users/Pages/CreateUser.php similarity index 100% rename from app/Filament/Resources/Users/Pages/CreateUser.php rename to src/Filament/Resources/Users/Pages/CreateUser.php diff --git a/app/Filament/Resources/Users/Pages/EditUser.php b/src/Filament/Resources/Users/Pages/EditUser.php similarity index 100% rename from app/Filament/Resources/Users/Pages/EditUser.php rename to src/Filament/Resources/Users/Pages/EditUser.php diff --git a/app/Filament/Resources/Users/Pages/ListUsers.php b/src/Filament/Resources/Users/Pages/ListUsers.php similarity index 100% rename from app/Filament/Resources/Users/Pages/ListUsers.php rename to src/Filament/Resources/Users/Pages/ListUsers.php diff --git a/app/Filament/Resources/Users/Pages/ViewUser.php b/src/Filament/Resources/Users/Pages/ViewUser.php similarity index 100% rename from app/Filament/Resources/Users/Pages/ViewUser.php rename to src/Filament/Resources/Users/Pages/ViewUser.php diff --git a/app/Filament/Resources/Users/Schemas/UserForm.php b/src/Filament/Resources/Users/Schemas/UserForm.php similarity index 100% rename from app/Filament/Resources/Users/Schemas/UserForm.php rename to src/Filament/Resources/Users/Schemas/UserForm.php diff --git a/app/Filament/Resources/Users/Schemas/UserInfolist.php b/src/Filament/Resources/Users/Schemas/UserInfolist.php similarity index 100% rename from app/Filament/Resources/Users/Schemas/UserInfolist.php rename to src/Filament/Resources/Users/Schemas/UserInfolist.php diff --git a/app/Filament/Resources/Users/Tables/UsersTable.php b/src/Filament/Resources/Users/Tables/UsersTable.php similarity index 100% rename from app/Filament/Resources/Users/Tables/UsersTable.php rename to src/Filament/Resources/Users/Tables/UsersTable.php diff --git a/app/Filament/Resources/Users/UserResource.php b/src/Filament/Resources/Users/UserResource.php similarity index 100% rename from app/Filament/Resources/Users/UserResource.php rename to src/Filament/Resources/Users/UserResource.php diff --git a/app/Http/Controllers/Controller.php b/src/Http/Controllers/Controller.php similarity index 100% rename from app/Http/Controllers/Controller.php rename to src/Http/Controllers/Controller.php diff --git a/app/Http/Controllers/EmailVerificationNotificationController.php b/src/Http/Controllers/EmailVerificationNotificationController.php similarity index 100% rename from app/Http/Controllers/EmailVerificationNotificationController.php rename to src/Http/Controllers/EmailVerificationNotificationController.php diff --git a/app/Http/Controllers/EmailVerificationPromptController.php b/src/Http/Controllers/EmailVerificationPromptController.php similarity index 100% rename from app/Http/Controllers/EmailVerificationPromptController.php rename to src/Http/Controllers/EmailVerificationPromptController.php diff --git a/app/Http/Controllers/ForgotPasswordController.php b/src/Http/Controllers/ForgotPasswordController.php similarity index 100% rename from app/Http/Controllers/ForgotPasswordController.php rename to src/Http/Controllers/ForgotPasswordController.php diff --git a/app/Http/Controllers/LoginController.php b/src/Http/Controllers/LoginController.php similarity index 100% rename from app/Http/Controllers/LoginController.php rename to src/Http/Controllers/LoginController.php diff --git a/app/Http/Controllers/MagicLinkController.php b/src/Http/Controllers/MagicLinkController.php similarity index 100% rename from app/Http/Controllers/MagicLinkController.php rename to src/Http/Controllers/MagicLinkController.php diff --git a/app/Http/Controllers/PasswordController.php b/src/Http/Controllers/PasswordController.php similarity index 100% rename from app/Http/Controllers/PasswordController.php rename to src/Http/Controllers/PasswordController.php diff --git a/app/Http/Controllers/RegisterController.php b/src/Http/Controllers/RegisterController.php similarity index 100% rename from app/Http/Controllers/RegisterController.php rename to src/Http/Controllers/RegisterController.php diff --git a/app/Http/Controllers/ReimpersonateController.php b/src/Http/Controllers/ReimpersonateController.php similarity index 100% rename from app/Http/Controllers/ReimpersonateController.php rename to src/Http/Controllers/ReimpersonateController.php diff --git a/app/Http/Controllers/ResetPasswordController.php b/src/Http/Controllers/ResetPasswordController.php similarity index 100% rename from app/Http/Controllers/ResetPasswordController.php rename to src/Http/Controllers/ResetPasswordController.php diff --git a/app/Http/Controllers/SocialiteController.php b/src/Http/Controllers/SocialiteController.php similarity index 100% rename from app/Http/Controllers/SocialiteController.php rename to src/Http/Controllers/SocialiteController.php diff --git a/app/Http/Controllers/VerifyEmailController.php b/src/Http/Controllers/VerifyEmailController.php similarity index 100% rename from app/Http/Controllers/VerifyEmailController.php rename to src/Http/Controllers/VerifyEmailController.php diff --git a/app/Http/Middleware/EnsureMagicLinkEnabled.php b/src/Http/Middleware/EnsureMagicLinkEnabled.php similarity index 100% rename from app/Http/Middleware/EnsureMagicLinkEnabled.php rename to src/Http/Middleware/EnsureMagicLinkEnabled.php diff --git a/app/Http/Requests/LoginRequest.php b/src/Http/Requests/LoginRequest.php similarity index 100% rename from app/Http/Requests/LoginRequest.php rename to src/Http/Requests/LoginRequest.php diff --git a/app/Http/Requests/RegisterRequest.php b/src/Http/Requests/RegisterRequest.php similarity index 100% rename from app/Http/Requests/RegisterRequest.php rename to src/Http/Requests/RegisterRequest.php diff --git a/app/Listeners/AssignUserRole.php b/src/Listeners/AssignUserRole.php similarity index 100% rename from app/Listeners/AssignUserRole.php rename to src/Listeners/AssignUserRole.php diff --git a/app/Listeners/Impersonation.php b/src/Listeners/Impersonation.php similarity index 100% rename from app/Listeners/Impersonation.php rename to src/Listeners/Impersonation.php diff --git a/app/Listeners/SendWelcomeNotification.php b/src/Listeners/SendWelcomeNotification.php similarity index 100% rename from app/Listeners/SendWelcomeNotification.php rename to src/Listeners/SendWelcomeNotification.php diff --git a/app/Listeners/UpdateUserLastLogin.php b/src/Listeners/UpdateUserLastLogin.php similarity index 100% rename from app/Listeners/UpdateUserLastLogin.php rename to src/Listeners/UpdateUserLastLogin.php diff --git a/app/Models/MagicLinkToken.php b/src/Models/MagicLinkToken.php similarity index 100% rename from app/Models/MagicLinkToken.php rename to src/Models/MagicLinkToken.php diff --git a/app/Models/SocialAccount.php b/src/Models/SocialAccount.php similarity index 100% rename from app/Models/SocialAccount.php rename to src/Models/SocialAccount.php diff --git a/app/Notifications/MagicLinkNotification.php b/src/Notifications/MagicLinkNotification.php similarity index 100% rename from app/Notifications/MagicLinkNotification.php rename to src/Notifications/MagicLinkNotification.php diff --git a/app/Notifications/WelcomeNotification.php b/src/Notifications/WelcomeNotification.php similarity index 100% rename from app/Notifications/WelcomeNotification.php rename to src/Notifications/WelcomeNotification.php diff --git a/app/Providers/AuthServiceProvider.php b/src/Providers/AuthServiceProvider.php similarity index 91% rename from app/Providers/AuthServiceProvider.php rename to src/Providers/AuthServiceProvider.php index 6d563c9..54f703b 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/src/Providers/AuthServiceProvider.php @@ -12,19 +12,6 @@ class AuthServiceProvider extends ModuleServiceProvider { - protected string $name = 'Auth'; - - protected string $nameLower = 'auth'; - - protected array $providers = [ - RouteServiceProvider::class, - ]; - - public function boot(): void - { - parent::boot(); - } - /** * Share Inertia data globally. */ @@ -130,6 +117,6 @@ protected function registerConfig(): void { parent::registerConfig(); - $this->mergeConfigFrom(module_path($this->name, 'config/services.php'), 'services'); + $this->mergeConfigFrom(module_path($this->moduleName(), 'config/services.php'), 'services'); } } diff --git a/app/Services/SocialiteService.php b/src/Services/SocialiteService.php similarity index 100% rename from app/Services/SocialiteService.php rename to src/Services/SocialiteService.php diff --git a/app/Traits/Sociable.php b/src/Traits/Sociable.php similarity index 100% rename from app/Traits/Sociable.php rename to src/Traits/Sociable.php From 4b04ef061ee1836a5342b366dbdc35bd43c1520c Mon Sep 17 00:00:00 2001 From: roble Date: Sun, 26 Apr 2026 16:07:28 +0100 Subject: [PATCH 02/12] fix: update translation keys to use namespaced format for auth messages --- src/Exceptions/AuthException.php | 4 ++-- src/Exceptions/SocialiteException.php | 14 +++++++------- .../EmailVerificationNotificationController.php | 2 +- src/Http/Controllers/ForgotPasswordController.php | 2 +- src/Http/Controllers/LoginController.php | 2 +- src/Http/Controllers/MagicLinkController.php | 8 ++++---- src/Http/Controllers/RegisterController.php | 2 +- src/Http/Controllers/ResetPasswordController.php | 4 ++-- src/Http/Controllers/SocialiteController.php | 10 +++++----- 9 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Exceptions/AuthException.php b/src/Exceptions/AuthException.php index 730ea8b..85b32f0 100644 --- a/src/Exceptions/AuthException.php +++ b/src/Exceptions/AuthException.php @@ -30,7 +30,7 @@ public function __construct(string $message = 'Authentication error.') */ public static function invalidCredentials(): self { - return new self(trans('auth.failed')); + return new self(trans('auth::auth.failed')); } /** @@ -41,7 +41,7 @@ public static function invalidCredentials(): self */ public static function throttle(int $seconds = 60): self { - $message = trans('auth.throttle', [ + $message = trans('auth::auth.throttle', [ 'seconds' => $seconds, 'minutes' => ceil($seconds / 60), ]); diff --git a/src/Exceptions/SocialiteException.php b/src/Exceptions/SocialiteException.php index 3d7a1b2..097c83a 100644 --- a/src/Exceptions/SocialiteException.php +++ b/src/Exceptions/SocialiteException.php @@ -8,36 +8,36 @@ class SocialiteException extends Exception { public static function invalidSocialUser(): self { - return new self(trans('socialite.invalid_user')); + return new self(trans('auth::socialite.invalid_user')); } public static function cannotDisconnectOnlyMethod(): self { - return new self(trans('socialite.cannot_disconnect_only_method')); + return new self(trans('auth::socialite.cannot_disconnect_only_method')); } public static function authenticationFailed(): self { - return new self(trans('socialite.error')); + return new self(trans('auth::socialite.error')); } public static function providerNotConnected(string $provider): self { - return new self(trans('socialite.not_connected', ['Provider' => $provider])); + return new self(trans('auth::socialite.not_connected', ['Provider' => $provider])); } public static function missingSocialAccountsRelation(): self { - return new self(trans('socialite.missing_social_accounts_relation')); + return new self(trans('auth::socialite.missing_social_accounts_relation')); } public static function accountAlreadyLinked(string $provider): self { - return new self(trans('socialite.account_already_linked', ['provider' => ucfirst($provider)])); + return new self(trans('auth::socialite.account_already_linked', ['provider' => ucfirst($provider)])); } public static function unsupportedProvider(string $provider): self { - return new self(trans('socialite.unsupported_provider', ['provider' => ucfirst($provider)])); + return new self(trans('auth::socialite.unsupported_provider', ['provider' => ucfirst($provider)])); } } diff --git a/src/Http/Controllers/EmailVerificationNotificationController.php b/src/Http/Controllers/EmailVerificationNotificationController.php index d2cc516..22f9ce6 100644 --- a/src/Http/Controllers/EmailVerificationNotificationController.php +++ b/src/Http/Controllers/EmailVerificationNotificationController.php @@ -18,6 +18,6 @@ public function store(Request $request): RedirectResponse $request->user()->sendEmailVerificationNotification(); - return back()->with('status', trans('auth.verification-link-sent')); + return back()->with('status', trans('auth::auth.verification-link-sent')); } } diff --git a/src/Http/Controllers/ForgotPasswordController.php b/src/Http/Controllers/ForgotPasswordController.php index daf9b75..ff861c9 100644 --- a/src/Http/Controllers/ForgotPasswordController.php +++ b/src/Http/Controllers/ForgotPasswordController.php @@ -37,6 +37,6 @@ public function store(Request $request): RedirectResponse $request->only('email') ); - return back()->with('status', trans('passwords.sent')); + return back()->with('status', trans('auth::passwords.sent')); } } diff --git a/src/Http/Controllers/LoginController.php b/src/Http/Controllers/LoginController.php index 2618caf..c1a0017 100644 --- a/src/Http/Controllers/LoginController.php +++ b/src/Http/Controllers/LoginController.php @@ -41,7 +41,7 @@ public function store(LoginRequest $request) request()->session()->regenerate(); Toast::default( - __('auth.welcome-back', ['name' => $user->name]), + __('auth::auth.welcome-back', ['name' => $user->name]), ); if ($request->session()->has('url.intended')) { diff --git a/src/Http/Controllers/MagicLinkController.php b/src/Http/Controllers/MagicLinkController.php index 821635a..03c6ad8 100644 --- a/src/Http/Controllers/MagicLinkController.php +++ b/src/Http/Controllers/MagicLinkController.php @@ -67,7 +67,7 @@ public function store(Request $request): RedirectResponse $user->notify(new MagicLinkNotification($url)); } - return back()->with('status', __('auth.magic-link-sent')); + return back()->with('status', __('auth::auth.magic-link-sent')); } /** @@ -85,21 +85,21 @@ public function authenticate(Request $request, string $token): \Symfony\Componen ->update(['used_at' => $now]); if (! $consumed) { - return redirect()->route('login')->with('error', __('auth.magic-link-expired')); + return redirect()->route('login')->with('error', __('auth::auth.magic-link-expired')); } $record = MagicLinkToken::where('token', $hashed)->with('user')->first(); $user = $record?->user; if ($user === null) { - return redirect()->route('login')->with('error', __('auth.magic-link-expired')); + return redirect()->route('login')->with('error', __('auth::auth.magic-link-expired')); } Auth::login($user); $request->session()->regenerate(); - Toast::default(__('auth.welcome-back', ['name' => $user->name])); + Toast::default(__('auth::auth.welcome-back', ['name' => $user->name])); $intended = $request->query('intended'); diff --git a/src/Http/Controllers/RegisterController.php b/src/Http/Controllers/RegisterController.php index 5b28533..bc945f5 100644 --- a/src/Http/Controllers/RegisterController.php +++ b/src/Http/Controllers/RegisterController.php @@ -37,7 +37,7 @@ public function store(RegisterRequest $request): RedirectResponse Auth::login($user); Toast::default( - __('auth.welcome', ['name' => $user->name]), + __('auth::auth.welcome', ['name' => $user->name]), ); return redirect()->intended(route('dashboard')); diff --git a/src/Http/Controllers/ResetPasswordController.php b/src/Http/Controllers/ResetPasswordController.php index 5a702dc..220ed6d 100644 --- a/src/Http/Controllers/ResetPasswordController.php +++ b/src/Http/Controllers/ResetPasswordController.php @@ -59,9 +59,9 @@ function ($user) use ($request) { // the application's home authenticated view. If there is an error we can // redirect them back to where they came from with their error message. if ($status == Password::PASSWORD_RESET) { - return redirect()->route('login')->with('status', trans($status)); + return redirect()->route('login')->with('status', trans('auth::'.$status)); } - return back()->with('error', trans($status)); + return back()->with('error', trans('auth::'.$status)); } } diff --git a/src/Http/Controllers/SocialiteController.php b/src/Http/Controllers/SocialiteController.php index d47c0f6..ea1e159 100644 --- a/src/Http/Controllers/SocialiteController.php +++ b/src/Http/Controllers/SocialiteController.php @@ -32,7 +32,7 @@ public function callback(string $provider): RedirectResponse ]); if ($validator->fails()) { - return back()->with('error', trans('socialite.error')); + return back()->with('error', trans('auth::socialite.error')); } // Check if user is already authenticated (account linking flow) @@ -41,11 +41,11 @@ public function callback(string $provider): RedirectResponse /** @var User $socialUser */ $socialUser = Socialite::driver($provider)->user(); $this->socialiteService->linkAccountToUser(Auth::user(), $provider, $socialUser); - Toast::success(trans('socialite.account_connected', ['provider' => ucfirst($provider)])); + Toast::success(trans('auth::socialite.account_connected', ['provider' => ucfirst($provider)])); } catch (SocialiteException $e) { Toast::error($e->getMessage()); } catch (\Exception $e) { - Toast::error(trans('socialite.error')); + Toast::error(trans('auth::socialite.error')); report($e); } finally { return back(); @@ -60,7 +60,7 @@ public function callback(string $provider): RedirectResponse request()->session()->regenerate(); Toast::default( - __($user->wasRecentlyCreated ? 'auth.welcome' : 'auth.welcome-back', [ + __($user->wasRecentlyCreated ? 'auth::auth.welcome' : 'auth::auth.welcome-back', [ 'name' => $user->name, ]), ); @@ -79,7 +79,7 @@ public function disconnect(string $provider): RedirectResponse try { $this->socialiteService->disconnectProvider($user, $provider); - Toast::success(trans('socialite.account_disconnected', ['provider' => $provider])); + Toast::success(trans('auth::socialite.account_disconnected', ['provider' => $provider])); } catch (SocialiteException $e) { Toast::error($e->getMessage()); } From c2570ac242dd7bedbfffdb873e943d2f6024de2e Mon Sep 17 00:00:00 2001 From: roble Date: Tue, 28 Apr 2026 20:47:02 +0100 Subject: [PATCH 03/12] feat(auth): add getNavigationGroupSort method to AuthPlugin --- src/Filament/AuthPlugin.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Filament/AuthPlugin.php b/src/Filament/AuthPlugin.php index 387fa3c..4a71c3c 100644 --- a/src/Filament/AuthPlugin.php +++ b/src/Filament/AuthPlugin.php @@ -23,6 +23,11 @@ public function getId(): string return 'auth'; } + public static function getNavigationGroupSort(): int + { + return 2; + } + public function boot(Panel $panel): void { $panel->navigationGroups([ From edb5c4590733bc1a7f3b703dffbcb8f5efdb4dad Mon Sep 17 00:00:00 2001 From: roble Date: Wed, 29 Apr 2026 21:37:16 +0100 Subject: [PATCH 04/12] feat(auth): add db:seed task to seed the Auth module database --- Taskfile.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Taskfile.yml b/Taskfile.yml index 3e54da6..5f75072 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -12,6 +12,12 @@ tasks: cmd: npx playwright test --project="@auth*" {{.CLI_ARGS}} interactive: true + # ── Database ────────────────────────────────────────────────── + + db:seed: + desc: Seed the Auth module database + cmd: php artisan modules:seed --module=auth + # ── Code Generation ──────────────────────────────────────────── types:generate: From 9633430f341c6243e5ce0b6978496967a3606d88 Mon Sep 17 00:00:00 2001 From: roble Date: Mon, 11 May 2026 21:15:02 +0100 Subject: [PATCH 05/12] feat(auth): restructure authentication module with new Vue components and layout --- resources/js/app.ts | 56 +------------------ resources/js/vue/app.ts | 54 ++++++++++++++++++ .../components/ImpersonationAlert.vue | 0 .../components/SocialiteProviders.vue | 0 .../js/{ => vue}/layouts/AuthCardLayout.vue | 0 .../js/{ => vue}/pages/ForgotPassword.vue | 0 resources/js/{ => vue}/pages/Login.vue | 0 resources/js/{ => vue}/pages/MagicLink.vue | 0 resources/js/{ => vue}/pages/Register.vue | 0 .../js/{ => vue}/pages/ResetPassword.vue | 0 resources/js/{ => vue}/pages/VerifyEmail.vue | 0 vite.config.js | 17 +----- 12 files changed, 57 insertions(+), 70 deletions(-) create mode 100644 resources/js/vue/app.ts rename resources/js/{ => vue}/components/ImpersonationAlert.vue (100%) rename resources/js/{ => vue}/components/SocialiteProviders.vue (100%) rename resources/js/{ => vue}/layouts/AuthCardLayout.vue (100%) rename resources/js/{ => vue}/pages/ForgotPassword.vue (100%) rename resources/js/{ => vue}/pages/Login.vue (100%) rename resources/js/{ => vue}/pages/MagicLink.vue (100%) rename resources/js/{ => vue}/pages/Register.vue (100%) rename resources/js/{ => vue}/pages/ResetPassword.vue (100%) rename resources/js/{ => vue}/pages/VerifyEmail.vue (100%) diff --git a/resources/js/app.ts b/resources/js/app.ts index ade9b9f..3043f15 100644 --- a/resources/js/app.ts +++ b/resources/js/app.ts @@ -1,55 +1 @@ -import { useDialog } from '@/composables/useDialog'; -import { registerGlobalComponent } from '@/lib/globalComponents'; -import { registerAction, registerIcon } from '@/lib/navigation'; -import { router } from '@inertiajs/vue3'; -import { trans } from 'laravel-vue-i18n'; -import { LogOut } from 'lucide-vue-next'; -import IconLogOut from '~icons/lucide/log-out'; -import ImpersonationAlert from './components/ImpersonationAlert.vue'; - -import '../css/style.css'; - -/** - * Auth module setup - * Called during app initialization before mounting - */ -export function setup() { - registerIcon('logout', IconLogOut); - registerAuthActions(); - registerGlobalComponent('top', ImpersonationAlert); -} - -/** - * Register auth-related navigation actions - */ -function registerAuthActions() { - // Logout action - registerAction('logout', async (event: MouseEvent) => { - event.preventDefault(); - - const { confirm } = useDialog(); - if ( - await confirm({ - title: trans('Log out'), - description: trans( - 'Are you sure you want to log out? You will need to sign in again.', - ), - confirmLabel: trans('Log out'), - cancelLabel: trans('Cancel'), - variant: 'destructive', - icon: LogOut, - align: 'left', - }) - ) { - router.post(route('logout')); - } - }); -} - -/** - * Auth module after mount logic - * Called after the app has been mounted - */ -export function afterMount() { - console.debug('Auth module after mount logic executed'); -} +export * from './vue/app'; diff --git a/resources/js/vue/app.ts b/resources/js/vue/app.ts new file mode 100644 index 0000000..d692f25 --- /dev/null +++ b/resources/js/vue/app.ts @@ -0,0 +1,54 @@ +import '@modules/auth/resources/css/style.css'; +import { useDialog } from '@/composables/useDialog'; +import { registerGlobalComponent } from '@/lib/globalComponents'; +import { registerAction, registerIcon } from '@/lib/navigation'; +import { router } from '@inertiajs/vue3'; +import { trans } from 'laravel-vue-i18n'; +import { LogOut } from 'lucide-vue-next'; +import IconLogOut from '~icons/lucide/log-out'; +import ImpersonationAlert from './components/ImpersonationAlert.vue'; + +/** + * Auth module setup + * Called during app initialization before mounting + */ +export function setup() { + registerIcon('logout', IconLogOut); + registerAuthActions(); + registerGlobalComponent('top', ImpersonationAlert); +} + +/** + * Register auth-related navigation actions + */ +function registerAuthActions() { + // Logout action + registerAction('logout', async (event: MouseEvent) => { + event.preventDefault(); + + const { confirm } = useDialog(); + if ( + await confirm({ + title: trans('Log out'), + description: trans( + 'Are you sure you want to log out? You will need to sign in again.', + ), + confirmLabel: trans('Log out'), + cancelLabel: trans('Cancel'), + variant: 'destructive', + icon: LogOut, + align: 'left', + }) + ) { + router.post(route('logout')); + } + }); +} + +/** + * Auth module after mount logic + * Called after the app has been mounted + */ +export function afterMount() { + console.debug('Auth module after mount logic executed'); +} diff --git a/resources/js/components/ImpersonationAlert.vue b/resources/js/vue/components/ImpersonationAlert.vue similarity index 100% rename from resources/js/components/ImpersonationAlert.vue rename to resources/js/vue/components/ImpersonationAlert.vue diff --git a/resources/js/components/SocialiteProviders.vue b/resources/js/vue/components/SocialiteProviders.vue similarity index 100% rename from resources/js/components/SocialiteProviders.vue rename to resources/js/vue/components/SocialiteProviders.vue diff --git a/resources/js/layouts/AuthCardLayout.vue b/resources/js/vue/layouts/AuthCardLayout.vue similarity index 100% rename from resources/js/layouts/AuthCardLayout.vue rename to resources/js/vue/layouts/AuthCardLayout.vue diff --git a/resources/js/pages/ForgotPassword.vue b/resources/js/vue/pages/ForgotPassword.vue similarity index 100% rename from resources/js/pages/ForgotPassword.vue rename to resources/js/vue/pages/ForgotPassword.vue diff --git a/resources/js/pages/Login.vue b/resources/js/vue/pages/Login.vue similarity index 100% rename from resources/js/pages/Login.vue rename to resources/js/vue/pages/Login.vue diff --git a/resources/js/pages/MagicLink.vue b/resources/js/vue/pages/MagicLink.vue similarity index 100% rename from resources/js/pages/MagicLink.vue rename to resources/js/vue/pages/MagicLink.vue diff --git a/resources/js/pages/Register.vue b/resources/js/vue/pages/Register.vue similarity index 100% rename from resources/js/pages/Register.vue rename to resources/js/vue/pages/Register.vue diff --git a/resources/js/pages/ResetPassword.vue b/resources/js/vue/pages/ResetPassword.vue similarity index 100% rename from resources/js/pages/ResetPassword.vue rename to resources/js/vue/pages/ResetPassword.vue diff --git a/resources/js/pages/VerifyEmail.vue b/resources/js/vue/pages/VerifyEmail.vue similarity index 100% rename from resources/js/pages/VerifyEmail.vue rename to resources/js/vue/pages/VerifyEmail.vue diff --git a/vite.config.js b/vite.config.js index e100344..589d91b 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,15 +1,2 @@ -/** - * Module Asset Configuration - * - * Define CSS, JS, and TS files to be compiled when this module is enabled. - * Paths are relative to this module's resources/assets/ directory. - * Vue components in resources/js/pages/ are discovered automatically. - * - * Example: - * export const paths = [ - * 'css/app.css', // Becomes modules/ModuleName/resources/assets/css/app.css - * 'js/app.ts', // Becomes modules/ModuleName/resources/assets/js/app.ts - * 'js/custom-feature.js', // Becomes modules/ModuleName/resources/assets/js/custom-feature.js - * ]; - */ -export const paths = ['css/style.css', 'js/app.ts']; +// TODO: figure out if we need to export anything here for the auth module's frontend assets. If not, we can just leave this file empty or delete it. +export default {}; From 6897477130c80e9dcbb6cafdf0fb530698b39ada Mon Sep 17 00:00:00 2001 From: roble Date: Sun, 17 May 2026 17:39:03 +0100 Subject: [PATCH 06/12] feat: implement react new authentication pages and components including login, registration, password reset, and email verification --- resources/js/react/app.tsx | 38 ++++ .../react/components/ImpersonationAlert.tsx | 173 ++++++++++++++++++ .../react/components/SocialiteProviders.tsx | 50 +++++ resources/js/react/layouts/AuthCardLayout.tsx | 55 ++++++ resources/js/react/pages/ForgotPassword.tsx | 60 ++++++ resources/js/react/pages/Login.tsx | 137 ++++++++++++++ resources/js/react/pages/MagicLink.tsx | 55 ++++++ resources/js/react/pages/Register.tsx | 109 +++++++++++ resources/js/react/pages/ResetPassword.tsx | 87 +++++++++ resources/js/react/pages/VerifyEmail.tsx | 41 +++++ 10 files changed, 805 insertions(+) create mode 100644 resources/js/react/app.tsx create mode 100644 resources/js/react/components/ImpersonationAlert.tsx create mode 100644 resources/js/react/components/SocialiteProviders.tsx create mode 100644 resources/js/react/layouts/AuthCardLayout.tsx create mode 100644 resources/js/react/pages/ForgotPassword.tsx create mode 100644 resources/js/react/pages/Login.tsx create mode 100644 resources/js/react/pages/MagicLink.tsx create mode 100644 resources/js/react/pages/Register.tsx create mode 100644 resources/js/react/pages/ResetPassword.tsx create mode 100644 resources/js/react/pages/VerifyEmail.tsx diff --git a/resources/js/react/app.tsx b/resources/js/react/app.tsx new file mode 100644 index 0000000..e4030f7 --- /dev/null +++ b/resources/js/react/app.tsx @@ -0,0 +1,38 @@ +import '@modules/auth/resources/css/style.css'; +import { registerGlobalComponent } from '@/lib/globalComponents'; +import { registerAction, registerIcon } from '@/lib/navigation'; +import { confirm } from '@/hooks/useDialog'; +import { router } from '@inertiajs/react'; +import { LogOut } from 'lucide-react'; +import IconLogOut from '~icons/lucide/log-out'; +import ImpersonationAlert from './components/ImpersonationAlert'; + +export function setup() { + registerIcon('logout', IconLogOut); + registerAuthActions(); + registerGlobalComponent('top', ImpersonationAlert); +} + +function registerAuthActions() { + registerAction('logout', async (event: MouseEvent) => { + event.preventDefault(); + + const confirmed = await confirm({ + title: 'Log out', + description: 'Are you sure you want to log out? You will need to sign in again.', + confirmLabel: 'Log out', + cancelLabel: 'Cancel', + variant: 'destructive', + icon: LogOut, + align: 'left', + }); + + if (confirmed) { + router.post(route('logout')); + } + }); +} + +export function afterMount() { + console.debug('Auth module after mount logic executed'); +} diff --git a/resources/js/react/components/ImpersonationAlert.tsx b/resources/js/react/components/ImpersonationAlert.tsx new file mode 100644 index 0000000..a18024c --- /dev/null +++ b/resources/js/react/components/ImpersonationAlert.tsx @@ -0,0 +1,173 @@ +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { cn } from '@/lib/utils'; +import type { User } from '@/types'; +import { router, usePage } from '@inertiajs/react'; +import { Drama, History, X } from 'lucide-react'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { useT } from '@/i18n'; + +interface Impersonation { + user: User; + route: string; + label: string; + recent: User[]; +} + +function getUserInitials(name: string): string { + return name + .split(' ') + .map((word) => word.charAt(0).toUpperCase()) + .slice(0, 2) + .join(''); +} + +function getRoleBadgeClasses(role: string): string { + return cn( + 'shrink-0 rounded-xl px-1 py-0.5 text-[9px] text-white uppercase', + role === 'admin' ? 'bg-red-800' : 'bg-cyan-700', + ); +} + +export default function ImpersonationAlert() { + const t = useT(); + const page = usePage(); + const impersonation = (page.props?.impersonation as Impersonation) || null; + const [isExpanded, setIsExpanded] = useState(false); + const alertRef = useRef(null); + + const collapse = useCallback(() => setIsExpanded(false), []); + + useEffect(() => { + if (!isExpanded) return; + + function handleClickOutside(e: MouseEvent) { + if (alertRef.current && !alertRef.current.contains(e.target as Node)) { + collapse(); + } + } + + function handleKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape') collapse(); + } + + document.addEventListener('mousedown', handleClickOutside); + document.addEventListener('keydown', handleKeyDown); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + document.removeEventListener('keydown', handleKeyDown); + }; + }, [isExpanded, collapse]); + + const reimpersonate = (userId: number) => { + router.post( + route('auth.impersonate.reimpersonate', { userId }), + {}, + { preserveScroll: true, onSuccess: collapse }, + ); + }; + + if (!impersonation) return null; + + return ( +
+ {!isExpanded ? ( + + ) : ( +
+ +
+ + + + {getUserInitials(impersonation.user.name)} + + +
+
+

+ {impersonation.user.name} +

+ {impersonation.user.role && ( + + {impersonation.user.role} + + )} +
+

{impersonation.user.email}

+
+
+ + {impersonation.label} + + + {impersonation.recent?.length > 0 && ( +
+

+ + {t('Recent impersonated users')} +

+
+ {impersonation.recent.map((user) => ( + + ))} +
+
+ )} +
+ )} +
+ ); +} diff --git a/resources/js/react/components/SocialiteProviders.tsx b/resources/js/react/components/SocialiteProviders.tsx new file mode 100644 index 0000000..5288ab2 --- /dev/null +++ b/resources/js/react/components/SocialiteProviders.tsx @@ -0,0 +1,50 @@ +import { Button } from '@/components/ui/button'; +import { usePage } from '@inertiajs/react'; +import { useT } from '@/i18n'; +import IconGithub from '~icons/simple-icons/github'; +import IconGoogle from '~icons/simple-icons/google'; + +type Provider = { name: string; icon: React.ComponentType<{ className?: string }> }; + +const providers: Provider[] = [ + { name: 'google', icon: IconGoogle }, + { name: 'github', icon: IconGithub }, +]; + +export default function SocialiteProviders() { + const t = useT(); + const page = usePage(); + const lastUsed = (page.props.auth as any)?.last_social_provider as string | undefined; + + if (!route().has('auth.socialite.redirect') || !providers.length) { + return null; + } + + return ( +
+ {providers.map(({ name, icon: Icon }) => ( +
+ + {lastUsed === name && ( + + {t('Last used')} + + )} +
+ ))} +
+ + {t('Or continue with email')} + +
+
+ ); +} diff --git a/resources/js/react/layouts/AuthCardLayout.tsx b/resources/js/react/layouts/AuthCardLayout.tsx new file mode 100644 index 0000000..76d935e --- /dev/null +++ b/resources/js/react/layouts/AuthCardLayout.tsx @@ -0,0 +1,55 @@ +import AlertMessage from '@/components/AlertMessage'; +import AppLogo from '@/components/AppLogo'; +import Footer from '@/components/Footer'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Head, Link, usePage } from '@inertiajs/react'; +import type { ReactNode } from 'react'; + +interface AuthCardLayoutProps { + title?: string; + description?: string; + cardClass?: string; + children: ReactNode; + outside?: ReactNode; +} + +export default function AuthCardLayout({ title, description, cardClass, children, outside }: AuthCardLayoutProps) { + const page = usePage(); + const status = page.props.status as string | undefined; + const error = page.props.error as string | undefined; + + return ( +
+
+ + + + +
+ +
+
+ + + {title} + {description} + + + {(status || error) && ( +
+ +
+ )} + {children} +
+
+
+ {outside} +
+
+
+ ); +} diff --git a/resources/js/react/pages/ForgotPassword.tsx b/resources/js/react/pages/ForgotPassword.tsx new file mode 100644 index 0000000..0fa3bf7 --- /dev/null +++ b/resources/js/react/pages/ForgotPassword.tsx @@ -0,0 +1,60 @@ +import { Button } from '@/components/ui/button'; +import { Field, FieldError } from '@/components/ui/field'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useT } from '@/i18n'; +import { Link, useForm, usePage } from '@inertiajs/react'; +import AuthCardLayout from '../layouts/AuthCardLayout'; + +export default function ForgotPassword() { + const t = useT(); + const page = usePage(); + const email = page.props.email ? String(page.props.email) : ''; + + const { data, setData, post, processing, errors } = useForm({ email }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + post(route('password.email'), { preserveScroll: true }); + }; + + return ( + +
+ + + setData('email', e.target.value)} + /> + {errors.email && {errors.email}} + + +
+ + {t('Back to login')} + + +
+
+
+ ); +} diff --git a/resources/js/react/pages/Login.tsx b/resources/js/react/pages/Login.tsx new file mode 100644 index 0000000..413d9e7 --- /dev/null +++ b/resources/js/react/pages/Login.tsx @@ -0,0 +1,137 @@ +import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Field, FieldError, FieldLabel } from '@/components/ui/field'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useT } from '@/i18n'; +import { Link, useForm, usePage } from '@inertiajs/react'; +import { Eye, EyeOff } from 'lucide-react'; +import { useState } from 'react'; +import SocialiteProviders from '../components/SocialiteProviders'; +import AuthCardLayout from '../layouts/AuthCardLayout'; + +export default function Login() { + const t = useT(); + const page = usePage(); + const [email, setEmail] = useState(''); + const [showPassword, setShowPassword] = useState(false); + + const { data, setData, post, processing, errors } = useForm({ + email: '', + password: '', + remember: false, + }); + + const forgotUrl = route('password.request', { email }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + post(route('login'), { preserveScroll: true }); + }; + + return ( + + + +
+ + + { setData('email', e.target.value); setEmail(e.target.value); }} + /> + {errors.email && {errors.email}} + + + + +
+ setData('password', e.target.value)} + /> + +
+ {errors.password && {errors.password}} +
+ +
+ + setData('remember', !!checked)} + /> + + {t('Remember-me')} + + + + {route().has('password.request') && ( + + {t('Forgot your password?')} + + )} +
+ + + +

+ {(page.props.auth as any)?.magic_link_enabled && ( + + {t('Login with magic link')} + + )} +

+ +

+ {t("Don't have an account?")}{' '} + + {t('Sign up')} + +

+
+
+ ); +} diff --git a/resources/js/react/pages/MagicLink.tsx b/resources/js/react/pages/MagicLink.tsx new file mode 100644 index 0000000..0b63ef3 --- /dev/null +++ b/resources/js/react/pages/MagicLink.tsx @@ -0,0 +1,55 @@ +import { Button } from '@/components/ui/button'; +import { Field, FieldError } from '@/components/ui/field'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useT } from '@/i18n'; +import { Link, useForm } from '@inertiajs/react'; +import AuthCardLayout from '../layouts/AuthCardLayout'; + +export default function MagicLink() { + const t = useT(); + const { data, setData, post, processing, errors } = useForm({ email: '' }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + post(route('magic-link.store'), { preserveScroll: true }); + }; + + return ( + +
+ + + setData('email', e.target.value)} + data-testid="magic-link-email" + /> + {errors.email && {errors.email}} + + +
+ + {t('Back to login')} + + +
+
+
+ ); +} diff --git a/resources/js/react/pages/Register.tsx b/resources/js/react/pages/Register.tsx new file mode 100644 index 0000000..8ff89bd --- /dev/null +++ b/resources/js/react/pages/Register.tsx @@ -0,0 +1,109 @@ +import { Button } from '@/components/ui/button'; +import { Field, FieldError } from '@/components/ui/field'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useT } from '@/i18n'; +import { Link, useForm } from '@inertiajs/react'; +import { Eye, EyeOff } from 'lucide-react'; +import { useState } from 'react'; +import SocialiteProviders from '../components/SocialiteProviders'; +import AuthCardLayout from '../layouts/AuthCardLayout'; + +export default function Register() { + const t = useT(); + const [showPassword, setShowPassword] = useState(false); + const { data, setData, post, processing, errors } = useForm({ + name: '', + email: '', + password: '', + password_confirmation: '', + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + post(route('register'), { preserveScroll: true }); + }; + + return ( + + + +
+ + + setData('name', e.target.value)} + /> + {errors.name && {errors.name}} + + + + + setData('email', e.target.value)} + /> + {errors.email && {errors.email}} + + + + +
+ setData('password', e.target.value)} + /> + +
+ {errors.password && {errors.password}} +
+ + + +

+ {t('Already registered?')}{' '} + + {t('Log in')} + +

+
+
+ ); +} diff --git a/resources/js/react/pages/ResetPassword.tsx b/resources/js/react/pages/ResetPassword.tsx new file mode 100644 index 0000000..f6c6830 --- /dev/null +++ b/resources/js/react/pages/ResetPassword.tsx @@ -0,0 +1,87 @@ +import { Button } from '@/components/ui/button'; +import { Field, FieldError } from '@/components/ui/field'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useT } from '@/i18n'; +import { useForm } from '@inertiajs/react'; +import AuthCardLayout from '../layouts/AuthCardLayout'; + +interface Props { + email: string; + token: string; +} + +export default function ResetPassword({ email, token }: Props) { + const t = useT(); + const { data, setData, post, processing, errors } = useForm({ + token, + email, + password: '', + password_confirmation: '', + }); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + post(route('password.store'), { preserveScroll: true }); + }; + + return ( + +
+ + + + + + {errors.email && {errors.email}} + + + + + setData('password', e.target.value)} + /> + {errors.password && {errors.password}} + + + + + setData('password_confirmation', e.target.value)} + /> + {errors.password_confirmation && {errors.password_confirmation}} + + + +
+
+ ); +} diff --git a/resources/js/react/pages/VerifyEmail.tsx b/resources/js/react/pages/VerifyEmail.tsx new file mode 100644 index 0000000..a913b16 --- /dev/null +++ b/resources/js/react/pages/VerifyEmail.tsx @@ -0,0 +1,41 @@ +import { Button } from '@/components/ui/button'; +import { useT } from '@/i18n'; +import { Link, useForm } from '@inertiajs/react'; +import AuthCardLayout from '../layouts/AuthCardLayout'; + +export default function VerifyEmail() { + const t = useT(); + const { post, processing } = useForm({}); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + post(route('verification.send')); + }; + + return ( + +
+ + +

+ + {t('Log Out')} + +

+
+
+ ); +} From f7ebd41d554706dcfbcc3f039a59683d2862e4b9 Mon Sep 17 00:00:00 2001 From: roble Date: Sun, 17 May 2026 20:26:47 +0100 Subject: [PATCH 07/12] feat(auth): update CLAUDE.md to include frontend dual-framework guidelines --- CLAUDE.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index c827c54..3eb07a7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,6 +19,14 @@ Authentication, registration, magic link (passwordless), password reset, email v | Layout | `AuthCardLayout` — card with logo, status alerts, page transitions | | Component | `SocialiteProviders` — Google/GitHub buttons with divider | +## Frontend + +Follows the dual-framework pattern (see root `CLAUDE.md` → Architecture > Frontend). + +- Both `resources/js/vue/` and `resources/js/react/` exist and must stay in sync +- `resources/js/app.ts` is a generated re-export — do not edit it directly +- `registerIcon()`, `registerAction()`, `registerGlobalComponent()` calls in `setup()` must be mirrored in both framework implementations + ## Routes **Guest routes** (`/auth/*`): login (GET/POST), register (GET/POST), forgot-password (GET/POST), reset-password/{token} (GET, signed), reset-password (POST, throttle:6,1), magic-link (GET/POST, throttle:5,1) From 487b09d5cfa096d12baef03d9239630ba5ed621a Mon Sep 17 00:00:00 2001 From: roble Date: Sat, 23 May 2026 17:44:03 +0100 Subject: [PATCH 08/12] feat(auth): update test workflow to include support for both Vue and React frameworks --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b3ba07e..f831a24 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,3 +14,4 @@ jobs: uses: saucebase-dev/saucebase/.github/workflows/test-module.yml@main with: module: auth + frameworks: '["vue","react"]' From 5a1bc239860e526dbf52f0e5c341d3a409f69ca0 Mon Sep 17 00:00:00 2001 From: roble Date: Mon, 25 May 2026 20:21:08 +0100 Subject: [PATCH 09/12] fix(auth): adjust layout styles for AuthCardLayout component --- resources/js/vue/layouts/AuthCardLayout.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/js/vue/layouts/AuthCardLayout.vue b/resources/js/vue/layouts/AuthCardLayout.vue index 01f0222..f79470a 100644 --- a/resources/js/vue/layouts/AuthCardLayout.vue +++ b/resources/js/vue/layouts/AuthCardLayout.vue @@ -21,7 +21,7 @@ defineProps<{ From 32ed030f0eebe4d0cef926ce8e7eee01b64bb493 Mon Sep 17 00:00:00 2001 From: roble Date: Wed, 27 May 2026 19:35:24 +0100 Subject: [PATCH 10/12] refactor(auth): improve code formatting and organization across multiple components --- resources/js/react/app.tsx | 7 +-- .../react/components/ImpersonationAlert.tsx | 48 ++++++++++++++---- .../react/components/SocialiteProviders.tsx | 23 +++++++-- resources/js/react/layouts/AuthCardLayout.tsx | 16 +++++- resources/js/react/pages/ForgotPassword.tsx | 18 +++++-- resources/js/react/pages/Login.tsx | 50 +++++++++++++++---- resources/js/react/pages/MagicLink.tsx | 16 ++++-- resources/js/react/pages/Register.tsx | 47 ++++++++++++++--- resources/js/react/pages/ResetPassword.tsx | 45 ++++++++++++++--- resources/js/react/pages/VerifyEmail.tsx | 6 ++- resources/js/vue/app.ts | 2 +- 11 files changed, 225 insertions(+), 53 deletions(-) diff --git a/resources/js/react/app.tsx b/resources/js/react/app.tsx index e4030f7..b6f14a5 100644 --- a/resources/js/react/app.tsx +++ b/resources/js/react/app.tsx @@ -1,8 +1,8 @@ -import '@modules/auth/resources/css/style.css'; +import { confirm } from '@/hooks/useDialog'; import { registerGlobalComponent } from '@/lib/globalComponents'; import { registerAction, registerIcon } from '@/lib/navigation'; -import { confirm } from '@/hooks/useDialog'; import { router } from '@inertiajs/react'; +import '@modules/auth/resources/css/style.css'; import { LogOut } from 'lucide-react'; import IconLogOut from '~icons/lucide/log-out'; import ImpersonationAlert from './components/ImpersonationAlert'; @@ -19,7 +19,8 @@ function registerAuthActions() { const confirmed = await confirm({ title: 'Log out', - description: 'Are you sure you want to log out? You will need to sign in again.', + description: + 'Are you sure you want to log out? You will need to sign in again.', confirmLabel: 'Log out', cancelLabel: 'Cancel', variant: 'destructive', diff --git a/resources/js/react/components/ImpersonationAlert.tsx b/resources/js/react/components/ImpersonationAlert.tsx index a18024c..28a7b62 100644 --- a/resources/js/react/components/ImpersonationAlert.tsx +++ b/resources/js/react/components/ImpersonationAlert.tsx @@ -1,10 +1,10 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { useT } from '@/i18n'; import { cn } from '@/lib/utils'; import type { User } from '@/types'; import { router, usePage } from '@inertiajs/react'; import { Drama, History, X } from 'lucide-react'; import { useCallback, useEffect, useRef, useState } from 'react'; -import { useT } from '@/i18n'; interface Impersonation { user: User; @@ -41,7 +41,10 @@ export default function ImpersonationAlert() { if (!isExpanded) return; function handleClickOutside(e: MouseEvent) { - if (alertRef.current && !alertRef.current.contains(e.target as Node)) { + if ( + alertRef.current && + !alertRef.current.contains(e.target as Node) + ) { collapse(); } } @@ -69,7 +72,11 @@ export default function ImpersonationAlert() { if (!impersonation) return null; return ( -
+
{!isExpanded ? (
- + {getUserInitials(impersonation.user.name)} @@ -114,12 +127,18 @@ export default function ImpersonationAlert() { {impersonation.user.name}

{impersonation.user.role && ( - + {impersonation.user.role} )}
-

{impersonation.user.email}

+

+ {impersonation.user.email} +

- + {getUserInitials(user.name)} @@ -154,12 +176,18 @@ export default function ImpersonationAlert() { {user.name}

{user.role && ( - + {user.role} )} -

{user.email}

+

+ {user.email} +

))} diff --git a/resources/js/react/components/SocialiteProviders.tsx b/resources/js/react/components/SocialiteProviders.tsx index 5288ab2..69dbb44 100644 --- a/resources/js/react/components/SocialiteProviders.tsx +++ b/resources/js/react/components/SocialiteProviders.tsx @@ -1,10 +1,13 @@ import { Button } from '@/components/ui/button'; -import { usePage } from '@inertiajs/react'; import { useT } from '@/i18n'; +import { usePage } from '@inertiajs/react'; import IconGithub from '~icons/simple-icons/github'; import IconGoogle from '~icons/simple-icons/google'; -type Provider = { name: string; icon: React.ComponentType<{ className?: string }> }; +type Provider = { + name: string; + icon: React.ComponentType<{ className?: string }>; +}; const providers: Provider[] = [ { name: 'google', icon: IconGoogle }, @@ -14,7 +17,9 @@ const providers: Provider[] = [ export default function SocialiteProviders() { const t = useT(); const page = usePage(); - const lastUsed = (page.props.auth as any)?.last_social_provider as string | undefined; + const lastUsed = (page.props.auth as any)?.last_social_provider as + | string + | undefined; if (!route().has('auth.socialite.redirect') || !providers.length) { return null; @@ -25,9 +30,17 @@ export default function SocialiteProviders() { {providers.map(({ name, icon: Icon }) => (
{lastUsed === name && ( diff --git a/resources/js/react/layouts/AuthCardLayout.tsx b/resources/js/react/layouts/AuthCardLayout.tsx index 76d935e..bd6d703 100644 --- a/resources/js/react/layouts/AuthCardLayout.tsx +++ b/resources/js/react/layouts/AuthCardLayout.tsx @@ -1,7 +1,13 @@ import AlertMessage from '@/components/AlertMessage'; import AppLogo from '@/components/AppLogo'; import Footer from '@/components/Footer'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; import { Head, Link, usePage } from '@inertiajs/react'; import type { ReactNode } from 'react'; @@ -13,7 +19,13 @@ interface AuthCardLayoutProps { outside?: ReactNode; } -export default function AuthCardLayout({ title, description, cardClass, children, outside }: AuthCardLayoutProps) { +export default function AuthCardLayout({ + title, + description, + cardClass, + children, + outside, +}: AuthCardLayoutProps) { const page = usePage(); const status = page.props.status as string | undefined; const error = page.props.error as string | undefined; diff --git a/resources/js/react/pages/ForgotPassword.tsx b/resources/js/react/pages/ForgotPassword.tsx index 0fa3bf7..ef709de 100644 --- a/resources/js/react/pages/ForgotPassword.tsx +++ b/resources/js/react/pages/ForgotPassword.tsx @@ -25,7 +25,11 @@ export default function ForgotPassword() { 'Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.', )} > -
+ setData('email', e.target.value)} /> - {errors.email && {errors.email}} + {errors.email && ( + + {errors.email} + + )}
@@ -50,7 +58,11 @@ export default function ForgotPassword() { > {t('Back to login')} -
diff --git a/resources/js/react/pages/Login.tsx b/resources/js/react/pages/Login.tsx index 413d9e7..6e84633 100644 --- a/resources/js/react/pages/Login.tsx +++ b/resources/js/react/pages/Login.tsx @@ -36,7 +36,11 @@ export default function Login() { > - + { setData('email', e.target.value); setEmail(e.target.value); }} + onChange={(e) => { + setData('email', e.target.value); + setEmail(e.target.value); + }} /> - {errors.email && {errors.email}} + {errors.email && ( + + {errors.email} + + )} @@ -65,19 +76,33 @@ export default function Login() { required data-testid="password" value={data.password} - onChange={(e) => setData('password', e.target.value)} + onChange={(e) => + setData('password', e.target.value) + } />
- {errors.password && {errors.password}} + {errors.password && ( + + {errors.password} + + )}
@@ -87,7 +112,9 @@ export default function Login() { name="remember" data-testid="remember-me" checked={data.remember} - onCheckedChange={(checked) => setData('remember', !!checked)} + onCheckedChange={(checked) => + setData('remember', !!checked) + } /> {t('Remember-me')} @@ -105,7 +132,12 @@ export default function Login() { )}
- diff --git a/resources/js/react/pages/MagicLink.tsx b/resources/js/react/pages/MagicLink.tsx index 0b63ef3..a431d73 100644 --- a/resources/js/react/pages/MagicLink.tsx +++ b/resources/js/react/pages/MagicLink.tsx @@ -18,9 +18,15 @@ export default function MagicLink() { return ( - + {t('Back to login')} - diff --git a/resources/js/react/pages/Register.tsx b/resources/js/react/pages/Register.tsx index 8ff89bd..6579862 100644 --- a/resources/js/react/pages/Register.tsx +++ b/resources/js/react/pages/Register.tsx @@ -31,7 +31,11 @@ export default function Register() { > - + setData('name', e.target.value)} /> - {errors.name && {errors.name}} + {errors.name && ( + + {errors.name} + + )} @@ -59,7 +67,11 @@ export default function Register() { value={data.email} onChange={(e) => setData('email', e.target.value)} /> - {errors.email && {errors.email}} + {errors.email && ( + + {errors.email} + + )} @@ -74,22 +86,41 @@ export default function Register() { required data-testid="password" value={data.password} - onChange={(e) => setData('password', e.target.value)} + onChange={(e) => + setData('password', e.target.value) + } /> - {errors.password && {errors.password}} + {errors.password && ( + + {errors.password} + + )} - diff --git a/resources/js/react/pages/ResetPassword.tsx b/resources/js/react/pages/ResetPassword.tsx index f6c6830..422a7d4 100644 --- a/resources/js/react/pages/ResetPassword.tsx +++ b/resources/js/react/pages/ResetPassword.tsx @@ -30,8 +30,17 @@ export default function ResetPassword({ email, token }: Props) { title={t('Reset Password')} description={t('Enter your new password below')} > - - + + @@ -43,7 +52,11 @@ export default function ResetPassword({ email, token }: Props) { value={data.email} readOnly /> - {errors.email && {errors.email}} + {errors.email && ( + + {errors.email} + + )} @@ -59,11 +72,17 @@ export default function ResetPassword({ email, token }: Props) { value={data.password} onChange={(e) => setData('password', e.target.value)} /> - {errors.password && {errors.password}} + {errors.password && ( + + {errors.password} + + )} - + setData('password_confirmation', e.target.value)} + onChange={(e) => + setData('password_confirmation', e.target.value) + } /> - {errors.password_confirmation && {errors.password_confirmation}} + {errors.password_confirmation && ( + + {errors.password_confirmation} + + )} - diff --git a/resources/js/react/pages/VerifyEmail.tsx b/resources/js/react/pages/VerifyEmail.tsx index a913b16..4472df5 100644 --- a/resources/js/react/pages/VerifyEmail.tsx +++ b/resources/js/react/pages/VerifyEmail.tsx @@ -19,7 +19,11 @@ export default function VerifyEmail() { "Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn't receive the email, we will gladly send you another.", )} > -
+ diff --git a/resources/js/vue/app.ts b/resources/js/vue/app.ts index d692f25..cf3ff5f 100644 --- a/resources/js/vue/app.ts +++ b/resources/js/vue/app.ts @@ -1,8 +1,8 @@ -import '@modules/auth/resources/css/style.css'; import { useDialog } from '@/composables/useDialog'; import { registerGlobalComponent } from '@/lib/globalComponents'; import { registerAction, registerIcon } from '@/lib/navigation'; import { router } from '@inertiajs/vue3'; +import '@modules/auth/resources/css/style.css'; import { trans } from 'laravel-vue-i18n'; import { LogOut } from 'lucide-vue-next'; import IconLogOut from '~icons/lucide/log-out'; From 44057a6983b99bf94efaeabbc85b58a4f14918ad Mon Sep 17 00:00:00 2001 From: Roble Date: Wed, 3 Jun 2026 21:35:37 +0100 Subject: [PATCH 11/12] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- database/seeders/DatabaseSeeder.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 5aa8a7d..02873eb 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -8,6 +8,11 @@ class DatabaseSeeder extends Seeder { public function run(): void { + if (! app()->environment(['local', 'testing'])) { + return; + } + $this->call(AuthDatabaseSeeder::class); } } +} From 2d8be00c744bf5f72ee8416a400a23877b097785 Mon Sep 17 00:00:00 2001 From: Roble Date: Wed, 3 Jun 2026 21:36:23 +0100 Subject: [PATCH 12/12] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- resources/js/react/layouts/AuthCardLayout.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/resources/js/react/layouts/AuthCardLayout.tsx b/resources/js/react/layouts/AuthCardLayout.tsx index bd6d703..d9981e8 100644 --- a/resources/js/react/layouts/AuthCardLayout.tsx +++ b/resources/js/react/layouts/AuthCardLayout.tsx @@ -59,9 +59,8 @@ export default function AuthCardLayout({ - {outside} +
+
-