256 lines
7.5 KiB
TypeScript
256 lines
7.5 KiB
TypeScript
import {
|
||
Body,
|
||
Controller,
|
||
Delete,
|
||
Get,
|
||
Param,
|
||
Patch,
|
||
Post,
|
||
Req,
|
||
} from "@nestjs/common";
|
||
import { IamService } from "./iam.service.js";
|
||
import {
|
||
Permissions,
|
||
RequirePermission,
|
||
} from "../middleware/permission.guard.js";
|
||
import {
|
||
type AuthenticatedRequest,
|
||
extractAuditContext,
|
||
} from "../middleware/auth.middleware.js";
|
||
import {
|
||
createRoleSchema,
|
||
updateRoleSchema,
|
||
updateRolePermissionsSchema,
|
||
createPermissionSchema,
|
||
updatePermissionSchema,
|
||
createViewportSchema,
|
||
updateViewportSchema,
|
||
verifyTotpSchema,
|
||
} from "./iam.dto.js";
|
||
|
||
/**
|
||
* RBAC 管理端点:角色/权限/视口 CRUD + TOTP 2FA(admin-portal 使用)。
|
||
*/
|
||
@Controller("v1/iam")
|
||
export class RbacController {
|
||
constructor(private readonly service: IamService) {}
|
||
|
||
private maybeContext(req: AuthenticatedRequest) {
|
||
return extractAuditContext(req);
|
||
}
|
||
|
||
// ============ 角色 ============
|
||
|
||
@Get("roles")
|
||
@RequirePermission(Permissions.IAM_ROLE_MANAGE)
|
||
async roles(): Promise<{ success: true; data: unknown[] }> {
|
||
const data = await this.service.getAllRoles();
|
||
return { success: true as const, data };
|
||
}
|
||
|
||
@Post("roles")
|
||
@RequirePermission(Permissions.IAM_ROLE_MANAGE)
|
||
async createRole(
|
||
@Body() body: unknown,
|
||
@Req() req: AuthenticatedRequest,
|
||
): Promise<{ success: true; data: unknown }> {
|
||
const dto = createRoleSchema.parse(body);
|
||
const data = await this.service.createRole(dto, this.maybeContext(req));
|
||
return { success: true as const, data };
|
||
}
|
||
|
||
@Patch("roles/:id")
|
||
@RequirePermission(Permissions.IAM_ROLE_MANAGE)
|
||
async updateRole(
|
||
@Param("id") id: string,
|
||
@Body() body: unknown,
|
||
@Req() req: AuthenticatedRequest,
|
||
): Promise<{ success: true; data: unknown }> {
|
||
const dto = updateRoleSchema.parse(body);
|
||
const data = await this.service.updateRole(id, dto, this.maybeContext(req));
|
||
return { success: true as const, data };
|
||
}
|
||
|
||
@Patch("roles/:id/permissions")
|
||
@RequirePermission(Permissions.IAM_ROLE_MANAGE)
|
||
async updateRolePermissions(
|
||
@Param("id") id: string,
|
||
@Body() body: unknown,
|
||
@Req() req: AuthenticatedRequest,
|
||
): Promise<{ success: true; data: { success: boolean } }> {
|
||
const dto = updateRolePermissionsSchema.parse(body);
|
||
await this.service.updateRolePermissions(
|
||
id,
|
||
dto.permissionIds,
|
||
this.maybeContext(req),
|
||
);
|
||
return { success: true as const, data: { success: true } };
|
||
}
|
||
|
||
@Post("roles/:roleId/permissions/:permissionId")
|
||
@RequirePermission(Permissions.IAM_ROLE_MANAGE)
|
||
async grantPermission(
|
||
@Param("roleId") roleId: string,
|
||
@Param("permissionId") permissionId: string,
|
||
@Req() req: AuthenticatedRequest,
|
||
): Promise<{ success: true; data: { success: boolean } }> {
|
||
await this.service.grantPermissionToRole(
|
||
roleId,
|
||
permissionId,
|
||
this.maybeContext(req),
|
||
);
|
||
return { success: true as const, data: { success: true } };
|
||
}
|
||
|
||
@Delete("roles/:roleId/permissions/:permissionId")
|
||
@RequirePermission(Permissions.IAM_ROLE_MANAGE)
|
||
async revokePermission(
|
||
@Param("roleId") roleId: string,
|
||
@Param("permissionId") permissionId: string,
|
||
@Req() req: AuthenticatedRequest,
|
||
): Promise<{ success: true; data: { success: boolean } }> {
|
||
await this.service.revokePermissionFromRole(
|
||
roleId,
|
||
permissionId,
|
||
this.maybeContext(req),
|
||
);
|
||
return { success: true as const, data: { success: true } };
|
||
}
|
||
|
||
// ============ 权限 ============
|
||
|
||
@Get("permissions")
|
||
@RequirePermission(Permissions.IAM_ROLE_MANAGE)
|
||
async permissions(): Promise<{ success: true; data: unknown[] }> {
|
||
const data = await this.service.getAllPermissions();
|
||
return { success: true as const, data };
|
||
}
|
||
|
||
@Post("permissions")
|
||
@RequirePermission(Permissions.IAM_ROLE_MANAGE)
|
||
async createPermission(
|
||
@Body() body: unknown,
|
||
@Req() req: AuthenticatedRequest,
|
||
): Promise<{ success: true; data: unknown }> {
|
||
const dto = createPermissionSchema.parse(body);
|
||
const data = await this.service.createPermission(
|
||
dto,
|
||
this.maybeContext(req),
|
||
);
|
||
return { success: true as const, data };
|
||
}
|
||
|
||
@Patch("permissions/:id")
|
||
@RequirePermission(Permissions.IAM_ROLE_MANAGE)
|
||
async updatePermission(
|
||
@Param("id") id: string,
|
||
@Body() body: unknown,
|
||
@Req() req: AuthenticatedRequest,
|
||
): Promise<{ success: true; data: unknown }> {
|
||
const dto = updatePermissionSchema.parse(body);
|
||
const data = await this.service.updatePermission(
|
||
id,
|
||
dto,
|
||
this.maybeContext(req),
|
||
);
|
||
return { success: true as const, data };
|
||
}
|
||
|
||
@Delete("permissions/:id")
|
||
@RequirePermission(Permissions.IAM_ROLE_MANAGE)
|
||
async deletePermission(
|
||
@Param("id") id: string,
|
||
@Req() req: AuthenticatedRequest,
|
||
): Promise<{ success: true; data: { success: boolean } }> {
|
||
await this.service.deletePermission(id, this.maybeContext(req));
|
||
return { success: true as const, data: { success: true } };
|
||
}
|
||
|
||
// ============ 视口 ============
|
||
|
||
@Post("viewports")
|
||
@RequirePermission(Permissions.IAM_ROLE_MANAGE)
|
||
async createViewport(
|
||
@Body() body: unknown,
|
||
@Req() req: AuthenticatedRequest,
|
||
): Promise<{ success: true; data: unknown }> {
|
||
const dto = createViewportSchema.parse(body);
|
||
const data = await this.service.createViewport(dto, this.maybeContext(req));
|
||
return { success: true as const, data };
|
||
}
|
||
|
||
@Patch("viewports/:id")
|
||
@RequirePermission(Permissions.IAM_ROLE_MANAGE)
|
||
async updateViewport(
|
||
@Param("id") id: string,
|
||
@Body() body: unknown,
|
||
@Req() req: AuthenticatedRequest,
|
||
): Promise<{ success: true; data: unknown }> {
|
||
const dto = updateViewportSchema.parse(body);
|
||
const data = await this.service.updateViewport(
|
||
id,
|
||
dto,
|
||
this.maybeContext(req),
|
||
);
|
||
return { success: true as const, data };
|
||
}
|
||
|
||
@Delete("viewports/:id")
|
||
@RequirePermission(Permissions.IAM_ROLE_MANAGE)
|
||
async deleteViewport(
|
||
@Param("id") id: string,
|
||
@Req() req: AuthenticatedRequest,
|
||
): Promise<{ success: true; data: { success: boolean } }> {
|
||
await this.service.deleteViewport(id, this.maybeContext(req));
|
||
return { success: true as const, data: { success: true } };
|
||
}
|
||
|
||
// ============ TOTP 2FA ============
|
||
|
||
@Post("totp/enable")
|
||
@RequirePermission(Permissions.IAM_USER_READ)
|
||
async enableTotp(@Req() req: AuthenticatedRequest): Promise<{
|
||
success: true;
|
||
data: { secret: string; qrUrl: string; backupCodes: string[] };
|
||
}> {
|
||
const userId = req.userId;
|
||
if (!userId) {
|
||
throw new Error("Missing user identity");
|
||
}
|
||
const data = await this.service.enableTotp(userId);
|
||
return { success: true as const, data };
|
||
}
|
||
|
||
@Post("totp/verify")
|
||
@RequirePermission(Permissions.IAM_USER_READ)
|
||
async verifyTotp(
|
||
@Body() body: unknown,
|
||
@Req() req: AuthenticatedRequest,
|
||
): Promise<{ success: true; data: { verified: boolean } }> {
|
||
const userId = req.userId;
|
||
if (!userId) {
|
||
throw new Error("Missing user identity");
|
||
}
|
||
const dto = verifyTotpSchema.parse(body);
|
||
const data = await this.service.verifyTotp(
|
||
userId,
|
||
dto.code,
|
||
this.maybeContext(req),
|
||
);
|
||
return { success: true as const, data };
|
||
}
|
||
|
||
@Post("totp/disable")
|
||
@RequirePermission(Permissions.IAM_USER_READ)
|
||
async disableTotp(
|
||
@Req() req: AuthenticatedRequest,
|
||
): Promise<{ success: true; data: { success: boolean } }> {
|
||
const userId = req.userId;
|
||
if (!userId) {
|
||
throw new Error("Missing user identity");
|
||
}
|
||
await this.service.disableTotp(userId, this.maybeContext(req));
|
||
return { success: true as const, data: { success: true } };
|
||
}
|
||
}
|