Run the stack with Postgres, Redis, PHP-FPM, and Nginx.
- Build and start
docker compose up -d --build-
App is available at http://localhost:8080
-
First run initializes:
- Composer install
- php artisan key:generate
- php artisan jwt:secret
- Migrations
- Useful commands
# Tail logs
docker compose logs -f app
docker compose logs -f web
# Run tests
docker compose exec app php artisan test
# Run migrations
docker compose exec app php artisan migrate --force-
Goal: Provide a secure OTP flow for login/registration, then issue JWTs and guide users to complete profile verification.
-
API surface (
routes/api/v1.php):POST /api/v1/otp/send→OtpController@sendPOST /api/v1/otp/verify→OtpController@verifyPOST /api/v1/verification(auth:api) →VerificationController@verificationGET /api/v1/panel(auth:api+user.verified) →PanelController@index
-
Layering:
- Thin controllers in
app/Http/Controllers/**keep HTTP concerns separate from business logic. - Core logic lives in services:
app/Services/Otp/SendOtpService.php: generate/persist OTP, cache, dispatch SMS.app/Services/Otp/VerifyOtpService.php: load latest active OTP, validate, consume, attempt tracking.app/Services/UserService.php: find-or-create user by phone and issue JWT.
- Validation via
FormRequests and a custom rule:SendRequest,VerifyRequest, andRules/Otp/ValidateOtpCode(enforces IP/device binding, attempts, hash match).
- Thin controllers in
-
Data model (
app/Models/OtpToken.php+ migration):- Stores
phone,code_hash,salt,purpose,attempts_count,max_attempts,expires_at,consumed_at,request_ip,device_id. - Mutator writes
code_hashassha256(pepper + salt + code)and generates a per-record randomsalt. - Reasoning: never store plaintext codes; per-record salts defeat rainbow tables; app-level pepper adds defense-in-depth.
- Stores
-
Security choices:
- Attempt limiting:
attempts_count <= max_attempts(configurable viaconfig/otp.php). - Binding: Verification requires the same
request_ipandX-Device-Idused at issuance to reduce relay abuse. - Rate limiting:
RouteServiceProviderdefinesotp-sendlimits (per-hour IP and resend cooldown per IP/phone) returning standardized JSON viaCustomResponse. - Cache: Latest OTP cached until
expires_atviaOtpCache(Redis) for fast lookups and to ensure only the newest OTP is valid.
- Attempt limiting:
-
Authentication:
- JWT via
tymon/jwt-authon theapiguard (config/auth.php,config/jwt.php). - Reasoning: stateless mobile-friendly API, clear TTLs, blacklist support, subject locking enabled.
- JWT via
-
Middleware:
AcceptJsonMiddlewareforcesAccept: application/jsonfor consistent API responses.EnsureUserVerifiedprevents panel access until profile verification is completed.
-
Configuration:
config/otp.phpexposes length, TTL, attempts, cooldowns, hourly limits, pepper, and cleanup days; all overrideable via env.
-
Operations:
- Console task
otp:cleanupremoves old OTPs; scheduled daily at 02:30 inbootstrap/app.php. - Dockerized stack: PHP-FPM app, Nginx, Postgres, Redis; a one-shot
migrateservice ensures schema readiness. - Healthchecks ensure Nginx waits for PHP-FPM; entrypoint sets app key and JWT secret.
- Console task
-
Testing:
- Feature tests cover send/verify flows, limits, and edge cases (
tests/Feature/Otp/*). - Unit tests cover services, rule behavior, and the OTP model mutator.
- Feature tests cover send/verify flows, limits, and edge cases (
-
Trade-offs and extensions:
- SMS dispatch is logged via
SmsServicefor local/dev; swap with a real provider without changing business logic. - Default OTP length (6) and short TTL balance UX and security; adjust via env for your threat model.
- Add additional verification fields or steps in
VerificationControllerwithout altering OTP core.
- SMS dispatch is logged via
For deeper details, see README-DECISIONS.md and the inline docblocks in the referenced classes.