client: auth context and API layer

Add JWT-based AuthContext (login/logout, token persistence), typed
HTTP client with auth header injection, REST API functions for all
game endpoints, and shared TypeScript types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-09 23:42:22 +02:00
parent ad02b274d2
commit 9e65fe5a3c
5 changed files with 417 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
import { API_URL } from '../config';
/** Errore restituito dal server nella forma { error: { code, message } }. */
export class ApiError extends Error {
constructor(
public readonly status: number,
public readonly code: string,
message: string,
) {
super(message);
this.name = 'ApiError';
}
}
let accessToken: string | null = null;
export function setAccessToken(token: string | null): void {
accessToken = token;
}
type RequestOptions = {
method?: 'GET' | 'POST';
body?: unknown;
};
export async function request<T>(path: string, options: RequestOptions = {}): Promise<T> {
const headers: Record<string, string> = {};
if (options.body !== undefined) headers['Content-Type'] = 'application/json';
if (accessToken) headers['Authorization'] = `Bearer ${accessToken}`;
let response: Response;
try {
response = await fetch(`${API_URL}/api/v1${path}`, {
method: options.method ?? 'GET',
headers,
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
});
} catch {
throw new ApiError(0, 'NETWORK_ERROR', 'Server non raggiungibile');
}
if (!response.ok) {
let code = 'REQUEST_ERROR';
let message = `Errore ${response.status}`;
try {
const payload = (await response.json()) as { error?: { code?: string; message?: string } };
code = payload.error?.code ?? code;
message = payload.error?.message ?? message;
} catch {
// risposta non JSON: si tengono i default
}
throw new ApiError(response.status, code, message);
}
return (await response.json()) as T;
}