115 lines
3.9 KiB
TypeScript
115 lines
3.9 KiB
TypeScript
/**
|
|
* Session-Management (plan.md Abschnitt 11).
|
|
*
|
|
* Signierte, HttpOnly-Cookies auf Basis von JWT (jose). Keine eigene
|
|
* Kryptografie, keine Klartext-Sessions in der Datenbank.
|
|
*/
|
|
import { SignJWT, jwtVerify } from 'jose';
|
|
import { cookies, headers } from 'next/headers';
|
|
import { getConfig } from '@/lib/config';
|
|
import { provisionMpmUser } from '@/lib/auth/mpm-provisioning';
|
|
import type { AuthenticatedUser } from '@/lib/permissions/permissions';
|
|
|
|
const SESSION_COOKIE_NAME = 'calendar_session';
|
|
const SESSION_MAX_AGE_SECONDS = 60 * 60 * 24 * 7; // 7 Tage
|
|
|
|
/** Fehler bei ungueltiger/abgelaufener Session. */
|
|
export class UnauthorizedError extends Error {
|
|
constructor() {
|
|
super('Nicht angemeldet oder Sitzung abgelaufen.');
|
|
this.name = 'UnauthorizedError';
|
|
}
|
|
}
|
|
|
|
async function getSessionKey(): Promise<Uint8Array> {
|
|
const secret = getConfig().sessionSecret;
|
|
return new TextEncoder().encode(secret);
|
|
}
|
|
|
|
/** Erstellt ein signiertes Session-Token fuer den Benutzer. */
|
|
export async function createSessionToken(user: {
|
|
id: string;
|
|
email: string;
|
|
role: string;
|
|
}): Promise<string> {
|
|
const key = await getSessionKey();
|
|
return new SignJWT({ sub: user.id, email: user.email, role: user.role })
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.setIssuedAt()
|
|
.setExpirationTime(`${SESSION_MAX_AGE_SECONDS}s`)
|
|
.sign(key);
|
|
}
|
|
|
|
/** Setzt das Session-Cookie sicher (HttpOnly, SameSite=Lax, Secure in Prod). */
|
|
export async function setSessionCookie(token: string): Promise<void> {
|
|
const cookieStore = await cookies();
|
|
cookieStore.set(SESSION_COOKIE_NAME, token, {
|
|
httpOnly: true,
|
|
sameSite: 'lax',
|
|
secure: process.env.NODE_ENV === 'production',
|
|
maxAge: SESSION_MAX_AGE_SECONDS,
|
|
path: '/',
|
|
});
|
|
}
|
|
|
|
/** Loescht das Session-Cookie (Logout). */
|
|
export async function clearSessionCookie(): Promise<void> {
|
|
const cookieStore = await cookies();
|
|
cookieStore.delete(SESSION_COOKIE_NAME);
|
|
}
|
|
|
|
/**
|
|
* Liest und verifiziert die aktuelle Session.
|
|
* Wirft UnauthorizedError, wenn keine gueltige Session existiert.
|
|
*/
|
|
export async function getAuthenticatedUser(): Promise<AuthenticatedUser> {
|
|
if (process.env.MPM_AUTH_MODE === 'true') {
|
|
const requestHeaders = await headers();
|
|
const userId = requestHeaders.get('x-kalendartool-mpm-user-id');
|
|
const username = requestHeaders.get('x-kalendartool-mpm-username');
|
|
const displayName = requestHeaders.get('x-kalendartool-mpm-display-name');
|
|
const role = requestHeaders.get('x-kalendartool-mpm-role');
|
|
if (!userId || !username || !role || !['ADMIN', 'USER'].includes(role)) {
|
|
throw new UnauthorizedError();
|
|
}
|
|
return provisionMpmUser({ userId, username, displayName, role: role as 'ADMIN' | 'USER' });
|
|
}
|
|
|
|
const cookieStore = await cookies();
|
|
const token = cookieStore.get(SESSION_COOKIE_NAME)?.value;
|
|
if (!token) {
|
|
throw new UnauthorizedError();
|
|
}
|
|
try {
|
|
const { payload } = await jwtVerify(token, await getSessionKey());
|
|
if (typeof payload.sub !== 'string' || typeof payload.role !== 'string') {
|
|
throw new UnauthorizedError();
|
|
}
|
|
// Alte Tokens (vor E-Mail-Erweiterung) enthalten keine E-Mail;
|
|
// in dem Fall wird ein Platzhalter verwendet, requireDbUser laedt
|
|
// ohnehin die frischen Daten aus der DB.
|
|
const email =
|
|
typeof payload.email === 'string' ? payload.email : 'unbekannt@lokal';
|
|
// Username ist nicht im JWT (kann sich aendern); requireDbUser
|
|
// laedt den aktuellen Wert aus der DB. Das JWT liefert null als
|
|
// Platzhalter, damit der Typ erfuellt ist.
|
|
return {
|
|
id: payload.sub,
|
|
email,
|
|
username: null,
|
|
role: payload.role as AuthenticatedUser['role'],
|
|
};
|
|
} catch {
|
|
throw new UnauthorizedError();
|
|
}
|
|
}
|
|
|
|
/** Wie getAuthenticatedUser, gibt aber null statt zu werfen. */
|
|
export async function tryGetAuthenticatedUser(): Promise<AuthenticatedUser | null> {
|
|
try {
|
|
return await getAuthenticatedUser();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|