New Authentication & Onboarding API
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": { ... }
}
System Username vs Display Username Architecture
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:
systemUsernameis auto-generated at signup (e.g.,usr_550e8400e29b41d4)userNameis user-chosen during onboarding (e.g.,alexvibes)- JWT tokens contain
systemUsername→ user can changeuserNamewithout 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 |
Link Account Flow
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..."
}
}
Confirm Account Link (with Password)
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"
}
}
}
Confirm Account Link (with OTP)
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"
}
Auto-Link (Unverified Email)
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"
}
}
Unlink Provider
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"
}
}
Link New Provider
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/googlePOST /api/v1/auth/signup/applePUT /api/v1/onboarding/name-birthdateGET /api/v1/onboarding/username/checkPOST /api/v1/onboarding/profile-picturePUT /api/v1/onboarding/profile-setupGET /api/v1/onboarding/interests/categoriesPOST /api/v1/onboarding/interestsPOST /api/v1/onboarding/interests/skipGET /api/v1/onboarding/completePOST /api/v1/auth/login/passwordPOST /api/v1/auth/login/otp/requestPOST /api/v1/auth/login/otp/verifyPOST /api/v1/auth/password/setGET /api/v1/auth/meGET /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:
- Explicit: User picks during onboarding (visible to user)
- 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:
- Gets the category tags from that content
- Calculates score based on action type
- Updates user's implicit interest scores silently
- 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