101 lines
4.0 KiB
TypeScript
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;
|
|
}
|
|
}
|