Files

122 lines
4.2 KiB
C#

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<List<ClinicalUserResponse>> ListAsync()
{
var users = await _db.ClinicalUsers
.AsNoTracking()
.OrderBy(u => u.Username)
.ToListAsync();
return users.Select(Map).ToList();
}
public async Task<ClinicalUserResponse> 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<ClinicalUserResponse> 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");
}
}
}