77 lines
2.7 KiB
TypeScript
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 };
|
|
}
|