Initial commit (unpacked platform)
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import Constants from "expo-constants";
|
||||
import { useAuth } from "./auth";
|
||||
|
||||
/**
|
||||
* API-klient. Mobilappen pratar ENDAST med Food API – aldrig direkt med
|
||||
* modelleverantörer, och innehåller inga AI-hemligheter (spec §31, §61.13).
|
||||
* 401 → automatisk refresh med roterande token → retry en gång.
|
||||
*/
|
||||
|
||||
const API_BASE =
|
||||
(Constants.expoConfig?.extra as { apiBaseUrl?: string } | undefined)?.apiBaseUrl ??
|
||||
"http://localhost:4000";
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(
|
||||
public readonly status: number,
|
||||
public readonly code: string,
|
||||
message: string,
|
||||
public readonly details?: unknown,
|
||||
) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
let refreshPromise: Promise<boolean> | null = null;
|
||||
|
||||
async function tryRefresh(): Promise<boolean> {
|
||||
if (!refreshPromise) {
|
||||
refreshPromise = (async () => {
|
||||
const { getRefreshToken, setSession, logout } = useAuth.getState();
|
||||
const refreshToken = await getRefreshToken();
|
||||
if (!refreshToken) return false;
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/v1/auth/refresh`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ refreshToken }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
await logout();
|
||||
return false;
|
||||
}
|
||||
const json = (await res.json()) as { accessToken: string; refreshToken: string };
|
||||
await setSession(
|
||||
{ accessToken: json.accessToken, refreshToken: json.refreshToken },
|
||||
useAuth.getState().user,
|
||||
);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
refreshPromise = null;
|
||||
}, 100);
|
||||
}
|
||||
})();
|
||||
}
|
||||
return refreshPromise;
|
||||
}
|
||||
|
||||
export async function api<T = unknown>(
|
||||
path: string,
|
||||
options: { method?: string; body?: unknown; retry?: boolean } = {},
|
||||
): Promise<T> {
|
||||
const { accessToken } = useAuth.getState();
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
method: options.method ?? "GET",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}),
|
||||
},
|
||||
body: options.body != null ? JSON.stringify(options.body) : undefined,
|
||||
});
|
||||
|
||||
if (res.status === 401 && options.retry !== false) {
|
||||
const refreshed = await tryRefresh();
|
||||
if (refreshed) return api<T>(path, { ...options, retry: false });
|
||||
}
|
||||
|
||||
const json = (await res.json().catch(() => ({}))) as {
|
||||
error?: { code?: string; message?: string; details?: unknown };
|
||||
};
|
||||
if (!res.ok) {
|
||||
throw new ApiError(
|
||||
res.status,
|
||||
json.error?.code ?? "UNKNOWN",
|
||||
json.error?.message ?? `HTTP ${res.status}`,
|
||||
json.error?.details,
|
||||
);
|
||||
}
|
||||
return json as T;
|
||||
}
|
||||
|
||||
/** Ladda upp en bild till en presignad URL (mock-S3 i dev, riktig S3 i prod). */
|
||||
export async function uploadImage(
|
||||
upload: { uploadUrl: string; headers: Record<string, string> },
|
||||
localUri: string,
|
||||
): Promise<void> {
|
||||
const blob = await (await fetch(localUri)).blob();
|
||||
const res = await fetch(upload.uploadUrl, {
|
||||
method: "PUT",
|
||||
headers: upload.headers,
|
||||
body: blob,
|
||||
});
|
||||
if (!res.ok) throw new ApiError(res.status, "UPLOAD_FAILED", "Bilduppladdningen misslyckades.");
|
||||
}
|
||||
|
||||
export { API_BASE };
|
||||
Reference in New Issue
Block a user