SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
1.5 KB · 39 lines typescript
Raw Blame History
1import 'server-only';2import { cookies } from 'next/headers';3import { signPayload, verifyPayload } from './crypto';45export const PENDING_COOKIE = 'ri_pending';6const TTL_S = 15 * 60;78export type PendingStage = 'verify' | 'totp' | 'email_code';910export interface Pending {11  uid: string;12  email: string;13  stage: PendingStage;14  next: string | null;15  /** true when the device was not trusted at password time (drives new-login e-mail) */16  newDevice: boolean;17}1819export async function setPending(p: Pending): Promise<void> {20  (await cookies()).set(PENDING_COOKIE, signPayload(p as unknown as Record<string, unknown>, TTL_S), { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax', path: '/', maxAge: TTL_S });21}2223export async function getPending(): Promise<Pending | null> {24  const raw = (await cookies()).get(PENDING_COOKIE)?.value;25  const p = verifyPayload<Record<string, unknown>>(raw);26  if (!p || typeof p.uid !== 'string' || typeof p.email !== 'string' || typeof p.stage !== 'string') return null;27  return { uid: p.uid, email: p.email, stage: p.stage as PendingStage, next: typeof p.next === 'string' ? p.next : null, newDevice: Boolean(p.newDevice) };28}2930export async function clearPending(): Promise<void> {31  (await cookies()).delete(PENDING_COOKIE);32}3334/** Only allow same-origin relative redirects. */35export function safeNext(next: string | null | undefined, fallback = '/collections'): string {36  if (!next || !next.startsWith('/') || next.startsWith('//') || next.includes('\\')) return fallback;37  return next;38}39