75 lines
2.7 KiB
TypeScript
75 lines
2.7 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { fetchMeteoWarnings } from "@/lib/server-warnings";
|
|
import { getPushSubscriptions, hasSentWarning, markWarningSent, pruneSentWarnings, removePushSubscription } from "@/lib/push-store";
|
|
import { isWebPushConfigured, sendWarningNotification } from "@/lib/push-service";
|
|
import type { WeatherWarning } from "@/types/imgw";
|
|
|
|
export const runtime = "nodejs";
|
|
|
|
function getTimestamp(value: string | null) {
|
|
if (!value) return null;
|
|
const timestamp = new Date(value).getTime();
|
|
return Number.isNaN(timestamp) ? null : timestamp;
|
|
}
|
|
|
|
function isRelevantWarning(warning: WeatherWarning, now: number) {
|
|
const validTo = getTimestamp(warning.validTo);
|
|
return warning.kind === "meteo" && (validTo === null || validTo > now);
|
|
}
|
|
|
|
function isCronAuthorized(request: Request) {
|
|
const secret = process.env.NOTIFICATIONS_CRON_SECRET;
|
|
if (!secret) return process.env.NODE_ENV !== "production";
|
|
const authHeader = request.headers.get("authorization");
|
|
const secretHeader = request.headers.get("x-cron-secret");
|
|
return authHeader === `Bearer ${secret}` || secretHeader === secret;
|
|
}
|
|
|
|
export async function GET(request: Request) {
|
|
if (!isCronAuthorized(request)) {
|
|
return NextResponse.json({ error: "Unauthorized." }, { status: 401 });
|
|
}
|
|
if (!isWebPushConfigured()) {
|
|
return NextResponse.json({ error: "Web Push is not configured." }, { status: 503 });
|
|
}
|
|
|
|
const now = Date.now();
|
|
const warnings = (await fetchMeteoWarnings(AbortSignal.timeout(12_000))).filter((warning) => isRelevantWarning(warning, now));
|
|
const subscriptions = getPushSubscriptions().filter((subscription) => subscription.enabled);
|
|
const activeWarningIds = new Set(warnings.map((warning) => warning.id));
|
|
let sent = 0;
|
|
let skipped = 0;
|
|
let failed = 0;
|
|
|
|
pruneSentWarnings(activeWarningIds);
|
|
|
|
for (const subscription of subscriptions) {
|
|
const matchingWarnings = warnings.filter((warning) => warning.provinces.includes(subscription.province));
|
|
for (const warning of matchingWarnings) {
|
|
if (hasSentWarning(subscription.endpoint, warning.id)) {
|
|
skipped += 1;
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
await sendWarningNotification(subscription, warning);
|
|
markWarningSent(subscription.endpoint, warning.id);
|
|
sent += 1;
|
|
} catch (error) {
|
|
failed += 1;
|
|
const statusCode = typeof error === "object" && error !== null && "statusCode" in error ? Number(error.statusCode) : null;
|
|
if (statusCode === 404 || statusCode === 410) removePushSubscription(subscription.endpoint);
|
|
}
|
|
}
|
|
}
|
|
|
|
return NextResponse.json({
|
|
ok: true,
|
|
subscriptions: subscriptions.length,
|
|
warnings: warnings.length,
|
|
sent,
|
|
skipped,
|
|
failed,
|
|
});
|
|
}
|