Files
HealthLog/src/app/api/notifications/apns/test/route.ts
T
Rob de Graaf c6f61fad33 feat(i18n): add Dutch as an eighth end-to-end locale
Adds messages/nl.json (AI-initial, informal je/jij register, full key
parity with en.json) and registers nl everywhere the locale list lives:
config, loaders, Intl/date-fns mapping, zod enums, OpenAPI spec, scripts,
the managed-profile picker and the notification locale guards.

Dutch follows the Korean pattern for the AI surfaces (English body plus a
Dutch reply-language footer, no safety-contract matrix) and gets its own
banks in the outbound safety screen (dose, risk, causal, continuation),
the prose-grounding and citation-coverage tables, the reference-range
prose and the no-key fallback copy, each with Dutch test fixtures.

PHQ-9 / GAD-7 / WHO-5 are not marked validated for nl yet: the Dutch
wording has not been read against the published forms, so Dutch readers
get the English items plus the existing note until it has.
2026-10-19 11:06:55 +02:00

88 lines
3.1 KiB
TypeScript

/**
* v1.4.47.6 — per-channel APNs test endpoint.
*
* Mirrors `/api/notifications/web-push/test`: fires a single self-test
* push to the calling user's iOS devices via `sendViaApns`. The
* `notification-status-card` UI on the Settings page maps the
* "Test senden" button on the APNS channel row to this endpoint.
*
* Why a separate endpoint per channel instead of `/api/admin/notifications/test`:
* - The admin test endpoint requires admin role; the per-channel test
* is available to any authenticated user who owns the channel.
* - The admin variant fans out to ALL channels at once; the per-channel
* variant lets the user verify a single channel after a fix without
* spamming the other channels too.
*/
import type { NextRequest } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { apiSuccess, apiError } from "@/lib/api-response";
import { checkRateLimit } from "@/lib/rate-limit";
import { annotate } from "@/lib/logging/context";
import { sendViaApns } from "@/lib/notifications/senders/apns";
import { defaultLocale, type Locale } from "@/lib/i18n/config";
import { getServerTranslator } from "@/lib/i18n/server-translator";
import { prisma } from "@/lib/db";
export const dynamic = "force-dynamic";
function isLocale(value: string | null | undefined): value is Locale {
return (
value === "de" ||
value === "en" ||
value === "fr" ||
value === "es" ||
value === "it" ||
value === "pl" ||
value === "ko" ||
value === "nl"
);
}
export const POST = apiHandler(async (request: NextRequest) => {
void request;
const { user } = await requireAuth();
annotate({ action: { name: "notifications.apns.test" } });
const rl = await checkRateLimit(`apns-test:${user.id}`, 5, 60_000);
if (!rl.allowed) {
return apiError("Too many test requests", 429, {
errorCode: "rate_limited_self",
});
}
const localeRow = await prisma.user.findUnique({
where: { id: user.id },
select: { locale: true },
});
const locale: Locale = isLocale(localeRow?.locale)
? localeRow.locale
: defaultLocale;
const t = getServerTranslator(locale).t;
// Send as MEDICATION_REMINDER so the dispatcher's time-sensitive /
// priority-10 branch fires. Otherwise the test runs at the default
// `active` interruption-level and iOS may summarise it into the
// Notification Center rather than presenting a lock-screen banner —
// leaving the user with no signal whether real medication reminders
// (which DO use time-sensitive) will surface on the lock screen.
// The title/body strings still read "Test notification" so the user
// doesn't mistake this for an actual scheduled dose.
const result = await sendViaApns(user.id, {
title: t("notifications.admin.testNotificationTitle"),
message: t("notifications.admin.testNotificationBody"),
eventType: "MEDICATION_REMINDER",
});
annotate({
meta: {
apns_test_ok: result.ok,
apns_test_reason: result.ok ? undefined : result.reason,
},
});
return apiSuccess({
ok: result.ok,
reason: result.ok ? undefined : result.reason,
});
});