owltide/server/api/auth/ap/telegram-login.post.ts

101 lines
3.1 KiB
TypeScript
Raw Normal View History

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