using ColaFlow.Modules.Identity.Application.Commands.AssignUserRole;
using ColaFlow.Modules.Identity.Application.Commands.RemoveUserFromTenant;
using ColaFlow.Modules.Identity.Application.Commands.UpdateUserRole;
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(IMediator mediator) : ControllerBase
{
///
/// List all users in a tenant with their roles
///
[HttpGet]
[Authorize(Policy = "RequireTenantAdmin")]
public async Task ListUsers(
[FromRoute] Guid tenantId,
[FromQuery] int pageNumber = 1,
[FromQuery] int pageSize = 20,
[FromQuery] string? search = null)
{
// SECURITY: Validate user belongs to target tenant
var userTenantIdClaim = User.FindFirst("tenant_id")?.Value;
if (userTenantIdClaim == null)
return Unauthorized(new { error = "Tenant information not found in token" });
var userTenantId = Guid.Parse(userTenantIdClaim);
if (userTenantId != tenantId)
return StatusCode(403, new { error = "Access denied: You can only manage users in your own tenant" });
var query = new ListTenantUsersQuery(tenantId, pageNumber, pageSize, search);
var result = await mediator.Send(query);
return Ok(result);
}
///
/// Assign or update a user's role in the tenant
///
[HttpPost("{userId}/role")]
[Authorize(Policy = "RequireTenantOwner")]
public async Task AssignRole(
[FromRoute] Guid tenantId,
[FromRoute] Guid userId,
[FromBody] AssignRoleRequest request)
{
// SECURITY: Validate user belongs to target tenant
var userTenantIdClaim = User.FindFirst("tenant_id")?.Value;
if (userTenantIdClaim == null)
return Unauthorized(new { error = "Tenant information not found in token" });
var userTenantId = Guid.Parse(userTenantIdClaim);
if (userTenantId != tenantId)
return StatusCode(403, new { error = "Access denied: You can only manage users in your own tenant" });
// Extract current user ID from claims
var currentUserIdClaim = User.FindFirst("user_id")?.Value;
if (currentUserIdClaim == null)
return Unauthorized(new { error = "User ID not found in token" });
var currentUserId = Guid.Parse(currentUserIdClaim);
var command = new AssignUserRoleCommand(tenantId, userId, request.Role, currentUserId);
await mediator.Send(command);
return Ok(new { Message = "Role assigned successfully" });
}
///
/// Update an existing user's role in the tenant (RESTful PUT endpoint)
///
[HttpPut("{userId:guid}/role")]
[Authorize(Policy = "RequireTenantOwner")]
public async Task> UpdateRole(
[FromRoute] Guid tenantId,
[FromRoute] Guid userId,
[FromBody] AssignRoleRequest request)
{
// SECURITY: Validate user belongs to target tenant
var userTenantIdClaim = User.FindFirst("tenant_id")?.Value;
if (userTenantIdClaim == null)
return Unauthorized(new { error = "Tenant information not found in token" });
var userTenantId = Guid.Parse(userTenantIdClaim);
if (userTenantId != tenantId)
return StatusCode(403, new { error = "Access denied: You can only manage users in your own tenant" });
// Extract current user ID from claims
var currentUserIdClaim = User.FindFirst("user_id")?.Value;
if (currentUserIdClaim == null)
return Unauthorized(new { error = "User ID not found in token" });
var currentUserId = Guid.Parse(currentUserIdClaim);
try
{
var command = new UpdateUserRoleCommand(tenantId, userId, request.Role, currentUserId);
var result = await mediator.Send(command);
return Ok(result);
}
catch (InvalidOperationException ex)
{
return BadRequest(new { error = ex.Message });
}
catch (ArgumentException ex)
{
return BadRequest(new { error = ex.Message });
}
}
///
/// Remove a user from the tenant
///
[HttpDelete("{userId}")]
[Authorize(Policy = "RequireTenantOwner")]
public async Task RemoveUser(
[FromRoute] Guid tenantId,
[FromRoute] Guid userId)
{
// SECURITY: Validate user belongs to target tenant
var userTenantIdClaim = User.FindFirst("tenant_id")?.Value;
if (userTenantIdClaim == null)
return Unauthorized(new { error = "Tenant information not found in token" });
var userTenantId = Guid.Parse(userTenantIdClaim);
if (userTenantId != tenantId)
return StatusCode(403, new { error = "Access denied: You can only manage users in your own tenant" });
// Extract current user ID from claims
var currentUserIdClaim = User.FindFirst("user_id")?.Value;
if (currentUserIdClaim == null)
return Unauthorized(new { error = "User ID not found in token" });
var currentUserId = Guid.Parse(currentUserIdClaim);
var command = new RemoveUserFromTenantCommand(tenantId, userId, currentUserId, null);
await mediator.Send(command);
return Ok(new { Message = "User removed from tenant successfully" });
}
///
/// Get available roles (Note: This endpoint doesn't use tenantId from route, so tenant validation is skipped.
/// It only returns static role definitions, not tenant-specific data.)
///
[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);