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.
109 lines
3.2 KiB
109 lines
3.2 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(),
|
|
});
|
|
|
|
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;
|
|
},
|
|
) {
|
|
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 (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 [];
|
|
}
|
|
}
|
|
|