MEGAINFRA-WEBAPP/src/proxy.ts
DEls c0b3f5fd13 feat(users): Last active column to spot people not using the app
users.last_active_at (migration applied) is stamped at sign-in and by the
proxy in the background at most every 15 minutes per person; a failed
stamp never blocks a request. The Users list shows "Today" / "5 days ago" /
"3 months ago" / "Never", amber after a quiet week and red after an idle
month, with a count of active accounts idle 30+ days.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 22:54:07 +02:00

46 lines
2.4 KiB
TypeScript

import { NextResponse, type NextFetchEvent, type NextRequest } from "next/server";
import { initialDestination, userFromRecord } from "./lib/auth/access";
import { canAccessPath } from "./lib/auth/path-access";
import { classifyAuthPath } from "./lib/auth/proxy-path";
import { activityIsStale, recordActivity } from "./lib/auth/last-active";
export async function proxy(request: NextRequest, event?: NextFetchEvent) {
const pathname = request.nextUrl.pathname;
const pathType = classifyAuthPath(pathname);
if (pathType === "public" || pathType === "machine") return NextResponse.next();
const isApi = pathname.startsWith("/api/");
const login = () => isApi
? NextResponse.json({ error: "Sign in required" }, { status: 401 })
: NextResponse.redirect(new URL(`/login?redirect=${encodeURIComponent(pathname)}`, request.url));
const token = request.cookies.get("mega_session")?.value;
if (!token) return login();
const base = process.env.NEXT_PUBLIC_POCKETBASE_URL;
if (!base) return NextResponse.json({ error: "Authentication is unavailable" }, { status: 503 });
let response: Response;
try {
response = await fetch(`${base}/api/collections/users/auth-refresh`, {
method: "POST", headers: { Authorization: token }, cache: "no-store",
});
} catch {
return NextResponse.json({ error: "Authentication is unavailable" }, { status: 503 });
}
if (!response.ok) return response.status >= 500
? NextResponse.json({ error: "Authentication is unavailable" }, { status: 503 })
: login();
const record = (await response.json() as { record?: { last_active_at?: unknown } }).record;
const user = userFromRecord(record);
if (!user) return login();
// Last active for the Users list: a background write, at most once per interval.
if (event && activityIsStale(record?.last_active_at)) event.waitUntil(recordActivity(user.id).catch(() => {}));
if (user.mustChangePassword) return isApi
? NextResponse.json({ error: "Change your password first", code: "password_change_required" }, { status: 403 })
: NextResponse.redirect(new URL("/change-password", request.url));
if (!canAccessPath(user, pathname)) return isApi
? NextResponse.json({ error: "Not allowed" }, { status: 403 })
: NextResponse.redirect(new URL(initialDestination(user), request.url));
return NextResponse.next();
}
export const config = { matcher: ["/dashboard/:path*", "/clock/:path*", "/api/:path*"] };