105 lines
3.4 KiB
TypeScript
105 lines
3.4 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 { warningMatchesCounty } from "@/lib/warning-regions";
|
|
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;
|
|
}
|
|
|
|
function getErrorMessage(error: unknown) {
|
|
return error instanceof Error ? error.message : "Unknown notification check error.";
|
|
}
|
|
|
|
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 });
|
|
}
|
|
|
|
try {
|
|
const now = Date.now();
|
|
const warnings = (await fetchMeteoWarnings(AbortSignal.timeout(12_000))).filter((warning) =>
|
|
isRelevantWarning(warning, now),
|
|
);
|
|
const subscriptions = getPushSubscriptions().filter(
|
|
(subscription) => subscription.region === "PL" && subscription.enabled && subscription.province,
|
|
);
|
|
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) =>
|
|
subscription.countyTeryt
|
|
? warningMatchesCounty(warning, subscription.countyTeryt)
|
|
: subscription.province !== null && 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,
|
|
});
|
|
} catch (error) {
|
|
return NextResponse.json(
|
|
{
|
|
error: "Unable to check IMGW meteorological warnings.",
|
|
details: getErrorMessage(error),
|
|
},
|
|
{ status: 502 },
|
|
);
|
|
}
|
|
}
|