添加账号系统
This commit is contained in:
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user