Files
HealthLog/src/app/api/settings/telegram/route.ts
T
Marc-André Bombeck 0c0c345e82 i18n: migrate from German-first to English-first default locale
Switch defaultLocale from "de" to "en" and make English the code-level
language throughout. All ~145 hardcoded German strings translated:

- API error messages (52 routes) now return English
- Zod validation messages in English
- Classification/analytics functions return English categories
- Notification type labels in English
- Component strings use i18n t() calls (heatmap, mood chart,
  medication form, intake history, password strength, insights card)
- ~50 new i18n keys in both de.json and en.json

Route migration:
- /stimmung → /mood, /zielwerte → /targets (new pages)
- 301 redirects in proxy.ts for legacy URLs
- Sidebar + bottom nav updated

Bump version to 1.0.1.
2026-03-12 21:31:48 +01:00

199 lines
5.3 KiB
TypeScript

import { prisma } from "@/lib/db";
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 { NextRequest } from "next/server";
import { z } from "zod/v4";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
/**
* Get Telegram notification settings for the current user.
* Never returns the bot token in plaintext.
*/
export const GET = apiHandler(async () => {
const { user } = await requireAuth();
const dbUser = await prisma.user.findUnique({
where: { id: user.id },
select: {
telegramBotToken: true,
telegramChatId: true,
telegramEnabled: true,
},
});
annotate({ action: { name: "settings.telegram.get" } });
return apiSuccess({
enabled: dbUser?.telegramEnabled ?? false,
hasBotToken: !!dbUser?.telegramBotToken,
chatId: dbUser?.telegramChatId ?? null,
});
});
/**
* Update Telegram notification settings.
*/
export const PUT = apiHandler(async (request: NextRequest) => {
const { user } = await requireAuth();
const current = await prisma.user.findUnique({
where: { id: user.id },
select: {
telegramBotToken: true,
telegramChatId: true,
telegramEnabled: true,
},
});
if (!current) return apiError("User not found", 404);
const { data: body, error: jsonError } = await safeJson(request);
if (jsonError) return jsonError;
const result = z.safeParse(telegramSettingsSchema, body);
if (!result.success) {
return apiError("Invalid input", 422);
}
const { botToken, chatId, enabled } = result.data;
const trimmedToken = botToken?.trim();
const trimmedChatId = chatId?.trim();
const hasTokenAfter =
botToken !== undefined ? !!trimmedToken : !!current.telegramBotToken;
const hasChatIdAfter =
chatId !== undefined ? !!trimmedChatId : !!current.telegramChatId;
if (enabled && (!hasTokenAfter || !hasChatIdAfter)) {
return apiError(
"Bot token and chat ID are required when Telegram is enabled",
422,
);
}
const currentTokenPlain = current.telegramBotToken
? decrypt(current.telegramBotToken)
: null;
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 data: Record<string, unknown> = { telegramEnabled: enabled };
if (botToken !== undefined) {
data.telegramBotToken = trimmedToken ? encrypt(trimmedToken) : null;
}
if (chatId !== undefined) {
data.telegramChatId = trimmedChatId || null;
}
await prisma.user.update({
where: { id: user.id },
data,
});
// Sync to NotificationChannel so the dispatcher can find Telegram
const finalToken = trimmedToken ? trimmedToken : currentTokenPlain;
const finalChatId = trimmedChatId ?? current.telegramChatId;
if (finalToken && finalChatId) {
const channelConfig = encrypt(
JSON.stringify({ botToken: finalToken, chatId: finalChatId }),
);
await prisma.notificationChannel.upsert({
where: {
userId_type: {
userId: user.id,
type: "TELEGRAM",
},
},
create: {
userId: user.id,
type: "TELEGRAM",
enabled,
config: channelConfig,
},
update: {
enabled,
config: channelConfig,
},
});
} else if (!enabled) {
// Remove the channel record if disabled and no credentials
await prisma.notificationChannel.deleteMany({
where: {
userId: user.id,
type: "TELEGRAM",
},
});
}
if (!enabled && tokenForWebhook) {
await deleteTelegramWebhook(tokenForWebhook).catch(() => {});
}
annotate({ action: { name: "settings.telegram.update" }, meta: { enabled } });
return apiSuccess({ updated: true });
});