sequenceDiagram
actor User
participant API as API Controller
participant Mediator as MediatR pipeline
(ValidationBehavior)
box Handler Layer
participant RegHandler as RegisterUserHandler
participant UserMgr as UserManager(ApplicationUser)
participant TokenSvc as TokenService
end
box Infrastructure Layer
participant Argon2 as Argon2PasswordHasher
participant JWT as JWT Infrastructure
participant SMTP as Email Provider
participant Template as Template Provider
end
box Repository / Cross-slice
participant UserStore as DapperUserStore
(IUserStore/IUserPasswordStore/IUserEmailStore)
participant ProfileHandler as CreateUserProfileHandler
(Profile slice)
participant EmailDispatcher as EmailDispatcher
(Features.Emails)
end
participant DB as SQL Server
User->>API: POST /api/auth/register {username, firstName, lastName, email, dateOfBirth, password}
activate API
API->>Mediator: Send(RegisterUserCommand)
activate Mediator
Note right of Mediator: ValidationBehavior runs RegisterUserValidator — username 3-64 chars alphanumeric+[._-], valid email ≤128 chars, password min 8 chars with upper/lower/number/special, DateOfBirth 19+ years old
alt Validation fails
Mediator->>API: throws FluentValidation.ValidationException
API->>User: 400 Bad Request {message, errors}
else Validation succeeds
Mediator->>RegHandler: Handle(command)
activate RegHandler
RegHandler->>RegHandler: user = new UserAccount{...}.ToApplicationUser()
RegHandler->>UserMgr: CreateAsync(user, password)
activate UserMgr
UserMgr->>Argon2: HashPassword(user, password)
activate Argon2
Note right of Argon2: Argon2id — salt 16 bytes, memory 64MB, iterations 4, parallelism 2 (fixed, to avoid thread-pool exhaustion), hash output 32 bytes
Argon2-->>UserMgr: PHC string in Argon2id format (algorithm, version, memory/time/parallelism params, salt, hash)
deactivate Argon2
UserMgr->>UserStore: SetPasswordHashAsync(user, hash)
Note right of UserStore: user.Id is still empty here — only sets the in-memory hash — CreateAsync writes it to UserCredential itself
UserMgr->>UserStore: FindByNameAsync(username)
activate UserStore
UserStore->>DB: SELECT ... FROM UserAccount WHERE Username = @Username
activate DB
DB-->>UserStore: null (user doesn't exist)
deactivate DB
deactivate UserStore
UserMgr->>UserStore: FindByEmailAsync(email)
activate UserStore
UserStore->>DB: SELECT ... FROM UserAccount WHERE Email = @Email
activate DB
DB-->>UserStore: null (email doesn't exist)
deactivate DB
deactivate UserStore
Note right of UserMgr: Default UserManager validators check uniqueness before CreateAsync — DapperUserStore.CreateAsync also guards a genuine race via a caught duplicate-key SqlException (2601/2627)
UserMgr->>UserStore: CreateAsync(user)
activate UserStore
UserStore->>DB: BEGIN TRANSACTION
activate DB
UserStore->>DB: INSERT INTO UserAccount (...) OUTPUT INSERTED.UserAccountID
DB-->>UserStore: UserAccountId (GUID)
UserStore->>DB: INSERT INTO UserCredential (UserAccountId, Hash)
UserStore->>DB: COMMIT TRANSACTION
deactivate DB
Note right of UserStore: Both statements run via Dapper against a single DbTransaction
UserStore-->>UserMgr: IdentityResult.Success (user.Id set)
deactivate UserStore
UserMgr-->>RegHandler: IdentityResult
deactivate UserMgr
alt !result.Succeeded
RegHandler->>Mediator: throw ConflictException (DuplicateUserName/DuplicateEmail → "Username or email already exists", otherwise joined error descriptions)
Mediator->>API: propagate
API->>User: 409 Conflict "Username or email already exists"
else result.Succeeded
RegHandler->>Mediator: Send(CreateUserProfileCommand(user.Id, ""))
activate Mediator
Mediator->>ProfileHandler: Handle(command)
activate ProfileHandler
ProfileHandler->>DB: INSERT INTO UserProfile (UserAccountId, Biography)
activate DB
DB-->>ProfileHandler: UserProfileId
deactivate DB
ProfileHandler-->>Mediator: UserProfileId
deactivate ProfileHandler
Mediator-->>RegHandler: UserProfileId
deactivate Mediator
RegHandler->>TokenSvc: GenerateAccessToken(user)
activate TokenSvc
TokenSvc->>JWT: GenerateJwt(userId, username, expiry)
activate JWT
Note right of JWT: HS256, expires 1 hour, claims sub/unique_name/iat/jti
JWT->>JWT: Create JWT with claims, sign with secret key
JWT-->>TokenSvc: Access Token
deactivate JWT
TokenSvc-->>RegHandler: Access Token
deactivate TokenSvc
RegHandler->>TokenSvc: GenerateRefreshToken(user)
activate TokenSvc
TokenSvc->>JWT: GenerateJwt(userId, username, expiry)
activate JWT
Note right of JWT: Expires 21 days (504h), same structure as access token, signed with a separate secret
JWT-->>TokenSvc: Refresh Token
deactivate JWT
TokenSvc-->>RegHandler: Refresh Token
deactivate TokenSvc
RegHandler->>TokenSvc: GenerateConfirmationToken(user)
activate TokenSvc
TokenSvc->>JWT: GenerateJwt(userId, username, expiry)
activate JWT
Note right of JWT: Expires 30 minutes, signed with a third, separate secret
JWT-->>TokenSvc: Confirmation Token
deactivate JWT
TokenSvc-->>RegHandler: Confirmation Token
deactivate TokenSvc
RegHandler->>Mediator: Send(SendRegistrationEmailCommand) (Shared.Application cross-slice contract)
activate Mediator
Note right of Mediator: Routed to Features.Emails' SendRegistrationEmailHandler — the second of two cross-slice dispatches in this flow (profile creation is the first)
Mediator->>EmailDispatcher: SendRegistrationEmailAsync(firstName, email, confirmationToken)
activate EmailDispatcher
EmailDispatcher->>Template: RenderUserRegisteredEmailAsync(firstName, confirmationLink)
activate Template
Note right of Template: Razor component — header/branding, welcome message, confirmation button, footer
Template->>Template: Render Razor component to HTML
Template-->>EmailDispatcher: HTML email content
deactivate Template
EmailDispatcher->>SMTP: SendAsync(email, subject, body)
activate SMTP
Note right of SMTP: MailKit — host/port from SMTP_HOST/SMTP_PORT, StartTLS when SMTP_USE_SSL=true, optional username/password auth
SMTP->>SMTP: Build MIME message, connect, authenticate, send, disconnect
SMTP-->>EmailDispatcher: Success / throws
deactivate SMTP
deactivate EmailDispatcher
deactivate Mediator
alt Email sent successfully
RegHandler->>RegHandler: emailSent = true
else mediator.Send throws
RegHandler->>RegHandler: catch(Exception), log to stderr, emailSent = false
end
RegHandler-->>Mediator: RegistrationPayload(userAccountId, username, refreshToken, accessToken, confirmationEmailSent)
deactivate RegHandler
Mediator-->>API: RegistrationPayload
deactivate Mediator
API->>User: 201 Created {message: "User registered successfully.", payload: {...}}
end
end
deactivate API