Files
archivmail/src/lib/api/routing_rules.ts
T
sysopsandClaude Sonnet 5 767373b206 feat(PROJ-46): E-Mail als primärer Login-Identifier für Tenant-User
Tenant-User (tenant_id IS NOT NULL) melden sich künftig per E-Mail an statt
per Username — behebt Verwechslungen wie im Support-Fall vom 2026-06-13
(Login schlug trotz Passwort-Reset fehl, weil E-Mail statt Username
verwendet wurde). Nicht-Tenant-User (Superadmin/System) können weiterhin
Username ODER E-Mail nutzen.

Neue Store.VerifyLogin() prüft erst per E-Mail (alle User), fällt dann auf
Username zurück (nur tenant_id IS NULL). VerifyPassword() bleibt für den
IMAP-Server-Login-Pfad (PROJ-26) unverändert. Bewusster Breaking Change für
Tenant-User, Datenqualität vorab geprüft (0 Kollisionen).

Security-Nachtrag: bcrypt-Dummy-Compare im "user not found"-Pfad ergänzt,
um Timing-basierte Identifier-Enumeration zu verhindern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 23:37:20 +02:00

95 lines
2.5 KiB
TypeScript

import { request } from "./core";
// PROJ-43: Tenant-Routing-Regeln — automatische Mandanten-Zuordnung nach
// Domain/Absender-Mustern mit Priorität + Dry-Run.
// CRUD unter /api/admin/routing-rules (domain_admin+; superadmin = alle Tenants,
// domain_admin = eigener Tenant, serverseitig erzwungen).
// Match-Typen — müssen mit den Backend-Konstanten übereinstimmen.
export type RoutingMatchType =
| "from_domain"
| "to_domain"
| "from_addr"
| "to_addr";
export interface RoutingRule {
id: number;
tenant_id: number;
match_type: RoutingMatchType;
pattern: string;
priority: number;
created_at: string;
}
export interface RoutingRulesResponse {
rules: RoutingRule[] | null;
}
// tenant_id optional: nur superadmin muss ihn setzen; domain_admin bekommt
// serverseitig seinen eigenen Tenant erzwungen.
export interface RoutingRuleInput {
tenant_id?: number;
match_type: RoutingMatchType;
pattern: string;
priority: number;
}
export interface RoutingDryRunSample {
id: string;
mail_from: string;
mail_to: string;
subject: string;
received_at: string | null;
}
export interface RoutingDryRunResult {
match_type: RoutingMatchType;
pattern: string;
match_count: number;
sample_limit: number;
sample: RoutingDryRunSample[] | null;
}
// Dry-Run kann entweder gegen eine bestehende Regel (rule_id) oder gegen ein
// noch nicht gespeichertes Muster (match_type + pattern) laufen.
export type RoutingDryRunInput =
| { rule_id: number; limit?: number }
| { match_type: RoutingMatchType; pattern: string; limit?: number };
export async function getRoutingRules(): Promise<RoutingRule[]> {
const data = await request<RoutingRulesResponse>("/api/admin/routing-rules");
return data.rules ?? [];
}
export async function createRoutingRule(
input: RoutingRuleInput
): Promise<{ id: number }> {
return request<{ id: number }>("/api/admin/routing-rules", {
method: "POST",
body: JSON.stringify(input),
});
}
export async function updateRoutingRule(
id: number,
input: RoutingRuleInput
): Promise<void> {
await request<void>(`/api/admin/routing-rules/${id}`, {
method: "PUT",
body: JSON.stringify(input),
});
}
export async function deleteRoutingRule(id: number): Promise<void> {
await request<void>(`/api/admin/routing-rules/${id}`, { method: "DELETE" });
}
export async function dryRunRoutingRule(
input: RoutingDryRunInput
): Promise<RoutingDryRunResult> {
return request<RoutingDryRunResult>("/api/admin/routing-rules/dry-run", {
method: "POST",
body: JSON.stringify(input),
});
}