Remove svgs, update puml diagrams -- separate auth diagram into multiple diagrams

This commit is contained in:
Aaron Po
2026-08-31 17:23:33 -04:00
parent 1fe35a2ea6
commit f28f188a54
26 changed files with 1231 additions and 990 deletions

View File

@@ -0,0 +1,109 @@
@startuml confirm-user
!include ../../../pipeline/diagrams/biergarten-weizen-theme.puml
skinparam sequenceMessageAlign center
skinparam maxMessageSize 200
title User Authentication Flow - Confirm User
actor User
participant "API\nController" as API
participant "MediatR pipeline\n(ValidationBehavior)" as Mediator
box "Handler Layer" #F1F3EA
participant "ConfirmUserHandler" as ConfirmHandler
participant "UserManager<ApplicationUser>" as UserMgr
participant "TokenService" as TokenSvc
end box
box "Infrastructure Layer" #EBECE3
participant "JWT\nInfrastructure" as JWT
end box
box "Repository / Cross-slice" #DBEEDD
participant "DapperUserStore\n(IUserStore/IUserPasswordStore/\nIUserEmailStore)" as UserStore
end box
database "SQL Server" as DB
note over UserMgr
ASP.NET Core Identity's UserManager<TUser>
replaces the deleted AuthRepository: handlers
no longer issue SQL or call Argon2 directly,
they delegate to UserManager, which in turn
calls DapperUserStore (persistence) and
Argon2PasswordHasher (IPasswordHasher<TUser>).
end note
User -> API: POST /api/auth/confirm?token=<token>\n(requires Bearer access token)
activate API
API -> Mediator: Send(ConfirmUserCommand(token))
activate Mediator
Mediator -> ConfirmHandler: Handle(command)
activate ConfirmHandler
ConfirmHandler -> TokenSvc: ValidateConfirmationTokenAsync(token)
activate TokenSvc
TokenSvc -> JWT: Validate against confirmation\ntoken secret, extract userId
activate JWT
JWT --> TokenSvc: ValidatedToken or throws
deactivate JWT
TokenSvc --> ConfirmHandler: ValidatedToken
deactivate TokenSvc
alt Token invalid or expired
ConfirmHandler -> Mediator: throw UnauthorizedException
Mediator -> API: propagate
API -> User: 401 Unauthorized
else Token valid
ConfirmHandler -> UserMgr: FindByIdAsync(validatedToken.UserId)
activate UserMgr
UserMgr -> UserStore: FindByIdAsync(userId)
activate UserStore
UserStore -> DB: SELECT ...\nFROM dbo.UserAccount\nWHERE UserAccountID = @Id
activate DB
DB --> UserStore: row or null
deactivate DB
UserStore --> UserMgr: ApplicationUser or null
deactivate UserStore
UserMgr --> ConfirmHandler: ApplicationUser or null
deactivate UserMgr
alt User not found
ConfirmHandler -> Mediator: throw UnauthorizedException\n"User account not found"
Mediator -> API: propagate
API -> User: 401 Unauthorized
else User found
ConfirmHandler -> UserStore: SetEmailConfirmedAsync(user, true)
note right of UserStore
IUserEmailStore<ApplicationUser> is injected
directly into the handler (not via UserManager),
setting the confirmed flag in-memory here.
end note
ConfirmHandler -> UserMgr: UpdateAsync(user)
activate UserMgr
UserMgr -> UserStore: UpdateAsync(user)
activate UserStore
UserStore -> DB: UPDATE dbo.UserAccount SET ...
UserStore -> DB: INSERT INTO dbo.UserVerification\n(UserAccountId, VerificationDateTime)
note right of UserStore
Idempotent: skipped (duplicate-key
exception swallowed) if a verification
row already exists.
end note
UserStore --> UserMgr: IdentityResult.Success
deactivate UserStore
UserMgr --> ConfirmHandler: IdentityResult
deactivate UserMgr
ConfirmHandler --> Mediator: ConfirmationPayload(\n user.Id, DateTime.UtcNow)
deactivate ConfirmHandler
Mediator --> API: ConfirmationPayload
deactivate Mediator
API -> User: 200 OK\n{message: "User with ID {id} is confirmed.",\npayload: {...}}
end
end
deactivate API
@enduml

View File

@@ -0,0 +1,91 @@
@startuml delete-account
!include ../../../pipeline/diagrams/biergarten-weizen-theme.puml
skinparam sequenceMessageAlign center
skinparam maxMessageSize 200
title User Authentication Flow - Delete Account
actor User
participant "API\nController" as API
participant "MediatR pipeline\n(ValidationBehavior)" as Mediator
box "Handler Layer" #F1F3EA
participant "DeleteAccountHandler" as DeleteAccountHandler
participant "UserManager<ApplicationUser>" as UserMgr
end box
box "Repository / Cross-slice" #DBEEDD
participant "DapperUserStore\n(IUserStore/IUserPasswordStore/\nIUserEmailStore)" as UserStore
end box
database "SQL Server" as DB
note over UserMgr
ASP.NET Core Identity's UserManager<TUser>
replaces the deleted AuthRepository: handlers
no longer issue SQL or call Argon2 directly,
they delegate to UserManager, which in turn
calls DapperUserStore (persistence) and
Argon2PasswordHasher (IPasswordHasher<TUser>).
end note
User -> API: DELETE /api/auth/account\n(requires Bearer access token)
activate API
API -> Mediator: Send(DeleteAccountCommand(\n GetAuthenticatedUserId()))
activate Mediator
Mediator -> DeleteAccountHandler: Handle(command)
activate DeleteAccountHandler
DeleteAccountHandler -> UserMgr: FindByIdAsync(userAccountId)
activate UserMgr
UserMgr -> UserStore: FindByIdAsync(userAccountId)
UserStore --> UserMgr: ApplicationUser or null
UserMgr --> DeleteAccountHandler: ApplicationUser or null
deactivate UserMgr
alt Account not found
DeleteAccountHandler -> Mediator: throw NotFoundException
Mediator -> API: propagate
API -> User: 404 Not Found
else Account found
DeleteAccountHandler -> UserMgr: DeleteAsync(user)
activate UserMgr
UserMgr -> UserStore: DeleteAsync(user)
activate UserStore
UserStore -> DB: DELETE FROM dbo.UserAccount\nWHERE UserAccountID = @Id
note right of UserStore
UserCredential/UserVerification/UserAvatar
cascade on delete. Posts, comments, photos,
and follows use ON DELETE NO ACTION - if any
reference this account, the DELETE throws a
foreign key violation (SQL error 547).
end note
alt Foreign key violation (dependent records exist)
UserStore --> UserMgr: IdentityResult.Failed\n(AccountHasDependentRecords)
else Delete succeeds
UserStore --> UserMgr: IdentityResult.Success
end
deactivate UserStore
UserMgr --> DeleteAccountHandler: IdentityResult
deactivate UserMgr
alt Account has dependent records
DeleteAccountHandler -> Mediator: throw ConflictException\n"Account cannot be deleted while it still\nhas associated posts, comments, photos,\nor follows."
Mediator -> API: propagate
API -> User: 409 Conflict
else !result.Succeeded (other failure)
DeleteAccountHandler -> Mediator: throw ConflictException
Mediator -> API: propagate
API -> User: 409 Conflict
else result.Succeeded
DeleteAccountHandler --> Mediator: (void)
deactivate DeleteAccountHandler
Mediator --> API: (void)
deactivate Mediator
API -> User: 200 OK\n{message: "Account deleted successfully."}
end
end
deactivate API
@enduml

View File

@@ -0,0 +1,30 @@
@startuml error-handling
!include ../../../pipeline/diagrams/biergarten-weizen-theme.puml
skinparam sequenceMessageAlign center
skinparam maxMessageSize 200
title User Authentication Flow - Error Handling (Global Exception Filter)
participant "API\nController" as API
note over API
API.Core's GlobalExceptionFilter catches:
- FluentValidation.ValidationException -> 400 Bad Request\n {message, errors} (grouped by property)
- ConflictException -> 409 Conflict
- NotFoundException -> 404 Not Found
- UnauthorizedException -> 401 Unauthorized
- ForbiddenException -> 403 Forbidden
- Domain.Exceptions.ValidationException -> 400 Bad Request
- Microsoft.Data.SqlClient.SqlException -> 503 Service Unavailable
- All others -> 500 Internal Server Error
end note
note over API
Applies uniformly across every authentication
endpoint (registration, login, refresh, confirm,
resend, update-username/email/password/profile,
delete-account) - see the per-flow diagrams for
where each exception type is thrown.
end note
@enduml

View File

@@ -0,0 +1,129 @@
@startuml login
!include ../../../pipeline/diagrams/biergarten-weizen-theme.puml
skinparam sequenceMessageAlign center
skinparam maxMessageSize 200
title User Authentication Flow - Login
actor User
participant "API\nController" as API
participant "MediatR pipeline\n(ValidationBehavior)" as Mediator
box "Handler Layer" #F1F3EA
participant "LoginHandler" as LoginHandler
participant "UserManager<ApplicationUser>" as UserMgr
participant "TokenService" as TokenSvc
end box
box "Infrastructure Layer" #EBECE3
participant "Argon2PasswordHasher" as Argon2
participant "JWT\nInfrastructure" as JWT
end box
box "Repository / Cross-slice" #DBEEDD
participant "DapperUserStore\n(IUserStore/IUserPasswordStore/\nIUserEmailStore)" as UserStore
end box
database "SQL Server" as DB
note over UserMgr
ASP.NET Core Identity's UserManager<TUser>
replaces the deleted AuthRepository: handlers
no longer issue SQL or call Argon2 directly,
they delegate to UserManager, which in turn
calls DapperUserStore (persistence) and
Argon2PasswordHasher (IPasswordHasher<TUser>).
end note
User -> API: POST /api/auth/login\n{username, password}
activate API
API -> Mediator: Send(LoginCommand)
activate Mediator
note right of Mediator
ValidationBehavior runs LoginValidator:
username and password must both be non-empty
end note
alt Validation fails
Mediator -> API: throws FluentValidation.ValidationException
API -> User: 400 Bad Request\n{message: "Validation failed", errors: {...}}
else Validation succeeds
Mediator -> LoginHandler: Handle(command)
activate LoginHandler
LoginHandler -> UserMgr: FindByNameAsync(username)
activate UserMgr
UserMgr -> UserStore: FindByNameAsync(username)
activate UserStore
UserStore -> DB: SELECT UserAccountID, Username, ...\nFROM dbo.UserAccount\nWHERE Username = @Username
activate DB
DB --> UserStore: UserAccount row or null
deactivate DB
UserStore -> UserStore: HydrateAsync\n(load active UserCredential.Hash,\nUserVerification -> EmailConfirmed)
UserStore --> UserMgr: ApplicationUser or null
deactivate UserStore
UserMgr --> LoginHandler: ApplicationUser or null
deactivate UserMgr
alt User not found
LoginHandler -> Mediator: throw UnauthorizedException\n"Invalid username or password."
Mediator -> API: propagate
API -> User: 401 Unauthorized
else User found
LoginHandler -> UserMgr: CheckPasswordAsync(user, password)
activate UserMgr
note right of UserMgr
HasPasswordAsync short-circuits to false
(no active UserCredential was hydrated)
without calling the hasher; otherwise
delegates to Argon2PasswordHasher.
end note
UserMgr -> Argon2: VerifyHashedPassword(\n user, user.PasswordHash, password)
activate Argon2
note right of Argon2
Delegates to IPasswordInfrastructure.Verify:
1. Split stored hash: "salt:hash"
2. Hash provided password with the same salt
3. Constant-time comparison\n (CryptographicOperations.FixedTimeEquals)
end note
Argon2 --> UserMgr: Success/Failed
deactivate Argon2
UserMgr --> LoginHandler: true/false
deactivate UserMgr
alt Password invalid
LoginHandler -> Mediator: throw UnauthorizedException
Mediator -> API: propagate
API -> User: 401 Unauthorized
else Password valid
LoginHandler -> TokenSvc: GenerateAccessToken(user)
activate TokenSvc
TokenSvc -> JWT: GenerateJwt(...)
activate JWT
JWT --> TokenSvc: Access Token
deactivate JWT
TokenSvc --> LoginHandler: Access Token
deactivate TokenSvc
LoginHandler -> TokenSvc: GenerateRefreshToken(user)
activate TokenSvc
TokenSvc -> JWT: GenerateJwt(...)
activate JWT
JWT --> TokenSvc: Refresh Token
deactivate JWT
TokenSvc --> LoginHandler: Refresh Token
deactivate TokenSvc
LoginHandler --> Mediator: LoginPayload(\n userAccountId, username,\n refreshToken, accessToken)
deactivate LoginHandler
Mediator --> API: LoginPayload
deactivate Mediator
API -> User: 200 OK\n{\n message: "Logged in successfully.",\n payload: {\n userAccountId, username,\n refreshToken, accessToken\n }\n}
end
end
end
deactivate API
@enduml

View File

@@ -0,0 +1,108 @@
@startuml refresh-token
!include ../../../pipeline/diagrams/biergarten-weizen-theme.puml
skinparam sequenceMessageAlign center
skinparam maxMessageSize 200
title User Authentication Flow - Refresh Token
actor User
participant "API\nController" as API
participant "MediatR pipeline\n(ValidationBehavior)" as Mediator
box "Handler Layer" #F1F3EA
participant "RefreshTokenHandler" as RefreshHandler
participant "UserManager<ApplicationUser>" as UserMgr
participant "TokenService" as TokenSvc
end box
box #EBECE3
participant "JWT\nInfrastructure" as JWT
end box
box "Repository / Cross-slice" #DBEEDD
participant "DapperUserStore\n(IUserStore/IUserPasswordStore/\nIUserEmailStore)" as UserStore
end box
database "SQL Server" as DB
note over UserMgr
ASP.NET Core Identity's UserManager<TUser>
replaces the deleted AuthRepository: handlers
no longer issue SQL or call Argon2 directly,
they delegate to UserManager, which in turn
calls DapperUserStore (persistence) and
Argon2PasswordHasher (IPasswordHasher<TUser>).
end note
User -> API: POST /api/auth/refresh\nHeader: X-Refresh-Token: <token>
activate API
API -> Mediator: Send(RefreshTokenCommand(refreshToken))
activate Mediator
note right of Mediator
ValidationBehavior runs RefreshTokenValidator:
refreshToken must be non-empty. Anonymous
endpoint - no Bearer token required.
end note
alt Validation fails
Mediator -> API: throws FluentValidation.ValidationException
API -> User: 400 Bad Request\n{message: "Validation failed", errors: {...}}
else Validation succeeds
Mediator -> RefreshHandler: Handle(command)
activate RefreshHandler
RefreshHandler -> TokenSvc: RefreshTokenAsync(refreshToken)
activate TokenSvc
TokenSvc -> JWT: ValidateRefreshTokenAsync(refreshToken)
activate JWT
note right of JWT
Validates signature/expiry against the
refresh token secret, extracts userId/
username claims.
end note
JWT --> TokenSvc: ValidatedToken or throws
deactivate JWT
alt Token invalid or expired
TokenSvc -> RefreshHandler: throw UnauthorizedException
RefreshHandler -> Mediator: propagate
Mediator -> API: propagate
API -> User: 401 Unauthorized
else Token valid
TokenSvc -> UserMgr: FindByIdAsync(validated.UserId)
activate UserMgr
UserMgr -> UserStore: FindByIdAsync(userId)
activate UserStore
UserStore -> DB: SELECT UserAccountID, Username, ...\nFROM dbo.UserAccount\nWHERE UserAccountID = @Id
activate DB
DB --> UserStore: row or null
deactivate DB
UserStore --> UserMgr: ApplicationUser or null
deactivate UserStore
UserMgr --> TokenSvc: ApplicationUser or null
deactivate UserMgr
alt User not found
TokenSvc -> RefreshHandler: throw UnauthorizedException\n"User account not found"
RefreshHandler -> Mediator: propagate
Mediator -> API: propagate
API -> User: 401 Unauthorized
else User found
TokenSvc -> JWT: GenerateAccessToken(user)\nGenerateRefreshToken(user)
JWT --> TokenSvc: new access + refresh tokens
TokenSvc --> RefreshHandler: RefreshTokenResult
deactivate TokenSvc
RefreshHandler --> Mediator: LoginPayload(\n userId, username,\n refreshToken, accessToken)
deactivate RefreshHandler
Mediator --> API: LoginPayload
deactivate Mediator
API -> User: 200 OK\n{message: "Token refreshed successfully.",\npayload: {...}}
end
end
end
deactivate API
@enduml

View File

@@ -0,0 +1,241 @@
@startuml registration
!include ../../../pipeline/diagrams/biergarten-weizen-theme.puml
skinparam sequenceMessageAlign center
skinparam maxMessageSize 200
title User Authentication Flow - Registration
actor User
participant "API\nController" as API
participant "MediatR pipeline\n(ValidationBehavior)" as Mediator
box "Handler Layer" #F1F3EA
participant "RegisterUserHandler" as RegHandler
participant "UserManager<ApplicationUser>" as UserMgr
participant "TokenService" as TokenSvc
end box
box "Infrastructure Layer" #EBECE3
participant "Argon2PasswordHasher" as Argon2
participant "JWT\nInfrastructure" as JWT
participant "Email\nProvider" as SMTP
participant "Template\nProvider" as Template
end box
box "Repository / Cross-slice" #DBEEDD
participant "DapperUserStore\n(IUserStore/IUserPasswordStore/\nIUserEmailStore)" as UserStore
participant "EmailDispatcher\n(Features.Emails)" as EmailDispatcher
end box
database "SQL Server" as DB
note over UserMgr
ASP.NET Core Identity's UserManager<TUser>
replaces the deleted AuthRepository: handlers
no longer issue SQL or call Argon2 directly,
they delegate to UserManager, which in turn
calls DapperUserStore (persistence) and
Argon2PasswordHasher (IPasswordHasher<TUser>).
end note
User -> API: POST /api/auth/register\n{username, firstName, lastName,\nemail, dateOfBirth, password}
activate API
API -> Mediator: Send(RegisterUserCommand)
activate Mediator
note right of Mediator
ValidationBehavior runs RegisterUserValidator
(FluentValidation) before the handler:
- Username: 3-64 chars, alphanumeric + [._-]
- Email: valid format, max 128 chars
- Password: min 8 chars, uppercase,\n lowercase, number, special char
- DateOfBirth: must be 19+ years old
end note
alt Validation fails
Mediator -> API: throws FluentValidation.ValidationException
API -> User: 400 Bad Request\n{message: "Validation failed", errors: {...}}
else Validation succeeds
Mediator -> RegHandler: Handle(command)
activate RegHandler
RegHandler -> RegHandler: user = new UserAccount{...}\n.ToApplicationUser()
RegHandler -> UserMgr: CreateAsync(user, password)
activate UserMgr
UserMgr -> Argon2: HashPassword(user, password)
activate Argon2
note right of Argon2
Delegates to IPasswordInfrastructure.Hash:
Argon2id parameters:
- Salt: 16 bytes (128-bit)
- Memory: 64MB
- Iterations: 4
- Parallelism: CPU count
- Hash output: 32 bytes
end note
Argon2 --> UserMgr: "base64(salt):base64(hash)"
deactivate Argon2
UserMgr -> UserStore: SetPasswordHashAsync(user, hash)
note right of UserStore
user.Id is still empty at this point -
only sets the in-memory hash. CreateAsync
writes it to dbo.UserCredential itself.
end note
UserMgr -> UserStore: FindByNameAsync(username)
activate UserStore
UserStore -> DB: SELECT UserAccountID, Username, ...\nFROM dbo.UserAccount\nWHERE Username = @Username
activate DB
DB --> UserStore: null (user doesn't exist)
deactivate DB
deactivate UserStore
UserMgr -> UserStore: FindByEmailAsync(email)
activate UserStore
UserStore -> DB: SELECT UserAccountID, Username, ...\nFROM dbo.UserAccount\nWHERE 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 is called;
DapperUserStore.CreateAsync also guards
against a genuine race via a caught
duplicate-key SqlException (2601/2627).
end note
UserMgr -> UserStore: CreateAsync(user)
activate UserStore
UserStore -> DB: BEGIN TRANSACTION
activate DB
UserStore -> DB: INSERT INTO dbo.UserAccount\n(Username, FirstName, LastName,\nDateOfBirth, Email)\nOUTPUT INSERTED.UserAccountID
DB --> UserStore: UserAccountId (GUID)
UserStore -> DB: INSERT INTO dbo.UserCredential\n(UserAccountId, Hash)
UserStore -> DB: COMMIT TRANSACTION
deactivate DB
note right of UserStore
Both statements run via Dapper's
ExecuteScalarAsync/ExecuteAsync against
a single DbTransaction, not a stored
procedure - no manual parameter binding
end note
UserStore --> UserMgr: IdentityResult.Success\n(user.Id set)
deactivate UserStore
UserMgr --> RegHandler: IdentityResult
deactivate UserMgr
alt !result.Succeeded
RegHandler -> Mediator: throw ConflictException\n(DuplicateUserName/DuplicateEmail codes ->\n"Username or email already exists";\notherwise joined error descriptions)
Mediator -> API: propagate
API -> User: 409 Conflict\n"Username or email already exists"
else result.Succeeded
RegHandler -> TokenSvc: GenerateAccessToken(user)
activate TokenSvc
TokenSvc -> JWT: GenerateJwt(userId, username, expiry)
activate JWT
note right of JWT
JWT Configuration:
- Algorithm: HS256
- Expires: 1 hour
- Claims:
* sub: userId
* unique_name: username
* iat / jti
end note
JWT -> JWT: Create JWT with claims
JWT -> JWT: 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
Refresh Token:
- Expires: 21 days (504 hours)
- Same structure as access token,\n signed with a separate secret
end note
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
Confirmation Token:
- Expires: 30 minutes (0.5 hours)
- Signed with a third, separate secret
end note
JWT --> TokenSvc: Confirmation Token
deactivate JWT
TokenSvc --> RegHandler: Confirmation Token
deactivate TokenSvc
RegHandler -> Mediator: Send(SendRegistrationEmailCommand)\n(Shared.Application cross-slice contract)
activate Mediator
Mediator -> EmailDispatcher: SendRegistrationEmailAsync(\n firstName, email, confirmationToken)
note right of Mediator
Routed to Features.Emails'
SendRegistrationEmailHandler,
the only cross-slice call in
the backend
end note
activate EmailDispatcher
EmailDispatcher -> Template: RenderUserRegisteredEmailAsync(\n firstName, confirmationLink)
activate Template
note right of Template
Razor Component:
- Header with branding
- Welcome message
- Confirmation button
- Footer
end note
Template -> Template: Render Razor component\nto HTML
Template --> EmailDispatcher: HTML email content
deactivate Template
EmailDispatcher -> SMTP: SendAsync(email, subject, body)
activate SMTP
note right of SMTP
SMTP Configuration (MailKit):
- Host/port: from env (SMTP_HOST/SMTP_PORT)
- TLS: StartTLS when SMTP_USE_SSL=true
- Auth: username/password, if configured
end note
SMTP -> SMTP: Build MIME message
SMTP -> SMTP: Connect, authenticate,\nsend, 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),\nlog to stderr,\nemailSent = false
end
RegHandler --> Mediator: RegistrationPayload(\n userAccountId, username,\n refreshToken, accessToken,\n confirmationEmailSent)
deactivate RegHandler
Mediator --> API: RegistrationPayload
deactivate Mediator
API -> User: 201 Created\n{\n message: "User registered successfully.",\n payload: {\n userAccountId, username,\n refreshToken, accessToken,\n confirmationEmailSent\n }\n}
end
end
deactivate API
@enduml

View File

@@ -0,0 +1,97 @@
@startuml resend-confirmation
!include ../../../pipeline/diagrams/biergarten-weizen-theme.puml
skinparam sequenceMessageAlign center
skinparam maxMessageSize 200
title User Authentication Flow - Resend Confirmation Email
actor User
participant "API\nController" as API
participant "MediatR pipeline\n(ValidationBehavior)" as Mediator
box "Handler Layer" #F1F3EA
participant "ResendConfirmationEmailHandler" as ResendHandler
participant "UserManager<ApplicationUser>" as UserMgr
participant "TokenService" as TokenSvc
end box
box "Infrastructure Layer" #EBECE3
participant "Email\nProvider" as SMTP
participant "Template\nProvider" as Template
end box
box "Repository / Cross-slice" #DBEEDD
participant "DapperUserStore\n(IUserStore/IUserPasswordStore/\nIUserEmailStore)" as UserStore
participant "EmailDispatcher\n(Features.Emails)" as EmailDispatcher
end box
database "SQL Server" as DB
note over UserMgr
ASP.NET Core Identity's UserManager<TUser>
replaces the deleted AuthRepository: handlers
no longer issue SQL or call Argon2 directly,
they delegate to UserManager, which in turn
calls DapperUserStore (persistence) and
Argon2PasswordHasher (IPasswordHasher<TUser>).
end note
User -> API: POST /api/auth/confirm/resend?userId=<id>\n(requires Bearer access token)
activate API
API -> Mediator: Send(ResendConfirmationEmailCommand(userId))
activate Mediator
Mediator -> ResendHandler: Handle(command)
activate ResendHandler
ResendHandler -> UserMgr: FindByIdAsync(userId)
activate UserMgr
UserMgr -> UserStore: FindByIdAsync(userId)
activate UserStore
UserStore -> DB: SELECT ...\nFROM dbo.UserAccount\nWHERE UserAccountID = @Id
activate DB
DB --> UserStore: row or null
deactivate DB
UserStore --> UserMgr: ApplicationUser or null
deactivate UserStore
UserMgr --> ResendHandler: ApplicationUser or null
deactivate UserMgr
alt User not found
ResendHandler -> ResendHandler: return silently\n(prevents user enumeration)
else User found
ResendHandler -> UserMgr: IsEmailConfirmedAsync(user)
UserMgr --> ResendHandler: bool
alt Already confirmed
ResendHandler -> ResendHandler: return silently (no-op)
else Not yet confirmed
ResendHandler -> TokenSvc: GenerateConfirmationToken(user)
activate TokenSvc
TokenSvc --> ResendHandler: confirmation token
deactivate TokenSvc
ResendHandler -> Mediator: Send(SendResendConfirmationEmailCommand)
activate Mediator
Mediator -> EmailDispatcher: SendResendConfirmationEmailAsync(\n firstName, email, confirmationToken)
activate EmailDispatcher
EmailDispatcher -> Template: Render email\n(same Razor pipeline as registration)
EmailDispatcher -> SMTP: SendAsync(email, subject, body)
deactivate EmailDispatcher
deactivate Mediator
end
end
ResendHandler --> Mediator: (void)
deactivate ResendHandler
Mediator --> API: (void)
deactivate Mediator
API -> User: 200 OK\n{message: "confirmation email has been resent"}
note right of API
Always 200 OK regardless of outcome above -
same user-enumeration precaution as a not-found
user or an already-confirmed account.
end note
deactivate API
@enduml

View File

@@ -0,0 +1,93 @@
@startuml update-email
!include ../../../pipeline/diagrams/biergarten-weizen-theme.puml
skinparam sequenceMessageAlign center
skinparam maxMessageSize 200
title User Authentication Flow - Update Email
actor User
participant "API\nController" as API
participant "MediatR pipeline\n(ValidationBehavior)" as Mediator
box "Handler Layer" #F1F3EA
participant "UpdateEmailHandler" as UpdateEmailHandler
participant "UserManager<ApplicationUser>" as UserMgr
end box
box "Repository / Cross-slice" #DBEEDD
participant "DapperUserStore\n(IUserStore/IUserPasswordStore/\nIUserEmailStore)" as UserStore
end box
database "SQL Server" as DB
note over UserMgr
ASP.NET Core Identity's UserManager<TUser>
replaces the deleted AuthRepository: handlers
no longer issue SQL or call Argon2 directly,
they delegate to UserManager, which in turn
calls DapperUserStore (persistence) and
Argon2PasswordHasher (IPasswordHasher<TUser>).
end note
User -> API: PATCH /api/auth/email\n{newEmail}\n(requires Bearer access token)
activate API
API -> Mediator: Send(UpdateEmailCommand(\n GetAuthenticatedUserId(), newEmail))
activate Mediator
note right of Mediator
ValidationBehavior runs UpdateEmailValidator
(same format rules as registration)
end note
alt Validation fails
Mediator -> API: throws FluentValidation.ValidationException
API -> User: 400 Bad Request
else Validation succeeds
Mediator -> UpdateEmailHandler: Handle(command)
activate UpdateEmailHandler
UpdateEmailHandler -> UserMgr: FindByIdAsync(userAccountId)
activate UserMgr
UserMgr -> UserStore: FindByIdAsync(userAccountId)
UserStore -> DB: SELECT ...
UserStore --> UserMgr: ApplicationUser or null
UserMgr --> UpdateEmailHandler: ApplicationUser or null
deactivate UserMgr
alt Account not found
UpdateEmailHandler -> Mediator: throw NotFoundException
Mediator -> API: propagate
API -> User: 404 Not Found
else Account found
UpdateEmailHandler -> UserMgr: SetEmailAsync(user, newEmail)
activate UserMgr
UserMgr -> UserStore: FindByEmailAsync(newEmail)
note right of UserMgr
Uniqueness check. Also resets
user.EmailConfirmed = false -
the caller must re-confirm via
POST /api/auth/confirm/resend.
end note
UserMgr -> UserStore: UpdateAsync(user)
UserStore -> DB: UPDATE dbo.UserAccount\nSET Email = @Email, ...
UserStore -> DB: DELETE FROM dbo.UserVerification\nWHERE UserAccountID = @Id
UserStore --> UserMgr: IdentityResult
UserMgr --> UpdateEmailHandler: IdentityResult
deactivate UserMgr
alt !result.Succeeded
UpdateEmailHandler -> Mediator: throw ConflictException\n(DuplicateEmail code ->\n"Email address already exists")
Mediator -> API: propagate
API -> User: 409 Conflict
else result.Succeeded
UpdateEmailHandler --> Mediator: UpdateEmailPayload(\n user.Id, user.Email,\n user.EmailConfirmed = false)
deactivate UpdateEmailHandler
Mediator --> API: UpdateEmailPayload
deactivate Mediator
API -> User: 200 OK\n{message: "Email updated successfully.",\npayload: {...}}
end
end
end
deactivate API
@enduml

View File

@@ -0,0 +1,111 @@
@startuml update-password
!include ../../../pipeline/diagrams/biergarten-weizen-theme.puml
skinparam sequenceMessageAlign center
skinparam maxMessageSize 200
title User Authentication Flow - Update Password
actor User
participant "API\nController" as API
participant "MediatR pipeline\n(ValidationBehavior)" as Mediator
box "Handler Layer" #F1F3EA
participant "UpdatePasswordHandler" as UpdatePasswordHandler
participant "UserManager<ApplicationUser>" as UserMgr
end box
box "Infrastructure Layer" #EBECE3
participant "Argon2PasswordHasher" as Argon2
end box
box "Repository / Cross-slice" #DBEEDD
participant "DapperUserStore\n(IUserStore/IUserPasswordStore/\nIUserEmailStore)" as UserStore
end box
database "SQL Server" as DB
note over UserMgr
ASP.NET Core Identity's UserManager<TUser>
replaces the deleted AuthRepository: handlers
no longer issue SQL or call Argon2 directly,
they delegate to UserManager, which in turn
calls DapperUserStore (persistence) and
Argon2PasswordHasher (IPasswordHasher<TUser>).
end note
User -> API: PATCH /api/auth/password\n{currentPassword, newPassword}\n(requires Bearer access token)
activate API
API -> Mediator: Send(UpdatePasswordCommand(\n GetAuthenticatedUserId(),\n currentPassword, newPassword))
activate Mediator
note right of Mediator
ValidationBehavior runs UpdatePasswordValidator
(same password strength rules as registration)
end note
alt Validation fails
Mediator -> API: throws FluentValidation.ValidationException
API -> User: 400 Bad Request
else Validation succeeds
Mediator -> UpdatePasswordHandler: Handle(command)
activate UpdatePasswordHandler
UpdatePasswordHandler -> UserMgr: FindByIdAsync(userAccountId)
activate UserMgr
UserMgr -> UserStore: FindByIdAsync(userAccountId)
UserStore --> UserMgr: ApplicationUser or null
UserMgr --> UpdatePasswordHandler: ApplicationUser or null
deactivate UserMgr
alt Account not found
UpdatePasswordHandler -> Mediator: throw NotFoundException
Mediator -> API: propagate
API -> User: 404 Not Found
else Account found
UpdatePasswordHandler -> UserMgr: ChangePasswordAsync(\n user, currentPassword, newPassword)
activate UserMgr
UserMgr -> Argon2: VerifyHashedPassword(\n user, user.PasswordHash, currentPassword)
activate Argon2
Argon2 --> UserMgr: Success/Failed
deactivate Argon2
alt Current password mismatch
UserMgr --> UpdatePasswordHandler: IdentityResult.Failed\n(PasswordMismatch)
else Current password matches
UserMgr -> Argon2: HashPassword(user, newPassword)
activate Argon2
Argon2 --> UserMgr: new hash
deactivate Argon2
UserMgr -> UserStore: SetPasswordHashAsync(user, newHash)
activate UserStore
UserStore -> DB: UPDATE dbo.UserCredential\nSET IsRevoked = 1, RevokedAt = GETDATE()\nWHERE UserAccountId = @Id AND IsRevoked = 0
UserStore -> DB: INSERT INTO dbo.UserCredential\n(UserAccountId, Hash)
note right of UserStore
Revoke-then-insert runs inside one
DbTransaction (credential rotation).
end note
UserStore --> UserMgr: (void)
deactivate UserStore
UserMgr --> UpdatePasswordHandler: IdentityResult.Success
end
deactivate UserMgr
alt Current password is incorrect
UpdatePasswordHandler -> Mediator: throw UnauthorizedException\n"Current password is incorrect"
Mediator -> API: propagate
API -> User: 401 Unauthorized
else !result.Succeeded (other failure)
UpdatePasswordHandler -> Mediator: throw ConflictException
Mediator -> API: propagate
API -> User: 409 Conflict
else result.Succeeded
UpdatePasswordHandler --> Mediator: UpdatePasswordPayload(\n user.Id, DateTime.UtcNow)
deactivate UpdatePasswordHandler
Mediator --> API: UpdatePasswordPayload
deactivate Mediator
API -> User: 200 OK\n{message: "Password updated successfully.",\npayload: {...}}
end
end
end
deactivate API
@enduml

View File

@@ -0,0 +1,92 @@
@startuml update-profile
!include ../../../pipeline/diagrams/biergarten-weizen-theme.puml
skinparam sequenceMessageAlign center
skinparam maxMessageSize 200
title User Authentication Flow - Update Profile
actor User
participant "API\nController" as API
participant "MediatR pipeline\n(ValidationBehavior)" as Mediator
box "Handler Layer" #F1F3EA
participant "UpdateProfileHandler" as UpdateProfileHandler
participant "UserManager<ApplicationUser>" as UserMgr
end box
box "Repository / Cross-slice" #DBEEDD
participant "DapperUserStore\n(IUserStore/IUserPasswordStore/\nIUserEmailStore)" as UserStore
end box
database "SQL Server" as DB
note over UserMgr
ASP.NET Core Identity's UserManager<TUser>
replaces the deleted AuthRepository: handlers
no longer issue SQL or call Argon2 directly,
they delegate to UserManager, which in turn
calls DapperUserStore (persistence) and
Argon2PasswordHasher (IPasswordHasher<TUser>).
end note
User -> API: PATCH /api/auth/profile\n{firstName, lastName, dateOfBirth}\n(requires Bearer access token)
activate API
API -> Mediator: Send(UpdateProfileCommand(\n GetAuthenticatedUserId(),\n firstName, lastName, dateOfBirth))
activate Mediator
note right of Mediator
ValidationBehavior runs UpdateProfileValidator
(same name/DateOfBirth rules as registration)
end note
alt Validation fails
Mediator -> API: throws FluentValidation.ValidationException
API -> User: 400 Bad Request
else Validation succeeds
Mediator -> UpdateProfileHandler: Handle(command)
activate UpdateProfileHandler
UpdateProfileHandler -> UserMgr: FindByIdAsync(userAccountId)
activate UserMgr
UserMgr -> UserStore: FindByIdAsync(userAccountId)
UserStore --> UserMgr: ApplicationUser or null
UserMgr --> UpdateProfileHandler: ApplicationUser or null
deactivate UserMgr
alt Account not found
UpdateProfileHandler -> Mediator: throw NotFoundException
Mediator -> API: propagate
API -> User: 404 Not Found
else Account found
UpdateProfileHandler -> UpdateProfileHandler: user.FirstName = ...\nuser.LastName = ...\nuser.DateOfBirth = ...
note right of UpdateProfileHandler
First/last name and date of birth aren't
tracked by any Identity store interface -
mutated directly on the ApplicationUser,
then saved via UserManager.UpdateAsync.
end note
UpdateProfileHandler -> UserMgr: UpdateAsync(user)
activate UserMgr
UserMgr -> UserStore: UpdateAsync(user)
UserStore -> DB: UPDATE dbo.UserAccount\nSET FirstName = @FirstName,\n LastName = @LastName,\n DateOfBirth = @DateOfBirth, ...
UserStore --> UserMgr: IdentityResult
UserMgr --> UpdateProfileHandler: IdentityResult
deactivate UserMgr
alt !result.Succeeded
UpdateProfileHandler -> Mediator: throw ConflictException
Mediator -> API: propagate
API -> User: 409 Conflict
else result.Succeeded
UpdateProfileHandler --> Mediator: UpdateProfilePayload(\n user.Id, firstName,\n lastName, dateOfBirth)
deactivate UpdateProfileHandler
Mediator --> API: UpdateProfilePayload
deactivate Mediator
API -> User: 200 OK\n{message: "Profile updated successfully.",\npayload: {...}}
end
end
end
deactivate API
@enduml

View File

@@ -0,0 +1,94 @@
@startuml update-username
!include ../../../pipeline/diagrams/biergarten-weizen-theme.puml
skinparam sequenceMessageAlign center
skinparam maxMessageSize 200
title User Authentication Flow - Update Username
actor User
participant "API\nController" as API
participant "MediatR pipeline\n(ValidationBehavior)" as Mediator
box "Handler Layer" #F1F3EA
participant "UpdateUsernameHandler" as UpdateUsernameHandler
participant "UserManager<ApplicationUser>" as UserMgr
end box
box "Repository / Cross-slice" #DBEEDD
participant "DapperUserStore\n(IUserStore/IUserPasswordStore/\nIUserEmailStore)" as UserStore
end box
database "SQL Server" as DB
note over UserMgr
ASP.NET Core Identity's UserManager<TUser>
replaces the deleted AuthRepository: handlers
no longer issue SQL or call Argon2 directly,
they delegate to UserManager, which in turn
calls DapperUserStore (persistence) and
Argon2PasswordHasher (IPasswordHasher<TUser>).
end note
User -> API: PATCH /api/auth/username\n{newUsername}\n(requires Bearer access token)
activate API
API -> Mediator: Send(UpdateUsernameCommand(\n GetAuthenticatedUserId(), newUsername))
activate Mediator
note right of Mediator
ValidationBehavior runs UpdateUsernameValidator
(same format rules as registration)
end note
alt Validation fails
Mediator -> API: throws FluentValidation.ValidationException
API -> User: 400 Bad Request
else Validation succeeds
Mediator -> UpdateUsernameHandler: Handle(command)
activate UpdateUsernameHandler
UpdateUsernameHandler -> UserMgr: FindByIdAsync(userAccountId)
activate UserMgr
UserMgr -> UserStore: FindByIdAsync(userAccountId)
activate UserStore
UserStore -> DB: SELECT ...\nFROM dbo.UserAccount\nWHERE UserAccountID = @Id
activate DB
DB --> UserStore: row or null
deactivate DB
UserStore --> UserMgr: ApplicationUser or null
deactivate UserStore
UserMgr --> UpdateUsernameHandler: ApplicationUser or null
deactivate UserMgr
alt Account not found
UpdateUsernameHandler -> Mediator: throw NotFoundException
Mediator -> API: propagate
API -> User: 404 Not Found
else Account found
UpdateUsernameHandler -> UserMgr: SetUserNameAsync(user, newUsername)
activate UserMgr
UserMgr -> UserStore: FindByNameAsync(newUsername)
note right of UserMgr
Uniqueness check before the UPDATE.
end note
UserMgr -> UserStore: UpdateAsync(user)
UserStore -> DB: UPDATE dbo.UserAccount\nSET Username = @UserName, ...
UserStore --> UserMgr: IdentityResult
UserMgr --> UpdateUsernameHandler: IdentityResult
deactivate UserMgr
alt !result.Succeeded
UpdateUsernameHandler -> Mediator: throw ConflictException\n(DuplicateUserName code ->\n"Username already exists")
Mediator -> API: propagate
API -> User: 409 Conflict
else result.Succeeded
UpdateUsernameHandler --> Mediator: UpdateUsernamePayload(\n user.Id, user.UserName)
deactivate UpdateUsernameHandler
Mediator --> API: UpdateUsernamePayload
deactivate Mediator
API -> User: 200 OK\n{message: "Username updated successfully.",\npayload: {...}}
end
end
end
deactivate API
@enduml