1.7 KiB
OTP Authentication
Staff authentication uses a passwordless OTP flow. No password is stored or required.
Flow
- Staff enters their email on the login page
- A 6-digit code is generated, stored on the
staffrecord, and emailed - Staff enters the code — if valid and not expired, they are logged in
Implementation
OtpService
Modules\Core\Services\OtpService handles generation and validation.
// Generate and email a code
$otpService->generateAndSend(string $email): bool
// Validate a submitted code — returns the Staff model on success, null on failure
$otpService->validate(string $email, string $code): ?Staff
Codes expire after 10 minutes. After a successful validation the code is cleared from the record.
Database
Two columns on the lunar_staff table (added by 2026_05_06_000001_add_otp_to_lunar_staff_table):
| Column | Type | Purpose |
|---|---|---|
otp_code |
string, nullable | The generated code |
otp_expires_at |
timestamp, nullable | Expiry time |
Login Page
Modules\Core\Filament\Pages\Login is a Filament SimplePage registered as the panel login via AppServiceProvider:
LunarPanel::panel(fn (Panel $panel) => $panel->login(Login::class));
It has two steps rendered in core::filament.pages.login:
- Step 1 — email form, submits to
requestOtp() - Step 2 — code form, submits to
authenticate()
After a valid code canAccessPanel() is checked before the session is established.
Modules\Core\Mail\OtpMail sends the code using the core::mail.otp view.
Disabling 2FA
Lunar's built-in 2FA plugin is disabled in AppServiceProvider since the OTP login replaces it:
LunarPanel::disableTwoFactorAuth();