146 lines
4.0 KiB
TypeScript
146 lines
4.0 KiB
TypeScript
import { cookies } from "next/headers";
|
|
import { NextResponse } from "next/server";
|
|
import { isSessionTokenRevoked, isActionTokenUsed } from "./token-blacklist";
|
|
|
|
const encoder = new TextEncoder();
|
|
|
|
interface AdminSession {
|
|
id: string;
|
|
email: string;
|
|
name: string;
|
|
exp: number;
|
|
}
|
|
|
|
interface ActionToken {
|
|
anfrageId: string;
|
|
status: "in_bearbeitung" | "abgeschlossen";
|
|
exp: number;
|
|
}
|
|
|
|
function toBase64Url(buffer: ArrayBuffer | Uint8Array): string {
|
|
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
return btoa(String.fromCharCode(...bytes))
|
|
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
}
|
|
|
|
function fromBase64Url(str: string): ArrayBuffer {
|
|
const b64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
return bytes.buffer as ArrayBuffer;
|
|
}
|
|
|
|
async function getKey(secret: string): Promise<CryptoKey> {
|
|
return crypto.subtle.importKey(
|
|
"raw",
|
|
encoder.encode(secret),
|
|
{ name: "HMAC", hash: "SHA-256" },
|
|
false,
|
|
["sign", "verify"]
|
|
);
|
|
}
|
|
|
|
export async function createSessionToken(
|
|
session: Omit<AdminSession, "exp">
|
|
): Promise<string> {
|
|
const secret = process.env.ADMIN_SESSION_TOKEN!;
|
|
const payload: AdminSession = {
|
|
...session,
|
|
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 2,
|
|
};
|
|
const data = toBase64Url(encoder.encode(JSON.stringify(payload)));
|
|
const key = await getKey(secret);
|
|
const sig = toBase64Url(await crypto.subtle.sign("HMAC", key, encoder.encode(data)));
|
|
return `${data}.${sig}`;
|
|
}
|
|
|
|
export async function requireAdmin(): Promise<AdminSession | NextResponse> {
|
|
const cookieStore = await cookies();
|
|
const token = cookieStore.get("admin_session")?.value;
|
|
|
|
if (!token) {
|
|
return NextResponse.json({ error: "Nicht authentifiziert" }, { status: 401 });
|
|
}
|
|
|
|
const session = await verifySessionToken(token);
|
|
if (!session) {
|
|
return NextResponse.json({ error: "Session ungültig" }, { status: 401 });
|
|
}
|
|
|
|
return session;
|
|
}
|
|
|
|
export async function verifySessionToken(token: string): Promise<AdminSession | null> {
|
|
try {
|
|
const secret = process.env.ADMIN_SESSION_TOKEN!;
|
|
const [data, sig] = token.split(".");
|
|
if (!data || !sig) return null;
|
|
|
|
if (await isSessionTokenRevoked(sig)) return null;
|
|
|
|
const key = await getKey(secret);
|
|
const valid = await crypto.subtle.verify(
|
|
"HMAC",
|
|
key,
|
|
fromBase64Url(sig),
|
|
encoder.encode(data)
|
|
);
|
|
if (!valid) return null;
|
|
|
|
const session: AdminSession = JSON.parse(
|
|
new TextDecoder().decode(new Uint8Array(fromBase64Url(data)))
|
|
);
|
|
if (session.exp < Math.floor(Date.now() / 1000)) return null;
|
|
|
|
return session;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function createActionToken(
|
|
anfrageId: string,
|
|
status: "in_bearbeitung" | "abgeschlossen",
|
|
ablaufTage = 7
|
|
): Promise<string> {
|
|
const secret = process.env.ADMIN_SESSION_TOKEN!;
|
|
const payload: ActionToken = {
|
|
anfrageId,
|
|
status,
|
|
exp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * ablaufTage,
|
|
};
|
|
const data = toBase64Url(encoder.encode(JSON.stringify(payload)));
|
|
const key = await getKey(secret);
|
|
const sig = toBase64Url(await crypto.subtle.sign("HMAC", key, encoder.encode(data)));
|
|
return `${data}.${sig}`;
|
|
}
|
|
|
|
export async function verifyActionToken(
|
|
token: string
|
|
): Promise<{ anfrageId: string; status: string } | null> {
|
|
try {
|
|
const secret = process.env.ADMIN_SESSION_TOKEN!;
|
|
const [data, sig] = token.split(".");
|
|
if (!data || !sig) return null;
|
|
|
|
if (await isActionTokenUsed(sig)) return null;
|
|
|
|
const key = await getKey(secret);
|
|
const valid = await crypto.subtle.verify(
|
|
"HMAC",
|
|
key,
|
|
fromBase64Url(sig),
|
|
encoder.encode(data)
|
|
);
|
|
if (!valid) return null;
|
|
|
|
const actionToken: ActionToken = JSON.parse(
|
|
new TextDecoder().decode(new Uint8Array(fromBase64Url(data)))
|
|
);
|
|
if (actionToken.exp < Math.floor(Date.now() / 1000)) return null;
|
|
|
|
return { anfrageId: actionToken.anfrageId, status: actionToken.status };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|