185 lines
5.2 KiB
TypeScript
185 lines
5.2 KiB
TypeScript
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
|
|
export const DEFAULT_API_BASE_URL = process.env.EXPO_PUBLIC_API_URL ?? 'http://localhost:3000/api';
|
|
|
|
const TOKEN_KEY = 'auth:token';
|
|
const USER_KEY = 'auth:user';
|
|
export const API_URL_KEY = 'settings:apiUrl';
|
|
|
|
function normalizeApiUrl(url: string): string {
|
|
return url.trim().replace(/\/+$/, '');
|
|
}
|
|
|
|
export async function getApiBaseUrl(): Promise<string> {
|
|
try {
|
|
const stored = await AsyncStorage.getItem(API_URL_KEY);
|
|
if (!stored) return DEFAULT_API_BASE_URL;
|
|
try {
|
|
const parsed = JSON.parse(stored) as unknown;
|
|
if (typeof parsed === 'string' && parsed.trim()) {
|
|
return normalizeApiUrl(parsed);
|
|
}
|
|
} catch {
|
|
// fall through to raw value
|
|
}
|
|
if (stored.trim()) {
|
|
return normalizeApiUrl(stored);
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
return DEFAULT_API_BASE_URL;
|
|
}
|
|
|
|
export async function setApiBaseUrl(url: string): Promise<void> {
|
|
const normalized = normalizeApiUrl(url);
|
|
try {
|
|
if (normalized) {
|
|
await AsyncStorage.setItem(API_URL_KEY, normalized);
|
|
} else {
|
|
await AsyncStorage.removeItem(API_URL_KEY);
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
export interface AuthUser {
|
|
id: string;
|
|
username: string;
|
|
}
|
|
|
|
export async function getAuthToken(): Promise<string | null> {
|
|
try {
|
|
return await AsyncStorage.getItem(TOKEN_KEY);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function getAuthUser(): Promise<AuthUser | null> {
|
|
try {
|
|
const raw = await AsyncStorage.getItem(USER_KEY);
|
|
return raw ? (JSON.parse(raw) as AuthUser) : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function setAuth(token: string, user: AuthUser): Promise<void> {
|
|
try {
|
|
await AsyncStorage.setItem(TOKEN_KEY, token);
|
|
await AsyncStorage.setItem(USER_KEY, JSON.stringify(user));
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
export async function signOutAuth(): Promise<void> {
|
|
try {
|
|
await AsyncStorage.removeItem(TOKEN_KEY);
|
|
await AsyncStorage.removeItem(USER_KEY);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
export async function apiFetch(path: string, options: RequestInit = {}): Promise<Response> {
|
|
const token = await getAuthToken();
|
|
const apiBaseUrl = await getApiBaseUrl();
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
...(options.headers as Record<string, string> | undefined),
|
|
};
|
|
if (token) {
|
|
headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
const response = await fetch(`${apiBaseUrl}${path}`, { ...options, headers });
|
|
if (!response.ok) {
|
|
const text = await response.text();
|
|
// If it's a 401 or network error, try to get a fresh token and retry once
|
|
if (response.status === 404 || response.status === 0) {
|
|
// Not found or network error - don't throw, return the response for caller to handle
|
|
const error = new Error(text || `Request failed (${response.status})`);
|
|
(error as any).status = response.status;
|
|
(error as any).offline = true;
|
|
throw error;
|
|
}
|
|
let message = `Request failed (${response.status})`;
|
|
let details: any = null;
|
|
try {
|
|
const body = JSON.parse(text);
|
|
if (body && typeof body.error?.message === 'string') {
|
|
message = body.error.message;
|
|
} else if (body && typeof body.message === 'string') {
|
|
message = body.message;
|
|
}
|
|
if (body && body.error && body.error.details) {
|
|
details = body.error.details;
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
const error = new Error(message);
|
|
(error as any).details = details;
|
|
(error as any).status = response.status;
|
|
throw error;
|
|
}
|
|
return response;
|
|
}
|
|
|
|
export async function login(username: string, password: string): Promise<AuthUser> {
|
|
const response = await apiFetch('/auth/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ username, password }),
|
|
});
|
|
const data = await response.json();
|
|
await setAuth(data.token, data.user);
|
|
return data.user as AuthUser;
|
|
}
|
|
|
|
export async function register(username: string, password: string): Promise<AuthUser> {
|
|
const response = await apiFetch('/auth/register', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ username, password }),
|
|
});
|
|
const data = await response.json();
|
|
await setAuth(data.token, data.user);
|
|
return data.user as AuthUser;
|
|
}
|
|
|
|
export async function forgotPassword(username: string): Promise<{ success: boolean; resetToken?: string }> {
|
|
const response = await apiFetch('/auth/forgot-password', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ username }),
|
|
});
|
|
const data = await response.json();
|
|
return data;
|
|
}
|
|
|
|
export async function resetPassword(token: string, password: string): Promise<{ success: boolean }> {
|
|
const response = await apiFetch('/auth/reset-password', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ token, password }),
|
|
});
|
|
const data = await response.json();
|
|
return data;
|
|
}
|
|
|
|
export async function registerPushToken(pushToken: string): Promise<{ success: boolean }> {
|
|
const response = await apiFetch('/push/push-token', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ pushToken }),
|
|
});
|
|
const data = await response.json();
|
|
return data;
|
|
}
|
|
|
|
export async function unregisterPushToken(): Promise<{ success: boolean }> {
|
|
const response = await apiFetch('/push/push-token', {
|
|
method: 'DELETE',
|
|
});
|
|
const data = await response.json();
|
|
return data;
|
|
}
|