Initial commit
**Motivations:** - Initialisation du versionning git pour le projet **Root causes:** - N/A (Nouveau projet) **Correctifs:** - N/A **Evolutions:** - Structure initiale du projet - Ajout du .gitignore **Pages affectées:** - Tous les fichiers
This commit is contained in:
258
web/js/api-client.js
Normal file
258
web/js/api-client.js
Normal file
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* API client: signed requests (Ed25519), load/save zoo state and world zoos.
|
||||
*/
|
||||
|
||||
import { getOrCreateKeyPair, signMessage } from "./auth-client.js";
|
||||
|
||||
/**
|
||||
* @returns {string} API base URL (from ?api=, window, or localStorage)
|
||||
*/
|
||||
export function getApiBase() {
|
||||
if (typeof window === "undefined") return "";
|
||||
let base = "";
|
||||
if (window.BUILAZOO_API_URL !== undefined && window.BUILAZOO_API_URL !== null && window.BUILAZOO_API_URL !== "") {
|
||||
base = String(window.BUILAZOO_API_URL);
|
||||
} else {
|
||||
try {
|
||||
const stored = localStorage.getItem("builazoo_api_url");
|
||||
if (stored && stored.trim() !== "") base = stored.trim();
|
||||
} catch (_) {
|
||||
// ignore localStorage access errors
|
||||
}
|
||||
}
|
||||
return base ? base.replace(/\/+$/, "") : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist API base URL so it is used on next load (getApiBase reads it from localStorage).
|
||||
* @param {string} url
|
||||
*/
|
||||
export function setApiBaseUrl(url) {
|
||||
const raw = url ? String(url).trim() : "";
|
||||
const trimmed = raw ? raw.replace(/\/+$/, "") : "";
|
||||
if (typeof window !== "undefined") window.BUILAZOO_API_URL = trimmed || undefined;
|
||||
try {
|
||||
if (trimmed) localStorage.setItem("builazoo_api_url", trimmed);
|
||||
else localStorage.removeItem("builazoo_api_url");
|
||||
} catch (_) {
|
||||
// ignore localStorage errors
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} body
|
||||
* @returns {Promise<string>} hex
|
||||
*/
|
||||
async function hashBody(body) {
|
||||
const enc = new TextEncoder().encode(body || "");
|
||||
const buf = await crypto.subtle.digest("SHA-256", enc);
|
||||
return Array.from(new Uint8Array(buf))
|
||||
.map((b) => b.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} method
|
||||
* @param {string} path
|
||||
* @param {string} [body]
|
||||
* @returns {Promise<Response>}
|
||||
*/
|
||||
export async function signedFetch(method, path, body) {
|
||||
const base = getApiBase();
|
||||
if (!base) throw new Error("No API base URL");
|
||||
const url = base + path;
|
||||
const timestamp = new Date().toISOString();
|
||||
const bodyStr = body !== null && body !== undefined ? (typeof body === "string" ? body : JSON.stringify(body)) : "";
|
||||
const bodyHash = await hashBody(bodyStr);
|
||||
const message = `${method}\n${path}\n${timestamp}\n${bodyHash}`;
|
||||
const keys = await getOrCreateKeyPair();
|
||||
if (!keys) throw new Error("No keypair");
|
||||
const signature = await signMessage(keys.privateKey, message);
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Public-Key": keys.publicKeyBase64,
|
||||
"X-Signature": signature,
|
||||
"X-Timestamp": timestamp,
|
||||
};
|
||||
const init = { method, headers };
|
||||
if (bodyStr) init.body = bodyStr;
|
||||
return fetch(url, init);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<{ worldZoos: Array<{ id: string, name: string, x: number, y: number, animalWeights: object }>, mapWidth: number, mapHeight: number }>}
|
||||
*/
|
||||
export async function loadZoos() {
|
||||
const base = getApiBase();
|
||||
if (!base) return { worldZoos: [], mapWidth: 100, mapHeight: 100 };
|
||||
const res = await fetch(base + "/api/zoos");
|
||||
if (!res.ok) throw new Error(`loadZoos ${res.status}`);
|
||||
const data = await res.json();
|
||||
return {
|
||||
worldZoos: data.worldZoos || [],
|
||||
mapWidth: data.mapWidth ?? 100,
|
||||
mapHeight: data.mapHeight ?? 100,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Load current user's zoo. Distinguishes 401 (account unknown → register) from 404 (no zoo → create zoo).
|
||||
* @returns {Promise<{ ok: true, data: { zooId: string, name: string, x: number, y: number, game_state: object | null } } | { status: 401 } | { status: 404 }>}
|
||||
*/
|
||||
export async function loadMyZoo() {
|
||||
const base = getApiBase();
|
||||
if (!base) return { status: 404 };
|
||||
const res = await signedFetch("GET", "/api/zoos/me");
|
||||
if (res.status === 401) return { status: 401 };
|
||||
if (res.status === 404) return { status: 404 };
|
||||
if (!res.ok) throw new Error(`loadMyZoo ${res.status}`);
|
||||
const data = await res.json();
|
||||
return { ok: true, data };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} gameState
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function saveMyZoo(gameState) {
|
||||
const base = getApiBase();
|
||||
if (!base) return;
|
||||
const res = await signedFetch("PATCH", "/api/zoos/me", { game_state: gameState });
|
||||
if (!res.ok) throw new Error(`saveMyZoo ${res.status}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} pseudo
|
||||
* @returns {Promise<{ id: string, pseudo: string }>}
|
||||
*/
|
||||
export async function register(pseudo) {
|
||||
const base = getApiBase();
|
||||
if (!base) throw new Error("No API base URL");
|
||||
const keys = await getOrCreateKeyPair();
|
||||
if (!keys) throw new Error("No keypair");
|
||||
const res = await fetch(base + "/api/auth/register", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-Public-Key": keys.publicKeyBase64,
|
||||
},
|
||||
body: JSON.stringify({ pseudo: pseudo.trim() }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.error || `register ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {object} gameState
|
||||
* @returns {Promise<{ zooId: string, name: string, x: number, y: number }>}
|
||||
*/
|
||||
export async function createMyZoo(name, gameState) {
|
||||
const res = await signedFetch("POST", "/api/zoos/me", { name, game_state: gameState });
|
||||
if (!res.ok) throw new Error(`createMyZoo ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch sales: with auth returns { asSeller, asBuyerUndelivered, active }; without auth only { active }.
|
||||
* @returns {Promise<{ asSeller?: Array<object>, asBuyerUndelivered?: Array<object>, active: Array<object> }>}
|
||||
*/
|
||||
export async function getSales() {
|
||||
const base = getApiBase();
|
||||
if (!base) return { active: [] };
|
||||
const res = await signedFetch("GET", "/api/sales");
|
||||
if (!res.ok) throw new Error(`getSales ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a server sale listing (asSeller item) to client SaleListing shape.
|
||||
* @param {{ id: string, seller_zoo_id: string, animal_id: string, is_baby: boolean, initial_price: number, end_at: string, status?: string, best_bid_amount?: number, best_bidder_zoo_id?: string, sold_at?: string, validated_at?: string | null, reproduction_score_at_sale?: number }} s
|
||||
* @returns {import("./types.js").SaleListing}
|
||||
*/
|
||||
export function mapServerListingToClient(s) {
|
||||
return {
|
||||
id: s.id,
|
||||
zooId: s.seller_zoo_id,
|
||||
animalId: s.animal_id,
|
||||
isBaby: s.is_baby,
|
||||
price: s.initial_price,
|
||||
endAt: Math.floor(new Date(s.end_at).getTime() / 1000),
|
||||
status: s.status,
|
||||
bestBidAmount: s.best_bid_amount,
|
||||
bestBidderZooId: s.best_bidder_zoo_id,
|
||||
validatedAt: s.validated_at ? Math.floor(new Date(s.validated_at).getTime() / 1000) : null,
|
||||
reproductionScoreAtSale: s.reproduction_score_at_sale,
|
||||
serverId: s.id,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a sale listing. Body: animalId, isBaby, price, endAt, reproductionScoreAtSale?.
|
||||
* @param {{ animalId: string, isBaby: boolean, price: number, endAt: string, reproductionScoreAtSale?: number }} payload
|
||||
* @returns {Promise<{ id: string }>}
|
||||
*/
|
||||
export async function createSale(payload) {
|
||||
const res = await signedFetch("POST", "/api/sales", payload);
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.error || `createSale ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Place a bid on a listing.
|
||||
* @param {string} listingId
|
||||
* @param {number} amount
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function placeBid(listingId, amount) {
|
||||
const res = await signedFetch("POST", `/api/sales/${listingId}/bid`, { amount });
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.error || `placeBid ${res.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seller accepts the best bid.
|
||||
* @param {string} listingId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function acceptSale(listingId) {
|
||||
const res = await signedFetch("POST", `/api/sales/${listingId}/accept`);
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.error || `acceptSale ${res.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seller rejects the current best bid.
|
||||
* @param {string} listingId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function rejectSale(listingId) {
|
||||
const res = await signedFetch("POST", `/api/sales/${listingId}/reject`);
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.error || `rejectSale ${res.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Buyer marks a won listing as delivered (after adding baby/animal to zoo).
|
||||
* @param {string} listingId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function deliverSale(listingId) {
|
||||
const res = await signedFetch("POST", `/api/sales/${listingId}/deliver`);
|
||||
if (!res.ok) {
|
||||
const err = await res.json().catch(() => ({}));
|
||||
throw new Error(err.error || `deliverSale ${res.status}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user