forked from wangziqi/gongxue-base
200 lines
7.5 KiB
TypeScript
200 lines
7.5 KiB
TypeScript
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<string, string[]> = {
|
|
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<string, unknown>;
|
|
templatePermissions: Record<string, unknown>;
|
|
menuPermissions: Record<string, unknown>;
|
|
modulePermissions: Record<string, unknown>;
|
|
fieldPermissions: Record<string, unknown>;
|
|
dataScope: Record<string, unknown>;
|
|
}
|
|
|
|
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<string, unknown>, 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<TenantAdminAuth> {
|
|
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<string, unknown>;
|
|
templatePermissions: Record<string, unknown>;
|
|
menuPermissions: Record<string, unknown>;
|
|
modulePermissions: Record<string, unknown>;
|
|
fieldPermissions: Record<string, unknown>;
|
|
dataScope: Record<string, unknown>;
|
|
}>(
|
|
`
|
|
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 || {},
|
|
};
|
|
}
|