using Microsoft.EntityFrameworkCore; public class UserService : IUserService { private readonly AppDbContext _db; private readonly ICurrentUserService _currentUser; public UserService(AppDbContext db, ICurrentUserService currentUser) { _db = db; _currentUser = currentUser; } public async Task> ListAsync() { var users = await _db.ClinicalUsers .AsNoTracking() .OrderBy(u => u.Username) .ToListAsync(); return users.Select(Map).ToList(); } public async Task CreateAsync(CreateUserRequest req) { ValidateUsername(req.Username); ValidatePassword(req.Password); ValidateDisplayName(req.DisplayName); var role = ParseRole(req.Role); var exists = await _db.ClinicalUsers.AnyAsync(u => u.Username == req.Username); if (exists) throw new ConflictException( $"Username '{req.Username}' is already taken.", "USERNAME_CONFLICT"); var user = new ClinicalUser { Id = Guid.NewGuid(), Username = req.Username.Trim(), PasswordHash = BCrypt.Net.BCrypt.HashPassword(req.Password), DisplayName = req.DisplayName.Trim(), Role = role, IsActive = true, CreatedAt = DateTimeOffset.UtcNow, }; _db.ClinicalUsers.Add(user); await _db.SaveChangesAsync(); return Map(user); } public async Task UpdateAsync(Guid id, UpdateUserRequest req) { var user = await _db.ClinicalUsers.FindAsync(id); if (user is null) throw new NotFoundException("User not found.", "USER_NOT_FOUND"); if (req.DisplayName is not null) { ValidateDisplayName(req.DisplayName); user.DisplayName = req.DisplayName.Trim(); } if (req.Role is not null) user.Role = ParseRole(req.Role); if (req.IsActive is not null) { if (!req.IsActive.Value && _currentUser.UserId == id) throw new ValidationException( "You cannot deactivate your own account.", "SELF_DEACTIVATION_DENIED"); user.IsActive = req.IsActive.Value; } await _db.SaveChangesAsync(); return Map(user); } private static ClinicalUserResponse Map(ClinicalUser u) => new(u.Id, u.Username, u.DisplayName, u.Role.ToDbString(), u.IsActive, u.CreatedAt, u.LastLoginAt); private static void ValidateUsername(string username) { if (string.IsNullOrWhiteSpace(username)) throw new ValidationException("Username is required.", "USERNAME_REQUIRED"); if (username.Trim().Length > 100) throw new ValidationException("Username must be 100 characters or fewer.", "USERNAME_TOO_LONG"); } private static void ValidatePassword(string password) { if (string.IsNullOrWhiteSpace(password)) throw new ValidationException("Password is required.", "PASSWORD_REQUIRED"); if (password.Length < 8) throw new ValidationException("Password must be at least 8 characters.", "PASSWORD_TOO_SHORT"); } private static void ValidateDisplayName(string displayName) { if (string.IsNullOrWhiteSpace(displayName)) throw new ValidationException("Display name is required.", "DISPLAY_NAME_REQUIRED"); if (displayName.Trim().Length > 200) throw new ValidationException("Display name must be 200 characters or fewer.", "DISPLAY_NAME_TOO_LONG"); } private static ClinicalRole ParseRole(string role) { if (string.IsNullOrWhiteSpace(role)) throw new ValidationException("Role is required.", "ROLE_REQUIRED"); try { return ClinicalRoleExtensions.FromDbString(role.Trim().ToUpperInvariant()); } catch (ArgumentOutOfRangeException) { throw new ValidationException( "Role must be one of: NURSE, PHYSICIAN, ADMIN, INTEGRATION.", "INVALID_ROLE"); } } }