# 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](#authentication-overview) - [Flow Diagrams](#flow-diagrams) - [API Endpoints Reference](#api-endpoints-reference) - [Data Models](#data-models) - [Error Handling](#error-handling) - [Frontend Integration Examples](#frontend-integration-examples) - [Testing Guide](#testing-guide) --- ## 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 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:** ```json { "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):** ```json { "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:** ```json // 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):** ```json { "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:** ```json // 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:** ```json { "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):** ```json { "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:** ```json { "token": "unique-invitation-token", "password": "SecurePass123!", "name": "Jane Smith", "phone": "+254712345678", "accept_terms": true } ``` **Success Response (201 Created):** ```json { "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:** ```json // 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:** ```json { "token": "unique-invitation-token" } ``` **Success Response (200 OK):** ```json { "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:** ```json { "email": "user@example.com", "password": "SecurePass123!" } ``` **Success Response (200 OK):** ```json { "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:** ```json // 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 ` **Success Response (200 OK):** ```json { "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:** ```json // 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 ` **Request:** ```json { "first_name": "Jane", "last_name": "Doe", "phone": "+254798765432" } ``` **Success Response (200 OK):** ```json { "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:** ```json // 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 ` **Success Response (200 OK):** ```json { "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 ` **Request:** ```json { "current_password": "OldPass123!", "new_password": "NewSecurePass456!" } ``` **Password Requirements:** - Min 8 characters - At least 1 uppercase letter - At least 1 digit **Success Response (200 OK):** ```json { "message": "Password changed successfully. Please login again with your new password." } ``` **Error Responses:** ```json // 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:** ```json { "email": "user@example.com" } ``` **Success Response (200 OK):** ```json { "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:** ```json { "token": "reset-token-from-email", "new_password": "NewSecurePass456!" } ``` **Success Response (200 OK):** ```json { "message": "Password reset successfully. You can now login with your new password." } ``` **Error Responses:** ```json // 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 ```typescript 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 ```typescript 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 ```json { "detail": "Human-readable error message" } ``` --- ## Frontend Integration Examples ### React/JavaScript Implementation ```javascript // 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 ```javascript // 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 ```javascript // 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 (

Login to SwiftOps

{error && (
{error}
)}
setEmail(e.target.value)} required disabled={loading} />
setPassword(e.target.value)} required disabled={loading} />
Forgot password?
); } ``` --- ## Testing Guide ### Using cURL ```bash # 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): ```json { "email": "user@example.com", "password": "SecurePass123!" } ``` - Tests (save token): ```javascript 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:** ```javascript // ✅ Good: localStorage for web apps localStorage.setItem('token', accessToken); // ✅ Better: Secure storage for mobile SecureStore.setItemAsync('token', accessToken); ``` 2. **Token Validation:** ```javascript // Always validate token on app startup if (hasToken()) { const result = await getCurrentUser(); if (!result.success) { clearToken(); // Invalid token redirectToLogin(); } } ``` 3. **Logout Handling:** ```javascript // 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:** ```javascript // Global error handler axios.interceptors.response.use( response => response, error => { if (error.response?.status === 401) { clearToken(); redirectToLogin(); } return Promise.reject(error); } ); ``` 5. **Password Validation:** ```javascript 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: - **Backend Team:** backend@swiftops.com - **Documentation:** https://docs.swiftops.com - **GitHub Issues:** https://github.com/yourorg/swiftops-backend/issues --- **Last Updated:** November 17, 2025 **API Version:** 1.0 **Maintained By:** SwiftOps Backend Team