feat(settings): Settings page with email, webhooks, notifications, profile [FASE-8]
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

- SystemConfig model + migration b033 for runtime key-value config
- GET/PATCH /system/email-config + POST /system/email-test (admin only)
- email_service reads SMTP config from DB (overrides .env)
- Webhooks now accessible to red_lead/blue_lead + admin
- GET /users/me already existed; /users/me/preferences already working
- SettingsPage with 4 role-aware tabs:
  * Profile & Jira: jira_account_id, user info
  * Notifications: role-specific email/in-app toggles (12 prefs)
  * Webhooks: full CRUD + test ping (leads + admin)
  * Email/SMTP: enable toggle, server config, test email (admin only)
- Added /settings route (all authenticated users)
- Settings link added to Sidebar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-05-19 15:10:31 +02:00
parent 93ebcf2b86
commit 0e1b8e2b39
10 changed files with 1493 additions and 24 deletions

View File

@@ -28,6 +28,7 @@ const ThreatActorDetailPage = React.lazy(() => import("./pages/ThreatActorDetail
const CampaignsPage = React.lazy(() => import("./pages/CampaignsPage"));
const CampaignDetailPage = React.lazy(() => import("./pages/CampaignDetailPage"));
const ComparisonPage = React.lazy(() => import("./pages/ComparisonPage"));
const SettingsPage = React.lazy(() => import("./pages/SettingsPage"));
export default function App() {
return (
@@ -92,6 +93,9 @@ export default function App() {
{/* ── Reports ──────────────────────────────────────────── */}
<Route path="/reports" element={<Suspense fallback={<LoadingSpinner text="Loading…" />}><ReportsPage /></Suspense>} />
{/* ── Settings (all authenticated users) ───────────────── */}
<Route path="/settings" element={<Suspense fallback={<LoadingSpinner text="Loading…" />}><SettingsPage /></Suspense>} />
{/* ── System (admin only) ──────────────────────────────── */}
<Route
path="/system"

View File

@@ -0,0 +1,147 @@
import client from "./client";
// ---------------------------------------------------------------------------
// Email / SMTP config (admin only)
// ---------------------------------------------------------------------------
export interface EmailConfigOut {
enabled: boolean;
host: string;
port: number;
username: string;
from_email: string;
use_tls: boolean;
}
export interface EmailConfigUpdate {
enabled?: boolean;
host?: string;
port?: number;
username?: string;
password?: string;
from_email?: string;
use_tls?: boolean;
}
export async function getEmailConfig(): Promise<EmailConfigOut> {
const { data } = await client.get<EmailConfigOut>("/system/email-config");
return data;
}
export async function updateEmailConfig(payload: EmailConfigUpdate): Promise<EmailConfigOut> {
const { data } = await client.patch<EmailConfigOut>("/system/email-config", payload);
return data;
}
export async function sendTestEmail(to: string): Promise<{ detail: string }> {
const { data } = await client.post<{ detail: string }>("/system/email-test", { to });
return data;
}
// ---------------------------------------------------------------------------
// Webhook config (admin + leads)
// ---------------------------------------------------------------------------
export interface WebhookOut {
id: string;
name: string;
url: string;
secret: string | null;
events: string[];
is_active: boolean;
created_at: string | null;
last_triggered_at: string | null;
failure_count: number;
}
export interface WebhookCreate {
name: string;
url: string;
secret?: string;
events: string[];
is_active?: boolean;
}
export interface WebhookUpdate {
name?: string;
url?: string;
secret?: string;
events?: string[];
is_active?: boolean;
}
export async function getWebhooks(): Promise<WebhookOut[]> {
const { data } = await client.get<WebhookOut[]>("/webhooks");
return data;
}
export async function createWebhook(payload: WebhookCreate): Promise<WebhookOut> {
const { data } = await client.post<WebhookOut>("/webhooks", payload);
return data;
}
export async function updateWebhook(id: string, payload: WebhookUpdate): Promise<WebhookOut> {
const { data } = await client.patch<WebhookOut>(`/webhooks/${id}`, payload);
return data;
}
export async function deleteWebhook(id: string): Promise<void> {
await client.delete(`/webhooks/${id}`);
}
export async function testWebhook(id: string): Promise<{ detail: string }> {
const { data } = await client.post<{ detail: string }>(`/webhooks/${id}/test`);
return data;
}
// ---------------------------------------------------------------------------
// User preferences (all users)
// ---------------------------------------------------------------------------
export interface NotificationPreferences {
// Universal
email_on_test_validated: boolean;
email_on_test_rejected: boolean;
email_on_campaign_completed: boolean;
email_on_new_mitre_techniques: boolean;
email_on_stale_coverage: boolean;
in_app_all: boolean;
// Tech + leads
email_on_assigned_to_campaign?: boolean;
email_on_test_state_change?: boolean;
// Leads only
email_on_all_team_validations?: boolean;
email_on_webhook_failures?: boolean;
// Admin only
email_on_new_users?: boolean;
email_on_system_errors?: boolean;
}
export interface UserPreferencesUpdate {
notification_preferences?: Partial<NotificationPreferences>;
jira_account_id?: string | null;
}
export interface UserMeOut {
id: string;
username: string;
email: string | null;
role: string;
is_active: boolean;
must_change_password: boolean;
created_at: string | null;
last_login: string | null;
notification_preferences: NotificationPreferences | null;
jira_account_id: string | null;
}
export async function getMe(): Promise<UserMeOut> {
const { data } = await client.get<UserMeOut>("/users/me");
return data;
}
export async function updateMyPreferences(payload: UserPreferencesUpdate): Promise<UserMeOut> {
const { data } = await client.patch<UserMeOut>("/users/me/preferences", payload);
return data;
}

View File

@@ -50,6 +50,7 @@ const mainLinks: NavItem[] = [
{ to: "/compliance", label: "Compliance", icon: ShieldCheck },
{ to: "/comparison", label: "Comparison", icon: GitCompareArrows, roles: ["admin", "red_lead", "blue_lead", "viewer"] },
{ to: "/reports", label: "Reports", icon: BarChart3 },
{ to: "/settings", label: "Settings", icon: Settings },
];
const systemLinks: NavItem[] = [

File diff suppressed because it is too large Load Diff