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
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:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user