344b1701dd
Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
47 lines
2.2 KiB
TypeScript
47 lines
2.2 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { prisma } from "@/lib/db";
|
|
import { clientIp } from "@/lib/ratelimit";
|
|
import { safeOpsNext, verifyAccessJwt } from "@/lib/ops/cfAccess";
|
|
import { logOperatorEvent, setOpsCookie } from "@/lib/ops/session";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
/* Single sign-on entry. Reached when a browser arrives with a Cloudflare Access assertion but no
|
|
* operator session (the sign-in page hands off here). The assertion is verified fail-closed
|
|
* (lib/ops/cfAccess.ts), the verified email is mapped to an EXISTING, ACTIVE operator, and the
|
|
* ordinary tc_ops session is minted, marked `sso`. Never creates an operator: an email Access
|
|
* admits but the console does not know falls back to the password door. Every failure lands on
|
|
* /ops/login?sso=failed — never a bypass, never a loop (the sign-in page does not re-trigger SSO
|
|
* when ?sso is present).
|
|
*
|
|
* Relative Location on purpose: an absolute URL built from the request would carry the origin
|
|
* nginx sees (127.0.0.1:3000), not the public host. */
|
|
function seeOther(location: string): NextResponse {
|
|
return new NextResponse(null, { status: 303, headers: { Location: location } });
|
|
}
|
|
|
|
export async function GET(req: NextRequest) {
|
|
const failed = seeOther("/ops/login?sso=failed");
|
|
const next = safeOpsNext(req.nextUrl.searchParams.get("next"));
|
|
const ip = clientIp(req.headers);
|
|
|
|
const email = await verifyAccessJwt(req.headers.get("cf-access-jwt-assertion"));
|
|
if (!email) return failed;
|
|
|
|
const op = await prisma.operator.findUnique({
|
|
where: { email },
|
|
select: { id: true, inactive: true, passwordHash: true },
|
|
});
|
|
if (!op || op.inactive) {
|
|
// The trail cannot name an operator it does not have; the address goes in the detail, since
|
|
// "who Access let in that the console refused" is the fact worth keeping.
|
|
console.warn("[ops sso] no active operator for", email, "from", ip);
|
|
return failed;
|
|
}
|
|
|
|
await setOpsCookie(op.id, op.passwordHash, true);
|
|
logOperatorEvent({ operatorId: op.id, action: "ops:signin.sso", ip });
|
|
prisma.operator.update({ where: { id: op.id }, data: { lastSeenAt: new Date() } }).catch(() => {});
|
|
return seeOther(next);
|
|
}
|