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 ( -