from scratch
This commit is contained in:
@@ -1,176 +0,0 @@
|
||||
import { API_BASE_URL, ENDPOINTS } from '../constants/api';
|
||||
import { ApiError, HttpError } from '../types';
|
||||
import { tokenStorage } from './secureStorage';
|
||||
|
||||
class ApiClient {
|
||||
private baseUrl: string;
|
||||
private accessToken: string | null = null;
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
this.baseUrl = baseUrl;
|
||||
}
|
||||
|
||||
setAccessToken(token: string | null) {
|
||||
this.accessToken = token;
|
||||
}
|
||||
|
||||
getAccessToken(): string | null {
|
||||
return this.accessToken;
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {},
|
||||
isRetry = false
|
||||
): Promise<T> {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...(options.headers as Record<string, string>),
|
||||
};
|
||||
|
||||
if (this.accessToken) {
|
||||
headers['Authorization'] = `Bearer ${this.accessToken}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (response.status === 401 && !isRetry) {
|
||||
const refreshed = await this.tryRefreshToken();
|
||||
if (refreshed) {
|
||||
return this.request<T>(endpoint, options, true);
|
||||
}
|
||||
throw new HttpError(401, 'Session expirée', 'UNAUTHORIZED');
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
let message = 'Request failed';
|
||||
let code: string | undefined;
|
||||
try {
|
||||
const body: ApiError = await response.json();
|
||||
message = body.error?.message || message;
|
||||
code = body.error?.code;
|
||||
} catch {}
|
||||
throw new HttpError(response.status, message, code);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
private async tryRefreshToken(): Promise<boolean> {
|
||||
try {
|
||||
const refreshToken = await tokenStorage.getRefreshToken();
|
||||
if (!refreshToken) return false;
|
||||
|
||||
const url = `${this.baseUrl}${ENDPOINTS.AUTH_REFRESH}`;
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ refresh_token: refreshToken }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
await this.clearAuth();
|
||||
return false;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
this.accessToken = data.access_token;
|
||||
await tokenStorage.setAccessToken(data.access_token);
|
||||
await tokenStorage.setRefreshToken(data.refresh_token);
|
||||
return true;
|
||||
} catch {
|
||||
await this.clearAuth();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async clearAuth() {
|
||||
this.accessToken = null;
|
||||
await tokenStorage.deleteAccessToken();
|
||||
await tokenStorage.deleteRefreshToken();
|
||||
await tokenStorage.deleteUser();
|
||||
}
|
||||
|
||||
async subscribeEvents(
|
||||
onEvent: (event: string, data: string) => void,
|
||||
onError: (err: Error) => void,
|
||||
): Promise<() => void> {
|
||||
const abortController = new AbortController();
|
||||
|
||||
const connect = async () => {
|
||||
try {
|
||||
const headers: Record<string, string> = {
|
||||
Accept: 'text/event-stream',
|
||||
};
|
||||
if (this.accessToken) {
|
||||
headers['Authorization'] = `Bearer ${this.accessToken}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${this.baseUrl}${ENDPOINTS.EVENTS}`, {
|
||||
headers,
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`SSE connection failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) throw new Error('Streaming not supported');
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
let currentEvent = '';
|
||||
|
||||
while (!abortController.signal.aborted) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('event: ')) {
|
||||
currentEvent = line.slice(7);
|
||||
} else if (line.startsWith('data: ')) {
|
||||
if (currentEvent) {
|
||||
onEvent(currentEvent, line.slice(6));
|
||||
currentEvent = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (!abortController.signal.aborted) {
|
||||
onError(err instanceof Error ? err : new Error(String(err)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
connect();
|
||||
|
||||
return () => abortController.abort();
|
||||
}
|
||||
|
||||
async get<T>(endpoint: string): Promise<T> {
|
||||
return this.request<T>(endpoint);
|
||||
}
|
||||
|
||||
async post<T>(endpoint: string, body?: unknown): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'POST',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async delete<T>(endpoint: string): Promise<T> {
|
||||
return this.request<T>(endpoint, { method: 'DELETE' });
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new ApiClient(API_BASE_URL);
|
||||
@@ -1,43 +0,0 @@
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
|
||||
const ACCESS_KEY = 'vaultdrop_access_token';
|
||||
const REFRESH_KEY = 'vaultdrop_refresh_token';
|
||||
const USER_KEY = 'vaultdrop_user';
|
||||
|
||||
export const tokenStorage = {
|
||||
async getAccessToken(): Promise<string | null> {
|
||||
return SecureStore.getItemAsync(ACCESS_KEY);
|
||||
},
|
||||
|
||||
async setAccessToken(token: string): Promise<void> {
|
||||
await SecureStore.setItemAsync(ACCESS_KEY, token);
|
||||
},
|
||||
|
||||
async deleteAccessToken(): Promise<void> {
|
||||
await SecureStore.deleteItemAsync(ACCESS_KEY);
|
||||
},
|
||||
|
||||
async getRefreshToken(): Promise<string | null> {
|
||||
return SecureStore.getItemAsync(REFRESH_KEY);
|
||||
},
|
||||
|
||||
async setRefreshToken(token: string): Promise<void> {
|
||||
await SecureStore.setItemAsync(REFRESH_KEY, token);
|
||||
},
|
||||
|
||||
async deleteRefreshToken(): Promise<void> {
|
||||
await SecureStore.deleteItemAsync(REFRESH_KEY);
|
||||
},
|
||||
|
||||
async getUser(): Promise<string | null> {
|
||||
return SecureStore.getItemAsync(USER_KEY);
|
||||
},
|
||||
|
||||
async setUser(user: string): Promise<void> {
|
||||
await SecureStore.setItemAsync(USER_KEY, user);
|
||||
},
|
||||
|
||||
async deleteUser(): Promise<void> {
|
||||
await SecureStore.deleteItemAsync(USER_KEY);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user