morethanadiagnosis-hub/mobile/app/lib/api.ts
Claude eb04163b3b
feat: implement authentication system for mobile app
- Add API integration layer with axios and token management
- Create Zustand auth store for state management
- Implement login screen with validation
- Implement signup screen with password strength indicator
- Implement forgot password flow with multi-step UI
- Update root layout with auth state protection
- Integrate profile screen with auth store
- Install AsyncStorage and expo-secure-store dependencies
2025-11-18 19:32:16 +00:00

284 lines
8.3 KiB
TypeScript

import axios, { AxiosError, AxiosInstance, InternalAxiosRequestConfig } from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {
LoginRequest,
SignupRequest,
ForgotPasswordRequest,
ResetPasswordRequest,
AuthResponse,
User,
Resource,
ResourceCategory,
CommunitySpace,
UserPreferences,
ApiError,
} from './types';
// Storage keys
const TOKEN_KEY = 'auth_token';
const REFRESH_TOKEN_KEY = 'refresh_token';
// API base URL - update this for your environment
const API_BASE_URL = process.env.EXPO_PUBLIC_API_URL || 'https://api.morethanadiagnosis.org/api/v1';
// Create axios instance
const api: AxiosInstance = axios.create({
baseURL: API_BASE_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json',
},
});
// Request interceptor - add auth token to requests
api.interceptors.request.use(
async (config: InternalAxiosRequestConfig) => {
const token = await AsyncStorage.getItem(TOKEN_KEY);
if (token && config.headers) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor - handle token refresh
api.interceptors.response.use(
(response) => response,
async (error: AxiosError<ApiError>) => {
const originalRequest = error.config;
// If 401 and we haven't tried to refresh yet
if (error.response?.status === 401 && originalRequest && !originalRequest._retry) {
originalRequest._retry = true;
try {
const refreshToken = await AsyncStorage.getItem(REFRESH_TOKEN_KEY);
if (refreshToken) {
const response = await axios.post<AuthResponse>(`${API_BASE_URL}/auth/refresh`, {
refreshToken,
});
const { accessToken, refreshToken: newRefreshToken } = response.data;
// Store new tokens
await AsyncStorage.setItem(TOKEN_KEY, accessToken);
await AsyncStorage.setItem(REFRESH_TOKEN_KEY, newRefreshToken);
// Retry original request with new token
if (originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
}
return api(originalRequest);
}
} catch (refreshError) {
// Refresh failed - clear tokens and redirect to login
await AsyncStorage.multiRemove([TOKEN_KEY, REFRESH_TOKEN_KEY]);
// The auth store should handle navigation
}
}
return Promise.reject(error);
}
);
// Extend AxiosRequestConfig to include _retry property
declare module 'axios' {
export interface InternalAxiosRequestConfig {
_retry?: boolean;
}
}
// ==========================================
// Authentication API
// ==========================================
export const authApi = {
login: async (data: LoginRequest): Promise<AuthResponse> => {
const response = await api.post<AuthResponse>('/auth/login', data);
const { accessToken, refreshToken } = response.data;
// Store tokens
await AsyncStorage.setItem(TOKEN_KEY, accessToken);
await AsyncStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
return response.data;
},
signup: async (data: SignupRequest): Promise<AuthResponse> => {
const response = await api.post<AuthResponse>('/auth/signup', data);
const { accessToken, refreshToken } = response.data;
// Store tokens
await AsyncStorage.setItem(TOKEN_KEY, accessToken);
await AsyncStorage.setItem(REFRESH_TOKEN_KEY, refreshToken);
return response.data;
},
forgotPassword: async (data: ForgotPasswordRequest): Promise<{ message: string }> => {
const response = await api.post<{ message: string }>('/auth/forgot-password', data);
return response.data;
},
resetPassword: async (data: ResetPasswordRequest): Promise<{ message: string }> => {
const response = await api.post<{ message: string }>('/auth/reset-password', data);
return response.data;
},
logout: async (): Promise<void> => {
try {
await api.post('/auth/logout');
} catch {
// Ignore errors on logout
} finally {
await AsyncStorage.multiRemove([TOKEN_KEY, REFRESH_TOKEN_KEY]);
}
},
refreshToken: async (): Promise<AuthResponse> => {
const refreshToken = await AsyncStorage.getItem(REFRESH_TOKEN_KEY);
if (!refreshToken) {
throw new Error('No refresh token available');
}
const response = await api.post<AuthResponse>('/auth/refresh', { refreshToken });
const { accessToken, refreshToken: newRefreshToken } = response.data;
await AsyncStorage.setItem(TOKEN_KEY, accessToken);
await AsyncStorage.setItem(REFRESH_TOKEN_KEY, newRefreshToken);
return response.data;
},
getStoredToken: async (): Promise<string | null> => {
return AsyncStorage.getItem(TOKEN_KEY);
},
};
// ==========================================
// User API
// ==========================================
export const userApi = {
getMe: async (): Promise<User> => {
const response = await api.get<User>('/user/me');
return response.data;
},
updateMe: async (data: Partial<User>): Promise<User> => {
const response = await api.put<User>('/user/me', data);
return response.data;
},
getPreferences: async (): Promise<UserPreferences> => {
const response = await api.get<UserPreferences>('/user/preferences');
return response.data;
},
updatePreferences: async (data: Partial<UserPreferences>): Promise<UserPreferences> => {
const response = await api.put<UserPreferences>('/user/preferences', data);
return response.data;
},
};
// ==========================================
// Resources API
// ==========================================
export const resourcesApi = {
getCategories: async (): Promise<ResourceCategory[]> => {
const response = await api.get<ResourceCategory[]>('/resources/categories');
return response.data;
},
getAll: async (): Promise<Resource[]> => {
const response = await api.get<Resource[]>('/resources');
return response.data;
},
getById: async (id: string): Promise<Resource> => {
const response = await api.get<Resource>(`/resources/${id}`);
return response.data;
},
getByCategory: async (categoryId: string): Promise<Resource[]> => {
const response = await api.get<Resource[]>(`/resources/category/${categoryId}`);
return response.data;
},
};
// ==========================================
// Community API
// ==========================================
export const communityApi = {
getSpaces: async (): Promise<CommunitySpace[]> => {
const response = await api.get<CommunitySpace[]>('/community');
return response.data;
},
getSpaceById: async (id: string): Promise<CommunitySpace> => {
const response = await api.get<CommunitySpace>(`/community/${id}`);
return response.data;
},
joinSpace: async (id: string): Promise<{ message: string }> => {
const response = await api.post<{ message: string }>(`/community/${id}/join`);
return response.data;
},
leaveSpace: async (id: string): Promise<{ message: string }> => {
const response = await api.post<{ message: string }>(`/community/${id}/leave`);
return response.data;
},
};
// ==========================================
// Helper functions
// ==========================================
export const handleApiError = (error: unknown): string => {
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError<ApiError>;
// Network error
if (!axiosError.response) {
return 'Network error. Please check your connection.';
}
// API error response
const apiError = axiosError.response.data;
if (apiError?.message) {
return apiError.message;
}
// HTTP status errors
switch (axiosError.response.status) {
case 400:
return 'Invalid request. Please check your input.';
case 401:
return 'Invalid credentials. Please try again.';
case 403:
return 'You do not have permission to perform this action.';
case 404:
return 'Resource not found.';
case 422:
return 'Validation error. Please check your input.';
case 500:
return 'Server error. Please try again later.';
default:
return 'An unexpected error occurred.';
}
}
if (error instanceof Error) {
return error.message;
}
return 'An unexpected error occurred.';
};
export default api;