mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
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>
196 lines
5.3 KiB
TypeScript
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 });
|
|
});
|