import 'server-only'; import { cookies } from 'next/headers'; import { and, eq, gt, isNull } from '@/lib/db'; import { newId } from '@rareindex/shared'; import { db, trustedDevices } from '@/lib/db'; import { hmacToken, randomToken } from './crypto'; import { clientInfo, deviceLabel } from './request'; import { DEVICE_COOKIE } from './session'; const TRUST_DAYS = 30; /** Is the current browser a trusted device for this user? */ export async function isTrustedDevice(userId: string): Promise { const raw = (await cookies()).get(DEVICE_COOKIE)?.value; if (!raw) return false; const rows = await db() .select({ id: trustedDevices.id }) .from(trustedDevices) .where(and(eq(trustedDevices.userId, userId), eq(trustedDevices.tokenHash, hmacToken(raw, 'device')), isNull(trustedDevices.revokedAt), gt(trustedDevices.expiresAt, new Date()))) .limit(1); if (!rows[0]) return false; void db().update(trustedDevices).set({ lastUsedAt: new Date() }).where(eq(trustedDevices.id, rows[0].id)).catch(() => {}); return true; } export async function trustThisDevice(userId: string): Promise { const raw = randomToken(32); const { ip, userAgent } = await clientInfo(); const expiresAt = new Date(Date.now() + TRUST_DAYS * 86_400_000); await db().insert(trustedDevices).values({ id: newId('event'), userId, tokenHash: hmacToken(raw, 'device'), label: deviceLabel(userAgent), userAgent, ip, expiresAt, lastUsedAt: new Date() }); (await cookies()).set(DEVICE_COOKIE, raw, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', expires: expiresAt }); } export async function revokeDevice(userId: string, deviceId: string): Promise { await db().update(trustedDevices).set({ revokedAt: new Date() }).where(and(eq(trustedDevices.id, deviceId), eq(trustedDevices.userId, userId))); } export async function revokeAllDevices(userId: string): Promise { await db().update(trustedDevices).set({ revokedAt: new Date() }).where(and(eq(trustedDevices.userId, userId), isNull(trustedDevices.revokedAt))); }