Skip to content

OTP

Directory: app/Services/Otp/

One-Time Password generation and verification service. Used by the setup wizard's email verification step, and by any feature that needs email-based code verification.

Overview

The OTP service provides two operations:

  1. Generate — creates a 6-digit numeric code, stores it in the otps table with a 10-minute expiry
  2. Verify — checks a submitted code against the stored one, handles expiry and invalid codes

Why It Exists

  • Security: Codes are generated using random_int() (cryptographically secure), not rand()
  • Self-cleaning: Old codes are deleted before new ones are generated; verified/expired codes are deleted on read
  • Traceable: Each OTP stores metadata (email, IP, user agent) for audit purposes
  • Time-limited: Codes expire after 10 minutes — no stale codes lingering

Methods

Generate::handle(string $email, array $meta = []): Otp

Generates a new OTP code for the given email address.

php
public static function handle(string $email, array $meta = []): Otp

Algorithm:

  1. Delete all existing OTPs for this email (clean slate)
  2. Generate a random 6-digit code: str_pad(random_int(0, 999999), 6, '0', STR_PAD_LEFT)
  3. Store in otps table with 10-minute expiry
  4. Return the Otp model instance

Example:

php
use App\Services\Otp\Generate;

$otp = Generate::handle('user@example.com', [
    'ip' => request()->ip(),
]);

// Send $otp->code to the user's email

Verify::handle(string $email, string $code): array

Verifies a submitted OTP code against stored records.

php
public static function handle(string $email, string $code): array

Algorithm:

  1. Query otps table where code matches AND meta->email matches
  2. If no match → return { success: false, message: 'Invalid or expired verification code.' }
  3. If matched but expired ($otp->isExpired()) → delete it, return { success: false, message: 'Verification code has expired.' }
  4. If valid → delete the OTP (one-time use), return { success: true, message: 'Email verified successfully.', otp: $otp }

Return values:

php
[
    'success' => bool,   // true if code is valid and not expired
    'message' => string, // human-readable status
    'otp'     => Otp|null, // the OTP model (only on success)
]

Example:

php
use App\Services\Otp\Verify;

$result = Verify::handle('user@example.com', '123456');

if ($result['success']) {
    // Email verified — proceed with registration
} else {
    // Show $result['message'] to the user
}

Frontend Integration

The OTP flow in the setup wizard follows the stores→services→api pattern:

super-user-otp-form.stores.ts
    ↓ sendOtp(email) / verifyOtp(email, code)
otp.services.ts
    ↓ send() / verify()
otp.api.ts
    ↓ POST /api/setup/send-otp / POST /api/setup/verify-otp
OtpController (Laravel)
    ↓ Generate::handle() / Verify::handle()

Related stores:

StorePurpose
super-user-otp-form.stores.tsOTP code input + send/verify state
super-user-otp-modal.stores.tsOTP modal open/close
super-user-otp-resend.stores.tsResend countdown timer (60s)
super-user-otp-validation.stores.tsOTP-related validation

Key Files

FilePurpose
app/Services/Otp/Generate.phpOTP code generation + database storage
app/Services/Otp/Verify.phpOTP code verification + expiry check
app/Models/Platform/Otp/Otp.phpThe otps model — code, expired_at, meta (JSON)
app/Http/Controllers/Api/OtpController.phpAPI controller (send + verify endpoints)
app/Observers/OtpObserver.phpOptional: cleanup or notification triggers
resources/js/modules/landing/setup/form/super-user/stores/otp-entry/Frontend OTP stores