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](#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