release: v1.32.0 — hardening and scalability (#580)

* release: prepare v1.32.0

* test: stabilize water capture re-entry
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-07-22 04:14:15 +02:00
1 parent b01822be48
commit 62eb431d66
372 files changed
+25178 -8561

No files matched your search

+100 -53
View File
@@ -3,6 +3,7 @@ import { apiSuccess, apiError, safeJson } from "@/lib/api-response";
import { decrypt, encrypt } from "@/lib/crypto";
import { deleteTelegramWebhook, setTelegramWebhook } from "@/lib/telegram";
import { telegramSettingsSchema } from "@/lib/validations/telegram";
import { notificationChannelEnabledSchema } from "@/lib/validations/notifications";
import { NextRequest } from "next/server";
import { z } from "zod/v4";
import { apiHandler, requireAuth } from "@/lib/api-handler";
@@ -33,6 +34,64 @@ export const GET = apiHandler(async () => {
});
});
async function registerTelegramWebhook(botToken: string) {
const appUrl = process.env.APP_URL ?? process.env.NEXT_PUBLIC_APP_URL;
if (!appUrl) {
return apiError(
"Server configuration error: APP_URL (or NEXT_PUBLIC_APP_URL) is missing.",
500,
);
}
let appBaseUrl: URL;
try {
appBaseUrl = new URL(appUrl);
} catch {
return apiError("Server configuration error: APP_URL is invalid.", 500);
}
if (appBaseUrl.protocol !== "https:") {
return apiError(
"Telegram webhook requires a public HTTPS URL (no http/localhost).",
422,
);
}
const localHosts: Record<string, true> = {
localhost: true,
"127.0.0.1": true,
"::1": true,
};
if (localHosts[appBaseUrl.hostname.toLowerCase()]) {
return apiError(
"Telegram webhook requires a publicly reachable domain (localhost is not allowed).",
422,
);
}
if (
appBaseUrl.port &&
!["80", "88", "443", "8443"].includes(appBaseUrl.port)
) {
return apiError(
"Telegram webhook only allows ports 80, 88, 443, or 8443.",
422,
);
}
const webhookUrl = new URL("/api/telegram/webhook", appBaseUrl).toString();
const webhookSecret = process.env.TELEGRAM_WEBHOOK_SECRET;
const ok = await setTelegramWebhook(botToken, webhookUrl, webhookSecret);
if (!ok) {
return apiError(
"Failed to set Telegram webhook. Check bot token and reachability.",
422,
);
}
return null;
}
/**
* Update Telegram notification settings.
*/
@@ -54,6 +113,45 @@ export const PUT = apiHandler(async (request: NextRequest) => {
});
if (jsonError) return jsonError;
const enabledOnly = notificationChannelEnabledSchema.safeParse(body);
if (enabledOnly.success) {
const { enabled } = enabledOnly.data;
if (enabled && (!current.telegramBotToken || !current.telegramChatId)) {
return apiError(
"Bot token and chat ID are required when Telegram is enabled",
422,
);
}
const currentTokenPlain = current.telegramBotToken
? decrypt(current.telegramBotToken)
: null;
if (enabled && currentTokenPlain) {
const webhookError = await registerTelegramWebhook(currentTokenPlain);
if (webhookError) return webhookError;
}
await prisma.user.update({
where: { id: user.id },
data: { telegramEnabled: enabled },
});
await prisma.notificationChannel.updateMany({
where: { userId: user.id, type: "TELEGRAM" },
data: { enabled },
});
if (!enabled && currentTokenPlain) {
await deleteTelegramWebhook(currentTokenPlain).catch(() => {});
}
annotate({
action: { name: "settings.telegram.update" },
meta: { enabled },
});
return apiSuccess({ updated: true });
}
const result = z.safeParse(telegramSettingsSchema, body);
if (!result.success) {
return apiError("Invalid input", 422);
@@ -81,59 +179,8 @@ export const PUT = apiHandler(async (request: NextRequest) => {
const tokenForWebhook = trimmedToken || currentTokenPlain;
if (enabled && tokenForWebhook) {
const appUrl = process.env.APP_URL ?? process.env.NEXT_PUBLIC_APP_URL;
if (!appUrl) {
return apiError(
"Server configuration error: APP_URL (or NEXT_PUBLIC_APP_URL) is missing.",
500,
);
}
let appBaseUrl: URL;
try {
appBaseUrl = new URL(appUrl);
} catch {
return apiError("Server configuration error: APP_URL is invalid.", 500);
}
if (appBaseUrl.protocol !== "https:") {
return apiError(
"Telegram webhook requires a public HTTPS URL (no http/localhost).",
422,
);
}
const localHosts = new Set(["localhost", "127.0.0.1", "::1"]);
if (localHosts.has(appBaseUrl.hostname.toLowerCase())) {
return apiError(
"Telegram webhook requires a publicly reachable domain (localhost is not allowed).",
422,
);
}
if (
appBaseUrl.port &&
!["80", "88", "443", "8443"].includes(appBaseUrl.port)
) {
return apiError(
"Telegram webhook only allows ports 80, 88, 443, or 8443.",
422,
);
}
const webhookUrl = new URL("/api/telegram/webhook", appBaseUrl).toString();
const webhookSecret = process.env.TELEGRAM_WEBHOOK_SECRET;
const ok = await setTelegramWebhook(
tokenForWebhook,
webhookUrl,
webhookSecret,
);
if (!ok) {
return apiError(
"Failed to set Telegram webhook. Check bot token and reachability.",
422,
);
}
const webhookError = await registerTelegramWebhook(tokenForWebhook);
if (webhookError) return webhookError;
}
const data: Record<string, unknown> = { telegramEnabled: enabled };