Spaces:
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
- Flow Diagrams
- API Endpoints Reference
- Data Models
- Error Handling
- Frontend Integration Examples
- 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:
{
"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 organizationclient_admin: Can only invite to their clientcontractor_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
Create Environment Variables:
base_url:https://your-api-domain.com/api/v1access_token: (will be set after login)
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);
Authenticated Requests:
- Add header:
Authorization: Bearer {{access_token}}
- Add header:
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
Token Storage:
// β Good: localStorage for web apps localStorage.setItem('token', accessToken); // β Better: Secure storage for mobile SecureStore.setItemAsync('token', accessToken);Token Validation:
// Always validate token on app startup if (hasToken()) { const result = await getCurrentUser(); if (!result.success) { clearToken(); // Invalid token redirectToLogin(); } }Logout Handling:
// Always clear token on logout async function logout() { try { await api.post('/auth/logout'); // Audit } finally { localStorage.removeItem('token'); // Always clear redirectToLogin(); } }Error Handling:
// Global error handler axios.interceptors.response.use( response => response, error => { if (error.response?.status === 401) { clearToken(); redirectToLogin(); } return Promise.reject(error); } );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:
- 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