import { HttpError, type RequestContext } from '../../core/http.js'; import { queryOne } from '../../core/db.js'; import { tenantIdFrom, userIdFrom } from '../../core/request.js'; const TENANT_ADMIN_ROLES = new Set([ 'tenant_owner', 'tenant_admin', 'tenant_operator', 'teacher', 'sales', 'agent', ]); const ROLE_PERMISSION_DEFAULTS: Record = { tenant_owner: ['*'], tenant_admin: ['*'], tenant_operator: ['content:*', 'marketing:*', 'codes:read', 'coupons:read', 'referral:read', 'crm:read'], teacher: ['content:*'], sales: ['codes:*', 'coupons:*', 'referral:*'], agent: ['codes:read', 'coupons:read', 'referral:self'], student: [], }; export interface TenantAdminAuth { tenantId: string; userId: string; role: string; roleTemplateId: string | null; roleTemplateCode: string | null; roleTemplateName: string | null; permissions: Record; templatePermissions: Record; menuPermissions: Record; modulePermissions: Record; fieldPermissions: Record; dataScope: Record; } function permissionKeys(permission: string) { const parts = permission.split(':').filter(Boolean); const keys = [permission]; for (let i = parts.length - 1; i >= 1; i -= 1) { keys.push(`${parts.slice(0, i).join(':')}:*`); } keys.push('*'); return keys; } function explicitPermission(permissions: Record, permission: string) { for (const key of permissionKeys(permission)) { const value = permissions[key]; if (typeof value === 'boolean') return value; } return null; } export function hasTenantPermission(auth: TenantAdminAuth, permission: string) { const explicit = explicitPermission(auth.permissions, permission); if (explicit !== null) return explicit; const templateExplicit = explicitPermission(auth.templatePermissions, permission); if (templateExplicit !== null) return templateExplicit; const defaults = ROLE_PERMISSION_DEFAULTS[auth.role] || []; return defaults.some(defaultPermission => { if (defaultPermission === '*') return true; if (defaultPermission === permission) return true; if (defaultPermission.endsWith(':*')) { return permission.startsWith(defaultPermission.slice(0, -1)); } return false; }); } export function requireTenantPermission(auth: TenantAdminAuth, permission: string) { if (!hasTenantPermission(auth, permission)) { throw new HttpError(403, `Tenant permission is required: ${permission}`, 'TENANT_PERMISSION_REQUIRED'); } } export function tenantPermissionCatalog() { return { permissions: [ { key: 'tenant:overview:read', label: '租户概览' }, { key: 'tenant:branding:write', label: '品牌配置' }, { key: 'tenant:settings:write', label: '公开设置' }, { key: 'tenant:domains:read', label: '域名查看' }, { key: 'tenant:domains:write', label: '域名管理' }, { key: 'tenant:payment:read', label: '商户配置查看' }, { key: 'tenant:payment:write', label: '商户配置管理' }, { key: 'tenant:auth:read', label: '登录配置查看' }, { key: 'tenant:auth:write', label: '登录配置管理' }, { key: 'tenant:secrets:read', label: '密钥掩码查看' }, { key: 'tenant:secrets:write', label: '密钥轮换' }, { key: 'marketing:read', label: '活动内容查看' }, { key: 'marketing:write', label: '活动内容管理' }, { key: 'codes:read', label: '激活码查看' }, { key: 'codes:write', label: '激活码管理' }, { key: 'coupons:read', label: '优惠券查看' }, { key: 'coupons:write', label: '优惠券管理' }, { key: 'referral:read', label: '客资全局查看' }, { key: 'referral:self', label: '本人客资查看' }, { key: 'referral:write', label: '客资归属管理' }, { key: 'crm:read', label: 'CRM 队列查看' }, { key: 'crm:write', label: 'CRM 入队和重试' }, { key: 'members:read', label: '成员查看' }, { key: 'members:write', label: '成员管理' }, { key: 'roles:read', label: '角色模板查看' }, { key: 'roles:write', label: '角色模板管理' }, { key: 'audit:read', label: '审计日志查看' }, { key: 'content:*', label: '内容维护' }, ], menuGroups: [ { key: 'dashboard', label: '数据看板' }, { key: 'content', label: '题库内容' }, { key: 'students', label: '学生管理' }, { key: 'teachers', label: '教师/班级' }, { key: 'marketing', label: '营销中心' }, { key: 'sales', label: '销售/代理' }, { key: 'crm', label: 'CRM' }, { key: 'commerce', label: '订单/权益' }, { key: 'settings', label: '租户设置' }, { key: 'audit', label: '审计日志' }, ], fieldGroups: [ { key: 'student.phone', label: '学生手机号' }, { key: 'student.wechat', label: '学生微信' }, { key: 'student.exam_intent', label: '考试意向' }, { key: 'order.amount', label: '订单金额' }, { key: 'referral.owner', label: '客资归属' }, { key: 'payment.secret_mask', label: '商户密钥掩码' }, ], roleDefaults: ROLE_PERMISSION_DEFAULTS, }; } export async function requireTenantAdmin(ctx: RequestContext): Promise { const tenantId = await tenantIdFrom(ctx); const userId = await userIdFrom(ctx); const membership = await queryOne<{ role: string; roleTemplateId: string | null; roleTemplateCode: string | null; roleTemplateName: string | null; permissions: Record; templatePermissions: Record; menuPermissions: Record; modulePermissions: Record; fieldPermissions: Record; dataScope: Record; }>( ` select tm.role, tm.permissions, tm.role_template_id as "roleTemplateId", rt.code as "roleTemplateCode", rt.name as "roleTemplateName", coalesce(rt.permissions, '{}'::jsonb) as "templatePermissions", coalesce(rt.menu_permissions, '{}'::jsonb) as "menuPermissions", coalesce(rt.module_permissions, '{}'::jsonb) as "modulePermissions", coalesce(rt.field_permissions, '{}'::jsonb) as "fieldPermissions", coalesce(rt.data_scope, '{}'::jsonb) as "dataScope" from public.tenant_memberships tm left join public.tenant_role_templates rt on rt.id = tm.role_template_id and rt.tenant_id = tm.tenant_id and rt.status = 'active' where tm.tenant_id = $1 and tm.user_id = $2 and tm.status = 'active' and tm.role = any($3::text[]) order by case role when 'tenant_owner' then 1 when 'tenant_admin' then 2 else 9 end limit 1 `, [tenantId, userId, Array.from(TENANT_ADMIN_ROLES)], ); if (!membership) { throw new HttpError(403, 'Tenant admin access is required', 'TENANT_ADMIN_REQUIRED'); } return { tenantId, userId, role: membership.role, roleTemplateId: membership.roleTemplateId, roleTemplateCode: membership.roleTemplateCode, roleTemplateName: membership.roleTemplateName, permissions: membership.permissions || {}, templatePermissions: membership.templatePermissions || {}, menuPermissions: membership.menuPermissions || {}, modulePermissions: membership.modulePermissions || {}, fieldPermissions: membership.fieldPermissions || {}, dataScope: membership.dataScope || {}, }; }