Add Telegram ownership for voice reviews
All checks were successful
Build and deploy Backend / build (push) Successful in 1m1s
All checks were successful
Build and deploy Backend / build (push) Successful in 1m1s
This commit is contained in:
98
src/auth/telegram.ts
Normal file
98
src/auth/telegram.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { createHmac, timingSafeEqual } from 'node:crypto';
|
||||
|
||||
import { config } from '../config.js';
|
||||
import { prisma } from '../prisma.js';
|
||||
|
||||
type TelegramInitDataUser = {
|
||||
id: number;
|
||||
username?: string;
|
||||
first_name?: string;
|
||||
last_name?: string;
|
||||
photo_url?: string;
|
||||
language_code?: string;
|
||||
};
|
||||
|
||||
function hmacSha256(key: string | Buffer, value: string) {
|
||||
return createHmac('sha256', key).update(value).digest();
|
||||
}
|
||||
|
||||
function assertValidHash(receivedHash: string, expectedHash: Buffer) {
|
||||
const received = Buffer.from(receivedHash, 'hex');
|
||||
if (received.length !== expectedHash.length) {
|
||||
throw new Error('Telegram init data hash is invalid.');
|
||||
}
|
||||
|
||||
if (!timingSafeEqual(received, expectedHash)) {
|
||||
throw new Error('Telegram init data hash is invalid.');
|
||||
}
|
||||
}
|
||||
|
||||
function parseTelegramInitData(initData: string) {
|
||||
if (!config.telegramMiniAppBotToken) {
|
||||
throw new Error('TELEGRAM_MINI_APP_BOT_TOKEN is required.');
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(initData);
|
||||
const receivedHash = params.get('hash');
|
||||
if (!receivedHash) {
|
||||
throw new Error('Telegram init data hash is required.');
|
||||
}
|
||||
|
||||
const authDate = Number(params.get('auth_date'));
|
||||
if (!Number.isFinite(authDate)) {
|
||||
throw new Error('Telegram auth_date is required.');
|
||||
}
|
||||
|
||||
const ageSeconds = Math.floor(Date.now() / 1000) - authDate;
|
||||
if (ageSeconds > config.telegramAuthMaxAgeSeconds) {
|
||||
throw new Error('Telegram init data is expired.');
|
||||
}
|
||||
|
||||
const dataCheckString = [...params.entries()]
|
||||
.filter(([key]) => key !== 'hash')
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join('\n');
|
||||
|
||||
const secretKey = hmacSha256('WebAppData', config.telegramMiniAppBotToken);
|
||||
const expectedHash = hmacSha256(secretKey, dataCheckString);
|
||||
assertValidHash(receivedHash, expectedHash);
|
||||
|
||||
const rawUser = params.get('user');
|
||||
if (!rawUser) {
|
||||
throw new Error('Telegram user is required.');
|
||||
}
|
||||
|
||||
return JSON.parse(rawUser) as TelegramInitDataUser;
|
||||
}
|
||||
|
||||
export async function getOrCreateTelegramUser(initData: string) {
|
||||
const user = parseTelegramInitData(initData);
|
||||
|
||||
return prisma.user.upsert({
|
||||
where: { telegramId: String(user.id) },
|
||||
create: {
|
||||
telegramId: String(user.id),
|
||||
username: user.username,
|
||||
firstName: user.first_name,
|
||||
lastName: user.last_name,
|
||||
photoUrl: user.photo_url,
|
||||
languageCode: user.language_code,
|
||||
},
|
||||
update: {
|
||||
username: user.username,
|
||||
firstName: user.first_name,
|
||||
lastName: user.last_name,
|
||||
photoUrl: user.photo_url,
|
||||
languageCode: user.language_code,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function requireTelegramUser(initData?: string) {
|
||||
if (!initData) {
|
||||
throw new Error('Telegram authorization is required.');
|
||||
}
|
||||
|
||||
return getOrCreateTelegramUser(initData);
|
||||
}
|
||||
Reference in New Issue
Block a user