mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
A loopback, link-local or metadata target answered private_origin_not_approved with a message telling the user to have the operator list it, and the parser refuses such an entry forever: a dead end the route test even pinned. Those targets now answer private_origin_not_grantable (catalogued, on all four routes, translated in seven locales) with a message that says no grant can open them and to use the relay's LAN address. Both senders carry the reason into the outcome so the test button forwards the same code. The malformed-entry warning printed the entry verbatim, which is where a pasted token or userinfo sits; the parser now hands callers scheme and host only, plus the reason in words. The Nightscout list throws a named error with that redacted entry and reason, and the connect route answers it as a 422 with the message instead of a bare 500, so a self-host that listed http://localhost:1337 reads what to change. The two settings cards translate a coded save refusal like the test button does; the docs and the changelog name the sub-host and sibling- port narrowing; the dispatcher floor tests assert the policy marker so a real NXDOMAIN cannot pass for a refusal. Refs #947
70 lines
2.4 KiB
TypeScript
70 lines
2.4 KiB
TypeScript
import { prisma } from "@/lib/db";
|
|
import { apiSuccess, apiError } from "@/lib/api-response";
|
|
import { decrypt } from "@/lib/crypto";
|
|
import { sendViaWebhook } from "@/lib/notifications/senders/webhook";
|
|
import type { WebhookChannelConfig } from "@/lib/notifications/types";
|
|
import { checkRateLimit } from "@/lib/rate-limit";
|
|
import { apiHandler, requireAuth } from "@/lib/api-handler";
|
|
import { annotate } from "@/lib/logging/context";
|
|
|
|
/**
|
|
* POST: send a test notification via the configured generic webhook.
|
|
*/
|
|
export const POST = apiHandler(async () => {
|
|
const { user } = await requireAuth();
|
|
|
|
const rl = await checkRateLimit(`webhook-test:${user.id}`, 5, 5 * 60 * 1000);
|
|
if (!rl.allowed) {
|
|
return apiError("Maximum 5 tests in 5 minutes", 429);
|
|
}
|
|
|
|
const channel = await prisma.notificationChannel.findUnique({
|
|
where: { userId_type: { userId: user.id, type: "WEBHOOK" } },
|
|
});
|
|
|
|
if (!channel) {
|
|
return apiError("Webhook is not configured", 400);
|
|
}
|
|
|
|
const config = JSON.parse(decrypt(channel.config)) as WebhookChannelConfig;
|
|
|
|
if (!config.url) {
|
|
return apiError("Webhook URL is required", 400);
|
|
}
|
|
|
|
const result = await sendViaWebhook(config, {
|
|
eventType: "SYSTEM_ALERT",
|
|
userId: user.id,
|
|
title: "HealthLog Test",
|
|
message:
|
|
"HealthLog: Connection successful! Webhook notifications are active.",
|
|
});
|
|
|
|
if (!result.ok) {
|
|
// A private-origin refusal is a policy decision, not a delivery fault:
|
|
// say so, with the code the card translates, instead of a bare 500 that
|
|
// sends the operator to the wide-event log to learn why (#947).
|
|
if (result.errorCode) {
|
|
annotate({
|
|
action: { name: "settings.webhook.test" },
|
|
meta: { success: false, refused: result.errorCode },
|
|
});
|
|
return apiError(
|
|
result.errorCode === "private_origin_not_grantable"
|
|
? "The target is loopback, link-local or a metadata address, which no operator grant can open. Use the relay's LAN address instead."
|
|
: "The target is on a private network the operator has not approved. List its exact origin (scheme://host:port) in NOTIFICATION_PRIVATE_ORIGINS on the server.",
|
|
422,
|
|
{ errorCode: result.errorCode },
|
|
);
|
|
}
|
|
return apiError("Failed to send test message", 500);
|
|
}
|
|
|
|
annotate({
|
|
action: { name: "settings.webhook.test" },
|
|
meta: { success: true },
|
|
});
|
|
|
|
return apiSuccess({ sent: true });
|
|
});
|