SPB Git

spb/autoscholar

Public
2.7 KB · 72 lines typescript
1import "server-only";2import { RateLimiter, SESSION_COOKIE } from "@autoscholar/auth";3import {4  type AuthenticatedSession,5  forbidden,6  getEnv,7  resolveSession,8  unauthorized,9  verifySessionCsrf,10} from "@autoscholar/core";11import { cookies } from "next/headers";12import { db } from "./db";13import { clientIp } from "./ip";1415export const loginLimiterIp = new RateLimiter(20, 15 * 60_000);16export const loginLimiterUser = new RateLimiter(10, 15 * 60_000);17export const adminMutationLimiter = new RateLimiter(240, 60_000);18export const publicLimiter = new RateLimiter(600, 60_000);19export const uploadLimiter = new RateLimiter(30, 10 * 60_000);2021export function cookieOptions() {22  const env = getEnv();23  const secure = env.NODE_ENV === "production" || env.APP_BASE_URL.startsWith("https://");24  return { httpOnly: true, secure, sameSite: "strict" as const, path: "/", maxAge: 12 * 60 * 60 };25}2627export async function currentSession(): Promise<AuthenticatedSession | null> {28  const jar = await cookies();29  const token = jar.get(SESSION_COOKIE)?.value;30  if (!token) return null;31  return resolveSession(db(), token);32}3334export async function sessionToken(): Promise<string | null> {35  const jar = await cookies();36  return jar.get(SESSION_COOKIE)?.value ?? null;37}3839/** Server-side guard for admin APIs. `allowPendingRotation` lets the change-password endpoints run before rotation. */40export async function requireAdmin(41  req: Request,42  opts: { mutation?: boolean; allowPendingRotation?: boolean } = {},43): Promise<AuthenticatedSession & { ip: string | null }> {44  const token = await sessionToken();45  const sess = token ? await resolveSession(db(), token) : null;46  if (!sess) throw unauthorized();47  if (sess.mustChangePassword && !opts.allowPendingRotation)48    throw forbidden("password rotation required before using the admin API");49  if (opts.mutation || !["GET", "HEAD"].includes(req.method)) {50    const presented = req.headers.get("x-csrf-token");51    if (!(await verifySessionCsrf(db(), token!, presented))) throw forbidden("invalid CSRF token");52    const origin = req.headers.get("origin");53    const base = new URL(getEnv().APP_BASE_URL).origin;54    if (origin && origin !== base && !isLocalOrigin(origin)) throw forbidden("cross-origin request rejected");55    const key = `${sess.userId}`;56    if (!adminMutationLimiter.check(key).allowed)57      throw new (await import("@autoscholar/core")).AppError("rate_limited", "too many requests", 429);58  }59  return { ...sess, ip: clientIp(req) };60}6162function isLocalOrigin(origin: string): boolean {63  try {64    const u = new URL(origin);65    return (66      ["localhost", "127.0.0.1", "[::1]"].includes(u.hostname) || /^192\.168\.\d+\.\d+$/.test(u.hostname)67    );68  } catch {69    return false;70  }71}72
Files · main
Menu

Theme