Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/0Crazy-0/ClinicFlow/llms.txt

Use this file to discover all available pages before exploring further.

Authentication in ClinicFlow is handled entirely in the application layer. Commands validate credentials, enforce lockout rules, and manage tokens — but JWT issuance is deliberately not part of this layer. LoginUserCommand returns only the authenticated user’s Guid; the presentation layer (e.g., an API controller or minimal-API endpoint) is responsible for generating and signing the JWT using that Id. This separation keeps the core logic free of infrastructure concerns.
Phone verification (SendPhoneVerificationCommand + VerifyPhoneCommand) must be completed before a patient can schedule appointments. The IsPhoneVerified flag on UserDto reflects the current state; the ScheduleByPatient handler checks it at scheduling time.

LoginUserCommand

Authenticates a user by email and password, enforces lockout rules, and returns the user’s Id for JWT issuance. Returns: IRequest<Guid> — the authenticated user’s Id.
public sealed record LoginUserCommand(string Email, string Password) : IRequest<Guid>;
Email
string
required
Registered email address. Must be a valid email format and within EmailAddress.MaximumLength.
Password
string
required
Plain-text password. The handler verifies it against the stored hash via IPasswordHasherService.
Handler behaviour:
  1. Looks up the user by email via IUserRepository.GetByEmailAsync. Throws BusinessRuleValidationException (DomainErrors.User.InvalidCredentials) if not found.
  2. Calls IPasswordHasherService.Verify(password, user.PasswordHash) to validate the credential.
  3. Invokes UserAuthenticationService.TryAuthenticate(user, isValid, utcNow):
    • On success: calls user.RecordLogin(loginTime) — clears failed attempts and lockout, updates LastLoginAt. Returns true.
    • On failure: calls user.RecordFailedLogin(referenceTime) — increments FailedLoginAttempts; if >= 5, sets LockoutEnd = now + 15 minutes. Returns false.
  4. Saves changes and then throws BusinessRuleValidationException (DomainErrors.User.InvalidCredentials) if TryAuthenticate returned false, so the lockout update is still persisted.
The handler always throws DomainErrors.User.InvalidCredentials for both “user not found” and “wrong password” cases to prevent email enumeration. Never distinguish these two cases in the presentation layer either.
Returns the authenticated user’s Guid Id. The presentation layer should use this Id to look up roles and issue a signed JWT.

LogoutUserCommand

Revokes all refresh tokens for a user, effectively ending their session. Returns: IRequest (no return value)
public sealed record LogoutUserCommand(Guid UserId) : IRequest;
UserId
Guid
required
The Id of the authenticated user to log out. Must be a non-empty Guid.
Handler behaviour: Delegates to IRefreshTokenService.RevokeAsync(userId). This revokes all active refresh tokens for the user. Short-lived access tokens remain valid until their natural expiry — enforce short TTLs (e.g., 15 minutes) at the presentation layer.

Phone Verification Commands

SendPhoneVerificationCommand

Dispatches an OTP code to the user’s registered phone number via IPhoneVerificationService. Returns: IRequest (no return value)
public sealed record SendPhoneVerificationCommand(Guid UserId) : IRequest;
UserId
Guid
required
Id of the user whose phone number should receive the OTP. Throws EntityNotFoundException if not found.
Handler behaviour: Loads the user, then calls IPhoneVerificationService.SendVerificationCodeAsync(user.PhoneNumber). The service implementation (infrastructure) is responsible for rate-limiting and delivery.

VerifyPhoneCommand

Validates the OTP entered by the user and marks the phone number as verified. Returns: IRequest (no return value)
public sealed record VerifyPhoneCommand(Guid UserId, string Code) : IRequest;
UserId
Guid
required
Id of the user attempting verification.
Code
string
required
The OTP code received by the user.
Handler behaviour:
  1. Loads the user or throws EntityNotFoundException.
  2. Calls IPhoneVerificationService.VerifyCodeAsync(user.PhoneNumber, code) — returns bool.
  3. Calls user.MarkPhoneAsVerified(isValid):
    • Throws DomainValidationException (DomainErrors.User.InvalidVerificationCode) if isValid is false.
    • Throws DomainValidationException (DomainErrors.User.PhoneAlreadyVerified) if the phone was already verified.
    • Sets IsPhoneVerified = true on success.
  4. Persists via IUnitOfWork.

ChangePasswordCommand

Allows an authenticated user to change their own password by supplying the current password for verification. Returns: IRequest (no return value)
public sealed record ChangePasswordCommand(Guid UserId, string CurrentPassword, string NewPassword)
    : IRequest;
UserId
Guid
required
Id of the authenticated user.
CurrentPassword
string
required
The user’s existing password in plain text for verification.
NewPassword
string
required
The new password to set. The validator enforces minimum length of 8 characters.
Handler behaviour:
  1. Fetches the user or throws EntityNotFoundException.
  2. Verifies CurrentPassword against user.PasswordHash via IPasswordHasherService.Verify. Throws BusinessRuleValidationException (DomainErrors.User.InvalidCredentials) on mismatch.
  3. Hashes NewPassword and calls user.ChangePassword(newHash), which also resets FailedLoginAttempts = 0 and clears LockoutEnd.
  4. Persists via IUnitOfWork.

RequestPasswordResetCommand

Initiates the forgot-password flow by generating a signed reset token and sending it to the user’s email address. Returns: IRequest (no return value)
public sealed record RequestPasswordResetCommand(string Email) : IRequest;
Email
string
required
The email address associated with the account. Must be a valid email format.
Handler behaviour:
  1. Looks up the user by email.
  2. If the email does not exist, the handler silently returns without error. This prevents email enumeration attacks.
  3. If found, calls IPasswordResetTokenService.GenerateTokenAsync(userId) to produce a time-limited token.
  4. Calls IEmailService.SendPasswordResetEmailAsync(email, token) to deliver the reset link.
The silent no-op on unknown emails is intentional and documented in the handler source. Always respond with a generic “if that email is registered, you’ll receive a link” message in your UI.

ResetPasswordCommand

Completes the password reset flow by validating the token and setting a new password. Returns: IRequest (no return value)
public sealed record ResetPasswordCommand(string Token, string NewPassword) : IRequest;
Token
string
required
The opaque reset token delivered via email. The service validates its signature and expiry.
NewPassword
string
required
The new password to set. Minimum 8 characters enforced by the validator.
Handler behaviour:
  1. Calls IPasswordResetTokenService.ValidateTokenAsync(token) — returns the associated userId?. Throws DomainValidationException (DomainErrors.Validation.InvalidValue) if the token is invalid or expired.
  2. Fetches the user by userId or throws EntityNotFoundException.
  3. Hashes NewPassword and calls user.ChangePassword(newHash), resetting FailedLoginAttempts and LockoutEnd.
  4. Persists via IUnitOfWork.

Authentication Flow Summary

Client                  Presentation Layer          Application Layer
  |                           |                           |
  |-- LoginUserCommand ------->|                           |
  |                           |-- IMediator.Send() ------->|
  |                           |                           | (validate credentials,
  |                           |                           |  enforce lockout,
  |                           |                           |  record login)
  |                           |<-- returns Guid userId ----|
  |                           |                           |
  |                           | (fetch user roles, issue JWT)
  |<-- JWT token -------------|
The application layer never touches JWT signing keys. The presentation layer receives the Guid, loads any additional claims (role, etc.), and calls its JWT provider.

Build docs developers (and LLMs) love