354 lines
13 KiB
TypeScript
354 lines
13 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server'
|
|
import { Prisma } from '@prisma/client'
|
|
import { getServerSession } from 'next-auth'
|
|
import { authOptions } from '@/lib/auth'
|
|
import { prisma } from '@/lib/prisma'
|
|
import { getAllMaterialTypes, isSuperAdmin } from '@/lib/department-permission'
|
|
import { filterSelectableMaterialTypes } from '@/lib/material-type-options'
|
|
import { BINDING_STATUS, PERMISSION_PATH_PROFILE_SELECT } from '@/lib/employee-directory'
|
|
import { selectVisibleOrganizationPeople } from '@/lib/auto-personnel-rules'
|
|
import {
|
|
collectOccupiedOrganizationPaths,
|
|
lastOrganizationPathSegment,
|
|
normalizeTaskDetailOnlyFlags,
|
|
pruneOrganizationTree,
|
|
resolveEmployeeOrganizationPaths,
|
|
} from '@/lib/organization-material-permissions'
|
|
import {
|
|
findConflictingDepartmentGroup,
|
|
makePermissionGroupSlug,
|
|
loadPermissionGroupDepartments,
|
|
} from '@/lib/permission-groups'
|
|
import {
|
|
packItemsToMap,
|
|
savePermissionPackItems,
|
|
} from '@/lib/user-permission-inherit'
|
|
|
|
type OrgNode = {
|
|
id: string
|
|
name: string
|
|
path: string
|
|
level: number
|
|
parentId: string | null
|
|
children: OrgNode[]
|
|
}
|
|
|
|
async function requireSuperAdmin() {
|
|
const session = await getServerSession(authOptions)
|
|
const userId = (session?.user as { id?: string } | undefined)?.id
|
|
if (!session || !(await isSuperAdmin(userId))) return null
|
|
return session
|
|
}
|
|
|
|
async function requireDingTalkUser(userId: string) {
|
|
const user = await prisma.user.findUnique({
|
|
where: { id: userId },
|
|
select: { id: true, dingtalkUserId: true },
|
|
})
|
|
if (!user) return { error: '没有这个账号', status: 400 as const }
|
|
if (!user.dingtalkUserId) {
|
|
return { error: '这个账号没有钉钉绑定,不能进权限组', status: 400 as const }
|
|
}
|
|
return { user }
|
|
}
|
|
|
|
export async function GET() {
|
|
try {
|
|
if (!(await requireSuperAdmin())) {
|
|
return NextResponse.json({ error: '需要超级管理员权限' }, { status: 403 })
|
|
}
|
|
|
|
const [packs, units, rosterProfiles, materialTypes, departmentRows] = await Promise.all([
|
|
prisma.materialPermissionPack.findMany({
|
|
orderBy: { name: 'asc' },
|
|
select: {
|
|
id: true,
|
|
slug: true,
|
|
name: true,
|
|
videoEarlyAccess: true,
|
|
items: {
|
|
select: {
|
|
materialType: true,
|
|
canView: true,
|
|
canDownload: true,
|
|
canDownloadSource: true,
|
|
canShare: true,
|
|
canEditAssets: true,
|
|
canAccessPrivate: true,
|
|
},
|
|
},
|
|
inheritors: {
|
|
orderBy: { updatedAt: 'desc' },
|
|
select: {
|
|
userId: true,
|
|
user: {
|
|
select: {
|
|
username: true,
|
|
realName: true,
|
|
employeeBindings: {
|
|
where: { status: BINDING_STATUS.ACTIVE },
|
|
take: 1,
|
|
select: {
|
|
employeeProfile: {
|
|
select: { ...PERMISSION_PATH_PROFILE_SELECT, displayName: true },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
prisma.organizationUnit.findMany({
|
|
where: { isActive: true },
|
|
select: { id: true, name: true, path: true, level: true, parentId: true, sourceDeptId: true },
|
|
orderBy: [{ level: 'asc' }, { name: 'asc' }],
|
|
}),
|
|
prisma.performanceSurveyEmployeeProfile.findMany({
|
|
select: {
|
|
departmentPath: true,
|
|
employmentState: true,
|
|
sourcePayload: true,
|
|
departmentMemberships: {
|
|
select: { sourceDeptId: true, departmentPath: true },
|
|
},
|
|
},
|
|
}),
|
|
getAllMaterialTypes().then(filterSelectableMaterialTypes),
|
|
loadPermissionGroupDepartments(),
|
|
])
|
|
|
|
const nodeMap = new Map<string, OrgNode>()
|
|
for (const unit of units) {
|
|
nodeMap.set(unit.id, {
|
|
id: unit.id,
|
|
name: unit.name,
|
|
path: unit.path,
|
|
level: unit.level,
|
|
parentId: unit.parentId,
|
|
children: [],
|
|
})
|
|
}
|
|
const roots: OrgNode[] = []
|
|
for (const node of Array.from(nodeMap.values())) {
|
|
if (node.parentId && nodeMap.has(node.parentId)) {
|
|
nodeMap.get(node.parentId)!.children.push(node)
|
|
} else {
|
|
roots.push(node)
|
|
}
|
|
}
|
|
const pathBySourceDeptId = new Map(units.map((unit) => [String(unit.sourceDeptId), unit.path]))
|
|
const occupiedPaths = collectOccupiedOrganizationPaths([
|
|
...selectVisibleOrganizationPeople(rosterProfiles).flatMap((profile) =>
|
|
resolveEmployeeOrganizationPaths(profile, pathBySourceDeptId),
|
|
),
|
|
...departmentRows.flatMap((row) => row.organizationUnit?.isActive ? [row.organizationUnit.path] : []),
|
|
])
|
|
const tree = pruneOrganizationTree(roots, occupiedPaths)
|
|
const departmentsByPack = new Map<string, typeof departmentRows>()
|
|
for (const row of departmentRows) {
|
|
const list = departmentsByPack.get(row.packId) || []
|
|
list.push(row)
|
|
departmentsByPack.set(row.packId, list)
|
|
}
|
|
|
|
return NextResponse.json({
|
|
materialTypes,
|
|
tree,
|
|
groups: packs.map((pack) => ({
|
|
slug: pack.slug,
|
|
name: pack.name,
|
|
videoEarlyAccess: pack.videoEarlyAccess,
|
|
permissions: packItemsToMap(pack.items),
|
|
departments: (departmentsByPack.get(pack.id) || []).map((row) => ({
|
|
id: row.id,
|
|
organizationUnitId: row.organizationUnitId,
|
|
path: row.organizationUnit?.path || row.departmentPath,
|
|
name: row.organizationUnit?.name || lastOrganizationPathSegment(row.departmentPath),
|
|
status: row.organizationUnit?.isActive ? 'active' : 'invalid',
|
|
})),
|
|
members: pack.inheritors.map((row) => {
|
|
const profile = row.user.employeeBindings[0]?.employeeProfile
|
|
const currentPath = resolveEmployeeOrganizationPaths(profile, pathBySourceDeptId)[0] || null
|
|
return {
|
|
userId: row.userId,
|
|
username: row.user.username,
|
|
displayName: profile?.displayName || row.user.realName || row.user.username,
|
|
departmentPath: currentPath,
|
|
departmentName: currentPath ? lastOrganizationPathSegment(currentPath) : null,
|
|
}
|
|
}),
|
|
})),
|
|
})
|
|
} catch (error) {
|
|
console.error('[permission-groups] GET failed', error)
|
|
return NextResponse.json({ error: '获取权限组失败' }, { status: 500 })
|
|
}
|
|
}
|
|
|
|
export async function PUT(request: NextRequest) {
|
|
try {
|
|
if (!(await requireSuperAdmin())) {
|
|
return NextResponse.json({ error: '需要超级管理员权限' }, { status: 403 })
|
|
}
|
|
|
|
const body = await request.json()
|
|
const action = String(body?.action || '').trim()
|
|
|
|
if (action === 'create') {
|
|
const name = String(body?.name || '').trim()
|
|
if (!name) return NextResponse.json({ error: '请填写组名' }, { status: 400 })
|
|
const slug = makePermissionGroupSlug(name)
|
|
await prisma.materialPermissionPack.create({
|
|
data: { slug, name },
|
|
})
|
|
return NextResponse.json({ ok: true, slug })
|
|
}
|
|
|
|
const slug = String(body?.slug || '').trim()
|
|
if (!slug) return NextResponse.json({ error: '缺少权限组' }, { status: 400 })
|
|
|
|
const pack = await prisma.materialPermissionPack.findUnique({
|
|
where: { slug },
|
|
select: { id: true, slug: true, name: true },
|
|
})
|
|
if (!pack) return NextResponse.json({ error: '没有这个权限组' }, { status: 400 })
|
|
|
|
if (action === 'rename') {
|
|
const name = String(body?.name || '').trim()
|
|
if (!name) return NextResponse.json({ error: '请填写组名' }, { status: 400 })
|
|
await prisma.materialPermissionPack.update({
|
|
where: { id: pack.id },
|
|
data: { name },
|
|
})
|
|
return NextResponse.json({ ok: true })
|
|
}
|
|
|
|
if (action === 'set-video-early-access') {
|
|
const enabled = body?.enabled === true
|
|
await prisma.materialPermissionPack.update({
|
|
where: { id: pack.id },
|
|
data: { videoEarlyAccess: enabled },
|
|
})
|
|
return NextResponse.json({ ok: true, videoEarlyAccess: enabled })
|
|
}
|
|
|
|
if (action === 'delete') {
|
|
await prisma.$transaction([
|
|
prisma.userPermissionInherit.deleteMany({ where: { packId: pack.id } }),
|
|
prisma.materialPermissionPack.delete({ where: { id: pack.id } }),
|
|
])
|
|
return NextResponse.json({ ok: true })
|
|
}
|
|
|
|
if (action === 'save-pack') {
|
|
const incoming: Array<{
|
|
materialType: string
|
|
canView?: boolean
|
|
canDownload?: boolean
|
|
canDownloadSource?: boolean
|
|
canShare?: boolean
|
|
canEditAssets?: boolean
|
|
canAccessPrivate?: boolean
|
|
}> = Array.isArray(body?.permissions) ? body.permissions : []
|
|
if (incoming.length === 0) {
|
|
return NextResponse.json({ error: '素材类型列表不能为空' }, { status: 400 })
|
|
}
|
|
const permissions = incoming.filter((item) => item.materialType).map((item) => {
|
|
const taskDetailOnlyFlags = normalizeTaskDetailOnlyFlags(item.materialType, item)
|
|
return {
|
|
materialType: item.materialType,
|
|
canView: Boolean(item.canView),
|
|
canDownload: Boolean(item.canDownload),
|
|
canDownloadSource: Boolean(item.canDownloadSource),
|
|
canShare: true,
|
|
canEditAssets: taskDetailOnlyFlags.canEditAssets,
|
|
canAccessPrivate: taskDetailOnlyFlags.canAccessPrivate,
|
|
}
|
|
})
|
|
await savePermissionPackItems(slug, permissions)
|
|
// 在事务提交后返回实际标准化的权限;客户端无需再加载整棵组织树才能确认保存。
|
|
return NextResponse.json({ ok: true, permissions: packItemsToMap(permissions) })
|
|
}
|
|
|
|
if (action === 'add-department' || action === 'remove-department') {
|
|
const organizationUnitId = String(body?.organizationUnitId || '').trim()
|
|
if (action === 'remove-department') {
|
|
const associationId = String(body?.associationId || '').trim()
|
|
if (!associationId && !organizationUnitId) return NextResponse.json({ error: '请刷新列表后选择部门' }, { status: 400 })
|
|
await prisma.materialPermissionPackDepartment.deleteMany({
|
|
where: { packId: pack.id, ...(associationId ? { id: associationId } : { organizationUnitId }) },
|
|
})
|
|
return NextResponse.json({ ok: true })
|
|
}
|
|
if (!organizationUnitId) return NextResponse.json({ error: '请刷新列表后选择部门' }, { status: 400 })
|
|
const unit = await prisma.organizationUnit.findFirst({
|
|
where: { isActive: true, id: organizationUnitId },
|
|
select: { path: true },
|
|
})
|
|
if (!unit) return NextResponse.json({ error: '花名册里没有这个部门' }, { status: 400 })
|
|
const path = unit.path
|
|
const conflict = await findConflictingDepartmentGroup(organizationUnitId, pack.id)
|
|
if (conflict) {
|
|
const name = lastOrganizationPathSegment(path) || path
|
|
return NextResponse.json(
|
|
{ error: `「${name}」已经在权限组「${conflict.name}」里,一个部门只能进一组。先从那组拿掉,再加到这里。` },
|
|
{ status: 409 },
|
|
)
|
|
}
|
|
|
|
try {
|
|
const association = await prisma.materialPermissionPackDepartment.upsert({
|
|
where: { organizationUnitId },
|
|
update: {},
|
|
create: { packId: pack.id, organizationUnitId, departmentPath: path },
|
|
select: { packId: true },
|
|
})
|
|
if (association.packId !== pack.id) {
|
|
return NextResponse.json({ error: '这个部门已在别的权限组里,请刷新列表。' }, { status: 409 })
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
|
|
const name = lastOrganizationPathSegment(path) || path
|
|
return NextResponse.json(
|
|
{ error: `「${name}」已经在别的权限组里,一个部门只能进一组。` },
|
|
{ status: 409 },
|
|
)
|
|
}
|
|
throw error
|
|
}
|
|
return NextResponse.json({ ok: true })
|
|
}
|
|
|
|
if (action === 'add-member' || action === 'remove-member') {
|
|
const userId = String(body?.userId || '').trim()
|
|
if (!userId) return NextResponse.json({ error: '请选择人员' }, { status: 400 })
|
|
|
|
if (action === 'remove-member') {
|
|
await prisma.userPermissionInherit.deleteMany({
|
|
where: { userId, packId: pack.id },
|
|
})
|
|
return NextResponse.json({ ok: true })
|
|
}
|
|
|
|
const checked = await requireDingTalkUser(userId)
|
|
if ('error' in checked) {
|
|
return NextResponse.json({ error: checked.error }, { status: checked.status })
|
|
}
|
|
|
|
await prisma.userPermissionInherit.upsert({
|
|
where: { userId_packId: { userId, packId: pack.id } },
|
|
update: {},
|
|
create: { userId, packId: pack.id },
|
|
})
|
|
return NextResponse.json({ ok: true })
|
|
}
|
|
|
|
return NextResponse.json({ error: '不认识这个操作' }, { status: 400 })
|
|
} catch (error) {
|
|
console.error('[permission-groups] PUT failed', error)
|
|
return NextResponse.json({ error: '保存权限组失败' }, { status: 500 })
|
|
}
|
|
}
|