import { SignJWT, jwtVerify } from 'jose' import { randomBytes, createHash } from 'crypto' import { cookies } from 'next/headers' import pool from './db' const COOKIE_NAME = 'pitch_session' const JWT_EXPIRY = '1h' const SESSION_EXPIRY_HOURS = 24 function getJwtSecret() { const secret = process.env.PITCH_JWT_SECRET if (!secret) throw new Error('PITCH_JWT_SECRET not set') return new TextEncoder().encode(secret) } export function hashToken(token: string): string { return createHash('sha256').update(token).digest('hex') } export function generateToken(): string { return randomBytes(48).toString('hex') } export interface JwtPayload { sub: string email: string sessionId: string } export async function createJwt(payload: JwtPayload): Promise { return new SignJWT({ ...payload }) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime(JWT_EXPIRY) .sign(getJwtSecret()) } export async function verifyJwt(token: string): Promise { try { const { payload } = await jwtVerify(token, getJwtSecret()) return payload as unknown as JwtPayload } catch { return null } } export async function createSession( investorId: string, ip: string | null, userAgent: string | null ): Promise<{ sessionId: string; jwt: string }> { // Revoke all existing sessions for this investor (single session enforcement) await pool.query( `UPDATE pitch_sessions SET revoked = true WHERE investor_id = $1 AND revoked = false`, [investorId] ) const sessionToken = generateToken() const tokenHash = hashToken(sessionToken) const expiresAt = new Date(Date.now() + SESSION_EXPIRY_HOURS * 60 * 60 * 1000) const { rows } = await pool.query( `INSERT INTO pitch_sessions (investor_id, token_hash, ip_address, user_agent, expires_at) VALUES ($1, $2, $3, $4, $5) RETURNING id`, [investorId, tokenHash, ip, userAgent, expiresAt] ) const sessionId = rows[0].id // Get investor email for JWT const investor = await pool.query( `SELECT email FROM pitch_investors WHERE id = $1`, [investorId] ) const jwt = await createJwt({ sub: investorId, email: investor.rows[0].email, sessionId, }) return { sessionId, jwt } } export async function validateSession(sessionId: string, investorId: string): Promise { const { rows } = await pool.query( `SELECT id FROM pitch_sessions WHERE id = $1 AND investor_id = $2 AND revoked = false AND expires_at > NOW()`, [sessionId, investorId] ) return rows.length > 0 } export async function revokeSession(sessionId: string): Promise { await pool.query( `UPDATE pitch_sessions SET revoked = true WHERE id = $1`, [sessionId] ) } export async function revokeAllSessions(investorId: string): Promise { await pool.query( `UPDATE pitch_sessions SET revoked = true WHERE investor_id = $1`, [investorId] ) } export async function setSessionCookie(jwt: string): Promise { const cookieStore = await cookies() cookieStore.set(COOKIE_NAME, jwt, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', maxAge: SESSION_EXPIRY_HOURS * 60 * 60, }) } export async function clearSessionCookie(): Promise { const cookieStore = await cookies() cookieStore.delete(COOKIE_NAME) } export async function getSessionFromCookie(): Promise { const cookieStore = await cookies() const token = cookieStore.get(COOKIE_NAME)?.value if (!token) return null return verifyJwt(token) } export function getClientIp(request: Request): string | null { const forwarded = request.headers.get('x-forwarded-for') if (forwarded) return forwarded.split(',')[0].trim() return null } export function validateAdminSecret(request: Request): boolean { const secret = process.env.PITCH_ADMIN_SECRET if (!secret) return false const auth = request.headers.get('authorization') if (!auth) return false return auth === `Bearer ${secret}` } export async function logAudit( investorId: string | null, action: string, details: Record = {}, request?: Request, slideId?: string, sessionId?: string, adminId?: string | null, targetInvestorId?: string | null, ): Promise { const ip = request ? getClientIp(request) : null const ua = request ? request.headers.get('user-agent') : null await pool.query( `INSERT INTO pitch_audit_logs (investor_id, action, details, ip_address, user_agent, slide_id, session_id, admin_id, target_investor_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, [investorId, action, JSON.stringify(details), ip, ua, slideId, sessionId, adminId ?? null, targetInvestorId ?? null] ) }