添加账号系统

This commit is contained in:
2026-04-17 12:58:38 +08:00
parent 032e207888
commit d3b03cecf2
27 changed files with 931 additions and 254 deletions
+98
View File
@@ -121,6 +121,8 @@ export async function bindDeviceToCaregiver(
const device = await ensureDeviceRegistration({ deviceUuid });
await migrateLegacyCaregiverDataForDevice(caregiverId, device.id);
await prisma.deviceBinding.upsert({
where: {
caregiverId_elderDeviceId: {
@@ -138,6 +140,102 @@ export async function bindDeviceToCaregiver(
return device;
}
async function migrateLegacyCaregiverDataForDevice(
caregiverId: string,
elderDeviceId: string,
) {
const legacyCaregivers = await prisma.caregiverAccount.findMany({
where: {
id: { not: caregiverId },
username: null,
bindings: {
some: {
elderDeviceId,
},
},
},
select: {
id: true,
},
take: 2,
});
if (legacyCaregivers.length !== 1) {
return;
}
const legacyCaregiverId = legacyCaregivers[0]?.id;
if (!legacyCaregiverId) {
return;
}
await prisma.$transaction(async (tx) => {
const legacyBindings = await tx.deviceBinding.findMany({
where: {
caregiverId: legacyCaregiverId,
},
select: {
elderDeviceId: true,
},
});
await Promise.all(
legacyBindings.map((binding) =>
tx.deviceBinding.upsert({
where: {
caregiverId_elderDeviceId: {
caregiverId,
elderDeviceId: binding.elderDeviceId,
},
},
update: {},
create: {
caregiverId,
elderDeviceId: binding.elderDeviceId,
},
}),
),
);
await tx.familyMessage.updateMany({
where: {
caregiverId: legacyCaregiverId,
},
data: {
caregiverId,
},
});
await tx.pushSubscription.updateMany({
where: {
caregiverId: legacyCaregiverId,
},
data: {
caregiverId,
},
});
await tx.deviceBinding.deleteMany({
where: {
caregiverId: legacyCaregiverId,
},
});
await tx.caregiverSession.deleteMany({
where: {
caregiverId: legacyCaregiverId,
},
});
await tx.caregiverAccount.delete({
where: {
id: legacyCaregiverId,
},
});
});
}
function parseImportance(rawImportance?: string | null) {
const normalizedImportance = rawImportance?.trim().toUpperCase();
+2 -2
View File
@@ -1,7 +1,7 @@
import { redirect } from "next/navigation";
import {
buildCaregiverSessionBootstrapPath,
buildCaregiverLoginPath,
getCaregiverSession,
} from "@/lib/session";
@@ -9,7 +9,7 @@ export async function requireCaregiverSession(redirectTo: string) {
const caregiver = await getCaregiverSession();
if (!caregiver) {
redirect(buildCaregiverSessionBootstrapPath(redirectTo));
redirect(buildCaregiverLoginPath(redirectTo));
}
return caregiver;
+153 -24
View File
@@ -1,3 +1,4 @@
import { compare, hash } from "bcryptjs";
import { cookies } from "next/headers";
import { randomUUID } from "node:crypto";
@@ -6,6 +7,8 @@ import { prisma } from "@/lib/prisma";
export const CAREGIVER_SESSION_COOKIE = "dh-caregiver-session";
const CAREGIVER_SESSION_MAX_AGE = 60 * 60 * 24 * 365;
const CAREGIVER_USERNAME_PATTERN = /^[a-z0-9][a-z0-9_-]{2,23}$/;
const CAREGIVER_PASSWORD_MIN_LENGTH = 8;
function createSessionToken() {
return randomUUID().replaceAll("-", "");
@@ -29,44 +32,170 @@ function normalizeRedirectPath(redirectTo?: string) {
return redirectTo;
}
async function upsertCaregiverByToken(sessionToken: string) {
return prisma.caregiverAccount.upsert({
where: { sessionToken },
update: {},
create: { sessionToken },
});
function normalizeCaregiverUsername(rawValue: string) {
return rawValue.trim().toLowerCase();
}
export function buildCaregiverSessionBootstrapPath(redirectTo: string) {
function parseCaregiverUsername(rawValue: string) {
const username = normalizeCaregiverUsername(rawValue);
if (!CAREGIVER_USERNAME_PATTERN.test(username)) {
throw new Error("用户名请使用 3 到 24 位字母、数字、下划线或横线。");
}
return username;
}
function parseCaregiverPassword(rawValue: string) {
if (rawValue.length < CAREGIVER_PASSWORD_MIN_LENGTH) {
throw new Error(`密码至少需要 ${CAREGIVER_PASSWORD_MIN_LENGTH} 位。`);
}
if (rawValue.length > 72) {
throw new Error("密码请控制在 72 位以内。");
}
return rawValue;
}
async function getCurrentSessionToken() {
return (await cookies()).get(CAREGIVER_SESSION_COOKIE)?.value || null;
}
async function createCaregiverSession(input: {
caregiverId: string;
userAgent?: string | null;
}) {
const currentSessionToken = await getCurrentSessionToken();
if (currentSessionToken) {
await prisma.caregiverSession.deleteMany({
where: {
sessionToken: currentSessionToken,
},
});
}
const sessionToken = createSessionToken();
await prisma.caregiverSession.create({
data: {
caregiverId: input.caregiverId,
sessionToken,
userAgent: input.userAgent?.trim() || null,
lastSeenAt: new Date(),
},
});
(await cookies()).set(CAREGIVER_SESSION_COOKIE, sessionToken, getCookieOptions());
}
export function buildCaregiverLoginPath(redirectTo: string) {
const normalizedRedirect = normalizeRedirectPath(redirectTo);
return `/api/family/session?redirectTo=${encodeURIComponent(normalizedRedirect)}`;
return `/login?redirectTo=${encodeURIComponent(normalizedRedirect)}`;
}
export function getCaregiverDisplayName(input: {
username?: string | null;
nickname?: string | null;
}) {
return input.nickname?.trim() || input.username?.trim() || "临时账号";
}
export function sanitizeCaregiverRedirectPath(redirectTo?: string | null) {
return normalizeRedirectPath(redirectTo || undefined);
}
export async function getCaregiverSession() {
const sessionToken = (await cookies()).get(CAREGIVER_SESSION_COOKIE)?.value;
const sessionToken = await getCurrentSessionToken();
if (!sessionToken) {
return null;
}
return upsertCaregiverByToken(sessionToken);
}
const session = await prisma.caregiverSession.findUnique({
where: { sessionToken },
include: {
caregiver: true,
},
});
export async function getOrCreateCaregiverSession() {
const cookieStore = await cookies();
let sessionToken = cookieStore.get(CAREGIVER_SESSION_COOKIE)?.value;
if (!sessionToken) {
sessionToken = createSessionToken();
cookieStore.set(CAREGIVER_SESSION_COOKIE, sessionToken, getCookieOptions());
if (!session) {
return null;
}
return upsertCaregiverByToken(sessionToken);
return session.caregiver;
}
export function createCaregiverSessionCookieValue() {
return {
sessionToken: createSessionToken(),
options: getCookieOptions(),
};
export async function registerCaregiverAccount(input: {
username: string;
password: string;
userAgent?: string | null;
}) {
const username = parseCaregiverUsername(input.username);
const password = parseCaregiverPassword(input.password);
const existingCaregiver = await prisma.caregiverAccount.findUnique({
where: { username },
});
if (existingCaregiver) {
throw new Error("这个用户名已经被注册了,请直接登录。");
}
const caregiver = await prisma.caregiverAccount.create({
data: {
username,
passwordHash: await hash(password, 10),
},
});
await createCaregiverSession({
caregiverId: caregiver.id,
userAgent: input.userAgent,
});
return caregiver;
}
export async function loginCaregiverAccount(input: {
username: string;
password: string;
userAgent?: string | null;
}) {
const username = parseCaregiverUsername(input.username);
const password = parseCaregiverPassword(input.password);
const caregiver = await prisma.caregiverAccount.findUnique({
where: { username },
});
if (!caregiver?.passwordHash) {
throw new Error("用户名或密码不正确。");
}
const passwordMatched = await compare(password, caregiver.passwordHash);
if (!passwordMatched) {
throw new Error("用户名或密码不正确。");
}
await createCaregiverSession({
caregiverId: caregiver.id,
userAgent: input.userAgent,
});
return caregiver;
}
export async function clearCurrentCaregiverSession() {
const sessionToken = await getCurrentSessionToken();
if (sessionToken) {
await prisma.caregiverSession.deleteMany({
where: {
sessionToken,
},
});
}
(await cookies()).delete(CAREGIVER_SESSION_COOKIE);
}