/* SPDX-FileCopyrightText: © 2025 Hornwitser SPDX-License-Identifier: AGPL-3.0-or-later */ import * as fs from "node:fs/promises"; import type { H3Event } from "h3"; import { z } from "zod/v4-mini"; import { readAuthenticationMethods, readUsers } from "~/server/database"; import { type TelegramAuthData, telegramAuthDataSchema } from "~/shared/types/telegram"; import type { ApiSession } from "~/shared/types/api"; const loginSchema = z.object({ authData: telegramAuthDataSchema, }); let cachedTelegramConfig: | undefined | { enabled: false } | { enabled: true, botUsername: string, secretKey: CryptoKey } ; async function useTelegramConfig(event: H3Event) { if (cachedTelegramConfig) return cachedTelegramConfig; const runtimeConfig = useRuntimeConfig(event); if (!runtimeConfig.public.authTelegramEnabled) { return cachedTelegramConfig = { enabled: false, }; } if (!runtimeConfig.telegramBotTokenFile) throw new Error("NUXT_TELEGRAM_BOT_TOKEN_FILE not configured"); if (!runtimeConfig.public.telegramBotUsername) throw new Error("NUXT_PUBLIC_TELEGRAM_BOT_USERNAME not configured"); const botToken = await fs.readFile(runtimeConfig.telegramBotTokenFile); const secretKey = await crypto.subtle.importKey( "raw", await crypto.subtle.digest("SHA-256", botToken), { name: "HMAC", hash: "SHA-256", }, false, ["verify"], ); return cachedTelegramConfig = { enabled: true, botUsername: runtimeConfig.public.telegramBotUsername, secretKey, } } async function validateTelegramAuthData(authData: TelegramAuthData, key: CryptoKey) { const { hash, ...checkData } = authData; const checkString = Object.entries(checkData).map(([key, value]) => `${key}=${value}`).sort().join("\n"); const signature = Buffer.from(hash, "hex"); return await crypto.subtle.verify("HMAC", key, signature, Buffer.from(checkString)); } export default defineEventHandler(async (event): Promise => { const { success, error, data } = loginSchema.safeParse(await readBody(event)); if (!success) { throw createError({ statusCode: 400, statusMessage: "Bad Request", message: z.prettifyError(error), }); } const config = await useTelegramConfig(event); if (!config.enabled) { throw createError({ statusCode: 403, statusMessage: "Forbidden", message: "Telegram authentication is disabled", }); } if (!await validateTelegramAuthData(data.authData, config.secretKey)) { throw createError({ statusCode: 403, statusMessage: "Forbidden", message: "Validating authentication data failed", }); } const slug = String(data.authData.id); const authMethods = await readAuthenticationMethods(); const method = authMethods.find(method => method.provider === "telegram" && method.slug === slug); let session; if (method) { const users = await readUsers(); const account = users.find(user => !user.deleted && user.id === method.userId); session = await setServerSession(event, account); } else { const name = data.authData.username ? "@" + data.authData.username : slug; session = await setServerSession(event, undefined, "telegram", slug, name); } return await serverSessionToApi(event, session); })