Files
moteva/frontend/src/infrastructure/authGateway.ts
T
root 263748af4a feat(api): add Lovart-style project and agent thread endpoints
Add canva/canda-compatible routes for lightweight project open/save and
agent thread history:
- POST /canva/project/queryProject, saveProject
- POST /canva/agent/queryAgentLastThread, agentThreadList
- GET /canda/chat-history, /canda/thread/status

Canvas save now accepts a compressed SHAKKERDATA:// snapshot payload,
preserves connections, and returns a SaveProject response; blank
successful image nodes are backfilled from generated artifacts. Frontend
designGateway adopts the new save/query flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 19:10:08 +08:00

220 lines
6.1 KiB
TypeScript

import type { AccountDevice, AuthCodeResponse, AuthOptions, AuthSession, AuthUser, StoredAuthSession } from "@/domain/auth";
import { createApiFailure, createNetworkFailure } from "@/infrastructure/userMessages";
const sessionStorageKey = "img-infinite-canvas.auth.session";
const accessTokenCookie = "usertoken";
const refreshTokenCookie = "refreshToken";
async function request<T>(path: string, body: unknown): Promise<T> {
let response: Response;
try {
response = await fetch(path, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(body)
});
} catch (err) {
throw createNetworkFailure("auth", err);
}
if (!response.ok) {
const payload = await response.json().catch(() => ({ error: response.statusText }));
throw createApiFailure("auth", response, payload);
}
return response.json() as Promise<T>;
}
async function get<T>(path: string): Promise<T> {
let response: Response;
try {
response = await fetch(path, { cache: "no-store" });
} catch (err) {
throw createNetworkFailure("auth", err);
}
if (!response.ok) {
const payload = await response.json().catch(() => ({ error: response.statusText }));
throw createApiFailure("auth", response, payload);
}
return response.json() as Promise<T>;
}
async function apiRequest<T>(path: string, init: RequestInit = {}): Promise<T> {
const headers = new Headers(init.headers);
if (!(init.body instanceof FormData)) {
headers.set("Content-Type", "application/json");
}
Object.entries(authHeaders()).forEach(([key, value]) => headers.set(key, value));
let response: Response;
try {
response = await fetch(path, {
...init,
headers,
cache: "no-store"
});
} catch (err) {
throw createNetworkFailure("auth", err);
}
if (!response.ok) {
const payload = await response.json().catch(() => ({ error: response.statusText }));
throw createApiFailure("auth", response, payload);
}
return response.json() as Promise<T>;
}
export const authGateway = {
options() {
return get<AuthOptions>("/api/auth/options");
},
requestGlobalEmailCode(email: string, turnstileToken: string) {
return request<AuthCodeResponse>("/api/auth/global/email-code", { email, turnstileToken });
},
loginGlobalEmailCode(email: string, code: string) {
return request<AuthSession>("/api/auth/global/email-login", { email, code });
},
loginGlobalGoogle(idToken: string) {
return request<AuthSession>("/api/auth/global/google-login", { idToken });
},
profile() {
return apiRequest<{ user: AuthUser }>("/api/account/profile");
},
updateProfile(userPatch: Partial<Pick<AuthUser, "name" | "avatarUrl">>) {
return apiRequest<{ user: AuthUser }>("/api/account/profile", {
method: "PATCH",
body: JSON.stringify(userPatch)
});
},
devices() {
return apiRequest<{ devices: AccountDevice[] }>("/api/account/devices");
},
removeDevices() {
return apiRequest<{ removed: number }>("/api/account/devices", {
method: "DELETE"
});
},
logoutAccount() {
return apiRequest<{ removed: number }>("/api/account/logout", {
method: "POST",
body: JSON.stringify({})
});
},
async uploadAvatar(file: File) {
const upload = await apiRequest<{
uploadUrl: string;
publicUrl: string;
headers: Record<string, string>;
}>("/api/assets/upload-url", {
method: "POST",
body: JSON.stringify({
fileName: file.name,
contentType: file.type || "application/octet-stream",
size: file.size
})
});
if (upload.uploadUrl && !upload.uploadUrl.includes("/memory-assets/")) {
await fetch(upload.uploadUrl, {
method: "PUT",
headers: upload.headers,
body: file
});
}
return upload.publicUrl;
}
};
export function readStoredAuthSession(): StoredAuthSession | null {
if (typeof window === "undefined") return null;
const raw = window.localStorage.getItem(sessionStorageKey);
if (!raw) return null;
try {
const session = JSON.parse(raw) as StoredAuthSession;
if (!session.accessToken || isSessionExpired(session)) {
clearStoredAuthSession();
return null;
}
return session;
} catch {
clearStoredAuthSession();
return null;
}
}
export function storeAuthSession(session: AuthSession): StoredAuthSession {
const stored = { ...session, savedAt: Date.now() };
if (typeof window !== "undefined") {
window.localStorage.setItem(sessionStorageKey, JSON.stringify(stored));
setCookie(accessTokenCookie, session.accessToken, session.expiresIn);
if (session.refreshToken && session.refreshExpiresIn) {
setCookie(refreshTokenCookie, session.refreshToken, session.refreshExpiresIn);
}
}
return stored;
}
export function updateStoredAuthUser(userPatch: Partial<AuthUser>): StoredAuthSession | null {
const current = readStoredAuthSession();
if (!current) return null;
const stored = {
...current,
user: {
...current.user,
...userPatch
}
};
if (typeof window !== "undefined") {
window.localStorage.setItem(sessionStorageKey, JSON.stringify(stored));
}
return stored;
}
export function clearStoredAuthSession() {
if (typeof window === "undefined") return;
window.localStorage.removeItem(sessionStorageKey);
expireCookie(accessTokenCookie);
expireCookie(refreshTokenCookie);
}
export function getAuthAccessToken() {
return readStoredAuthSession()?.accessToken ?? "";
}
export function authHeaders() {
const token = getAuthAccessToken();
if (!token) return {};
return {
Authorization: `Bearer ${token}`,
token
} satisfies Record<string, string>;
}
function isSessionExpired(session: StoredAuthSession) {
const expiresIn = Number(session.expiresIn || 0);
return expiresIn > 0 && session.savedAt + expiresIn * 1000 <= Date.now() + 30_000;
}
function setCookie(name: string, value: string, maxAge: number) {
document.cookie = `${name}=${encodeURIComponent(value)}; Path=/; Max-Age=${Math.max(0, Math.floor(maxAge))}; SameSite=Lax`;
}
function expireCookie(name: string) {
document.cookie = `${name}=; Path=/; Max-Age=0; SameSite=Lax`;
}