Files
HealthLog/src/app/api/settings/telegram/route.ts
T
Marc-André BombeckandClaude Opus 4.7 02b99553b9 chore(style): repo-wide prettier sweep before v1.4.6 tag
Long-overdue prettier sweep; prettier had drifted vs. checked-in code
across ~130 files (mostly long-argument line-break wrapping in TS and
table-padding in markdown). Pure formatting — no semantic changes —
and required to clear the v1.4.6 release gate's `pnpm format:check`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 02:27:17 +02:00

196 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 });
});