more package declarations
This commit is contained in:
249
packages/mobile-api-client/src/api/api-client.ts
Normal file
249
packages/mobile-api-client/src/api/api-client.ts
Normal 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;
|
||||
};
|
||||
46
packages/mobile-api-client/src/api/auth.service.ts
Normal file
46
packages/mobile-api-client/src/api/auth.service.ts
Normal 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();
|
||||
47
packages/mobile-api-client/src/api/device.service.ts
Normal file
47
packages/mobile-api-client/src/api/device.service.ts
Normal 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();
|
||||
53
packages/mobile-api-client/src/api/notification.service.ts
Normal file
53
packages/mobile-api-client/src/api/notification.service.ts
Normal 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();
|
||||
53
packages/mobile-api-client/src/api/subscription.service.ts
Normal file
53
packages/mobile-api-client/src/api/subscription.service.ts
Normal 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();
|
||||
Reference in New Issue
Block a user