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.

Every principal in ClinicFlow — whether a patient booking appointments or an administrator managing the clinic — is modelled as a User entity. The domain handles password hashing, tracks failed login attempts, enforces a 15-minute lockout after five consecutive failures, and provides a complete password reset flow using a secure token delivered by email.

The User Entity

public class User : BaseEntity
{
    public const int MaxFailedLoginAttempts = 5;
    private static readonly TimeSpan LockoutDuration = TimeSpan.FromMinutes(15);

    public UserRole  Role               { get; private set; }
    public EmailAddress Email           { get; private set; }
    public string    PasswordHash       { get; private set; }
    public PhoneNumber PhoneNumber      { get; private set; }
    public bool      IsActive           { get; private set; }
    public bool      IsPhoneVerified    { get; private set; }
    public int       FailedLoginAttempts { get; private set; }
    public DateTime? LockoutEnd         { get; private set; }
    public DateTime? LastLoginAt        { get; private set; }
}
FieldTypeDefaultNotes
EmailEmailAddressValue object; unique per user
PasswordHashstringStored hash; never the raw password
PhoneNumberPhoneNumberValue object; used for SMS verification
RoleUserRoleset at registrationImmutable after creation
IsActivebooltrueSet to false by DeactivateUser
IsPhoneVerifiedboolfalseFlipped by VerifyPhone
FailedLoginAttemptsint0Incremented on each failed login
LockoutEndDateTime?nullSet to now + 15 min after 5 failures
LastLoginAtDateTime?nullUpdated on every successful login

UserRole Enum

public enum UserRole
{
    /// <summary>A patient who can book and manage their own appointments.</summary>
    Patient = 1,

    Doctor = 2,

    /// <summary>Front-desk staff responsible for scheduling and administrative tasks.</summary>
    Receptionist = 3,

    /// <summary>System administrator with full access to all operations.</summary>
    Admin = 4,
}
Each registration command is role-scoped — there is no single “register with role” command to prevent privilege escalation.

Account Lockout

User.RecordFailedLogin increments FailedLoginAttempts. When the count reaches MaxFailedLoginAttempts (5), LockoutEnd is set to the current UTC time plus LockoutDuration (15 minutes).
public void RecordFailedLogin(DateTime referenceTime)
{
    FailedLoginAttempts++;

    if (FailedLoginAttempts >= MaxFailedLoginAttempts)
        LockoutEnd = referenceTime.Add(LockoutDuration); // +15 minutes
}
User.RecordLogin resets both FailedLoginAttempts and LockoutEnd to their initial values on a successful authentication. Calling ChangePassword also clears the lockout, so a successful password reset always unblocks a locked-out account.
After 5 consecutive failed login attempts, the account is automatically locked for 15 minutes. During this window every login attempt — even with the correct password — is rejected with AccountLockedOut. The lockout cannot be manually shortened; use ReactivateUser only if the account was also deactivated, or wait for the lockout window to expire.

Registration Commands

ClinicFlow exposes four distinct registration commands, one per role. All share the same fields but are isolated so that role-specific authorization policies can be applied at the presentation layer.

RegisterUser

Self-registration for patients.
public sealed record RegisterUserCommand(
    string Email,
    string Password,
    string PhoneNumber
) : IRequest<Guid>;
Assigns UserRole.Patient. Checks for duplicate email before creating the user.

RegisterDoctorUser

Creates a user account for a physician.
public sealed record RegisterDoctorUserCommand(
    string Email,
    string Password,
    string PhoneNumber
) : IRequest<Guid>;
Assigns UserRole.Doctor. After registration, create a linked Doctor profile with CreateDoctorProfile.

RegisterReceptionistUser

Registers a front-desk receptionist.
public sealed record RegisterReceptionistUserCommand(
    string Email,
    string Password,
    string PhoneNumber
) : IRequest<Guid>;
Assigns UserRole.Receptionist.

RegisterAdminUser

Registers a system administrator.
public sealed record RegisterAdminUserCommand(
    string Email,
    string Password,
    string PhoneNumber
) : IRequest<Guid>;
Assigns UserRole.Admin. Should be protected by an existing admin authorization policy in the presentation layer.
All handlers hash the raw password via IPasswordHasherService.Hash before calling User.Create, so the plaintext password is never stored.

Authentication

LoginUser

public sealed record LoginUserCommand(string Email, string Password) : IRequest<Guid>;
Returns: Guid — the authenticated User.Id. The handler delegates to UserAuthenticationService.TryAuthenticate, which calls either User.RecordLogin or User.RecordFailedLogin based on the result of IPasswordHasherService.Verify. The FailedLoginAttempts counter (and any lockout) is persisted immediately via SaveChangesAsync even when authentication fails — ensuring the lockout state is durable across restarts.
public static class UserAuthenticationService
{
    public static bool TryAuthenticate(User user, bool isPasswordValid, DateTime loginTime)
    {
        if (!isPasswordValid)
        {
            user.RecordFailedLogin(loginTime);
            return false;
        }

        user.RecordLogin(loginTime);
        return true;
    }
}
LoginUser returns only the user’s Guid. JWT generation, access token signing, and refresh token issuance are not part of the Application layer — they are expected to be handled by the presentation (API) layer after a successful login. The IRefreshTokenService interface (see below) is provided so the application can revoke tokens when needed (e.g., on logout or password change), without the domain depending on JWT infrastructure.

IRefreshTokenService

public interface IRefreshTokenService
{
    /// <summary>Revokes all active refresh tokens for the specified user.</summary>
    Task RevokeAsync(Guid userId, CancellationToken cancellationToken = default);
}
Implementations should be provided by the Infrastructure / Presentation layer. LogoutUser uses this interface to invalidate the user’s session.

Phone Verification

Phone verification is a two-step process orchestrated through IPhoneVerificationService — a domain interface (in ClinicFlow.Domain.Interfaces.Services) implemented in the Infrastructure layer.
1

Send verification code

Dispatches an SMS to the phone number on file.
public sealed record SendPhoneVerificationCommand(Guid UserId) : IRequest;
The handler loads the user, reads User.PhoneNumber, and calls IPhoneVerificationService.SendVerificationCodeAsync(user.PhoneNumber, cancellationToken).
2

Verify code

Submits the code received via SMS.
public sealed record VerifyPhoneCommand(Guid UserId, string Code) : IRequest;
The handler calls IPhoneVerificationService.VerifyCodeAsync(user.PhoneNumber, code, cancellationToken), passes the boolean result into User.MarkPhoneAsVerified(isVerificationCodeValid), and saves. Throws InvalidVerificationCode if the code is wrong and PhoneAlreadyVerified if the number is already verified.

IPhoneVerificationService

public interface IPhoneVerificationService
{
    Task SendVerificationCodeAsync(
        PhoneNumber phoneNumber,
        CancellationToken cancellationToken = default);

    Task<bool> VerifyCodeAsync(
        PhoneNumber phoneNumber,
        string code,
        CancellationToken cancellationToken = default);
}

Password Management

ChangePassword

Requires knowledge of the current password. On success, clears FailedLoginAttempts and LockoutEnd.
public sealed record ChangePasswordCommand(
    Guid   UserId,
    string CurrentPassword,
    string NewPassword
) : IRequest;
The handler verifies the current password hash before calling User.ChangePassword(newHash). Submitting an incorrect CurrentPassword throws InvalidCredentials.

Password Reset Flow

The reset flow is designed to prevent email enumeration: RequestPasswordReset silently succeeds even when the email does not exist.
1

RequestPasswordReset

Generates a secure time-limited token and emails it to the user.
public sealed record RequestPasswordResetCommand(string Email) : IRequest;
Handler: loads user by email (returns early if not found), calls IPasswordResetTokenService.GenerateTokenAsync, then IEmailService.SendPasswordResetEmailAsync.
2

ResetPassword

Validates the token and sets a new password.
public sealed record ResetPasswordCommand(string Token, string NewPassword) : IRequest;
Handler: calls IPasswordResetTokenService.ValidateTokenAsync to resolve the UserId (throws InvalidValue if the token is expired or invalid), hashes the new password, and calls User.ChangePassword — which also clears any active lockout.

Supporting Interfaces

public interface IPasswordResetTokenService
{
    /// <summary>Generates a secure, temporary reset token for the user.</summary>
    Task<string> GenerateTokenAsync(Guid userId, CancellationToken cancellationToken = default);

    /// <summary>
    /// Validates the token and returns the associated user ID,
    /// or null if invalid / expired.
    /// </summary>
    Task<Guid?> ValidateTokenAsync(string token, CancellationToken cancellationToken = default);
}

public interface IEmailService
{
    /// <summary>Sends the password reset link to the specified email address.</summary>
    Task SendPasswordResetEmailAsync(
        string email,
        string resetToken,
        CancellationToken cancellationToken = default);
}

Deactivation and Reactivation

public sealed record DeactivateUserCommand(Guid UserId) : IRequest;
public sealed record ReactivateUserCommand(Guid UserId) : IRequest;
DeactivateUser sets IsActive = false; ReactivateUser sets it back to true and resets FailedLoginAttempts and LockoutEnd. A deactivated user cannot log in — User.RecordLogin throws AccountInactive regardless of the password.

ICurrentUserService

The presentation layer must provide an implementation of ICurrentUserService so that application-layer handlers can resolve the identity of the caller without depending on HTTP context directly.
public interface ICurrentUserService
{
    Guid     Id              { get; }
    string   Email           { get; }
    UserRole Role            { get; }
    bool     IsAuthenticated { get; }
}

Queries

GetUsers

Paginated, filterable list of all users — intended for admin dashboards.
public sealed record GetUsersQuery(
    int       PageNumber,
    int       PageSize,
    UserRole? Role,
    bool?     IsActive,
    string?   SearchTerm
) : IRequest<PaginatedList<UserDto>>;
SearchTerm is matched against Email and PhoneNumber. All filter parameters are optional.

GetLockedOutUsers

Returns all users whose LockoutEnd is in the future — useful for support staff monitoring repeated login failures.
public sealed record GetLockedOutUsersQuery(int PageNumber, int PageSize)
    : IRequest<PaginatedList<UserDto>>;

UserDto

public sealed record UserDto(
    Guid      Id,
    string    Email,
    string    PhoneNumber,
    UserRole  Role,
    bool      IsActive,
    bool      IsPhoneVerified,
    DateTime? LastLoginAt,
    int       FailedLoginAttempts,
    DateTime? LockoutEnd
);
LockoutEnd is null for users who are not currently locked out. A non-null value greater than the current UTC time means the account is actively locked.

Build docs developers (and LLMs) love