gongxiangp2/lib/department-permission.ts

687 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { prisma } from './prisma';
import { filterSelectableMaterialTypes, MATERIAL_TYPE_OPTIONS } from './material-type-options';
import { getCompatibleVideoMaterialTypes, isVideoMaterialType } from './video-material-types';
const superAdminChecksInFlight = new Map<string, Promise<boolean>>();
const viewableMaterialTypesInFlight = new Map<string, Promise<string[]>>();
/**
* 判断是否是超级管理员(从数据库读取)
*/
export async function isSuperAdmin(userId: string | null | undefined): Promise<boolean> {
if (!userId) {
return false;
}
const existingCheck = superAdminChecksInFlight.get(userId);
if (existingCheck) {
return existingCheck;
}
const check = prisma.user.findUnique({
where: { id: userId },
select: { isSuperAdmin: true },
}).then((user) => user?.isSuperAdmin ?? false);
superAdminChecksInFlight.set(userId, check);
try {
return await check;
} finally {
if (superAdminChecksInFlight.get(userId) === check) {
superAdminChecksInFlight.delete(userId);
}
}
}
/**
* 同步版本的超级管理员判断(用于已经获取到用户信息的情况)
*/
export function isSuperAdminSync(isSuperAdmin: boolean): boolean {
return isSuperAdmin;
}
/**
* 角色权限检查辅助函数(阶段四新增)
*/
/**
* 判断是否是系统管理员(包括ADMIN和SYSTEM_ADMIN,向后兼容)
*/
export function isSystemAdmin(role: string | null | undefined): boolean {
if (!role) return false;
// ADMIN 视为 SYSTEM_ADMIN(向后兼容)
return role === 'ADMIN' || role === 'SYSTEM_ADMIN';
}
/**
* 判断是否是部门管理员
*/
export function isDepartmentAdmin(role: string | null | undefined): boolean {
if (!role) return false;
return role === 'DEPT_ADMIN';
}
/**
* 判断是否是管理员(系统管理员或部门管理员)
*/
export function isAdmin(role: string | null | undefined): boolean {
if (!role) return false;
// 包括所有管理员角色:ADMIN(兼容)、SYSTEM_ADMIN、DEPT_ADMIN
return isSystemAdmin(role) || isDepartmentAdmin(role);
}
/**
* 判断是否有系统管理权限(系统管理员或超级管理员)
*/
export function hasSystemAdminPermission(
role: string | null | undefined,
isSuperAdminFlag: boolean = false
): boolean {
return isSuperAdminFlag || isSystemAdmin(role);
}
/**
* 判断是否有部门管理权限(部门管理员、系统管理员或超级管理员)
*/
export function hasDepartmentAdminPermission(
role: string | null | undefined,
isSuperAdminFlag: boolean = false
): boolean {
return isSuperAdminFlag || isSystemAdmin(role) || isDepartmentAdmin(role);
}
/**
* 获取当前超级管理员数量
*/
export async function getSuperAdminCount(): Promise<number> {
return await prisma.user.count({
where: { isSuperAdmin: true },
});
}
/**
* 检查是否可以添加新的超级管理员(最多4个)
*/
export async function canAddSuperAdmin(): Promise<boolean> {
const count = await getSuperAdminCount();
return count < 4;
}
// 默认素材类型列表(作为后备,用于新部门创建默认权限配置时)
// 如果数据库中已有素材,会优先使用数据库中的素材类型
// 已按分类排序
export const DEFAULT_MATERIAL_TYPES = MATERIAL_TYPE_OPTIONS;
/**
* 从数据库中获取所有素材类型(去重)
*/
export async function getAllMaterialTypes(): Promise<string[]> {
try {
// 从materials表中查询所有不重复的materialType
const materials = await prisma.material.findMany({
select: {
materialType: true,
},
distinct: ['materialType'],
});
const dbTypes = materials.map((m) => m.materialType).filter(Boolean);
// 合并数据库中的类型和默认类型,去重
const allTypesSet = new Set([...dbTypes, ...DEFAULT_MATERIAL_TYPES]);
// 按照 DEFAULT_MATERIAL_TYPES 的顺序排序,未在列表中的类型放在最后
const sortedTypes = [
...DEFAULT_MATERIAL_TYPES.filter(type => allTypesSet.has(type)),
...Array.from(allTypesSet).filter(type => !DEFAULT_MATERIAL_TYPES.includes(type as any)).sort()
];
return sortedTypes;
} catch (error) {
console.error('[getAllMaterialTypes] Error:', error);
// 如果查询失败,返回默认类型列表
return [...DEFAULT_MATERIAL_TYPES];
}
}
export interface DepartmentPermission {
department: string;
materialType: string;
canView: boolean;
canDownload: boolean;
canShare: boolean;
canDownloadSource: boolean;
linkedToDepartment: string | null;
}
type DepartmentRecord = {
id: string;
name: string;
parentId?: string | null;
};
type DepartmentPermissionRecord = {
department: string;
materialType: string;
canView: boolean;
canDownload: boolean;
canShare: boolean;
canDownloadSource: boolean;
linkedToDepartment: string | null;
};
function getDepartmentPermissionKey(department: string, materialType: string): string {
return `${department}\u0000${materialType}`;
}
function findDepartmentPermissionRecordFromMap(
permissionMap: Map<string, DepartmentPermissionRecord>,
department: string,
materialType: string,
): DepartmentPermissionRecord | null {
const normalizedType = String(materialType || '').trim();
const lookupTypes = isVideoMaterialType(normalizedType)
? getCompatibleVideoMaterialTypes(normalizedType)
: [normalizedType];
for (const lookupType of lookupTypes) {
const permission = permissionMap.get(getDepartmentPermissionKey(department, lookupType));
if (permission) {
return permission;
}
}
return null;
}
function resolveDepartmentPermissionFromMap(
department: string,
materialType: string,
permissionMap: Map<string, DepartmentPermissionRecord>,
departmentByName: Map<string, DepartmentRecord>,
departmentById: Map<string, DepartmentRecord>,
visitedDepartments: Set<string> = new Set(),
): DepartmentPermission | null {
if (visitedDepartments.has(department)) {
return null;
}
const nextVisitedDepartments = new Set(visitedDepartments);
nextVisitedDepartments.add(department);
const permission = findDepartmentPermissionRecordFromMap(
permissionMap,
department,
materialType,
);
if (permission?.linkedToDepartment) {
const linkedPermission = findDepartmentPermissionRecordFromMap(
permissionMap,
permission.linkedToDepartment,
materialType,
);
if (linkedPermission) {
return {
department,
materialType,
canView: linkedPermission.canView,
canDownload: linkedPermission.canDownload,
canShare: linkedPermission.canShare,
canDownloadSource: linkedPermission.canDownloadSource,
linkedToDepartment: permission.linkedToDepartment,
};
}
}
if (permission) {
return {
department: permission.department,
materialType: permission.materialType,
canView: permission.canView,
canDownload: permission.canDownload,
canShare: permission.canShare,
canDownloadSource: permission.canDownloadSource,
linkedToDepartment: permission.linkedToDepartment,
};
}
const currentDepartment = departmentByName.get(department);
const parentDepartment = currentDepartment?.parentId
? departmentById.get(currentDepartment.parentId)
: undefined;
if (!parentDepartment) {
return null;
}
const parentPermission = resolveDepartmentPermissionFromMap(
parentDepartment.name,
materialType,
permissionMap,
departmentByName,
departmentById,
nextVisitedDepartments,
);
if (!parentPermission) {
return null;
}
return {
department,
materialType,
canView: parentPermission.canView,
canDownload: parentPermission.canDownload,
canShare: parentPermission.canShare,
canDownloadSource: parentPermission.canDownloadSource,
linkedToDepartment: parentPermission.linkedToDepartment || parentDepartment.name,
};
}
async function findDepartmentPermissionRecord(
department: string,
materialType: string
) {
const normalizedType = String(materialType || '').trim();
const lookupTypes = isVideoMaterialType(normalizedType)
? getCompatibleVideoMaterialTypes(normalizedType)
: [normalizedType];
for (const lookupType of lookupTypes) {
const permission = await prisma.departmentMaterialPermission.findUnique({
where: {
department_materialType: {
department,
materialType: lookupType,
},
},
});
if (permission) {
return permission;
}
}
return null;
}
/**
* 获取部门的实际权限(考虑关联部门和权限继承)
*
* @param department 部门名称
* @param materialType 素材类型
* @param departments 部门列表(用于权限继承检查,可选)
* @param descendantMap 部门后裔映射(可选,用于加速权限继承检查)
* @returns 部门权限配置
*/
export async function getDepartmentPermission(
department: string,
materialType: string,
departments?: Array<{ id: string; name: string; parentId?: string | null }>,
descendantMap?: Map<string, Set<string>>
): Promise<DepartmentPermission | null> {
// 先查询当前部门的权限配置
const permission = await findDepartmentPermissionRecord(department, materialType);
// 如果有关联部门,从关联部门读取权限
if (permission?.linkedToDepartment) {
const linkedPermission = await findDepartmentPermissionRecord(
permission.linkedToDepartment,
materialType
);
if (linkedPermission) {
return {
department,
materialType,
canView: linkedPermission.canView,
canDownload: linkedPermission.canDownload,
canShare: linkedPermission.canShare,
canDownloadSource: linkedPermission.canDownloadSource,
linkedToDepartment: permission.linkedToDepartment,
};
}
}
// 如果当前部门有明确的权限配置,直接返回
if (permission) {
return {
department: permission.department,
materialType: permission.materialType,
canView: permission.canView,
canDownload: permission.canDownload,
canShare: permission.canShare,
canDownloadSource: permission.canDownloadSource,
linkedToDepartment: permission.linkedToDepartment,
};
}
// 权限继承:如果没有找到当前部门的权限配置,尝试从父部门继承
if (departments && departments.length > 0) {
// 构建部门映射
const deptMap = new Map<string, { id: string; name: string; parentId?: string | null }>();
departments.forEach(dept => {
deptMap.set(dept.name, dept);
});
// 获取当前部门的父部门链
const currentDept = deptMap.get(department);
if (currentDept?.parentId) {
// 查找父部门
const parentDept = departments.find(d => d.id === currentDept.parentId);
if (parentDept) {
// 递归获取父部门的权限
const parentPermission = await getDepartmentPermission(
parentDept.name,
materialType,
departments,
descendantMap
);
// 如果父部门有权限配置,继承父部门的权限
if (parentPermission) {
return {
department,
materialType,
canView: parentPermission.canView,
canDownload: parentPermission.canDownload,
canShare: parentPermission.canShare,
canDownloadSource: parentPermission.canDownloadSource,
linkedToDepartment: parentPermission.linkedToDepartment || parentDept.name,
};
}
}
}
}
// 如果都没有找到,返回 null(表示没有权限配置)
return null;
}
/**
* 获取部门所有素材类型的权限(支持权限继承)
*
* @param department 部门名称
* @param departments 部门列表(用于权限继承检查,可选)
* @param materialTypes 素材类型列表(可选,如果不提供则从数据库获取)
* @returns 部门所有素材类型的权限配置
*/
export async function getDepartmentPermissions(
department: string,
departments?: Array<{ id: string; name: string; parentId?: string | null }>,
materialTypes?: string[]
): Promise<Record<string, DepartmentPermission>> {
// 如果没有提供素材类型列表,从数据库获取所有素材类型
const allMaterialTypes = materialTypes || await getAllMaterialTypes();
const permissionRecords = await prisma.departmentMaterialPermission.findMany({
select: {
department: true,
materialType: true,
canView: true,
canDownload: true,
canShare: true,
canDownloadSource: true,
linkedToDepartment: true,
},
});
const permissionMap = new Map<string, DepartmentPermissionRecord>(
permissionRecords.map((permission) => [
getDepartmentPermissionKey(permission.department, permission.materialType),
permission,
]),
);
const departmentByName = new Map<string, DepartmentRecord>();
const departmentById = new Map<string, DepartmentRecord>();
for (const departmentRecord of departments || []) {
departmentByName.set(departmentRecord.name, departmentRecord);
departmentById.set(departmentRecord.id, departmentRecord);
}
const result: Record<string, DepartmentPermission> = {};
// 权限记录一次性批量读取后,在内存中解析直接配置、关联部门和父部门继承。
for (const materialType of allMaterialTypes) {
const permission = resolveDepartmentPermissionFromMap(
department,
materialType,
permissionMap,
departmentByName,
departmentById,
);
if (permission) {
result[materialType] = permission;
}
}
return result;
}
/**
* 获取用户部门名称(双读:优先使用 departmentId,回退到 department)
* 阶段二新增:支持外键关联
*/
export async function getUserDepartmentName(
user: { department?: string | null; departmentId?: string | null; departmentRef?: { name: string } | null } | null | undefined
): Promise<string | null> {
if (!user) {
return null;
}
// 优先使用新字段(departmentId + departmentRef)
if (user.departmentRef?.name) {
return user.departmentRef.name;
}
// 回退到旧字段(department)
return user.department || null;
}
/**
* 获取用户有查看权限的素材类型列表(支持权限继承)
*/
export async function getUserViewableMaterialTypes(
userDepartment: string | null | undefined,
userRole: string,
managedDepartment: string | null | undefined,
userId?: string | null | undefined
): Promise<string[]> {
const requestKey = JSON.stringify([
userDepartment || '',
userRole || '',
managedDepartment || '',
userId || '',
]);
const existingRequest = viewableMaterialTypesInFlight.get(requestKey);
if (existingRequest) {
return [...(await existingRequest)];
}
const request = loadUserViewableMaterialTypes(
userDepartment,
managedDepartment,
userId,
);
viewableMaterialTypesInFlight.set(requestKey, request);
try {
return [...(await request)];
} finally {
if (viewableMaterialTypesInFlight.get(requestKey) === request) {
viewableMaterialTypesInFlight.delete(requestKey);
}
}
}
async function loadUserViewableMaterialTypes(
userDepartment: string | null | undefined,
managedDepartment: string | null | undefined,
userId?: string | null | undefined,
): Promise<string[]> {
// 超级管理员可以看到所有素材类型
if (userId && await isSuperAdmin(userId)) {
return await getAllMaterialTypes();
}
// 如果没有部门信息,返回空数组
const department = managedDepartment || userDepartment;
if (!department) {
return [];
}
// 获取部门列表(用于权限继承检查)
const departments = await prisma.department.findMany({
select: { id: true, name: true, parentId: true },
});
// 获取该部门所有素材类型的权限(包括继承的权限)
const permissions = await getDepartmentPermissions(department, departments);
// 获取有查看权限的素材类型
const viewableTypes = Object.values(permissions)
.filter((p) => p.canView)
.map((p) => p.materialType);
// 按照 DEFAULT_MATERIAL_TYPES 的顺序排序
const sortedTypes = [
...DEFAULT_MATERIAL_TYPES.filter(type => viewableTypes.includes(type as string)),
...viewableTypes.filter(type => !DEFAULT_MATERIAL_TYPES.includes(type as any)).sort()
];
return sortedTypes;
}
/**
* 检查用户是否有查看权限(支持权限继承)
*/
export async function canUserViewMaterial(
userDepartment: string | null | undefined,
userRole: string,
managedDepartment: string | null | undefined,
materialType: string,
userId?: string | null | undefined
): Promise<boolean> {
// 超级管理员可以查看所有素材
if (userId && await isSuperAdmin(userId)) {
return true;
}
// 如果没有部门信息,不允许查看
const department = managedDepartment || userDepartment;
if (!department) {
return false;
}
// 获取部门列表(用于权限继承检查)
const departments = await prisma.department.findMany({
select: { id: true, name: true, parentId: true },
});
const permission = await getDepartmentPermission(department, materialType, departments);
return permission?.canView ?? false;
}
/**
* 检查用户是否有下载权限(支持权限继承)
*/
export async function canUserDownloadMaterial(
userDepartment: string | null | undefined,
userRole: string,
managedDepartment: string | null | undefined,
materialType: string,
userId?: string | null | undefined
): Promise<boolean> {
// 超级管理员可以下载所有素材
if (userId && await isSuperAdmin(userId)) {
console.log(`[Permission] 超级管理员 ${userId} 允许下载 ${materialType}`);
return true;
}
// 如果没有部门信息,不允许下载
const department = managedDepartment || userDepartment;
if (!department) {
console.log(`[Permission] 用户无部门信息,拒绝下载 ${materialType}`);
return false;
}
// 获取部门列表(用于权限继承检查)
const departments = await prisma.department.findMany({
select: { id: true, name: true, parentId: true },
});
const permission = await getDepartmentPermission(department, materialType, departments);
// 增强日志:记录权限检查结果
console.log(`[Permission] 用户部门: ${department}, 素材类型: ${materialType}, 权限: ${permission?.canDownload ?? false}, 继承自: ${permission?.linkedToDepartment || '无'}`);
return permission?.canDownload ?? false;
}
/**
* 为部门创建默认权限配置
* 使用数据库中实际存在的所有素材类型
*/
export async function createDefaultPermissionsForDepartment(
department: string
): Promise<void> {
const existingPermissions = await prisma.departmentMaterialPermission.findMany({
where: { department },
});
const existingTypes = new Set(existingPermissions.map((p: any) => p.materialType));
// 获取数据库中所有素材类型
const allMaterialTypes = filterSelectableMaterialTypes(await getAllMaterialTypes());
const newPermissions = allMaterialTypes
.filter((type) => !existingTypes.has(type))
.map((materialType) => ({
department,
materialType,
canView: false, // 默认不允许查看
canDownload: false, // 默认不允许下载
canShare: true, // 默认允许分享
canDownloadSource: false, // 默认不允许下载源文件
linkedToDepartment: null,
}));
if (newPermissions.length > 0) {
await prisma.departmentMaterialPermission.createMany({
data: newPermissions,
});
}
}
/**
* 检查用户是否有下载源文件(PSD)权限
* 用于任务-详情类型的 PSD 下载权限控制
*/
export async function canUserDownloadSource(
userDepartment: string | null | undefined,
userRole: string,
managedDepartment: string | null | undefined,
materialType: string,
userId?: string | null | undefined
): Promise<boolean> {
// 超级管理员可以下载所有源文件
if (userId && await isSuperAdmin(userId)) {
return true;
}
// 如果没有部门信息,不允许下载
const department = managedDepartment || userDepartment;
if (!department) {
return false;
}
// 获取部门列表(用于权限继承检查),保持和查看/下载权限同一套口径
const departments = await prisma.department.findMany({
select: { id: true, name: true, parentId: true },
});
const permission = await getDepartmentPermission(department, materialType, departments);
return permission?.canDownloadSource ?? false;
}