feat(backend): Implement Day 6 Role Management API

Add complete role management functionality for tenant administrators to manage user roles within their tenants.

Changes:
- Extended IUserTenantRoleRepository with pagination, role counting, and last owner check methods
- Extended IUserRepository with GetByIdAsync(Guid) and GetByIdsAsync for flexible user retrieval
- Extended IRefreshTokenRepository with GetByUserAndTenantAsync and UpdateRangeAsync
- Implemented repository methods in Infrastructure layer
- Created DTOs: UserWithRoleDto and PagedResultDto<T>
- Implemented ListTenantUsersQuery with pagination support
- Implemented AssignUserRoleCommand to assign/update user roles
- Implemented RemoveUserFromTenantCommand with token revocation
- Created TenantUsersController with 4 endpoints (list, assign, remove, get-roles)
- Added comprehensive PowerShell test script

Security Features:
- Only TenantOwner can assign/update/remove roles
- Prevents removal of last TenantOwner (lockout protection)
- Prevents manual assignment of AIAgent role (reserved for MCP)
- Cross-tenant access protection
- Automatic refresh token revocation when user removed

API Endpoints:
- GET /api/tenants/{id}/users - List users with roles (paginated)
- POST /api/tenants/{id}/users/{userId}/role - Assign/update role
- DELETE /api/tenants/{id}/users/{userId} - Remove user from tenant
- GET /api/tenants/roles - Get available roles

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Yaojia Wang
2025-11-03 19:11:51 +01:00
parent e604b762ff
commit cbc040621f
16 changed files with 664 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
using ColaFlow.Modules.Identity.Application.Commands.AssignUserRole;
using ColaFlow.Modules.Identity.Application.Commands.RemoveUserFromTenant;
using ColaFlow.Modules.Identity.Application.Queries.ListTenantUsers;
using ColaFlow.Modules.Identity.Application.Dtos;
using MediatR;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace ColaFlow.API.Controllers;
[ApiController]
[Route("api/tenants/{tenantId}/users")]
[Authorize]
public class TenantUsersController : ControllerBase
{
private readonly IMediator _mediator;
public TenantUsersController(IMediator mediator)
{
_mediator = mediator;
}
/// <summary>
/// List all users in a tenant with their roles
/// </summary>
[HttpGet]
[Authorize(Policy = "RequireTenantAdmin")]
public async Task<IActionResult> ListUsers(
[FromRoute] Guid tenantId,
[FromQuery] int pageNumber = 1,
[FromQuery] int pageSize = 20,
[FromQuery] string? search = null)
{
var query = new ListTenantUsersQuery(tenantId, pageNumber, pageSize, search);
var result = await _mediator.Send(query);
return Ok(result);
}
/// <summary>
/// Assign or update a user's role in the tenant
/// </summary>
[HttpPost("{userId}/role")]
[Authorize(Policy = "RequireTenantOwner")]
public async Task<IActionResult> AssignRole(
[FromRoute] Guid tenantId,
[FromRoute] Guid userId,
[FromBody] AssignRoleRequest request)
{
var command = new AssignUserRoleCommand(tenantId, userId, request.Role);
await _mediator.Send(command);
return Ok(new { Message = "Role assigned successfully" });
}
/// <summary>
/// Remove a user from the tenant
/// </summary>
[HttpDelete("{userId}")]
[Authorize(Policy = "RequireTenantOwner")]
public async Task<IActionResult> RemoveUser(
[FromRoute] Guid tenantId,
[FromRoute] Guid userId)
{
var command = new RemoveUserFromTenantCommand(tenantId, userId);
await _mediator.Send(command);
return Ok(new { Message = "User removed from tenant successfully" });
}
/// <summary>
/// Get available roles
/// </summary>
[HttpGet("../roles")]
[Authorize(Policy = "RequireTenantAdmin")]
public IActionResult GetAvailableRoles()
{
var roles = new[]
{
new { Name = "TenantOwner", Description = "Full control over the tenant" },
new { Name = "TenantAdmin", Description = "Manage users and projects" },
new { Name = "TenantMember", Description = "Create and edit tasks" },
new { Name = "TenantGuest", Description = "Read-only access" }
};
return Ok(roles);
}
}
public record AssignRoleRequest(string Role);