Skip to main content

New Authentication & Onboarding API

Authentication & Onboarding API Specification

Overview

Hybrid Auth Strategy:

  • Signup: Phone/Email + OTP (passwordless initially)
  • After onboarding: Optional password setup
  • Login: Password (if set) OR OTP OR Google/Apple
  • Sensitive actions: Require OTP or password confirmation

Response Format Standard: All responses follow GlobeSuccessResponseBuilder or GlobeFailureResponseBuilder format:

{
  "success": true/false,
  "httpStatus": "OK/BAD_REQUEST/etc",
  "message": "Human readable message",
  "action_time": "2025-01-11T15:20:00",
  "data": { ... }
}

SystemDEVICE UsernameSECURITY ARCHITECTURE

The Problem We're Solving

Without hardware-bound keys:
─────────────────────────────────────────────────────────
Attacker steals victim's password
    ↓
Attacker generates fake deviceId: "fake-device-123"
    ↓
Attacker logs in → Server asks for OTP
    ↓
Attacker does SIM swap / social engineering → gets OTP
    ↓
Attacker is now "trusted" forever 😱
    ↓
Victim can't kick attacker out (attacker has valid device)


With hardware-bound keys:
─────────────────────────────────────────────────────────
Attacker steals victim's password
    ↓
Attacker tries to login with fake deviceId
    ↓
Server: "Sign this challenge with your private key"
    ↓
Attacker: "I don't have the private key..." 😤
    ↓
Private key is locked inside victim's phone hardware
    ↓
Attack FAILS ✅

Asymmetric Cryptography (Key Pair Concept)

┌─────────────────────────────────────────────────────────────┐
│                    ASYMMETRIC CRYPTOGRAPHY                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Private Key                      Public Key               │
│   ───────────                      ──────────               │
│   • Secret                         • Shareable              │
│   • Never leaves device            • Stored on server       │
│   • Used to SIGN                   • Used to VERIFY         │
│   • Locked in hardware             • Anyone can have it     │
│                                                             │
│   ┌─────────────┐                 ┌─────────────┐          │
│   │ Private Key │──── generates ──▶│ Public Key  │          │
│   │    🔐       │                  │    🔓       │          │
│   └─────────────┘                  └─────────────┘          │
│         │                                │                  │
│         ▼                                ▼                  │
│   Sign("hello")                    Verify(signature)        │
│         │                                │                  │
│         ▼                                ▼                  │
│   "MEUCIQC7..."                    true / false             │
│   (signature)                                               │
│                                                             │
│   KEY POINT: You CANNOT derive private key from public key  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Platform Security Overview

┌─────────────────────────────────────────────────────────────┐
│                         iOS DEVICE                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   ┌─────────────────────────────────────────────────────┐  │
│   │                    Main Processor                    │  │
│   │   Your app lives here                                │  │
│   │   Can request signatures                             │  │
│   │   CANNOT access private key                          │  │
│   └─────────────────────────────────────────────────────┘  │
│                           │                                 │
│                           │ "Please sign this"              │
│                           ▼                                 │
│   ┌─────────────────────────────────────────────────────┐  │
│   │              SECURE ENCLAVE (Separate Chip)          │  │
│   │   ┌─────────────────────────────────────────────┐   │  │
│   │   │   Private Key 🔐                            │   │  │
│   │   │   • Generated HERE                          │   │  │
│   │   │   • Stored HERE                             │   │  │
│   │   │   • NEVER leaves                            │   │  │
│   │   │   • Cannot be read by main processor        │   │  │
│   │   │   • Cannot be extracted even if jailbroken  │   │  │
│   │   └─────────────────────────────────────────────┘   │  │
│   │   Returns: signature (NOT the key)                   │  │
│   └─────────────────────────────────────────────────────┘  │
│                                                             │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│                       ANDROID DEVICE                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Option A: StrongBox (Dedicated Security Chip)             │
│   • Separate hardware chip (like iOS Secure Enclave)        │
│   • Best security                                           │
│   • Available on Pixel 3+, Samsung S10+, etc.               │
│                                                             │
│   Option B: TEE (Trusted Execution Environment)             │
│   • Isolated area within main processor                     │
│   • Very good security                                      │
│   • Available on most Android 7+ devices                    │
│                                                             │
│   Both use Android Keystore API                             │
│   Same result: Private key cannot be extracted              │
│                                                             │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│                       WEB BROWSER                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   Problem: No dedicated hardware security                   │
│   Solution: PKCE-style ephemeral keys (explained below)     │
│                                                             │
│   • Generate keypair in MEMORY (not stored)                 │
│   • Session-bound (dies when tab closes)                    │
│   • Combined with fingerprint + token binding               │
│   • NEVER fully trusted (always require OTP for sensitive)  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Platform Security Comparison

AspectiOSAndroidWeb
Key StorageSecure EnclaveStrongBox / TEEMemory only
Hardware Protected✅ Yes✅ Yes❌ No
Key Extractable❌ Never❌ NeverN/A (ephemeral)
Forgery Possible❌ No❌ No⚠️ Harder
Trust Level⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Trust Duration30 days30 days7 days
Sensitive ActionsSome without OTPSome without OTPALWAYS OTP

Why Attacker Cannot Forge (Mobile)

What attacker has access to:
──────────────────────────────────────────────────────────────
✅ Victim's email/username (public or leaked)
✅ Victim's password (phishing, data breach, etc.)
✅ Victim's deviceId (could intercept network traffic)
✅ Victim's publicKey (stored on server, not secret)
✅ Can request fresh nonce anytime

What attacker CANNOT get:
──────────────────────────────────────────────────────────────
❌ Victim's privateKey (locked in hardware)
   • Cannot extract from Secure Enclave
   • Cannot extract even with physical device access
   • Cannot extract even if device is jailbroken

Without privateKey:
──────────────────────────────────────────────────────────────
❌ Cannot create valid signature
❌ Server rejects login
❌ Attack fails

Challenge/Nonce Purpose

Without nonce:
──────────────────────────────────────────────────────────────
1. Victim logs in legitimately
2. Attacker intercepts: { deviceId, signature }
3. Attacker replays exact same request
4. Server: "Signature valid!" ✅
5. Attacker is in 😱

Problem: Signature is always the same for same deviceId


With nonce:
──────────────────────────────────────────────────────────────
1. Victim logs in legitimately
   - Gets nonce "ch_abc123"
   - Signs "ch_abc123|timestamp|deviceId"
   
2. Attacker intercepts the request
   
3. Attacker replays exact same request 1 minute later
   
4. Server: "Nonce ch_abc123 already used/expired!" ❌

5. Attacker tries to get new nonce and replay
   - Gets nonce "ch_xyz789"
   - But old signature was for "ch_abc123"
   - Signature doesn't match new nonce ❌
   
6. Attacker cannot create new signature
   - Needs privateKey to sign "ch_xyz789|..."
   - privateKey is in victim's phone ❌

7. Attack fails ✅


Key insight: 
──────────────────────────────────────────────────────────────
Nonce makes each signature UNIQUE and TIME-LIMITED
Even captured valid signatures become useless after ~60 seconds

WEB SECURITY MODEL (PKCE-Style)

The Problem: Web Cannot Keep Secrets

Mobile App:
────────────────────────────────────────────────────────
✅ Compiled binary (hard to reverse engineer)
✅ Secure storage (Keychain, Keystore)
✅ Hardware protection (Secure Enclave, TEE)
✅ Can store secrets safely


Web Browser:
────────────────────────────────────────────────────────
❌ JavaScript is readable (View Source)
❌ localStorage/IndexedDB accessible via DevTools
❌ No hardware-protected storage
❌ Any "secret" can be extracted

If we store a private key in browser:
→ Open DevTools → Application → IndexedDB → Copy the key → Use anywhere 😱

The Solution: PKCE-Style Ephemeral Keys

OAuth2 had the same problem. Their solution: Don't store a secret. Generate a temporary one-time proof.

┌─────────────────────────────────────────────────────────────┐
│              WEB DEVICE AUTH (PKCE-Style)                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Instead of:                                                │
│  Store private key → Sign challenges                        │
│  (Private key can be stolen from IndexedDB)                 │
│                                                             │
│  We do:                                                     │
│  Each session: Generate fresh keypair in MEMORY             │
│  Register public key with server for THIS SESSION           │
│  Private key lives only in JavaScript memory                │
│  When tab closes → key is gone → nothing to steal           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Web Security Layers

┌─────────────────────────────────────────────────────────────┐
│                    WEB SECURITY MODEL                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  LAYER 1: Session-Bound Cryptographic Proof                 │
│  • Generate keypair in memory (not stored)                  │
│  • Proves: "Same browser tab that started auth"             │
│  • Prevents: Request interception/replay                    │
│                                                             │
│  LAYER 2: Browser Fingerprint                               │
│  • Collect browser characteristics                          │
│  • Proves: "Likely same browser/device"                     │
│  • Prevents: Token theft to different browser               │
│                                                             │
│  LAYER 3: Bound Tokens                                      │
│  • Tokens bound to fingerprint + IP range                   │
│  • Server validates on each request                         │
│  • Prevents: Token theft/export                             │
│                                                             │
│  LAYER 4: Never Fully Trust                                 │
│  • Web devices NEVER get "trusted" status                   │
│  • Sensitive actions ALWAYS require OTP                     │
│  • Shorter token lifetime than mobile                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Web vs DisplayMobile UsernameTrust ArchitectureLevels

┌─────────────────────────────────────────────────────────────┐
│                  TRUST LEVEL COMPARISON                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  MOBILE (iOS/Android):                                      │
│  After OTP verification:                                    │
│  • Device becomes "TRUSTED"                                 │
│  • Trust lasts 30 days                                      │
│  • Password-only login allowed                              │
│  • Most actions without re-auth                             │
│  • Hardware key proves device identity                      │
│                                                             │
│  WEB (Browser):                                             │
│  After OTP verification:                                    │
│  • Device is "RECOGNIZED" (not trusted)                     │
│  • Recognition lasts 7 days (shorter)                       │
│  • Password-only login allowed for basic actions            │
│  • Sensitive actions ALWAYS need OTP:                       │
│    • Change password                                        │
│    • Change email/phone                                     │
│    • View payment methods                                   │
│    • Delete account                                         │
│    • Large purchases                                        │
│  • Session-bound keys (weaker than hardware)                │
│                                                             │
│  WHY THE DIFFERENCE:                                        │
│  Mobile: Hardware guarantees "this is THE device"           │
│  Web: Software only guarantees "probably same browser"      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

AUTHENTICATION FLOWS

Device Registration (First App Launch - Mobile)

┌──────────────┐                              ┌──────────────┐
│    Device    │                              │    Server    │
└──────┬───────┘                              └──────┬───────┘
       │                                             │
       │  1. Generate key pair in hardware           │
       │     Private key → Secure Enclave            │
       │     Public key → exportable                 │
       │                                             │
       │  2. GET /auth/challenge                     │
       │────────────────────────────────────────────▶│
       │                                             │
       │  3. { nonce: "ch_abc123", expiresIn: 60 }   │
       │◀────────────────────────────────────────────│
       │                                             │
       │  4. Sign nonce with private key             │
       │     message = "ch_abc123|timestamp|deviceId"│
       │     signature = sign(message, privateKey)   │
       │                                             │
       │  5. POST /auth/device/register              │
       │     {                                       │
       │       deviceId: "ios_xyz...",               │
       │       publicKey: "MFkw...",                 │
       │       nonce: "ch_abc123",                   │
       │       timestamp: 1736611200000,             │
       │       signature: "MEUC...",                 │
       │       platform: "IOS"                       │
       │     }                                       │
       │────────────────────────────────────────────▶│
       │                                             │
       │                      6. Verify signature    │
       │                         using publicKey     │
       │                                             │
       │                      7. Store:              │
       │                         deviceId → publicKey│
       │                                             │
       │  8. { success: true, deviceId: "ios_xyz" }  │
       │◀────────────────────────────────────────────│
       │                                             │

Login Flow (Mobile - With Device Signature)

┌──────────────┐                              ┌──────────────┐
│    Device    │                              │    Server    │
└──────┬───────┘                              └──────┬───────┘
       │                                             │
       │  1. GET /auth/challenge                     │
       │────────────────────────────────────────────▶│
       │                                             │
       │  2. { nonce: "ch_xyz789", expiresIn: 60 }   │
       │◀────────────────────────────────────────────│
       │                                             │
       │  3. Sign: signature = sign(                 │
       │       "ch_xyz789|1736611200000|ios_xyz...", │
       │       privateKey                            │
       │     )                                       │
       │                                             │
       │  4. POST /auth/login                        │
       │     {                                       │
       │       identifier: "alex@email.com",         │
       │       password: "***",                      │
       │       deviceAuth: {                         │
       │         deviceId: "ios_xyz...",             │
       │         nonce: "ch_xyz789",                 │
       │         timestamp: 1736611200000,           │
       │         signature: "MEUC...",               │
       │         platform: "IOS"                     │
       │       }                                     │
       │     }                                       │
       │────────────────────────────────────────────▶│
       │                                             │
       │                      5. Validate nonce      │
       │                         (exists in Redis?)  │
       │                                             │
       │                      6. Delete nonce        │
       │                         (one-time use)      │
       │                                             │
       │                      7. Get publicKey       │
       │                         for deviceId        │
       │                                             │
       │                      8. Verify signature    │
       │                                             │
       │                      9. Check password      │
       │                                             │
       │                     10. Check device trust  │
       │                         status              │
       │                                             │
       │  11. { accessToken, refreshToken, ... }     │
       │◀────────────────────────────────────────────│
       │                                             │

Web Session Flow (PKCE-Style)

┌──────────────┐                              ┌──────────────┐
│   Browser    │                              │    Server    │
└──────┬───────┘                              └──────┬───────┘
       │                                             │
       │  User opens login page                      │
       │                                             │
       │  1. Generate keypair IN MEMORY              │
       │     const keyPair = await crypto.subtle    │
       │       .generateKey(ECDSA, P-256)            │
       │     ⚠️ NOT stored anywhere                  │
       │     ⚠️ Lives only in JS variable            │
       │                                             │
       │  2. Generate browser fingerprint            │
       │     • Screen size, timezone, language       │
       │     • Canvas hash, WebGL renderer           │
       │     • → Hash all into single ID             │
       │                                             │
       │  3. POST /auth/web/session                  │
       │     {                                       │
       │       publicKey: "MFkw...",                 │
       │       fingerprint: "fp_abc123..."           │
       │     }                                       │
       │────────────────────────────────────────────▶│
       │                                             │
       │                      4. Generate sessionId  │
       │                      5. Store in Redis:     │
       │                         sessionId →         │
       │                           publicKey,        │
       │                           fingerprint,      │
       │                           ipAddress         │
       │                         TTL: 10 minutes     │
       │                                             │
       │  6. { sessionId: "ws_xyz...",               │
       │       nonce: "ch_abc..." }                  │
       │◀────────────────────────────────────────────│
       │                                             │
       │  User enters email + password               │
       │                                             │
       │  7. Sign the nonce                          │
       │     signature = sign(                       │
       │       nonce + timestamp + sessionId,        │
       │       privateKey  ← still in memory         │
       │     )                                       │
       │                                             │
       │  8. POST /auth/login                        │
       │     {                                       │
       │       identifier: "alex@email.com",         │
       │       password: "***",                      │
       │       webAuth: {                            │
       │         sessionId: "ws_xyz...",             │
       │         nonce: "ch_abc...",                 │
       │         timestamp: 1736611200000,           │
       │         signature: "MEUC...",               │
       │         fingerprint: "fp_abc123..."         │
       │       }                                     │
       │     }                                       │
       │────────────────────────────────────────────▶│
       │                                             │
       │                      9. Get session from    │
       │                         Redis               │
       │                                             │
       │                     10. Verify:             │
       │                         • Session exists    │
       │                         • Not expired       │
       │                         • Signature valid   │
       │                         • Fingerprint match │
       │                         • IP in range       │
       │                                             │
       │                     11. Delete session      │
       │                         (one-time use)      │
       │                                             │
       │                     12. Create BOUND tokens │
       │                         (bound to fp + IP)  │
       │                                             │
       │  13. {                                      │
       │        accessToken: "...",                  │
       │        refreshToken: "...",                 │
       │        device: { trusted: false }           │
       │      }                                      │
       │◀────────────────────────────────────────────│
       │                                             │

SCALABLE ARCHITECTURE

┌─────────────────────────────────────────────────────────────────────────────────┐
│                          NEXTGATE SECURITY ARCHITECTURE                          │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                  │
│  ┌───────────────────────────────────────────────────────────────────────────┐  │
│  │                         API GATEWAY / LOAD BALANCER                        │  │
│  │                    (Rate Limiting, DDoS Protection)                        │  │
│  └───────────────────────────────────────────────────────────────────────────┘  │
│                                  │                                               │
│                                  ▼                                               │
│  ┌───────────────────────────────────────────────────────────────────────────┐  │
│  │                        AUTH SERVICE (Stateless)                            │  │
│  │  ┌─────────────────────────────────────────────────────────────────────┐  │  │
│  │  │                    Challenge/Nonce Service                           │  │  │
│  │  │  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐                 │  │  │
│  │  │  │ Node 1  │  │ Node 2  │  │ Node 3  │  │ Node N  │  (Stateless)    │  │  │
│  │  │  └─────────┘  └─────────┘  └─────────┘  └─────────┘                 │  │  │
│  │  │       │            │            │            │                       │  │  │
│  │  │       └────────────┴─────┬──────┴────────────┘                       │  │  │
│  │  │                          │                                           │  │  │
│  │  │                          ▼                                           │  │  │
│  │  │              ┌───────────────────────┐                               │  │  │
│  │  │              │   Redis Cluster       │  (Nonce storage, 60s TTL)     │  │  │
│  │  │              │   (High Availability) │                               │  │  │
│  │  │              └───────────────────────┘                               │  │  │
│  │  └─────────────────────────────────────────────────────────────────────┘  │  │
│  │                                                                            │  │
│  │  ┌─────────────────────────────────────────────────────────────────────┐  │  │
│  │  │                  Signature Verification Service                      │  │  │
│  │  │  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐                 │  │  │
│  │  │  │ Node 1  │  │ Node 2  │  │ Node 3  │  │ Node N  │  (Stateless)    │  │  │
│  │  │  └─────────┘  └─────────┘  └─────────┘  └─────────┘                 │  │  │
│  │  │       │            │            │            │                       │  │  │
│  │  │       └────────────┴─────┬──────┴────────────┘                       │  │  │
│  │  │                          │                                           │  │  │
│  │  │                          ▼                                           │  │  │
│  │  │     ┌───────────────────────────────────────────────────────┐       │  │  │
│  │  │     │              PostgreSQL (Primary-Replica)              │       │  │  │
│  │  │     │  device_keys table (deviceId → publicKey)              │       │  │  │
│  │  │     │  Cached in Redis for fast lookups                      │       │  │  │
│  │  │     └───────────────────────────────────────────────────────┘       │  │  │
│  │  └─────────────────────────────────────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────────────────────────────────────┘  │
│                                                                                  │
│  SCALABILITY:                                                                    │
│  • Challenge generation: < 5ms (any node can generate)                           │
│  • Signature verification: < 10ms (pure computation)                             │
│  • Public keys cached in Redis (1-hour TTL)                                      │
│  • Can handle 10,000+ logins/second per node                                     │
│  • Horizontal scaling: just add more nodes                                       │
│                                                                                  │
└──────────────────────────────────────────────────────────────────────────────────┘

TOKEN BINDING (Web Extra Protection)

┌─────────────────────────────────────────────────────────────┐
│                     TOKEN BINDING                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Traditional Token:                                         │
│  {                                                          │
│    "sub": "user_123",                                       │
│    "exp": 1736614800                                        │
│  }                                                          │
│  Problem: Anyone with this token can use it                 │
│                                                             │
│                                                             │
│  Bound Token (What we do for web):                          │
│  {                                                          │
│    "sub": "user_123",                                       │
│    "exp": 1736614800,                                       │
│    "device_fp": "fp_abc123...",     ← Must match           │
│    "ip_hash": "a1b2c3...",          ← Must be in range     │
│    "platform": "WEB"                 ← Affects trust level  │
│  }                                                          │
│                                                             │
│  On every request, server checks:                           │
│  • Current fingerprint ≈ token's device_fp                  │
│  • Current IP in same /24 range as token's IP               │
│  • If mismatch → reject OR require re-auth                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

BROWSER FINGERPRINT

┌─────────────────────────────────────────────────────────────┐
│                  BROWSER FINGERPRINT                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Components collected:                                      │
│  1. Screen: "1920x1080"                                     │
│  2. Color Depth: 24                                         │
│  3. Timezone: "Africa/Dar_es_Salaam"                        │
│  4. Language: "en-US"                                       │
│  5. Platform: "MacIntel"                                    │
│  6. CPU Cores: 8                                            │
│  7. Memory: 8 (GB, approximate)                             │
│  8. Canvas Hash: "a1b2c3..." (drawn image fingerprint)      │
│  9. WebGL Renderer: "Apple M1"                              │
│  10. Audio Context fingerprint                              │
│                                                             │
│  All combined → SHA256 → "fp_7f9a2b3c..."                   │
│                                                             │
│  Stability:                                                 │
│  • ~90% of users have unique fingerprint                    │
│  • Changes if: browser update, OS update, new monitor       │
│  • We allow ~15% variance (fuzzy matching)                  │
│                                                             │
│  Privacy Note:                                              │
│  • NOT used for tracking users                              │
│  • Only to verify "same browser" for security               │
│  • Stored hashed, associated with user session              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

ATTACK SCENARIO ANALYSIS

ATTACK 1: Steal the private key (Mobile)
────────────────────────────────────────────────────────────────
Attacker: Tries to extract key from device
Result: Key is in Secure Enclave / StrongBox
        Cannot be extracted even with physical access
Verdict: ❌ FAILED


ATTACK 2: Steal the private key (Web)
────────────────────────────────────────────────────────────────
Attacker: Opens DevTools, looks for private key
Result: Key is in JavaScript memory, not in storage
        When attacker opens DevTools, they're in THEIR session
        with THEIR keypair, not victim's
Verdict: ❌ FAILED


ATTACK 3: Intercept and replay request
────────────────────────────────────────────────────────────────
Attacker: Captures { sessionId, nonce, signature }
Attacker: Replays the exact request
Server: "Nonce already used/expired"
Verdict: ❌ FAILED (nonce is one-time use)


ATTACK 4: Get new nonce, use old signature
────────────────────────────────────────────────────────────────
Attacker: Gets new nonce "ch_NEW..."
Attacker: Uses old signature (signed for "ch_OLD...")
Server: Signature doesn't match nonce "ch_NEW..."
Verdict: ❌ FAILED (signature bound to specific nonce)


ATTACK 5: Steal tokens from victim's browser
────────────────────────────────────────────────────────────────
Attacker: Uses XSS to steal accessToken
Attacker: Uses token from different browser/IP
Server: Fingerprint mismatch! IP range mismatch!
Verdict: ❌ FAILED (tokens bound to browser + IP)


ATTACK 6: Create fake session from different browser
────────────────────────────────────────────────────────────────
Attacker: Knows victim's email + password
Attacker: Creates own session (own keypair, own fingerprint)
Attacker: Logs in successfully... BUT
Server: "New device detected, OTP required"
Attacker: Doesn't have victim's phone
Verdict: ❌ FAILED (OTP still required for new device)

SUMMARY: WHAT WE STORE WHERE

┌─────────────────────────────────────────────────────────────┐
│              WHAT WE STORE WHERE (MOBILE)                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ON DEVICE (Secure Storage):                                │
│  ✅ Private key (in Secure Enclave / Keystore)              │
│  ✅ deviceId (identifier)                                   │
│  ✅ accessToken (short-lived)                               │
│  ✅ refreshToken (long-lived)                               │
│                                                             │
│  ON SERVER:                                                 │
│  ✅ Public key (for signature verification)                 │
│  ✅ deviceId → userId mapping                               │
│  ✅ Trust status and expiry                                 │
│                                                             │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│              WHAT WE STORE WHERE (WEB)                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  IN BROWSER:                                                │
│  ✅ accessToken (short-lived, bound)                        │
│  ✅ refreshToken (medium-lived, bound)                      │
│  ✅ deviceId (just an identifier, not secret)               │
│  ❌ NO private keys stored                                  │
│  ❌ NO long-term secrets                                    │
│                                                             │
│  IN MEMORY ONLY (during session):                           │
│  ✅ Ephemeral keypair (dies when tab closes)                │
│                                                             │
│  ON SERVER:                                                 │
│  ✅ Session public key (Redis, 10-min TTL)                  │
│  ✅ Browser fingerprint hash                                │
│  ✅ IP range for token binding                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

DEVICE SECURITY API ENDPOINTS

1. Get Challenge (All Platforms)

GET /api/v1/auth/challenge

Called before any authenticated action (login, register device, refresh token).

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Challenge generated",
  "action_time": "2025-01-11T16:00:00Z",
  "data": {
    "nonce": "ch_dGhpcyBpcyBhIHNlY3VyZSByYW5kb20gbm9uY2U",
    "expiresIn": 60,
    "expiresAt": "2025-01-11T16:01:00Z"
  }
}

2. Register Device (Mobile - First Launch)

POST /api/v1/auth/device/register

Called on first app launch to register the device's public key.

Request:

{
  "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  "publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...",
  "nonce": "ch_dGhpcyBpcyBhIHNlY3VyZSByYW5kb20gbm9uY2U",
  "timestamp": 1736611200000,
  "signature": "MEUCIQC7...",
  "platform": "IOS"
}
FieldDescription
deviceIdSHA256 hash of public key, prefixed with platform
publicKeyBase64-encoded ECDSA P-256 public key
nonceChallenge from /auth/challenge
timestampCurrent time in milliseconds
signatureSign(nonce|timestamp|deviceId, privateKey)
platformIOS, ANDROID, or WEB

Response (Success):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Device registered successfully",
  "action_time": "2025-01-11T16:00:05Z",
  "data": {
    "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
    "registered": true,
    "securityLevel": "SECURE_ENCLAVE",
    "note": "Device will be linked to user account on login/signup"
  }
}

Response (Invalid Signature):

{
  "success": false,
  "httpStatus": "UNAUTHORIZED",
  "message": "Invalid device signature",
  "action_time": "2025-01-11T16:00:05Z",
  "data": {
    "code": "INVALID_SIGNATURE",
    "suggestion": "Ensure keys are generated correctly"
  }
}

3. Initialize Web Session (Web Only)

POST /api/v1/auth/web/session

Called when user opens login page in browser.

Request:

{
  "publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...",
  "fingerprint": "fp_7f9a2b3c4d5e6f7a8b9c0d1e2f3a4b5c"
}
FieldDescription
publicKeyEphemeral public key (generated in memory)
fingerprintBrowser fingerprint hash

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Web session initialized",
  "action_time": "2025-01-11T16:00:00Z",
  "data": {
    "sessionId": "ws_abc123xyz789",
    "nonce": "ch_dGhpcyBpcyBhIHNlY3VyZSByYW5kb20gbm9uY2U",
    "expiresIn": 600,
    "expiresAt": "2025-01-11T16:10:00Z"
  }
}

4. Login with Device Auth (Mobile)

POST /api/v1/auth/login

Request:

{
  "identifier": "alex@example.com",
  "password": "MySecurePass123!",
  "deviceAuth": {
    "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
    "nonce": "ch_dGhpcyBpcyBhIHNlY3VyZSByYW5kb20gbm9uY2U",
    "timestamp": 1736611200000,
    "signature": "MEUCIQC7...",
    "platform": "IOS"
  }
}

Response (Success - Trusted Device):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Login successful",
  "action_time": "2025-01-11T16:00:10Z",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "requiresOtp": false,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "profilePictureUrl": "https://storage.example.com/..."
    },
    "device": {
      "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
      "trusted": true,
      "securityLevel": "SECURE_ENCLAVE",
      "trustExpiresAt": "2025-02-10T16:00:10Z"
    }
  }
}

Response (New Device - OTP Required):

{
  "success": true,
  "httpStatus": "OK",
  "message": "New device detected. Please verify with OTP.",
  "action_time": "2025-01-11T16:00:10Z",
  "data": {
    "requiresOtp": true,
    "otpReason": "NEW_DEVICE",
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "otpSentTo": "+255*****678",
    "otpMethod": "SMS",
    "expiresAt": "2025-01-11T16:10:00Z",
    "device": {
      "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
      "isNew": true,
      "securityLevel": "SECURE_ENCLAVE"
    }
  }
}

Response (Invalid Signature):

{
  "success": false,
  "httpStatus": "UNAUTHORIZED",
  "message": "Invalid device signature",
  "action_time": "2025-01-11T16:00:10Z",
  "data": {
    "code": "INVALID_SIGNATURE",
    "suggestion": "Please ensure your app is up to date"
  }
}

Response (Nonce Expired/Used):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Challenge expired or already used",
  "action_time": "2025-01-11T16:00:10Z",
  "data": {
    "code": "INVALID_NONCE",
    "suggestion": "Request a new challenge and try again"
  }
}

5. Login with Web Auth (Web Browser)

POST /api/v1/auth/login

Request:

{
  "identifier": "alex@example.com",
  "password": "MySecurePass123!",
  "webAuth": {
    "sessionId": "ws_abc123xyz789",
    "nonce": "ch_dGhpcyBpcyBhIHNlY3VyZSByYW5kb20gbm9uY2U",
    "timestamp": 1736611200000,
    "signature": "MEUCIQC7...",
    "fingerprint": "fp_7f9a2b3c4d5e6f7a8b9c0d1e2f3a4b5c"
  }
}

Response (Success - Web is NEVER fully trusted):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Login successful",
  "action_time": "2025-01-11T16:00:10Z",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "requiresOtp": false,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "displayName": "Alex Johnson"
    },
    "device": {
      "trusted": false,
      "recognized": true,
      "platform": "WEB",
      "recognitionExpiresAt": "2025-01-18T16:00:10Z",
      "restrictions": [
        "OTP required for password change",
        "OTP required for email change",
        "OTP required for phone change",
        "OTP required for payment actions",
        "OTP required for account deletion"
      ]
    },
    "tokenBinding": {
      "boundToFingerprint": true,
      "boundToIpRange": true,
      "note": "Token will be invalidated if used from different browser/network"
    }
  }
}

6. Verify Device OTP (After New Device Detected)

POST /api/v1/auth/device/verify-otp

Request:

{
  "tempToken": "eyJhbGciOiJIUzI1NiIs...",
  "otpCode": "123456",
  "trustDevice": true,
  "deviceAuth": {
    "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
    "nonce": "ch_newNonceForVerification",
    "timestamp": 1736611260000,
    "signature": "MEUCIQD8...",
    "platform": "IOS"
  }
}

Response (Success):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Device verified and trusted",
  "action_time": "2025-01-11T16:01:00Z",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "displayName": "Alex Johnson"
    },
    "device": {
      "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
      "trusted": true,
      "securityLevel": "SECURE_ENCLAVE",
      "trustExpiresAt": "2025-02-10T16:01:00Z"
    }
  }
}

7. Refresh Token (With Device Signature)

POST /api/v1/auth/token/refresh

Refresh tokens also require device signature to prevent stolen refresh token usage.

Request (Mobile):

{
  "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
  "deviceAuth": {
    "deviceId": "ios_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
    "nonce": "ch_refreshNonce123",
    "timestamp": 1736614800000,
    "signature": "MEUCIQDx...",
    "platform": "IOS"
  }
}

Request (Web):

{
  "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
  "webAuth": {
    "sessionId": "ws_abc123xyz789",
    "nonce": "ch_refreshNonce123",
    "timestamp": 1736614800000,
    "signature": "MEUCIQDx...",
    "fingerprint": "fp_7f9a2b3c4d5e6f7a8b9c0d1e2f3a4b5c"
  }
}

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Token refreshed",
  "action_time": "2025-01-11T17:00:00Z",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600
  }
}

Response (Device Mismatch):

{
  "success": false,
  "httpStatus": "UNAUTHORIZED",
  "message": "Token does not belong to this device",
  "action_time": "2025-01-11T17:00:00Z",
  "data": {
    "code": "DEVICE_MISMATCH",
    "suggestion": "Please login again"
  }
}

DEVICE KEYS DATABASE SCHEMA

-- Device Keys Table (stores public keys for verification)
CREATE TABLE device_keys (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID REFERENCES accounts(id) ON DELETE CASCADE,
    device_id VARCHAR(64) NOT NULL UNIQUE,
    
    -- Cryptographic identity
    public_key TEXT NOT NULL,
    key_algorithm VARCHAR(20) DEFAULT 'EC_P256',
    
    -- Device metadata (server-derived from User-Agent)
    platform VARCHAR(20) NOT NULL,  -- IOS, ANDROID, WEB
    device_name VARCHAR(255),
    security_level VARCHAR(20),     -- SECURE_ENCLAVE, STRONGBOX, TEE, SOFTWARE
    
    -- Trust status
    is_trusted BOOLEAN DEFAULT FALSE,
    trust_expires_at TIMESTAMP,
    
    -- Activity tracking
    last_active_at TIMESTAMP DEFAULT NOW(),
    last_ip_address VARCHAR(45),
    last_location VARCHAR(255),
    
    -- Audit
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW(),
    
    CONSTRAINT unique_user_device UNIQUE (user_id, device_id)
);

CREATE INDEX idx_device_keys_user_id ON device_keys(user_id);
CREATE INDEX idx_device_keys_device_id ON device_keys(device_id);
CREATE INDEX idx_device_keys_last_active ON device_keys(last_active_at);


-- Web Sessions Table (Redis is preferred, but DB fallback)
CREATE TABLE web_sessions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    session_id VARCHAR(64) NOT NULL UNIQUE,
    
    -- Session data
    public_key TEXT NOT NULL,
    fingerprint_hash VARCHAR(64) NOT NULL,
    ip_address VARCHAR(45),
    
    -- Expiry
    expires_at TIMESTAMP NOT NULL,
    used_at TIMESTAMP,  -- NULL = not used yet
    
    -- Audit
    created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_web_sessions_session_id ON web_sessions(session_id);
CREATE INDEX idx_web_sessions_expires ON web_sessions(expires_at);

REDIS KEYS STRUCTURE

# Nonce/Challenge storage (60 second TTL)
nonce:{nonce_value} = {ip_address}|{created_timestamp}

# Web session storage (10 minute TTL)
websession:{session_id} = {
  "publicKey": "MFkw...",
  "fingerprint": "fp_abc123",
  "ipAddress": "196.41.xxx.xxx",
  "createdAt": 1736611200000
}

# Public key cache (1 hour TTL)
pubkey:{device_id} = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE..."

# Device trust cache (matches trust expiry)
devicetrust:{device_id} = {
  "userId": "550e8400-e29b-41d4-a716-446655440000",
  "trusted": true,
  "expiresAt": 1739203200000
}

Problem: If username is used in JWT tokens, changing username requires logout (bad UX).

Solution: Separate system identifier from display username.

Field Type Purpose Can Change? Used In
id UUID Primary key ❌ Never DB relations
systemUsername String Internal identifier ❌ Never JWT tokens, internal APIs
userName String Public @handle ✅ Yes Profile URL, mentions, search, display

How it works:

  • systemUsername is auto-generated at signup (e.g., usr_550e8400e29b41d4)
  • userName is user-chosen during onboarding (e.g., alexvibes)
  • JWT tokens contain systemUsername → user can change userName without logout
  • Profile URLs use userName: app.com/@alexvibes
  • Mentions use userName: @alexvibes

Username Change Flow:

User changes userName from "alex" to "alexnew"
       │
       ▼
┌─────────────────────────────────┐
│ 1. Validate new userName        │
│ 2. Check availability           │
│ 3. Update userName in DB        │
│ 4. Return success               │
│                                 │
│ JWT stays valid (uses           │
│ systemUsername, unchanged)      │
│                                 │
│ NO LOGOUT REQUIRED ✅           │
└─────────────────────────────────┘

Database:

account_table:
  id              UUID PRIMARY KEY
  system_username VARCHAR(50) UNIQUE NOT NULL  -- "usr_550e8400e29b41d4"
  user_name       VARCHAR(30) UNIQUE NOT NULL  -- "alexvibes"
  ...

Flow Diagram

┌─────────────────────────────────────────────────────────────────┐
│                    SIGNUP FLOW (5 Screens)                      │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Screen 1: Sign Up Method                                       │
│  ┌─────────────────┐                                            │
│  │ Phone/Email/    │──► OTP Sent ──► Verify OTP ──► Account     │
│  │ Google/Apple    │                              Created       │
│  └─────────────────┘                                            │
│           │                                                     │
│           ▼                                                     │
│  Screen 2: Name & Birthdate                                     │
│  ┌─────────────────┐                                            │
│  │ Display Name    │──► Validate Age ──► Save                   │
│  │ Birthdate       │                                            │
│  └─────────────────┘                                            │
│           │                                                     │
│           ▼                                                     │
│  Screen 3: Profile Setup                                        │
│  ┌─────────────────┐                                            │
│  │ Profile Pic     │──► Username Check ──► Save                 │
│  │ Username        │                                            │
│  │ Bio             │                                            │
│  └─────────────────┘                                            │
│           │                                                     │
│           ▼                                                     │
│  Screen 4: Interests                                            │
│  ┌─────────────────┐                                            │
│  │ Select 5-10     │──► Save Preferences                        │
│  │ Categories      │                                            │
│  └─────────────────┘                                            │
│           │                                                     │
│           ▼                                                     │
│  Screen 5: Complete! ──► Home Feed                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                       LOGIN FLOW                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │                    DEVICE CHECK FIRST                    │   │
│  │  ┌─────────────┐                                         │   │
│  │  │ Device Info │──► Known & Active? ──► YES ──► Continue │   │
│  │  └─────────────┘         │                               │   │
│  │                          NO                              │   │
│  │                          │                               │   │
│  │                          ▼                               │   │
│  │                    OTP Required First                    │   │
│  │                    (New/Inactive Device)                 │   │
│  └─────────────────────────────────────────────────────────┘   │
│                             │                                   │
│                             ▼                                   │
│  Option A: Password Login (if password set & device trusted)    │
│  ┌─────────────────┐                                            │
│  │ Identifier +    │──► Validate ──► Access Token               │
│  │ Password        │                                            │
│  └─────────────────┘                                            │
│        │                                                        │
│        └──► "Forgot Password?" ──► Password Reset Flow          │
│        └──► "Login with OTP instead" ──► Option B               │
│                                                                 │
│  Option B: Passwordless/OTP Login (always available)            │
│  ┌─────────────────┐                                            │
│  │ Phone/Email     │──► OTP Sent ──► Verify ──► Access Token    │
│  └─────────────────┘                                            │
│        │                                                        │
│        └──► "Lost access to phone/email?" ──► Account Recovery  │
│                                                                 │
│  Option C: Social Login (Google/Apple)                          │
│  ┌─────────────────┐                                            │
│  │ Google/Apple    │──► OAuth ──► Check Existing ──► Link/Create│
│  └─────────────────┘                      │                     │
│                                           ▼                     │
│                              ┌─────────────────────────┐        │
│                              │ Email matches existing? │        │
│                              │ YES → Link Account Flow │        │
│                              │ NO  → Create New        │        │
│                              └─────────────────────────┘        │
│                                                                 │
│  Account Recovery (Lost Access)                                 │
│  ┌─────────────────┐                                            │
│  │ Verify Identity │──► Support Ticket ──► Manual Review        │
│  │ (ID upload,     │                                            │
│  │  selfie, etc.)  │                                            │
│  └─────────────────┘                                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

SECURITY ARCHITECTURE

Why We Need Extra Protection for OAuth Users

The Problem:

Attacker hacks victim's Google account
           │
           ▼
Attacker can access ALL apps linked to that Google
           │
           ▼
😱 If we only rely on Google, attacker owns the account

Our Solution: Multi-Layer Security

┌─────────────────────────────────────────────────────────────────┐
│                    NEXTGATE SECURITY LAYERS                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Layer 1: Authentication (Who are you?)                         │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ Password / OTP / Google / Apple                          │   │
│  │ (Any of these can authenticate)                          │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  Layer 2: Device Trust (Is this your device?)                   │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ New device? → OTP to PHONE required                      │   │
│  │ Inactive 30+ days? → OTP to PHONE required               │   │
│  │ Trusted device? → Pass through                           │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  Layer 3: Sensitive Actions (Extra verification)                │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ Change password     → OTP to PHONE                       │   │
│  │ Change email        → OTP to PHONE                       │   │
│  │ Change phone        → OTP to OLD phone + NEW phone       │   │
│  │ Link/Unlink OAuth   → OTP to PHONE                       │   │
│  │ Delete account      → OTP to PHONE + Password (if set)   │   │
│  │ Large purchases     → OTP to PHONE (configurable)        │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  🔑 KEY INSIGHT: Phone is the ULTIMATE trust anchor             │
│     Even if Google/Apple/Email is hacked, phone protects you    │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Phone as Ultimate Recovery Method

Auth Method Compromised Can Attacker Access Account?
Password leaked ❌ No - needs device OTP or phone OTP
Email hacked ❌ No - sensitive actions need phone OTP
Google hacked ❌ No - new device needs phone OTP
Apple hacked ❌ No - new device needs phone OTP
Phone stolen (unlocked) ⚠️ Partial - but needs password for sensitive actions
Phone + Password both ✅ Yes - full access (this is expected)

Mandatory Phone Verification

During onboarding, we STRONGLY encourage phone verification:

┌─────────────────────────────────────────────────────────────────┐
│                   ONBOARDING PHONE PROMPT                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  "Add your phone number for account security"                   │
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │ 🔒 Recover your account if you lose access               │   │
│  │ 🔒 Get alerts about suspicious activity                  │   │
│  │ 🔒 Verify sensitive actions                              │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  [+255] [___________] [Verify]                                  │
│                                                                 │
│  [Skip for now] ← Show warning:                                 │
│  "Without a verified phone, you may lose access to your         │
│   account if you forget your password or lose access to         │
│   your Google/Apple account."                                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

ACCOUNT LINKING & MERGING

When OAuth Email Matches Existing Account

Scenario Matrix:

Existing Account State OAuth Provider Action
Email verified + password Google (same email) Prompt to link
Email verified + no password Google (same email) Prompt to link
Email unverified Google (same email) Auto-link (Google verified it)
Phone only (no email set) Google (new email) Prompt to add email or create new
Email verified but different Google (different email) Create new account

POST /api/v1/auth/oauth/google

Request:

{
  "idToken": "eyJhbGciOiJSUzI1NiIs...",
  "deviceInfo": {
    "deviceId": "abc123-device-fingerprint",
    "deviceName": "iPhone 15 Pro",
    "deviceType": "MOBILE_IOS"
  }
}

Response (Email Matches Existing - Link Required):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Account found with this email. Please verify to link.",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "action": "LINK_REQUIRED",
    "existingAccount": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "maskedEmail": "a***@example.com",
      "maskedPhone": "+255*****678",
      "hasPassword": true,
      "authProviders": ["PHONE", "EMAIL"],
      "createdAt": "2024-06-15T10:00:00"
    },
    "linkOptions": [
      {
        "method": "PASSWORD",
        "available": true,
        "description": "Enter your password to link"
      },
      {
        "method": "OTP_PHONE",
        "available": true,
        "description": "Get OTP on +255*****678"
      },
      {
        "method": "OTP_EMAIL",
        "available": true,
        "description": "Get OTP on a***@example.com"
      }
    ],
    "oauthProvider": "GOOGLE",
    "oauthEmail": "alex@example.com",
    "tempToken": "eyJhbGciOiJIUzI1NiIs..."
  }
}

POST /api/v1/auth/account/link/confirm

Request:

{
  "tempToken": "eyJhbGciOiJIUzI1NiIs...",
  "method": "PASSWORD",
  "password": "MySecurePass123!"
}

Response (Success):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Google account linked successfully",
  "action_time": "2025-01-11T16:01:00",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "email": "alex@example.com",
      "authProviders": ["PHONE", "EMAIL", "GOOGLE"],
      "onboardingComplete": true
    },
    "linkedProvider": {
      "provider": "GOOGLE",
      "email": "alex@example.com",
      "linkedAt": "2025-01-11T16:01:00"
    }
  }
}

POST /api/v1/auth/account/link/request-otp

Request:

{
  "tempToken": "eyJhbGciOiJIUzI1NiIs...",
  "method": "OTP_PHONE"
}

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "OTP sent to your phone",
  "action_time": "2025-01-11T16:01:00",
  "data": {
    "otpToken": "eyJhbGciOiJIUzI1NiIs...",
    "sentTo": "+255*****678",
    "method": "SMS",
    "expiresAt": "2025-01-11T16:11:00"
  }
}

POST /api/v1/auth/account/link/confirm

Request:

{
  "tempToken": "eyJhbGciOiJIUzI1NiIs...",
  "method": "OTP_PHONE",
  "otpCode": "123456"
}

When existing account has unverified email that matches Google email:

Response (Auto-Linked):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Account linked and email verified",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "action": "AUTO_LINKED",
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "email": "alex@example.com",
      "isEmailVerified": true,
      "authProviders": ["PHONE", "GOOGLE"]
    },
    "note": "Your Google account has been linked and email verified automatically"
  }
}

Create New Account (No Match)

Response (New Account):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Account created successfully",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "action": "CREATED",
    "isNewUser": true,
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "user": {
      "id": "660e8400-e29b-41d4-a716-446655440001",
      "systemUsername": "usr_660e8400e29b41d4",
      "userName": null,
      "email": "alex@example.com",
      "firstName": "Alex",
      "lastName": "Johnson",
      "profilePictureUrl": "https://lh3.googleusercontent.com/...",
      "isEmailVerified": true,
      "hasPassword": false,
      "authProviders": ["GOOGLE"],
      "onboardingStep": "NAME_BIRTHDATE",
      "onboardingComplete": false
    },
    "promptAddPhone": true,
    "phonePromptMessage": "Add your phone number to secure your account and enable recovery options"
  }
}

MANAGE LINKED ACCOUNTS

Get Linked Providers

GET /api/v1/auth/providers

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Linked providers retrieved",
  "action_time": "2025-01-11T18:00:00",
  "data": {
    "providers": [
      {
        "provider": "PHONE",
        "identifier": "+255*****678",
        "verified": true,
        "linkedAt": "2025-01-01T10:00:00",
        "isPrimary": true,
        "canUnlink": false,
        "unlinkBlockedReason": "Phone is your primary recovery method"
      },
      {
        "provider": "EMAIL",
        "identifier": "a***@example.com",
        "verified": true,
        "linkedAt": "2025-01-01T10:00:00",
        "isPrimary": false,
        "canUnlink": true
      },
      {
        "provider": "GOOGLE",
        "identifier": "a***@gmail.com",
        "verified": true,
        "linkedAt": "2025-01-11T16:01:00",
        "isPrimary": false,
        "canUnlink": true
      }
    ],
    "availableToLink": [
      {
        "provider": "APPLE",
        "description": "Sign in with Apple"
      }
    ],
    "hasPassword": true,
    "securityNote": "You have 3 ways to access your account"
  }
}

DELETE /api/v1/auth/providers/{provider}

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Request:

{
  "verificationMethod": "OTP_PHONE",
  "otpCode": "123456"
}

Response (Success):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Google account unlinked",
  "action_time": "2025-01-11T18:10:00",
  "data": {
    "unlinkedProvider": "GOOGLE",
    "remainingProviders": ["PHONE", "EMAIL"],
    "securityNote": "You can no longer sign in with Google"
  }
}

Response (Cannot Unlink - Only Provider):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Cannot unlink your only sign-in method",
  "action_time": "2025-01-11T18:10:00",
  "data": {
    "code": "CANNOT_UNLINK_ONLY_PROVIDER",
    "suggestion": "Add another sign-in method before unlinking this one"
  }
}

POST /api/v1/auth/providers/link

Request:

{
  "provider": "APPLE",
  "identityToken": "eyJraWQiOiJXNldjT0...",
  "authorizationCode": "c7e8a3f1b2d4..."
}

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Apple account linked successfully",
  "action_time": "2025-01-11T18:15:00",
  "data": {
    "linkedProvider": {
      "provider": "APPLE",
      "identifier": "a***@privaterelay.appleid.com",
      "linkedAt": "2025-01-11T18:15:00"
    },
    "totalProviders": 4
  }
}

ACCOUNT RECOVERY (Lost Access)

When User Can't Access Any Auth Method

POST /api/v1/auth/recovery/request

Request:

{
  "identifier": "alexvibes",
  "recoveryReason": "LOST_PHONE",
  "contactEmail": "backup@anotheremail.com"
}

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Recovery request submitted",
  "action_time": "2025-01-11T20:00:00",
  "data": {
    "ticketId": "REC-2025-001234",
    "status": "PENDING_REVIEW",
    "estimatedResponseTime": "24-48 hours",
    "nextSteps": [
      "Check your backup email for instructions",
      "Prepare identity verification documents",
      "Our team will contact you within 48 hours"
    ],
    "requiredDocuments": [
      "Government-issued ID (passport, national ID)",
      "Selfie holding ID",
      "Proof of account ownership (screenshots, transaction history)"
    ]
  }
}

SENSITIVE ACTIONS - OTP VERIFICATION

All sensitive actions require OTP to phone (regardless of how user logged in):

Sensitive Actions List

Action OTP Required? Additional Verification
Change password ✅ Phone OTP Current password (if set)
Change email ✅ Phone OTP -
Change phone ✅ Old phone OTP + New phone OTP -
Link OAuth provider ✅ Phone OTP -
Unlink OAuth provider ✅ Phone OTP -
Delete account ✅ Phone OTP Password (if set)
View full payment methods ✅ Phone OTP -
Add payment method ❌ No -
Remove payment method ✅ Phone OTP -
Large purchase (>$100) ⚙️ Configurable -
Export account data ✅ Phone OTP -
Change security settings ✅ Phone OTP Password (if set)

Request OTP for Sensitive Action

POST /api/v1/auth/sensitive-action/request-otp

Request:

{
  "action": "CHANGE_EMAIL",
  "newEmail": "newemail@example.com"
}

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "OTP sent to your phone",
  "action_time": "2025-01-11T19:00:00",
  "data": {
    "actionToken": "eyJhbGciOiJIUzI1NiIs...",
    "action": "CHANGE_EMAIL",
    "sentTo": "+255*****678",
    "method": "SMS",
    "expiresAt": "2025-01-11T19:10:00"
  }
}

Confirm Sensitive Action

POST /api/v1/auth/sensitive-action/confirm

Request:

{
  "actionToken": "eyJhbGciOiJIUzI1NiIs...",
  "otpCode": "123456"
}

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Email updated successfully",
  "action_time": "2025-01-11T19:01:00",
  "data": {
    "action": "CHANGE_EMAIL",
    "completed": true,
    "changes": {
      "previousEmail": "a***@example.com",
      "newEmail": "n***@example.com",
      "emailVerified": false
    },
    "note": "Please verify your new email address"
  }
}

NO PHONE? FALLBACK OPTIONS

If user didn't add phone during onboarding:

Prompt to Add Phone (Shown on sensitive actions)

{
  "success": false,
  "httpStatus": "FORBIDDEN",
  "message": "Phone verification required for this action",
  "action_time": "2025-01-11T19:00:00",
  "data": {
    "code": "PHONE_REQUIRED",
    "action": "CHANGE_EMAIL",
    "options": [
      {
        "option": "ADD_PHONE",
        "description": "Add and verify your phone number first",
        "endpoint": "/api/v1/profile/add-phone"
      },
      {
        "option": "USE_PASSWORD",
        "available": true,
        "description": "Use your password instead (less secure)"
      },
      {
        "option": "CONTACT_SUPPORT",
        "description": "Contact support for manual verification"
      }
    ]
  }
}

SUMMARY: AUTH METHODS & SECURITY

Complete Auth Provider Matrix

Provider Can Signup? Can Login? Provides Email? Provides Phone? Trust Level
Phone + OTP HIGH
Email + OTP MEDIUM
Email + Password ❌ (need OTP first) MEDIUM
Google OAuth MEDIUM
Apple OAuth ✅ (may be relay) MEDIUM
Password only ✅ (trusted device) LOW

Security Recommendations for Users

{
  "securityScore": 75,
  "level": "GOOD",
  "recommendations": [
    {
      "priority": "HIGH",
      "action": "ADD_PHONE",
      "title": "Verify your phone number",
      "description": "Enables account recovery and secures sensitive actions",
      "completed": false
    },
    {
      "priority": "MEDIUM",
      "action": "SET_PASSWORD",
      "title": "Set a password",
      "description": "Faster login on trusted devices",
      "completed": true
    },
    {
      "priority": "LOW",
      "action": "LINK_BACKUP_EMAIL",
      "title": "Add backup email",
      "description": "Alternative recovery option",
      "completed": false
    }
  ]
}

---

## SCREEN 1: Sign Up

### 1.1 Initiate Signup (Phone)

**POST** `/api/v1/auth/signup/initiate`

**Request:**
```json
{
  "method": "PHONE",
  "phoneNumber": "+255712345678"
}

Response (Success):

{
  "success": true,
  "httpStatus": "OK",
  "message": "OTP sent to your phone",
  "action_time": "2025-01-11T15:20:00",
  "data": {
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "method": "PHONE",
    "maskedIdentifier": "+255*****678",
    "expiresAt": "2025-01-11T15:30:00",
    "resendAllowedAt": "2025-01-11T15:22:00",
    "attemptsRemaining": 3
  }
}

Response (Phone Already Registered):

{
  "success": false,
  "httpStatus": "CONFLICT",
  "message": "This phone number is already registered",
  "action_time": "2025-01-11T15:20:00",
  "data": {
    "code": "ACCOUNT_EXISTS",
    "field": "phoneNumber",
    "suggestion": "Please login instead"
  }
}

1.2 Initiate Signup (Email)

POST /api/v1/auth/signup/initiate

Request:

{
  "method": "EMAIL",
  "email": "alex@example.com"
}

Response (Success):

{
  "success": true,
  "message": "OTP sent to your email",
  "data": {
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "method": "EMAIL",
    "maskedIdentifier": "a***@example.com",
    "expiresAt": "2025-01-11T15:30:00Z",
    "resendAllowedAt": "2025-01-11T15:22:00Z",
    "attemptsRemaining": 3
  },
  "timestamp": "2025-01-11T15:20:00Z"
}

1.3 Verify Signup OTP

POST /api/v1/auth/signup/verify

Request:

{
  "tempToken": "eyJhbGciOiJIUzI1NiIs...",
  "otpCode": "123456"
}

Response (Success - Account Created):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Account created successfully",
  "action_time": "2025-01-11T15:21:00",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": null,
      "phoneNumber": "+255712345678",
      "email": null,
      "isPhoneVerified": true,
      "isEmailVerified": false,
      "hasPassword": false,
      "onboardingStep": "NAME_BIRTHDATE",
      "onboardingComplete": false,
      "createdAt": "2025-01-11T15:20:00"
    }
  }
}

Response (Invalid OTP):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Invalid OTP code",
  "action_time": "2025-01-11T15:21:00",
  "data": {
    "code": "INVALID_OTP",
    "attemptsRemaining": 2
  }
}

Response (OTP Expired):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "OTP has expired",
  "action_time": "2025-01-11T15:35:00",
  "data": {
    "code": "OTP_EXPIRED",
    "suggestion": "Please request a new OTP"
  }
}

1.4 Resend OTP

POST /api/v1/auth/otp/resend

Request:

{
  "tempToken": "eyJhbGciOiJIUzI1NiIs..."
}

Response (Success):

{
  "success": true,
  "message": "OTP resent successfully",
  "data": {
    "newTempToken": "eyJhbGciOiJIUzI1NiIs...",
    "maskedIdentifier": "+255*****678",
    "expiresAt": "2025-01-11T15:40:00Z",
    "resendAllowedAt": "2025-01-11T15:32:00Z",
    "attemptsRemaining": 2
  },
  "timestamp": "2025-01-11T15:30:00Z"
}

Response (Too Soon):

{
  "success": false,
  "message": "Please wait before requesting another OTP",
  "error": {
    "code": "RESEND_COOLDOWN",
    "resendAllowedAt": "2025-01-11T15:32:00Z",
    "waitSeconds": 90
  },
  "timestamp": "2025-01-11T15:30:30Z"
}

1.5 Social Signup (Google)

POST /api/v1/auth/signup/google

Request:

{
  "idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6Ikp..."
}

Response (Success - New User):

{
  "success": true,
  "message": "Account created successfully",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "isNewUser": true,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440001",
      "email": "alex@gmail.com",
      "firstName": "Alex",
      "lastName": "Johnson",
      "profilePictureUrl": "https://lh3.googleusercontent.com/...",
      "isEmailVerified": true,
      "hasPassword": false,
      "authProvider": "GOOGLE",
      "onboardingStep": "NAME_BIRTHDATE",
      "onboardingComplete": false,
      "createdAt": "2025-01-11T15:20:00Z"
    }
  },
  "timestamp": "2025-01-11T15:20:00Z"
}

Response (Existing User - Login):

{
  "success": true,
  "message": "Welcome back!",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "isNewUser": false,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440001",
      "userName": "alexj",
      "email": "alex@gmail.com",
      "displayName": "Alex Johnson",
      "profilePictureUrl": "https://storage.example.com/...",
      "isEmailVerified": true,
      "hasPassword": true,
      "authProvider": "GOOGLE",
      "onboardingComplete": true
    }
  },
  "timestamp": "2025-01-11T15:20:00Z"
}

1.6 Social Signup (Apple)

POST /api/v1/auth/signup/apple

Request:

{
  "identityToken": "eyJraWQiOiJXNldjT0...",
  "authorizationCode": "c7e8a3f1b2d4...",
  "firstName": "Alex",
  "lastName": "Johnson"
}

Note: Apple only provides name on first authorization

Response: Same structure as Google response


SCREEN 2: Name & Birthdate

2.1 Save Name & Birthdate

PUT /api/v1/onboarding/name-birthdate

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Request:

{
  "displayName": "Alex Johnson",
  "firstName": "Alex",
  "lastName": "Johnson",
  "birthDate": "1995-06-15"
}

Response (Success):

{
  "success": true,
  "message": "Profile updated successfully",
  "data": {
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "displayName": "Alex Johnson",
      "firstName": "Alex",
      "lastName": "Johnson",
      "birthDate": "1995-06-15",
      "age": 29,
      "onboardingStep": "PROFILE_SETUP",
      "onboardingComplete": false
    }
  },
  "timestamp": "2025-01-11T15:22:00Z"
}

Response (Underage - Below 13):

{
  "success": false,
  "message": "You must be at least 13 years old to use this app",
  "error": {
    "code": "UNDERAGE",
    "field": "birthDate",
    "minimumAge": 13
  },
  "timestamp": "2025-01-11T15:22:00Z"
}

Response (Invalid Date):

{
  "success": false,
  "message": "Invalid birth date",
  "error": {
    "code": "INVALID_DATE",
    "field": "birthDate",
    "details": "Birth date cannot be in the future"
  },
  "timestamp": "2025-01-11T15:22:00Z"
}

SCREEN 3: Profile Setup

3.1 Check Username Availability

GET /api/v1/onboarding/username/check?username=alexvibes

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Note: Always returns suggestions (even when available) so user can pick alternatives

Response (Available):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Username is available",
  "action_time": "2025-01-11T15:23:00",
  "data": {
    "username": "alexvibes",
    "available": true,
    "valid": true,
    "suggestions": [
      "alexvibes_",
      "alexvibes1",
      "thealexvibes",
      "alexvibes_official",
      "real_alexvibes"
    ]
  }
}

Response (Taken):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Username is already taken",
  "action_time": "2025-01-11T15:23:00",
  "data": {
    "username": "alex",
    "available": false,
    "valid": true,
    "suggestions": [
      "alex123",
      "alex_vibes",
      "alexcool",
      "alex2025",
      "thealex"
    ]
  }
}

Response (Invalid Format):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Invalid username format",
  "action_time": "2025-01-11T15:23:00",
  "data": {
    "username": "123alex",
    "available": false,
    "valid": false,
    "validationError": "Username must start with a letter",
    "suggestions": [
      "alex123",
      "alex_user",
      "alexnew",
      "user_alex",
      "the_alex"
    ]
  }
}

3.2 Upload Profile Picture

POST /api/v1/onboarding/profile-picture

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: multipart/form-data

Request (Form Data):

file: [binary image data]

Response (Success):

{
  "success": true,
  "message": "Profile picture uploaded successfully",
  "data": {
    "profilePictureUrl": "https://storage.example.com/users/550e8400.../profile/pic_1736605380.jpg",
    "thumbnailUrl": "https://storage.example.com/users/550e8400.../profile/pic_1736605380_thumb.jpg"
  },
  "timestamp": "2025-01-11T15:23:00Z"
}

Response (Invalid File):

{
  "success": false,
  "message": "Invalid file type",
  "error": {
    "code": "INVALID_FILE_TYPE",
    "allowedTypes": ["image/jpeg", "image/png", "image/webp"],
    "maxSizeMB": 5
  },
  "timestamp": "2025-01-11T15:23:00Z"
}

3.3 Save Profile Setup

PUT /api/v1/onboarding/profile-setup

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Request:

{
  "userName": "alexvibes",
  "bio": "Fashion lover | Shopaholic | Event creator 🔥",
  "profilePictureUrl": "https://storage.example.com/users/550e8400.../profile/pic_1736605380.jpg"
}

Response (Success):

{
  "success": true,
  "message": "Profile setup completed",
  "data": {
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "bio": "Fashion lover | Shopaholic | Event creator 🔥",
      "profilePictureUrl": "https://storage.example.com/users/550e8400.../profile/pic_1736605380.jpg",
      "onboardingStep": "INTERESTS",
      "onboardingComplete": false
    }
  },
  "timestamp": "2025-01-11T15:24:00Z"
}

Response (Username Taken - Race Condition):

{
  "success": false,
  "message": "Username was just taken by another user",
  "error": {
    "code": "USERNAME_TAKEN",
    "field": "userName",
    "suggestions": [
      "alexvibes1",
      "alexvibes_",
      "thealexvibes"
    ]
  },
  "timestamp": "2025-01-11T15:24:00Z"
}

SCREEN 4: Interests

4.1 Get Available Interest Categories

GET /api/v1/onboarding/interests/categories

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Response:

{
  "success": true,
  "message": "Categories retrieved successfully",
  "data": {
    "categories": [
      {
        "id": "cat_001",
        "name": "Fashion",
        "icon": "👗",
        "iconUrl": "https://cdn.example.com/icons/fashion.png",
        "color": "#FF6B6B"
      },
      {
        "id": "cat_002",
        "name": "Electronics",
        "icon": "📱",
        "iconUrl": "https://cdn.example.com/icons/electronics.png",
        "color": "#4ECDC4"
      },
      {
        "id": "cat_003",
        "name": "Beauty & Cosmetics",
        "icon": "💄",
        "iconUrl": "https://cdn.example.com/icons/beauty.png",
        "color": "#FF69B4"
      },
      {
        "id": "cat_004",
        "name": "Food & Drinks",
        "icon": "🍔",
        "iconUrl": "https://cdn.example.com/icons/food.png",
        "color": "#F39C12"
      },
      {
        "id": "cat_005",
        "name": "Sports & Fitness",
        "icon": "⚽",
        "iconUrl": "https://cdn.example.com/icons/sports.png",
        "color": "#2ECC71"
      },
      {
        "id": "cat_006",
        "name": "Music & Dance",
        "icon": "🎵",
        "iconUrl": "https://cdn.example.com/icons/music.png",
        "color": "#9B59B6"
      },
      {
        "id": "cat_007",
        "name": "Home & Decor",
        "icon": "🏠",
        "iconUrl": "https://cdn.example.com/icons/home.png",
        "color": "#E67E22"
      },
      {
        "id": "cat_008",
        "name": "Tech & Gadgets",
        "icon": "💻",
        "iconUrl": "https://cdn.example.com/icons/tech.png",
        "color": "#3498DB"
      },
      {
        "id": "cat_009",
        "name": "Travel",
        "icon": "✈️",
        "iconUrl": "https://cdn.example.com/icons/travel.png",
        "color": "#1ABC9C"
      },
      {
        "id": "cat_010",
        "name": "Gaming",
        "icon": "🎮",
        "iconUrl": "https://cdn.example.com/icons/gaming.png",
        "color": "#8E44AD"
      },
      {
        "id": "cat_011",
        "name": "Books & Reading",
        "icon": "📚",
        "iconUrl": "https://cdn.example.com/icons/books.png",
        "color": "#D35400"
      },
      {
        "id": "cat_012",
        "name": "Art & Design",
        "icon": "🎨",
        "iconUrl": "https://cdn.example.com/icons/art.png",
        "color": "#E74C3C"
      },
      {
        "id": "cat_013",
        "name": "Health & Wellness",
        "icon": "🧘",
        "iconUrl": "https://cdn.example.com/icons/wellness.png",
        "color": "#27AE60"
      },
      {
        "id": "cat_014",
        "name": "Automotive",
        "icon": "🚗",
        "iconUrl": "https://cdn.example.com/icons/auto.png",
        "color": "#34495E"
      },
      {
        "id": "cat_015",
        "name": "Pets & Animals",
        "icon": "🐾",
        "iconUrl": "https://cdn.example.com/icons/pets.png",
        "color": "#F1C40F"
      },
      {
        "id": "cat_016",
        "name": "Photography",
        "icon": "📷",
        "iconUrl": "https://cdn.example.com/icons/photo.png",
        "color": "#7F8C8D"
      },
      {
        "id": "cat_017",
        "name": "Kids & Baby",
        "icon": "👶",
        "iconUrl": "https://cdn.example.com/icons/kids.png",
        "color": "#FFB6C1"
      },
      {
        "id": "cat_018",
        "name": "Business & Finance",
        "icon": "💼",
        "iconUrl": "https://cdn.example.com/icons/business.png",
        "color": "#2C3E50"
      },
      {
        "id": "cat_019",
        "name": "Entertainment",
        "icon": "🎬",
        "iconUrl": "https://cdn.example.com/icons/entertainment.png",
        "color": "#C0392B"
      },
      {
        "id": "cat_020",
        "name": "DIY & Crafts",
        "icon": "🛠️",
        "iconUrl": "https://cdn.example.com/icons/diy.png",
        "color": "#16A085"
      }
    ],
    "minimumSelection": 3,
    "recommendedSelection": 5,
    "maximumSelection": 15
  },
  "timestamp": "2025-01-11T15:25:00Z"
}

4.2 Save User Interests

POST /api/v1/onboarding/interests

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Request:

{
  "categoryIds": [
    "cat_001",
    "cat_003",
    "cat_006",
    "cat_009",
    "cat_012"
  ]
}

Response (Success):

{
  "success": true,
  "message": "Interests saved successfully",
  "data": {
    "selectedCount": 5,
    "interests": [
      { "id": "cat_001", "name": "Fashion" },
      { "id": "cat_003", "name": "Beauty & Cosmetics" },
      { "id": "cat_006", "name": "Music & Dance" },
      { "id": "cat_009", "name": "Travel" },
      { "id": "cat_012", "name": "Art & Design" }
    ],
    "onboardingStep": "COMPLETE",
    "onboardingComplete": true
  },
  "timestamp": "2025-01-11T15:26:00Z"
}

Response (Too Few Selected):

{
  "success": false,
  "message": "Please select at least 3 interests",
  "error": {
    "code": "MINIMUM_NOT_MET",
    "selectedCount": 1,
    "minimumRequired": 3
  },
  "timestamp": "2025-01-11T15:26:00Z"
}

4.3 Skip Interests (Optional)

POST /api/v1/onboarding/interests/skip

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Request:

{}

Response:

{
  "success": true,
  "message": "Interests skipped. You can update them later in settings.",
  "data": {
    "onboardingStep": "COMPLETE",
    "onboardingComplete": true,
    "reminder": "We'll show you general content. Update your interests anytime for a personalized feed!"
  },
  "timestamp": "2025-01-11T15:26:00Z"
}

SCREEN 5: Complete Onboarding

5.1 Get Onboarding Summary & Suggestions

GET /api/v1/onboarding/complete

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Response:

{
  "success": true,
  "message": "Welcome to the app! 🎉",
  "data": {
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "profilePictureUrl": "https://storage.example.com/users/550e8400.../profile/pic.jpg",
      "bio": "Fashion lover | Shopaholic | Event creator 🔥",
      "onboardingComplete": true,
      "hasPassword": false
    },
    "suggestions": {
      "accountsToFollow": [
        {
          "id": "user_001",
          "userName": "fashionista",
          "displayName": "Fashion Hub",
          "profilePictureUrl": "https://storage.example.com/...",
          "bio": "Latest fashion trends",
          "isVerified": true,
          "followerCount": 12500,
          "matchReason": "Based on your interest in Fashion"
        },
        {
          "id": "user_002",
          "userName": "beautyguru",
          "displayName": "Beauty Tips Daily",
          "profilePictureUrl": "https://storage.example.com/...",
          "bio": "Makeup tutorials & reviews",
          "isVerified": true,
          "followerCount": 8300,
          "matchReason": "Based on your interest in Beauty & Cosmetics"
        }
      ],
      "shopsToFollow": [
        {
          "id": "shop_001",
          "name": "Style Studio",
          "logoUrl": "https://storage.example.com/...",
          "category": "Fashion",
          "rating": 4.8,
          "productCount": 156,
          "matchReason": "Top rated in Fashion"
        }
      ],
      "upcomingEvents": [
        {
          "id": "event_001",
          "title": "Summer Fashion Show",
          "coverImageUrl": "https://storage.example.com/...",
          "startDate": "2025-01-20T18:00:00Z",
          "attendeeCount": 234,
          "matchReason": "Fashion event near you"
        }
      ]
    },
    "nextSteps": [
      {
        "action": "FOLLOW_ACCOUNTS",
        "title": "Follow 5 accounts",
        "description": "Get started by following accounts you like",
        "reward": null
      },
      {
        "action": "SET_PASSWORD",
        "title": "Set a password",
        "description": "For faster login next time",
        "reward": null
      },
      {
        "action": "FIRST_POST",
        "title": "Create your first post",
        "description": "Share something with the community",
        "reward": null
      }
    ]
  },
  "timestamp": "2025-01-11T15:27:00Z"
}

LOGIN FLOWS (With Device Trust)

Login Option A: Password Login

POST /api/v1/auth/login/password

Request:

{
  "identifier": "alexvibes",
  "password": "MySecurePass123!",
  "deviceInfo": {
    "deviceId": "abc123-device-fingerprint",
    "deviceName": "iPhone 15 Pro",
    "deviceType": "MOBILE_IOS",
    "appVersion": "1.2.0"
  }
}

identifier can be: username, email, or phone number

Response (Success - Trusted Device):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Login successful",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "requiresOtp": false,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "profilePictureUrl": "https://storage.example.com/...",
      "email": "alex@example.com",
      "phoneNumber": "+255712345678",
      "isEmailVerified": true,
      "isPhoneVerified": true,
      "hasPassword": true,
      "onboardingComplete": true
    },
    "device": {
      "deviceId": "abc123-device-fingerprint",
      "deviceName": "iPhone 15 Pro",
      "trusted": true,
      "trustExpiresAt": "2025-02-10T16:00:00"
    }
  }
}

Response (New Device - OTP Required):

{
  "success": true,
  "httpStatus": "OK",
  "message": "New device detected. Please verify with OTP.",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "requiresOtp": true,
    "otpReason": "NEW_DEVICE",
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "otpSentTo": "+255*****678",
    "otpMethod": "SMS",
    "expiresAt": "2025-01-11T16:10:00",
    "device": {
      "deviceId": "xyz789-new-device",
      "deviceName": "Chrome on Windows",
      "isNew": true
    },
    "securityNote": "We detected a login from a new device. For your security, please verify with OTP."
  }
}

Response (Inactive Device 30+ days - OTP Required):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Device inactive for 30+ days. Please verify with OTP.",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "requiresOtp": true,
    "otpReason": "INACTIVE_DEVICE",
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "otpSentTo": "+255*****678",
    "otpMethod": "SMS",
    "expiresAt": "2025-01-11T16:10:00",
    "device": {
      "deviceId": "abc123-device-fingerprint",
      "deviceName": "iPhone 15 Pro",
      "lastActiveAt": "2024-12-01T10:00:00",
      "inactiveDays": 41
    },
    "securityNote": "This device hasn't been used in 41 days. Please verify it's you."
  }
}

Response (Wrong Password):

{
  "success": false,
  "httpStatus": "UNAUTHORIZED",
  "message": "Invalid credentials",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "code": "INVALID_CREDENTIALS",
    "attemptsRemaining": 4,
    "suggestion": "Forgot password? Use OTP login instead"
  }
}

Response (Account Locked):

{
  "success": false,
  "httpStatus": "LOCKED",
  "message": "Account temporarily locked due to multiple failed attempts",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "code": "ACCOUNT_LOCKED",
    "unlockAt": "2025-01-11T16:30:00",
    "suggestion": "Try OTP login or wait 30 minutes"
  }
}

Response (No Password Set):

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "No password set for this account",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "code": "NO_PASSWORD",
    "suggestion": "Use OTP login or social login"
  }
}

Login Option B: OTP Login (Passwordless)

B.1 Request OTP

POST /api/v1/auth/login/otp/request

Request:

{
  "identifier": "+255712345678",
  "deviceInfo": {
    "deviceId": "abc123-device-fingerprint",
    "deviceName": "iPhone 15 Pro",
    "deviceType": "MOBILE_IOS",
    "appVersion": "1.2.0"
  }
}

identifier can be: email or phone number

Response (Success):

{
  "success": true,
  "httpStatus": "OK",
  "message": "OTP sent to your phone",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "method": "SMS",
    "maskedIdentifier": "+255*****678",
    "expiresAt": "2025-01-11T16:10:00",
    "resendAllowedAt": "2025-01-11T16:02:00",
    "device": {
      "deviceId": "abc123-device-fingerprint",
      "deviceName": "iPhone 15 Pro",
      "isNew": false,
      "lastActiveAt": "2025-01-10T14:30:00"
    }
  }
}

Response (User Not Found):

{
  "success": false,
  "httpStatus": "NOT_FOUND",
  "message": "No account found with this phone number",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "code": "USER_NOT_FOUND",
    "suggestion": "Would you like to create an account?",
    "createAccountUrl": "/api/v1/auth/signup/initiate"
  }
}

B.2 Verify OTP Login

POST /api/v1/auth/login/otp/verify

Request:

{
  "tempToken": "eyJhbGciOiJIUzI1NiIs...",
  "otpCode": "123456",
  "trustDevice": true,
  "deviceInfo": {
    "deviceId": "abc123-device-fingerprint",
    "deviceName": "iPhone 15 Pro",
    "deviceType": "MOBILE_IOS",
    "appVersion": "1.2.0"
  }
}

Response (Success - Device Trusted):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Login successful",
  "action_time": "2025-01-11T16:01:00",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "profilePictureUrl": "https://storage.example.com/...",
      "hasPassword": false,
      "onboardingComplete": true
    },
    "device": {
      "deviceId": "abc123-device-fingerprint",
      "deviceName": "iPhone 15 Pro",
      "trusted": true,
      "trustExpiresAt": "2025-02-10T16:01:00"
    },
    "promptSetPassword": true,
    "passwordPromptMessage": "Set a password for faster login on trusted devices"
  }
}

Response (Success - Device NOT Trusted by user choice):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Login successful (device not saved)",
  "action_time": "2025-01-11T16:01:00",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "profilePictureUrl": "https://storage.example.com/...",
      "hasPassword": false,
      "onboardingComplete": true
    },
    "device": {
      "trusted": false,
      "note": "This device will require OTP on next login"
    }
  }
}

Login Option C: Social Login (Google/Apple)

POST /api/v1/auth/oauth/google

Request:

{
  "idToken": "eyJhbGciOiJSUzI1NiIs...",
  "deviceInfo": {
    "deviceId": "abc123-device-fingerprint",
    "deviceName": "iPhone 15 Pro",
    "deviceType": "MOBILE_IOS",
    "appVersion": "1.2.0"
  }
}

Response (Existing User - Trusted Device):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Welcome back!",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "action": "LOGIN",
    "isNewUser": false,
    "requiresOtp": false,
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "email": "alex@gmail.com",
      "profilePictureUrl": "https://storage.example.com/...",
      "isEmailVerified": true,
      "hasPassword": true,
      "authProviders": ["PHONE", "EMAIL", "GOOGLE"],
      "onboardingComplete": true
    },
    "device": {
      "deviceId": "abc123-device-fingerprint",
      "deviceName": "iPhone 15 Pro",
      "trusted": true,
      "trustExpiresAt": "2025-02-10T16:00:00"
    }
  }
}

Response (Existing User - New Device - OTP Required):

{
  "success": true,
  "httpStatus": "OK",
  "message": "New device detected. Please verify with OTP.",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "action": "DEVICE_VERIFICATION_REQUIRED",
    "isNewUser": false,
    "requiresOtp": true,
    "otpReason": "NEW_DEVICE",
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "otpSentTo": "+255*****678",
    "otpMethod": "SMS",
    "expiresAt": "2025-01-11T16:10:00",
    "user": {
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "profilePictureUrl": "https://storage.example.com/..."
    },
    "device": {
      "deviceId": "xyz789-new-device",
      "deviceName": "Chrome on Windows",
      "isNew": true
    },
    "securityNote": "Even though you signed in with Google, we need to verify this new device."
  }
}

Response (Existing User - No Phone - Fallback Options):

{
  "success": true,
  "httpStatus": "OK",
  "message": "New device detected. Please verify to continue.",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "action": "VERIFICATION_REQUIRED_NO_PHONE",
    "isNewUser": false,
    "requiresVerification": true,
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "user": {
      "userName": "alexvibes",
      "displayName": "Alex Johnson"
    },
    "device": {
      "deviceId": "xyz789-new-device",
      "deviceName": "Chrome on Windows",
      "isNew": true
    },
    "verificationOptions": [
      {
        "method": "ADD_PHONE",
        "description": "Add phone number to receive OTP",
        "recommended": true
      },
      {
        "method": "OTP_EMAIL",
        "description": "Send OTP to a***@gmail.com",
        "available": true,
        "lessSecure": true,
        "note": "Email is less secure than phone"
      },
      {
        "method": "PASSWORD",
        "available": true,
        "description": "Enter your password"
      }
    ],
    "securityNote": "For better security, we recommend adding a phone number."
  }
}

Response (New User - Account Created):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Account created successfully",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "action": "CREATED",
    "isNewUser": true,
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "user": {
      "id": "660e8400-e29b-41d4-a716-446655440001",
      "systemUsername": "usr_660e8400e29b41d4",
      "userName": null,
      "email": "alex@gmail.com",
      "firstName": "Alex",
      "lastName": "Johnson",
      "profilePictureUrl": "https://lh3.googleusercontent.com/...",
      "isEmailVerified": true,
      "hasPassword": false,
      "authProviders": ["GOOGLE"],
      "onboardingStep": "NAME_BIRTHDATE",
      "onboardingComplete": false
    },
    "device": {
      "deviceId": "abc123-device-fingerprint",
      "deviceName": "iPhone 15 Pro",
      "trusted": true,
      "trustExpiresAt": "2025-02-10T16:00:00",
      "note": "First device is automatically trusted"
    },
    "promptAddPhone": true,
    "phonePromptMessage": "Add your phone number to secure your account"
  }
}

Response (Email Matches Existing - Link Required):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Account found with this email. Please verify to link.",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "action": "LINK_REQUIRED",
    "isNewUser": false,
    "existingAccount": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "maskedEmail": "a***@example.com",
      "maskedPhone": "+255*****678",
      "hasPassword": true,
      "authProviders": ["PHONE", "EMAIL"],
      "createdAt": "2024-06-15T10:00:00"
    },
    "linkOptions": [
      {
        "method": "PASSWORD",
        "available": true,
        "description": "Enter your password to link"
      },
      {
        "method": "OTP_PHONE",
        "available": true,
        "description": "Get OTP on +255*****678"
      },
      {
        "method": "OTP_EMAIL",
        "available": true,
        "description": "Get OTP on a***@example.com"
      }
    ],
    "oauthProvider": "GOOGLE",
    "oauthEmail": "alex@gmail.com",
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "device": {
      "deviceId": "abc123-device-fingerprint",
      "deviceName": "iPhone 15 Pro",
      "note": "Device will be trusted after account link"
    }
  }
}

Apple Sign In

POST /api/v1/auth/oauth/apple

Request:

{
  "identityToken": "eyJraWQiOiJXNldjT0...",
  "authorizationCode": "c7e8a3f1b2d4...",
  "firstName": "Alex",
  "lastName": "Johnson",
  "deviceInfo": {
    "deviceId": "abc123-device-fingerprint",
    "deviceName": "iPhone 15 Pro",
    "deviceType": "MOBILE_IOS",
    "appVersion": "1.2.0"
  }
}

Note: Apple only provides name on first authorization. Store it!

Responses: Same structure as Google OAuth responses above.


Verify Device OTP (After any login triggers device verification)

POST /api/v1/auth/login/verify-device

Request:

{
  "tempToken": "eyJhbGciOiJIUzI1NiIs...",
  "otpCode": "123456",
  "trustDevice": true,
  "deviceInfo": {
    "deviceId": "xyz789-new-device",
    "deviceName": "Chrome on Windows",
    "deviceType": "WEB_BROWSER"
  }
}

Response (Success):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Device verified and trusted",
  "action_time": "2025-01-11T16:02:00",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "profilePictureUrl": "https://storage.example.com/...",
      "onboardingComplete": true
    },
    "device": {
      "deviceId": "xyz789-new-device",
      "deviceName": "Chrome on Windows",
      "trusted": true,
      "trustExpiresAt": "2025-02-10T16:02:00"
    }
  }
}

POST-ONBOARDING: Set Password (Optional)

POST /api/v1/auth/password/set

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Request:

{
  "newPassword": "MySecurePass123!",
  "confirmPassword": "MySecurePass123!"
}

Response (Success):

{
  "success": true,
  "message": "Password set successfully. You can now use password login.",
  "data": {
    "hasPassword": true,
    "passwordStrength": {
      "score": 85,
      "level": "STRONG",
      "feedback": "Great password!"
    }
  },
  "timestamp": "2025-01-11T16:05:00Z"
}

Response (Weak Password):

{
  "success": false,
  "message": "Password is too weak",
  "error": {
    "code": "WEAK_PASSWORD",
    "passwordStrength": {
      "score": 40,
      "level": "WEAK",
      "feedback": "Add uppercase letters and special characters"
    },
    "requirements": [
      "At least 8 characters",
      "At least one uppercase letter",
      "At least one lowercase letter",
      "At least one number",
      "At least one special character (@$!%*?&#)"
    ]
  },
  "timestamp": "2025-01-11T16:05:00Z"
}

TOKEN MANAGEMENT

Refresh Token

POST /api/v1/auth/token/refresh

Request:

{
  "refreshToken": "eyJhbGciOiJIUzI1NiIs..."
}

Response (Success):

{
  "success": true,
  "message": "Token refreshed successfully",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600
  },
  "timestamp": "2025-01-11T17:00:00Z"
}

Response (Invalid/Expired Refresh Token):

{
  "success": false,
  "message": "Session expired. Please login again.",
  "error": {
    "code": "REFRESH_TOKEN_EXPIRED"
  },
  "timestamp": "2025-01-11T17:00:00Z"
}

Logout

POST /api/v1/auth/logout

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Request:

{
  "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
  "logoutAllDevices": false
}

Response:

{
  "success": true,
  "message": "Logged out successfully",
  "data": null,
  "timestamp": "2025-01-11T18:00:00Z"
}

PASSWORD RESET

Request Password Reset

POST /api/v1/auth/password/reset/request

Request:

{
  "identifier": "alex@example.com"
}

Response:

{
  "success": true,
  "message": "Password reset OTP sent to your email",
  "data": {
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "maskedIdentifier": "a***@example.com",
    "expiresAt": "2025-01-11T16:10:00Z"
  },
  "timestamp": "2025-01-11T16:00:00Z"
}

Verify Reset OTP & Set New Password

POST /api/v1/auth/password/reset/confirm

Request:

{
  "tempToken": "eyJhbGciOiJIUzI1NiIs...",
  "otpCode": "123456",
  "newPassword": "MyNewSecurePass123!",
  "confirmPassword": "MyNewSecurePass123!"
}

Response:

{
  "success": true,
  "message": "Password reset successfully. Please login with your new password.",
  "data": null,
  "timestamp": "2025-01-11T16:02:00Z"
}

GET CURRENT USER (Check Auth Status)

GET /api/v1/auth/me

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "User retrieved successfully",
  "action_time": "2025-01-11T18:00:00",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "systemUsername": "usr_550e8400e29b41d4",
    "userName": "alexvibes",
    "displayName": "Alex Johnson",
    "firstName": "Alex",
    "lastName": "Johnson",
    "email": "alex@example.com",
    "phoneNumber": "+255712345678",
    "profilePictureUrl": "https://storage.example.com/...",
    "bio": "Fashion lover | Shopaholic | Event creator 🔥",
    "birthDate": "1995-06-15",
    "isEmailVerified": true,
    "isPhoneVerified": true,
    "hasPassword": true,
    "authProviders": ["PHONE", "GOOGLE"],
    "onboardingComplete": true,
    "onboardingStep": "COMPLETE",
    "interests": [
      { "id": "cat_001", "name": "Fashion" },
      { "id": "cat_003", "name": "Beauty & Cosmetics" }
    ],
    "createdAt": "2025-01-11T15:20:00",
    "updatedAt": "2025-01-11T15:27:00"
  }
}

ONBOARDING STATUS CHECK

GET /api/v1/onboarding/status

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Response (Incomplete):

{
  "success": true,
  "message": "Onboarding in progress",
  "data": {
    "onboardingComplete": false,
    "currentStep": "PROFILE_SETUP",
    "completedSteps": ["SIGNUP", "NAME_BIRTHDATE"],
    "remainingSteps": ["PROFILE_SETUP", "INTERESTS"],
    "progress": 50
  },
  "timestamp": "2025-01-11T15:23:00Z"
}

Response (Complete):

{
  "success": true,
  "message": "Onboarding complete",
  "data": {
    "onboardingComplete": true,
    "currentStep": "COMPLETE",
    "completedSteps": ["SIGNUP", "NAME_BIRTHDATE", "PROFILE_SETUP", "INTERESTS"],
    "remainingSteps": [],
    "progress": 100
  },
  "timestamp": "2025-01-11T15:27:00Z"
}

ERROR RESPONSE FORMAT (Standard)

All error responses follow GlobeFailureResponseBuilder structure:

{
  "success": false,
  "httpStatus": "BAD_REQUEST",
  "message": "Human readable error message",
  "action_time": "2025-01-11T15:20:00",
  "data": {
    "code": "ERROR_CODE",
    "field": "fieldName",
    "details": "Additional details if any",
    "suggestion": "What user can do"
  }
}

Common Error Codes

Code HTTP Status Description
VALIDATION_ERROR BAD_REQUEST Request validation failed
INVALID_OTP BAD_REQUEST OTP code is incorrect
OTP_EXPIRED BAD_REQUEST OTP has expired
INVALID_CREDENTIALS UNAUTHORIZED Wrong password
TOKEN_EXPIRED UNAUTHORIZED Access token expired
REFRESH_TOKEN_EXPIRED UNAUTHORIZED Refresh token expired
UNAUTHORIZED UNAUTHORIZED Not authenticated
FORBIDDEN FORBIDDEN Not allowed
USER_NOT_FOUND NOT_FOUND User doesn't exist
ACCOUNT_EXISTS CONFLICT Account already exists
USERNAME_TAKEN CONFLICT Username is taken
PHONE_ALREADY_REGISTERED CONFLICT Phone belongs to another account
PHONE_TEMPORARILY_RESERVED CONFLICT Phone claimed but unverified
ACCOUNT_LOCKED LOCKED Too many failed attempts
RESEND_COOLDOWN TOO_MANY_REQUESTS Wait before resending OTP
RATE_LIMITED TOO_MANY_REQUESTS Too many requests
WEAK_PASSWORD UNPROCESSABLE_ENTITY Password doesn't meet requirements
UNDERAGE UNPROCESSABLE_ENTITY User is under minimum age
MIN_INTERESTS_REQUIRED UNPROCESSABLE_ENTITY Must have minimum interests
MAX_INTERESTS_REACHED UNPROCESSABLE_ENTITY Maximum interests reached
SERVER_ERROR INTERNAL_SERVER_ERROR Internal server error

ENUMS REFERENCE

OnboardingStep

SIGNUP
NAME_BIRTHDATE
PROFILE_SETUP
INTERESTS
COMPLETE

AuthProvider

PHONE
EMAIL
GOOGLE
APPLE

OTP Purpose

SIGNUP_VERIFICATION
LOGIN_OTP
PASSWORD_RESET
EMAIL_VERIFICATION
PHONE_VERIFICATION

SUMMARY: What's New vs Existing

New Endpoints

  • POST /api/v1/auth/signup/initiate (replaces /register)
  • POST /api/v1/auth/signup/verify (replaces /verify-otp)
  • POST /api/v1/auth/signup/google
  • POST /api/v1/auth/signup/apple
  • PUT /api/v1/onboarding/name-birthdate
  • GET /api/v1/onboarding/username/check
  • POST /api/v1/onboarding/profile-picture
  • PUT /api/v1/onboarding/profile-setup
  • GET /api/v1/onboarding/interests/categories
  • POST /api/v1/onboarding/interests
  • POST /api/v1/onboarding/interests/skip
  • GET /api/v1/onboarding/complete
  • POST /api/v1/auth/login/password
  • POST /api/v1/auth/login/otp/request
  • POST /api/v1/auth/login/otp/verify
  • POST /api/v1/auth/password/set
  • GET /api/v1/auth/me
  • GET /api/v1/onboarding/status

Modified Endpoints

  • POST /api/v1/auth/otp/resend (updated response)
  • POST /api/v1/auth/token/refresh (same, just standardized response)
  • POST /api/v1/auth/password/reset/request (updated from /psw-reset-otp)
  • POST /api/v1/auth/password/reset/confirm (updated from /reset-password)

Deprecated/Removed

  • POST /api/v1/auth/register (replaced by /signup/initiate)
  • POST /api/v1/auth/login (replaced by password/OTP specific endpoints)


INTEREST SYSTEM

The interest system tracks user preferences through two methods:

  1. Explicit: User picks during onboarding (visible to user)
  2. Implicit: Silent tracking based on user behavior (invisible to user)

Scores update silently in the background as users interact with content.


Interest Scoring Logic

Action Score Notes
Explicitly selected (onboarding) +100 Base score for picked interests
View post +1 Quick glance
Like post +3 Shows interest
Comment on post +5 Higher engagement
Share post +7 Strong signal
Save/Bookmark +5 Wants to revisit
Follow user in category +10 Committed interest
View product +2 Shopping interest
Add to cart +8 Purchase intent
Purchase product +15 Strongest signal
View event +2 Curious
Attend event +12 Committed
Search term +4 Active seeking
Time spent (per 30sec) +1 Passive engagement
Scroll past quickly -1 Not interested
Hide/Not interested -20 Explicit dislike
Report content -30 Strong negative

Score Decay: 10% reduction per week of no interaction (keeps recommendations fresh)

Max Score: 1000 per category


ADMIN: Interest Category Management

Create Category

POST /api/v1/admin/interests/categories

Headers:

Authorization: Bearer {admin_token}

Request:

{
  "name": "Fashion",
  "icon": "👗",
  "iconUrl": "https://cdn.example.com/icons/fashion.png",
  "color": "#FF6B6B",
  "keywords": ["clothes", "style", "outfit", "wear", "dress"],
  "displayOrder": 1,
  "isActive": true
}

Response:

{
  "success": true,
  "message": "Category created successfully",
  "data": {
    "id": "cat_001",
    "name": "Fashion",
    "icon": "👗",
    "iconUrl": "https://cdn.example.com/icons/fashion.png",
    "color": "#FF6B6B",
    "keywords": ["clothes", "style", "outfit", "wear", "dress"],
    "displayOrder": 1,
    "isActive": true,
    "createdAt": "2025-01-11T16:00:00Z",
    "updatedAt": "2025-01-11T16:00:00Z"
  },
  "timestamp": "2025-01-11T16:00:00Z"
}

List All Categories (Admin View)

GET /api/v1/admin/interests/categories?includeInactive=true

Response:

{
  "success": true,
  "message": "Categories retrieved successfully",
  "data": {
    "categories": [
      {
        "id": "cat_001",
        "name": "Fashion",
        "icon": "👗",
        "iconUrl": "https://cdn.example.com/icons/fashion.png",
        "color": "#FF6B6B",
        "keywords": ["clothes", "style", "outfit"],
        "displayOrder": 1,
        "isActive": true,
        "stats": {
          "usersExplicit": 15420,
          "usersImplicit": 28750,
          "totalEngagements": 89500,
          "postsTagged": 8920,
          "productsTagged": 3450,
          "shopsTagged": 234,
          "eventsTagged": 89
        },
        "createdAt": "2025-01-01T10:00:00Z",
        "updatedAt": "2025-01-10T14:30:00Z"
      },
      {
        "id": "cat_002",
        "name": "Electronics",
        "icon": "📱",
        "iconUrl": "https://cdn.example.com/icons/electronics.png",
        "color": "#4ECDC4",
        "keywords": ["phone", "laptop", "gadget", "tech"],
        "displayOrder": 2,
        "isActive": true,
        "stats": {
          "usersExplicit": 12300,
          "usersImplicit": 31000,
          "totalEngagements": 125000,
          "postsTagged": 5600,
          "productsTagged": 8900,
          "shopsTagged": 456,
          "eventsTagged": 23
        },
        "createdAt": "2025-01-01T10:00:00Z",
        "updatedAt": "2025-01-10T14:30:00Z"
      }
    ],
    "totalCategories": 20,
    "activeCategories": 18,
    "inactiveCategories": 2
  },
  "timestamp": "2025-01-11T16:00:00Z"
}

Update Category

PUT /api/v1/admin/interests/categories/{categoryId}

Request:

{
  "name": "Fashion & Style",
  "icon": "👗",
  "iconUrl": "https://cdn.example.com/icons/fashion-v2.png",
  "color": "#FF5252",
  "keywords": ["clothes", "style", "outfit", "wear", "dress", "apparel"],
  "isActive": true
}

Response:

{
  "success": true,
  "message": "Category updated successfully",
  "data": {
    "id": "cat_001",
    "name": "Fashion & Style",
    "icon": "👗",
    "iconUrl": "https://cdn.example.com/icons/fashion-v2.png",
    "color": "#FF5252",
    "keywords": ["clothes", "style", "outfit", "wear", "dress", "apparel"],
    "displayOrder": 1,
    "isActive": true,
    "updatedAt": "2025-01-11T16:05:00Z"
  },
  "timestamp": "2025-01-11T16:05:00Z"
}

Reorder Categories

PUT /api/v1/admin/interests/categories/reorder

Request:

{
  "order": [
    { "categoryId": "cat_003", "displayOrder": 1 },
    { "categoryId": "cat_001", "displayOrder": 2 },
    { "categoryId": "cat_002", "displayOrder": 3 }
  ]
}

Response:

{
  "success": true,
  "message": "Categories reordered successfully",
  "data": {
    "updated": 3
  },
  "timestamp": "2025-01-11T16:10:00Z"
}

Deactivate Category (Soft Delete)

DELETE /api/v1/admin/interests/categories/{categoryId}

Response:

{
  "success": true,
  "message": "Category deactivated successfully",
  "data": {
    "id": "cat_015",
    "name": "Outdated Category",
    "isActive": false,
    "deactivatedAt": "2025-01-11T16:15:00Z",
    "note": "Category hidden from users. Existing user data preserved."
  },
  "timestamp": "2025-01-11T16:15:00Z"
}

Get Category Analytics

GET /api/v1/admin/interests/categories/{categoryId}/analytics?period=30d

Response:

{
  "success": true,
  "message": "Analytics retrieved successfully",
  "data": {
    "categoryId": "cat_001",
    "categoryName": "Fashion",
    "period": "LAST_30_DAYS",
    "overview": {
      "totalUsers": 44170,
      "explicitUsers": 15420,
      "implicitUsers": 28750,
      "averageScore": 67.5,
      "totalEngagements": 89500
    },
    "trend": {
      "direction": "UP",
      "percentageChange": 12.5,
      "newUsersThisPeriod": 2340
    },
    "engagement": {
      "likes": 34000,
      "comments": 12500,
      "shares": 8900,
      "saves": 15600,
      "purchases": 3200
    },
    "topContent": {
      "topPosts": [
        { "postId": "post_123", "engagements": 4500 },
        { "postId": "post_456", "engagements": 3200 }
      ],
      "topProducts": [
        { "productId": "prod_789", "sales": 234 },
        { "productId": "prod_012", "sales": 189 }
      ]
    },
    "demographics": {
      "ageGroups": [
        { "range": "13-17", "percentage": 15 },
        { "range": "18-24", "percentage": 35 },
        { "range": "25-34", "percentage": 30 },
        { "range": "35-44", "percentage": 12 },
        { "range": "45+", "percentage": 8 }
      ]
    }
  },
  "timestamp": "2025-01-11T16:20:00Z"
}

PUBLIC: Interest Categories

Get Active Categories (Onboarding & Settings)

GET /api/v1/interests/categories

No auth required for onboarding, shows only active categories

Response:

{
  "success": true,
  "message": "Categories retrieved successfully",
  "data": {
    "categories": [
      {
        "id": "cat_001",
        "name": "Fashion",
        "icon": "👗",
        "iconUrl": "https://cdn.example.com/icons/fashion.png",
        "color": "#FF6B6B"
      },
      {
        "id": "cat_002",
        "name": "Electronics",
        "icon": "📱",
        "iconUrl": "https://cdn.example.com/icons/electronics.png",
        "color": "#4ECDC4"
      },
      {
        "id": "cat_003",
        "name": "Beauty & Cosmetics",
        "icon": "💄",
        "iconUrl": "https://cdn.example.com/icons/beauty.png",
        "color": "#FF69B4"
      },
      {
        "id": "cat_004",
        "name": "Food & Drinks",
        "icon": "🍔",
        "iconUrl": "https://cdn.example.com/icons/food.png",
        "color": "#F39C12"
      },
      {
        "id": "cat_005",
        "name": "Sports & Fitness",
        "icon": "⚽",
        "iconUrl": "https://cdn.example.com/icons/sports.png",
        "color": "#2ECC71"
      },
      {
        "id": "cat_006",
        "name": "Music & Dance",
        "icon": "🎵",
        "iconUrl": "https://cdn.example.com/icons/music.png",
        "color": "#9B59B6"
      },
      {
        "id": "cat_007",
        "name": "Home & Decor",
        "icon": "🏠",
        "iconUrl": "https://cdn.example.com/icons/home.png",
        "color": "#E67E22"
      },
      {
        "id": "cat_008",
        "name": "Tech & Gadgets",
        "icon": "💻",
        "iconUrl": "https://cdn.example.com/icons/tech.png",
        "color": "#3498DB"
      },
      {
        "id": "cat_009",
        "name": "Travel",
        "icon": "✈️",
        "iconUrl": "https://cdn.example.com/icons/travel.png",
        "color": "#1ABC9C"
      },
      {
        "id": "cat_010",
        "name": "Gaming",
        "icon": "🎮",
        "iconUrl": "https://cdn.example.com/icons/gaming.png",
        "color": "#8E44AD"
      },
      {
        "id": "cat_011",
        "name": "Books & Reading",
        "icon": "📚",
        "iconUrl": "https://cdn.example.com/icons/books.png",
        "color": "#D35400"
      },
      {
        "id": "cat_012",
        "name": "Art & Design",
        "icon": "🎨",
        "iconUrl": "https://cdn.example.com/icons/art.png",
        "color": "#E74C3C"
      },
      {
        "id": "cat_013",
        "name": "Health & Wellness",
        "icon": "🧘",
        "iconUrl": "https://cdn.example.com/icons/wellness.png",
        "color": "#27AE60"
      },
      {
        "id": "cat_014",
        "name": "Automotive",
        "icon": "🚗",
        "iconUrl": "https://cdn.example.com/icons/auto.png",
        "color": "#34495E"
      },
      {
        "id": "cat_015",
        "name": "Pets & Animals",
        "icon": "🐾",
        "iconUrl": "https://cdn.example.com/icons/pets.png",
        "color": "#F1C40F"
      },
      {
        "id": "cat_016",
        "name": "Photography",
        "icon": "📷",
        "iconUrl": "https://cdn.example.com/icons/photo.png",
        "color": "#7F8C8D"
      },
      {
        "id": "cat_017",
        "name": "Kids & Baby",
        "icon": "👶",
        "iconUrl": "https://cdn.example.com/icons/kids.png",
        "color": "#FFB6C1"
      },
      {
        "id": "cat_018",
        "name": "Business & Finance",
        "icon": "💼",
        "iconUrl": "https://cdn.example.com/icons/business.png",
        "color": "#2C3E50"
      },
      {
        "id": "cat_019",
        "name": "Entertainment",
        "icon": "🎬",
        "iconUrl": "https://cdn.example.com/icons/entertainment.png",
        "color": "#C0392B"
      },
      {
        "id": "cat_020",
        "name": "DIY & Crafts",
        "icon": "🛠️",
        "iconUrl": "https://cdn.example.com/icons/diy.png",
        "color": "#16A085"
      }
    ],
    "selectionRules": {
      "minimum": 3,
      "recommended": 5,
      "maximum": 15,
      "canSkip": true
    }
  },
  "timestamp": "2025-01-11T15:25:00Z"
}

USER: Interest Management

Get My Interests

GET /api/v1/interests/me

Headers:

Authorization: Bearer {access_token}

Response:

{
  "success": true,
  "message": "Interests retrieved successfully",
  "data": {
    "explicit": [
      {
        "categoryId": "cat_001",
        "categoryName": "Fashion",
        "icon": "👗",
        "color": "#FF6B6B",
        "source": "ONBOARDING",
        "addedAt": "2025-01-11T15:26:00Z"
      },
      {
        "categoryId": "cat_003",
        "categoryName": "Beauty & Cosmetics",
        "icon": "💄",
        "color": "#FF69B4",
        "source": "SETTINGS",
        "addedAt": "2025-01-12T10:00:00Z"
      }
    ],
    "topImplicit": [
      {
        "categoryId": "cat_002",
        "categoryName": "Electronics",
        "icon": "📱",
        "color": "#4ECDC4",
        "score": 87,
        "trend": "RISING"
      },
      {
        "categoryId": "cat_006",
        "categoryName": "Music & Dance",
        "icon": "🎵",
        "color": "#9B59B6",
        "score": 65,
        "trend": "STABLE"
      },
      {
        "categoryId": "cat_009",
        "categoryName": "Travel",
        "icon": "✈️",
        "color": "#1ABC9C",
        "score": 42,
        "trend": "FALLING"
      }
    ],
    "summary": {
      "explicitCount": 2,
      "implicitCount": 8,
      "topCategory": "Fashion",
      "feedPersonalization": "HIGH"
    }
  },
  "timestamp": "2025-01-11T18:00:00Z"
}

Update My Explicit Interests

PUT /api/v1/interests/me

Headers:

Authorization: Bearer {access_token}

Request:

{
  "categoryIds": ["cat_001", "cat_003", "cat_006", "cat_010", "cat_012"]
}

Response:

{
  "success": true,
  "message": "Interests updated successfully",
  "data": {
    "explicit": [
      { "categoryId": "cat_001", "categoryName": "Fashion" },
      { "categoryId": "cat_003", "categoryName": "Beauty & Cosmetics" },
      { "categoryId": "cat_006", "categoryName": "Music & Dance" },
      { "categoryId": "cat_010", "categoryName": "Gaming" },
      { "categoryId": "cat_012", "categoryName": "Art & Design" }
    ],
    "changes": {
      "added": ["cat_006", "cat_010", "cat_012"],
      "removed": [],
      "unchanged": ["cat_001", "cat_003"]
    },
    "feedImpact": "Your feed will now show more Music, Gaming, and Art content"
  },
  "timestamp": "2025-01-11T18:05:00Z"
}

Add Single Interest

POST /api/v1/interests/me/{categoryId}

Headers:

Authorization: Bearer {access_token}

Response:

{
  "success": true,
  "message": "Interest added successfully",
  "data": {
    "categoryId": "cat_015",
    "categoryName": "Pets & Animals",
    "icon": "🐾",
    "addedAt": "2025-01-11T18:10:00Z",
    "totalExplicit": 6
  },
  "timestamp": "2025-01-11T18:10:00Z"
}

Response (Max Reached):

{
  "success": false,
  "message": "Maximum interests reached",
  "error": {
    "code": "MAX_INTERESTS_REACHED",
    "current": 15,
    "maximum": 15,
    "suggestion": "Remove an interest before adding a new one"
  },
  "timestamp": "2025-01-11T18:10:00Z"
}

Remove Single Interest

DELETE /api/v1/interests/me/{categoryId}

Headers:

Authorization: Bearer {access_token}

Response:

{
  "success": true,
  "message": "Interest removed successfully",
  "data": {
    "categoryId": "cat_015",
    "categoryName": "Pets & Animals",
    "removedAt": "2025-01-11T18:15:00Z",
    "totalExplicit": 5,
    "note": "You may still see some Pets & Animals content based on your activity"
  },
  "timestamp": "2025-01-11T18:15:00Z"
}

Response (Minimum Required):

{
  "success": false,
  "message": "Cannot remove interest",
  "error": {
    "code": "MIN_INTERESTS_REQUIRED",
    "current": 3,
    "minimum": 3,
    "suggestion": "Add another interest before removing this one"
  },
  "timestamp": "2025-01-11T18:15:00Z"
}

Hide Content Category (Negative Signal)

POST /api/v1/interests/me/{categoryId}/hide

User explicitly says "not interested" - strong negative signal

Headers:

Authorization: Bearer {access_token}

Response:

{
  "success": true,
  "message": "You'll see less of this content",
  "data": {
    "categoryId": "cat_018",
    "categoryName": "Business & Finance",
    "action": "HIDDEN",
    "feedImpact": "Business & Finance content will be significantly reduced in your feed",
    "canUndo": true,
    "undoExpiry": "2025-01-11T18:30:00Z"
  },
  "timestamp": "2025-01-11T18:20:00Z"
}

Unhide Content Category

DELETE /api/v1/interests/me/{categoryId}/hide

Response:

{
  "success": true,
  "message": "Category unhidden",
  "data": {
    "categoryId": "cat_018",
    "categoryName": "Business & Finance",
    "action": "UNHIDDEN",
    "feedImpact": "Business & Finance content will appear normally based on your activity"
  },
  "timestamp": "2025-01-11T18:25:00Z"
}

Get Hidden Categories

GET /api/v1/interests/me/hidden

Response:

{
  "success": true,
  "message": "Hidden categories retrieved",
  "data": {
    "hidden": [
      {
        "categoryId": "cat_018",
        "categoryName": "Business & Finance",
        "icon": "💼",
        "hiddenAt": "2025-01-11T18:20:00Z"
      },
      {
        "categoryId": "cat_014",
        "categoryName": "Automotive",
        "icon": "🚗",
        "hiddenAt": "2025-01-05T12:00:00Z"
      }
    ],
    "totalHidden": 2
  },
  "timestamp": "2025-01-11T18:30:00Z"
}

INTERNAL: Silent Interest Tracking

These are NOT public API endpoints. They are triggered internally when users interact with content.

How It Works

When user performs any action on content (post, product, shop, event), the system:

  1. Gets the category tags from that content
  2. Calculates score based on action type
  3. Updates user's implicit interest scores silently
  4. No API call from client needed

Actions That Trigger Tracking

User Action System Tracks
Views post POST_VIEW on post's categories
Likes post POST_LIKE on post's categories
Comments on post POST_COMMENT on post's categories
Shares post POST_SHARE on post's categories
Saves post POST_SAVE on post's categories
Views product PRODUCT_VIEW on product's category
Adds to cart PRODUCT_CART on product's category
Purchases PRODUCT_PURCHASE on product's category
Views shop SHOP_VIEW on shop's categories
Follows shop SHOP_FOLLOW on shop's categories
Views event EVENT_VIEW on event's categories
RSVPs to event EVENT_RSVP on event's categories
Follows user USER_FOLLOW on user's primary categories
Searches SEARCH on matched categories
Scrolls past quickly SCROLL_PAST (negative)
Clicks "Not interested" HIDE_CONTENT (strong negative)

Content Must Have Category Tags

For tracking to work, all content must be tagged:

// Post
{
  "id": "post_123",
  "content": "Check out my new outfit!",
  "categoryIds": ["cat_001"]  // Fashion
}

// Product
{
  "id": "prod_456", 
  "name": "Wireless Earbuds",
  "categoryId": "cat_002"  // Electronics
}

// Shop
{
  "id": "shop_789",
  "name": "StyleHub",
  "categoryIds": ["cat_001", "cat_003"]  // Fashion, Beauty
}

// Event
{
  "id": "event_012",
  "title": "Summer Music Festival",
  "categoryIds": ["cat_006", "cat_019"]  // Music, Entertainment
}

FEED: Using Interests for Recommendations

Get Personalized Feed

GET /api/v1/feed?page=1&size=20

Feed algorithm uses interest scores to rank content

Response includes personalization info:

{
  "success": true,
  "data": {
    "posts": [
      {
        "id": "post_123",
        "content": "...",
        "author": { "...": "..." },
        "categoryIds": ["cat_001"],
        "relevanceScore": 0.95,
        "relevanceReason": "EXPLICIT_INTEREST"
      },
      {
        "id": "post_456",
        "content": "...",
        "author": { "...": "..." },
        "categoryIds": ["cat_002"],
        "relevanceScore": 0.87,
        "relevanceReason": "HIGH_IMPLICIT_SCORE"
      },
      {
        "id": "post_789",
        "content": "...",
        "author": { "...": "..." },
        "categoryIds": ["cat_015"],
        "relevanceScore": 0.45,
        "relevanceReason": "TRENDING"
      }
    ],
    "personalization": {
      "status": "ACTIVE",
      "basedOn": {
        "explicitInterests": 5,
        "implicitInterests": 8,
        "followedAccounts": 23
      }
    },
    "pagination": { "...": "..." }
  }
}

SUMMARY: Interest System Endpoints

Admin Endpoints

Method Endpoint Description
POST /api/v1/admin/interests/categories Create category
GET /api/v1/admin/interests/categories List all (with stats)
PUT /api/v1/admin/interests/categories/{id} Update category
DELETE /api/v1/admin/interests/categories/{id} Deactivate category
PUT /api/v1/admin/interests/categories/reorder Reorder categories
GET /api/v1/admin/interests/categories/{id}/analytics Get analytics

Public Endpoints

Method Endpoint Description
GET /api/v1/interests/categories Get active categories

User Endpoints (Requires Auth)

Method Endpoint Description
GET /api/v1/interests/me Get my interests
PUT /api/v1/interests/me Update all explicit
POST /api/v1/interests/me/{id} Add single interest
DELETE /api/v1/interests/me/{id} Remove single interest
POST /api/v1/interests/me/{id}/hide Hide category
DELETE /api/v1/interests/me/{id}/hide Unhide category
GET /api/v1/interests/me/hidden Get hidden list

Internal (No Public API)

  • Silent tracking triggered by user actions
  • Score calculation and decay
  • Feed personalization

New Database Fields Needed

AccountEntity

// Existing fields to keep
private UUID id;
private String userName;              // Public @handle (changeable)
private String phoneNumber;
private String email;
private String password;
private String firstName;
private String lastName;
private String middleName;
private String bio;
private String location;
private Boolean isVerified;
private Boolean isEmailVerified;
private Boolean isPhoneVerified;
private boolean twoFactorEnabled;
private String twoFactorSecret;
private boolean locked;
private String lockedReason;
private LocalDateTime createdAt;
private LocalDateTime editedAt;
private Set<Roles> roles;
private List<String> profilePictureUrls;
private boolean isBucketCreated;

// NEW fields to add
private String systemUsername;        // Internal identifier (never changes) - "usr_550e8400e29b41d4"
private LocalDate birthDate;          // For age gating
private String displayName;           // Full display name "Alex Johnson"
private String authProvider;          // PHONE, EMAIL, GOOGLE, APPLE (primary signup method)
private String googleId;              // Google OAuth ID
private String appleId;               // Apple OAuth ID
private String onboardingStep;        // SIGNUP, NAME_BIRTHDATE, PROFILE_SETUP, INTERESTS, COMPLETE
private Boolean onboardingComplete;   // Quick check flag
private Boolean hasPassword;          // Whether user set a password
private LocalDateTime phoneClaimedAt; // When phone was first set (for claim expiry)
private LocalDateTime phoneVerifiedAt;// When phone was verified
private LocalDateTime emailClaimedAt; // When email was first set
private LocalDateTime emailVerifiedAt;// When email was verified

New Entities

InterestCategory (Admin managed)

- id (UUID)
- name (String)
- icon (String) - emoji
- iconUrl (String) - image URL
- color (String) - hex color
- keywords (List<String>) - for auto-tagging
- displayOrder (Integer)
- isActive (Boolean)
- createdAt (LocalDateTime)
- updatedAt (LocalDateTime)

UserInterest (User's interests)

- id (UUID)
- userId (UUID)
- categoryId (UUID)
- source (Enum: ONBOARDING, SETTINGS, IMPLICIT)
- score (Integer) - 0 to 1000
- isExplicit (Boolean)
- isHidden (Boolean)
- lastInteractionAt (LocalDateTime)
- createdAt (LocalDateTime)
- updatedAt (LocalDateTime)

InterestEvent (Tracking log - optional, for analytics)

- id (UUID)
- userId (UUID)
- categoryId (UUID)
- actionType (Enum)
- weight (Integer)
- sourceType (Enum: POST, PRODUCT, SHOP, EVENT, USER, SEARCH)
- sourceId (UUID)
- createdAt (LocalDateTime)

JWT Token Structure

Access Token Payload:

{
  "sub": "usr_550e8400e29b41d4",
  "tokenType": "ACCESS",
  "iat": 1736605200,
  "exp": 1736608800
}

Refresh Token Payload:

{
  "sub": "usr_550e8400e29b41d4",
  "tokenType": "REFRESH",
  "iat": 1736605200,
  "exp": 1768141200
}

Note: sub (subject) uses systemUsername, NOT userName. This allows username changes without token invalidation.


Username Change Flow (No Logout Required)

PUT /api/v1/profile/update-basic-info

Request:

{
  "userName": "newusername"
}

Response (Success):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Username updated successfully",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "systemUsername": "usr_550e8400e29b41d4",
    "userName": "newusername",
    "previousUserName": "oldusername",
    "note": "Your profile URL is now: app.com/@newusername"
  }
}

JWT token remains valid because it uses systemUsername which hasn't changed.


System Username Generation

Generated automatically at account creation:

public String generateSystemUsername(UUID userId) {
    // Take first 16 chars of UUID (without hyphens)
    String shortId = userId.toString().replace("-", "").substring(0, 16);
    return "usr_" + shortId;
}

// Example:
// UUID: 550e8400-e29b-41d4-a716-446655440000
// systemUsername: usr_550e8400e29b41d4

Rules:

  • Prefix: usr_
  • Length: 20 characters total
  • Characters: lowercase alphanumeric only
  • Unique: derived from UUID
  • Never displayed to users
  • Never changeable

DEVICE TRUST & LOGIN SECURITY

Overview

The system tracks user devices and applies a sliding trust window:

  • New device → OTP required before password login
  • Inactive device (30+ days) → OTP required (re-verification)
  • Trusted active device → Password login allowed
  • Suspicious activity → OTP required

Trust Sliding Window

Device Activity Timeline:
─────────────────────────────────────────────────────────────────►
                                                              NOW
     │                    │                    │
     ▼                    ▼                    ▼
  Day 0               Day 25               Day 35
  (Login)            (Last use)           (Login attempt)
     │                    │                    │
     │◄────── TRUSTED ────►│                   │
     │                    │◄─── 30 DAY GAP ───►│
     │                                         │
     │                              Device now UNTRUSTED
     │                              OTP required to re-trust

Login Decision Matrix

Device Status Last Activity Password Set? Action
New (unknown) Never Yes OTP first → then password → trust device
New (unknown) Never No OTP only → trust device
Known trusted < 30 days Yes Password only ✅
Known trusted < 30 days No Auto-login or OTP
Known trusted > 30 days Yes OTP first → then password → re-trust
Known trusted > 30 days No OTP → re-trust
Known untrusted Any Any OTP required
Any (suspicious) Any Any OTP required + security alert

Suspicious Activity Triggers

  • 3+ failed password attempts
  • Login from new country/region
  • Multiple devices logging in simultaneously
  • Unusual login time (if pattern established)
  • IP address on blocklist

LOGIN FLOWS (Updated with Device Trust)

Password Login - Full Flow

POST /api/v1/auth/login/password

Request:

{
  "identifier": "alexvibes",
  "password": "MySecurePass123!",
  "deviceInfo": {
    "deviceId": "abc123-device-fingerprint",
    "deviceName": "iPhone 15 Pro",
    "deviceType": "MOBILE_IOS",
    "appVersion": "1.2.0"
  }
}

Response (Success - Trusted Device):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Login successful",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "requiresOtp": false,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "profilePictureUrl": "https://storage.example.com/...",
      "onboardingComplete": true
    },
    "device": {
      "deviceId": "abc123-device-fingerprint",
      "deviceName": "iPhone 15 Pro",
      "trusted": true,
      "trustExpiresAt": "2025-02-10T16:00:00"
    }
  }
}

Response (New/Untrusted Device - OTP Required):

{
  "success": true,
  "httpStatus": "OK",
  "message": "New device detected. Please verify with OTP.",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "requiresOtp": true,
    "otpReason": "NEW_DEVICE",
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "otpSentTo": "+255*****678",
    "otpMethod": "SMS",
    "expiresAt": "2025-01-11T16:10:00",
    "device": {
      "deviceId": "xyz789-new-device",
      "deviceName": "Chrome on Windows",
      "isNew": true
    },
    "securityNote": "We detected a login from a new device. For your security, please verify with OTP."
  }
}

Response (Inactive Device - OTP Required):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Device inactive for 30+ days. Please verify with OTP.",
  "action_time": "2025-01-11T16:00:00",
  "data": {
    "requiresOtp": true,
    "otpReason": "INACTIVE_DEVICE",
    "tempToken": "eyJhbGciOiJIUzI1NiIs...",
    "otpSentTo": "a***@example.com",
    "otpMethod": "EMAIL",
    "expiresAt": "2025-01-11T16:10:00",
    "device": {
      "deviceId": "abc123-device-fingerprint",
      "deviceName": "iPhone 15 Pro",
      "lastActiveAt": "2024-12-01T10:00:00",
      "inactiveDays": 41
    },
    "securityNote": "This device hasn't been used in 41 days. Please verify it's you."
  }
}

Verify Device OTP (Step 2 for untrusted devices)

POST /api/v1/auth/login/verify-device

Request:

{
  "tempToken": "eyJhbGciOiJIUzI1NiIs...",
  "otpCode": "123456",
  "trustDevice": true,
  "deviceInfo": {
    "deviceId": "xyz789-new-device",
    "deviceName": "Chrome on Windows",
    "deviceType": "WEB_BROWSER"
  }
}

Response (Success):

{
  "success": true,
  "httpStatus": "OK",
  "message": "Device verified and trusted",
  "action_time": "2025-01-11T16:02:00",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "user": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "systemUsername": "usr_550e8400e29b41d4",
      "userName": "alexvibes",
      "displayName": "Alex Johnson",
      "profilePictureUrl": "https://storage.example.com/...",
      "onboardingComplete": true
    },
    "device": {
      "deviceId": "xyz789-new-device",
      "deviceName": "Chrome on Windows",
      "trusted": true,
      "trustExpiresAt": "2025-02-10T16:02:00"
    }
  }
}

Login Without Trusting Device (One-time access)

Request:

{
  "tempToken": "eyJhbGciOiJIUzI1NiIs...",
  "otpCode": "123456",
  "trustDevice": false
}

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Login successful (device not saved)",
  "action_time": "2025-01-11T16:02:00",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
    "tokenType": "Bearer",
    "expiresIn": 3600,
    "user": { ... },
    "device": {
      "trusted": false,
      "note": "This device will require OTP on next login"
    }
  }
}

DEVICE MANAGEMENT

Get My Devices

GET /api/v1/auth/devices

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Devices retrieved successfully",
  "action_time": "2025-01-11T18:00:00",
  "data": {
    "devices": [
      {
        "id": "device-uuid-1",
        "deviceId": "abc123-device-fingerprint",
        "deviceName": "iPhone 15 Pro",
        "deviceType": "MOBILE_IOS",
        "isTrusted": true,
        "isCurrentDevice": true,
        "lastActiveAt": "2025-01-11T18:00:00",
        "lastIpAddress": "196.41.xxx.xxx",
        "lastLocation": "Dar es Salaam, Tanzania",
        "createdAt": "2025-01-01T10:00:00"
      },
      {
        "id": "device-uuid-2",
        "deviceId": "xyz789-device-fingerprint",
        "deviceName": "Chrome on MacBook",
        "deviceType": "WEB_BROWSER",
        "isTrusted": true,
        "isCurrentDevice": false,
        "lastActiveAt": "2025-01-10T14:30:00",
        "lastIpAddress": "196.41.xxx.xxx",
        "lastLocation": "Dar es Salaam, Tanzania",
        "createdAt": "2024-12-15T08:00:00"
      },
      {
        "id": "device-uuid-3",
        "deviceId": "old-device-fingerprint",
        "deviceName": "Samsung Galaxy S21",
        "deviceType": "MOBILE_ANDROID",
        "isTrusted": false,
        "isCurrentDevice": false,
        "lastActiveAt": "2024-11-20T09:00:00",
        "lastIpAddress": "41.59.xxx.xxx",
        "lastLocation": "Arusha, Tanzania",
        "createdAt": "2024-06-01T12:00:00",
        "untrustedReason": "Inactive for 52 days"
      }
    ],
    "totalDevices": 3,
    "trustedDevices": 2
  }
}

Remove/Logout Device

DELETE /api/v1/auth/devices/{deviceId}

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Device removed successfully",
  "action_time": "2025-01-11T18:05:00",
  "data": {
    "removedDeviceId": "device-uuid-3",
    "removedDeviceName": "Samsung Galaxy S21",
    "note": "This device has been logged out and will require OTP to login again"
  }
}

Logout All Other Devices

POST /api/v1/auth/devices/logout-all

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Request:

{
  "password": "MySecurePass123!",
  "keepCurrentDevice": true
}

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "All other devices logged out",
  "action_time": "2025-01-11T18:10:00",
  "data": {
    "devicesLoggedOut": 2,
    "currentDeviceKept": true,
    "note": "All other devices will require OTP to login again"
  }
}

Rename Device

PUT /api/v1/auth/devices/{deviceId}

Request:

{
  "deviceName": "My Work Laptop"
}

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Device renamed successfully",
  "action_time": "2025-01-11T18:15:00",
  "data": {
    "deviceId": "device-uuid-2",
    "deviceName": "My Work Laptop",
    "previousName": "Chrome on MacBook"
  }
}

LOGIN EVENTS/HISTORY

Get Login History

GET /api/v1/auth/login-history?page=1&size=20

Headers:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Login history retrieved",
  "action_time": "2025-01-11T18:20:00",
  "data": {
    "events": [
      {
        "id": "event-uuid-1",
        "loginMethod": "PASSWORD",
        "deviceName": "iPhone 15 Pro",
        "deviceType": "MOBILE_IOS",
        "ipAddress": "196.41.xxx.xxx",
        "location": "Dar es Salaam, Tanzania",
        "status": "SUCCESS",
        "requiresOtp": false,
        "createdAt": "2025-01-11T16:00:00"
      },
      {
        "id": "event-uuid-2",
        "loginMethod": "OTP",
        "deviceName": "Chrome on Windows",
        "deviceType": "WEB_BROWSER",
        "ipAddress": "196.41.xxx.xxx",
        "location": "Dar es Salaam, Tanzania",
        "status": "SUCCESS",
        "requiresOtp": true,
        "otpReason": "NEW_DEVICE",
        "createdAt": "2025-01-11T14:30:00"
      },
      {
        "id": "event-uuid-3",
        "loginMethod": "PASSWORD",
        "deviceName": "Unknown Device",
        "deviceType": "WEB_BROWSER",
        "ipAddress": "102.89.xxx.xxx",
        "location": "Lagos, Nigeria",
        "status": "FAILED_PASSWORD",
        "requiresOtp": false,
        "createdAt": "2025-01-10T23:45:00",
        "securityAlert": true
      }
    ],
    "pagination": {
      "page": 1,
      "size": 20,
      "totalElements": 45,
      "totalPages": 3
    },
    "securitySummary": {
      "totalLogins30Days": 28,
      "failedAttempts30Days": 2,
      "uniqueDevices30Days": 3,
      "uniqueLocations30Days": 1
    }
  }
}

SECURITY ALERTS

Get Security Alerts

GET /api/v1/auth/security-alerts

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Security alerts retrieved",
  "action_time": "2025-01-11T18:25:00",
  "data": {
    "alerts": [
      {
        "id": "alert-uuid-1",
        "type": "SUSPICIOUS_LOGIN",
        "severity": "HIGH",
        "title": "Login attempt from new location",
        "description": "Someone tried to login from Lagos, Nigeria",
        "deviceName": "Unknown Device",
        "ipAddress": "102.89.xxx.xxx",
        "location": "Lagos, Nigeria",
        "status": "UNREAD",
        "actionTaken": "BLOCKED",
        "createdAt": "2025-01-10T23:45:00",
        "actions": [
          { "action": "DISMISS", "label": "This was me" },
          { "action": "SECURE_ACCOUNT", "label": "Secure my account" }
        ]
      },
      {
        "id": "alert-uuid-2",
        "type": "NEW_DEVICE_LOGIN",
        "severity": "MEDIUM",
        "title": "New device added",
        "description": "Chrome on Windows was added to your account",
        "deviceName": "Chrome on Windows",
        "location": "Dar es Salaam, Tanzania",
        "status": "READ",
        "actionTaken": null,
        "createdAt": "2025-01-11T14:30:00",
        "actions": [
          { "action": "DISMISS", "label": "This was me" },
          { "action": "REMOVE_DEVICE", "label": "Remove this device" }
        ]
      }
    ],
    "unreadCount": 1,
    "totalAlerts": 2
  }
}

Dismiss Security Alert

POST /api/v1/auth/security-alerts/{alertId}/dismiss

Request:

{
  "action": "DISMISS",
  "feedback": "This was me logging in from a friend's device"
}

Response:

{
  "success": true,
  "httpStatus": "OK",
  "message": "Alert dismissed",
  "action_time": "2025-01-11T18:30:00",
  "data": {
    "alertId": "alert-uuid-1",
    "status": "DISMISSED",
    "unreadCount": 0
  }
}

New Database Entities for Device Trust

UserDevice

- id (UUID)
- userId (UUID) FK → AccountEntity
- deviceId (String) - fingerprint from client (unique per user)
- deviceName (String) - "iPhone 15 Pro", "Chrome on Windows"
- deviceType (Enum) - MOBILE_IOS, MOBILE_ANDROID, WEB_BROWSER, DESKTOP_APP
- userAgent (String)
- lastIpAddress (String)
- lastLocation (String)
- isTrusted (Boolean)
- trustExpiresAt (LocalDateTime) - 30 days sliding window
- lastActiveAt (LocalDateTime)
- createdAt (LocalDateTime)
- updatedAt (LocalDateTime)

Indexes:
- (userId, deviceId) UNIQUE
- (userId, isTrusted)
- (lastActiveAt)

LoginEvent

- id (UUID)
- userId (UUID) FK → AccountEntity
- deviceId (UUID) FK → UserDevice (nullable for failed attempts)
- loginMethod (Enum) - PASSWORD, OTP_SMS, OTP_EMAIL, GOOGLE, APPLE
- ipAddress (String)
- location (String)
- status (Enum) - SUCCESS, FAILED_PASSWORD, FAILED_OTP, BLOCKED, REQUIRES_OTP
- requiresOtp (Boolean)
- otpReason (Enum) - NEW_DEVICE, INACTIVE_DEVICE, SUSPICIOUS, MANUAL
- createdAt (LocalDateTime)

Indexes:
- (userId, createdAt)
- (userId, status)
- (ipAddress, createdAt) - for rate limiting

SecurityAlert

- id (UUID)
- userId (UUID) FK → AccountEntity
- loginEventId (UUID) FK → LoginEvent (nullable)
- type (Enum) - SUSPICIOUS_LOGIN, NEW_DEVICE_LOGIN, FAILED_ATTEMPTS, PASSWORD_CHANGED, etc.
- severity (Enum) - LOW, MEDIUM, HIGH, CRITICAL
- title (String)
- description (String)
- deviceName (String)
- ipAddress (String)
- location (String)
- status (Enum) - UNREAD, READ, DISMISSED, ACTIONED
- actionTaken (String)
- createdAt (LocalDateTime)
- readAt (LocalDateTime)
- dismissedAt (LocalDateTime)

Indexes:
- (userId, status)
- (userId, createdAt)

Device Trust Configuration

# application.yml
device:
  trust:
    enabled: true
    window-days: 30                    # Trust window duration
    max-devices-per-user: 10           # Max trusted devices
    auto-cleanup-days: 90              # Remove inactive devices after
    
login:
  security:
    max-failed-attempts: 5             # Before temporary lock
    lock-duration-minutes: 30          # Temporary lock duration
    suspicious-countries: [XX, YY]     # Countries requiring extra verification
    alert-on-new-country: true         # Send alert on new country login