Files
qipai/backend/src/auth/rbac-repository.ts
T

101 lines
4.0 KiB
TypeScript

import type { ResultSetHeader, RowDataPacket } from 'mysql2/promise';
import type { MySqlPool } from '../db/mysql.js';
export const roleCodes = [
'CUSTOMER', 'CLEANER', 'STAFF', 'STORE_ADMIN', 'TENANT_ADMIN', 'PLATFORM_ADMIN'
] as const;
export interface AccessProfile {
roles: string[];
capabilities: string[];
storeIds: string[];
}
interface CodeRow extends RowDataPacket { code: string }
interface StoreRow extends RowDataPacket { storeId: string }
export class RbacRepository {
constructor(private readonly pool: MySqlPool) {}
async ensureCustomerRole(tenantId: string, userId: string): Promise<void> {
await this.pool.execute(
`INSERT IGNORE INTO qipai_roles (tenant_id, code, name) VALUES
(?, 'CUSTOMER', '顾客'), (?, 'CLEANER', '保洁员'), (?, 'STAFF', '门店员工'),
(?, 'STORE_ADMIN', '门店管理员'), (?, 'TENANT_ADMIN', '租户管理员'),
(?, 'PLATFORM_ADMIN', '平台管理员')`,
Array(6).fill(tenantId)
);
await this.pool.execute(
`INSERT IGNORE INTO qipai_user_roles (tenant_id, user_id, role_id)
SELECT ?, ?, id FROM qipai_roles
WHERE tenant_id = ? AND code = 'CUSTOMER' AND status = 'ACTIVE'`,
[tenantId, userId, tenantId]
);
await this.pool.execute(
`INSERT IGNORE INTO qipai_role_permissions (tenant_id, role_id, permission_id)
SELECT ?, r.id, p.id FROM qipai_roles r
INNER JOIN qipai_permissions p ON
(r.code = 'CUSTOMER' AND p.code IN ('profile.read', 'order.self.read'))
OR (r.code = 'STORE_ADMIN'
AND p.code IN ('user.read', 'staff.manage', 'session.reset',
'store.operation.read', 'store.operation.write',
'device.read', 'device.write'))
OR (r.code IN ('TENANT_ADMIN', 'PLATFORM_ADMIN')
AND p.code IN ('user.read', 'staff.manage', 'session.reset', 'tenant.manage',
'device.read', 'device.write'))
WHERE r.tenant_id = ?`,
[tenantId, tenantId]
);
}
async getAccessProfile(tenantId: string, userId: string): Promise<AccessProfile> {
const [roles] = await this.pool.execute<CodeRow[]>(
`SELECT DISTINCT r.code FROM qipai_user_roles ur
INNER JOIN qipai_roles r
ON r.id = ur.role_id AND r.tenant_id = ur.tenant_id
AND r.status = 'ACTIVE' AND r.deleted_at IS NULL
WHERE ur.tenant_id = ? AND ur.user_id = ? ORDER BY r.code`,
[tenantId, userId]
);
const [permissions] = await this.pool.execute<CodeRow[]>(
`SELECT DISTINCT p.code FROM qipai_user_roles ur
INNER JOIN qipai_roles r
ON r.id = ur.role_id AND r.tenant_id = ur.tenant_id
AND r.status = 'ACTIVE' AND r.deleted_at IS NULL
INNER JOIN qipai_role_permissions rp
ON rp.tenant_id = ur.tenant_id AND rp.role_id = ur.role_id
INNER JOIN qipai_permissions p ON p.id = rp.permission_id
WHERE ur.tenant_id = ? AND ur.user_id = ? ORDER BY p.code`,
[tenantId, userId]
);
const [stores] = await this.pool.execute<StoreRow[]>(
`SELECT DISTINCT store_id AS storeId FROM qipai_user_store_scopes
WHERE tenant_id = ? AND user_id = ? ORDER BY store_id`,
[tenantId, userId]
);
return {
roles: roles.map((row) => row.code),
capabilities: permissions.map((row) => row.code),
storeIds: stores.map((row) => String(row.storeId))
};
}
async grantStore(input: {
tenantId: string; userId: string; storeId: string; scopeType: 'STAFF' | 'CLEANER';
}): Promise<boolean> {
const [result] = await this.pool.execute<ResultSetHeader>(
`INSERT IGNORE INTO qipai_user_store_scopes
(tenant_id, user_id, store_id, scope_type)
SELECT ?, ?, s.id, ?
FROM qipai_stores s
INNER JOIN qipai_users u ON u.id = ? AND u.tenant_id = ?
WHERE s.id = ? AND s.tenant_id = ? AND s.deleted_at IS NULL`,
[
input.tenantId, input.userId, input.scopeType,
input.userId, input.tenantId, input.storeId, input.tenantId
]
);
return result.affectedRows === 1;
}
}