mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-14 17:54:42 +00:00
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> for assistance with diagram creation and documentation prose updates.
166 lines
7.8 KiB
Plaintext
166 lines
7.8 KiB
Plaintext
sequenceDiagram
|
|
actor User
|
|
participant API as API Controller
|
|
participant Mediator as MediatR pipeline<br/>(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<br/>(IUserStore/IUserPasswordStore/IUserEmailStore)
|
|
participant ProfileHandler as CreateUserProfileHandler<br/>(Profile slice)
|
|
participant EmailDispatcher as EmailDispatcher<br/>(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
|