import { users, appRoles, permissions, appRolePermissions, roleMembers, roles } from "@shared/schema"; import { db } from "../db"; import { eq, and, asc, inArray } from "drizzle-orm"; import { BillingStorage } from "./billing.storage"; import { invalidateAccessibleTasksForUser, invalidateAccessibleTasksForOrg } from "../utils/cache"; // In-memory permission cache let _permCache: Map | null = null; let _permCacheTs = 0; const PERM_CACHE_TTL = 60_000; async function getPermissionCache(): Promise> { const now = Date.now(); if (_permCache && (now - _permCacheTs) < PERM_CACHE_TTL) { return _permCache; } const rows = await db.select({ appRoleSlug: appRoles.slug, permissionCode: permissions.code, }).from(appRolePermissions) .innerJoin(appRoles, eq(appRolePermissions.appRoleId, appRoles.id)) .innerJoin(permissions, eq(appRolePermissions.permissionId, permissions.id)); const map = new Map(); for (const r of rows) { if (!map.has(r.appRoleSlug)) map.set(r.appRoleSlug, []); map.get(r.appRoleSlug)!.push(r.permissionCode); } _permCache = map; _permCacheTs = now; return map; } export function invalidatePermissionCache(): void { _permCache = null; _permCacheTs = 0; } export class RolesStorage extends BillingStorage { // ===================== // Roles // ===================== async getRoles(organizationId: number) { const { roles, roleMembers } = await import('@shared/schema'); const roleRows = await db.select({ id: roles.id, organizationId: roles.organizationId, name: roles.name, description: roles.description, parentId: roles.parentId, managerId: roles.managerId, gpsAccess: roles.gpsAccess, financeAccess: roles.financeAccess, createdAt: roles.createdAt, }).from(roles).where(eq(roles.organizationId, organizationId)).orderBy(asc(roles.name)); if (roleRows.length === 0) return []; const roleIds = roleRows.map(r => r.id); const memberRows = await db.select({ roleId: roleMembers.roleId, memberId: roleMembers.id, userId: roleMembers.userId, user: { id: users.id, firstName: users.firstName, lastName: users.lastName, email: users.email, }, }).from(roleMembers) .innerJoin(users, eq(roleMembers.userId, users.id)) .where(inArray(roleMembers.roleId, roleIds)); const membersByRole = new Map(); memberRows.forEach(m => { if (!membersByRole.has(m.roleId)) membersByRole.set(m.roleId, []); membersByRole.get(m.roleId)!.push(m); }); const managerIds = roleRows.map(r => r.managerId).filter((id): id is number => id !== null && id !== undefined); const managers = managerIds.length > 0 ? await db.select({ id: users.id, firstName: users.firstName, lastName: users.lastName, middleName: users.middleName, email: users.email, }).from(users).where(inArray(users.id, managerIds)) : []; const managerMap = new Map(managers.map(m => [m.id, m])); // Build role tree for recursive member counting const roleMap = new Map(roleRows.map(r => [r.id, { ...r, children: [] as typeof roleRows, directMemberCount: (membersByRole.get(r.id) ?? []).length }])); const treeRoots: typeof roleMap extends Map ? V[] : never[] = []; for (const role of roleMap.values()) { if (role.parentId && roleMap.has(role.parentId)) { (roleMap.get(role.parentId) as any).children.push(role); } else { treeRoots.push(role as any); } } const computeTotal = (node: any): number => { let total = node.directMemberCount; for (const child of node.children) { total += computeTotal(child); } return total; }; for (const role of roleMap.values()) { (role as any).totalMemberCount = computeTotal(role); } return roleRows.map(role => ({ ...role, memberCount: (roleMap.get(role.id) as any).totalMemberCount, directMemberCount: (roleMap.get(role.id) as any).directMemberCount, totalMemberCount: (roleMap.get(role.id) as any).totalMemberCount, members: (membersByRole.get(role.id) ?? []).map(m => ({ id: m.memberId, userId: m.userId, user: m.user, })), manager: role.managerId ? managerMap.get(role.managerId) ?? null : null, })); } async getRole(id: number, organizationId: number) { const { roles } = await import('@shared/schema'); const [role] = await db.select().from(roles).where(and(eq(roles.id, id), eq(roles.organizationId, organizationId))); return role; } async createRole(data: { organizationId: number; name: string; description?: string | null; parentId?: number | null; managerId?: number | null }) { const { roles } = await import('@shared/schema'); if (data.parentId) { const [parent] = await db.select().from(roles).where(and(eq(roles.id, data.parentId), eq(roles.organizationId, data.organizationId))); if (!parent) throw new Error('Родительская роль не найдена в этой организации'); } if (data.managerId) { const [manager] = await db.select().from(users).where(and(eq(users.id, data.managerId), eq(users.organizationId, data.organizationId))); if (!manager) throw new Error('Руководитель не найден в этой организации'); } const [role] = await db.insert(roles).values({ organizationId: data.organizationId, name: data.name, description: data.description ?? null, parentId: data.parentId ?? null, managerId: data.managerId ?? null, }).returning(); return role; } async updateRole(id: number, organizationId: number, updates: { name?: string; description?: string | null; parentId?: number | null; managerId?: number | null; gpsAccess?: boolean; financeAccess?: boolean }) { const { roles } = await import('@shared/schema'); if (updates.parentId) { if (updates.parentId === id) throw new Error('Роль не может быть родителем самой себя'); const [parent] = await db.select().from(roles).where(and(eq(roles.id, updates.parentId), eq(roles.organizationId, organizationId))); if (!parent) throw new Error('Родительская роль не найдена в этой организации'); } if (updates.managerId) { const [manager] = await db.select().from(users).where(and(eq(users.id, updates.managerId), eq(users.organizationId, organizationId))); if (!manager) throw new Error('Руководитель не найден в этой организации'); } const [role] = await db.update(roles).set(updates).where(and(eq(roles.id, id), eq(roles.organizationId, organizationId))).returning(); // Смена parentId меняет наследование доступа по иерархии ролей — сброс по организации if ('parentId' in updates) { invalidateAccessibleTasksForOrg(organizationId); } return role; } async deleteRole(id: number, organizationId: number) { const { roles } = await import('@shared/schema'); await db.update(roles).set({ parentId: null }).where(and(eq(roles.parentId, id), eq(roles.organizationId, organizationId))); await db.delete(roles).where(and(eq(roles.id, id), eq(roles.organizationId, organizationId))); invalidateAccessibleTasksForOrg(organizationId); } async getRoleMembersByRole(roleId: number): Promise<{ id: number; userId: number; createdAt: Date | null }[]> { const { roleMembers } = await import('@shared/schema'); return db.select().from(roleMembers).where(eq(roleMembers.roleId, roleId)); } async getRoleMembers(roleId: number, organizationId: number) { const { roles, roleMembers } = await import('@shared/schema'); const [role] = await db.select().from(roles).where(and(eq(roles.id, roleId), eq(roles.organizationId, organizationId))); if (!role) throw new Error('Роль не найдена'); const members = await db.select({ id: roleMembers.id, userId: roleMembers.userId, createdAt: roleMembers.createdAt, user: { id: users.id, firstName: users.firstName, lastName: users.lastName, email: users.email, }, }).from(roleMembers) .innerJoin(users, eq(roleMembers.userId, users.id)) .where(eq(roleMembers.roleId, roleId)); let manager = null; if (role.managerId) { const [managerRow] = await db.select({ id: users.id, firstName: users.firstName, lastName: users.lastName, middleName: users.middleName, email: users.email, }).from(users).where(eq(users.id, role.managerId)); manager = managerRow ?? null; } return { ...role, members, manager }; } async addRoleMember(roleId: number, userId: number, organizationId: number) { const { roles, roleMembers } = await import('@shared/schema'); const [role] = await db.select().from(roles).where(and(eq(roles.id, roleId), eq(roles.organizationId, organizationId))); if (!role) throw new Error('Роль не найдена'); const [targetUser] = await db.select().from(users).where(and(eq(users.id, userId), eq(users.organizationId, organizationId))); if (!targetUser) throw new Error('Пользователь не найден в этой организации'); await db.insert(roleMembers).values({ roleId, userId }).onConflictDoNothing(); invalidateAccessibleTasksForUser(userId, organizationId); } async removeRoleMember(roleId: number, userId: number, organizationId: number) { const { roles, roleMembers } = await import('@shared/schema'); const [role] = await db.select().from(roles).where(and(eq(roles.id, roleId), eq(roles.organizationId, organizationId))); if (!role) throw new Error('Роль не найдена'); const [targetUser] = await db.select().from(users).where(and(eq(users.id, userId), eq(users.organizationId, organizationId))); if (!targetUser) throw new Error('Пользователь не найден в этой организации'); await db.delete(roleMembers).where(and(eq(roleMembers.roleId, roleId), eq(roleMembers.userId, userId))); invalidateAccessibleTasksForUser(userId, organizationId); } async moveRoleMember(sourceRoleId: number, targetRoleId: number | null, userId: number, organizationId: number) { const { roles, roleMembers } = await import('@shared/schema'); const [sourceRole] = await db.select().from(roles).where(and(eq(roles.id, sourceRoleId), eq(roles.organizationId, organizationId))); if (!sourceRole) throw new Error('Исходная роль не найдена'); if (targetRoleId !== null) { const [targetRole] = await db.select().from(roles).where(and(eq(roles.id, targetRoleId), eq(roles.organizationId, organizationId))); if (!targetRole) throw new Error('Целевая роль не найдена'); } const [targetUser] = await db.select().from(users).where(and(eq(users.id, userId), eq(users.organizationId, organizationId))); if (!targetUser) throw new Error('Пользователь не найден в этой организации'); await db.delete(roleMembers).where(and(eq(roleMembers.roleId, sourceRoleId), eq(roleMembers.userId, userId))); if (targetRoleId !== null) { await db.insert(roleMembers).values({ roleId: targetRoleId, userId }).onConflictDoNothing(); } invalidateAccessibleTasksForUser(userId, organizationId); } async getUserRoles(userId: number, organizationId: number) { const { roles, roleMembers } = await import('@shared/schema'); return db.select({ id: roles.id, organizationId: roles.organizationId, name: roles.name, description: roles.description, parentId: roles.parentId, managerId: roles.managerId, gpsAccess: roles.gpsAccess, financeAccess: roles.financeAccess, createdAt: roles.createdAt, }).from(roleMembers) .innerJoin(roles, eq(roleMembers.roleId, roles.id)) .where(and(eq(roleMembers.userId, userId), eq(roles.organizationId, organizationId))); } // ===================== // App Roles & Permissions // ===================== async hasAppRolePermission(appRoleSlug: string, permissionCodes: string[]): Promise { const cache = await getPermissionCache(); const perms = cache.get(appRoleSlug) || []; return permissionCodes.some(c => perms.includes(c)); } async getAppRolePermissions(appRoleSlug: string): Promise { const cache = await getPermissionCache(); return cache.get(appRoleSlug) || []; } async seedAppRolesAndPermissions(): Promise { // Seed app roles await db.insert(appRoles).values([ { slug: 'admin', name: 'Администратор', description: 'Полный доступ ко всем функциям приложения', isSystem: true }, { slug: 'user', name: 'Пользователь', description: 'Базовый доступ к задачам и чату', isSystem: true }, ]).onConflictDoNothing(); // Seed permissions const perms = [ { code: 'super_admin.access', name: 'Доступ к супер-админке', description: 'Управление организациями на уровне супер-админа', module: 'system' }, { code: 'users.manage', name: 'Управление пользователями', description: 'Создание, редактирование и удаление пользователей', module: 'users' }, { code: 'roles.manage', name: 'Управление ролями', description: 'Создание и редактирование организационных ролей', module: 'users' }, { code: 'forms.manage', name: 'Управление формами', description: 'Создание и редактирование форм и полей', module: 'forms' }, { code: 'tasks.view_all', name: 'Видеть все задачи', description: 'Доступ ко всем задачам организации', module: 'tasks' }, { code: 'tasks.edit_all', name: 'Редактировать все задачи', description: 'Редактирование любых задач', module: 'tasks' }, { code: 'directories.manage', name: 'Управление справочниками', description: 'Создание и редактирование справочников', module: 'directories' }, { code: 'finance.view', name: 'Просмотр финансов', description: 'Доступ к модулю Финансы', module: 'finance' }, { code: 'finance.manage', name: 'Управление финансами', description: 'Редактирование финансовых данных', module: 'finance' }, { code: 'billing.view', name: 'Просмотр биллинга', description: 'Просмотр биллинговой информации', module: 'billing' }, { code: 'billing.manage', name: 'Управление биллингом', description: 'Управление биллингом и оплатой', module: 'billing' }, { code: 'settings.manage', name: 'Управление настройками', description: 'Настройки организации', module: 'settings' }, { code: 'automations.manage', name: 'Управление автоматизациями', description: 'Создание и редактирование автоматизаций', module: 'automations' }, { code: 'bots.manage', name: 'Управление ботами', description: 'Создание и настройка ботов', module: 'bots' }, { code: 'chat.manage', name: 'Управление чатами', description: 'Администрирование групповых чатов', module: 'chat' }, { code: 'reports.view', name: 'Просмотр отчётов', description: 'Доступ к отчётам', module: 'reports' }, { code: 'profile.edit', name: 'Редактирование профиля', description: 'Изменение собственного профиля', module: 'profile' }, { code: 'inbox.view', name: 'Доступ к inbox', description: 'Просмотр inbox и главной страницы', module: 'inbox' }, ]; await db.insert(permissions).values(perms).onConflictDoNothing(); // Link admin -> all permissions const adminRole = await db.select().from(appRoles).where(eq(appRoles.slug, 'admin')).limit(1); if (adminRole.length > 0) { const allPerms = await db.select({ id: permissions.id }).from(permissions); for (const p of allPerms) { await db.insert(appRolePermissions).values({ appRoleId: adminRole[0].id, permissionId: p.id }).onConflictDoNothing(); } } // Link user -> minimal permissions const userRole = await db.select().from(appRoles).where(eq(appRoles.slug, 'user')).limit(1); if (userRole.length > 0) { const minPermCodes = ['profile.edit', 'inbox.view']; const minPerms = await db.select({ id: permissions.id }).from(permissions).where(inArray(permissions.code, minPermCodes)); for (const p of minPerms) { await db.insert(appRolePermissions).values({ appRoleId: userRole[0].id, permissionId: p.id }).onConflictDoNothing(); } } invalidatePermissionCache(); } // ===================== // User Organizational Roles // ===================== async setUserOrganizationalRoles(userId: number, roleIds: number[]): Promise { await db.delete(roleMembers).where(eq(roleMembers.userId, userId)); if (roleIds.length > 0) { const values = roleIds.map(roleId => ({ userId, roleId })); await db.insert(roleMembers).values(values).onConflictDoNothing(); } // organizationId не приходит аргументом — сбрасываем кэш по всем организациям пользователя const userRows = await db.select({ organizationId: users.organizationId }).from(users).where(eq(users.id, userId)).limit(1); if (userRows[0]?.organizationId) { invalidateAccessibleTasksForUser(userId, userRows[0].organizationId); } } async getUserOrganizationalRoles(userId: number, organizationId: number) { return db.select({ id: roles.id, name: roles.name, description: roles.description, parentId: roles.parentId, }).from(roleMembers) .innerJoin(roles, eq(roleMembers.roleId, roles.id)) .where(and(eq(roleMembers.userId, userId), eq(roles.organizationId, organizationId))); } }