Initial commit

This commit is contained in:
Zoe
2026-08-07 22:13:25 -05:00
commit 8006fd67f3
40 changed files with 11017 additions and 0 deletions

104
server/utils/auth.ts Normal file
View File

@@ -0,0 +1,104 @@
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
import type { H3Event } from "h3";
import {
createError,
deleteCookie,
getCookie,
getRequestHeader,
setCookie,
} from "h3";
const COOKIE = "quickdrop_auth";
const DAY = 86400000;
function config() {
const runtime = useRuntimeConfig();
const configuredTtl = Number(
process.env.AUTH_TOKEN_TTL_DAYS || runtime.authTokenTtlDays || 7,
);
const ttlDays =
Number.isFinite(configuredTtl) && configuredTtl > 0 ? configuredTtl : 7;
return {
passcode: String(process.env.AUTH_PASSCODE || runtime.authPasscode || ""),
secret: String(process.env.AUTH_SECRET || runtime.authSecret || ""),
ttl: ttlDays * DAY,
};
}
function mac(value: string, secret: string) {
return createHmac("sha256", secret).update(value).digest("base64url");
}
export function authConfigured() {
const c = config();
return Boolean(c.passcode && c.secret);
}
export function issueSession(event: H3Event) {
const c = config();
if (!c.secret) throw new Error("Authentication is not configured.");
const payload = `${Date.now() + c.ttl}.${randomBytes(24).toString("base64url")}`;
setCookie(event, COOKIE, `${payload}.${mac(payload, c.secret)}`, {
httpOnly: true,
sameSite: "lax",
secure: process.env.NODE_ENV === "production",
path: "/",
maxAge: Math.floor(c.ttl / 1000),
});
}
export function clearAuthCookie(event: H3Event) {
deleteCookie(event, COOKIE, { path: "/" });
}
export function isAuthenticated(event: H3Event): boolean {
const c = config();
const token = getCookie(event, COOKIE);
if (!token || !c.secret) return false;
const split = token.lastIndexOf(".");
if (split < 1) return false;
const payload = token.slice(0, split);
const supplied = Buffer.from(token.slice(split + 1));
const expected = Buffer.from(mac(payload, c.secret));
if (
supplied.length !== expected.length ||
!timingSafeEqual(supplied, expected)
)
return false;
const expiry = Number(payload.slice(0, payload.indexOf(".")));
return Number.isSafeInteger(expiry) && expiry > Date.now();
}
export function requireAuth(event: H3Event) {
if (!isAuthenticated(event))
throw createError({
statusCode: 401,
statusMessage: "Authentication required.",
});
requireSameOrigin(event);
}
export function verifyPasscode(value: unknown): boolean {
const c = config();
if (!c.passcode || typeof value !== "string") return false;
const supplied = Buffer.from(value);
const expected = Buffer.from(c.passcode);
return (
supplied.length === expected.length && timingSafeEqual(supplied, expected)
);
}
export function requireSameOrigin(event: H3Event) {
const origin = getRequestHeader(event, "origin");
const site = getRequestHeader(event, "sec-fetch-site");
if (
site &&
["cross-site", "same-site"].includes(site) &&
site === "cross-site"
)
throw createError({
statusCode: 403,
statusMessage: "Cross-site state change rejected.",
});
if (origin) {
const host = getRequestHeader(event, "host");
const proto = getRequestHeader(event, "x-forwarded-proto") || "http";
try {
if (new URL(origin).host !== host || !["http", "https"].includes(proto))
throw new Error();
} catch {
throw createError({ statusCode: 403, statusMessage: "Origin rejected." });
}
}
}
export { COOKIE };