Initial commit: Kalendartool (Next.js, Prisma, Docker)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Kühn
2026-10-08 14:38:04 +02:00
commit bc49c3074e
94 changed files with 12812 additions and 0 deletions

79
lib/auth/hub.ts Normal file
View File

@@ -0,0 +1,79 @@
/**
* JWKS-Client: lädt die öffentlichen Hub-Schlüssel und cached sie.
* Tools validieren Hub-Tokens gegen diesen Key (RS256) – kein geteiltes Secret.
*
* platform-plan.md §5.2: Der Hub exposet /.well-known/jwks.json.
*/
import { createRemoteJWKSet, jwtVerify, type JWTPayload } from "jose";
import { getConfig } from "@/lib/config";
const JWKS_CACHE_TTL_MS = 5 * 60 * 1000; // 5 Minuten (passend zum Hub-Cache-Header)
interface CachedJwks {
jwks: ReturnType<typeof createRemoteJWKSet>;
fetchedAt: number;
}
let cache: CachedJwks | null = null;
function getHubUrl(): string {
const hubUrl = getConfig().hubUrl;
if (!hubUrl) {
throw new Error("HUB_URL fehlt – Hub-SSO ist nicht konfiguriert.");
}
return hubUrl.replace(/\/$/, "");
}
/** Remote-JWKSet mit einfachem TTL-Cache (jose hat keinen eingebauten). */
function getJwksSet(): ReturnType<typeof createRemoteJWKSet> {
const now = Date.now();
if (cache && now - cache.fetchedAt < JWKS_CACHE_TTL_MS) {
return cache.jwks;
}
const jwks = createRemoteJWKSet(new URL(`${getHubUrl()}/.well-known/jwks.json`));
cache = { jwks, fetchedAt: now };
return jwks;
}
export interface HubTokenResult {
hubId: string;
email: string;
name: string;
isAdmin: boolean;
}
/**
* Validiert ein Hub-Token (Signatur, TTL, aud, iss).
* Wirft bei jedem Fehler – der Aufrufer entscheidet über die Response.
*/
export async function verifyHubToken(token: string): Promise<HubTokenResult> {
const config = getConfig();
const expectedAud = config.hubToolSlug;
if (!expectedAud) {
throw new Error("HUB_TOOL_SLUG fehlt – Tool-Slug für Hub-SSO nicht konfiguriert.");
}
const { payload } = await jwtVerify(token, getJwksSet(), {
algorithms: ["RS256"],
audience: expectedAud,
issuer: config.hubIssuer ?? "http://localhost:3001",
clockTolerance: 5,
});
const hubId = payload.sub;
const email = payload.email;
const name = payload.name;
if (typeof hubId !== "string" || typeof email !== "string" || typeof name !== "string") {
throw new Error("Hub-Token enthält unvollständige Claims.");
}
return {
hubId,
email,
name,
isAdmin: payload.isAdmin === true,
};
}
/** Nur für Typ-Export (jose JWTPayload) – nicht Teil des Contracts. */
export type { JWTPayload };