Files
inven/middleware.ts
2026-03-23 16:16:45 -05:00

78 lines
2.0 KiB
TypeScript

import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";
const SESSION_COOKIE = "inven_session";
function getAuthSecret() {
return process.env.AUTH_SECRET || "dev-insecure-auth-secret";
}
async function sign(value: string) {
const key = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(getAuthSecret()),
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signature = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(value));
return Array.from(new Uint8Array(signature))
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("");
}
function decodeBase64Url(value: string) {
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
return atob(padded);
}
async function hasValidSession(request: NextRequest) {
const raw = request.cookies.get(SESSION_COOKIE)?.value;
if (!raw) {
return false;
}
const [base, signature] = raw.split(".");
if (!base || !signature || (await sign(base)) !== signature) {
return false;
}
try {
const payload = JSON.parse(decodeBase64Url(base)) as { expiresAt?: number };
return typeof payload.expiresAt === "number" && payload.expiresAt > Date.now();
} catch {
return false;
}
}
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const isPublic =
pathname === "/login" ||
pathname.startsWith("/_next") ||
pathname.startsWith("/favicon") ||
pathname === "/api/health";
const authenticated = await hasValidSession(request);
if (!authenticated && !isPublic) {
const url = request.nextUrl.clone();
url.pathname = "/login";
return NextResponse.redirect(url);
}
if (authenticated && pathname === "/login") {
const url = request.nextUrl.clone();
url.pathname = "/";
return NextResponse.redirect(url);
}
return NextResponse.next();
}
export const config = {
matcher: ["/((?!.*\\..*).*)"]
};