feature: Degraded Operations Visibility
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user