2025-07-09 15:21:39 +02:00
|
|
|
/*
|
|
|
|
SPDX-FileCopyrightText: © 2025 Hornwitser <code@hornwitser.no>
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
*/
|
|
|
|
import * as fs from "node:fs/promises";
|
|
|
|
import type { H3Event } from "h3";
|
2025-07-09 18:08:39 +02:00
|
|
|
import { z } from "zod/v4-mini";
|
2025-07-09 15:21:39 +02:00
|
|
|
import { readAuthenticationMethods, readUsers } from "~/server/database";
|
|
|
|
import { type TelegramAuthData, telegramAuthDataSchema } from "~/shared/types/telegram";
|
2025-07-09 18:08:39 +02:00
|
|
|
import type { ApiSession } from "~/shared/types/api";
|
2025-07-09 15:21:39 +02:00
|
|
|
|
|
|
|
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<ApiSession> => {
|
|
|
|
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);
|
|
|
|
})
|