Browse Source

feat(profile): add email field to user profile and validation

Introduce an email field in the user profile, allowing users to input their email address. Implement email format validation on the server side to ensure proper formatting before saving. Update the profile form to include the email input, enhancing user experience and notification capabilities.

Made-with: Cursor
main
npmrun 3 weeks ago
parent
commit
4ca171da6f
  1. 11
      app/pages/me/profile/index.vue
  2. 17
      server/api/me/profile.put.ts
  3. 4
      server/service/profile/index.ts

11
app/pages/me/profile/index.vue

@ -9,6 +9,7 @@ const toast = useToast()
type ProfileGet = { type ProfileGet = {
profile: { profile: {
email: string | null
nickname: string | null nickname: string | null
avatar: string | null avatar: string | null
avatarVisibility: string avatarVisibility: string
@ -27,6 +28,7 @@ type MeConfigGet = {
} }
const state = reactive({ const state = reactive({
email: '',
nickname: '', nickname: '',
avatar: '', avatar: '',
avatarVisibility: 'private', avatarVisibility: 'private',
@ -184,6 +186,7 @@ async function load() {
const p = profilePayload.profile const p = profilePayload.profile
const cfg = meCfgPayload.config const cfg = meCfgPayload.config
state.nickname = p.nickname ?? '' state.nickname = p.nickname ?? ''
state.email = p.email ?? ''
state.avatar = p.avatar ?? '' state.avatar = p.avatar ?? ''
state.avatarVisibility = p.avatarVisibility state.avatarVisibility = p.avatarVisibility
state.bioMarkdown = p.bioMarkdown ?? '' state.bioMarkdown = p.bioMarkdown ?? ''
@ -219,6 +222,7 @@ async function save() {
method: 'PUT', method: 'PUT',
notify: false, notify: false,
body: { body: {
email: state.email.trim() || null,
nickname: state.nickname || null, nickname: state.nickname || null,
avatar: state.avatar || null, avatar: state.avatar || null,
avatarVisibility: state.avatarVisibility, avatarVisibility: state.avatarVisibility,
@ -327,6 +331,13 @@ async function save() {
<UInput v-model="state.nickname" /> <UInput v-model="state.nickname" />
</UFormField> </UFormField>
<UFormField <UFormField
label="邮箱"
name="email"
description="用于接收评论通知与测试邮件,留空表示不接收邮件通知。"
>
<UInput v-model="state.email" type="email" placeholder="you@example.com" />
</UFormField>
<UFormField
label="公开主页顶栏名称" label="公开主页顶栏名称"
name="publicHomeHeaderTitle" name="publicHomeHeaderTitle"
description="在 /@你的 slug 页面左上角显示。留空则使用全站站点名称。" description="在 /@你的 slug 页面左上角显示。留空则使用全站站点名称。"

17
server/api/me/profile.put.ts

@ -1,10 +1,23 @@
import { updateProfile } from "#server/service/profile"; import { updateProfile } from "#server/service/profile";
import { visibilitySchema } from "#server/constants/visibility"; import { visibilitySchema } from "#server/constants/visibility";
import { isUniqueConstraintViolation } from "#server/utils/db-unique-constraint"; import { isUniqueConstraintViolation } from "#server/utils/db-unique-constraint";
import { ZodError, z } from "zod";
function normalizeEmailInput(raw: string | null | undefined) {
if (raw === undefined || raw === null) {
return raw;
}
const value = raw.trim();
if (!value) {
return null;
}
return z.string().email("邮箱格式不合法").parse(value);
}
export default defineWrappedResponseHandler(async (event) => { export default defineWrappedResponseHandler(async (event) => {
const user = await event.context.auth.requireUser(); const user = await event.context.auth.requireUser();
const body = await readBody<{ const body = await readBody<{
email?: string | null;
nickname?: string | null; nickname?: string | null;
avatar?: string | null; avatar?: string | null;
avatarVisibility?: string; avatarVisibility?: string;
@ -19,6 +32,7 @@ export default defineWrappedResponseHandler(async (event) => {
try { try {
const row = await updateProfile(user.id, { const row = await updateProfile(user.id, {
email: normalizeEmailInput(body.email),
nickname: body.nickname, nickname: body.nickname,
avatar: body.avatar, avatar: body.avatar,
avatarVisibility: avatarVisibility:
@ -37,6 +51,9 @@ export default defineWrappedResponseHandler(async (event) => {
} }
return R.success({ ok: true }); return R.success({ ok: true });
} catch (e) { } catch (e) {
if (e instanceof ZodError) {
throw createError({ statusCode: 400, statusMessage: e.issues[0]?.message ?? "参数不合法" });
}
if (isUniqueConstraintViolation(e)) { if (isUniqueConstraintViolation(e)) {
throw createError({ statusCode: 409, statusMessage: "公开链接 slug 已被占用" }); throw createError({ statusCode: 409, statusMessage: "公开链接 slug 已被占用" });
} }

4
server/service/profile/index.ts

@ -52,6 +52,7 @@ export async function getProfileRow(userId: number) {
export async function updateProfile( export async function updateProfile(
userId: number, userId: number,
patch: { patch: {
email?: string | null;
nickname?: string | null; nickname?: string | null;
avatar?: string | null; avatar?: string | null;
avatarVisibility?: Visibility; avatarVisibility?: Visibility;
@ -66,6 +67,9 @@ export async function updateProfile(
) { ) {
const updates: Record<string, unknown> = {}; const updates: Record<string, unknown> = {};
if (patch.email !== undefined) {
updates.email = patch.email?.trim() || null;
}
if (patch.nickname !== undefined) { if (patch.nickname !== undefined) {
updates.nickname = patch.nickname?.trim() || null; updates.nickname = patch.nickname?.trim() || null;
} }

Loading…
Cancel
Save