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