owltide/server/api/auth/ap/telegram-login.post.ts
Hornwitser f69381c44c
Some checks failed
/ build (push) Failing after 28s
/ deploy (push) Has been skipped
Set verbatimModuleSyntax for server code
The nuxi typecheck command complains about type only imports that are
not declared as such, but the VsCode environment does not.  There's
probably a missmatch somewhere in the configuration for Nuxt that I'm
not going to dig into.  Workaround this issue for now by setting the
option in the tsconfig.json file for the server.
2025-07-09 18:10:42 +02:00

100 lines
3.1 KiB
TypeScript

/*
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";
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<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);
})