Files
gongxue-base/apps/api/src/features/tenant-admin/auth.ts
2026-06-21 21:54:43 +08:00

131 lines
4.6 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;
permissions: 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 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: 'audit:read', label: '审计日志查看' },
{ key: 'content:*', label: '内容维护' },
],
roleDefaults: ROLE_PERMISSION_DEFAULTS,
};
}
export async function requireTenantAdmin(ctx: RequestContext): Promise<TenantAdminAuth> {
const tenantId = tenantIdFrom(ctx);
const userId = userIdFrom(ctx);
const membership = await queryOne<{ role: string; permissions: Record<string, unknown> }>(
`
select role, permissions
from public.tenant_memberships
where tenant_id = $1
and user_id = $2
and status = 'active'
and 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, permissions: membership.permissions || {} };
}