Add Telegram bot login sessions
All checks were successful
Build and deploy Backend / build (push) Successful in 49s
All checks were successful
Build and deploy Backend / build (push) Successful in 49s
This commit is contained in:
37
prisma/migrations/3_add_telegram_bot_sessions/migration.sql
Normal file
37
prisma/migrations/3_add_telegram_bot_sessions/migration.sql
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "UserSession" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"tokenHash" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "UserSession_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "TelegramLoginRequest" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"tokenHash" TEXT NOT NULL,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'PENDING',
|
||||||
|
"sessionToken" TEXT,
|
||||||
|
"userId" TEXT,
|
||||||
|
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "TelegramLoginRequest_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "UserSession_tokenHash_key" ON "UserSession"("tokenHash");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "TelegramLoginRequest_tokenHash_key" ON "TelegramLoginRequest"("tokenHash");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "UserSession" ADD CONSTRAINT "UserSession_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "TelegramLoginRequest" ADD CONSTRAINT "TelegramLoginRequest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
@@ -35,11 +35,34 @@ model User {
|
|||||||
lastName String?
|
lastName String?
|
||||||
photoUrl String?
|
photoUrl String?
|
||||||
languageCode String?
|
languageCode String?
|
||||||
|
sessions UserSession[]
|
||||||
|
loginRequests TelegramLoginRequest[]
|
||||||
voiceExperiences VoiceExperience[]
|
voiceExperiences VoiceExperience[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model UserSession {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
tokenHash String @unique
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
expiresAt DateTime
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
|
model TelegramLoginRequest {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
tokenHash String @unique
|
||||||
|
status String @default("PENDING")
|
||||||
|
sessionToken String?
|
||||||
|
userId String?
|
||||||
|
user User? @relation(fields: [userId], references: [id])
|
||||||
|
expiresAt DateTime
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
model VoiceExperience {
|
model VoiceExperience {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
placeId String
|
placeId String
|
||||||
|
|||||||
189
src/auth/telegram-bot-login.ts
Normal file
189
src/auth/telegram-bot-login.ts
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import { randomBytes } from 'node:crypto';
|
||||||
|
|
||||||
|
import { config } from '../config.js';
|
||||||
|
import { prisma } from '../prisma.js';
|
||||||
|
import {
|
||||||
|
sha256Hex,
|
||||||
|
upsertTelegramUser,
|
||||||
|
type TelegramUserPayload,
|
||||||
|
} from './telegram.js';
|
||||||
|
|
||||||
|
type TelegramUpdate = {
|
||||||
|
message?: {
|
||||||
|
chat: { id: number };
|
||||||
|
text?: string;
|
||||||
|
from?: TelegramBotUser;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type TelegramBotUser = {
|
||||||
|
id: number;
|
||||||
|
is_bot?: boolean;
|
||||||
|
first_name?: string;
|
||||||
|
last_name?: string;
|
||||||
|
username?: string;
|
||||||
|
language_code?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loginPrefix = 'login_';
|
||||||
|
|
||||||
|
function randomToken() {
|
||||||
|
return randomBytes(32).toString('base64url');
|
||||||
|
}
|
||||||
|
|
||||||
|
function expiresIn(seconds: number) {
|
||||||
|
return new Date(Date.now() + seconds * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function botApiUrl(method: string) {
|
||||||
|
if (!config.telegramMiniAppBotToken) {
|
||||||
|
throw new Error('TELEGRAM_MINI_APP_BOT_TOKEN is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return `https://api.telegram.org/bot${config.telegramMiniAppBotToken}/${method}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callTelegram(method: string, body: Record<string, unknown>) {
|
||||||
|
const response = await fetch(botApiUrl(method), {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json' },
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Telegram ${method} failed with ${response.status}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = (await response.json()) as { ok: boolean; description?: string };
|
||||||
|
if (!payload.ok) {
|
||||||
|
throw new Error(payload.description ?? `Telegram ${method} failed.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function userPayload(from: TelegramBotUser): TelegramUserPayload {
|
||||||
|
return {
|
||||||
|
id: from.id,
|
||||||
|
username: from.username,
|
||||||
|
firstName: from.first_name,
|
||||||
|
lastName: from.last_name,
|
||||||
|
languageCode: from.language_code,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendLoginMessage(chatId: number, text: string, token?: string) {
|
||||||
|
const replyMarkup = token
|
||||||
|
? {
|
||||||
|
inline_keyboard: [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: 'Вернуться в MapFlow',
|
||||||
|
url: `${config.webAppUrl}?telegram_login=${encodeURIComponent(token)}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
await callTelegram('sendMessage', {
|
||||||
|
chat_id: chatId,
|
||||||
|
text,
|
||||||
|
reply_markup: replyMarkup,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createTelegramBotLogin() {
|
||||||
|
const token = randomToken();
|
||||||
|
const expiresAt = expiresIn(config.botLoginMaxAgeSeconds);
|
||||||
|
|
||||||
|
await prisma.telegramLoginRequest.create({
|
||||||
|
data: {
|
||||||
|
tokenHash: sha256Hex(token),
|
||||||
|
expiresAt,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
botUrl: `https://t.me/${config.telegramBotUsername}?start=${loginPrefix}${token}`,
|
||||||
|
expiresAt: expiresAt.toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTelegramBotLoginStatus(token: string) {
|
||||||
|
const request = await prisma.telegramLoginRequest.findUnique({
|
||||||
|
where: { tokenHash: sha256Hex(token) },
|
||||||
|
include: { user: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!request) {
|
||||||
|
return { status: 'EXPIRED', sessionToken: null, user: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.status === 'PENDING' && request.expiresAt <= new Date()) {
|
||||||
|
await prisma.telegramLoginRequest.update({
|
||||||
|
where: { id: request.id },
|
||||||
|
data: { status: 'EXPIRED' },
|
||||||
|
});
|
||||||
|
return { status: 'EXPIRED', sessionToken: null, user: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: request.status,
|
||||||
|
sessionToken: request.sessionToken,
|
||||||
|
user: request.user,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleTelegramBotWebhook(
|
||||||
|
update: TelegramUpdate,
|
||||||
|
secretToken?: string,
|
||||||
|
) {
|
||||||
|
if (config.telegramWebhookSecret && secretToken !== config.telegramWebhookSecret) {
|
||||||
|
throw new Error('Telegram webhook secret is invalid.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = update.message;
|
||||||
|
const text = message?.text;
|
||||||
|
const from = message?.from;
|
||||||
|
const chatId = message?.chat.id;
|
||||||
|
if (!message || !text || !from || !chatId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [command, payload] = text.split(' ');
|
||||||
|
if (command !== '/start' || !payload?.startsWith(loginPrefix)) {
|
||||||
|
await sendLoginMessage(chatId, 'Открой вход с сайта MapFlow.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = payload.slice(loginPrefix.length);
|
||||||
|
const request = await prisma.telegramLoginRequest.findUnique({
|
||||||
|
where: { tokenHash: sha256Hex(token) },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!request || request.status !== 'PENDING' || request.expiresAt <= new Date()) {
|
||||||
|
await sendLoginMessage(chatId, 'Ссылка входа устарела. Открой вход заново.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await upsertTelegramUser(userPayload(from));
|
||||||
|
const sessionToken = randomToken();
|
||||||
|
await prisma.userSession.create({
|
||||||
|
data: {
|
||||||
|
tokenHash: sha256Hex(sessionToken),
|
||||||
|
userId: user.id,
|
||||||
|
expiresAt: expiresIn(config.sessionMaxAgeSeconds),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.telegramLoginRequest.update({
|
||||||
|
where: { id: request.id },
|
||||||
|
data: {
|
||||||
|
status: 'CONFIRMED',
|
||||||
|
sessionToken,
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await sendLoginMessage(chatId, 'Готово. Можно вернуться в MapFlow.', token);
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ export type TelegramLoginData = {
|
|||||||
hash: string;
|
hash: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TelegramUserPayload = {
|
export type TelegramUserPayload = {
|
||||||
id: number;
|
id: number;
|
||||||
username?: string;
|
username?: string;
|
||||||
firstName?: string;
|
firstName?: string;
|
||||||
@@ -34,16 +34,21 @@ type TelegramUserPayload = {
|
|||||||
type TelegramAuthData = {
|
type TelegramAuthData = {
|
||||||
telegramInitData?: string;
|
telegramInitData?: string;
|
||||||
telegramLoginData?: string;
|
telegramLoginData?: string;
|
||||||
|
mapflowSessionToken?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
function hmacSha256(key: string | Buffer, value: string) {
|
function hmacSha256(key: string | Buffer, value: string) {
|
||||||
return createHmac('sha256', key).update(value).digest();
|
return createHmac('sha256', key).update(value).digest();
|
||||||
}
|
}
|
||||||
|
|
||||||
function sha256(value: string) {
|
export function sha256(value: string) {
|
||||||
return createHash('sha256').update(value).digest();
|
return createHash('sha256').update(value).digest();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sha256Hex(value: string) {
|
||||||
|
return createHash('sha256').update(value).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
function assertValidHash(receivedHash: string, expectedHash: Buffer, dataType: string) {
|
function assertValidHash(receivedHash: string, expectedHash: Buffer, dataType: string) {
|
||||||
const received = Buffer.from(receivedHash, 'hex');
|
const received = Buffer.from(receivedHash, 'hex');
|
||||||
if (received.length !== expectedHash.length) {
|
if (received.length !== expectedHash.length) {
|
||||||
@@ -134,7 +139,7 @@ function parseTelegramLoginJson(loginData: string) {
|
|||||||
return parseTelegramLoginData(JSON.parse(loginData) as TelegramLoginData);
|
return parseTelegramLoginData(JSON.parse(loginData) as TelegramLoginData);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function upsertTelegramUser(user: TelegramUserPayload) {
|
export async function upsertTelegramUser(user: TelegramUserPayload) {
|
||||||
return prisma.user.upsert({
|
return prisma.user.upsert({
|
||||||
where: { telegramId: String(user.id) },
|
where: { telegramId: String(user.id) },
|
||||||
create: {
|
create: {
|
||||||
@@ -163,7 +168,24 @@ export async function getOrCreateTelegramLoginUser(loginData: TelegramLoginData)
|
|||||||
return upsertTelegramUser(parseTelegramLoginData(loginData));
|
return upsertTelegramUser(parseTelegramLoginData(loginData));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUserBySessionToken(sessionToken: string) {
|
||||||
|
const session = await prisma.userSession.findUnique({
|
||||||
|
where: { tokenHash: sha256Hex(sessionToken) },
|
||||||
|
include: { user: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!session || session.expiresAt <= new Date()) {
|
||||||
|
throw new Error('Telegram authorization is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.user;
|
||||||
|
}
|
||||||
|
|
||||||
export async function requireTelegramUser(authData: TelegramAuthData) {
|
export async function requireTelegramUser(authData: TelegramAuthData) {
|
||||||
|
if (authData.mapflowSessionToken) {
|
||||||
|
return getUserBySessionToken(authData.mapflowSessionToken);
|
||||||
|
}
|
||||||
|
|
||||||
if (authData.telegramInitData) {
|
if (authData.telegramInitData) {
|
||||||
return upsertTelegramUser(parseTelegramInitData(authData.telegramInitData));
|
return upsertTelegramUser(parseTelegramInitData(authData.telegramInitData));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,12 @@ export const config = {
|
|||||||
databaseUrl: process.env.DATABASE_URL ?? '',
|
databaseUrl: process.env.DATABASE_URL ?? '',
|
||||||
hatchetToken: process.env.HATCHET_CLIENT_TOKEN ?? '',
|
hatchetToken: process.env.HATCHET_CLIENT_TOKEN ?? '',
|
||||||
telegramMiniAppBotToken: process.env.TELEGRAM_MINI_APP_BOT_TOKEN ?? '',
|
telegramMiniAppBotToken: process.env.TELEGRAM_MINI_APP_BOT_TOKEN ?? '',
|
||||||
|
telegramBotUsername: process.env.TELEGRAM_BOT_USERNAME ?? 'carfteebot',
|
||||||
|
telegramWebhookSecret: process.env.TELEGRAM_WEBHOOK_SECRET ?? '',
|
||||||
|
webAppUrl: process.env.WEB_APP_URL ?? 'https://map.craftee.vn',
|
||||||
telegramAuthMaxAgeSeconds: Number(
|
telegramAuthMaxAgeSeconds: Number(
|
||||||
process.env.TELEGRAM_AUTH_MAX_AGE_SECONDS ?? '86400',
|
process.env.TELEGRAM_AUTH_MAX_AGE_SECONDS ?? '86400',
|
||||||
),
|
),
|
||||||
|
sessionMaxAgeSeconds: Number(process.env.SESSION_MAX_AGE_SECONDS ?? '2592000'),
|
||||||
|
botLoginMaxAgeSeconds: Number(process.env.BOT_LOGIN_MAX_AGE_SECONDS ?? '300'),
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -142,6 +142,25 @@ exports.Prisma.UserScalarFieldEnum = {
|
|||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.Prisma.UserSessionScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
tokenHash: 'tokenHash',
|
||||||
|
userId: 'userId',
|
||||||
|
expiresAt: 'expiresAt',
|
||||||
|
createdAt: 'createdAt'
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.Prisma.TelegramLoginRequestScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
tokenHash: 'tokenHash',
|
||||||
|
status: 'status',
|
||||||
|
sessionToken: 'sessionToken',
|
||||||
|
userId: 'userId',
|
||||||
|
expiresAt: 'expiresAt',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
};
|
||||||
|
|
||||||
exports.Prisma.VoiceExperienceScalarFieldEnum = {
|
exports.Prisma.VoiceExperienceScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
placeId: 'placeId',
|
placeId: 'placeId',
|
||||||
@@ -192,6 +211,8 @@ exports.VoiceExperienceStatus = exports.$Enums.VoiceExperienceStatus = {
|
|||||||
exports.Prisma.ModelName = {
|
exports.Prisma.ModelName = {
|
||||||
Place: 'Place',
|
Place: 'Place',
|
||||||
User: 'User',
|
User: 'User',
|
||||||
|
UserSession: 'UserSession',
|
||||||
|
TelegramLoginRequest: 'TelegramLoginRequest',
|
||||||
VoiceExperience: 'VoiceExperience'
|
VoiceExperience: 'VoiceExperience'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
3274
src/generated/prisma/index.d.ts
vendored
3274
src/generated/prisma/index.d.ts
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "prisma-client-359590a92aedb32557346cd24ba7dd77e7dd4a806bf36f6641715b1b22de87e3",
|
"name": "prisma-client-37c1b83274036b18947b6108de6a4d4254d7d73ddef2fb10ea1aa825e682d461",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
"browser": "default.js",
|
"browser": "default.js",
|
||||||
|
|||||||
@@ -35,11 +35,34 @@ model User {
|
|||||||
lastName String?
|
lastName String?
|
||||||
photoUrl String?
|
photoUrl String?
|
||||||
languageCode String?
|
languageCode String?
|
||||||
|
sessions UserSession[]
|
||||||
|
loginRequests TelegramLoginRequest[]
|
||||||
voiceExperiences VoiceExperience[]
|
voiceExperiences VoiceExperience[]
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model UserSession {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
tokenHash String @unique
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
expiresAt DateTime
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
|
||||||
|
model TelegramLoginRequest {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
tokenHash String @unique
|
||||||
|
status String @default("PENDING")
|
||||||
|
sessionToken String?
|
||||||
|
userId String?
|
||||||
|
user User? @relation(fields: [userId], references: [id])
|
||||||
|
expiresAt DateTime
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
model VoiceExperience {
|
model VoiceExperience {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
placeId String
|
placeId String
|
||||||
|
|||||||
@@ -5,12 +5,17 @@ import {
|
|||||||
requireTelegramUser,
|
requireTelegramUser,
|
||||||
type TelegramLoginData,
|
type TelegramLoginData,
|
||||||
} from '../auth/telegram.js';
|
} from '../auth/telegram.js';
|
||||||
|
import {
|
||||||
|
createTelegramBotLogin,
|
||||||
|
getTelegramBotLoginStatus,
|
||||||
|
} from '../auth/telegram-bot-login.js';
|
||||||
import { listPlaces, listVoiceExperiences } from './places.js';
|
import { listPlaces, listVoiceExperiences } from './places.js';
|
||||||
import { createVoiceExperience } from './voice-experiences.js';
|
import { createVoiceExperience } from './voice-experiences.js';
|
||||||
|
|
||||||
export type GraphqlContext = {
|
export type GraphqlContext = {
|
||||||
telegramInitData?: string;
|
telegramInitData?: string;
|
||||||
telegramLoginData?: string;
|
telegramLoginData?: string;
|
||||||
|
mapflowSessionToken?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const schema = /* GraphQL */ `
|
export const schema = /* GraphQL */ `
|
||||||
@@ -83,13 +88,28 @@ export const schema = /* GraphQL */ `
|
|||||||
user: User!
|
user: User!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TelegramBotLoginPayload {
|
||||||
|
token: String!
|
||||||
|
botUrl: String!
|
||||||
|
expiresAt: String!
|
||||||
|
}
|
||||||
|
|
||||||
|
type TelegramBotLoginStatus {
|
||||||
|
status: String!
|
||||||
|
sessionToken: String
|
||||||
|
user: User
|
||||||
|
}
|
||||||
|
|
||||||
type Query {
|
type Query {
|
||||||
health: String!
|
health: String!
|
||||||
|
me: User!
|
||||||
|
telegramBotLoginStatus(token: String!): TelegramBotLoginStatus!
|
||||||
places: [Place!]!
|
places: [Place!]!
|
||||||
voiceExperiences: [VoiceExperience!]!
|
voiceExperiences: [VoiceExperience!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
|
startTelegramBotLogin: TelegramBotLoginPayload!
|
||||||
authenticateTelegram(input: AuthenticateTelegramInput!): AuthPayload!
|
authenticateTelegram(input: AuthenticateTelegramInput!): AuthPayload!
|
||||||
authenticateTelegramLogin(input: AuthenticateTelegramLoginInput!): AuthPayload!
|
authenticateTelegramLogin(input: AuthenticateTelegramLoginInput!): AuthPayload!
|
||||||
createVoiceExperience(input: CreateVoiceExperienceInput!): VoiceExperience!
|
createVoiceExperience(input: CreateVoiceExperienceInput!): VoiceExperience!
|
||||||
@@ -100,6 +120,12 @@ export const resolvers = {
|
|||||||
JSON: GraphQLJSONObject,
|
JSON: GraphQLJSONObject,
|
||||||
Query: {
|
Query: {
|
||||||
health: () => 'ok',
|
health: () => 'ok',
|
||||||
|
me: async (_: unknown, __: unknown, context: unknown) => {
|
||||||
|
const graphqlContext = context as GraphqlContext;
|
||||||
|
return requireTelegramUser(graphqlContext);
|
||||||
|
},
|
||||||
|
telegramBotLoginStatus: async (_: unknown, args: { token: string }) =>
|
||||||
|
getTelegramBotLoginStatus(args.token),
|
||||||
places: async (_: unknown, __: unknown, context: unknown) => {
|
places: async (_: unknown, __: unknown, context: unknown) => {
|
||||||
const graphqlContext = context as GraphqlContext;
|
const graphqlContext = context as GraphqlContext;
|
||||||
await requireTelegramUser(graphqlContext);
|
await requireTelegramUser(graphqlContext);
|
||||||
@@ -112,6 +138,7 @@ export const resolvers = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
|
startTelegramBotLogin: async () => createTelegramBotLogin(),
|
||||||
authenticateTelegram: async (
|
authenticateTelegram: async (
|
||||||
_: unknown,
|
_: unknown,
|
||||||
args: { input: { initData: string } },
|
args: { input: { initData: string } },
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import mercurius from 'mercurius';
|
|||||||
import { config } from './config.js';
|
import { config } from './config.js';
|
||||||
import { prisma } from './prisma.js';
|
import { prisma } from './prisma.js';
|
||||||
import { resolvers, schema } from './graphql/schema.js';
|
import { resolvers, schema } from './graphql/schema.js';
|
||||||
|
import { handleTelegramBotWebhook } from './auth/telegram-bot-login.js';
|
||||||
|
|
||||||
const app = Fastify({ logger: true });
|
const app = Fastify({ logger: true });
|
||||||
|
|
||||||
@@ -14,15 +15,24 @@ app.register(mercurius, {
|
|||||||
context: async (request) => {
|
context: async (request) => {
|
||||||
const initDataHeader = request.headers['x-telegram-init-data'];
|
const initDataHeader = request.headers['x-telegram-init-data'];
|
||||||
const loginDataHeader = request.headers['x-telegram-login-data'];
|
const loginDataHeader = request.headers['x-telegram-login-data'];
|
||||||
|
const sessionTokenHeader = request.headers['x-mapflow-session-token'];
|
||||||
return {
|
return {
|
||||||
telegramInitData: Array.isArray(initDataHeader) ? initDataHeader[0] : initDataHeader,
|
telegramInitData: Array.isArray(initDataHeader) ? initDataHeader[0] : initDataHeader,
|
||||||
telegramLoginData: Array.isArray(loginDataHeader) ? loginDataHeader[0] : loginDataHeader,
|
telegramLoginData: Array.isArray(loginDataHeader) ? loginDataHeader[0] : loginDataHeader,
|
||||||
|
mapflowSessionToken: Array.isArray(sessionTokenHeader) ? sessionTokenHeader[0] : sessionTokenHeader,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/health', async () => ({ ok: true }));
|
app.get('/health', async () => ({ ok: true }));
|
||||||
|
|
||||||
|
app.post('/telegram/webhook', async (request, reply) => {
|
||||||
|
const secretHeader = request.headers['x-telegram-bot-api-secret-token'];
|
||||||
|
const secretToken = Array.isArray(secretHeader) ? secretHeader[0] : secretHeader;
|
||||||
|
await handleTelegramBotWebhook(request.body as never, secretToken);
|
||||||
|
return reply.send({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
app.addHook('onClose', async () => {
|
app.addHook('onClose', async () => {
|
||||||
await prisma.$disconnect();
|
await prisma.$disconnect();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user