2026-01-06 15:33:03 +00:00
|
|
|
// API Client for Gatehouse Backend
|
2026-01-07 14:29:40 +00:00
|
|
|
// Uses Bearer token authentication
|
2026-01-06 15:33:03 +00:00
|
|
|
|
2026-01-06 15:42:41 +00:00
|
|
|
import { config } from '@/config';
|
2026-01-06 15:33:03 +00:00
|
|
|
|
|
|
|
|
interface ApiResponse<T = unknown> {
|
|
|
|
|
version: string;
|
|
|
|
|
success: boolean;
|
|
|
|
|
code: number;
|
|
|
|
|
message: string;
|
|
|
|
|
data?: T;
|
|
|
|
|
error?: {
|
|
|
|
|
type: string;
|
|
|
|
|
details: Record<string, unknown>;
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface User {
|
|
|
|
|
id: string;
|
|
|
|
|
email: string;
|
2026-01-06 16:06:53 +00:00
|
|
|
email_verified: boolean;
|
2026-01-06 15:33:03 +00:00
|
|
|
full_name: string | null;
|
|
|
|
|
avatar_url: string | null;
|
2026-01-06 16:06:53 +00:00
|
|
|
status: string;
|
|
|
|
|
last_login_at: string | null;
|
|
|
|
|
created_at: string;
|
|
|
|
|
updated_at: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface Organization {
|
|
|
|
|
id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
slug: string;
|
|
|
|
|
description: string | null;
|
|
|
|
|
logo_url: string | null;
|
2026-01-06 15:33:03 +00:00
|
|
|
is_active: boolean;
|
2026-01-06 16:06:53 +00:00
|
|
|
role: string;
|
2026-01-06 15:33:03 +00:00
|
|
|
created_at: string;
|
|
|
|
|
updated_at: string;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-06 16:06:53 +00:00
|
|
|
export interface OrganizationsResponse {
|
|
|
|
|
organizations: Organization[];
|
|
|
|
|
count: number;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-06 15:33:03 +00:00
|
|
|
export interface LoginResponse {
|
|
|
|
|
user: User;
|
2026-01-07 14:29:40 +00:00
|
|
|
token: string;
|
|
|
|
|
expires_at: string;
|
2026-01-06 15:33:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ProfileResponse {
|
|
|
|
|
user: User;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class ApiError extends Error {
|
|
|
|
|
code: number;
|
|
|
|
|
type: string;
|
|
|
|
|
details: Record<string, unknown>;
|
|
|
|
|
|
|
|
|
|
constructor(message: string, code: number, type: string, details: Record<string, unknown> = {}) {
|
|
|
|
|
super(message);
|
|
|
|
|
this.name = 'ApiError';
|
|
|
|
|
this.code = code;
|
|
|
|
|
this.type = type;
|
|
|
|
|
this.details = details;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 14:29:40 +00:00
|
|
|
// Token storage keys
|
|
|
|
|
const TOKEN_KEY = 'gatehouse_token';
|
|
|
|
|
const TOKEN_EXPIRY_KEY = 'gatehouse_token_expiry';
|
|
|
|
|
|
|
|
|
|
// Token management
|
|
|
|
|
export const tokenManager = {
|
|
|
|
|
getToken: (): string | null => {
|
|
|
|
|
const token = localStorage.getItem(TOKEN_KEY);
|
|
|
|
|
const expiry = localStorage.getItem(TOKEN_EXPIRY_KEY);
|
|
|
|
|
|
|
|
|
|
// Check if token is expired
|
|
|
|
|
if (token && expiry) {
|
|
|
|
|
const expiryDate = new Date(expiry);
|
|
|
|
|
if (expiryDate <= new Date()) {
|
|
|
|
|
tokenManager.clearToken();
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return token;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
setToken: (token: string, expiresAt: string): void => {
|
|
|
|
|
localStorage.setItem(TOKEN_KEY, token);
|
|
|
|
|
localStorage.setItem(TOKEN_EXPIRY_KEY, expiresAt);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
clearToken: (): void => {
|
|
|
|
|
localStorage.removeItem(TOKEN_KEY);
|
|
|
|
|
localStorage.removeItem(TOKEN_EXPIRY_KEY);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
hasValidToken: (): boolean => {
|
|
|
|
|
return tokenManager.getToken() !== null;
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Central request function - all API calls go through here
|
2026-01-06 15:33:03 +00:00
|
|
|
async function request<T>(
|
|
|
|
|
endpoint: string,
|
2026-01-07 14:29:40 +00:00
|
|
|
options: RequestInit = {},
|
|
|
|
|
requiresAuth = true
|
2026-01-06 15:33:03 +00:00
|
|
|
): Promise<T> {
|
2026-01-07 14:29:40 +00:00
|
|
|
const headers: Record<string, string> = {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
...(options.headers as Record<string, string>),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Add Authorization header if we have a token and auth is required
|
|
|
|
|
if (requiresAuth) {
|
|
|
|
|
const token = tokenManager.getToken();
|
|
|
|
|
if (token) {
|
|
|
|
|
headers['Authorization'] = `Bearer ${token}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-06 15:42:41 +00:00
|
|
|
const response = await fetch(`${config.api.baseUrl}${endpoint}`, {
|
2026-01-06 15:33:03 +00:00
|
|
|
...options,
|
2026-01-07 14:29:40 +00:00
|
|
|
headers,
|
2026-01-06 15:33:03 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const json: ApiResponse<T> = await response.json();
|
|
|
|
|
|
|
|
|
|
if (!json.success) {
|
2026-01-07 14:29:40 +00:00
|
|
|
// Clear token on 401 errors
|
|
|
|
|
if (json.code === 401) {
|
|
|
|
|
tokenManager.clearToken();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-06 15:33:03 +00:00
|
|
|
throw new ApiError(
|
|
|
|
|
json.message || 'An error occurred',
|
|
|
|
|
json.code,
|
|
|
|
|
json.error?.type || 'UNKNOWN_ERROR',
|
|
|
|
|
json.error?.details || {}
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return json.data as T;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-07 14:29:40 +00:00
|
|
|
// Centralized API client - all routes defined here
|
2026-01-06 15:33:03 +00:00
|
|
|
export const api = {
|
|
|
|
|
auth: {
|
2026-01-07 14:29:40 +00:00
|
|
|
login: async (email: string, password: string, remember_me = false): Promise<LoginResponse> => {
|
|
|
|
|
const response = await request<LoginResponse>('/auth/login', {
|
2026-01-06 15:33:03 +00:00
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify({ email, password, remember_me }),
|
2026-01-07 14:29:40 +00:00
|
|
|
}, false); // Login doesn't require auth
|
|
|
|
|
|
|
|
|
|
// Store token on successful login
|
|
|
|
|
tokenManager.setToken(response.token, response.expires_at);
|
|
|
|
|
|
|
|
|
|
return response;
|
|
|
|
|
},
|
2026-01-06 15:33:03 +00:00
|
|
|
|
2026-01-07 14:29:40 +00:00
|
|
|
logout: async (): Promise<void> => {
|
|
|
|
|
try {
|
|
|
|
|
await request<void>('/auth/logout', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
// Always clear token on logout
|
|
|
|
|
tokenManager.clearToken();
|
|
|
|
|
}
|
|
|
|
|
},
|
2026-01-06 15:33:03 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
users: {
|
|
|
|
|
me: () => request<ProfileResponse>('/users/me'),
|
|
|
|
|
|
|
|
|
|
updateMe: (data: { full_name?: string; avatar_url?: string }) =>
|
|
|
|
|
request<ProfileResponse>('/users/me', {
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
body: JSON.stringify(data),
|
|
|
|
|
}),
|
2026-01-06 16:06:53 +00:00
|
|
|
|
|
|
|
|
organizations: () => request<OrganizationsResponse>('/users/me/organizations'),
|
2026-01-06 15:33:03 +00:00
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export { ApiError };
|