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>
46 lines
2.4 KiB
TypeScript
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*"] };
|