MEGAINFRA-WEBAPP/src/app/api/auth/login/route.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

42 lines
2.2 KiB
TypeScript

import { cookies } from "next/headers";
import PocketBase from "pocketbase";
import { initialDestination } from "@/lib/auth/access";
import { authMode, SESSION_COOKIE, userFromRecord } from "@/lib/auth/session";
import { PB_URL } from "@/lib/pb";
import { canAccessPath } from "@/lib/auth/path-access";
import { SESSION_MAX_AGE_SECONDS } from "@/lib/auth/session-policy";
import { recordActivity } from "@/lib/auth/last-active";
export async function POST(request: Request): Promise<Response> {
if (authMode() !== "users") return Response.json({ error: "Named login is not enabled" }, { status: 404 });
let body: Record<string, unknown>;
try { body = await request.json(); }
catch { return Response.json({ error: "Enter your email and password" }, { status: 400 }); }
const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
const password = typeof body.password === "string" ? body.password : "";
if (!email || !password) return Response.json({ error: "Enter your email and password" }, { status: 400 });
const pb = new PocketBase(PB_URL);
try {
const result = await pb.collection("users").authWithPassword(email, password);
const user = userFromRecord(result.record);
if (!user) return Response.json({ error: "Invalid email or password" }, { status: 401 });
// A failed stamp must not fail the sign-in.
await recordActivity(user.id).catch(() => {});
(await cookies()).set(SESSION_COOKIE, pb.authStore.token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
maxAge: SESSION_MAX_AGE_SECONDS,
});
let landing = initialDestination(user);
if (!user.mustChangePassword && typeof body.redirect === "string" && body.redirect.startsWith("/") && !body.redirect.startsWith("//")) {
const target = new URL(body.redirect, "http://app.local");
if (target.origin === "http://app.local" && !target.pathname.startsWith("/api/") && canAccessPath(user, target.pathname)) landing = `${target.pathname}${target.search}${target.hash}`;
}
return Response.json({ user: { id: user.id, name: user.name, role: user.role }, landing });
} catch {
return Response.json({ error: "Invalid email or password" }, { status: 401 });
}
}