feature: No user management endpoints (create, update, deactivate)

This commit is contained in:
voltsrage
2026-06-27 15:45:01 +08:00
parent 5a2e95c984
commit 299e6e1453
13 changed files with 684 additions and 4 deletions
@@ -0,0 +1,235 @@
using FluentValidation;
public class LoginRequestValidator : AbstractValidator<LoginRequest>
{
public LoginRequestValidator()
{
RuleFor(x => x.Username).NotEmpty();
RuleFor(x => x.Password).NotEmpty();
}
}
public class RefreshRequestValidator : AbstractValidator<RefreshRequest>
{
public RefreshRequestValidator()
{
RuleFor(x => x.RefreshToken).NotEmpty();
}
}
public class LogoutRequestValidator : AbstractValidator<LogoutRequest>
{
public LogoutRequestValidator()
{
RuleFor(x => x.RefreshToken).NotEmpty();
}
}
public class UpsertDraftPatientRequestValidator : AbstractValidator<UpsertDraftPatientRequest>
{
public UpsertDraftPatientRequestValidator()
{
RuleFor(x => x.FullName).MaximumLength(200).When(x => x.FullName is not null);
RuleFor(x => x.DateOfBirth).LessThanOrEqualTo(DateOnly.FromDateTime(DateTime.UtcNow))
.WithMessage("Date of birth cannot be in the future.")
.When(x => x.DateOfBirth.HasValue);
RuleFor(x => x.Sex)
.Must(s => IsAllowedSex(s))
.WithMessage("Sex must be 'male', 'female', 'other', or 'unknown'.")
.When(x => x.Sex is not null);
}
private static bool IsAllowedSex(string? sex)
{
if (sex is null) return true;
return sex.Trim().ToLowerInvariant() switch
{
"m" or "male" or "f" or "female" or "other" or "unknown" => true,
_ => false
};
}
}
public class UpsertDraftEncounterRequestValidator : AbstractValidator<UpsertDraftEncounterRequest>
{
public UpsertDraftEncounterRequestValidator()
{
RuleFor(x => x.AdmissionDate).LessThanOrEqualTo(DateTimeOffset.UtcNow)
.WithMessage("Admission date cannot be in the future.")
.When(x => x.AdmissionDate.HasValue);
RuleFor(x => x.RoomBed).MaximumLength(50).When(x => x.RoomBed is not null);
RuleFor(x => x.AdmissionReason).MaximumLength(500).When(x => x.AdmissionReason is not null);
RuleFor(x => x.DischargeDiagnosis).MaximumLength(500).When(x => x.DischargeDiagnosis is not null);
}
}
public class CreateDraftObservationRequestValidator : AbstractValidator<CreateDraftObservationRequest>
{
public CreateDraftObservationRequestValidator()
{
RuleFor(x => x.ObservationCode).NotEmpty();
RuleFor(x => x.Unit).NotEmpty();
RuleFor(x => x.RecordedAt).NotEmpty()
.LessThanOrEqualTo(DateTimeOffset.UtcNow.AddMinutes(5))
.WithMessage("RecordedAt cannot be in the future.");
}
}
public class UpdateDraftObservationRequestValidator : AbstractValidator<UpdateDraftObservationRequest>
{
public UpdateDraftObservationRequestValidator()
{
RuleFor(x => x.ObservationCode).NotEmpty();
RuleFor(x => x.Unit).NotEmpty();
RuleFor(x => x.RecordedAt).NotEmpty()
.LessThanOrEqualTo(DateTimeOffset.UtcNow.AddMinutes(5))
.WithMessage("RecordedAt cannot be in the future.");
}
}
public class VerifyBatchRequestValidator : AbstractValidator<VerifyBatchRequest>
{
private static readonly HashSet<string> _validStatuses = new() { "ok", "warning", "error" };
public VerifyBatchRequestValidator()
{
RuleFor(x => x.FieldChecks).NotEmpty()
.WithMessage("At least one field check is required.");
RuleForEach(x => x.FieldChecks).ChildRules(check =>
{
check.RuleFor(c => c.FieldName).NotEmpty();
check.RuleFor(c => c.Status).NotEmpty()
.Must(s => _validStatuses.Contains(s))
.WithMessage("Status must be 'ok', 'warning', or 'error'.");
});
}
}
public class RejectBatchRequestValidator : AbstractValidator<RejectBatchRequest>
{
public RejectBatchRequestValidator()
{
RuleFor(x => x.Reason)
.NotEmpty()
.WithErrorCode("REJECTION_REASON_REQUIRED")
.WithMessage("Rejection reason is required.");
RuleFor(x => x.Reason)
.MinimumLength(10)
.When(x => !string.IsNullOrEmpty(x.Reason))
.WithErrorCode("REJECTION_REASON_TOO_SHORT")
.WithMessage("Rejection reason must be at least 10 characters.");
}
}
public class RecordObservationsRequestValidator : AbstractValidator<RecordObservationsRequest>
{
public RecordObservationsRequestValidator()
{
RuleFor(x => x.Observations)
.NotEmpty()
.WithErrorCode("EMPTY_OBSERVATIONS")
.WithMessage("At least one observation is required.")
.Must(o => o.Count <= 10)
.WithMessage("Maximum 10 observations per request.");
RuleFor(x => x.ClinicianAttestation)
.Equal(true)
.WithErrorCode("ATTESTATION_REQUIRED")
.WithMessage("Clinician attestation is required.");
RuleFor(x => x.PasswordConfirm).NotEmpty();
RuleForEach(x => x.Observations).SetValidator(new LiveCaptureObservationRequestValidator());
}
}
public class OpenEncounterWithVitalsRequestValidator : AbstractValidator<OpenEncounterWithVitalsRequest>
{
public OpenEncounterWithVitalsRequestValidator()
{
RuleFor(x => x.PatientId).NotEmpty();
RuleFor(x => x.Department).NotEmpty();
RuleFor(x => x.AdmissionReason).NotEmpty();
RuleFor(x => x.Observations)
.NotEmpty()
.WithErrorCode("EMPTY_OBSERVATIONS")
.WithMessage("At least one observation is required.")
.Must(o => o.Count <= 10)
.WithMessage("Maximum 10 observations per request.");
RuleFor(x => x.ClinicianAttestation)
.Equal(true)
.WithErrorCode("ATTESTATION_REQUIRED")
.WithMessage("Clinician attestation is required.");
RuleFor(x => x.PasswordConfirm).NotEmpty();
RuleForEach(x => x.Observations).SetValidator(new LiveCaptureObservationRequestValidator());
}
}
public class LiveCaptureObservationRequestValidator : AbstractValidator<LiveCaptureObservationRequest>
{
public LiveCaptureObservationRequestValidator()
{
RuleFor(x => x.ObservationCode).NotEmpty();
RuleFor(x => x.Unit).NotEmpty();
RuleFor(x => x.RecordedAt).NotEmpty();
}
}
public class CreateUserRequestValidator : AbstractValidator<CreateUserRequest>
{
private static readonly HashSet<string> _validRoles = new()
{
"INTAKE_CLERK", "DATA_ENTRY_CLERK", "VERIFIER",
"CLINICAL_APPROVER", "CLINICIAN", "ADMINISTRATOR"
};
public CreateUserRequestValidator()
{
RuleFor(x => x.Username).NotEmpty().MaximumLength(50);
RuleFor(x => x.Password).NotEmpty().MinimumLength(8)
.Matches("[A-Z]").WithMessage("Password must contain at least one uppercase letter.")
.Matches("[0-9]").WithMessage("Password must contain at least one digit.");
RuleFor(x => x.FullName).NotEmpty().MaximumLength(200);
RuleFor(x => x.Role).NotEmpty()
.Must(r => _validRoles.Contains(r))
.WithMessage("Role must be one of: INTAKE_CLERK, DATA_ENTRY_CLERK, VERIFIER, CLINICAL_APPROVER, CLINICIAN, ADMINISTRATOR.");
}
}
public class UpdateUserRequestValidator : AbstractValidator<UpdateUserRequest>
{
private static readonly HashSet<string> _validRoles = new()
{
"INTAKE_CLERK", "DATA_ENTRY_CLERK", "VERIFIER",
"CLINICAL_APPROVER", "CLINICIAN", "ADMINISTRATOR"
};
public UpdateUserRequestValidator()
{
RuleFor(x => x.FullName).MaximumLength(200).When(x => x.FullName is not null);
RuleFor(x => x.Role)
.Must(r => _validRoles.Contains(r!))
.WithMessage("Role must be one of: INTAKE_CLERK, DATA_ENTRY_CLERK, VERIFIER, CLINICAL_APPROVER, CLINICIAN, ADMINISTRATOR.")
.When(x => x.Role is not null);
}
}
public class ResetPasswordRequestValidator : AbstractValidator<ResetPasswordRequest>
{
public ResetPasswordRequestValidator()
{
RuleFor(x => x.NewPassword).NotEmpty().MinimumLength(8)
.Matches("[A-Z]").WithMessage("Password must contain at least one uppercase letter.")
.Matches("[0-9]").WithMessage("Password must contain at least one digit.");
}
}
public class ChangePasswordRequestValidator : AbstractValidator<ChangePasswordRequest>
{
public ChangePasswordRequestValidator()
{
RuleFor(x => x.CurrentPassword).NotEmpty();
RuleFor(x => x.NewPassword).NotEmpty().MinimumLength(8)
.Matches("[A-Z]").WithMessage("Password must contain at least one uppercase letter.")
.Matches("[0-9]").WithMessage("Password must contain at least one digit.");
}
}
@@ -0,0 +1,41 @@
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
public class ValidationFilter : IAsyncActionFilter
{
private readonly IServiceProvider _serviceProvider;
public ValidationFilter(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider;
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
foreach (var argument in context.ActionArguments.Values)
{
if (argument is null) continue;
var argumentType = argument.GetType();
var validatorType = typeof(IValidator<>).MakeGenericType(argumentType);
var validator = _serviceProvider.GetService(validatorType) as IValidator;
if (validator is null) continue;
var validationContext = new ValidationContext<object>(argument);
var result = await validator.ValidateAsync(validationContext);
if (!result.IsValid)
{
var errors = string.Join("; ", result.Errors.Select(e => e.ErrorMessage));
var errorCode = result.Errors
.Select(e => e.ErrorCode)
.FirstOrDefault(code => !string.IsNullOrWhiteSpace(code))
?? "VALIDATION_ERROR";
context.Result = new UnprocessableEntityObjectResult(
ApiResponse<object>.Fail(422, errors, errorCode));
return;
}
}
await next();
}
}