owltide/server/api/auth/ap/telegram-login.post.ts
Hornwitser 80cec71308
All checks were successful
/ build (push) Successful in 1m37s
/ deploy (push) Has been skipped
Use sync access for the temp JSON file database
Replace all async reads and writes to the JSON database with the sync
reads and writes to prevent a data corruption race condition where two
requests are processed at the same time and write to the same file, or
one reads while the other writes causing read of partially written data.
2025-09-20 23:04:16 +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 = readAuthenticationMethods();
const method = authMethods.find(method => method.provider === "telegram" && method.slug === slug);
let session;
if (method) {
const users = 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);
})