TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import 'server-only';2import { cache } from 'react';3import { cookies } from 'next/headers';4import { redirect } from 'next/navigation';5import { and, eq, gt, isNull } from '@/lib/db';6import { newId } from '@rareindex/shared';7import { db, sessions, users } from '@/lib/db';8import { hmacToken, randomToken } from './crypto';9import { clientInfo } from './request';1011export const SESSION_COOKIE = 'ri_session';12export const DEVICE_COOKIE = 'ri_device';13export const CURRENCY_COOKIE = 'ri_currency';14const SESSION_DAYS = 30;1516export type SessionUser = typeof users.$inferSelect;1718const isProd = () => process.env.NODE_ENV === 'production';1920export async function createSession(userId: string): Promise<void> {21 const raw = randomToken(32);22 const { ip, userAgent } = await clientInfo();23 const expiresAt = new Date(Date.now() + SESSION_DAYS * 86_400_000);24 await db().insert(sessions).values({ id: hmacToken(raw, 'session'), userId, expiresAt, userAgent, ip, lastSeenAt: new Date() });25 const jar = await cookies();26 jar.set(SESSION_COOKIE, raw, { httpOnly: true, secure: isProd(), sameSite: 'lax', path: '/', expires: expiresAt });27}2829export async function destroySession(): Promise<void> {30 const jar = await cookies();31 const raw = jar.get(SESSION_COOKIE)?.value;32 if (raw) await db().update(sessions).set({ revokedAt: new Date() }).where(eq(sessions.id, hmacToken(raw, 'session')));33 jar.delete(SESSION_COOKIE);34}3536export async function revokeAllSessions(userId: string, keepCurrent = true): Promise<number> {37 const jar = await cookies();38 const raw = jar.get(SESSION_COOKIE)?.value;39 const keepId = keepCurrent && raw ? hmacToken(raw, 'session') : null;40 const rows = await db().select({ id: sessions.id }).from(sessions).where(and(eq(sessions.userId, userId), isNull(sessions.revokedAt)));41 let n = 0;42 for (const r of rows) {43 if (r.id === keepId) continue;44 await db().update(sessions).set({ revokedAt: new Date() }).where(eq(sessions.id, r.id));45 n++;46 }47 return n;48}4950export async function currentSessionId(): Promise<string | null> {51 const jar = await cookies();52 const raw = jar.get(SESSION_COOKIE)?.value;53 return raw ? hmacToken(raw, 'session') : null;54}5556/** Resolve the signed-in user for this request (memoised per request). Null when anonymous. */57export const getCurrentUser = cache(async (): Promise<SessionUser | null> => {58 const jar = await cookies();59 const raw = jar.get(SESSION_COOKIE)?.value;60 if (!raw) return null;61 const id = hmacToken(raw, 'session');62 const rows = await db()63 .select({ user: users, sessionId: sessions.id, lastSeenAt: sessions.lastSeenAt })64 .from(sessions)65 .innerJoin(users, eq(users.id, sessions.userId))66 .where(and(eq(sessions.id, id), isNull(sessions.revokedAt), gt(sessions.expiresAt, new Date())))67 .limit(1);68 const row = rows[0];69 if (!row) return null;70 if (row.user.deletedAt) return null;71 // touch at most every 10 minutes (cheap activity tracking)72 if (!row.lastSeenAt || Date.now() - row.lastSeenAt.getTime() > 600_000) {73 void db().update(sessions).set({ lastSeenAt: new Date() }).where(eq(sessions.id, id)).catch(() => {});74 }75 return row.user;76});7778export async function requireUser(next?: string): Promise<SessionUser> {79 const u = await getCurrentUser();80 if (!u) redirect(`/login${next ? `?next=${encodeURIComponent(next)}` : ''}`);81 return u;82}8384export function newUserId(): string {85 return newId('user');86}8788export const DISPLAY_CURRENCIES = ['USD', 'CAD', 'EUR', 'GBP', 'JPY'] as const;89export type DisplayCurrency = (typeof DISPLAY_CURRENCIES)[number];9091/** Display currency for this request: user preference → cookie → USD. */92export async function getDisplayCurrency(): Promise<DisplayCurrency> {93 const u = await getCurrentUser();94 const pref = u?.displayCurrency;95 if (pref && (DISPLAY_CURRENCIES as readonly string[]).includes(pref)) return pref as DisplayCurrency;96 const c = (await cookies()).get(CURRENCY_COOKIE)?.value;97 return c && (DISPLAY_CURRENCIES as readonly string[]).includes(c) ? (c as DisplayCurrency) : 'USD';98}99