Files
iistwin/server/storage/roles.storage.ts
Ильяс Султанов 1f5ecb6da4 fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric,
  чтобы браузер не округлял значения через input type=number
- пробелы при вставке в number-поля удаляются
- бэкенд нормализует значения number-полей в строку перед сохранением
- добавлен хелпер normalizeFieldValueForStorage

Closes: искажение расчётного счёта и других длинных числовых полей
2026-07-07 21:03:40 +03:00

355 lines
17 KiB
TypeScript

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";
// In-memory permission cache
let _permCache: Map<string, string[]> | null = null;
let _permCacheTs = 0;
const PERM_CACHE_TTL = 60_000;
async function getPermissionCache(): Promise<Map<string, string[]>> {
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<string, string[]>();
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,
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<number, typeof memberRows>();
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<number, infer V> ? 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 }) {
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();
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)));
}
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();
}
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)));
}
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();
}
}
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,
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<boolean> {
const cache = await getPermissionCache();
const perms = cache.get(appRoleSlug) || [];
return permissionCodes.some(c => perms.includes(c));
}
async getAppRolePermissions(appRoleSlug: string): Promise<string[]> {
const cache = await getPermissionCache();
return cache.get(appRoleSlug) || [];
}
async seedAppRolesAndPermissions(): Promise<void> {
// 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<void> {
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();
}
}
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)));
}
}