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

@@ -1,5 +1,5 @@
import { db } from "../db";
import { tasks, taskFieldValues, users, organizations, forms, formFields, formTabs, regularTableRows, taskRelations } from "@shared/schema";
import { tasks, taskFieldValues, users, organizations, forms, formFields, formTabs, regularTableRows, taskRelations, safeUserColumns, type SafeUser } from "@shared/schema";
import { eq, and, asc, sql } from "drizzle-orm";
import type { TemplateVariable } from "@shared/schema";
import { TransformService, type TransformConfig, type TransformType } from "./transform.service";
@@ -14,10 +14,10 @@ export interface ResolutionContext {
type: string;
name: string;
}>;
assignee: typeof users.$inferSelect | null;
creator: typeof users.$inferSelect | null;
assignee: SafeUser | null;
creator: SafeUser | null;
organization: typeof organizations.$inferSelect | null;
currentUser: typeof users.$inferSelect | null;
currentUser: SafeUser | null;
}
export interface ResolutionResult {
@@ -89,8 +89,9 @@ export class DataResolutionService {
.where(eq(taskFieldValues.taskId, taskId));
}
private async getUser(userId: number) {
const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1);
private async getUser(userId: number): Promise<SafeUser | null> {
// Для подстановки в шаблоны документов достаточно безопасных колонок (без хэша пароля и токенов)
const [user] = await db.select(safeUserColumns).from(users).where(eq(users.id, userId)).limit(1);
return user || null;
}
@@ -243,7 +244,7 @@ export class DataResolutionService {
}
}
private getUserField(user: typeof users.$inferSelect, key: string): string | null {
private getUserField(user: SafeUser, key: string): string | null {
switch (key) {
case 'fullName':
return `${user.lastName || ''} ${user.firstName || ''} ${user.middleName || ''}`.trim() || user.email || '';