Spaces:
Sleeping
Sleeping
| # 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 <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:** | |
| ```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 <token>` | |
| **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 <token>` | |
| **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 <token>` | |
| **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 <token>` | |
| **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 ( | |
| <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 | |
| ```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 | |