78 lines
1.9 KiB
TypeScript
78 lines
1.9 KiB
TypeScript
import "server-only";
|
|
|
|
import { randomBytes } from "crypto";
|
|
import { cookies } from "next/headers";
|
|
import type { User } from "@prisma/client";
|
|
import { prisma } from "@/lib/db";
|
|
import { SESSION_COOKIE, SESSION_MAX_AGE_DAYS } from "./constants";
|
|
|
|
function sessionExpiry() {
|
|
return new Date(Date.now() + SESSION_MAX_AGE_DAYS * 24 * 60 * 60 * 1000);
|
|
}
|
|
|
|
export async function createSession(userId: string) {
|
|
const token = randomBytes(32).toString("hex");
|
|
const expiresAt = sessionExpiry();
|
|
|
|
await prisma.session.create({
|
|
data: { userId, token, expiresAt },
|
|
});
|
|
|
|
const cookieStore = await cookies();
|
|
cookieStore.set(SESSION_COOKIE, token, {
|
|
httpOnly: true,
|
|
sameSite: "lax",
|
|
secure: process.env.NODE_ENV === "production",
|
|
path: "/",
|
|
expires: expiresAt,
|
|
});
|
|
|
|
return token;
|
|
}
|
|
|
|
export async function destroySession() {
|
|
const cookieStore = await cookies();
|
|
const token = cookieStore.get(SESSION_COOKIE)?.value;
|
|
if (token) {
|
|
await prisma.session.deleteMany({ where: { token } }).catch(() => undefined);
|
|
cookieStore.delete(SESSION_COOKIE);
|
|
}
|
|
}
|
|
|
|
export async function getSessionUser(): Promise<User | null> {
|
|
const cookieStore = await cookies();
|
|
const token = cookieStore.get(SESSION_COOKIE)?.value;
|
|
if (!token) return null;
|
|
|
|
const session = await prisma.session.findUnique({
|
|
where: { token },
|
|
include: { user: true },
|
|
});
|
|
|
|
if (!session || session.expiresAt < new Date()) {
|
|
if (session) {
|
|
await prisma.session.delete({ where: { id: session.id } }).catch(() => undefined);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
return session.user;
|
|
}
|
|
|
|
export async function requireSessionUser(): Promise<User> {
|
|
const user = await getSessionUser();
|
|
if (!user) {
|
|
throw new AuthError("請先登入", 401);
|
|
}
|
|
return user;
|
|
}
|
|
|
|
export class AuthError extends Error {
|
|
status: number;
|
|
|
|
constructor(message: string, status = 401) {
|
|
super(message);
|
|
this.name = "AuthError";
|
|
this.status = status;
|
|
}
|
|
} |