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>
95 lines
2.5 KiB
TypeScript
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),
|
|
});
|
|
}
|