feat(PROJ-1): httpOnly Cookie, Auditor-Guard, Nutzer-Aktionen (C)
Backend: - Login setzt httpOnly SameSite=Strict Cookie (archivmail_session) - Logout löscht Cookie + blacklistet Token - authMiddleware: Cookie first, Bearer als Fallback (CLI kompatibel) Frontend: - api.ts: credentials: include statt localStorage/Bearer Token - updateUser(), deleteUser() hinzugefügt - useAuth: kein localStorage mehr, nur /api/auth/me requireRole: "admin" | "auditor" | undefined - Login-Seite: kein localStorage - Navbar: kein localStorage - Admin: Nutzer-Aktionen (Sperren/Freischalten, Löschen, Passwort-Reset) Löschen verhindert wenn letzter Admin (HTTP 409) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+36
-30
@@ -1,31 +1,22 @@
|
||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || "";
|
||||
|
||||
function getToken(): string | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
return localStorage.getItem("archivmail_token");
|
||||
}
|
||||
|
||||
async function request<T>(
|
||||
path: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const token = getToken();
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
...(options.headers as Record<string, string>),
|
||||
};
|
||||
if (token) {
|
||||
headers["Authorization"] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
...options,
|
||||
headers,
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (res.status === 401) {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.removeItem("archivmail_token");
|
||||
window.location.href = "/";
|
||||
}
|
||||
throw new Error("Unauthorized");
|
||||
@@ -44,12 +35,16 @@ async function request<T>(
|
||||
// Types
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string;
|
||||
username: string;
|
||||
role: string;
|
||||
user: {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
role: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
email: string;
|
||||
role: string;
|
||||
@@ -136,6 +131,13 @@ export interface CreateUserRequest {
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface UpdateUserRequest {
|
||||
email?: string;
|
||||
role?: string;
|
||||
active?: boolean;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
// API functions
|
||||
|
||||
export async function login(
|
||||
@@ -187,6 +189,17 @@ export async function createUser(data: CreateUserRequest): Promise<User> {
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateUser(id: number, data: UpdateUserRequest): Promise<User> {
|
||||
return request<User>(`/api/users/${id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteUser(id: number): Promise<void> {
|
||||
await request<void>(`/api/users/${id}`, { method: "DELETE" });
|
||||
}
|
||||
|
||||
export interface StorageStats {
|
||||
total_mails: number;
|
||||
total_bytes: number;
|
||||
@@ -212,9 +225,8 @@ export async function downloadMailAttachment(
|
||||
id: string,
|
||||
index: number
|
||||
): Promise<{ blob: Blob; filename: string }> {
|
||||
const token = getToken();
|
||||
const res = await fetch(`${API_BASE}/api/mails/${id}/attachments/${index}`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) throw new Error(`Download fehlgeschlagen: ${res.status}`);
|
||||
const disposition = res.headers.get("Content-Disposition") || "";
|
||||
@@ -226,9 +238,8 @@ export async function downloadMailAttachment(
|
||||
export async function downloadMailRaw(
|
||||
id: string
|
||||
): Promise<{ blob: Blob; filename: string }> {
|
||||
const token = getToken();
|
||||
const res = await fetch(`${API_BASE}/api/mails/${id}/raw`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) throw new Error(`Download fehlgeschlagen: ${res.status}`);
|
||||
return { blob: await res.blob(), filename: `${id}.eml` };
|
||||
@@ -237,11 +248,11 @@ export async function downloadMailRaw(
|
||||
export interface ServiceStatus {
|
||||
name: string;
|
||||
display_name: string;
|
||||
active: string; // active | inactive | failed | unknown
|
||||
sub: string; // running | dead | exited | ...
|
||||
enabled: string; // enabled | disabled | static | unknown
|
||||
active: string;
|
||||
sub: string;
|
||||
enabled: string;
|
||||
description: string;
|
||||
external_blocked?: boolean; // only present for archivmail
|
||||
external_blocked?: boolean;
|
||||
}
|
||||
|
||||
export async function getServices(): Promise<ServiceStatus[]> {
|
||||
@@ -397,9 +408,8 @@ export async function getSystemStats(): Promise<SystemStats> {
|
||||
// ── Export ────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function exportMailPDF(id: string): Promise<{ blob: Blob; filename: string }> {
|
||||
const token = getToken();
|
||||
const res = await fetch(`${API_BASE}/api/export/pdf/${id}`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) throw new Error("PDF export failed");
|
||||
const blob = await res.blob();
|
||||
@@ -409,16 +419,12 @@ export async function exportMailPDF(id: string): Promise<{ blob: Blob; filename:
|
||||
}
|
||||
|
||||
export async function exportMailsZIP(ids: string[], attachments: boolean): Promise<{ blob: Blob }> {
|
||||
const token = getToken();
|
||||
const res = await fetch(`${API_BASE}/api/export/zip`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
headers: { "Content-Type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ ids, attachments }),
|
||||
});
|
||||
if (!res.ok) throw new Error("ZIP export failed");
|
||||
const blob = await res.blob();
|
||||
return { blob };
|
||||
return { blob: await res.blob() };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user