security(users): SafeUser — passwordHash/токены не уходят клиенту

Шаг 0.8 плана production-готовности:
- shared/schema.ts: тип SafeUser + safeUserColumns
- getUsersByOrganization, listUsers, searchUsers, getUsersByRole,
  getUsersByOrgRoleId, documents getUser — без чувствительных колонок
- sync (initial/delta), автоматизации ctx.users.list, MCP list_users — sanitized
This commit is contained in:
2026-09-07 21:03:00 +03:00
parent 26419f395f
commit cf6f937108
12 changed files with 79 additions and 51 deletions

View File

@@ -2,7 +2,7 @@ import { Router } from "express";
import { storage } from "../storage";
import { authenticateToken, type AuthenticatedRequest } from "../middleware/auth.middleware";
import { tenantIsolation } from "../middleware/tenant.middleware";
import { type Task, type FormStatus, type User, roles, tasks } from "@shared/schema";
import { type Task, type FormStatus, type SafeUser, roles, tasks } from "@shared/schema";
import { db } from "../db";
import { eq, inArray } from "drizzle-orm";
import { tasksMinimalCache } from "../utils/cache";
@@ -204,7 +204,7 @@ export function registerTaskCrudListRoutes(router: ReturnType<typeof import("exp
db.select().from(roles).where(eq(roles.organizationId, req.organizationId!)),
]);
const statusMap = new Map(statuses.map((s: FormStatus) => [s.id, s]));
const userMap = new Map(users.map((u: User) => [u.id, u]));
const userMap = new Map(users.map((u: SafeUser) => [u.id, u]));
const roleMap = new Map(orgRoles.map((r) => [r.id, r]));
const contractNumberFields = formFields.filter((f) => f.type === 'contract-number' && (f as any).options?.contractNumber?.legalEntityFormId);
@@ -250,7 +250,7 @@ export function registerTaskCrudListRoutes(router: ReturnType<typeof import("exp
db.select().from(roles).where(eq(roles.organizationId, req.organizationId!)),
]);
const statusMap = new Map(statuses.map((s: FormStatus) => [s.id, s]));
const userMap = new Map(users.map((u: User) => [u.id, u]));
const userMap = new Map(users.map((u: SafeUser) => [u.id, u]));
const roleMap = new Map(orgRoles.map((r) => [r.id, r]));
const contractNumberFields = formFields.filter((f) => f.type === 'contract-number' && (f as any).options?.contractNumber?.legalEntityFormId);
@@ -348,7 +348,7 @@ export function registerTaskCrudListRoutes(router: ReturnType<typeof import("exp
// Отображаемое имя одного элемента user-поля (`user:<id>` / `role:<id>` / legacy-число)
function resolveUserColumnEntry(
entry: string,
userMap: Map<number, User>,
userMap: Map<number, SafeUser>,
roleMap?: Map<number, { name?: string }>
): string {
if (entry.startsWith('user:')) {
@@ -373,7 +373,7 @@ export function registerTaskCrudListRoutes(router: ReturnType<typeof import("exp
task: Task & { fieldValues?: Record<number, unknown>; assignees?: Array<{ userId: number; user?: { firstName: string | null; middleName: string | null; lastName: string | null } }> },
column: string,
statusMap: Map<number, FormStatus>,
userMap: Map<number, User>,
userMap: Map<number, SafeUser>,
fields?: Array<{ id: number; type: string; options?: unknown }>,
roleMap?: Map<number, { name?: string }>,
tableRowMap?: Map<number, Map<number, string>>,
@@ -678,7 +678,7 @@ export function registerTaskCrudListRoutes(router: ReturnType<typeof import("exp
const statuses = await storage.getFormStatuses(formId, req.organizationId!);
const users = await storage.getUsersByOrganization(req.organizationId!);
const statusMap = new Map(statuses.map((s: FormStatus) => [s.id, s]));
const userMap = new Map(users.map((u: User) => [u.id, u]));
const userMap = new Map(users.map((u: SafeUser) => [u.id, u]));
Object.entries(context).forEach(([col, val]) => {
if (!val) return;
@@ -703,7 +703,7 @@ export function registerTaskCrudListRoutes(router: ReturnType<typeof import("exp
const statuses = await storage.getFormStatuses(formId, req.organizationId!);
const users = await storage.getUsersByOrganization(req.organizationId!);
const statusMap = new Map(statuses.map((s: FormStatus) => [s.id, s]));
const userMap = new Map(users.map((u: User) => [u.id, u]));
const userMap = new Map(users.map((u: SafeUser) => [u.id, u]));
// Отладка: логируем select/radio-group/table поля и их options
if (column.startsWith('field_')) {