feat(email): generic webhook for all notification emails, hide SMTP UI
Aegis CI / lint-and-test (push) Has been cancelled
Snyk Security Scan / Python vulnerabilities (backend) (push) Has been cancelled
Snyk Security Scan / npm vulnerabilities (frontend) (push) Has been cancelled
Snyk Security Scan / Docker image vulnerabilities (backend) (push) Has been cancelled

- Renamed the password-setup webhook (email_webhook.*, /system/email-webhook-config)
  and made it the single transport for all notification emails.
- New webhook_email_service.py shared by password_setup_service and
  notification_service.
- Wired test validated/rejected, campaign completed, and new-MITRE-technique
  notifications through the webhook (previously dead SMTP code, never triggered).
- Added POST /system/email-webhook-test to send a real test email.
- Hid the Email/SMTP settings tab from the UI (SMTP code kept intact, unused).
- Redact email_webhook.api_key on config export.
This commit is contained in:
kitos
2026-07-20 10:24:15 +02:00
parent bbe7f49c86
commit 1d0d880929
11 changed files with 442 additions and 146 deletions
+11 -6
View File
@@ -199,29 +199,34 @@ export async function testJiraConnection(): Promise<{
return data;
}
export interface PasswordWebhookConfigOut {
export interface EmailWebhookConfigOut {
configured: boolean;
url: string;
api_key_set: boolean;
}
export async function getPasswordWebhookConfig(): Promise<PasswordWebhookConfigOut> {
const { data } = await client.get<PasswordWebhookConfigOut>("/system/password-webhook-config");
export async function getEmailWebhookConfig(): Promise<EmailWebhookConfigOut> {
const { data } = await client.get<EmailWebhookConfigOut>("/system/email-webhook-config");
return data;
}
/** apiKey is optional — omit or pass "" to leave the currently-stored key unchanged. */
export async function updatePasswordWebhookConfig(
export async function updateEmailWebhookConfig(
url: string,
apiKey?: string,
): Promise<PasswordWebhookConfigOut> {
const { data } = await client.patch<PasswordWebhookConfigOut>("/system/password-webhook-config", {
): Promise<EmailWebhookConfigOut> {
const { data } = await client.patch<EmailWebhookConfigOut>("/system/email-webhook-config", {
url,
api_key: apiKey || undefined,
});
return data;
}
export async function testEmailWebhook(to: string): Promise<{ detail: string }> {
const { data } = await client.post<{ detail: string }>("/system/email-webhook-test", { to });
return data;
}
export async function testTempoConnection(): Promise<{
status: "ok" | "error" | "disabled";
message?: string;
+46 -16
View File
@@ -53,8 +53,9 @@ import {
getJiraConfig,
updateJiraConfig,
testJiraConnection,
getPasswordWebhookConfig,
updatePasswordWebhookConfig,
getEmailWebhookConfig,
updateEmailWebhookConfig,
testEmailWebhook,
testTempoConnection,
type EmailConfigUpdate,
type WebhookCreate,
@@ -1685,15 +1686,16 @@ function SystemInfoSection() {
);
}
function PasswordWebhookSection() {
function EmailWebhookSection() {
const qc = useQueryClient();
const [url, setUrl] = useState("");
const [apiKey, setApiKey] = useState("");
const [testTo, setTestTo] = useState("");
const [toast, setToast] = useState<{ msg: string; type: "success" | "error" } | null>(null);
const { data, isLoading } = useQuery({
queryKey: ["password-webhook-config"],
queryFn: getPasswordWebhookConfig,
queryKey: ["email-webhook-config"],
queryFn: getEmailWebhookConfig,
});
useEffect(() => {
@@ -1701,15 +1703,22 @@ function PasswordWebhookSection() {
}, [data]);
const saveMutation = useMutation({
mutationFn: () => updatePasswordWebhookConfig(url, apiKey),
mutationFn: () => updateEmailWebhookConfig(url, apiKey),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["password-webhook-config"] });
qc.invalidateQueries({ queryKey: ["email-webhook-config"] });
setApiKey("");
setToast({ msg: "Webhook configuration saved", type: "success" });
},
onError: () => setToast({ msg: "Failed to save webhook configuration", type: "error" }),
});
const testMutation = useMutation({
mutationFn: () => testEmailWebhook(testTo),
onSuccess: () => setToast({ msg: `Test email sent to ${testTo}`, type: "success" }),
onError: () =>
setToast({ msg: "Failed to send test email. Check the webhook URL/API key and logs.", type: "error" }),
});
if (isLoading) {
return (
<div className="flex justify-center py-8">
@@ -1721,14 +1730,14 @@ function PasswordWebhookSection() {
return (
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
<h2 className="mb-2 text-lg font-semibold text-white flex items-center gap-2">
<Mail className="h-5 w-5 text-cyan-400" /> Password Setup Email Webhook
<Mail className="h-5 w-5 text-cyan-400" /> Email Webhook
</h2>
<p className="mb-4 text-sm text-gray-500">
When an admin sends a set-password or password-reset link from User Management, a{" "}
<code className="text-gray-400">{"{ to, subject, body }"}</code> payload is POSTed to
this URL point it at your Power Automate flow (or similar) that actually delivers the
email. The API key, if set, is sent as an <code className="text-gray-400">x-api-key</code>{" "}
header.
All platform notification emails password setup/reset, test validated/rejected,
campaign completed, new MITRE techniques, and more are sent by POSTing a{" "}
<code className="text-gray-400">{"{ to, subject, body }"}</code> payload to this URL
point it at your Power Automate flow (or similar) that actually delivers the email. The
API key, if set, is sent as an <code className="text-gray-400">x-api-key</code> header.
</p>
<div className="space-y-3">
<div>
@@ -1766,9 +1775,30 @@ function PasswordWebhookSection() {
</div>
{!data?.configured && (
<p className="mt-2 text-xs text-amber-400/80">
Not configured yet "Send Email" in User Management will fail until a URL is saved here.
Not configured yet notification emails will silently fail to send until a URL is
saved here.
</p>
)}
<div className="mt-6 border-t border-gray-800 pt-4">
<p className="mb-3 text-sm font-medium text-gray-300">Send Test Email</p>
<div className="flex gap-2">
<input
type="email"
value={testTo}
onChange={(e) => setTestTo(e.target.value)}
placeholder="you@company.com"
className="flex-1 rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-sm text-gray-300 placeholder-gray-500 focus:border-cyan-500 focus:outline-none"
/>
<button
onClick={() => testTo && testMutation.mutate()}
disabled={testMutation.isPending || !testTo}
className="flex items-center gap-2 rounded-lg bg-gray-700 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-gray-600 disabled:opacity-50"
>
{testMutation.isPending && <Loader2 className="h-4 w-4 animate-spin" />}
Send Test
</button>
</div>
</div>
{toast && (
<Toast message={toast.msg} type={toast.type} onClose={() => setToast(null)} />
)}
@@ -1799,7 +1829,7 @@ export default function SettingsPage() {
icon: Webhook,
show: isAdmin,
},
{ id: "email", label: "Email / SMTP", icon: Mail, show: isAdmin },
{ id: "email", label: "Email / SMTP", icon: Mail, show: false },
{ id: "jira", label: "Jira", icon: Link2, show: isAdmin },
{ id: "sso", label: "SSO / Azure AD", icon: KeyRound, show: isAdmin },
{ id: "system", label: "System", icon: Server, show: isAdmin },
@@ -1873,7 +1903,7 @@ export default function SettingsPage() {
{activeTab === "system" && isAdmin && (
<div className="space-y-6">
<SystemInfoSection />
<PasswordWebhookSection />
<EmailWebhookSection />
<ExportImportSection />
</div>
)}