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 })
}
}