import type { GridMap, ListedMap, MapAccessRequest, MapPermission, MapRole, MapState, PublicAccess, UserInfo, } from "./types"; const GRID_BASE = "/api/grid"; const AUTH_BASE = "/api/auth"; async function request(url: string, init?: RequestInit): Promise { const res = await fetch(url, { ...init, credentials: "include", headers: { ...(init?.headers as Record), }, }); if (!res.ok) { const text = await res.text().catch(() => res.statusText); throw new Error(`${res.status}: ${text}`); } // Read the bdoy if it exists const text = await res.text(); if (!text) return undefined as T; // Parse JSON if it exists const contentType = res.headers.get("content-type") ?? ""; if (contentType.includes("application/json")) { return JSON.parse(text) as T; } else if (contentType.includes("text/plain")) { return text as unknown as T; } throw new Error(`Expected JSON or text but got: ${contentType}`); } export const api = { /** List maps where the authenticated user has a direct role or has favorited. */ listMaps: (): Promise => request(`${GRID_BASE}/maps`), /** Create a new map (authenticated). */ createMap: ( name: string, public_access: PublicAccess = "private", ): Promise => request(`${GRID_BASE}/maps`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, public_access }), }), /** Get full map state (cells, tokens, colors). */ getMap: (id: string): Promise => request(`${GRID_BASE}/maps/${id}`), /** Update map name and/or public_access (owner only). */ updateMap: ( id: string, payload: { name?: string; public_access?: PublicAccess }, ): Promise => request(`${GRID_BASE}/maps/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }), /** Delete a map (owner only). */ deleteMap: (id: string): Promise => request(`${GRID_BASE}/maps/${id}`, { method: "DELETE" }), // ---- Permissions ---- /** List all permissions for a map including usernames (owner only). */ listPermissions: (mapId: string): Promise => request(`${GRID_BASE}/maps/${mapId}/permissions`), /** * Add or update a user's role by username. * Pass `role: null` to remove the user's permission entirely. */ updatePermission: ( mapId: string, username: string, role: MapRole | null, ): Promise => request(`${GRID_BASE}/maps/${mapId}/permissions`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, role }), }), /** Favorite a map (adds it to the user's map list). */ favoriteMap: (id: string): Promise => request(`${GRID_BASE}/maps/${id}/favorite`, { method: "POST" }), /** Un-favorite a map. */ unfavoriteMap: (id: string): Promise => request(`${GRID_BASE}/maps/${id}/favorite`, { method: "DELETE" }), /** Request viewer or editor access to a map. */ requestAccess: (mapId: string, role: "editor" | "viewer"): Promise => request(`${GRID_BASE}/maps/${mapId}/access-requests`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ role }), }), /** List pending access requests for a map (owner only). */ listAccessRequests: (mapId: string): Promise => request(`${GRID_BASE}/maps/${mapId}/access-requests`), /** Approve or deny a pending access request (owner only). */ resolveAccessRequest: ( mapId: string, requestId: string, action: "approve" | "deny", ): Promise => request(`${GRID_BASE}/maps/${mapId}/access-requests/${requestId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action }), }), }; export const auth = { /** Fetch the currently authenticated user's info. Returns null if not logged in. */ async me(): Promise { try { return await request(`${AUTH_BASE}/me`); } catch { return null; } }, /** Register a new local account. */ async register(username: string, password: string): Promise { await request(`${AUTH_BASE}/register`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password }), }); }, /** Login with username and password. */ async loginLocal(username: string, password: string): Promise { await request(`${AUTH_BASE}/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password }), }); }, /** Start Discord OAuth login flow (anonymous). */ async loginDiscord(redirectUri?: string): Promise { const target = encodeURIComponent(redirectUri ?? window.location.href); const response = await request( `${AUTH_BASE}/discord/authorize?redirect_uri=${target}`, ); window.location.href = JSON.parse(response); }, /** Start Discord OAuth connect flow (authenticated users only). */ async connectDiscord(redirectUri?: string): Promise { const target = encodeURIComponent( redirectUri ?? window.location.origin + "/account", ); const response = await request( `${AUTH_BASE}/discord/connect?redirect_uri=${target}`, ); window.location.href = JSON.parse(response); }, /** Clear the session cookie server-side and reload. */ async logout(): Promise { try { await request(`${AUTH_BASE}/logout`, { method: "POST" }); } catch { // Ignore errors } window.location.href = "/map"; }, /** * Update the user's first and/or last name. * Pass an empty string to clear a field; pass undefined to leave it unchanged. */ async updateProfile( firstName?: string, lastName?: string, ): Promise { return request(`${AUTH_BASE}/profile`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ first_name: firstName, last_name: lastName }), }); }, /** * Change or set the account password. * `currentPassword` is required when the account already has a password, * and can be omitted (null/undefined) for OAuth-only accounts setting a * password for the first time. */ async changePassword( currentPassword: string | null, newPassword: string, ): Promise { await request(`${AUTH_BASE}/change-password`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ current_password: currentPassword ?? undefined, new_password: newPassword, }), }); }, /** Disconnect an OAuth provider (requires a password to be set first). */ async disconnectProvider(provider: string): Promise { await request(`${AUTH_BASE}/connections/${provider}`, { method: "DELETE", }); }, };