89 lines
2.4 KiB
TypeScript
89 lines
2.4 KiB
TypeScript
import { randomBytes, scrypt } from "node:crypto";
|
|
import { jwtVerify, SignJWT } from "@panva/jose";
|
|
|
|
import {
|
|
type AuthPayload,
|
|
InvitePayload,
|
|
isAuthPayload,
|
|
isInvitePayload,
|
|
} from "../model/interfaces.ts";
|
|
import { JWT_SECRET } from "../config.ts";
|
|
|
|
if (!JWT_SECRET) {
|
|
throw new Error(
|
|
"GERBEUR_JWT_SECRET environment variable is required. Generate one with: openssl rand -hex 32",
|
|
);
|
|
}
|
|
const JWT_KEY = new TextEncoder().encode(JWT_SECRET);
|
|
|
|
// ── Invite tokens ─────────────────────────────────────────────────────────────
|
|
|
|
export async function createInviteToken(inviterId: string): Promise<string> {
|
|
return await new SignJWT({ purpose: "invite", inviterId })
|
|
.setProtectedHeader({ alg: "HS256" })
|
|
.setJti(crypto.randomUUID())
|
|
.setExpirationTime("7d")
|
|
.sign(JWT_KEY);
|
|
}
|
|
|
|
export async function verifyInviteToken(
|
|
token: string,
|
|
): Promise<InvitePayload | null> {
|
|
try {
|
|
const { payload } = await jwtVerify(token, JWT_KEY);
|
|
if (!isInvitePayload(payload)) return null;
|
|
return payload as InvitePayload;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function createJWT(
|
|
payload: Omit<AuthPayload, "exp">,
|
|
): Promise<string> {
|
|
return await new SignJWT(payload)
|
|
.setProtectedHeader({ alg: "HS256" })
|
|
.setExpirationTime("24h")
|
|
.sign(JWT_KEY);
|
|
}
|
|
|
|
export async function verifyJWT(token: string): Promise<AuthPayload | null> {
|
|
try {
|
|
const { payload } = await jwtVerify(token, JWT_KEY);
|
|
|
|
if (!isAuthPayload(payload)) {
|
|
return null;
|
|
}
|
|
|
|
return payload;
|
|
} catch (err) {
|
|
console.error("JWT verification failed:", err);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function hashPassword(password: string): Promise<string> {
|
|
const salt = randomBytes(16).toString("hex");
|
|
|
|
return new Promise((resolve, reject) => {
|
|
scrypt(password, salt, 64, (err, derivedKey) => {
|
|
if (err) reject(err);
|
|
else resolve(`${derivedKey.toString("hex")}.${salt}`);
|
|
});
|
|
});
|
|
}
|
|
|
|
export function verifyPassword(
|
|
password: string,
|
|
storedHash: string,
|
|
): Promise<boolean> {
|
|
const [hash, salt] = storedHash.split(".");
|
|
|
|
return new Promise((resolve, reject) => {
|
|
scrypt(password, salt, 64, (err, derivedKey) => {
|
|
if (err) reject(err);
|
|
else resolve(hash === derivedKey.toString("hex"));
|
|
});
|
|
});
|
|
}
|