fix(materials): grant super admins full restricted asset access
This commit is contained in:
parent
268ca4886f
commit
03e54dd541
@ -1,4 +1,4 @@
|
||||
import { buildMaterialOrganizationViewWhere } from '@/lib/material-organization-access'
|
||||
import { buildMaterialOrganizationWhere } from '@/lib/material-organization-access'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
@ -38,7 +38,7 @@ export async function GET(request: NextRequest) {
|
||||
const endDate = searchParams.get('endDate')
|
||||
const departmentKey = searchParams.get('departmentKey')
|
||||
|
||||
const accessWhere = await buildMaterialOrganizationViewWhere((session.user as any).id)
|
||||
const accessWhere = await buildMaterialOrganizationWhere((session.user as any).id)
|
||||
const stats = await getMaterialUploadStats({
|
||||
accessWhere,
|
||||
startDate,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { canViewOperationsGMaterials, getOperationsGMaterialIds } from '@/lib/material-organization-access'
|
||||
import { canUseOperationsGMaterials, getOperationsGMaterialIds } from '@/lib/material-organization-access'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
@ -53,7 +53,7 @@ export async function GET(request: NextRequest) {
|
||||
const endDate = searchParams.get('endDate')
|
||||
const keyword = normalizeKeyword(searchParams.get('keyword'))
|
||||
|
||||
const excludedIds = await canViewOperationsGMaterials((session.user as any).id) ? [] : await getOperationsGMaterialIds()
|
||||
const excludedIds = await canUseOperationsGMaterials((session.user as any).id) ? [] : await getOperationsGMaterialIds()
|
||||
const materialLogWhere: any = { ...(excludedIds.length ? { materialId: { notIn: excludedIds } } : {}) }
|
||||
const downloadLogWhere: any = { ...(excludedIds.length ? { materialId: { notIn: excludedIds } } : {}) }
|
||||
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { invalidateMaterialFacetsCache } from '@/lib/material-facets-cache'
|
||||
import { canAccessOperationsGMaterials } from '@/lib/material-organization-access'
|
||||
import { isRestrictedMaterial } from '@/lib/material-access-scope'
|
||||
import { buildMaterialPrivacyWhere, TASK_DETAIL_PRIVACY_SELECT } from '@/lib/task-detail-privacy'
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { isSystemAdmin } from '@/lib/department-permission'
|
||||
import { isSuperAdmin, isSystemAdmin } from '@/lib/department-permission'
|
||||
import { createMaterialLog } from '@/lib/material-log'
|
||||
import { enqueueMaterialPurge, ensureMaterialPurgeWorkerStarted, MaterialPurgeError } from '@/lib/material-purge'
|
||||
|
||||
@ -25,19 +26,18 @@ export async function POST(request: NextRequest) {
|
||||
const session = await getServerSession(authOptions)
|
||||
|
||||
// 使用统一的权限检查函数(与单条删除保持一致)
|
||||
if (!session || !isSystemAdmin((session.user as any)?.role)) {
|
||||
const operatorId = (session?.user as any)?.id
|
||||
if (!session || (!isSystemAdmin(session.user?.role) && !(await isSuperAdmin(operatorId)))) {
|
||||
return NextResponse.json({ error: '无权限,需要系统管理员权限' }, { status: 403 })
|
||||
}
|
||||
if (!operatorId) {
|
||||
return NextResponse.json({ error: '缺少操作人信息' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { ids, permanent } = await request.json()
|
||||
if (permanent !== undefined && typeof permanent !== 'boolean') {
|
||||
return NextResponse.json({ error: '删除方式无效' }, { status: 400 })
|
||||
}
|
||||
const operatorId = (session.user as any).id
|
||||
|
||||
if (!operatorId) {
|
||||
return NextResponse.json({ error: '缺少操作人信息' }, { status: 401 })
|
||||
}
|
||||
|
||||
// 校验 ids 是数组且非空
|
||||
if (!Array.isArray(ids) || ids.length === 0 || ids.length > 100) {
|
||||
@ -63,15 +63,19 @@ export async function POST(request: NextRequest) {
|
||||
// 查询素材(需要处理任务号)
|
||||
const materials = await prisma.material.findMany({
|
||||
where: { id: { in: ids } },
|
||||
select: { id: true, taskNo: true, productModel: true, materialType: true, accessScope: true }
|
||||
select: { id: true, taskNo: true, productModel: true, materialType: true, ...TASK_DETAIL_PRIVACY_SELECT }
|
||||
})
|
||||
|
||||
if (materials.length === 0) {
|
||||
return NextResponse.json({ error: '未找到要删除的素材' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (materials.some((material) => material.accessScope === 'operations_g') && !(await canAccessOperationsGMaterials(operatorId))) {
|
||||
return NextResponse.json({ error: '无权限操作运营中心 G 专属素材' }, { status: 403 })
|
||||
if (materials.some(isRestrictedMaterial)) {
|
||||
const privacyWhere = await buildMaterialPrivacyWhere(operatorId)
|
||||
const materialIds = materials.map((material) => material.id)
|
||||
if (privacyWhere.OR && await prisma.material.count({ where: { id: { in: materialIds }, ...privacyWhere } }) !== materialIds.length) {
|
||||
return NextResponse.json({ error: '无权限操作部分素材' }, { status: 403 })
|
||||
}
|
||||
}
|
||||
|
||||
const deletedAt = new Date()
|
||||
|
||||
@ -1,17 +1,18 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { isSystemAdmin } from '@/lib/department-permission'
|
||||
import { isSuperAdmin, isSystemAdmin } from '@/lib/department-permission'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { canViewOperationsGMaterials, getOperationsGMaterialIds } from '@/lib/material-organization-access'
|
||||
import { canUseOperationsGMaterials, getOperationsGMaterialIds } from '@/lib/material-organization-access'
|
||||
import { ensureMaterialPurgeWorkerStarted, MaterialPurgeError, retryMaterialPurge, toPurgeJobView } from '@/lib/material-purge'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
async function operator() {
|
||||
const session = await getServerSession(authOptions)
|
||||
if (!session?.user || !(session.user as any).id) return null
|
||||
return isSystemAdmin((session.user as any).role) ? (session.user as any).id as string : null
|
||||
const user = session?.user
|
||||
if (!user?.id) return null
|
||||
return isSystemAdmin(user.role) || await isSuperAdmin(user.id) ? user.id : null
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
@ -22,7 +23,7 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: '清理任务无效' }, { status: 400 })
|
||||
}
|
||||
try {
|
||||
const excludedIds = await canViewOperationsGMaterials(operatorId) ? [] : await getOperationsGMaterialIds()
|
||||
const excludedIds = await canUseOperationsGMaterials(operatorId) ? [] : await getOperationsGMaterialIds()
|
||||
const scopeWhere = excludedIds.length ? { materialId: { notIn: excludedIds } } : {}
|
||||
const [pending, tracked] = await Promise.all([
|
||||
prisma.materialPurgeJob.findMany({ where: { ...scopeWhere, status: { not: 'completed' } }, orderBy: { createdAt: 'asc' }, take: 100 }),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { canAccessOperationsGMaterials } from '@/lib/material-organization-access'
|
||||
import { canAccessMaterialPrivacy } from '@/lib/task-detail-privacy'
|
||||
import { isRestrictedMaterial } from '@/lib/material-access-scope'
|
||||
import { createHash, timingSafeEqual } from 'crypto'
|
||||
import { lstat, realpath } from 'fs/promises'
|
||||
@ -292,7 +292,7 @@ export async function POST(request: NextRequest) {
|
||||
throw new RegeneratePdfPreviewInputError('素材不存在', 404)
|
||||
}
|
||||
const session = isRestrictedMaterial(material) ? await getServerSession(authOptions) : null
|
||||
if (isRestrictedMaterial(material) && !(await canAccessOperationsGMaterials((session?.user as any)?.id))) {
|
||||
if (!(await canAccessMaterialPrivacy(material, (session?.user as any)?.id))) {
|
||||
throw new RegeneratePdfPreviewInputError('无权限操作此素材', 403)
|
||||
}
|
||||
if (material.materialType !== '品牌VI灵感') {
|
||||
|
||||
@ -10,7 +10,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { buildMaterialOrganizationViewWhere } from '@/lib/material-organization-access'
|
||||
import { buildMaterialOrganizationWhere } from '@/lib/material-organization-access'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
@ -24,7 +24,7 @@ export async function GET(req: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
const organizationWhere = await buildMaterialOrganizationViewWhere((session.user as any).id)
|
||||
const organizationWhere = await buildMaterialOrganizationWhere((session.user as any).id)
|
||||
|
||||
// 计算时间范围(30天前和7天前)
|
||||
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { buildMaterialOrganizationViewWhere } from '@/lib/material-organization-access'
|
||||
import { buildMaterialPrivacyWhere } from '@/lib/task-detail-privacy'
|
||||
/**
|
||||
* 回收站 API - 获取已删除的素材
|
||||
*
|
||||
@ -9,13 +9,15 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { isSuperAdmin, isSystemAdmin } from '@/lib/department-permission'
|
||||
import { rewriteMaterialStorageUrlsForClient } from '@/lib/url-rewrite'
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions)
|
||||
|
||||
if (!session || (session.user as any).role !== 'ADMIN') {
|
||||
const user = session?.user
|
||||
if (!user?.id || (!isSystemAdmin(user.role) && !(await isSuperAdmin(user.id)))) {
|
||||
return NextResponse.json(
|
||||
{ error: '无权限' },
|
||||
{ status: 403 }
|
||||
@ -27,7 +29,7 @@ export async function GET(req: NextRequest) {
|
||||
const pageSize = parseInt(searchParams.get('pageSize') || '20')
|
||||
const skip = (page - 1) * pageSize
|
||||
|
||||
const scopeWhere = await buildMaterialOrganizationViewWhere((session.user as any).id)
|
||||
const scopeWhere = await buildMaterialPrivacyWhere(user.id)
|
||||
|
||||
// 获取已删除的素材
|
||||
const [deletedMaterials, total] = await Promise.all([
|
||||
|
||||
@ -11,7 +11,7 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { buildMaterialOrganizationViewWhere } from '@/lib/material-organization-access'
|
||||
import { buildMaterialOrganizationWhere } from '@/lib/material-organization-access'
|
||||
import { LEGACY_COMPATIBLE_VIDEO_MATERIAL_TYPES } from '@/lib/video-material-types'
|
||||
import { USER_TRANSCODE_FAILURE_MESSAGE } from '@/lib/video-transcode-task'
|
||||
|
||||
@ -27,7 +27,7 @@ export async function GET(req: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
const organizationWhere = await buildMaterialOrganizationViewWhere((session.user as any).id)
|
||||
const organizationWhere = await buildMaterialOrganizationWhere((session.user as any).id)
|
||||
|
||||
// 识别视频素材类型
|
||||
const videoMaterialTypes = [...LEGACY_COMPATIBLE_VIDEO_MATERIAL_TYPES]
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { buildMaterialOrganizationViewWhere } from '@/lib/material-organization-access'
|
||||
import { buildMaterialOrganizationWhere } from '@/lib/material-organization-access'
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
@ -12,7 +12,7 @@ export async function GET() {
|
||||
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
||||
}
|
||||
|
||||
const scopeWhere = await buildMaterialOrganizationViewWhere((session.user as any).id);
|
||||
const scopeWhere = await buildMaterialOrganizationWhere((session.user as any).id);
|
||||
|
||||
const categories = await prisma.category.findMany({
|
||||
orderBy: { name: 'asc' },
|
||||
|
||||
@ -323,7 +323,7 @@ export async function GET(
|
||||
canAccessMaterialPrivacy(material as any, userId),
|
||||
isSuperAdmin(userId),
|
||||
]);
|
||||
if (!hasPrivacyAccess && !userIsSuperAdmin) {
|
||||
if (!hasPrivacyAccess) {
|
||||
return NextResponse.json({ error: '无权限查看此素材' }, { status: 403, headers: { 'Cache-Control': 'private, no-store' } });
|
||||
}
|
||||
|
||||
@ -332,7 +332,7 @@ export async function GET(
|
||||
(material as any).materialType,
|
||||
(material as any).createdAt,
|
||||
new Date(),
|
||||
{ bypassWaitPeriod: userHasVideoEarlyAccess(session?.user) },
|
||||
{ bypassWaitPeriod: userIsSuperAdmin || userHasVideoEarlyAccess(session?.user) },
|
||||
);
|
||||
|
||||
const buildVideoPreviewBlockedResponse = () =>
|
||||
@ -348,7 +348,7 @@ export async function GET(
|
||||
);
|
||||
|
||||
const canBypassVideoPreviewGate = isSystemAdmin(userRole) || userIsSuperAdmin;
|
||||
const canEdit = hasPrivacyAccess && ((material as any).userId === userId || canBypassVideoPreviewGate);
|
||||
const canEdit = (material as any).userId === userId || canBypassVideoPreviewGate;
|
||||
const materialWithUser = material as any;
|
||||
const displayAuthorName =
|
||||
materialWithUser.user?.realName ||
|
||||
@ -429,9 +429,9 @@ export async function GET(
|
||||
return buildVideoPreviewBlockedResponse();
|
||||
}
|
||||
|
||||
if (hasPrivacyAccess && canBypassVideoPreviewGate) {
|
||||
if (canBypassVideoPreviewGate) {
|
||||
canAccessSource = true;
|
||||
} else if (hasPrivacyAccess) {
|
||||
} else {
|
||||
const userDownloadSetting = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { canDownload: true },
|
||||
|
||||
@ -90,10 +90,10 @@ export async function GET(
|
||||
const userId = (session.user as any).id
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { canDownload: true },
|
||||
select: { canDownload: true, isSuperAdmin: true },
|
||||
})
|
||||
|
||||
if (!user?.canDownload) {
|
||||
if (!user?.canDownload && !user?.isSuperAdmin) {
|
||||
return NextResponse.json({ error: '您没有下载权限,请联系管理员' }, { status: 403 })
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { isSuperAdmin } from '@/lib/department-permission'
|
||||
import { createMaterialLog } from '@/lib/material-log'
|
||||
import { deleteFileIfUnreferenced, deleteLocalFile } from '@/lib/file-cleanup'
|
||||
import {
|
||||
@ -54,8 +53,7 @@ export async function GET(
|
||||
}
|
||||
const session = await getServerSession(authOptions)
|
||||
const userId = (session?.user as any)?.id
|
||||
const hasPrivacyAccess = await canAccessMaterialPrivacy(material, userId)
|
||||
if (!hasPrivacyAccess && !(await isSuperAdmin(userId))) {
|
||||
if (!(await canAccessMaterialPrivacy(material, userId))) {
|
||||
return NextResponse.json({ error: '无权限访问此素材' }, {
|
||||
status: session?.user ? 403 : 401,
|
||||
headers: { 'Cache-Control': 'private, no-store' },
|
||||
@ -87,7 +85,7 @@ export async function GET(
|
||||
skuName: sku.skuName,
|
||||
previewUrl: (sku as any).previewUrl || '',
|
||||
zipFileName: (sku as any).fileName || '',
|
||||
zipFilePath: hasPrivacyAccess ? sku.filePath : '',
|
||||
zipFilePath: sku.filePath,
|
||||
zipFileSize: sku.fileSize.toString(),
|
||||
sortOrder: sku.sortOrder,
|
||||
whiteBackgroundSourceType: sku.whiteBackgroundSourceType,
|
||||
|
||||
@ -27,7 +27,7 @@ export async function canManageSkuMaterial(
|
||||
): Promise<boolean> {
|
||||
const stored = await prisma.material.findUnique({ where: { id: material.id }, select: TASK_DETAIL_PRIVACY_SELECT })
|
||||
if (!stored || !(await canAccessMaterialPrivacy(stored, user.id))) return false
|
||||
if (material.userId === user.id || isSystemAdmin(user.role) || user.isSuperAdmin === true) {
|
||||
if (material.userId === user.id || isSystemAdmin(user.role)) {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@ -80,10 +80,10 @@ export async function POST(
|
||||
const userId = (session.user as any).id
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { canDownload: true },
|
||||
select: { canDownload: true, isSuperAdmin: true },
|
||||
})
|
||||
|
||||
if (!user?.canDownload) {
|
||||
if (!user?.canDownload && !user?.isSuperAdmin) {
|
||||
return NextResponse.json({ error: '您没有下载权限,请联系管理员' }, { status: 403 })
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@ import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { authOptions } from '@/lib/auth'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { isSuperAdmin } from '@/lib/department-permission'
|
||||
import { rewriteMediaUrlForClient } from '@/lib/url-rewrite'
|
||||
import { createMaterialLog } from '@/lib/material-log'
|
||||
import { resolveWhiteBackgroundPreviewFileUrl } from '@/lib/white-background-preview'
|
||||
@ -67,8 +66,7 @@ export async function GET(
|
||||
}
|
||||
const session = await getServerSession(authOptions)
|
||||
const userId = (session?.user as any)?.id
|
||||
const hasPrivacyAccess = await canAccessMaterialPrivacy(material, userId)
|
||||
if (!hasPrivacyAccess && (validateSkuName !== null || !(await isSuperAdmin(userId)))) {
|
||||
if (!(await canAccessMaterialPrivacy(material, userId))) {
|
||||
return NextResponse.json({ error: '无权限访问该私密素材' }, { status: 403, headers: { 'Cache-Control': 'private, no-store' } })
|
||||
}
|
||||
|
||||
@ -139,7 +137,6 @@ export async function GET(
|
||||
whiteBackgroundGroup?.other?.[0] ||
|
||||
whiteBackgroundGroup?.zipAssets?.[0]
|
||||
const previewAssetUrl = resolveWhiteBackgroundPreviewFileUrl(previewAsset?.fileUrl || '')
|
||||
const canShowWhiteBackgroundPreview = hasPrivacyAccess || /\.(webp|png|jpe?g)(?:[?#]|$)/i.test(previewAssetUrl || '')
|
||||
const uploader = sku.whiteBackgroundUploadedBy
|
||||
? uploaderMap.get(sku.whiteBackgroundUploadedBy) || null
|
||||
: null
|
||||
@ -150,7 +147,7 @@ export async function GET(
|
||||
skuName: sku.skuName,
|
||||
previewUrl: rewriteMediaUrlForClient((sku as any).previewUrl || '', request, 'image') || '',
|
||||
zipFileName: (sku as any).fileName || '',
|
||||
zipFilePath: hasPrivacyAccess ? sku.filePath : '',
|
||||
zipFilePath: sku.filePath,
|
||||
zipFileSize: sku.fileSize.toString(),
|
||||
sortOrder: sku.sortOrder,
|
||||
createdAt: sku.createdAt,
|
||||
@ -165,9 +162,7 @@ export async function GET(
|
||||
whiteBackgroundUploadedAt: sku.whiteBackgroundUploadedAt,
|
||||
whiteBackgroundUploader: uploader,
|
||||
whiteBackgroundFileName: whiteBackgroundUploaded ? `${sku.skuName}_白底图.zip` : '',
|
||||
whiteBackgroundFileUrl: canShowWhiteBackgroundPreview
|
||||
? rewriteMediaUrlForClient(previewAssetUrl || '', request, 'image') || ''
|
||||
: '',
|
||||
whiteBackgroundFileUrl: rewriteMediaUrlForClient(previewAssetUrl || '', request, 'image') || '',
|
||||
}
|
||||
}),
|
||||
total: skus.length,
|
||||
|
||||
@ -14,6 +14,7 @@ import { lookup } from 'mime-types';
|
||||
import {
|
||||
canUserDownloadMaterial,
|
||||
canUserDownloadSource,
|
||||
isSuperAdmin,
|
||||
} from '@/lib/department-permission';
|
||||
import {
|
||||
DownloadErrorCode,
|
||||
@ -21,7 +22,7 @@ import {
|
||||
} from '@/lib/download-errors';
|
||||
import { is3DOriginalType, isProductPackageType } from '@/lib/material-type-mapper';
|
||||
import { ensureMaterialDownloadLogsTable } from '@/lib/material-download-log';
|
||||
import { resolveStoredUploadPath } from '@/lib/upload-paths';
|
||||
import { getUploadRootDir, resolveStoredUploadPath } from '@/lib/upload-paths';
|
||||
import {
|
||||
getVideoDownloadAvailability,
|
||||
getVideoDownloadUnavailableMessage,
|
||||
@ -47,9 +48,10 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const userId = (session.user as any).id;
|
||||
const userRole = (session.user as any).role;
|
||||
const userIsSuperAdmin = await isSuperAdmin(userId);
|
||||
|
||||
// 检查用户级别的下载权限
|
||||
if (userRole !== 'ADMIN') {
|
||||
if (userRole !== 'ADMIN' && !userIsSuperAdmin) {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { canDownload: true },
|
||||
@ -109,7 +111,7 @@ export async function GET(request: NextRequest) {
|
||||
material.materialType,
|
||||
material.createdAt,
|
||||
new Date(),
|
||||
{ bypassWaitPeriod: userHasVideoEarlyAccess(session.user) },
|
||||
{ bypassWaitPeriod: userIsSuperAdmin || userHasVideoEarlyAccess(session.user) },
|
||||
);
|
||||
if (!videoDownloadAvailability.allowed) {
|
||||
return NextResponse.json(
|
||||
@ -126,8 +128,7 @@ export async function GET(request: NextRequest) {
|
||||
const managedDepartment = (session.user as any).managedDepartment;
|
||||
|
||||
// 超级管理员不受限制
|
||||
const { isSuperAdmin } = await import('@/lib/department-permission');
|
||||
if (!(await isSuperAdmin(userId))) {
|
||||
if (!userIsSuperAdmin) {
|
||||
const canDownload = isRestrictedMaterial(material) || await canUserDownloadMaterial(
|
||||
userDepartment,
|
||||
userRole,
|
||||
@ -261,7 +262,7 @@ async function serveFile(filePath: string, material: any, fileNameSuffix: string
|
||||
);
|
||||
}
|
||||
|
||||
const allowedDirs = getAllowedFileDirectories();
|
||||
const allowedDirs = [getUploadRootDir(), ...getAllowedFileDirectories()];
|
||||
if (!isPathInAllowedDirs(filePath, allowedDirs)) {
|
||||
console.error('[Download] 路径越界:', filePath, '允许的目录:', allowedDirs);
|
||||
return NextResponse.json(
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { invalidateMaterialFacetsCache } from '@/lib/material-facets-cache'
|
||||
import { canAccessOperationsGMaterials } from '@/lib/material-organization-access'
|
||||
import { canAccessMaterialPrivacy } from '@/lib/task-detail-privacy'
|
||||
import { isSuperAdmin, isSystemAdmin } from '@/lib/department-permission'
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
@ -15,7 +16,8 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
||||
}
|
||||
const userRole = (session.user as any).role;
|
||||
if (userRole !== 'ADMIN') {
|
||||
const userId = (session.user as any).id;
|
||||
if (!isSystemAdmin(userRole) && !(await isSuperAdmin(userId))) {
|
||||
return NextResponse.json({ error: '无权限' }, { status: 403 });
|
||||
}
|
||||
|
||||
@ -34,8 +36,8 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: '素材不存在' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (material.accessScope === 'operations_g' && !(await canAccessOperationsGMaterials((session.user as any).id))) {
|
||||
return NextResponse.json({ error: '无权限操作运营中心 G 专属素材' }, { status: 403 });
|
||||
if (!(await canAccessMaterialPrivacy(material, userId))) {
|
||||
return NextResponse.json({ error: '无权限操作此素材' }, { status: 403 });
|
||||
}
|
||||
|
||||
const restoredTaskNo = getOriginalTaskNo((material as any).taskNo);
|
||||
@ -143,7 +145,6 @@ export async function POST(request: NextRequest) {
|
||||
]);
|
||||
|
||||
// 记录操作日志
|
||||
const userId = (session.user as any).id;
|
||||
await createMaterialLog({
|
||||
materialId: id,
|
||||
userId,
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import { invalidateMaterialFacetsCache } from '@/lib/material-facets-cache'
|
||||
import { canAccessOperationsGMaterials } from '@/lib/material-organization-access'
|
||||
import { isOperationsGMaterial, isPreviewAssetType, isRestrictedMaterial } from '@/lib/material-access-scope'
|
||||
import { canAccessMaterialPrivacy, isPrivateTaskDetail, TASK_DETAIL_PRIVACY_SELECT } from '@/lib/task-detail-privacy';
|
||||
import { isRestrictedMaterial } from '@/lib/material-access-scope'
|
||||
import { canAccessMaterialPrivacy, TASK_DETAIL_PRIVACY_SELECT } from '@/lib/task-detail-privacy';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { isSystemAdmin } from '@/lib/department-permission';
|
||||
import { canUserViewMaterial, isSuperAdmin, isSystemAdmin } from '@/lib/department-permission';
|
||||
import { createMaterialLog } from '@/lib/material-log';
|
||||
import { enqueueMaterialPurge, ensureMaterialPurgeWorkerStarted, MaterialPurgeError } from '@/lib/material-purge';
|
||||
import { normalizeUrl, normalizeMaterial } from '@/lib/utils';
|
||||
@ -176,9 +175,7 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ material: null });
|
||||
}
|
||||
const userId = (session.user as any).id;
|
||||
const { isSuperAdmin, canUserViewMaterial } = await import('@/lib/department-permission');
|
||||
const hasPrivacyAccess = await canAccessMaterialPrivacy(material, userId);
|
||||
if (!hasPrivacyAccess && !(await isSuperAdmin(userId))) {
|
||||
if (!(await canAccessMaterialPrivacy(material, userId))) {
|
||||
return NextResponse.json({ error: '无权限查看此素材' }, { status: 403, headers: { 'Cache-Control': 'private, no-store' } });
|
||||
}
|
||||
|
||||
@ -266,7 +263,6 @@ export async function GET(request: NextRequest) {
|
||||
// 优先使用用户的真实姓名或用户名,如果没有才使用 authorName
|
||||
const displayAuthorName = material.user?.realName || material.user?.username || material.authorName;
|
||||
const formattedAssets = (material.assets || [])
|
||||
.filter((asset: any) => hasPrivacyAccess || isPreviewAssetType(asset.assetType))
|
||||
.map((asset: any) => formatTaskDetailAssetSummary(
|
||||
asset,
|
||||
displayAuthorName,
|
||||
@ -275,9 +271,6 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const normalizedMaterial = normalizeMaterial({
|
||||
...material,
|
||||
isViewOnly: !hasPrivacyAccess,
|
||||
zipFileName: hasPrivacyAccess ? material.zipFileName : '',
|
||||
videoSourceUrl: hasPrivacyAccess ? material.videoSourceUrl : null,
|
||||
authorName: displayAuthorName, // 使用处理后的作者名
|
||||
previewUrl, // 使用处理后的previewUrl
|
||||
zipFileSize: material.zipFileSize.toString(),
|
||||
@ -773,10 +766,6 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
const formattedMaterials = materials.map(material => {
|
||||
const isViewOnly = context.isSuperAdmin && (
|
||||
(isOperationsGMaterial(material) && !context.canAccessOperationsGMaterials) ||
|
||||
(isPrivateTaskDetail(material) && !context.canAccessPrivateTaskDetails && material.taskDetailRequesterUserId !== user.id)
|
||||
);
|
||||
// 处理previewUrl的fallback逻辑
|
||||
let previewUrl = material.previewUrl || material.videoCoverUrl || '';
|
||||
|
||||
@ -814,7 +803,6 @@ export async function GET(request: NextRequest) {
|
||||
// 优先使用用户的真实姓名或用户名,如果没有才使用 authorName
|
||||
const displayAuthorName = material.user?.realName || material.user?.username || material.authorName;
|
||||
const formattedAssets = (material.assets || [])
|
||||
.filter((asset: any) => !isViewOnly || isPreviewAssetType(asset.assetType))
|
||||
.map((asset: any) => formatTaskDetailAssetSummary(
|
||||
asset,
|
||||
displayAuthorName,
|
||||
@ -823,7 +811,6 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const formatted = {
|
||||
...material,
|
||||
isViewOnly,
|
||||
authorName: displayAuthorName, // 使用处理后的作者名
|
||||
previewUrl, // 使用处理后的previewUrl
|
||||
zipFileSize: material.zipFileSize.toString(),
|
||||
@ -843,7 +830,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
// 处理音频文件的zipFileName(特殊处理)
|
||||
let processedZipFileName = formatted.zipFileName;
|
||||
if (!isViewOnly && formatted.zipFileName && formatted.materialType === '音乐') {
|
||||
if (formatted.zipFileName && formatted.materialType === '音乐') {
|
||||
const zipFileName = formatted.zipFileName;
|
||||
if (zipFileName.startsWith('http://') || zipFileName.startsWith('https://')) {
|
||||
// 老素材:S3 URL,使用统一规则(音频通常>1MB,会走代理)
|
||||
@ -871,13 +858,13 @@ export async function GET(request: NextRequest) {
|
||||
// 视频:使用统一规则(内网用户直接访问内网MinIO,外网用户通常>1MB会走代理)
|
||||
videoPreview540Url: rewriteMediaUrlForClient(formatted.videoPreview540Url, request, 'video'),
|
||||
videoPreviewClipUrl: rewriteMediaUrlForClient(formatted.videoPreviewClipUrl, request, 'video'),
|
||||
videoSourceUrl: isViewOnly ? null : rewriteMediaUrlForClient(formatted.videoSourceUrl, request, 'video'),
|
||||
videoSourceUrl: rewriteMediaUrlForClient(formatted.videoSourceUrl, request, 'video'),
|
||||
// 音频文件:特殊处理
|
||||
zipFileName: isViewOnly ? '' : processedZipFileName,
|
||||
zipFileName: processedZipFileName,
|
||||
assets: formattedAssets,
|
||||
};
|
||||
|
||||
return applyVideoPreviewPolicy(rewrittenMaterial, new Date(), { bypassWaitPeriod: userHasVideoEarlyAccess(session?.user) });
|
||||
return applyVideoPreviewPolicy(rewrittenMaterial, new Date(), { bypassWaitPeriod: context.isSuperAdmin || userHasVideoEarlyAccess(session?.user) });
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
@ -921,7 +908,8 @@ export async function DELETE(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
const userRole = (session.user as any).role;
|
||||
if (!isSystemAdmin(userRole)) {
|
||||
const userId = (session.user as any).id;
|
||||
if (!isSystemAdmin(userRole) && !(await isSuperAdmin(userId))) {
|
||||
return NextResponse.json(
|
||||
{ error: '无权限' },
|
||||
{ status: 403 }
|
||||
@ -957,10 +945,8 @@ export async function DELETE(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
const userId = (session.user as any).id;
|
||||
|
||||
if (material.accessScope === 'operations_g' && !(await canAccessOperationsGMaterials(userId))) {
|
||||
return NextResponse.json({ error: '无权限操作运营中心 G 专属素材' }, { status: 403 });
|
||||
if (!(await canAccessMaterialPrivacy(material, userId))) {
|
||||
return NextResponse.json({ error: '无权限操作此素材' }, { status: 403 });
|
||||
}
|
||||
|
||||
// 软删除
|
||||
|
||||
@ -11,7 +11,6 @@ import {
|
||||
canUserDownloadSource,
|
||||
hasSystemAdminPermission,
|
||||
isSuperAdmin,
|
||||
isSystemAdmin,
|
||||
} from '@/lib/department-permission';
|
||||
import { isProductPackageType, is3DOriginalType, needsAssetsList } from '@/lib/material-type-mapper';
|
||||
import { deriveTaskDetailIsFullSet } from '@/lib/task-detail-full-set';
|
||||
@ -19,6 +18,7 @@ import { canManageMaterial } from '@/lib/material-authorization';
|
||||
import { detectFileKindFromName, getCertificateReportFileKindByAssetType, getDefaultCertificateExpiryDate, isCertificateReportMaterialType, isPublicCertificateReportPdf } from '@/lib/certificate-report';
|
||||
import { DownloadErrorCode } from '@/lib/download-errors';
|
||||
import { getVideoPreviewPolicyFields, userHasVideoEarlyAccess } from '@/lib/video-download-policy';
|
||||
import { isVideoMaterialType } from '@/lib/video-material-types';
|
||||
|
||||
// 解析 displayImages 字段
|
||||
function parseDisplayImages(displayImages: any): string[] {
|
||||
@ -160,8 +160,7 @@ export async function GET(request: NextRequest) {
|
||||
if (!material) {
|
||||
return NextResponse.json({ error: '素材不存在' }, { status: 404 });
|
||||
}
|
||||
const hasPrivacyAccess = await canAccessMaterialPrivacy(material as any, userId);
|
||||
if (!hasPrivacyAccess && !(await isSuperAdmin(userId))) {
|
||||
if (!(await canAccessMaterialPrivacy(material as any, userId))) {
|
||||
return NextResponse.json({ error: '请登录有权限的账号查看此素材' }, { status: userId ? 403 : 401, headers: { 'Cache-Control': 'private, no-store' } });
|
||||
}
|
||||
|
||||
@ -169,7 +168,7 @@ export async function GET(request: NextRequest) {
|
||||
(material as any).materialType,
|
||||
(material as any).createdAt,
|
||||
new Date(),
|
||||
{ bypassWaitPeriod: userHasVideoEarlyAccess(session?.user) },
|
||||
{ bypassWaitPeriod: userHasVideoEarlyAccess(session?.user) || (isVideoMaterialType((material as any).materialType) && await isSuperAdmin(userId)) },
|
||||
);
|
||||
|
||||
if (videoPreviewPolicy.videoPreviewBlocked) {
|
||||
@ -293,7 +292,7 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
// 任务-详情 PSD 和任务原图 ZIP 按各自素材类型检查源文件权限。
|
||||
let canAccessSource = false;
|
||||
if (hasPrivacyAccess && userId && currentUser) {
|
||||
if (userId && currentUser) {
|
||||
if (hasSystemAdminPermission(currentUser.role, Boolean(currentUser.isSuperAdmin))) {
|
||||
canAccessSource = true;
|
||||
} else if (currentUser.canDownload) {
|
||||
@ -321,9 +320,8 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
const capabilities = {
|
||||
canFavorite: Boolean(hasPrivacyAccess && userId && currentUser),
|
||||
canFavorite: Boolean(userId && currentUser),
|
||||
canEdit: Boolean(
|
||||
hasPrivacyAccess &&
|
||||
userId &&
|
||||
currentUser &&
|
||||
(
|
||||
@ -331,14 +329,13 @@ export async function GET(request: NextRequest) {
|
||||
hasSystemAdminPermission(currentUser.role, Boolean(currentUser.isSuperAdmin))
|
||||
)
|
||||
),
|
||||
// 与 DELETE /api/materials 当前契约保持一致:仅系统管理员角色可删除。
|
||||
canDelete: Boolean(hasPrivacyAccess && currentUser && isSystemAdmin(currentUser.role)),
|
||||
canDelete: Boolean(currentUser && hasSystemAdminPermission(currentUser.role, currentUser.isSuperAdmin)),
|
||||
};
|
||||
|
||||
// 任务-详情单图编辑(替换/改名/删除)与元数据编辑权限口径不同:
|
||||
// 走 canManageMaterial,托管部门的成员也能维护本部门素材。
|
||||
let canManageTaskDetailAssets = false;
|
||||
if (hasPrivacyAccess && userId && currentUser && isProductPackageType(material.materialType as unknown as string)) {
|
||||
if (userId && currentUser && isProductPackageType(material.materialType as unknown as string)) {
|
||||
canManageTaskDetailAssets = await canManageMaterial(
|
||||
{ id: userId, role: currentUser.role, isSuperAdmin: currentUser.isSuperAdmin },
|
||||
{ id: material.id, userId: material.userId } as any,
|
||||
@ -352,7 +349,6 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const response: any = {
|
||||
...material,
|
||||
isViewOnly: !hasPrivacyAccess,
|
||||
zipFileName: getSourceDisplayName(material.zipFileName || material.zipFilePath),
|
||||
zipFilePath: '',
|
||||
videoSourceUrl: canAccessSource ? material.videoSourceUrl : null,
|
||||
|
||||
@ -2,7 +2,7 @@ import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { buildMaterialViewWhere } from '@/lib/task-detail-privacy';
|
||||
import { buildMaterialPrivacyWhere } from '@/lib/task-detail-privacy';
|
||||
|
||||
// GET - 获取素材统计数据(只统计未删除的素材)
|
||||
export async function GET() {
|
||||
@ -11,7 +11,7 @@ export async function GET() {
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
||||
}
|
||||
const privacyWhere = await buildMaterialViewWhere((session.user as any).id);
|
||||
const privacyWhere = await buildMaterialPrivacyWhere((session.user as any).id);
|
||||
|
||||
// 获取总数(只统计未删除的素材)
|
||||
let total: number;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { canAccessOperationsGMaterials, canViewOperationsGMaterials } from '@/lib/material-organization-access'
|
||||
import { canUseOperationsGMaterials } from '@/lib/material-organization-access'
|
||||
// 视频转码API - 完整版本(支持自动恢复)
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
@ -6,7 +6,7 @@ import { prisma } from '@/lib/prisma';
|
||||
import { transcodeQueue, VIDEO_MATERIAL_TYPES } from '@/lib/video-transcode-queue';
|
||||
import { executeTranscodeTask, USER_TRANSCODE_FAILURE_MESSAGE } from '@/lib/video-transcode-task';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
import { canUserViewMaterial, hasSystemAdminPermission } from '@/lib/department-permission';
|
||||
import { canUserViewMaterial, hasSystemAdminPermission, isSuperAdmin } from '@/lib/department-permission';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const maxDuration = 3600; // 1小时
|
||||
@ -69,10 +69,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
const sessionUser = session.user as any;
|
||||
const isSystemAdministrator = hasSystemAdminPermission(
|
||||
sessionUser.role,
|
||||
Boolean(sessionUser.isSuperAdmin),
|
||||
);
|
||||
const isSystemAdministrator = hasSystemAdminPermission(sessionUser.role) || await isSuperAdmin(sessionUser.id);
|
||||
const retryAfter = consumeRateLimit(`status:${String(sessionUser.id || 'unknown')}`, STATUS_RATE_LIMIT);
|
||||
if (retryAfter) {
|
||||
return rateLimitResponse(retryAfter);
|
||||
@ -111,7 +108,7 @@ export async function GET(request: NextRequest) {
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
const canAccessG = await canViewOperationsGMaterials(sessionUser.id);
|
||||
const canAccessG = await canUseOperationsGMaterials(sessionUser.id);
|
||||
if (material.accessScope === 'operations_g' && !canAccessG) {
|
||||
return NextResponse.json({ error: '无权限访问此素材' }, { status: 403, headers: { 'Cache-Control': 'private, no-store' } });
|
||||
}
|
||||
@ -173,10 +170,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
const sessionUser = session.user as any;
|
||||
const isSystemAdministrator = hasSystemAdminPermission(
|
||||
sessionUser.role,
|
||||
Boolean(sessionUser.isSuperAdmin),
|
||||
);
|
||||
const isSystemAdministrator = hasSystemAdminPermission(sessionUser.role) || await isSuperAdmin(sessionUser.id);
|
||||
|
||||
const retryAfter = consumeRateLimit(`start:${String(sessionUser.id || 'unknown')}`, START_RATE_LIMIT);
|
||||
if (retryAfter) {
|
||||
@ -223,7 +217,7 @@ export async function POST(request: NextRequest) {
|
||||
);
|
||||
}
|
||||
|
||||
const canAccessG = await canAccessOperationsGMaterials(sessionUser.id);
|
||||
const canAccessG = await canUseOperationsGMaterials(sessionUser.id);
|
||||
if (material.accessScope === 'operations_g' && !canAccessG) {
|
||||
return NextResponse.json({ error: '无权限操作此素材' }, { status: 403 });
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { buildMaterialOrganizationViewWhere } from '@/lib/material-organization-access'
|
||||
import { buildMaterialOrganizationWhere } from '@/lib/material-organization-access'
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
@ -13,7 +13,7 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
||||
}
|
||||
|
||||
const scopeWhere = await buildMaterialOrganizationViewWhere((session.user as any).id);
|
||||
const scopeWhere = await buildMaterialOrganizationWhere((session.user as any).id);
|
||||
|
||||
const platforms = await prisma.platform.findMany({
|
||||
orderBy: { createdAt: 'asc' },
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { buildMaterialOrganizationViewWhere } from '@/lib/material-organization-access'
|
||||
import { buildMaterialOrganizationWhere } from '@/lib/material-organization-access'
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
@ -12,7 +12,7 @@ export async function GET() {
|
||||
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
||||
}
|
||||
|
||||
const scopeWhere = await buildMaterialOrganizationViewWhere((session.user as any).id);
|
||||
const scopeWhere = await buildMaterialOrganizationWhere((session.user as any).id);
|
||||
|
||||
const categories = await prisma.productCategory.findMany({
|
||||
orderBy: { name: 'asc' },
|
||||
|
||||
@ -15,6 +15,7 @@ import {
|
||||
} from '@/lib/proxy-media-security'
|
||||
import { findBlockedVideoPreviewByUrl, isVideoMediaPath } from '@/lib/video-preview-access'
|
||||
import { userHasVideoEarlyAccess } from '@/lib/video-download-policy'
|
||||
import { isSuperAdmin } from '@/lib/department-permission'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const runtime = 'nodejs'
|
||||
@ -165,7 +166,7 @@ export async function GET(request: NextRequest) {
|
||||
targetUrl.pathname,
|
||||
],
|
||||
new Date(),
|
||||
{ bypassWaitPeriod: userHasVideoEarlyAccess(videoSession?.user) },
|
||||
{ bypassWaitPeriod: userHasVideoEarlyAccess(videoSession?.user) || await isSuperAdmin(videoSession?.user.id) },
|
||||
)
|
||||
|
||||
if (blockedPreview) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { buildMaterialOrganizationViewWhere } from '@/lib/material-organization-access'
|
||||
import { buildMaterialOrganizationWhere } from '@/lib/material-organization-access'
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
@ -12,7 +12,7 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: '未登录' }, { status: 401 });
|
||||
}
|
||||
|
||||
const scopeWhere = await buildMaterialOrganizationViewWhere((session.user as any).id);
|
||||
const scopeWhere = await buildMaterialOrganizationWhere((session.user as any).id);
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const category = searchParams.get('category');
|
||||
|
||||
@ -11,12 +11,13 @@ import {
|
||||
canUserDownloadMaterial,
|
||||
canUserDownloadSource,
|
||||
hasSystemAdminPermission,
|
||||
isSuperAdmin,
|
||||
} from '@/lib/department-permission'
|
||||
import { DownloadErrorCode } from '@/lib/download-errors'
|
||||
import { is3DOriginalType, isProductPackageType } from '@/lib/material-type-mapper'
|
||||
import { findBlockedVideoPreviewByUrl, isVideoMediaPath } from '@/lib/video-preview-access'
|
||||
import { userHasVideoEarlyAccess } from '@/lib/video-download-policy'
|
||||
import { canAccessMaterialPrivacy, canViewMaterialPrivacy, TASK_DETAIL_PRIVACY_SELECT, type TaskDetailPrivacyMaterial } from '@/lib/task-detail-privacy'
|
||||
import { canAccessMaterialPrivacy, TASK_DETAIL_PRIVACY_SELECT, type TaskDetailPrivacyMaterial } from '@/lib/task-detail-privacy'
|
||||
|
||||
export const runtime = 'nodejs'
|
||||
export const dynamic = 'force-dynamic'
|
||||
@ -269,8 +270,7 @@ async function handleFileRequest(
|
||||
}
|
||||
if (isRestrictedMaterial(access.material)) {
|
||||
const session = await getServerSession(authOptions)
|
||||
const checkPrivacy = access.kind === 'preview' ? canViewMaterialPrivacy : canAccessMaterialPrivacy
|
||||
if (!(await checkPrivacy(access.material, (session?.user as any)?.id))) {
|
||||
if (!(await canAccessMaterialPrivacy(access.material, (session?.user as any)?.id))) {
|
||||
return new Response(headOnly ? null : '无权限访问此素材', {
|
||||
status: session?.user ? 403 : 401,
|
||||
headers: { 'Cache-Control': 'private, no-store', Vary: 'Cookie' },
|
||||
@ -301,7 +301,7 @@ async function handleFileRequest(
|
||||
normalizedRelativePath,
|
||||
],
|
||||
new Date(),
|
||||
{ bypassWaitPeriod: userHasVideoEarlyAccess(videoSession?.user) },
|
||||
{ bypassWaitPeriod: userHasVideoEarlyAccess(videoSession?.user) || await isSuperAdmin(videoSession?.user.id) },
|
||||
)
|
||||
|
||||
if (blockedPreview) {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { buildMaterialViewWhere } from '@/lib/task-detail-privacy';
|
||||
import { buildMaterialPrivacyWhere } from '@/lib/task-detail-privacy';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
@ -47,7 +47,7 @@ export async function GET(request: NextRequest) {
|
||||
const favorites = await prisma.materialFavorite.findMany({
|
||||
where: {
|
||||
userId,
|
||||
material: await buildMaterialViewWhere(userId),
|
||||
material: await buildMaterialPrivacyWhere(userId),
|
||||
},
|
||||
include: {
|
||||
material: {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { buildMaterialViewWhere } from '@/lib/task-detail-privacy';
|
||||
import { buildMaterialPrivacyWhere } from '@/lib/task-detail-privacy';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
@ -43,7 +43,7 @@ export async function GET(request: NextRequest) {
|
||||
const materials = await prisma.material.findMany({
|
||||
where: {
|
||||
userId,
|
||||
AND: [await buildMaterialViewWhere(userId)],
|
||||
AND: [await buildMaterialPrivacyWhere(userId)],
|
||||
deletedAt: null,
|
||||
},
|
||||
select: {
|
||||
@ -107,7 +107,7 @@ export async function GET(request: NextRequest) {
|
||||
const materials = await prisma.material.findMany({
|
||||
where: {
|
||||
userId,
|
||||
AND: [await buildMaterialViewWhere(userId)],
|
||||
AND: [await buildMaterialPrivacyWhere(userId)],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@ -1492,7 +1492,7 @@ export default function MaterialLibraryPage() {
|
||||
<MaterialGrid
|
||||
materials={materials as any}
|
||||
displayedCount={displayedCount}
|
||||
session={session ? { user: { id: (session.user as any)?.id || '', role: (session.user as any)?.role || '' } } : null}
|
||||
session={session?.user ? { user: session.user } : null}
|
||||
loading={loading}
|
||||
isTransitionLoading={isTransitionLoading}
|
||||
playingAudioId={playingAudioId}
|
||||
|
||||
@ -1563,7 +1563,7 @@ export default function Home() {
|
||||
<MaterialGrid
|
||||
materials={materials as any}
|
||||
displayedCount={displayedCount}
|
||||
session={session ? { user: { id: (session.user as any)?.id || '', role: (session.user as any)?.role || '' } } : null}
|
||||
session={session?.user ? { user: session.user } : null}
|
||||
loading={loading}
|
||||
isTransitionLoading={isTransitionLoading}
|
||||
playingAudioId={playingAudioId}
|
||||
|
||||
@ -71,7 +71,6 @@ interface Material {
|
||||
isLiked?: boolean // 是否已点赞
|
||||
isFavorited?: boolean // 是否已收藏
|
||||
capabilities?: MaterialDetailCapabilities
|
||||
isViewOnly?: boolean
|
||||
taskNo?: string // 产品全案任务号
|
||||
taskStatus?: string // 产品全案任务状态
|
||||
taskFetchedAt?: string // 产品全案被获取时间
|
||||
@ -601,7 +600,7 @@ export default function SharePage() {
|
||||
: ''
|
||||
|
||||
const canManageStandardModelSku = Boolean(
|
||||
session?.user && material && !material.isViewOnly,
|
||||
session?.user && material,
|
||||
)
|
||||
|
||||
const reportViewerItems: ViewerAssetItem[] = material && isCertificateReportMaterialType(material.materialType)
|
||||
@ -1698,7 +1697,6 @@ export default function SharePage() {
|
||||
assets: material.assets || [],
|
||||
}}
|
||||
canDownloadSource={material.canDownloadSource ?? false} // 产品全案PSD下载权限参考"详情模板"类型的 canDownloadSource 权限
|
||||
canDownload={!material.isViewOnly}
|
||||
showToast={showToast}
|
||||
canManageAssets={Boolean(material.canManageTaskDetailAssets)}
|
||||
editingAssetId={taskDetailEditingAssetId}
|
||||
@ -2060,7 +2058,6 @@ export default function SharePage() {
|
||||
<SkuList
|
||||
materialId={materialId}
|
||||
canEdit={canManageStandardModelSku}
|
||||
canDownload={!material.isViewOnly}
|
||||
onEditClick={() => setShowSkuEditModal(true)}
|
||||
onSelectionChange={setSelectedSkuIds}
|
||||
refreshKey={skuListRefreshKey}
|
||||
@ -2160,7 +2157,7 @@ export default function SharePage() {
|
||||
)}
|
||||
|
||||
{/* 操作按钮 - 统一样式 */}
|
||||
{!material.isViewOnly && <div className="space-y-3 pt-2">
|
||||
<div className="space-y-3 pt-2">
|
||||
{material.materialType === '标准模型' ? (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
@ -2252,7 +2249,7 @@ export default function SharePage() {
|
||||
</button>
|
||||
)}
|
||||
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -41,13 +41,12 @@ interface Sku {
|
||||
interface SkuListProps {
|
||||
materialId: string
|
||||
canEdit: boolean
|
||||
canDownload?: boolean
|
||||
onEditClick: () => void
|
||||
onSelectionChange?: (selectedIds: Set<string>) => void
|
||||
refreshKey?: number
|
||||
}
|
||||
|
||||
export default function SkuList({ materialId, canEdit, canDownload = true, onEditClick, onSelectionChange, refreshKey = 0 }: SkuListProps) {
|
||||
export default function SkuList({ materialId, canEdit, onEditClick, onSelectionChange, refreshKey = 0 }: SkuListProps) {
|
||||
const [skus, setSkus] = useState<Sku[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const { toast, showToast } = useToast()
|
||||
@ -207,7 +206,7 @@ export default function SkuList({ materialId, canEdit, canDownload = true, onEdi
|
||||
)}
|
||||
</div>
|
||||
|
||||
{canDownload && <div className="mb-3 flex items-center justify-between rounded-[8px] border border-[#e7e8ea] bg-[#fafafa] px-3 py-2">
|
||||
<div className="mb-3 flex items-center justify-between rounded-[8px] border border-[#e7e8ea] bg-[#fafafa] px-3 py-2">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
@ -220,7 +219,7 @@ export default function SkuList({ materialId, canEdit, canDownload = true, onEdi
|
||||
{selectedSkuIds.size > 0 && (
|
||||
<span className="text-[12px] text-[#86898e]">已选 {selectedSkuIds.size} 个</span>
|
||||
)}
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto space-y-2 pr-1" style={{ maxHeight: 'calc(100vh - 500px)' }}>
|
||||
{skus.map((sku) => (
|
||||
@ -228,12 +227,12 @@ export default function SkuList({ materialId, canEdit, canDownload = true, onEdi
|
||||
key={sku.id}
|
||||
className="flex items-center gap-2 rounded-[8px] border border-[#e7e8ea] p-2 transition-colors hover:bg-[#fafafa]"
|
||||
>
|
||||
{canDownload && <input
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedSkuIds.has(sku.id)}
|
||||
onChange={() => handleSelectSku(sku.id)}
|
||||
className="w-4 h-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 flex-shrink-0"
|
||||
/>}
|
||||
/>
|
||||
|
||||
<div
|
||||
className="w-10 h-10 flex-shrink-0 rounded overflow-hidden cursor-pointer hover:opacity-80 transition-opacity"
|
||||
@ -252,7 +251,7 @@ export default function SkuList({ materialId, canEdit, canDownload = true, onEdi
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="truncate text-[13px] font-medium text-[#1a1c1d]">{sku.skuName}</p>
|
||||
<p className="truncate text-[12px] text-[#86898e]">
|
||||
模型:{(canDownload ? sku.zipFilePath : sku.zipFileName)?.trim() ? '已上传' : '未上传'}
|
||||
模型:{sku.zipFilePath && sku.zipFilePath.trim() ? '已上传' : '未上传'}
|
||||
<span className="mx-1.5 text-[#e7e8ea]">·</span>
|
||||
白底图:{sku.whiteBackgroundUploaded ? `${sku.whiteBackgroundCount || 0}张` : '未上传'}
|
||||
{sku.whiteBackgroundUploaded && sku.whiteBackgroundComplete === false ? '(未补齐)' : ''}
|
||||
@ -269,7 +268,7 @@ export default function SkuList({ materialId, canEdit, canDownload = true, onEdi
|
||||
)}
|
||||
</div>
|
||||
|
||||
{canDownload && <div className="flex flex-shrink-0 items-center gap-1.5">
|
||||
<div className="flex flex-shrink-0 items-center gap-1.5">
|
||||
<button
|
||||
onClick={() => handleDownloadWhiteBackground(sku)}
|
||||
disabled={!sku.whiteBackgroundUploaded || downloadingWhiteBgSkuId === sku.id}
|
||||
@ -284,7 +283,7 @@ export default function SkuList({ materialId, canEdit, canDownload = true, onEdi
|
||||
>
|
||||
{downloadingSkuId === sku.id ? '下载中' : '模型'}
|
||||
</button>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -52,7 +52,6 @@ interface ProductPackageDetailProps {
|
||||
assets: Asset[];
|
||||
};
|
||||
canDownloadSource: boolean;
|
||||
canDownload?: boolean;
|
||||
onDownload: (type: 'all' | 'main' | 'sku' | 'detail' | 'psd', assetId?: string) => Promise<void>;
|
||||
onBack?: () => void;
|
||||
showToast?: (message: string, options?: { variant?: 'success' | 'error' }) => void;
|
||||
@ -218,7 +217,6 @@ function AssetEditMenu({
|
||||
export default function ProductPackageDetail({
|
||||
material,
|
||||
canDownloadSource,
|
||||
canDownload = true,
|
||||
onDownload,
|
||||
onBack,
|
||||
showToast,
|
||||
@ -639,7 +637,7 @@ export default function ProductPackageDetail({
|
||||
)}
|
||||
|
||||
{/* 下载按钮组 */}
|
||||
{canDownload && <div className={`${detailPanelClass} p-4`}>
|
||||
<div className={`${detailPanelClass} p-4`}>
|
||||
<h4 className={detailSectionTitleClass}>批量下载</h4>
|
||||
{downloading && (
|
||||
<div className="mb-3 rounded-[8px] border border-[#e7e8ea] bg-[#fafafa] p-3">
|
||||
@ -772,7 +770,7 @@ export default function ProductPackageDetail({
|
||||
下载全部
|
||||
</button>
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
{/* 返回上一页按钮 */}
|
||||
{onBack && (
|
||||
|
||||
@ -18,7 +18,6 @@ import {
|
||||
// 素材类型定义
|
||||
export interface Material {
|
||||
accessScope?: string
|
||||
isViewOnly?: boolean
|
||||
|
||||
id: string
|
||||
productModel: string
|
||||
@ -66,14 +65,15 @@ export interface Material {
|
||||
}
|
||||
|
||||
// 用户会话类型
|
||||
interface UserSession {
|
||||
export interface MaterialCardUser {
|
||||
id: string
|
||||
role: string
|
||||
isSuperAdmin?: boolean
|
||||
}
|
||||
|
||||
interface MaterialCardProps {
|
||||
material: Material
|
||||
session: { user: UserSession } | null
|
||||
session: { user: MaterialCardUser } | null
|
||||
// 音频播放状态
|
||||
playingAudioId: string | null
|
||||
audioElement: HTMLAudioElement | null
|
||||
@ -161,10 +161,13 @@ export default function MaterialCard({
|
||||
const [shouldLoadVideoPreview, setShouldLoadVideoPreview] = useState(false)
|
||||
|
||||
// 判断素材类型
|
||||
const canManageLibrary = session?.user.isSuperAdmin || session?.user.role === 'ADMIN'
|
||||
const isVideoType = isVideoMaterialType(material.materialType)
|
||||
const videoDownloadAvailability = getVideoDownloadAvailability(
|
||||
material.materialType,
|
||||
material.createdAt,
|
||||
new Date(),
|
||||
{ bypassWaitPeriod: session?.user.isSuperAdmin },
|
||||
)
|
||||
const isDownloadWaiting = !videoDownloadAvailability.allowed
|
||||
const isPreviewWaiting = isVideoType && (material.videoPreviewBlocked || isDownloadWaiting)
|
||||
@ -172,7 +175,7 @@ export default function MaterialCard({
|
||||
material.videoPreviewUnavailableMessage ||
|
||||
getVideoDownloadUnavailableMessage(videoDownloadAvailability).replace('下载', '查看详情')
|
||||
const isDownloadDisabled = downloadingId === material.id
|
||||
const isAudioMaterial = material.materialType === '音乐' && !material.isViewOnly
|
||||
const isAudioMaterial = material.materialType === '音乐'
|
||||
|
||||
// 视频预览URL
|
||||
const videoPreviewUrl = isPreviewWaiting ? null : material.videoPreviewClipUrl || material.videoPreview540Url
|
||||
@ -316,7 +319,7 @@ export default function MaterialCard({
|
||||
>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="truncate">{label}</span>
|
||||
{!material.isViewOnly && status === 'failed' && !material.videoPreviewClipUrl && !material.videoPreview540Url && (
|
||||
{status === 'failed' && !material.videoPreviewClipUrl && !material.videoPreview540Url && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
@ -636,7 +639,7 @@ export default function MaterialCard({
|
||||
ID {material.id}
|
||||
</p>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-[13px]">
|
||||
{!material.isViewOnly && <button
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (isDownloadWaiting) {
|
||||
@ -665,7 +668,7 @@ export default function MaterialCard({
|
||||
{downloadingId !== material.id && !isDownloadWaiting && material.downloadCount > 0 && (
|
||||
<span className="font-en text-[11px] tabular-nums text-[#86898e]">{material.downloadCount}</span>
|
||||
)}
|
||||
</button>}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onShare(material.id)}
|
||||
@ -676,7 +679,7 @@ export default function MaterialCard({
|
||||
<ShareGlyph />
|
||||
分享
|
||||
</button>
|
||||
{!material.isViewOnly && <button
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
@ -688,8 +691,8 @@ export default function MaterialCard({
|
||||
title={material.isLiked ? '取消点赞' : '点赞'}
|
||||
>
|
||||
点赞 <span className="font-en text-[12px] tabular-nums tracking-[0.06em]">{material.likeCount}</span>
|
||||
</button>}
|
||||
{!material.isViewOnly && session?.user && (
|
||||
</button>
|
||||
{session?.user && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
@ -705,7 +708,7 @@ export default function MaterialCard({
|
||||
{material.isFavorited ? '已收藏' : '收藏'}
|
||||
</button>
|
||||
)}
|
||||
{!material.isViewOnly && session?.user && (material.userId === session.user.id || session.user.role === 'ADMIN') && (
|
||||
{session?.user && (material.userId === session.user.id || canManageLibrary) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onEdit(material)}
|
||||
@ -715,7 +718,7 @@ export default function MaterialCard({
|
||||
编辑
|
||||
</button>
|
||||
)}
|
||||
{!material.isViewOnly && session?.user && session.user.role === 'ADMIN' && (
|
||||
{canManageLibrary && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
|
||||
@ -1,18 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import React from 'react'
|
||||
import MaterialCard, { Material } from './MaterialCard'
|
||||
|
||||
// 用户会话类型
|
||||
interface UserSession {
|
||||
id: string
|
||||
role: string
|
||||
}
|
||||
import MaterialCard, { type Material, type MaterialCardUser } from './MaterialCard'
|
||||
|
||||
interface MaterialGridProps {
|
||||
materials: Material[]
|
||||
displayedCount: number
|
||||
session: { user: UserSession } | null
|
||||
session: { user: MaterialCardUser } | null
|
||||
loading: boolean
|
||||
isTransitionLoading: boolean
|
||||
// 音频播放状态
|
||||
|
||||
@ -5,13 +5,13 @@
|
||||
| 值 | 界面名称 | 使用规则 |
|
||||
|---|---|---|
|
||||
| `general` | 通用素材 | 沿用原有素材类型、私密与操作权限 |
|
||||
| `operations_g` | 运营中心 G 专属 | 整个运营中心 G 及其下属部门共享,超级管理员可查看 |
|
||||
| `operations_g` | 运营中心 G 专属 | 整个运营中心 G 及其下属部门共享,超级管理员可查看、下载和管理 |
|
||||
|
||||
历史记录默认 `general`。专属素材不设置自动开放期限,也不能叠加个人私密。
|
||||
|
||||
组织资格复用正式账号与人员绑定、当前组织任职路径。运营中心 G 绑定 HR 部门编号 `911666246`,部门改名后读取当前节点路径。账号调出或人员离职后失去成员资格。超级管理员可以在素材库查看个人私密和部门专属素材。下载与维护按素材原有的授权规则判断。
|
||||
组织资格复用正式账号与人员绑定、当前组织任职路径。运营中心 G 绑定 HR 部门编号 `911666246`,部门改名后读取当前节点路径。账号调出或人员离职后失去成员资格。超级管理员可以在素材库跨部门查看、下载和管理素材,包括个人私密与部门专属素材。
|
||||
|
||||
上传与编辑的“使用范围”字段只对运营中心 G 成员显示。其他账号上传时默认使用通用素材。专属素材的预览、图片、视频派生产物使用本地鉴权存储。列表、搜索、统计、详情、分享、日志和回收站使用统一查看规则。下载、导出和编辑按各自的授权规则判断。外部 API 与对外通知仅提供通用且非个人私密的素材,下载令牌和重试回放按当前范围核验。
|
||||
上传与编辑的“使用范围”字段及首页的专属筛选只对运营中心 G 成员显示。其他账号上传时默认使用通用素材。专属素材的预览、图片、视频派生产物使用本地鉴权存储。列表、搜索、详情、分享和回收站按当前账号的素材范围限制访问。外部 API 与对外通知仅提供通用且非个人私密的素材,下载令牌和重试回放按当前范围核验。
|
||||
|
||||
性能监控中的素材任务遵循相同范围。上传会话尚未关联素材时,仅上传者和 G 内管理员可查看文件名。
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
/** @jest-environment node */
|
||||
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { isSuperAdmin } from '@/lib/department-permission'
|
||||
import { getEmployeeByUserId, getEmployeeOrganizationPathsByUserId } from '@/lib/employee-directory'
|
||||
import { canUserAccessPrivateTaskDetail } from '@/lib/user-permission-inherit'
|
||||
import { canAccessOperationsGMaterials, resolveMaterialUploadAccessScope, OPERATIONS_G_SOURCE_DEPT_ID } from '@/lib/material-organization-access'
|
||||
@ -22,6 +23,7 @@ const exclusive = { accessScope: 'operations_g', userId: 'uploader', taskDetailR
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
;(isSuperAdmin as jest.Mock).mockResolvedValue(false)
|
||||
;(prisma.organizationUnit.findUnique as jest.Mock).mockResolvedValue({ path: root, isActive: true })
|
||||
;(getEmployeeByUserId as jest.Mock).mockResolvedValue({ employmentState: 'ACTIVE' })
|
||||
;(getEmployeeOrganizationPathsByUserId as jest.Mock).mockResolvedValue([`${root}/运营七部(HZ)/设计组`])
|
||||
@ -82,6 +84,14 @@ test('匿名访问专属素材被拒绝,通用素材保留原有准入', async
|
||||
expect(await canAccessMaterialPrivacy({ accessScope: 'general' }, null)).toBe(true)
|
||||
})
|
||||
|
||||
test('中心外超级管理员可操作专属素材,上传范围仍按中心成员资格', async () => {
|
||||
;(isSuperAdmin as jest.Mock).mockResolvedValue(true)
|
||||
;(getEmployeeOrganizationPathsByUserId as jest.Mock).mockResolvedValue([])
|
||||
expect(await canAccessMaterialPrivacy(exclusive, 'super-admin')).toBe(true)
|
||||
expect(await buildMaterialPrivacyWhere('super-admin')).toEqual({})
|
||||
await expect(resolveMaterialUploadAccessScope('operations_g', 'super-admin')).rejects.toMatchObject({ status: 403 })
|
||||
})
|
||||
|
||||
test('上传校验拒绝非法范围和中心外专属上传', async () => {
|
||||
await expect(resolveMaterialUploadAccessScope('any-department', 'member')).rejects.toMatchObject({ status: 400 })
|
||||
;(getEmployeeOrganizationPathsByUserId as jest.Mock).mockResolvedValue([])
|
||||
@ -112,7 +122,7 @@ test('中心内可浏览各类型专属素材,通用素材继续遵守原类
|
||||
test('列表、收藏、月份计数的共用过滤不向中心外泄露专属条目', async () => {
|
||||
;(getEmployeeOrganizationPathsByUserId as jest.Mock).mockResolvedValue([])
|
||||
const outside = await buildMaterialPrivacyWhere('other')
|
||||
expect(outside.OR.every((branch: any) => branch.accessScope === 'general')).toBe(true)
|
||||
expect(outside.OR?.every((branch: any) => branch.accessScope === 'general')).toBe(true)
|
||||
;(getEmployeeOrganizationPathsByUserId as jest.Mock).mockResolvedValue([root])
|
||||
expect((await buildMaterialPrivacyWhere('member')).OR).toContainEqual({ accessScope: 'operations_g' })
|
||||
})
|
||||
|
||||
@ -10,7 +10,10 @@ jest.mock('next-auth', () => ({ getServerSession: jest.fn() }))
|
||||
jest.mock('@/lib/auth', () => ({ authOptions: {} }))
|
||||
jest.mock('@/lib/prisma', () => ({ prisma: {} }))
|
||||
jest.mock('@/lib/material-log', () => ({ createMaterialLog: jest.fn() }))
|
||||
jest.mock('@/lib/department-permission', () => ({ isSystemAdmin: (role: string) => ['ADMIN', 'SYSTEM_ADMIN'].includes(role) }))
|
||||
jest.mock('@/lib/department-permission', () => ({
|
||||
isSystemAdmin: (role: string) => ['ADMIN', 'SYSTEM_ADMIN'].includes(role),
|
||||
isSuperAdmin: jest.fn().mockResolvedValue(false),
|
||||
}))
|
||||
jest.mock('@/lib/material-purge', () => ({
|
||||
enqueueMaterialPurge: jest.fn(), ensureMaterialPurgeWorkerStarted: jest.fn(),
|
||||
MaterialPurgeError: class extends Error { constructor(message: string, readonly status = 409) { super(message) } },
|
||||
|
||||
@ -50,8 +50,8 @@ describe('materials download route video wait period', () => {
|
||||
managedDepartment: null,
|
||||
},
|
||||
})
|
||||
;(prisma.user.findUnique as jest.Mock).mockResolvedValue({ isSuperAdmin: true })
|
||||
;(isSuperAdmin as jest.Mock).mockResolvedValue(true)
|
||||
;(prisma.user.findUnique as jest.Mock).mockResolvedValue({ canDownload: true })
|
||||
;(isSuperAdmin as jest.Mock).mockResolvedValue(false)
|
||||
;(canUserDownloadMaterial as jest.Mock).mockResolvedValue(true)
|
||||
;(canUserDownloadSource as jest.Mock).mockResolvedValue(true)
|
||||
})
|
||||
@ -196,6 +196,15 @@ describe('materials download route video wait period', () => {
|
||||
expect(body.details).toContain('满 15 天')
|
||||
})
|
||||
|
||||
test('超级管理员可以跳过视频等待期', async () => {
|
||||
;(isSuperAdmin as jest.Mock).mockResolvedValue(true)
|
||||
mockMaterial('淘系-主图视频', new Date())
|
||||
|
||||
const response = await GET(new NextRequest('http://localhost/api/materials/download?id=material-1&validate=1'))
|
||||
expect(response.status).toBe(404)
|
||||
expect((await response.json()).code).toBe('NO_DOWNLOADABLE_FILES')
|
||||
})
|
||||
|
||||
test('安装视频不受 15 天限制', async () => {
|
||||
mockMaterial('安装视频', new Date())
|
||||
|
||||
|
||||
@ -193,7 +193,7 @@ describe('admin regenerate-pdf-preview route', () => {
|
||||
expect(body.previewUrl).toBe('https://preview.example/new.jpg')
|
||||
expect(getServerSession).not.toHaveBeenCalled()
|
||||
expect(withImageProcessingSlot).toHaveBeenCalledTimes(1)
|
||||
expect(processPdfUpload).toHaveBeenCalledWith(sourcePath, '')
|
||||
expect(processPdfUpload).toHaveBeenCalledWith(sourcePath, '', undefined)
|
||||
expect(prisma.material.update).toHaveBeenCalledWith({
|
||||
where: { id: 'material-1' },
|
||||
data: { previewUrl: 'https://preview.example/new.jpg' },
|
||||
|
||||
@ -36,7 +36,7 @@ jest.mock('@/lib/prisma', () => ({ prisma: {
|
||||
jest.mock('@/lib/user-permission-inherit', () => ({ canUserAccessPrivateTaskDetail: jest.fn(), canUserEditTaskDetailAssets: jest.fn().mockResolvedValue(false) }))
|
||||
jest.mock('@/lib/department-permission', () => ({
|
||||
isSuperAdmin: jest.fn().mockResolvedValue(false), isSystemAdmin: jest.fn().mockReturnValue(false),
|
||||
hasSystemAdminPermission: jest.fn().mockReturnValue(false), canUserViewMaterial: jest.fn().mockResolvedValue(false),
|
||||
hasSystemAdminPermission: jest.fn((_role, isSuperAdmin) => isSuperAdmin === true), canUserViewMaterial: jest.fn().mockResolvedValue(false),
|
||||
canUserDownloadMaterial: jest.fn().mockResolvedValue(false), canUserDownloadSource: jest.fn().mockResolvedValue(false),
|
||||
}))
|
||||
jest.mock('@/lib/download-token', () => ({ isTokenExpired: jest.fn(() => false), verifyDownloadToken: jest.fn(() => ({ materialId: 'private-one', downloadType: 'all' })) }))
|
||||
@ -73,11 +73,11 @@ test('私密详情/编辑详情/分享/下载都先拒绝无特权的上传者
|
||||
test.each([
|
||||
material,
|
||||
{ ...material, taskDetailPrivate: false, accessScope: 'operations_g' },
|
||||
])('超级管理员可查看受限素材的详情和分享页', async (restrictedMaterial) => {
|
||||
;(getServerSession as jest.Mock).mockResolvedValue({ user: { id: 'super-admin', role: 'ADMIN' } })
|
||||
])('超级管理员可以查看、下载和编辑受限素材', async (restrictedMaterial) => {
|
||||
;(getServerSession as jest.Mock).mockResolvedValue({ user: { id: 'super-admin', role: 'USER' } })
|
||||
;(isSuperAdmin as jest.Mock).mockResolvedValue(true)
|
||||
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({ ...restrictedMaterial, zipFileSize: BigInt(0), assets: [] })
|
||||
;(prisma.user.findUnique as jest.Mock).mockResolvedValue({ role: 'ADMIN', isSuperAdmin: true, canDownload: true })
|
||||
;(prisma.user.findUnique as jest.Mock).mockResolvedValue({ role: 'USER', isSuperAdmin: true, canDownload: false })
|
||||
|
||||
const url = 'https://example.com/api/materials'
|
||||
const params = { id: restrictedMaterial.id }
|
||||
@ -86,10 +86,11 @@ test.each([
|
||||
for (const response of [detail, shared]) {
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.headers.get('cache-control')).toContain('no-store')
|
||||
expect(await response.json()).toMatchObject({ id: params.id, canDownloadSource: false })
|
||||
expect(await response.json()).toMatchObject({ id: params.id, canDownloadSource: true })
|
||||
}
|
||||
expect((await getDetail(new NextRequest(`${url}/${params.id}?intent=edit`), { params })).status).toBe(403)
|
||||
expect((await getDownload(new NextRequest(`${url}/download?id=${params.id}`))).status).toBe(403)
|
||||
const edit = await getDetail(new NextRequest(`${url}/${params.id}?intent=edit`), { params })
|
||||
expect(edit.status).toBe(200)
|
||||
expect(await edit.json()).toMatchObject({ canEdit: true, canSelectOperationsGScope: false })
|
||||
})
|
||||
|
||||
test('所选需求运营可看详情,即使没有公开任务详情类型查看权限', async () => {
|
||||
@ -185,16 +186,25 @@ describe('私密预览真实文件入口', () => {
|
||||
expect(await response.text()).toBe('private-image-bytes')
|
||||
})
|
||||
|
||||
test('超级管理员能读取私密预览,源文件仍需私密授权', async () => {
|
||||
;(getServerSession as jest.Mock).mockResolvedValue({ user: { id: 'super-admin', role: 'ADMIN' } })
|
||||
test('超级管理员可读取私密预览并下载源文件', async () => {
|
||||
;(getServerSession as jest.Mock).mockResolvedValue({ user: { id: 'super-admin', role: 'USER' } })
|
||||
;(isSuperAdmin as jest.Mock).mockResolvedValue(true)
|
||||
;(prisma.user.findUnique as jest.Mock).mockResolvedValue({ role: 'USER', isSuperAdmin: true, canDownload: false })
|
||||
const response = await GET(new NextRequest(`https://example.com/api/uploads/${segments.join('/')}`), { params: { path: segments } })
|
||||
expect(response.status).toBe(200)
|
||||
expect(await response.text()).toBe('private-image-bytes')
|
||||
|
||||
;(prisma.material.findMany as jest.Mock).mockResolvedValue([{ ...material, zipFilePath: `uploads/${segments.join('/')}` }])
|
||||
const source = await GET(new NextRequest(`https://example.com/api/uploads/${segments.join('/')}`), { params: { path: segments } })
|
||||
expect(source.status).toBe(403)
|
||||
expect(source.status).toBe(200)
|
||||
expect(await source.text()).toBe('private-image-bytes')
|
||||
|
||||
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
||||
...material, zipFilePath: path.join(root, ...segments), zipFileName: 'fixture.zip', productModel: 'fixture',
|
||||
})
|
||||
const download = await getDownload(new NextRequest('https://example.com/api/materials/download?id=private-one'))
|
||||
expect(download.status).toBe(200)
|
||||
expect(await download.text()).toBe('private-image-bytes')
|
||||
})
|
||||
|
||||
test('未被源文件记录引用的旧原件不能按派生预览放行', async () => {
|
||||
|
||||
@ -4,13 +4,12 @@ import { NextRequest } from 'next/server'
|
||||
import { getServerSession } from 'next-auth'
|
||||
import { POST } from '@/app/api/materials/transcode/route'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { hasSystemAdminPermission } from '@/lib/department-permission'
|
||||
import { hasSystemAdminPermission, isSuperAdmin } from '@/lib/department-permission'
|
||||
import { transcodeQueue } from '@/lib/video-transcode-queue'
|
||||
import { canAccessOperationsGMaterials } from '@/lib/material-organization-access'
|
||||
import { canUseOperationsGMaterials } from '@/lib/material-organization-access'
|
||||
|
||||
jest.mock('@/lib/material-organization-access', () => ({
|
||||
canAccessOperationsGMaterials: jest.fn(),
|
||||
canViewOperationsGMaterials: jest.fn(),
|
||||
canUseOperationsGMaterials: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('next-auth', () => ({
|
||||
@ -34,6 +33,7 @@ jest.mock('@/lib/prisma', () => ({
|
||||
jest.mock('@/lib/department-permission', () => ({
|
||||
canUserViewMaterial: jest.fn(),
|
||||
hasSystemAdminPermission: jest.fn(),
|
||||
isSuperAdmin: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock('@/lib/video-transcode-task', () => ({
|
||||
@ -67,7 +67,8 @@ function createRequest() {
|
||||
describe('transcode route authorization', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
;(canAccessOperationsGMaterials as jest.Mock).mockResolvedValue(true)
|
||||
;(canUseOperationsGMaterials as jest.Mock).mockResolvedValue(true)
|
||||
;(isSuperAdmin as jest.Mock).mockResolvedValue(false)
|
||||
;(transcodeQueue.add as jest.Mock).mockResolvedValue(undefined)
|
||||
;(getServerSession as jest.Mock).mockResolvedValue({
|
||||
user: { id: 'owner-1', role: 'USER', isSuperAdmin: false },
|
||||
@ -163,7 +164,7 @@ describe('transcode route authorization', () => {
|
||||
test('调出运营中心 G 的上传者不能重试专属视频,管理员也不能越过组织范围', async () => {
|
||||
;(getServerSession as jest.Mock).mockResolvedValue({ user: { id: 'moved-owner', role: 'ADMIN' } })
|
||||
;(hasSystemAdminPermission as jest.Mock).mockReturnValue(true)
|
||||
;(canAccessOperationsGMaterials as jest.Mock).mockResolvedValue(false)
|
||||
;(canUseOperationsGMaterials as jest.Mock).mockResolvedValue(false)
|
||||
;(prisma.material.findFirst as jest.Mock).mockResolvedValue({
|
||||
id: 'material-1', userId: 'moved-owner', accessScope: 'operations_g',
|
||||
materialType: '淘系-主图视频', videoSourceUrl: 'uploads/video.mp4', videoProcessingStatus: 'failed',
|
||||
@ -175,7 +176,7 @@ describe('transcode route authorization', () => {
|
||||
test('中心外管理员的转码队列只返回通用素材', async () => {
|
||||
;(getServerSession as jest.Mock).mockResolvedValue({ user: { id: 'outside-admin', role: 'ADMIN' } })
|
||||
;(hasSystemAdminPermission as jest.Mock).mockReturnValue(true)
|
||||
;(canAccessOperationsGMaterials as jest.Mock).mockResolvedValue(false)
|
||||
;(canUseOperationsGMaterials as jest.Mock).mockResolvedValue(false)
|
||||
;(prisma.material.findMany as jest.Mock).mockResolvedValue([{ id: 'material-1' }])
|
||||
;(transcodeQueue.getStatus as jest.Mock).mockReturnValueOnce({
|
||||
waiting: ['g-video'], processingIds: ['material-1'], maxConcurrent: 1,
|
||||
|
||||
@ -54,7 +54,7 @@ describe('video download policy integration', () => {
|
||||
|
||||
// 列表必须应用策略,且豁免标记来自当前登录用户(站内豁免,外部接口不传)。
|
||||
expect(content).toContain('applyVideoPreviewPolicy(rewrittenMaterial')
|
||||
expect(content).toContain('bypassWaitPeriod: userHasVideoEarlyAccess(')
|
||||
expect(content).toContain('bypassWaitPeriod: context.isSuperAdmin || userHasVideoEarlyAccess(')
|
||||
})
|
||||
|
||||
test('本地上传文件代理应拦截等待期内的视频预览文件', () => {
|
||||
|
||||
@ -73,8 +73,7 @@ export async function canManageMaterial(
|
||||
if (!(await canAccessMaterialPrivacy(privacyMaterial, user.id))) return false
|
||||
if (
|
||||
material.userId === user.id ||
|
||||
isSystemAdmin(user.role) ||
|
||||
user.isSuperAdmin === true
|
||||
isSystemAdmin(user.role)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import { isSuperAdmin } from '@/lib/department-permission'
|
||||
// HR 组织树中运营中心 G 的稳定部门编号。
|
||||
export const OPERATIONS_G_SOURCE_DEPT_ID = 911666246
|
||||
|
||||
/** G 成员资格用于显示筛选和设置素材使用范围。 */
|
||||
export async function canAccessOperationsGMaterials(userId?: string | null): Promise<boolean> {
|
||||
if (!userId) return false
|
||||
const [employee, paths, organization] = await Promise.all([
|
||||
@ -45,16 +46,12 @@ export async function resolveMaterialUploadAccessScope(
|
||||
return scope
|
||||
}
|
||||
|
||||
export async function buildMaterialOrganizationWhere(userId?: string | null) {
|
||||
return await canAccessOperationsGMaterials(userId) ? {} : { accessScope: 'general' }
|
||||
}
|
||||
|
||||
export async function canViewOperationsGMaterials(userId?: string | null): Promise<boolean> {
|
||||
export async function canUseOperationsGMaterials(userId?: string | null): Promise<boolean> {
|
||||
return await isSuperAdmin(userId) || canAccessOperationsGMaterials(userId)
|
||||
}
|
||||
|
||||
export async function buildMaterialOrganizationViewWhere(userId?: string | null) {
|
||||
return await canViewOperationsGMaterials(userId) ? {} : { accessScope: 'general' }
|
||||
export async function buildMaterialOrganizationWhere(userId?: string | null) {
|
||||
return await canUseOperationsGMaterials(userId) ? {} : { accessScope: 'general' }
|
||||
}
|
||||
|
||||
export async function getOperationsGMaterialIds(): Promise<string[]> {
|
||||
|
||||
@ -2,7 +2,8 @@ import { invalidateMaterialFacetsCache } from '@/lib/material-facets-cache'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { Prisma, type MaterialPurgeJob } from '@prisma/client'
|
||||
import { prisma } from '@/lib/prisma'
|
||||
import { canAccessOperationsGMaterials } from '@/lib/material-organization-access'
|
||||
import { isRestrictedMaterial } from '@/lib/material-access-scope'
|
||||
import { buildMaterialPrivacyWhere, canAccessMaterialPrivacy, type TaskDetailPrivacyMaterial } from '@/lib/task-detail-privacy'
|
||||
import { collectPurgeFiles, findPurgeFileReferences, purgeFileIdentity, removePurgeFile, type PurgeFile } from '@/lib/material-purge-files'
|
||||
|
||||
export class MaterialPurgeError extends Error {
|
||||
@ -46,9 +47,11 @@ export async function enqueueMaterialPurge(input: unknown, operatorUserId: strin
|
||||
await lock(tx, 'product-package-upload-worker')
|
||||
await tx.$queryRaw`SELECT 1 AS locked FROM pg_advisory_xact_lock(193582731)`
|
||||
const jobs = await tx.materialPurgeJob.findMany({ where: { materialId: { in: ids } } })
|
||||
if (jobs.some((job) => (job.materialSnapshot as { accessScope?: string }).accessScope === 'operations_g')
|
||||
&& !(await canAccessOperationsGMaterials(operatorUserId))) {
|
||||
throw new MaterialPurgeError('无权限操作运营中心 G 专属素材', 403)
|
||||
const canAccessJobs = await Promise.all(jobs.map((job) =>
|
||||
canAccessMaterialPrivacy(job.materialSnapshot as TaskDetailPrivacyMaterial, operatorUserId),
|
||||
))
|
||||
if (canAccessJobs.includes(false)) {
|
||||
throw new MaterialPurgeError('无权限操作部分素材', 403)
|
||||
}
|
||||
const newIds = ids.filter(id => !jobs.some(job => job.materialId === id))
|
||||
if (!newIds.length) return jobs.map(toPurgeJobView)
|
||||
@ -57,9 +60,11 @@ export async function enqueueMaterialPurge(input: unknown, operatorUserId: strin
|
||||
where: { id: { in: newIds } }, include: { assets: true, skus: true },
|
||||
})
|
||||
if (materials.length !== newIds.length) throw new MaterialPurgeError('部分素材已不存在,请刷新后重试', 404)
|
||||
if (materials.some((material) => material.accessScope === 'operations_g')
|
||||
&& !(await canAccessOperationsGMaterials(operatorUserId))) {
|
||||
throw new MaterialPurgeError('无权限操作运营中心 G 专属素材', 403)
|
||||
if (materials.some(isRestrictedMaterial)) {
|
||||
const privacyWhere = await buildMaterialPrivacyWhere(operatorUserId)
|
||||
if (privacyWhere.OR && await tx.material.count({ where: { id: { in: newIds }, ...privacyWhere } }) !== materials.length) {
|
||||
throw new MaterialPurgeError('无权限操作部分素材', 403)
|
||||
}
|
||||
}
|
||||
if (materials.some(material => !material.deletedAt)) throw new MaterialPurgeError('只能彻底删除回收站中的素材')
|
||||
const uploading = await tx.productPackageUploadJob.count({ where: { materialId: { in: newIds }, status: { in: ['queued', 'processing'] } } })
|
||||
@ -74,6 +79,8 @@ export async function enqueueMaterialPurge(input: unknown, operatorUserId: strin
|
||||
}
|
||||
const snapshot = {
|
||||
accessScope: material.accessScope || 'general',
|
||||
taskDetailPrivate: material.taskDetailPrivate,
|
||||
taskDetailRequesterUserId: material.taskDetailRequesterUserId,
|
||||
id: material.id, productModel: material.productModel, materialType: material.materialType,
|
||||
taskNo: material.taskNo, ownerUserId: material.userId, deletedAt: material.deletedAt!.toISOString(),
|
||||
}
|
||||
@ -186,9 +193,8 @@ export async function processNextMaterialPurge(): Promise<boolean> {
|
||||
export async function retryMaterialPurge(jobId: string, operatorUserId: string) {
|
||||
if (typeof jobId !== 'string' || !UUID.test(jobId)) throw new MaterialPurgeError('清理任务无效', 400)
|
||||
const job = await prisma.materialPurgeJob.findUnique({ where: { id: jobId } })
|
||||
if ((job?.materialSnapshot as { accessScope?: string } | undefined)?.accessScope === 'operations_g'
|
||||
&& !(await canAccessOperationsGMaterials(operatorUserId))) {
|
||||
throw new MaterialPurgeError('无权限操作运营中心 G 专属素材', 403)
|
||||
if (job && !(await canAccessMaterialPrivacy(job.materialSnapshot as TaskDetailPrivacyMaterial, operatorUserId))) {
|
||||
throw new MaterialPurgeError('无权限操作此素材', 403)
|
||||
}
|
||||
return prisma.$transaction(async tx => {
|
||||
const result = await tx.materialPurgeJob.updateMany({ where: { id: jobId, status: 'failed' }, data: {
|
||||
|
||||
@ -21,26 +21,19 @@ export function isPrivateTaskDetail(material: TaskDetailPrivacyMaterial): boolea
|
||||
return material.taskDetailPrivate === true
|
||||
}
|
||||
|
||||
/** 下载和维护仍按素材自身的私密权限和部门范围准入。 */
|
||||
/** 登录账号的素材范围准入。外部接口独立使用公开范围条件。 */
|
||||
export async function canAccessMaterialPrivacy(
|
||||
material: TaskDetailPrivacyMaterial,
|
||||
userId?: string | null,
|
||||
): Promise<boolean> {
|
||||
if (isOperationsGMaterial(material)) return canAccessOperationsGMaterials(userId)
|
||||
if (!isPrivateTaskDetail(material)) return true
|
||||
if (!isRestrictedMaterial(material)) return true
|
||||
if (!userId) return false
|
||||
if (await isSuperAdmin(userId)) return true
|
||||
if (isOperationsGMaterial(material)) return canAccessOperationsGMaterials(userId)
|
||||
if (material.taskDetailRequesterUserId === userId) return true
|
||||
return canUserAccessPrivateTaskDetail(userId)
|
||||
}
|
||||
|
||||
export async function canViewMaterialPrivacy(
|
||||
material: TaskDetailPrivacyMaterial,
|
||||
userId?: string | null,
|
||||
): Promise<boolean> {
|
||||
if (isRestrictedMaterial(material) && await isSuperAdmin(userId)) return true
|
||||
return canAccessMaterialPrivacy(material, userId)
|
||||
}
|
||||
|
||||
export function privateTaskDetailAccessWhere(userId: string, canAccessAll: boolean) {
|
||||
return {
|
||||
materialType: '任务-详情',
|
||||
@ -51,6 +44,8 @@ export function privateTaskDetailAccessWhere(userId: string, canAccessAll: boole
|
||||
}
|
||||
|
||||
export async function buildMaterialPrivacyWhere(userId: string) {
|
||||
if (await isSuperAdmin(userId)) return {}
|
||||
|
||||
const [canAccessPrivate, canAccessOperationsG] = await Promise.all([
|
||||
canUserAccessPrivateTaskDetail(userId),
|
||||
canAccessOperationsGMaterials(userId),
|
||||
@ -61,7 +56,3 @@ export async function buildMaterialPrivacyWhere(userId: string) {
|
||||
...(canAccessOperationsG ? [{ accessScope: 'operations_g' }] : []),
|
||||
] }
|
||||
}
|
||||
|
||||
export async function buildMaterialViewWhere(userId: string) {
|
||||
return await isSuperAdmin(userId) ? {} : buildMaterialPrivacyWhere(userId)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user