gh_UserManager/apps/web/middleware.ts
bermooda-company 54d5891edf user
2026-08-23 23:59:14 +03:30

86 lines
2.5 KiB
TypeScript

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import jwt from "jsonwebtoken";
const PROTECTED_PREFIXES = ["/account", "/admin"];
const API_BASE =
process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000/api/v1";
const JWT_PUBLIC_KEY_RAW = process.env.JWT_PUBLIC_KEY;
if (!JWT_PUBLIC_KEY_RAW) {
throw new Error("JWT_PUBLIC_KEY environment variable is required");
}
const JWT_PUBLIC_KEY = JWT_PUBLIC_KEY_RAW;
function isExpired(token: string): boolean {
try {
const decoded = jwt.verify(token, JWT_PUBLIC_KEY, {
algorithms: ["RS256"],
}) as any;
return Date.now() / 1000 >= decoded.exp;
} catch {
return true;
}
}
export async function middleware(req: NextRequest) {
const { pathname } = req.nextUrl;
const isProtected = PROTECTED_PREFIXES.some(
(p) => pathname === p || pathname.startsWith(`${p}/`),
);
if (!isProtected) return NextResponse.next();
const access = req.cookies.get("access_token")?.value;
if (access && !isExpired(access)) return NextResponse.next();
// Server-side refresh: validate the session against the backend using the
// refresh cookie instead of trusting the (possibly forged/expired) access JWT.
const refresh = req.cookies.get("refresh_token")?.value;
if (refresh) {
try {
const res = await fetch(`${API_BASE}/auth/refresh/`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refresh }),
cache: "no-store",
});
if (res.ok) {
const data = await res.json();
const newAccess = data.access as string | undefined;
const newRefresh = (data.refresh as string) || refresh;
if (newAccess) {
const response = NextResponse.next();
const secure = req.nextUrl.protocol === "https:";
const base = {
httpOnly: true,
sameSite: "lax" as const,
path: "/",
secure,
};
response.cookies.set("access_token", newAccess, {
...base,
maxAge: 1800,
});
response.cookies.set("refresh_token", newRefresh, {
...base,
maxAge: 60 * 60 * 24 * 30,
});
return response;
}
}
} catch {
// fall through to redirect
}
}
const url = req.nextUrl.clone();
url.pathname = "/login";
url.searchParams.set("next", pathname);
return NextResponse.redirect(url);
}
export const config = {
matcher: ["/account/:path*", "/admin/:path*"],
};