You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

129 lines
3.8 KiB

import { dbGlobal } from "drizzle-pkg/lib/db";
import { users } from "drizzle-pkg/lib/schema/auth";
import { eq } from "drizzle-orm";
import { visibilitySchema, type Visibility } from "#server/constants/visibility";
import { syncProfileMediaRefs } from "#server/service/media";
import { z } from "zod";
const publicSlugValue = z
.string()
.regex(/^[a-z0-9]([a-z0-9-]{0,28}[a-z0-9])?$/)
.min(3)
.max(30);
/** Nuxt UI / Iconify icon id, e.g. i-lucide-heart, i-simple-icons-github */
const socialLinkIconSchema = z.preprocess(
(v) => {
if (v === "" || v === null || v === undefined) {
return undefined;
}
return typeof v === "string" ? v.trim() : v;
},
z
.string()
.min(4)
.max(120)
.regex(/^i-[a-z0-9]+(?:-[a-z0-9]+)*$/, "icon 须为小写 Nuxt Icon 名,例如 i-lucide-mail")
.optional(),
);
const linkItemSchema = z.object({
label: z.string().min(1).max(80),
url: z.string().url().max(2000),
visibility: visibilitySchema,
icon: socialLinkIconSchema.optional(),
});
const discoverLocationPatchSchema = z.union([
z.null(),
z.string().max(200).transform((s) => {
const t = s.trim();
return t.length > 0 ? t : null;
}),
]);
export type SocialLinkItem = z.infer<typeof linkItemSchema>;
export async function getProfileRow(userId: number) {
const [row] = await dbGlobal.select().from(users).where(eq(users.id, userId)).limit(1);
return row ?? null;
}
export async function updateProfile(
userId: number,
patch: {
nickname?: string | null;
avatar?: string | null;
avatarVisibility?: Visibility;
bioMarkdown?: string | null;
bioVisibility?: Visibility;
socialLinks?: SocialLinkItem[];
publicSlug?: string | null;
discoverVisible?: boolean;
discoverLocation?: string | null;
discoverShowLocation?: boolean;
},
) {
const updates: Record<string, unknown> = {};
if (patch.nickname !== undefined) {
updates.nickname = patch.nickname?.trim() || null;
}
if (patch.avatar !== undefined) {
updates.avatar = patch.avatar?.trim() || null;
}
if (patch.avatarVisibility !== undefined) {
updates.avatarVisibility = visibilitySchema.parse(patch.avatarVisibility);
}
if (patch.bioMarkdown !== undefined) {
updates.bioMarkdown = patch.bioMarkdown?.trim() || null;
}
if (patch.bioVisibility !== undefined) {
updates.bioVisibility = visibilitySchema.parse(patch.bioVisibility);
}
if (patch.socialLinks !== undefined) {
const parsed = z.array(linkItemSchema).max(50).parse(patch.socialLinks);
updates.socialLinksJson = JSON.stringify(parsed);
}
if (patch.publicSlug !== undefined) {
if (patch.publicSlug === null || patch.publicSlug === "") {
updates.publicSlug = null;
} else {
updates.publicSlug = publicSlugValue.parse(patch.publicSlug.trim().toLowerCase());
}
}
if (patch.discoverVisible !== undefined) {
updates.discoverVisible = patch.discoverVisible;
}
if (patch.discoverLocation !== undefined) {
updates.discoverLocation = discoverLocationPatchSchema.parse(patch.discoverLocation);
}
if (patch.discoverShowLocation !== undefined) {
updates.discoverShowLocation = patch.discoverShowLocation;
}
if (Object.keys(updates).length === 0) {
return getProfileRow(userId);
}
const syncMedia = patch.bioMarkdown !== undefined || patch.avatar !== undefined;
await dbGlobal.update(users).set(updates as never).where(eq(users.id, userId));
const row = await getProfileRow(userId);
if (row && syncMedia) {
await syncProfileMediaRefs(userId, row.bioMarkdown ?? null, row.avatar ?? null);
}
return row;
}
export function parseSocialLinksJson(raw: string | null | undefined): SocialLinkItem[] {
if (!raw || raw === "[]") {
return [];
}
try {
const data = JSON.parse(raw) as unknown;
return z.array(linkItemSchema).parse(data);
} catch {
return [];
}
}