视频提前访问改为权限组级能力,人进组即获得,看板直接开关
This commit is contained in:
parent
d374a0bba2
commit
d6a41d9283
@ -86,7 +86,7 @@ RUN test -s "/app/public/brand/header-logo.png" && \
|
||||
test -f "/app/prisma/migrations/20260824_expand_ding_workflow_insights/migration.sql" && \
|
||||
test -f "/app/prisma/migrations/20260826_add_ding_workflow_demand_type/migration.sql" && \
|
||||
test -f "/app/prisma/migrations/20260827_add_ding_workflow_detail_fields/migration.sql" && \
|
||||
test -f "/app/prisma/migrations/20260826_add_user_video_early_access/migration.sql"
|
||||
test -f "/app/prisma/migrations/20260826_add_pack_video_early_access/migration.sql"
|
||||
|
||||
# 这两个动态路由负责兜底访问本地上传文件;如果它们在构建上下文里丢失,
|
||||
# 线上音频/视频就会直接 404。这里提前失败,避免再把缺路由的镜像发出去。
|
||||
@ -205,7 +205,7 @@ COPY --from=builder --chown=root:root /app/prisma/migrations/20260820_add_ding_w
|
||||
COPY --from=builder --chown=root:root /app/prisma/migrations/20260824_expand_ding_workflow_insights ./prisma/migrations/20260824_expand_ding_workflow_insights
|
||||
COPY --from=builder --chown=root:root /app/prisma/migrations/20260826_add_ding_workflow_demand_type ./prisma/migrations/20260826_add_ding_workflow_demand_type
|
||||
COPY --from=builder --chown=root:root /app/prisma/migrations/20260827_add_ding_workflow_detail_fields ./prisma/migrations/20260827_add_ding_workflow_detail_fields
|
||||
COPY --from=builder --chown=root:root /app/prisma/migrations/20260826_add_user_video_early_access ./prisma/migrations/20260826_add_user_video_early_access
|
||||
COPY --from=builder --chown=root:root /app/prisma/migrations/20260826_add_pack_video_early_access ./prisma/migrations/20260826_add_pack_video_early_access
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/lib ./lib
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/scripts/public-traffic-rollup-from-nginx.py ./scripts/public-traffic-rollup-from-nginx.py
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/scripts/collapse-performance-survey-template-versions.mjs ./scripts/collapse-performance-survey-template-versions.mjs
|
||||
|
||||
@ -22,7 +22,6 @@ export async function GET() {
|
||||
isActive: true, // 保留,向后兼容
|
||||
status: true, // 新增:用户状态(阶段一)
|
||||
canDownload: true,
|
||||
videoEarlyAccess: true, // 视频提前访问豁免(仅超管可开关)
|
||||
department: true, // 保留,向后兼容
|
||||
departmentId: true, // 新增:部门ID(阶段二)
|
||||
managedDepartment: true, // 管理员负责的部门
|
||||
|
||||
@ -1,64 +0,0 @@
|
||||
// 切换用户「视频提前访问」豁免API(跳过视频类素材 15 天保护期)
|
||||
// 仅超级管理员可操作,与"设为管理员"同级的高敏感授权。
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
import { authOptions } from '@/lib/auth';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
|
||||
// POST - 切换视频提前访问豁免(仅超级管理员)
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user || !(session.user as any).id) {
|
||||
return NextResponse.json(
|
||||
{ error: '未登录' },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!(session.user as any).isSuperAdmin) {
|
||||
return NextResponse.json(
|
||||
{ error: '仅超级管理员可操作' },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const { userId } = await request.json();
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json(
|
||||
{ error: '缺少用户ID' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: '用户不存在' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: { videoEarlyAccess: !user.videoEarlyAccess },
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
message: updatedUser.videoEarlyAccess
|
||||
? '已开启视频提前访问(跳过 15 天保护期)'
|
||||
: '已关闭视频提前访问(恢复 15 天保护期)',
|
||||
videoEarlyAccess: updatedUser.videoEarlyAccess,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error toggling video early access:', error);
|
||||
return NextResponse.json(
|
||||
{ error: '操作失败' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -69,6 +69,7 @@ export async function GET() {
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
videoEarlyAccess: true,
|
||||
items: {
|
||||
select: {
|
||||
materialType: true,
|
||||
@ -170,6 +171,7 @@ export async function GET() {
|
||||
groups: packs.map((pack) => ({
|
||||
slug: pack.slug,
|
||||
name: pack.name,
|
||||
videoEarlyAccess: pack.videoEarlyAccess,
|
||||
permissions: packItemsToMap(pack.items),
|
||||
departments: (departmentsByPack.get(pack.id) || []).map((path) => ({
|
||||
path,
|
||||
@ -232,6 +234,15 @@ export async function PUT(request: NextRequest) {
|
||||
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 } }),
|
||||
|
||||
@ -30,6 +30,7 @@ type Member = {
|
||||
type Group = {
|
||||
slug: string
|
||||
name: string
|
||||
videoEarlyAccess?: boolean
|
||||
permissions: Record<string, MaterialPermissionFlags>
|
||||
departments: Array<{ path: string; name: string }>
|
||||
members: Member[]
|
||||
@ -464,6 +465,35 @@ export default function PermissionGroupsDashboard() {
|
||||
部门进组是底子,人进组是加餐。没单独进组的下级,跟着最近的上级组走。
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 rounded-lg border border-[#e2e8f0] bg-[#f8fafc] px-4 py-2.5">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-semibold text-[#101828]">视频提前访问</p>
|
||||
<p className="mt-0.5 text-xs text-[#66758b]">
|
||||
开启后,本组成员不受视频类素材 15 天保护期限制
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={Boolean(selected.videoEarlyAccess)}
|
||||
disabled={saving}
|
||||
onClick={() =>
|
||||
void mutate(
|
||||
{ action: 'set-video-early-access', slug: selected.slug, enabled: !selected.videoEarlyAccess },
|
||||
selected.videoEarlyAccess ? '已关闭视频提前访问' : '已开启视频提前访问',
|
||||
)
|
||||
}
|
||||
className={`relative h-6 w-11 flex-shrink-0 rounded-full transition-colors ${
|
||||
selected.videoEarlyAccess ? 'bg-[#1677ff]' : 'bg-[#cfd8e3]'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-0.5 h-5 w-5 rounded-full bg-white shadow transition-all ${
|
||||
selected.videoEarlyAccess ? 'left-[22px]' : 'left-0.5'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@ -5,6 +5,7 @@ import bcrypt from 'bcryptjs'
|
||||
import { resolveSecureAuthCookieForCurrentRequest } from '@/lib/auth-cookie'
|
||||
import { authorizeDingtalkH5UserForNextAuth } from '@/lib/dingtalk-login'
|
||||
import { resolveBrandCenterAccessForUser } from '@/lib/brand-center-access'
|
||||
import { resolveUserVideoEarlyAccess } from '@/lib/user-permission-inherit'
|
||||
|
||||
const LEGACY_DINGTALK_AUTO_LOGIN_PASSWORD = 'dingtalk_auto_login'
|
||||
|
||||
@ -154,7 +155,7 @@ export const authOptions: NextAuthOptions = {
|
||||
console.error('读取品牌中心访问范围失败,已按无权限处理:', error)
|
||||
return false
|
||||
})
|
||||
token.videoEarlyAccess = (user as any).videoEarlyAccess === true
|
||||
token.videoEarlyAccess = await resolveUserVideoEarlyAccess(user.id).catch(() => false)
|
||||
} else if (token.id) {
|
||||
const lastCheckedAt = Number((token as any).authCheckedAt || 0)
|
||||
const accessClaimMissing = typeof (token as any).canAccessBrandCenter !== 'boolean'
|
||||
@ -169,7 +170,6 @@ export const authOptions: NextAuthOptions = {
|
||||
department: true,
|
||||
managedDepartment: true,
|
||||
isSuperAdmin: true,
|
||||
videoEarlyAccess: true,
|
||||
},
|
||||
})
|
||||
|
||||
@ -184,7 +184,7 @@ export const authOptions: NextAuthOptions = {
|
||||
token.isSuperAdmin = currentUser.isSuperAdmin
|
||||
token.department = currentUser.department || ''
|
||||
token.managedDepartment = currentUser.managedDepartment || null
|
||||
token.videoEarlyAccess = currentUser.videoEarlyAccess === true
|
||||
token.videoEarlyAccess = await resolveUserVideoEarlyAccess(String(token.id)).catch(() => false)
|
||||
token.canAccessBrandCenter = await resolveBrandCenterAccessForUser(String(token.id), {
|
||||
role: currentUser.role,
|
||||
isSuperAdmin: currentUser.isSuperAdmin,
|
||||
|
||||
@ -102,6 +102,24 @@ export async function loadUserInheritPack(userId: string): Promise<PermissionPac
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 视频提前访问豁免:用户加入的任意权限组开启了该组级能力即获得。
|
||||
* 部门进组是素材可见范围的底子,不参与此判定;豁免按"人进组是加餐"的口径只认个人挂载。
|
||||
*/
|
||||
export async function resolveUserVideoEarlyAccess(userId: string): Promise<boolean> {
|
||||
if (!userId) return false
|
||||
try {
|
||||
const assignment = await prisma.userPermissionInherit.findFirst({
|
||||
where: { userId, pack: { videoEarlyAccess: true } },
|
||||
select: { id: true },
|
||||
})
|
||||
return Boolean(assignment)
|
||||
} catch (error) {
|
||||
if (isMissingInheritTable(error)) return false
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function savePermissionPackItems(
|
||||
slug: string,
|
||||
permissions: Array<MaterialPermissionFlags & { materialType: string }>,
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
-- 视频提前访问:权限组级能力。人进组(UserPermissionInherit)即获得豁免,默认关闭,仅超级管理员在权限组看板开关。
|
||||
ALTER TABLE "material_permission_packs" ADD COLUMN "video_early_access" BOOLEAN NOT NULL DEFAULT false;
|
||||
@ -1,2 +0,0 @@
|
||||
-- 视频提前访问豁免:默认关闭,仅超级管理员可在用户管理中为个别用户开启。
|
||||
ALTER TABLE "users" ADD COLUMN "video_early_access" BOOLEAN NOT NULL DEFAULT false;
|
||||
@ -18,7 +18,6 @@ model User {
|
||||
role Role @default(USER)
|
||||
isActive Boolean @default(false)
|
||||
canDownload Boolean @default(true)
|
||||
videoEarlyAccess Boolean @default(false) @map("video_early_access")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
department String @default("")
|
||||
@ -391,6 +390,7 @@ model MaterialPermissionPack {
|
||||
id String @id @default(uuid())
|
||||
slug String @unique
|
||||
name String
|
||||
videoEarlyAccess Boolean @default(false) @map("video_early_access")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
items MaterialPermissionPackItem[]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user