From 335ba994ab38c60236145f080cffc55d8f28dca7 Mon Sep 17 00:00:00 2001 From: Ruslan Bakiev Date: Sat, 4 Apr 2026 10:13:50 +0700 Subject: [PATCH] Expose manager telegram avatars --- src/access.js | 99 +++++++++++++++++++++++++++++++++++++++ src/resolvers.js | 113 +++++++-------------------------------------- src/schema.graphql | 1 + src/server.js | 20 +++++++- 4 files changed, 136 insertions(+), 97 deletions(-) create mode 100644 src/access.js diff --git a/src/access.js b/src/access.js new file mode 100644 index 0000000..2c82d90 --- /dev/null +++ b/src/access.js @@ -0,0 +1,99 @@ +export const MANAGER_ROLES = ['MANAGER', 'SUPER_MANAGER']; +const NO_CLIENT_IDS = ['__no_managed_clients__']; + +export function isSuperManager(user) { + return user?.role === 'SUPER_MANAGER'; +} + +export function isManagerRole(role) { + return MANAGER_ROLES.includes(role); +} + +function normalizeManagedClientIds(clientIds) { + if (clientIds == null) { + return null; + } + + return clientIds.length ? clientIds : NO_CLIENT_IDS; +} + +export async function getManagedClientIds(prisma, manager) { + if (isSuperManager(manager)) { + return null; + } + + const [managedOrders, acceptedInvitations, reviewedRequests, reviewedWithdrawals] = await Promise.all([ + prisma.order.findMany({ + where: { managerId: manager.id }, + select: { customerId: true }, + }), + prisma.invitation.findMany({ + where: { + managerId: manager.id, + acceptedById: { not: null }, + }, + select: { acceptedById: true }, + }), + prisma.registrationRequest.findMany({ + where: { + reviewedById: manager.id, + requesterId: { not: null }, + }, + select: { requesterId: true }, + }), + prisma.rewardWithdrawalRequest.findMany({ + where: { reviewedById: manager.id }, + select: { requesterId: true }, + }), + ]); + + const clientIds = new Set(); + + for (const order of managedOrders) { + if (order.customerId) { + clientIds.add(order.customerId); + } + } + + for (const invitation of acceptedInvitations) { + if (invitation.acceptedById) { + clientIds.add(invitation.acceptedById); + } + } + + for (const request of reviewedRequests) { + if (request.requesterId) { + clientIds.add(request.requesterId); + } + } + + for (const withdrawal of reviewedWithdrawals) { + if (withdrawal.requesterId) { + clientIds.add(withdrawal.requesterId); + } + } + + return [...clientIds]; +} + +export async function getManagedClientUserWhere(prisma, manager) { + const managedClientIds = normalizeManagedClientIds(await getManagedClientIds(prisma, manager)); + + if (managedClientIds == null) { + return { role: 'CLIENT' }; + } + + return { + role: 'CLIENT', + id: { in: managedClientIds }, + }; +} + +export async function canManagerAccessUser(prisma, manager, userId) { + if (isSuperManager(manager) || userId === manager.id) { + return true; + } + + const managedClientIds = await getManagedClientIds(prisma, manager); + return managedClientIds.includes(userId); +} diff --git a/src/resolvers.js b/src/resolvers.js index 8d0764f..318d459 100644 --- a/src/resolvers.js +++ b/src/resolvers.js @@ -8,14 +8,19 @@ import { maskAuthDestination, verifyLoginChallengeCode, } from './auth.js'; +import { + MANAGER_ROLES, + canManagerAccessUser, + getManagedClientUserWhere, + isManagerRole, + isSuperManager, +} from './access.js'; import { sendLoginCodeEmail } from './mailer.js'; import { dispatchToUserConnections, sendMessengerMessage } from './messenger.js'; import { dateTimeScalar, jsonScalar } from './scalars.js'; import { fetchTelegramConnectionProfile } from './telegram.js'; const ACTIVE_ORDER_STATUSES = ['NEW', 'MANAGER_PROCESSING', 'WAITING_DOUBLE_CONFIRM', 'CONFIRMED', 'IN_PROGRESS']; -const MANAGER_ROLES = ['MANAGER', 'SUPER_MANAGER']; -const NO_CLIENT_IDS = ['__no_managed_clients__']; function toFloat(value) { return value == null ? null : Number(value); @@ -40,101 +45,8 @@ function requireManagerAccess(context) { return requireAnyRole(context, MANAGER_ROLES); } -function isSuperManager(user) { - return user.role === 'SUPER_MANAGER'; -} - -function isManagerRole(role) { - return MANAGER_ROLES.includes(role); -} - -function normalizeManagedClientIds(clientIds) { - if (clientIds == null) { - return null; - } - - return clientIds.length ? clientIds : NO_CLIENT_IDS; -} - -async function getManagedClientIds(prisma, manager) { - if (isSuperManager(manager)) { - return null; - } - - const [managedOrders, acceptedInvitations, reviewedRequests, reviewedWithdrawals] = await Promise.all([ - prisma.order.findMany({ - where: { managerId: manager.id }, - select: { customerId: true }, - }), - prisma.invitation.findMany({ - where: { - managerId: manager.id, - acceptedById: { not: null }, - }, - select: { acceptedById: true }, - }), - prisma.registrationRequest.findMany({ - where: { - reviewedById: manager.id, - requesterId: { not: null }, - }, - select: { requesterId: true }, - }), - prisma.rewardWithdrawalRequest.findMany({ - where: { reviewedById: manager.id }, - select: { requesterId: true }, - }), - ]); - - const clientIds = new Set(); - - for (const order of managedOrders) { - if (order.customerId) { - clientIds.add(order.customerId); - } - } - - for (const invitation of acceptedInvitations) { - if (invitation.acceptedById) { - clientIds.add(invitation.acceptedById); - } - } - - for (const request of reviewedRequests) { - if (request.requesterId) { - clientIds.add(request.requesterId); - } - } - - for (const withdrawal of reviewedWithdrawals) { - if (withdrawal.requesterId) { - clientIds.add(withdrawal.requesterId); - } - } - - return [...clientIds]; -} - -async function getManagedClientUserWhere(prisma, manager) { - const managedClientIds = normalizeManagedClientIds(await getManagedClientIds(prisma, manager)); - - if (managedClientIds == null) { - return { role: 'CLIENT' }; - } - - return { - role: 'CLIENT', - id: { in: managedClientIds }, - }; -} - async function assertManagerCanAccessUser(prisma, manager, userId) { - if (isSuperManager(manager) || userId === manager.id) { - return; - } - - const managedClientIds = await getManagedClientIds(prisma, manager); - if (!managedClientIds.includes(userId)) { + if (!await canManagerAccessUser(prisma, manager, userId)) { throw new Error('User is not available for this manager.'); } } @@ -594,6 +506,14 @@ export const resolvers = { const users = await context.prisma.user.findMany({ where: managedUsersWhere, include: { + messengerConnections: { + where: { + type: 'TELEGRAM', + isActive: true, + }, + orderBy: { createdAt: 'desc' }, + take: 1, + }, counterpartyProfile: { select: { companyName: true, @@ -626,6 +546,7 @@ export const resolvers = { createdAt: user.createdAt, orderCount: user._count.clientOrders, lastOrderAt: user.clientOrders[0]?.createdAt ?? null, + telegramConnection: user.messengerConnections[0] ?? null, })); }, diff --git a/src/schema.graphql b/src/schema.graphql index 957163f..4bc7bab 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -147,6 +147,7 @@ type ManagerUser { createdAt: DateTime! orderCount: Int! lastOrderAt: DateTime + telegramConnection: MessengerConnection } type MessengerConnection { diff --git a/src/server.js b/src/server.js index 669dfba..2a5ecc0 100644 --- a/src/server.js +++ b/src/server.js @@ -18,6 +18,7 @@ import { issueTemporaryLoginToken, verifyAccessToken, } from './auth.js'; +import { canManagerAccessUser, isManagerRole } from './access.js'; import { buildContext } from './context.js'; import { sendMessengerMessage } from './messenger.js'; import { prisma } from './prisma-client.js'; @@ -258,12 +259,29 @@ app.get('/messenger/avatar/:connectionId', async (req, res) => { const connection = await prisma.messengerConnection.findFirst({ where: { id: connectionId, - userId: user.id, type: 'TELEGRAM', isActive: true, }, }); + if (!connection) { + res.status(404).json({ error: 'Telegram avatar not found.' }); + return; + } + + if (connection.userId !== user.id) { + if (!isManagerRole(user.role)) { + res.status(403).json({ error: 'Access denied.' }); + return; + } + + const canAccess = await canManagerAccessUser(prisma, user, connection.userId); + if (!canAccess) { + res.status(403).json({ error: 'Access denied.' }); + return; + } + } + if (!connection?.avatarFileId) { res.status(404).json({ error: 'Telegram avatar not found.' }); return;