72 lines
3.0 KiB
TypeScript
72 lines
3.0 KiB
TypeScript
import { hashIp, lookupCountry } from "~/modules/ipAddress/services/geoip.server";
|
|
import { getTrustedClientIpOrNull } from "~/utils/server/IpUtils";
|
|
import { loadRequiredWebConfig } from "@hivegrid/config";
|
|
import { createAdminSession, revokeAdminSessionByToken } from "@hivegrid/db/admin-sessions.server";
|
|
import { getHostRoutingContext, getRequestRoutingHost } from "~/utils/app/HostRouting.server";
|
|
|
|
import { createUserSession as createServerSessionRow, revokeSessionByToken } from "./sessionStore.server";
|
|
|
|
// R-P2a S4b — Bruecke zwischen Cookie-Login und dem serverseitigen Session-Store. Additiv:
|
|
// das Login-Cookie bleibt der primaere Auth-Mechanismus, parallel wird eine UserSession-Zeile
|
|
// gefuehrt, damit Geraeteliste, Revoke und "ueberall abmelden" greifen.
|
|
|
|
export function extractRawClientIp(request: Request): string | null {
|
|
return getTrustedClientIpOrNull(request);
|
|
}
|
|
|
|
// AD-22: persistiert KEINE Klartext-IP. Die IP wird gehasht (hashIp, Wiedererkennung "gleiches
|
|
// Geraet") und das Land lokal aufgeloest (lookupCountry; die IP verlaesst die Infra nicht).
|
|
export async function buildSessionDeviceContext(request: Request): Promise<{ ipHash: string | null; userAgent: string | null; approxLocation: string | null }> {
|
|
const rawIp = extractRawClientIp(request);
|
|
const country = rawIp ? await lookupCountry(rawIp) : null;
|
|
return {
|
|
ipHash: rawIp ? hashIp(rawIp) : null,
|
|
userAgent: request.headers.get("user-agent"),
|
|
approxLocation: country?.countryName ?? null,
|
|
};
|
|
}
|
|
|
|
// Every authenticated cookie is backed by this row. Store failures propagate
|
|
// so callers cannot issue an unrevocable fallback cookie.
|
|
export async function startServerSession({
|
|
request,
|
|
userId,
|
|
adminAuthenticationMethods,
|
|
adminMfaAssuredAt,
|
|
}: {
|
|
request: Request;
|
|
userId: string;
|
|
adminAuthenticationMethods?: readonly string[];
|
|
adminMfaAssuredAt?: Date;
|
|
}): Promise<string> {
|
|
const context = await buildSessionDeviceContext(request);
|
|
const routing = loadRequiredWebConfig().routing;
|
|
const host = getRequestRoutingHost(request, new URL(request.url), routing);
|
|
if (getHostRoutingContext(host, routing) === "admin") {
|
|
if (!adminAuthenticationMethods || !adminMfaAssuredAt) {
|
|
throw new Error("Admin server session requires Authentik MFA assurance");
|
|
}
|
|
const { rawToken } = await createAdminSession({
|
|
adminUserId: userId,
|
|
authenticationMethods: adminAuthenticationMethods,
|
|
mfaAssuredAt: adminMfaAssuredAt,
|
|
...context,
|
|
});
|
|
return rawToken;
|
|
}
|
|
const { rawToken } = await createServerSessionRow({ userId, ...context });
|
|
return rawToken;
|
|
}
|
|
|
|
export async function endServerSession(rawToken: string | undefined | null, request?: Request): Promise<void> {
|
|
if (request) {
|
|
const routing = loadRequiredWebConfig().routing;
|
|
const host = getRequestRoutingHost(request, new URL(request.url), routing);
|
|
if (getHostRoutingContext(host, routing) === "admin") {
|
|
await revokeAdminSessionByToken(rawToken);
|
|
return;
|
|
}
|
|
}
|
|
await revokeSessionByToken(rawToken);
|
|
}
|