more package declarations

This commit is contained in:
2026-05-17 21:52:38 -04:00
parent a8a5930ced
commit f118d3a4f3
44 changed files with 14019 additions and 1918 deletions

View File

@@ -0,0 +1,249 @@
/**
* API Client for ShieldAI services
* Handles authentication, request/response interception, and error handling
*/
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
import { tokenStorage } from '../storage/token-storage';
import { requestQueue } from '../utils/request-queue';
import type { AuthTokens, AuthResponse, RefreshTokenRequest } from '../types';
export interface ApiClientConfig {
baseURL: string;
timeout?: number;
debug?: boolean;
}
export class ApiClient {
private client: AxiosInstance;
private config: ApiClientConfig;
private isRefreshing = false;
private refreshSubscribers: Set<(token: string) => void> = new Set();
constructor(config: ApiClientConfig) {
this.config = {
baseURL: config.baseURL,
timeout: config.timeout ?? 30000,
debug: config.debug ?? false,
};
this.client = axios.create({
baseURL: this.config.baseURL,
timeout: this.config.timeout,
headers: {
'Content-Type': 'application/json',
},
});
this.setupInterceptors();
}
private setupInterceptors(): void {
// Request interceptor - add auth token
this.client.interceptors.request.use(
async (config) => {
if (this.config.debug) {
console.log('[API] Request:', config.method?.toUpperCase(), config.url);
}
// Add auth token if available
const token = await tokenStorage.getAccessToken();
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
// Queue request if offline
if (!requestQueue.isOnline() && this.requiresNetwork(config)) {
await requestQueue.enqueue(config);
throw new Error('OFFLINE');
}
return config;
},
(error) => {
if (error.message === 'OFFLINE') {
return Promise.reject({ offline: true, config: error.config });
}
return Promise.reject(error);
}
);
// Response interceptor - handle errors and token refresh
this.client.interceptors.response.use(
(response) => {
if (this.config.debug) {
console.log('[API] Response:', response.status, response.config.url);
}
return response;
},
async (error: AxiosError) => {
const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean };
// Handle 401 - unauthorized
if (error.response?.status === 401 && !originalRequest._retry) {
if (this.isRefreshing) {
// Wait for refresh to complete
return new Promise((resolve) => {
this.refreshSubscribers.add((token) => {
originalRequest.headers = originalRequest.headers || {};
originalRequest.headers.Authorization = `Bearer ${token}`;
resolve(this.client(originalRequest));
});
});
}
originalRequest._retry = true;
this.isRefreshing = true;
try {
const refreshToken = await tokenStorage.getRefreshToken();
if (!refreshToken) {
throw new Error('No refresh token');
}
const newTokens = await this.refreshAccessToken(refreshToken);
await tokenStorage.saveTokens(newTokens.accessToken, newTokens.refreshToken);
// Retry failed requests
this.refreshSubscribers.forEach((callback) => callback(newTokens.accessToken));
this.refreshSubscribers.clear();
originalRequest.headers = originalRequest.headers || {};
originalRequest.headers.Authorization = `Bearer ${newTokens.accessToken}`;
return this.client(originalRequest);
} catch (refreshError) {
// Refresh failed - clear tokens and redirect to login
await tokenStorage.clearTokens();
this.refreshSubscribers.clear();
return Promise.reject(refreshError);
} finally {
this.isRefreshing = false;
}
}
return Promise.reject(error);
}
);
}
private requiresNetwork(config: AxiosRequestConfig): boolean {
// Don't queue GET requests that can be cached
const method = (config.method || 'get').toLowerCase();
return method !== 'get';
}
private async refreshAccessToken(refreshToken: string): Promise<AuthTokens> {
const response = await this.client.post<AuthResponse>('/auth/refresh', {
refreshToken,
});
return {
accessToken: response.data.tokens.accessToken,
refreshToken: response.data.tokens.refreshToken,
expiresIn: response.data.tokens.expiresIn,
tokenType: response.data.tokens.tokenType,
};
}
// Subscribe to token refresh
onTokenRefresh(callback: (token: string) => void): () => void {
this.refreshSubscribers.add(callback);
return () => this.refreshSubscribers.delete(callback);
}
// Public API methods
async get<T>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.client.get<T>(url, config);
}
async post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.client.post<T>(url, data, config);
}
async put<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.client.put<T>(url, data, config);
}
async patch<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.client.patch<T>(url, data, config);
}
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<AxiosResponse<T>> {
return this.client.delete<T>(url, config);
}
// Auth methods
async login(email: string, password: string): Promise<AuthResponse> {
const response = await this.post<AuthResponse>('/auth/login', {
email,
password,
});
if (response.data.tokens) {
await tokenStorage.saveTokens(
response.data.tokens.accessToken,
response.data.tokens.refreshToken
);
}
return response.data;
}
async register(data: {
email: string;
password: string;
firstName?: string;
lastName?: string;
}): Promise<AuthResponse> {
const response = await this.post<AuthResponse>('/auth/register', data);
if (response.data.tokens) {
await tokenStorage.saveTokens(
response.data.tokens.accessToken,
response.data.tokens.refreshToken
);
}
return response.data;
}
async logout(): Promise<void> {
try {
await this.post('/auth/logout');
} finally {
await tokenStorage.clearTokens();
}
}
async isAuthenticated(): Promise<boolean> {
const token = await tokenStorage.getAccessToken();
return !!token;
}
// Health check
async healthCheck(): Promise<{ status: string; version: string }> {
const response = await this.get<{ status: string; version: string }>('/health');
return response.data;
}
// Get the underlying axios instance for advanced usage
getClient(): AxiosInstance {
return this.client;
}
}
// Default client instance
let defaultClient: ApiClient | null = null;
export const createApiClient = (config: ApiClientConfig): ApiClient => {
defaultClient = new ApiClient(config);
return defaultClient;
};
export const getApiClient = (): ApiClient => {
if (!defaultClient) {
throw new Error('API Client not initialized. Call createApiClient first.');
}
return defaultClient;
};

View File

@@ -0,0 +1,46 @@
/**
* Authentication API service
*/
import { getApiClient } from './api-client';
import type {
User,
AuthResponse,
LoginCredentials,
RegisterData,
AuthTokens
} from '../types';
export class AuthService {
private api = getApiClient();
async login(credentials: LoginCredentials): Promise<AuthResponse> {
const response = await this.api.login(credentials.email, credentials.password);
return response;
}
async register(data: RegisterData): Promise<AuthResponse> {
const response = await this.api.register(data);
return response;
}
async logout(): Promise<void> {
await this.api.logout();
}
async getCurrentUser(): Promise<User> {
const response = await this.api.get<{ user: User; authType: string }>('/auth/user/me');
return response.data.user;
}
async refreshToken(): Promise<AuthTokens> {
const response = await this.api.get<AuthTokens>('/auth/refresh-token');
return response.data;
}
async isAuthenticated(): Promise<boolean> {
return await this.api.isAuthenticated();
}
}
export const authService = new AuthService();

View File

@@ -0,0 +1,47 @@
/**
* Device API service
*/
import { getApiClient } from './api-client';
import type { Device, DeviceRegistration, DeviceListResponse } from '../types';
export class DeviceService {
private api = getApiClient();
async registerDevice(data: DeviceRegistration): Promise<Device> {
const response = await this.api.post<Device>('/api/v1/devices/register', data);
return response.data;
}
async updatePushToken(pushToken: string): Promise<Device> {
const response = await this.api.patch<Device>('/api/v1/devices/push-token', {
pushToken,
});
return response.data;
}
async getDevices(): Promise<DeviceListResponse> {
const response = await this.api.get<DeviceListResponse>('/api/v1/devices');
return response.data;
}
async getDevice(deviceId: string): Promise<Device> {
const response = await this.api.get<Device>(`/api/v1/devices/${deviceId}`);
return response.data;
}
async deleteDevice(deviceId: string): Promise<void> {
await this.api.delete(`/api/v1/devices/${deviceId}`);
}
async getCurrentDevice(): Promise<Device | null> {
try {
const response = await this.api.get<Device>('/api/v1/devices/current');
return response.data;
} catch {
return null;
}
}
}
export const deviceService = new DeviceService();

View File

@@ -0,0 +1,53 @@
/**
* Notification API service
*/
import { getApiClient } from './api-client';
import type { Notification, NotificationListResponse, NotificationPreferences } from '../types';
export class NotificationService {
private api = getApiClient();
async getNotifications(params?: {
page?: number;
limit?: number;
unreadOnly?: boolean;
}): Promise<NotificationListResponse> {
const response = await this.api.get<NotificationListResponse>('/notifications', { params });
return response.data;
}
async getNotification(notificationId: string): Promise<Notification> {
const response = await this.api.get<Notification>(`/notifications/${notificationId}`);
return response.data;
}
async markAsRead(notificationId: string): Promise<void> {
await this.api.patch(`/notifications/${notificationId}/read`);
}
async markAllAsRead(): Promise<void> {
await this.api.post('/notifications/read-all');
}
async deleteNotification(notificationId: string): Promise<void> {
await this.api.delete(`/notifications/${notificationId}`);
}
async getPreferences(): Promise<NotificationPreferences> {
const response = await this.api.get<NotificationPreferences>('/notifications/preferences');
return response.data;
}
async updatePreferences(preferences: NotificationPreferences): Promise<NotificationPreferences> {
const response = await this.api.put<NotificationPreferences>('/notifications/preferences', preferences);
return response.data;
}
async getUnreadCount(): Promise<number> {
const response = await this.api.get<{ count: number }>('/notifications/unread-count');
return response.data.count;
}
}
export const notificationService = new NotificationService();

View File

@@ -0,0 +1,53 @@
/**
* Subscription API service
*/
import { getApiClient } from './api-client';
import type {
Subscription,
SubscriptionTier,
SubscriptionStatusResponse,
CreateSubscriptionRequest,
UpdateSubscriptionRequest,
} from '../types';
export class SubscriptionService {
private api = getApiClient();
async getSubscription(): Promise<SubscriptionStatusResponse> {
const response = await this.api.get<SubscriptionStatusResponse>('/billing/subscription');
return response.data;
}
async createSubscription(data: CreateSubscriptionRequest): Promise<Subscription> {
const response = await this.api.post<Subscription>('/billing/subscription', data);
return response.data;
}
async updateSubscription(data: UpdateSubscriptionRequest): Promise<Subscription> {
const response = await this.api.patch<Subscription>('/billing/subscription', data);
return response.data;
}
async cancelSubscription(): Promise<Subscription> {
const response = await this.api.delete<Subscription>('/billing/subscription');
return response.data;
}
async getTiers(): Promise<SubscriptionTier[]> {
const response = await this.api.get<SubscriptionTier[]>('/billing/tiers');
return response.data;
}
async createCheckoutSession(tier: string): Promise<{ url: string }> {
const response = await this.api.post<{ url: string }>('/billing/checkout', { tier });
return response.data;
}
async createCustomerPortalSession(): Promise<{ url: string }> {
const response = await this.api.post<{ url: string }>('/billing/customer-portal');
return response.data;
}
}
export const subscriptionService = new SubscriptionService();