diff --git a/CLAUDE.md b/CLAUDE.md index 3eb07a7..1c4b260 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,9 +12,10 @@ Authentication, registration, magic link (passwordless), password reset, email v | Requests | `LoginRequest` (credential validation + rate limiting), `RegisterRequest` (password hashing in `passedValidation`) | | Exceptions | `AuthException` (credentials, throttle), `SocialiteException` (disconnect, account linking, provider validation) | | Listeners | `AssignUserRole` (Registered), `UpdateUserLastLogin` (Login), `Impersonation` (TakeImpersonation — session history) | -| Notifications | `WelcomeNotification` (Registered), `MagicLinkNotification` (passwordless login link, 15-min expiry) | +| Notifications | `WelcomeNotification` (Registered), `MagicLinkNotification` (passwordless login link with configured expiry) | +| Settings | `AuthSettings` (`magic_link_enabled`, `magic_link_expiry`) | | Trait | `Sociable` — added to User model (socialAccounts relation, connected_providers, disconnect) | -| Filament | `AuthPlugin`, `UserResource` (list, create, view, edit), `UserForm`, `UsersTable` | +| Filament | `AuthPlugin`, `AuthenticationSettings`, `UserResource` (list, create, view, edit), `UserForm`, `UsersTable` | | Pages | `Login`, `Register`, `ForgotPassword`, `ResetPassword`, `VerifyEmail`, `MagicLink` | | Layout | `AuthCardLayout` — card with logo, status alerts, page transitions | | Component | `SocialiteProviders` — Google/GitHub buttons with divider | @@ -60,10 +61,15 @@ Also prevents account takeover: linking a social ID already owned by another use Uses `lab404/laravel-impersonate` + `filament-impersonate`. Session stores history at `impersonation.recent_history` (max 4 user IDs). `ReimpersonateController` lets admins re-impersonate from recent list (max 3 shown in UI, filters deleted users and self). Stop via `filament-impersonate.leave` route. ### Magic Link Flow -`MagicLinkController::store()` silently finds the user by email (no error on unknown email). If found: deletes existing tokens for that user, creates a new `MagicLinkToken` (token = SHA-256 hash of `Str::random(64)`, expires in 15 min), and sends `MagicLinkNotification` with the plain-token URL. +`MagicLinkController::store()` silently finds the user by email (no error on unknown email). If found: deletes existing tokens for that user, creates a new `MagicLinkToken` (token = SHA-256 hash of `Str::random(64)`, expiry controlled by `AuthSettings`), and sends `MagicLinkNotification` with the plain-token URL. `MagicLinkController::authenticate()` hashes the incoming token, looks it up, calls `isValid()` (not expired + not used), logs in the user, marks the token used, and redirects to intended URL or dashboard. +`AuthSettings` is auto-discovered from `src/Settings`, with defaults installed +from `database/settings`. Magic links are enabled by default with a 15-minute +expiry. Administrators manage both values through the `AuthenticationSettings` +Filament page under the Settings navigation group. + **Token storage:** Plain token only lives in the email link. DB stores `hash('sha256', $plainToken)`. This means even if the DB is compromised, tokens cannot be forged or replayed. ### Logout Action Handler diff --git a/config/config.php b/config/config.php deleted file mode 100644 index d1aaae6..0000000 --- a/config/config.php +++ /dev/null @@ -1,20 +0,0 @@ - [ - 'enabled' => env('AUTH_MAGIC_LINK_ENABLED', true), - 'expiry' => env('AUTH_MAGIC_LINK_EXPIRY', 15), // minutes - ], - -]; diff --git a/database/settings/2026_07_30_160000_create_auth_settings.php b/database/settings/2026_07_30_160000_create_auth_settings.php new file mode 100644 index 0000000..b82be70 --- /dev/null +++ b/database/settings/2026_07_30_160000_create_auth_settings.php @@ -0,0 +1,12 @@ +migrator->add('auth.magic_link_enabled', true); + $this->migrator->add('auth.magic_link_expiry', 15); + } +}; diff --git a/resources/js/react/layouts/AuthCardLayout.tsx b/resources/js/react/layouts/AuthCardLayout.tsx index 132efc8..cf1c212 100644 --- a/resources/js/react/layouts/AuthCardLayout.tsx +++ b/resources/js/react/layouts/AuthCardLayout.tsx @@ -1,14 +1,4 @@ -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 CardLayout from '@/layouts/CardLayout'; import type { ReactNode } from 'react'; interface AuthCardLayoutProps { @@ -19,6 +9,13 @@ interface AuthCardLayoutProps { outside?: ReactNode; } +/** + * The auth module's name for the shared centred-card layout. + * + * The presentation moved to core so that naming a workspace looks like signing up rather + * than like a different product. This wrapper stays so the module's pages keep their own + * vocabulary, and so anything auth-specific has an obvious home later. + */ export default function AuthCardLayout({ title, description, @@ -26,42 +23,14 @@ export default function AuthCardLayout({ 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 ? ( -
- -
- ) : null} - {children} -
-
-
- {outside} -
-
+ + {children} + ); } diff --git a/resources/js/react/pages/Register.tsx b/resources/js/react/pages/Register.tsx index 6612f6b..4971378 100644 --- a/resources/js/react/pages/Register.tsx +++ b/resources/js/react/pages/Register.tsx @@ -140,7 +140,7 @@ export default function Register() { /> {t('I agree to the')}{' '} +
+

+ {t( + 'Before getting started, could you verify your email address by clicking on the link we just emailed to you?', + )} +

+

+ {t( + 'If you did not receive the email, you can click the button below to request another.', + )} +

@@ -33,7 +38,7 @@ export default function VerifyEmail() { href={route('logout')} method="post" as="button" - className="text-primary/70 cursor-pointer font-medium underline-offset-4 hover:underline" + className="text-primary cursor-pointer font-medium underline-offset-4 hover:underline" data-testid="logout-link" > {t('Log Out')} diff --git a/resources/js/vue/app.ts b/resources/js/vue/app.ts index 14edbcc..ac952b9 100644 --- a/resources/js/vue/app.ts +++ b/resources/js/vue/app.ts @@ -2,9 +2,9 @@ import { useDialog } from '@/composables/useDialog'; import { registerGlobalComponent } from '@/lib/globalComponents'; import { registerAction, registerIcon } from '@/lib/navigation'; import { router } from '@inertiajs/vue3'; +import { LogOut } from '@lucide/vue'; import '@modules/auth/resources/css/style.css'; import { trans } from 'laravel-vue-i18n'; -import { LogOut } from '@lucide/vue'; import IconLogOut from '~icons/lucide/log-out'; import ImpersonationAlert from './components/ImpersonationAlert.vue'; diff --git a/resources/js/vue/layouts/AuthCardLayout.vue b/resources/js/vue/layouts/AuthCardLayout.vue index 8f1d4cf..5bf2ef8 100644 --- a/resources/js/vue/layouts/AuthCardLayout.vue +++ b/resources/js/vue/layouts/AuthCardLayout.vue @@ -1,18 +1,13 @@ diff --git a/resources/js/vue/pages/Register.vue b/resources/js/vue/pages/Register.vue index 36eac09..547de3d 100644 --- a/resources/js/vue/pages/Register.vue +++ b/resources/js/vue/pages/Register.vue @@ -84,7 +84,10 @@ const canSubmit = computed( :aria-invalid="!!termsError" v-model="termsRef" /> - + {{ $t('I agree to the') }}