Initial scaffold: offertesysteem (clients, projects, quotes, quote_items)
- Next.js 15 + TypeScript + Tailwind, standalone output - SQLite schema: users, clients, projects, quotes, quote_items - HMAC-signed sessie-cookie auth - Dockerfile (Node 22 Alpine, multi-stage) - create-admin script - Doel: portal.rikxplatform.nl Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+70
@@ -0,0 +1,70 @@
|
||||
import { cookies } from "next/headers";
|
||||
import { createHmac, timingSafeEqual, randomBytes, scryptSync } from "crypto";
|
||||
import { getDb } from "./db";
|
||||
|
||||
const SESSION_SECRET = process.env.SESSION_SECRET!;
|
||||
const COOKIE_NAME = "session";
|
||||
const MAX_AGE = 60 * 60 * 24 * 7; // 7 dagen
|
||||
|
||||
type SessionPayload = { userId: number; email: string; name: string };
|
||||
|
||||
function sign(payload: SessionPayload): string {
|
||||
const data = JSON.stringify(payload);
|
||||
const encoded = Buffer.from(data).toString("base64url");
|
||||
const sig = createHmac("sha256", SESSION_SECRET).update(encoded).digest("base64url");
|
||||
return `${encoded}.${sig}`;
|
||||
}
|
||||
|
||||
function verify(token: string): SessionPayload | null {
|
||||
const [encoded, sig] = token.split(".");
|
||||
if (!encoded || !sig) return null;
|
||||
const expected = createHmac("sha256", SESSION_SECRET).update(encoded).digest("base64url");
|
||||
try {
|
||||
if (!timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(Buffer.from(encoded, "base64url").toString());
|
||||
}
|
||||
|
||||
export async function getSession(): Promise<SessionPayload | null> {
|
||||
const jar = await cookies();
|
||||
const token = jar.get(COOKIE_NAME)?.value;
|
||||
if (!token) return null;
|
||||
return verify(token);
|
||||
}
|
||||
|
||||
export async function createSession(payload: SessionPayload) {
|
||||
const jar = await cookies();
|
||||
jar.set(COOKIE_NAME, sign(payload), {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
maxAge: MAX_AGE,
|
||||
path: "/",
|
||||
});
|
||||
}
|
||||
|
||||
export async function destroySession() {
|
||||
const jar = await cookies();
|
||||
jar.delete(COOKIE_NAME);
|
||||
}
|
||||
|
||||
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(":");
|
||||
const attempt = scryptSync(password, salt, 64).toString("hex");
|
||||
return timingSafeEqual(Buffer.from(hash, "hex"), Buffer.from(attempt, "hex"));
|
||||
}
|
||||
|
||||
export function getUserByEmail(email: string) {
|
||||
const db = getDb();
|
||||
return db.prepare("SELECT * FROM users WHERE email = ?").get(email) as
|
||||
| { id: number; email: string; name: string; password_hash: string; role: string }
|
||||
| undefined;
|
||||
}
|
||||
Reference in New Issue
Block a user