diff --git a/frontend/src/app/api/account/route.ts b/frontend/src/app/api/account/route.ts index 13444e88..d64bf8be 100644 --- a/frontend/src/app/api/account/route.ts +++ b/frontend/src/app/api/account/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server"; -import { isAdminEmail } from "@/lib/supabase/admin"; +import { createAdminSupabaseClient, isAdminEmail } from "@/lib/supabase/admin"; import { isSupabaseConfigurationError, } from "@/lib/supabase/config"; @@ -7,6 +7,27 @@ import { createServerSupabaseClient } from "@/lib/supabase/server"; export const runtime = "nodejs"; +type ProfilePatchPayload = { + name?: unknown; + birth_date?: unknown; + birth_time?: unknown; + country_code?: unknown; + province_code?: unknown; + city_code?: unknown; + district_code?: unknown; + latitude?: unknown; + longitude?: unknown; + timezone_offset?: unknown; +}; + +function nullableString(value: unknown) { + return typeof value === "string" && value.trim() ? value.trim() : null; +} + +function nullableNumber(value: unknown) { + return typeof value === "number" && Number.isFinite(value) ? value : null; +} + export async function GET() { try { const supabase = await createServerSupabaseClient(); @@ -38,3 +59,50 @@ export async function GET() { return NextResponse.json({ error: "账户服务暂时不可用" }, { status: 500 }); } } + +export async function PATCH(request: Request) { + try { + const supabase = await createServerSupabaseClient(); + const { data: { user }, error: authError } = await supabase.auth.getUser(); + + if (authError || !user) { + return NextResponse.json({ error: "请先登录" }, { status: 401 }); + } + + const payload = await request.json().catch(() => null) as ProfilePatchPayload | null; + if (!payload || typeof payload !== "object") { + return NextResponse.json({ error: "账户资料格式不正确" }, { status: 400 }); + } + + const admin = createAdminSupabaseClient(); + const { data, error } = await admin + .from("profiles") + .upsert({ + id: user.id, + name: nullableString(payload.name), + birth_date: nullableString(payload.birth_date), + birth_time: nullableString(payload.birth_time), + country_code: nullableString(payload.country_code), + province_code: nullableString(payload.province_code), + city_code: nullableString(payload.city_code), + district_code: nullableString(payload.district_code), + latitude: nullableNumber(payload.latitude), + longitude: nullableNumber(payload.longitude), + timezone_offset: nullableNumber(payload.timezone_offset), + updated_at: new Date().toISOString(), + }, { onConflict: "id" }) + .select("id") + .single(); + + if (error || !data) { + return NextResponse.json({ error: "暂时无法保存账户资料" }, { status: 500 }); + } + + return NextResponse.json({ ok: true }); + } catch (error) { + if (isSupabaseConfigurationError(error)) { + return NextResponse.json({ error: "Supabase 尚未配置", code: "SUPABASE_NOT_CONFIGURED" }, { status: 503 }); + } + return NextResponse.json({ error: "账户服务暂时不可用" }, { status: 500 }); + } +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 2e0b62e1..0d278c12 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -1320,10 +1320,10 @@ export default function Home() { if (!account) throw new Error("账户尚未加载完成"); if (process.env.NODE_ENV === "development" && uiPreview.current) return; const birthPlace = selectedBirthPlace(nextProfile); - const { data, error } = await createBrowserSupabaseClient() - .from("profiles") - .upsert({ - id: account.user.id, + const response = await fetch("/api/account", { + method: "PATCH", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ name: nextProfile.name.trim() || null, birth_date: nextProfile.date || null, birth_time: nextProfile.time || null, @@ -1334,12 +1334,12 @@ export default function Home() { latitude: birthPlace?.lat ?? null, longitude: birthPlace?.lon ?? null, timezone_offset: birthPlace?.tz ?? null, - updated_at: new Date().toISOString(), - }, { onConflict: "id" }) - .select("id") - .maybeSingle(); - if (error) throw error; - if (!data) throw new Error("账户档案不存在,请重新登录后再试。"); + }), + }); + if (!response.ok) { + const payload = await response.json().catch(() => null) as { error?: string } | null; + throw new Error(payload?.error || "账户资料暂时无法保存。"); + } await saveCloudChartProfile({ ...buildSelfChartRecord(nextProfile), updatedAt: timestamp() }).catch(() => null); } diff --git a/frontend/tests/profile-persistence.test.ts b/frontend/tests/profile-persistence.test.ts index 625301e9..255c8b0a 100644 --- a/frontend/tests/profile-persistence.test.ts +++ b/frontend/tests/profile-persistence.test.ts @@ -4,5 +4,11 @@ import test from "node:test"; test("upserts a missing profile when saving account details", () => { const source = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8"); + assert.match(source, /fetch\("\/api\/account",\s*\{\s*method:\s*"PATCH"/s); +}); + +test("account route upserts profiles with the server admin client", () => { + const source = readFileSync(new URL("../src/app/api/account/route.ts", import.meta.url), "utf8"); + assert.match(source, /createAdminSupabaseClient\(\)/); assert.match(source, /\.from\("profiles"\)\s*\.upsert\(/); });