Add Telegram Mini App auth flow

This commit is contained in:
Ruslan Bakiev
2026-04-04 14:21:18 +07:00
parent a0cbae390c
commit 2499aa1a6a
5 changed files with 380 additions and 50 deletions

View File

@@ -15,6 +15,7 @@ import {
isManagerRole,
} from './access.js';
import { sendLoginCodeEmail } from './mailer.js';
import { upsertActiveMessengerConnection } from './messenger-connections.js';
import { dispatchToUserConnections, sendMessengerMessage } from './messenger.js';
import { dateTimeScalar, jsonScalar } from './scalars.js';
import { fetchTelegramConnectionProfile } from './telegram.js';
@@ -281,6 +282,33 @@ function formatOrderStatusMessage(order, status, note) {
return `Заказ ${order.code} изменил статус: ${status}.${suffix}`;
}
function buildFrontendAppUrl(path) {
const baseUrl = String(
process.env.TELEGRAM_MINI_APP_URL ||
process.env.WEB_FRONTEND_URL ||
process.env.NUXT_PUBLIC_SITE_URL ||
'',
).trim().replace(/\/$/, '');
const normalizedPath = String(path || '').trim();
if (!baseUrl || !normalizedPath.startsWith('/')) {
return null;
}
return `${baseUrl}${normalizedPath}`;
}
function buildUserOrderPath(orderId, role) {
const normalizedOrderId = String(orderId || '').trim();
if (!normalizedOrderId) {
return '';
}
return isManagerRole(role)
? `/client-orders/${normalizedOrderId}`
: `/orders/${normalizedOrderId}`;
}
async function notifyOrderStakeholders(context, order, status, note) {
const recipients = [order.customerId, order.managerId].filter(Boolean);
if (!recipients.length) {
@@ -289,8 +317,22 @@ async function notifyOrderStakeholders(context, order, status, note) {
const message = formatOrderStatusMessage(order, status, note);
const uniqueRecipients = [...new Set(recipients)];
const users = await context.prisma.user.findMany({
where: {
id: { in: uniqueRecipients },
},
select: {
id: true,
role: true,
},
});
const userRoleMap = new Map(users.map((user) => [user.id, user.role]));
await Promise.allSettled(
uniqueRecipients.map((userId) => dispatchToUserConnections(context.prisma, userId, message)),
uniqueRecipients.map((userId) => dispatchToUserConnections(context.prisma, userId, message, {
buttonUrl: buildFrontendAppUrl(buildUserOrderPath(order.id, userRoleMap.get(userId))),
buttonText: 'Открыть заказ',
})),
);
}
@@ -820,41 +862,11 @@ export const resolvers = {
}
if (login.messengerConnection) {
await context.prisma.messengerConnection.updateMany({
where: {
userId: user.id,
type: login.messengerConnection.type,
isActive: true,
NOT: { channelId: login.messengerConnection.channelId },
},
data: { isActive: false },
});
await context.prisma.messengerConnection.upsert({
where: {
userId_type_channelId: {
userId: user.id,
type: login.messengerConnection.type,
channelId: login.messengerConnection.channelId,
},
},
update: {
isActive: true,
displayName: login.messengerConnection.displayName ?? null,
username: login.messengerConnection.username ?? null,
avatarFileId: login.messengerConnection.avatarFileId ?? null,
avatarFileUniqueId: login.messengerConnection.avatarFileUniqueId ?? null,
},
create: {
userId: user.id,
type: login.messengerConnection.type,
channelId: login.messengerConnection.channelId,
isActive: true,
displayName: login.messengerConnection.displayName ?? null,
username: login.messengerConnection.username ?? null,
avatarFileId: login.messengerConnection.avatarFileId ?? null,
avatarFileUniqueId: login.messengerConnection.avatarFileUniqueId ?? null,
},
await upsertActiveMessengerConnection(context.prisma, {
userId: user.id,
type: login.messengerConnection.type,
channelId: login.messengerConnection.channelId,
profile: login.messengerConnection,
});
}