feat(auth): enterprise authentication and user management

This commit is contained in:
Schubert Ferenc 2026-07-02 22:19:12 +02:00
parent 4bc8b20a21
commit 86a32a942c
36 changed files with 2239 additions and 324 deletions

View file

@ -1,5 +1,5 @@
import axios from "axios";
export const api = axios.create({
baseURL: "http://localhost:8000",
});
baseURL: "/api",
});

View file

@ -0,0 +1,103 @@
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
const ACCESS_TOKEN_COOKIE = "access_token";
const DEFAULT_COOKIE_MAX_AGE_SECONDS = 60 * 60;
export function getHermesUrl() {
return process.env.HERMES_INTERNAL_URL?.replace(/\/$/, "") ?? null;
}
export async function getAccessToken() {
const cookieStore = await cookies();
return cookieStore.get(ACCESS_TOKEN_COOKIE)?.value;
}
function isSecureCookieEnabled() {
if (process.env.AUTH_COOKIE_SECURE) {
return process.env.AUTH_COOKIE_SECURE !== "false";
}
return process.env.NODE_ENV === "production";
}
function normalizeMaxAge(maxAge?: number) {
if (typeof maxAge !== "number" || !Number.isFinite(maxAge) || maxAge <= 0) {
return DEFAULT_COOKIE_MAX_AGE_SECONDS;
}
return Math.floor(maxAge);
}
export function setAuthCookie(response: NextResponse, token: string, maxAge: number) {
response.cookies.set({
name: ACCESS_TOKEN_COOKIE,
value: token,
httpOnly: true,
secure: isSecureCookieEnabled(),
sameSite: "lax",
path: "/",
maxAge: normalizeMaxAge(maxAge),
});
}
export function clearAuthCookie(response: NextResponse) {
response.cookies.set({
name: ACCESS_TOKEN_COOKIE,
value: "",
httpOnly: true,
secure: isSecureCookieEnabled(),
sameSite: "lax",
path: "/",
maxAge: 0,
});
}
export function unauthorizedResponse() {
return NextResponse.json(
{ detail: "Nicht authentifiziert" },
{ status: 401 },
);
}
export function upstreamUnavailableResponse() {
return NextResponse.json(
{ detail: "Hermes ist nicht erreichbar" },
{ status: 502 },
);
}
export function upstreamConfigurationErrorResponse() {
return NextResponse.json(
{ detail: "Hermes ist nicht konfiguriert" },
{ status: 500 },
);
}
export async function readJson(response: Response) {
const text = await response.text();
if (!text) {
return null;
}
try {
return JSON.parse(text);
} catch {
return { detail: text };
}
}
export async function hermesJsonResponse(response: Response) {
if (response.status === 204) {
return new NextResponse(null, {
status: 204,
});
}
const body = await readJson(response);
return NextResponse.json(body, {
status: response.status,
});
}

View file

@ -0,0 +1,56 @@
import { NextRequest, NextResponse } from "next/server";
function normalizeOrigin(origin: string) {
return origin.replace(/\/$/, "");
}
function getConfiguredOrigins() {
return (process.env.ATHENA_PUBLIC_ORIGIN ?? "")
.split(",")
.map((origin) => origin.trim())
.filter(Boolean)
.map(normalizeOrigin);
}
function getForwardedOrigin(request: NextRequest) {
const forwardedHost = request.headers.get("x-forwarded-host");
if (!forwardedHost) {
return null;
}
const forwardedProto = request.headers.get("x-forwarded-proto") ?? "https";
const firstHost = forwardedHost.split(",")[0]?.trim();
const firstProto = forwardedProto.split(",")[0]?.trim();
if (!firstHost || !firstProto) {
return null;
}
return `${firstProto}://${firstHost}`;
}
export function assertSameOrigin(request: NextRequest) {
const origin = request.headers.get("origin");
if (!origin) {
return null;
}
const requestOrigins = [
request.nextUrl.origin,
getForwardedOrigin(request),
...getConfiguredOrigins(),
]
.filter((value): value is string => Boolean(value))
.map(normalizeOrigin);
if (!requestOrigins.includes(normalizeOrigin(origin))) {
return NextResponse.json(
{ detail: "Ungültiger Ursprung der Anfrage" },
{ status: 403 },
);
}
return null;
}