using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Manager.Services; using ManagerService.Data; using ManagerService.DTOs; using ManagerService.Helpers; using ManagerService.Service; using ManagerService.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using NSwag.Annotations; namespace ManagerService.Controllers { [Authorize(Policy = ManagerService.Service.Security.Policies.InstanceAdmin)] [ApiController, Route("api/[controller]")] [OpenApiTag("User", Description = "User management")] public class UserController : ControllerBase { private UserDatabaseService _userService; private readonly ILogger _logger; private readonly ProfileLogic _profileLogic; private readonly MyInfoMateDbContext _myInfoMateDbContext; private readonly IEmailService _emailService; private readonly IConfiguration _configuration; IHexIdGeneratorService idService = new HexIdGeneratorService(); /// /// Plafond d'utilisateurs par instance, tous plans confondus. /// /// /// ⚠️ Dette assumee pour la V1 : le chiffre est en dur. Le faire varier par plan /// demanderait une colonne sur SubscriptionPlan, qui n'en porte aucune sur les /// utilisateurs, donc une migration apres le gel du schema (lot B). /// Le SuperAdmin n'est pas soumis au plafond : c'est la porte de service interne /// tant qu'aucun champ ne permet de relever la limite d'un client. /// private const int MaxUsersPerInstance = 5; public UserController(ILogger logger, UserDatabaseService userService, ProfileLogic profileLogic, MyInfoMateDbContext myInfoMateDbContext, IEmailService emailService, IConfiguration configuration) { _logger = logger; _userService = userService; _profileLogic = profileLogic; _myInfoMateDbContext = myInfoMateDbContext; _emailService = emailService; _configuration = configuration; } private string? GetCallerInstanceId() => User.FindFirst(ManagerService.Service.Security.ClaimTypes.InstanceId)?.Value; private bool IsSuperAdmin() => User.HasClaim(ManagerService.Service.Security.ClaimTypes.Permission, ManagerService.Service.Security.Permissions.SuperAdmin); private UserRole GetCallerRole() { if (User.HasClaim(ManagerService.Service.Security.ClaimTypes.Permission, ManagerService.Service.Security.Permissions.SuperAdmin)) return UserRole.SuperAdmin; if (User.HasClaim(ManagerService.Service.Security.ClaimTypes.Permission, ManagerService.Service.Security.Permissions.InstanceAdmin)) return UserRole.InstanceAdmin; if (User.HasClaim(ManagerService.Service.Security.ClaimTypes.Permission, ManagerService.Service.Security.Permissions.ContentEditor)) return UserRole.ContentEditor; return UserRole.Viewer; } /// /// Get a list of user /// [ProducesResponseType(typeof(List), 200)] [ProducesResponseType(typeof(string), 500)] [HttpGet] public ObjectResult Get() { try { var query = _myInfoMateDbContext.Users.AsQueryable(); if (!IsSuperAdmin()) query = query.Where(u => u.InstanceId == GetCallerInstanceId()); return new OkObjectResult(query.ToList().Select(u => u.ToDTO())); } catch (Exception ex) { return new ObjectResult(ex.Message) { StatusCode = 500 }; } } /// /// Get a specific user /// /// id user [ProducesResponseType(typeof(UserDetailDTO), 200)] [ProducesResponseType(typeof(string), 404)] [ProducesResponseType(typeof(string), 500)] [HttpGet("{id}")] public ObjectResult GetDetail(string id) { try { User user = _myInfoMateDbContext.Users.FirstOrDefault(i => i.Id == id); if (user == null || (!IsSuperAdmin() && user.InstanceId != GetCallerInstanceId())) throw new KeyNotFoundException("This user was not found"); return new OkObjectResult(user.ToDTO()); } catch (KeyNotFoundException ex) { return new NotFoundObjectResult(ex.Message) {}; } catch (Exception ex) { return new ObjectResult(ex.Message) { StatusCode = 500 }; } } /// /// Create an user /// /// New user info [ProducesResponseType(typeof(UserDetailDTO), 200)] [ProducesResponseType(typeof(string), 400)] [ProducesResponseType(typeof(string), 409)] [ProducesResponseType(typeof(string), 422)] [ProducesResponseType(typeof(string), 500)] [HttpPost] public async Task CreateUser([FromBody] UserDetailDTO newUserDTO) { try { if (newUserDTO == null) throw new ArgumentNullException("User param is null"); if (newUserDTO.instanceId == null) throw new ArgumentNullException("InstanceId is null"); var requestedRole = newUserDTO.role ?? UserRole.ContentEditor; if (requestedRole < GetCallerRole()) throw new UnauthorizedAccessException("Cannot assign a role higher than your own"); User newUser = new User(); newUser.InstanceId = IsSuperAdmin() ? newUserDTO.instanceId : GetCallerInstanceId(); if (!IsSuperAdmin() && _myInfoMateDbContext.Users.Count(u => u.InstanceId == newUser.InstanceId) >= MaxUsersPerInstance) return UnprocessableEntity($"This instance has reached its limit of {MaxUsersPerInstance} users"); newUser.Email = newUserDTO.email; newUser.FirstName = newUserDTO.firstName; newUser.LastName = newUserDTO.lastName; newUser.Role = requestedRole; newUser.Token = Guid.NewGuid().ToString(); newUser.DateCreation = DateTime.Now.ToUniversalTime(); newUser.Id = idService.GenerateHexId(); if (_myInfoMateDbContext.Users.Any(u => u.Email == newUser.Email)) throw new InvalidOperationException("This Email is already used"); // No password provided: invite the user by email instead — they set their own // password via a token link (same mechanism as onboarding/forgot-password). string inviteRawToken = null; if (string.IsNullOrEmpty(newUserDTO.password)) { newUser.Password = _profileLogic.HashPassword(PasswordUtils.GetUniqueKey()); inviteRawToken = PasswordTokenHelper.GenerateRawToken(); newUser.PasswordTokenHash = PasswordTokenHelper.Hash(inviteRawToken); newUser.PasswordTokenExpiresAt = PasswordTokenHelper.DefaultExpiry(); } else { newUser.Password = _profileLogic.HashPassword(newUserDTO.password); } _myInfoMateDbContext.Add(newUser); _myInfoMateDbContext.SaveChanges(); if (inviteRawToken != null) { var managerAppUrl = _configuration["AppUrls:ManagerApp"]; await _emailService.SendUserInvitationEmailAsync(newUser.Email, $"{managerAppUrl}/set-password?token={inviteRawToken}"); } return new OkObjectResult(newUser.ToDTO()); } catch (ArgumentNullException ex) { return new BadRequestObjectResult(ex.Message) {}; } catch (UnauthorizedAccessException ex) { return new ObjectResult(ex.Message) { StatusCode = 403 }; } catch (InvalidOperationException ex) { return new ConflictObjectResult(ex.Message) {}; } catch (Exception ex) { return new ObjectResult(ex.Message) { StatusCode = 500 }; } } /// /// Update an user /// /// User to update [ProducesResponseType(typeof(UserDetailDTO), 200)] [ProducesResponseType(typeof(string), 400)] [ProducesResponseType(typeof(string), 404)] [ProducesResponseType(typeof(string), 500)] [HttpPut] public ObjectResult UpdateUser([FromBody] UserDetailDTO updatedUser) { try { if (updatedUser == null) throw new ArgumentNullException("User param is null"); User user = _myInfoMateDbContext.Users.FirstOrDefault(u => u.Id == updatedUser.id); if (user == null || (!IsSuperAdmin() && user.InstanceId != GetCallerInstanceId())) throw new KeyNotFoundException("User does not exist"); if (!IsSuperAdmin() && user.Role < GetCallerRole()) throw new UnauthorizedAccessException("Cannot modify a user with a higher role than your own"); user.FirstName = updatedUser.firstName; user.LastName = updatedUser.lastName; if (updatedUser.role.HasValue) { if (updatedUser.role.Value < GetCallerRole()) throw new UnauthorizedAccessException("Cannot assign a role higher than your own"); user.Role = updatedUser.role.Value; } _myInfoMateDbContext.SaveChanges(); return new OkObjectResult(user.ToDTO()); } catch (ArgumentNullException ex) { return new BadRequestObjectResult(ex.Message) {}; } catch (UnauthorizedAccessException ex) { return new ObjectResult(ex.Message) { StatusCode = 403 }; } catch (KeyNotFoundException ex) { return new NotFoundObjectResult(ex.Message) {}; } catch (Exception ex) { return new ObjectResult(ex.Message) { StatusCode = 500 }; } } /// /// Delete an user /// /// Id of user to delete [ProducesResponseType(typeof(string), 202)] [ProducesResponseType(typeof(string), 400)] [ProducesResponseType(typeof(string), 404)] [ProducesResponseType(typeof(string), 500)] [HttpDelete("{id}")] public ObjectResult DeleteUser(string id) { try { if (id == null) throw new ArgumentNullException("User param is null"); User user = _myInfoMateDbContext.Users.FirstOrDefault(u => u.Id == id); if (user == null || (!IsSuperAdmin() && user.InstanceId != GetCallerInstanceId())) throw new KeyNotFoundException("User does not exist"); if (!IsSuperAdmin() && user.Role < GetCallerRole()) throw new UnauthorizedAccessException("Cannot delete a user with a higher role than your own"); _myInfoMateDbContext.Remove(user); _myInfoMateDbContext.SaveChanges(); return new ObjectResult("The user has been deleted") { StatusCode = 202 }; } catch (ArgumentNullException ex) { return new BadRequestObjectResult(ex.Message) { }; } catch (UnauthorizedAccessException ex) { return new ObjectResult(ex.Message) { StatusCode = 403 }; } catch (KeyNotFoundException ex) { return new NotFoundObjectResult(ex.Message) { }; } catch (Exception ex) { return new ObjectResult(ex.Message) { StatusCode = 500 }; } } } }