swiftops-backend / docs /api /auth /AUTH_API_COMPLETE.md
kamau1's picture
feat(auth): fix platform admin registration flow and update OTP/registration schemas
ab3ba46

Authentication API - Complete Frontend Developer Guide

API Base URL: https://your-api-domain.com/api/v1
Last Updated: November 17, 2025
Version: 1.0


πŸ“‹ Quick Navigation


Authentication Overview

Three User Creation Methods

Method Use Case Frequency Authentication Required
Platform Admin Bootstrap First system admin setup Once (system setup) ❌ No (Public)
Invitation-Based All regular users 95% of users βœ… Admin creates invitation
Login Existing user access Daily use ❌ No (Public)

User Roles in System

Role Description Organization
platform_admin Full system access None
client_admin Telecom operator admin Client
contractor_admin Field service company admin Contractor
sales_manager Sales team manager Client/Contractor
project_manager Project coordinator Client/Contractor
dispatcher Operations coordinator Contractor
field_agent Field technician Contractor
sales_agent Sales representative Client/Contractor

Flow Diagrams

Complete Authentication Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    AUTHENTICATION FLOWS                          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ” FLOW 1: PLATFORM ADMIN BOOTSTRAP (First Admin Only)
─────────────────────────────────────────────────────────
User Action                     Backend Action
───────────────────────────────────────────────────────────────────
1. POST /auth/register
   {email, password,         β†’  Validate data
    first_name, last_name}   β†’  Send OTP to admin email
                             ←  {message: "OTP sent"}

2. Check email for OTP
   (6-digit code)

3. POST /auth/complete-registration
   {email, otp_code}         β†’  Verify OTP
                             β†’  Create Supabase Auth user
                             β†’  Create local user profile
                             ←  {access_token, user}

4. Store token & redirect to dashboard


πŸ‘₯ FLOW 2: INVITATION-BASED (Primary Method - 95% of Users)
─────────────────────────────────────────────────────────
Admin Action                    User Action
───────────────────────────────────────────────────────────────────
1. POST /invitations
   {email, role, org}        β†’  Create invitation
                             β†’  Send WhatsApp/Email
                             ←  {invitation details}

                                2. User receives message
                                   with invitation link

                                3. POST /invitations/accept
                                   {token, password, name}
                             β†’  Validate token
                             β†’  Create Supabase Auth user
                             β†’  Create local user profile
                             ←  {access_token, user}

                                4. Store token & redirect


πŸ”‘ FLOW 3: STANDARD LOGIN (Daily Use)
─────────────────────────────────────
User Action                     Backend Action
───────────────────────────────────────────────────────────────────
1. POST /auth/login
   {email, password}         β†’  Verify credentials
                             β†’  Check is_active status
                             ←  {access_token, user}

2. Store token in localStorage

3. All subsequent requests:
   Authorization: Bearer <token>

4. GET /auth/me              β†’  Validate token
                             β†’  Return user profile
                             ←  {user details}

API Endpoints Reference

1. Platform Admin Bootstrap

1.1 Register Platform Admin (Step 1)

Endpoint: POST /api/v1/auth/register
Access: Public
Rate Limit: 3 requests/hour per IP

Request:

{
  "email": "admin@example.com",
  "password": "SecurePass123!",
  "first_name": "John",
  "last_name": "Doe",
  "phone": "+254712345678"
}

Validations:

  • Email: Valid email format
  • Password: Min 8 chars, 1 uppercase, 1 digit
  • First/Last Name: Min 1 character
  • Phone: Optional, max 50 characters

Success Response (200 OK):

{
  "message": "Registration request received. OTP verification code sent to admin@configured-email.com. Please check your email and verify using /auth/complete-registration endpoint with your email and the OTP code."
}

Error Responses:

// 400 - Email already exists
{
  "detail": "Email already registered"
}

// 400 - Validation error
{
  "detail": "Password must contain at least one digit"
}

1.2 Complete Registration (Step 2)

Endpoint: POST /api/v1/auth/complete-registration?email={email}&otp_code={code}
Access: Public
Rate Limit: 5 requests/hour per IP

Request:

POST /api/v1/auth/complete-registration?email=admin@example.com&otp_code=123456

Success Response (201 Created):

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer",
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "admin@example.com",
    "first_name": "John",
    "last_name": "Doe",
    "full_name": "John Doe",
    "is_active": true
  }
}

Error Responses:

// 400 - Invalid OTP
{
  "detail": "Invalid or expired OTP. 2 attempts remaining."
}

// 400 - Already registered
{
  "detail": "Email already registered"
}

// 400 - Registration data not found
{
  "detail": "Registration data not found or expired. Please start registration process again."
}

2. Invitation-Based User Creation

2.1 Create Invitation (Admin Only)

Endpoint: POST /api/v1/invitations
Access: Authenticated (admin roles)
Required Permission: invite_users

Request:

{
  "email": "user@example.com",
  "phone": "+254712345678",
  "invited_role": "field_agent",
  "client_id": "uuid",
  "contractor_id": null,
  "invitation_method": "whatsapp"
}

Authorization Rules:

  • platform_admin: Can invite to any organization
  • client_admin: Can only invite to their client
  • contractor_admin: Can only invite to their contractor

Success Response (201 Created):

{
  "id": "uuid",
  "email": "user@example.com",
  "phone": "+254712345678",
  "invited_role": "field_agent",
  "client_id": "uuid",
  "contractor_id": null,
  "token": "unique-invitation-token",
  "status": "pending",
  "invitation_method": "whatsapp",
  "invited_at": "2025-11-17T10:00:00Z",
  "expires_at": "2025-11-24T10:00:00Z",
  "whatsapp_sent": true,
  "whatsapp_sent_at": "2025-11-17T10:00:05Z"
}

2.2 Accept Invitation (Public)

Endpoint: POST /api/v1/invitations/accept
Access: Public
Rate Limit: 10 requests/hour per IP

Request:

{
  "token": "unique-invitation-token",
  "password": "SecurePass123!",
  "name": "Jane Smith",
  "phone": "+254712345678",
  "accept_terms": true
}

Success Response (201 Created):

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer",
  "user": {
    "id": "uuid",
    "email": "user@example.com",
    "first_name": "Jane",
    "last_name": "Smith",
    "full_name": "Jane Smith",
    "is_active": true,
    "role": "field_agent",
    "status": "active"
  }
}

Error Responses:

// 400 - Invalid token
{
  "detail": "Invalid or expired invitation token"
}

// 400 - Already accepted
{
  "detail": "Invitation has already been accepted"
}

// 400 - Weak password
{
  "detail": "Password must contain at least one uppercase letter"
}

2.3 Validate Invitation (Check Before Accept)

Endpoint: POST /api/v1/invitations/validate
Access: Public

Request:

{
  "token": "unique-invitation-token"
}

Success Response (200 OK):

{
  "email": "user@example.com",
  "invited_role": "field_agent",
  "organization_name": "TechInstall Ltd",
  "invited_at": "2025-11-17T10:00:00Z",
  "expires_at": "2025-11-24T10:00:00Z",
  "is_expired": false,
  "is_valid": true
}

3. Standard Login

3.1 Login

Endpoint: POST /api/v1/auth/login
Access: Public
Rate Limit: 10 requests/minute per IP

Request:

{
  "email": "user@example.com",
  "password": "SecurePass123!"
}

Success Response (200 OK):

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer",
  "user": {
    "id": "uuid",
    "email": "user@example.com",
    "first_name": "Jane",
    "last_name": "Smith",
    "full_name": "Jane Smith",
    "is_active": true
  }
}

Error Responses:

// 401 - Invalid credentials
{
  "detail": "Incorrect email or password"
}

// 403 - Account inactive
{
  "detail": "Account is inactive. Please contact support."
}

// 404 - User not found
{
  "detail": "User profile not found"
}

3.2 Get Current User Profile

Endpoint: GET /api/v1/auth/me
Access: Authenticated
Headers: Authorization: Bearer <token>

Success Response (200 OK):

{
  "id": "uuid",
  "email": "user@example.com",
  "name": "Jane Smith",
  "phone": "+254712345678",
  "phone_alternate": null,
  "role": "field_agent",
  "status": "active",
  "is_active": true,
  "client_id": null,
  "contractor_id": "uuid",
  "display_name": "Jane S.",
  "created_at": "2025-11-17T10:00:00Z",
  "updated_at": "2025-11-17T10:00:00Z"
}

Error Responses:

// 401 - Invalid/expired token
{
  "detail": "Could not validate credentials"
}

// 403 - Inactive user
{
  "detail": "Inactive user"
}

3.3 Update Profile

Endpoint: PUT /api/v1/auth/me
Access: Authenticated
Headers: Authorization: Bearer <token>

Request:

{
  "first_name": "Jane",
  "last_name": "Doe",
  "phone": "+254798765432"
}

Success Response (200 OK):

{
  "id": "uuid",
  "email": "user@example.com",
  "name": "Jane Doe",
  "phone": "+254798765432",
  "role": "field_agent",
  "status": "active",
  "is_active": true,
  "updated_at": "2025-11-17T11:00:00Z"
}

Error Responses:

// 400 - Phone already in use
{
  "detail": "Phone number already in use"
}

3.4 Logout

Endpoint: POST /api/v1/auth/logout
Access: Authenticated
Headers: Authorization: Bearer <token>

Success Response (200 OK):

{
  "message": "Logged out successfully"
}

Note: JWT tokens can't be invalidated server-side. This endpoint is for audit logging. Client must delete the token.


4. Password Management

4.1 Change Password (Logged In)

Endpoint: POST /api/v1/auth/change-password
Access: Authenticated
Rate Limit: 5 requests/hour per user
Headers: Authorization: Bearer <token>

Request:

{
  "current_password": "OldPass123!",
  "new_password": "NewSecurePass456!"
}

Password Requirements:

  • Min 8 characters
  • At least 1 uppercase letter
  • At least 1 digit

Success Response (200 OK):

{
  "message": "Password changed successfully. Please login again with your new password."
}

Error Responses:

// 400 - Incorrect current password
{
  "detail": "Current password is incorrect or password change failed"
}

4.2 Forgot Password (Request Reset)

Endpoint: POST /api/v1/auth/forgot-password
Access: Public
Rate Limit: 3 requests/hour per IP

Request:

{
  "email": "user@example.com"
}

Success Response (200 OK):

{
  "message": "If an account with this email exists, a password reset link has been sent."
}

Note: Always returns success to prevent email enumeration attacks.


4.3 Reset Password (With Token)

Endpoint: POST /api/v1/auth/reset-password
Access: Public
Rate Limit: 5 requests/hour per IP

Request:

{
  "token": "reset-token-from-email",
  "new_password": "NewSecurePass456!"
}

Success Response (200 OK):

{
  "message": "Password reset successfully. You can now login with your new password."
}

Error Responses:

// 400 - Invalid/expired token
{
  "detail": "Invalid or expired password reset token"
}

// 400 - Weak password
{
  "detail": "Password must contain at least one digit"
}

Data Models

TokenResponse

interface TokenResponse {
  access_token: string;
  token_type: "bearer";
  user: {
    id: string;
    email: string;
    first_name: string;
    last_name: string;
    full_name: string;
    is_active: boolean;
  };
}

UserProfile

interface UserProfile {
  id: string;
  email: string;
  name: string;
  phone: string | null;
  phone_alternate: string | null;
  role: UserRole;
  status: UserStatus;
  is_active: boolean;
  client_id: string | null;
  contractor_id: string | null;
  display_name: string | null;
  created_at: string; // ISO 8601
  updated_at: string; // ISO 8601
}

type UserRole = 
  | "platform_admin"
  | "client_admin"
  | "contractor_admin"
  | "sales_manager"
  | "project_manager"
  | "dispatcher"
  | "field_agent"
  | "sales_agent";

type UserStatus = 
  | "invited"
  | "pending_setup"
  | "active"
  | "suspended";

Error Handling

HTTP Status Codes

Code Meaning When It Happens
200 OK Request successful
201 Created User/resource created successfully
400 Bad Request Validation error, invalid data
401 Unauthorized Invalid/missing/expired token
403 Forbidden Account inactive or insufficient permissions
404 Not Found User/resource doesn't exist
429 Too Many Requests Rate limit exceeded
500 Internal Server Error Server error (contact support)

Error Response Format

{
  "detail": "Human-readable error message"
}

Frontend Integration Examples

React/JavaScript Implementation

// auth.service.js
class AuthService {
  constructor() {
    this.baseURL = 'https://your-api-domain.com/api/v1';
    this.tokenKey = 'swiftops_token';
  }

  // Store token in localStorage
  setToken(token) {
    localStorage.setItem(this.tokenKey, token);
  }

  // Get token from localStorage
  getToken() {
    return localStorage.getItem(this.tokenKey);
  }

  // Remove token (logout)
  clearToken() {
    localStorage.removeItem(this.tokenKey);
  }

  // Get auth headers
  getAuthHeaders() {
    const token = this.getToken();
    return {
      'Content-Type': 'application/json',
      ...(token && { 'Authorization': `Bearer ${token}` })
    };
  }

  // 1. Login
  async login(email, password) {
    try {
      const response = await fetch(`${this.baseURL}/auth/login`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password })
      });

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.detail || 'Login failed');
      }

      const data = await response.json();
      this.setToken(data.access_token);
      
      return {
        success: true,
        user: data.user,
        token: data.access_token
      };
    } catch (error) {
      return {
        success: false,
        error: error.message
      };
    }
  }

  // 2. Get current user
  async getCurrentUser() {
    try {
      const response = await fetch(`${this.baseURL}/auth/me`, {
        method: 'GET',
        headers: this.getAuthHeaders()
      });

      if (!response.ok) {
        if (response.status === 401) {
          this.clearToken();
          throw new Error('Session expired. Please login again.');
        }
        throw new Error('Failed to fetch user profile');
      }

      const user = await response.json();
      return {
        success: true,
        user
      };
    } catch (error) {
      return {
        success: false,
        error: error.message
      };
    }
  }

  // 3. Update profile
  async updateProfile(updates) {
    try {
      const response = await fetch(`${this.baseURL}/auth/me`, {
        method: 'PUT',
        headers: this.getAuthHeaders(),
        body: JSON.stringify(updates)
      });

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.detail || 'Update failed');
      }

      const user = await response.json();
      return {
        success: true,
        user
      };
    } catch (error) {
      return {
        success: false,
        error: error.message
      };
    }
  }

  // 4. Change password
  async changePassword(currentPassword, newPassword) {
    try {
      const response = await fetch(`${this.baseURL}/auth/change-password`, {
        method: 'POST',
        headers: this.getAuthHeaders(),
        body: JSON.stringify({
          current_password: currentPassword,
          new_password: newPassword
        })
      });

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.detail || 'Password change failed');
      }

      const data = await response.json();
      
      // Force logout after password change
      this.clearToken();
      
      return {
        success: true,
        message: data.message
      };
    } catch (error) {
      return {
        success: false,
        error: error.message
      };
    }
  }

  // 5. Forgot password
  async forgotPassword(email) {
    try {
      const response = await fetch(`${this.baseURL}/auth/forgot-password`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email })
      });

      const data = await response.json();
      
      return {
        success: true,
        message: data.message
      };
    } catch (error) {
      return {
        success: false,
        error: error.message
      };
    }
  }

  // 6. Reset password
  async resetPassword(token, newPassword) {
    try {
      const response = await fetch(`${this.baseURL}/auth/reset-password`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          token,
          new_password: newPassword
        })
      });

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.detail || 'Password reset failed');
      }

      const data = await response.json();
      return {
        success: true,
        message: data.message
      };
    } catch (error) {
      return {
        success: false,
        error: error.message
      };
    }
  }

  // 7. Accept invitation
  async acceptInvitation(token, password, name, phone) {
    try {
      const response = await fetch(`${this.baseURL}/invitations/accept`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          token,
          password,
          name,
          phone,
          accept_terms: true
        })
      });

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.detail || 'Invitation acceptance failed');
      }

      const data = await response.json();
      this.setToken(data.access_token);
      
      return {
        success: true,
        user: data.user,
        token: data.access_token
      };
    } catch (error) {
      return {
        success: false,
        error: error.message
      };
    }
  }

  // 8. Logout
  async logout() {
    try {
      // Call logout endpoint for audit
      await fetch(`${this.baseURL}/auth/logout`, {
        method: 'POST',
        headers: this.getAuthHeaders()
      });
    } catch (error) {
      console.error('Logout audit failed:', error);
    } finally {
      // Always clear token
      this.clearToken();
    }
  }

  // Check if user is authenticated
  isAuthenticated() {
    return !!this.getToken();
  }
}

// Export singleton instance
export default new AuthService();

React Hook Example

// useAuth.js
import { useState, useEffect } from 'react';
import AuthService from './auth.service';

export function useAuth() {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    loadUser();
  }, []);

  const loadUser = async () => {
    if (!AuthService.isAuthenticated()) {
      setLoading(false);
      return;
    }

    const result = await AuthService.getCurrentUser();
    if (result.success) {
      setUser(result.user);
    } else {
      setError(result.error);
      AuthService.clearToken();
    }
    setLoading(false);
  };

  const login = async (email, password) => {
    setError(null);
    const result = await AuthService.login(email, password);
    
    if (result.success) {
      setUser(result.user);
    } else {
      setError(result.error);
    }
    
    return result;
  };

  const logout = async () => {
    await AuthService.logout();
    setUser(null);
  };

  const updateProfile = async (updates) => {
    setError(null);
    const result = await AuthService.updateProfile(updates);
    
    if (result.success) {
      setUser(result.user);
    } else {
      setError(result.error);
    }
    
    return result;
  };

  return {
    user,
    loading,
    error,
    login,
    logout,
    updateProfile,
    isAuthenticated: AuthService.isAuthenticated(),
    reload: loadUser
  };
}

Usage in Components

// LoginPage.jsx
import React, { useState } from 'react';
import { useAuth } from './useAuth';
import { useNavigate } from 'react-router-dom';

function LoginPage() {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);
  
  const { login } = useAuth();
  const navigate = useNavigate();

  const handleSubmit = async (e) => {
    e.preventDefault();
    setError('');
    setLoading(true);

    const result = await login(email, password);
    
    setLoading(false);
    
    if (result.success) {
      navigate('/dashboard');
    } else {
      setError(result.error);
    }
  };

  return (
    <div className="login-page">
      <h1>Login to SwiftOps</h1>
      
      {error && (
        <div className="alert alert-error">{error}</div>
      )}
      
      <form onSubmit={handleSubmit}>
        <div className="form-group">
          <label>Email</label>
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            required
            disabled={loading}
          />
        </div>
        
        <div className="form-group">
          <label>Password</label>
          <input
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            required
            disabled={loading}
          />
        </div>
        
        <button type="submit" disabled={loading}>
          {loading ? 'Logging in...' : 'Login'}
        </button>
      </form>
      
      <a href="/forgot-password">Forgot password?</a>
    </div>
  );
}

Testing Guide

Using cURL

# 1. Login
curl -X POST https://your-api-domain.com/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "password": "SecurePass123!"
  }'

# Save the access_token from response

# 2. Get current user
curl -X GET https://your-api-domain.com/api/v1/auth/me \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

# 3. Update profile
curl -X PUT https://your-api-domain.com/api/v1/auth/me \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "first_name": "Jane",
    "phone": "+254712345678"
  }'

# 4. Forgot password
curl -X POST https://your-api-domain.com/api/v1/auth/forgot-password \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com"
  }'

Using Postman

  1. Create Environment Variables:

    • base_url: https://your-api-domain.com/api/v1
    • access_token: (will be set after login)
  2. Login Request:

    • Method: POST
    • URL: {{base_url}}/auth/login
    • Body (JSON):
      {
        "email": "user@example.com",
        "password": "SecurePass123!"
      }
      
    • Tests (save token):
      const response = pm.response.json();
      pm.environment.set("access_token", response.access_token);
      
  3. Authenticated Requests:

    • Add header: Authorization: Bearer {{access_token}}

Common Scenarios

Scenario 1: First-Time Setup

1. POST /auth/register (admin details)
2. Check admin email for OTP
3. POST /auth/complete-registration (email + OTP)
4. Store token, redirect to dashboard
5. Platform admin is now logged in

Scenario 2: Inviting a Field Agent

1. Admin logged in, navigates to "Invite User"
2. Admin: POST /invitations (field agent details)
3. Field agent receives WhatsApp message with link
4. Agent clicks link, opens invitation page
5. Agent: POST /invitations/accept (token + password + details)
6. Agent is logged in, redirected to dashboard

Scenario 3: Password Reset

1. User clicks "Forgot Password"
2. POST /auth/forgot-password (email)
3. User receives email with reset link
4. User clicks link, opens reset page
5. POST /auth/reset-password (token + new password)
6. User redirected to login
7. POST /auth/login (email + new password)

Scenario 4: Daily Login

1. User opens app
2. Check localStorage for token
3. If token exists: GET /auth/me
   - Success: Show dashboard
   - Fail (401): Clear token, show login
4. If no token: Show login page
5. POST /auth/login
6. Store token, redirect to dashboard

Security Best Practices

Frontend Security

  1. Token Storage:

    // βœ… Good: localStorage for web apps
    localStorage.setItem('token', accessToken);
    
    // βœ… Better: Secure storage for mobile
    SecureStore.setItemAsync('token', accessToken);
    
  2. Token Validation:

    // Always validate token on app startup
    if (hasToken()) {
      const result = await getCurrentUser();
      if (!result.success) {
        clearToken(); // Invalid token
        redirectToLogin();
      }
    }
    
  3. Logout Handling:

    // Always clear token on logout
    async function logout() {
      try {
        await api.post('/auth/logout'); // Audit
      } finally {
        localStorage.removeItem('token'); // Always clear
        redirectToLogin();
      }
    }
    
  4. Error Handling:

    // Global error handler
    axios.interceptors.response.use(
      response => response,
      error => {
        if (error.response?.status === 401) {
          clearToken();
          redirectToLogin();
        }
        return Promise.reject(error);
      }
    );
    
  5. Password Validation:

    function validatePassword(password) {
      if (password.length < 8) {
        return 'Password must be at least 8 characters';
      }
      if (!/\d/.test(password)) {
        return 'Password must contain at least one digit';
      }
      if (!/[A-Z]/.test(password)) {
        return 'Password must contain at least one uppercase letter';
      }
      return null; // Valid
    }
    

FAQ

Q: How long do access tokens last?

A: 24 hours (1440 minutes). After expiry, user must login again.

Q: Can I refresh tokens?

A: Currently, no. When token expires, user must re-login. This is a security feature.

Q: What happens when I call logout?

A: The server logs the action for audit. You must delete the token client-side.

Q: Can I register regular users via /auth/register?

A: No. Only platform admins during bootstrap. All other users must use invitation flow.

Q: How do I know if a user is a platform admin vs field agent?

A: Check the role field in the user object after login/get-profile.

Q: What if invitation token expires?

A: Admin must create a new invitation. Old token cannot be renewed.

Q: Can users change their email?

A: No. Email is the unique identifier. Contact platform admin for email changes.

Q: What if wrong OTP is entered during registration?

A: User has limited attempts. After that, must restart registration process.


Support

For API issues, contact:


Last Updated: November 17, 2025
API Version: 1.0
Maintained By: SwiftOps Backend Team