feat(web): add persistent beam avatars
Staging Backend Quality Gate / validate (push) Successful in 13m34s
Staging Backend Quality Gate / publish (push) Has been cancelled

This commit is contained in:
Jesse
2026-08-07 12:07:57 +08:00
parent e7482c49c6
commit 49f6edd7fb
11 changed files with 420 additions and 2 deletions
@@ -0,0 +1,56 @@
import { NextResponse } from "next/server";
import { createServerSupabaseClient } from "@/lib/supabase/server";
import {
beamAvatarFromProfile,
beamAvatarPatchSchema,
createBeamAvatarProfilePatch,
} from "@/lib/beam-avatar";
export const runtime = "nodejs";
function selfHostedOnly() {
return process.env.AUTH_PROVIDER?.trim() === "self-hosted";
}
export async function PATCH(request: Request) {
if (!selfHostedOnly()) {
return NextResponse.json({ error: "头像编辑仅在 staging 开放" }, { status: 404 });
}
try {
const client = await createServerSupabaseClient();
const { data: { user }, error: authError } = await client.auth.getUser();
if (authError || !user) {
return NextResponse.json({ error: "请先登录" }, { status: 401 });
}
const parsed = beamAvatarPatchSchema.safeParse(
await request.json().catch(() => null),
);
if (!parsed.success) {
return NextResponse.json({
error: "头像设置格式不正确",
details: parsed.error.flatten(),
}, { status: 400 });
}
const values = {
...createBeamAvatarProfilePatch(parsed.data),
updated_at: new Date().toISOString(),
};
const { data: profile, error } = await client
.from("profiles")
.update(values)
.eq("id", user.id)
.select("avatar_seed,avatar_palette,avatar_renderer_version")
.maybeSingle();
if (error || !profile) {
return NextResponse.json({ error: "暂时无法保存头像" }, { status: 500 });
}
return NextResponse.json({ avatar: beamAvatarFromProfile(profile) });
} catch {
return NextResponse.json({ error: "头像服务暂时不可用" }, { status: 500 });
}
}
+19
View File
@@ -8,6 +8,7 @@ import {
applyAccountProfileConcurrencyGuards,
resolveAccountBirthTimeApplicationPatch,
} from "@/lib/account-profile-patch";
import { optionalBeamAvatarFromProfile } from "@/lib/beam-avatar";
import { createAdminSupabaseClient, isAdminUser } from "@/lib/supabase/admin";
import {
isSupabaseConfigurationError,
@@ -110,6 +111,23 @@ export async function GET() {
if (profileError || !profile) {
return NextResponse.json({ error: "暂时无法读取账户余额" }, { status: 500 });
}
let avatar = null;
if (process.env.AUTH_PROVIDER?.trim() === "self-hosted") {
const { data: avatarProfile, error: avatarError } = await supabase
.from("profiles")
.select("avatar_seed,avatar_palette,avatar_renderer_version")
.eq("id", userId)
.single();
if (avatarError || !avatarProfile) {
return NextResponse.json({ error: "暂时无法读取头像" }, { status: 500 });
}
avatar = optionalBeamAvatarFromProfile(avatarProfile);
if (!avatar) {
return NextResponse.json({ error: "头像设置无效" }, { status: 500 });
}
}
const rectificationCase = resolveAccountRectificationV4Case(
Array.isArray(rectificationV4CaseRows) ? rectificationV4CaseRows : [],
) ?? resolveAccountRectificationCase(
@@ -134,6 +152,7 @@ export async function GET() {
return NextResponse.json({
user: { id: user.id, email: user.email ?? null },
avatar,
credits: profile.credits,
isAdmin,
adminUrl,