201 lines
4.9 KiB
TypeScript
201 lines
4.9 KiB
TypeScript
import { compare, hash } from "bcryptjs";
|
|
import { cookies } from "next/headers";
|
|
import { randomUUID } from "node:crypto";
|
|
|
|
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("-", "");
|
|
}
|
|
|
|
function getCookieOptions() {
|
|
return {
|
|
httpOnly: true,
|
|
sameSite: "lax" as const,
|
|
secure: process.env.NODE_ENV === "production",
|
|
path: "/",
|
|
maxAge: CAREGIVER_SESSION_MAX_AGE,
|
|
};
|
|
}
|
|
|
|
function normalizeRedirectPath(redirectTo?: string) {
|
|
if (!redirectTo || !redirectTo.startsWith("/") || redirectTo.startsWith("//")) {
|
|
return "/";
|
|
}
|
|
|
|
return redirectTo;
|
|
}
|
|
|
|
function normalizeCaregiverUsername(rawValue: string) {
|
|
return rawValue.trim().toLowerCase();
|
|
}
|
|
|
|
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 `/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 getCurrentSessionToken();
|
|
|
|
if (!sessionToken) {
|
|
return null;
|
|
}
|
|
|
|
const session = await prisma.caregiverSession.findUnique({
|
|
where: { sessionToken },
|
|
include: {
|
|
caregiver: true,
|
|
},
|
|
});
|
|
|
|
if (!session) {
|
|
return null;
|
|
}
|
|
|
|
return session.caregiver;
|
|
}
|
|
|
|
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);
|
|
} |