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:
@@ -1,4 +1,4 @@
|
||||
import { users, userNotifications, bookmarkFolders, bookmarks, type User, type UserNotification, type BookmarkFolder, type Bookmark, type InsertUserNotification, type InsertBookmarkFolder, type InsertBookmark } from "@shared/schema";
|
||||
import { users, userNotifications, bookmarkFolders, bookmarks, safeUserColumns, type User, type SafeUser, type UserNotification, type BookmarkFolder, type Bookmark, type InsertUserNotification, type InsertBookmarkFolder, type InsertBookmark } from "@shared/schema";
|
||||
import { db } from "../db";
|
||||
import { eq, and, desc, isNull, sql } from "drizzle-orm";
|
||||
import crypto from "crypto";
|
||||
@@ -347,7 +347,8 @@ export class SocialStorage extends TasksStorage {
|
||||
}
|
||||
|
||||
// User Search
|
||||
async searchUsers(query: string, organizationId: number, limit = 10): Promise<User[]> {
|
||||
// Поиск уходит клиенту — выбираем только безопасные колонки (без хэша пароля и токенов)
|
||||
async searchUsers(query: string, organizationId: number, limit = 10): Promise<SafeUser[]> {
|
||||
// Поддержка похожих символов: і/и для корректного поиска
|
||||
const normalizedQuery = query
|
||||
.replace(/і/g, 'и')
|
||||
@@ -355,7 +356,7 @@ export class SocialStorage extends TasksStorage {
|
||||
.trim();
|
||||
|
||||
const result = await db
|
||||
.select()
|
||||
.select(safeUserColumns)
|
||||
.from(users)
|
||||
.where(and(
|
||||
eq(users.organizationId, organizationId),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { users, organizations, forms, tasks, type User, type Organization, type Task } from "@shared/schema";
|
||||
import { users, organizations, forms, tasks, safeUserColumns, type User, type SafeUser, type Organization, type Task } from "@shared/schema";
|
||||
import { taskRelations, type TaskRelation } from "@shared/schema";
|
||||
import { taskReminders, type TaskReminder, type InsertTaskReminder } from "@shared/schema";
|
||||
import { taskAuditLog, type TaskAuditLog, type InsertTaskAuditLog } from "@shared/schema";
|
||||
@@ -96,9 +96,10 @@ export class TaskMetaStorage extends ContentStorage {
|
||||
.where(eq(taskReminders.id, id));
|
||||
}
|
||||
|
||||
async getUsersByRole(role: string, organizationId: number): Promise<User[]> {
|
||||
// Массовые выборки пользователей — только безопасные колонки (без хэша пароля и токенов)
|
||||
async getUsersByRole(role: string, organizationId: number): Promise<SafeUser[]> {
|
||||
return db
|
||||
.select()
|
||||
.select(safeUserColumns)
|
||||
.from(users)
|
||||
.where(and(
|
||||
eq(users.organizationId, organizationId),
|
||||
@@ -107,18 +108,17 @@ export class TaskMetaStorage extends ContentStorage {
|
||||
));
|
||||
}
|
||||
|
||||
async getUsersByOrgRoleId(roleId: number, organizationId: number): Promise<User[]> {
|
||||
const rows = await db
|
||||
.select({ user: users })
|
||||
.from(roleMembers)
|
||||
.innerJoin(users, eq(roleMembers.userId, users.id))
|
||||
async getUsersByOrgRoleId(roleId: number, organizationId: number): Promise<SafeUser[]> {
|
||||
return db
|
||||
.select(safeUserColumns)
|
||||
.from(users)
|
||||
.innerJoin(roleMembers, eq(roleMembers.userId, users.id))
|
||||
.innerJoin(roles, eq(roleMembers.roleId, roles.id))
|
||||
.where(and(
|
||||
eq(roleMembers.roleId, roleId),
|
||||
eq(roles.organizationId, organizationId),
|
||||
eq(users.isActive, true),
|
||||
));
|
||||
return rows.map(r => r.user);
|
||||
}
|
||||
|
||||
// === Task Audit Log ===
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { users, forms, tasks, taskMessages, messageReads, bots, roles, taskViews, type User, type Form, type FormTab, type FormField, type FormStatus, type StatusTransition, type Task, type TaskFieldValue, type TaskMessage, type TaskMessageWithAuthor, type MessageRead, type DataTableRow } from "@shared/schema";
|
||||
import { users, forms, tasks, taskMessages, messageReads, bots, roles, taskViews, type SafeUser, type Form, type FormTab, type FormField, type FormStatus, type StatusTransition, type Task, type TaskFieldValue, type TaskMessage, type TaskMessageWithAuthor, type MessageRead, type DataTableRow } from "@shared/schema";
|
||||
import { taskRelations } from "@shared/schema";
|
||||
import { taskAssignees, type TaskAssignee } from "@shared/schema";
|
||||
import { db } from "../db";
|
||||
@@ -18,7 +18,7 @@ export class TasksStorage extends TasksCoreStorage {
|
||||
fieldValues: TaskFieldValue[];
|
||||
subtasks: Task[];
|
||||
messages: TaskMessageWithAuthor[];
|
||||
users: User[];
|
||||
users: Array<SafeUser & { fullName: string }>;
|
||||
hasRelations: boolean;
|
||||
assignees: Array<TaskAssignee & { user: { id: number; firstName: string | null; lastName: string | null; email: string } }>;
|
||||
} | null> {
|
||||
@@ -53,7 +53,7 @@ export class TasksStorage extends TasksCoreStorage {
|
||||
this.getTaskAssignees(task.id, organizationId),
|
||||
]);
|
||||
|
||||
const usersWithFullName = orgUsers.map((u: User) => ({
|
||||
const usersWithFullName = orgUsers.map((u: SafeUser) => ({
|
||||
...u,
|
||||
fullName: formatUserName(u)
|
||||
}));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { users, organizations, userSessions, invitations, userCustomFields, userCustomValues, roleMembers, forms, formFields, formStatuses, tasks, taskFieldValues, userOfflineSubscriptions, type User, type Organization, type UserSession, type Invitation, type InsertUser, type InsertOrganization, type InsertUserSession, type InsertInvitation, type UserWithOrganization, type UserCustomField, type UserCustomValue, type CreateUserCustomField, type UpdateUserCustomField, type Task, type FormField, type FormStatus, type UserOfflineSubscription, type InsertUserOfflineSubscription } from "@shared/schema";
|
||||
import { users, organizations, userSessions, invitations, userCustomFields, userCustomValues, roleMembers, forms, formFields, formStatuses, tasks, taskFieldValues, userOfflineSubscriptions, safeUserColumns, type User, type SafeUser, type Organization, type UserSession, type Invitation, type InsertUser, type InsertOrganization, type InsertUserSession, type InsertInvitation, type UserWithOrganization, type UserCustomField, type UserCustomValue, type CreateUserCustomField, type UpdateUserCustomField, type Task, type FormField, type FormStatus, type UserOfflineSubscription, type InsertUserOfflineSubscription } from "@shared/schema";
|
||||
import { db } from "../db";
|
||||
import { eq, and, desc, asc, sql, ilike, or, gte, lte, inArray } from "drizzle-orm";
|
||||
import crypto from "crypto";
|
||||
@@ -16,7 +16,7 @@ export interface ListUsersOptions {
|
||||
}
|
||||
|
||||
export interface ListUsersResult {
|
||||
users: User[];
|
||||
users: SafeUser[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
@@ -90,9 +90,10 @@ export class UsersStorage {
|
||||
};
|
||||
}
|
||||
|
||||
async getUsersByOrganization(organizationId: number): Promise<User[]> {
|
||||
// Массовая выдача пользователей клиенту — только безопасные колонки (без хэша пароля и токенов)
|
||||
async getUsersByOrganization(organizationId: number): Promise<SafeUser[]> {
|
||||
return await db
|
||||
.select()
|
||||
.select(safeUserColumns)
|
||||
.from(users)
|
||||
.where(eq(users.organizationId, organizationId))
|
||||
.orderBy(desc(users.createdAt));
|
||||
@@ -142,7 +143,7 @@ export class UsersStorage {
|
||||
.where(whereClause);
|
||||
|
||||
let query = db
|
||||
.select()
|
||||
.select(safeUserColumns)
|
||||
.from(users)
|
||||
.where(whereClause)
|
||||
.orderBy(orderDir)
|
||||
@@ -157,9 +158,10 @@ export class UsersStorage {
|
||||
) as any;
|
||||
}
|
||||
|
||||
const rows = await query;
|
||||
// При явном списке колонок drizzle возвращает плоские строки даже с join
|
||||
const rows: SafeUser[] = await query;
|
||||
return {
|
||||
users: rows.map((r: any) => ('users' in r ? r.users : r)),
|
||||
users: rows,
|
||||
total: countResult?.total || 0,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user