Files

77 lines
2.7 KiB
TypeScript

import { scrypt, randomBytes, randomUUID, timingSafeEqual } from "crypto";
import { promisify } from "util";
import { query } from "@/lib/db-postgres";
const scryptAsync = promisify(scrypt) as (
password: string,
salt: string,
keylen: number,
) => Promise<Buffer>;
/**
* Hash a password with scrypt (Node stdlib — no native deps, Alpine-safe).
* Format: `scrypt$<saltHex>$<keyHex>`.
*/
export async function hashPassword(password: string): Promise<string> {
const salt = randomBytes(16).toString("hex");
const derived = await scryptAsync(password, salt, 64);
return `scrypt$${salt}$${derived.toString("hex")}`;
}
export async function verifyPassword(
password: string,
stored: string,
): Promise<boolean> {
const parts = stored.split("$");
if (parts.length !== 3 || parts[0] !== "scrypt") return false;
const [, salt, keyHex] = parts;
const keyBuf = Buffer.from(keyHex, "hex");
const derived = await scryptAsync(password, salt, 64);
return keyBuf.length === derived.length && timingSafeEqual(keyBuf, derived);
}
// ── Session cookie ───────────────────────────────────────────────────────────
// Must match the cookie NextAuth issues for OAuth (see lib/auth/authOptions.ts)
// so getServerSession reads sessions created here exactly the same way.
const nextAuthUrl = (process.env.NEXTAUTH_URL ?? "").trim();
const isLocalNextAuth =
nextAuthUrl.startsWith("http://localhost") ||
nextAuthUrl.startsWith("http://127.0.0.1") ||
(process.env.NODE_ENV === "development" && !nextAuthUrl);
export const SESSION_COOKIE_NAME = isLocalNextAuth
? "next-auth.session-token"
: "__Secure-next-auth.session-token";
const SESSION_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
export function sessionCookieOptions(expires: Date) {
return {
httpOnly: true,
sameSite: "lax" as const,
path: "/",
secure: !isLocalNextAuth,
expires,
...(isLocalNextAuth ? {} : { domain: ".vibnai.com" }),
};
}
/**
* Create a database-backed NextAuth session row for a user and return the token
* to set as the session cookie. This is the same `sessions` table the Prisma
* adapter uses for OAuth, so the resulting session works everywhere
* (`authSession()`, `useSession()`, sign-out, etc.).
*/
export async function createDbSession(
userId: string,
): Promise<{ token: string; expires: Date }> {
const token = randomBytes(32).toString("hex");
const expires = new Date(Date.now() + SESSION_MAX_AGE_MS);
await query(
`INSERT INTO sessions (id, session_token, user_id, expires)
VALUES ($1, $2, $3, $4)`,
[randomUUID(), token, userId, expires],
);
return { token, expires };
}