From d7cc6b0141ae801b4504cb0785c80792a57fb09c Mon Sep 17 00:00:00 2001 From: zetaloop Date: Sat, 25 Apr 2026 14:13:45 +0800 Subject: [PATCH] feat(api): add account mutation clients --- lib/api/auth.ts | 6 +++++ lib/api/files.ts | 67 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/api/index.ts | 6 ++--- lib/api/users.ts | 24 +++++++++++++++-- 4 files changed, 98 insertions(+), 5 deletions(-) diff --git a/lib/api/auth.ts b/lib/api/auth.ts index 25e56c5..4f8e24c 100644 --- a/lib/api/auth.ts +++ b/lib/api/auth.ts @@ -54,6 +54,12 @@ export async function login(input: LoginInput): Promise { return res.user } +export async function logout(): Promise { + await httpJson("/api/v1/auth/logout", { + method: "POST", + }) +} + export async function resetPassword(input: { email: string vcode: string diff --git a/lib/api/files.ts b/lib/api/files.ts index 498e0df..a04c984 100644 --- a/lib/api/files.ts +++ b/lib/api/files.ts @@ -1,3 +1,36 @@ +import { isApiError } from "@/lib/errors" + +export type UploadFileType = "avatar" | "chat" | "post" | "verification" | "dispute" + +function getCookieValue(name: string): string | null { + if (typeof document === "undefined") return null + if (!document.cookie) return null + + for (const part of document.cookie.split("; ")) { + if (part.startsWith(`${name}=`)) return part.slice(name.length + 1) + } + return null +} + +async function readJsonBody(res: Response): Promise<{ json: unknown | null; text: string }> { + const text = await res.text() + if (!text) return { json: null, text: "" } + + try { + return { json: JSON.parse(text) as unknown, text } + } catch { + return { json: null, text } + } +} + +function messageFromJson(json: unknown): string | undefined { + if (typeof json !== "object" || json === null) return undefined + const value = json as { message?: unknown; msg?: unknown } + if (typeof value.message === "string") return value.message + if (typeof value.msg === "string") return value.msg + return undefined +} + export async function getFileById(fileId: string): Promise { const res = await fetch(`/api/v1/files?key=${encodeURIComponent(fileId)}`) @@ -31,3 +64,37 @@ export async function getFileById(fileId: string): Promise { throw { code, msg } } + +export async function uploadFile(file: File, type: UploadFileType): Promise { + const formData = new FormData() + formData.set("type", type) + formData.set("file", file) + + const headers = new Headers() + const xsrfToken = getCookieValue("__Host-XSRF-TOKEN") + if (xsrfToken) headers.set("XSRF-TOKEN", xsrfToken) + + const res = await fetch("/api/v1/upload", { + method: "POST", + headers, + body: formData, + }) + + const { json, text } = await readJsonBody(res) + + if (res.ok) { + if (typeof json === "object" && json !== null) { + const value = json as { url?: unknown } + if (typeof value.url === "string") return value.url + } + throw { code: 500, msg: "上传响应缺少文件地址" } + } + + if (res.status === 401) throw new Error("UNAUTHORIZED") + if (isApiError(json)) throw json + + throw { + code: res.status, + msg: messageFromJson(json) ?? (text || res.statusText || "Request failed"), + } +} diff --git a/lib/api/index.ts b/lib/api/index.ts index 5fd50cf..6d932fd 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -1,4 +1,4 @@ -export { login, register, resetPassword } from "./auth" +export { login, logout, register, resetPassword } from "./auth" export { sendForgotPasswordCode } from "./auth-extra" export { getChatSessionById, listChatMessages, listChatSessions } from "./chat" export { requestWithAuth } from "./client" @@ -6,7 +6,7 @@ export { addComment, listCommentsByPost, toggleCommentLike } from "./comments" export { getDisputeByOrderId, listDisputes } from "./disputes" export { sendEmailVerificationCode } from "./email" export { isFavorited, listFavorites } from "./favorites" -export { getFileById } from "./files" +export { getFileById, uploadFile } from "./files" export { getGameById, listGames } from "./games" export { listNotifications, markNotificationAsRead } from "./notifications" export { getOrderById, listOrders, listOrdersByConsumer } from "./orders" @@ -16,4 +16,4 @@ export { listReviews, listReviewsByOrder, listReviewsByTargetUser } from "./revi export { getServiceById, listServices, listServicesByPlayer } from "./services" export { getShopById, getShopByOwnerId, listShops } from "./shops" export { getWalletBalance, listWalletTransactions } from "./transactions" -export { getCurrentUserForLogin, getUserById } from "./users" +export { getCurrentUserForLogin, getUserById, switchCurrentRole, updateCurrentUser } from "./users" diff --git a/lib/api/users.ts b/lib/api/users.ts index a8e982a..a512bba 100644 --- a/lib/api/users.ts +++ b/lib/api/users.ts @@ -1,10 +1,16 @@ import { httpJson } from "@/lib/api/http" import { isApiError } from "@/lib/errors" -import type { User } from "@/lib/types" +import type { User, UserRole } from "@/lib/types" + +export type UpdateCurrentUserInput = { + nickname?: string + avatar?: string + bio?: string +} export async function getUserById(userId: string): Promise { try { - return await httpJson(`/api/v1/users/${userId}`) + return await httpJson(`/api/v1/users/${encodeURIComponent(userId)}`) } catch (err) { if (isApiError(err) && err.code === 404) return undefined throw err @@ -14,3 +20,17 @@ export async function getUserById(userId: string): Promise { export async function getCurrentUserForLogin(): Promise { return httpJson("/api/v1/users/me") } + +export async function updateCurrentUser(input: UpdateCurrentUserInput): Promise { + return httpJson("/api/v1/users/me", { + method: "PUT", + json: input, + }) +} + +export async function switchCurrentRole(role: UserRole): Promise { + await httpJson("/api/v1/users/me/switch-role", { + method: "POST", + json: { role }, + }) +}