Repository navigation
Expand file tree
/
Copy pathauth.ts
More file actions
98 lines (87 loc) · 2.98 KB
/
Copy pathauth.ts
File metadata and controls
98 lines (87 loc) · 2.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// ============================================================
// SchemeAI server-side authentication
// - scrypt password hashing with per-user salt (Node crypto)
// - DB-backed opaque session tokens in httpOnly, sameSite cookies
// - timing-safe comparisons
// ============================================================
import { randomBytes, scryptSync, timingSafeEqual, createHash } from "crypto";
import { cookies } from "next/headers";
import { db } from "@/lib/db";
const SESSION_COOKIE = "schemeai_session";
const SESSION_TTL_MS = 1000 * 60 * 60 * 24 * 14; // 14 days
export function hashPassword(password: string): string {
const salt = randomBytes(16).toString("hex");
const hash = scryptSync(password, salt, 64).toString("hex");
return `${salt}:${hash}`;
}
export function verifyPassword(password: string, stored: string): boolean {
const [salt, hash] = stored.split(":");
if (!salt || !hash) return false;
const candidate = scryptSync(password, salt, 64);
const expected = Buffer.from(hash, "hex");
if (candidate.length !== expected.length) return false;
return timingSafeEqual(candidate, expected);
}
function newToken(): string {
return randomBytes(32).toString("hex");
}
function tokenHash(token: string): string {
// store a hash of the token, not the raw token
return createHash("sha256").update(token).digest("hex");
}
export async function createSession(userId: string): Promise<void> {
const token = newToken();
const expiresAt = new Date(Date.now() + SESSION_TTL_MS);
await db.session.create({ data: { token: tokenHash(token), userId, expiresAt } });
const jar = await cookies();
jar.set(SESSION_COOKIE, token, {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/",
expires: expiresAt,
});
}
export async function destroySession(): Promise<void> {
const jar = await cookies();
const token = jar.get(SESSION_COOKIE)?.value;
if (token) {
await db.session.deleteMany({ where: { token: tokenHash(token) } }).catch(() => undefined);
}
jar.delete(SESSION_COOKIE);
}
export interface SessionUser {
id: string;
email: string;
name: string | null;
role: string;
}
export async function getSessionUser(): Promise<SessionUser | null> {
try {
const jar = await cookies();
const token = jar.get(SESSION_COOKIE)?.value;
if (!token) return null;
const session = await db.session.findUnique({
where: { token: tokenHash(token) },
include: { user: true },
});
if (!session) return null;
if (session.expiresAt < new Date()) {
await db.session.delete({ where: { id: session.id } }).catch(() => undefined);
return null;
}
return {
id: session.user.id,
email: session.user.email,
name: session.user.name,
role: session.user.role,
};
} catch {
return null;
}
}
export async function requireAdmin(): Promise<SessionUser | null> {
const u = await getSessionUser();
if (!u || u.role !== "admin") return null;
return u;
}