feat(frontend): email+password auth, /signin + /signup pages, marketing consolidation, onboarding workspace naming + full data persistence
This commit is contained in:
76
vibn-frontend/lib/auth/password.ts
Normal file
76
vibn-frontend/lib/auth/password.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user