mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-14 17:54:42 +00:00
Refactor documentation and diagrams for web/pipeline. (#272)
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> for assistance with diagram creation and documentation prose updates.
This commit is contained in:
5
docs/web/diagrams/auth/confirm-user.md
Normal file
5
docs/web/diagrams/auth/confirm-user.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# User Authentication Flow — Confirm User
|
||||
|
||||
```mermaid
|
||||
--8<-- "web/diagrams/auth/confirm-user.mmd"
|
||||
```
|
||||
79
docs/web/diagrams/auth/confirm-user.mmd
Normal file
79
docs/web/diagrams/auth/confirm-user.mmd
Normal file
@@ -0,0 +1,79 @@
|
||||
sequenceDiagram
|
||||
actor User
|
||||
participant API as API Controller
|
||||
participant Mediator as MediatR pipeline<br/>(ValidationBehavior)
|
||||
box Handler Layer
|
||||
participant ConfirmHandler as ConfirmUserHandler
|
||||
participant UserMgr as UserManager(ApplicationUser)
|
||||
participant TokenSvc as TokenService
|
||||
end
|
||||
box Infrastructure Layer
|
||||
participant JWT as JWT Infrastructure
|
||||
end
|
||||
box Repository / Cross-slice
|
||||
participant UserStore as DapperUserStore<br/>(IUserStore/IUserPasswordStore/IUserEmailStore)
|
||||
end
|
||||
participant DB as SQL Server
|
||||
|
||||
User->>API: POST /api/auth/confirm?token={token} (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 token 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 ... FROM UserAccount WHERE 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 "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
|
||||
|
||||
ConfirmHandler->>UserMgr: UpdateAsync(user)
|
||||
activate UserMgr
|
||||
UserMgr->>UserStore: UpdateAsync(user)
|
||||
activate UserStore
|
||||
UserStore->>DB: UPDATE UserAccount SET ...
|
||||
UserStore->>DB: INSERT INTO UserVerification (UserAccountId, VerificationDateTime)
|
||||
Note right of UserStore: Idempotent — skipped (duplicate-key exception swallowed) if a verification row already exists
|
||||
UserStore-->>UserMgr: IdentityResult.Success
|
||||
deactivate UserStore
|
||||
UserMgr-->>ConfirmHandler: IdentityResult
|
||||
deactivate UserMgr
|
||||
|
||||
ConfirmHandler-->>Mediator: ConfirmationPayload(user.Id, DateTime.UtcNow)
|
||||
deactivate ConfirmHandler
|
||||
Mediator-->>API: ConfirmationPayload
|
||||
deactivate Mediator
|
||||
API->>User: 200 OK {message: "User with ID {id} is confirmed.", payload: {...}}
|
||||
end
|
||||
end
|
||||
deactivate API
|
||||
5
docs/web/diagrams/auth/delete-account.md
Normal file
5
docs/web/diagrams/auth/delete-account.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# User Authentication Flow — Delete Account
|
||||
|
||||
```mermaid
|
||||
--8<-- "web/diagrams/auth/delete-account.mmd"
|
||||
```
|
||||
65
docs/web/diagrams/auth/delete-account.mmd
Normal file
65
docs/web/diagrams/auth/delete-account.mmd
Normal file
@@ -0,0 +1,65 @@
|
||||
sequenceDiagram
|
||||
actor User
|
||||
participant API as API Controller
|
||||
participant Mediator as MediatR pipeline<br/>(ValidationBehavior)
|
||||
box Handler Layer
|
||||
participant DeleteAccountHandler
|
||||
participant UserMgr as UserManager(ApplicationUser)
|
||||
end
|
||||
box Repository / Cross-slice
|
||||
participant UserStore as DapperUserStore<br/>(IUserStore/IUserPasswordStore/IUserEmailStore)
|
||||
end
|
||||
participant DB as SQL Server
|
||||
|
||||
User->>API: DELETE /api/auth/account (requires Bearer access token)
|
||||
activate API
|
||||
API->>Mediator: Send(DeleteAccountCommand(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 UserAccount WHERE UserAccountID = @Id
|
||||
Note right of UserStore: UserCredential, UserVerification, and UserProfile cascade on delete<br/>(UserAvatar cascades transitively through UserProfile).<br/>Posts, comments, photos, and follows use ON DELETE NO ACTION<br/>if any reference this account, the DELETE throws a foreign key violation (SQL error 547)
|
||||
|
||||
alt Foreign key violation (dependent records exist)
|
||||
UserStore-->>UserMgr: IdentityResult.Failed (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<br>"Account cannot be deleted while it still has associated posts, comments, photos, or 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 {message: "Account deleted successfully."}
|
||||
end
|
||||
end
|
||||
deactivate API
|
||||
11
docs/web/diagrams/auth/error-handling.md
Normal file
11
docs/web/diagrams/auth/error-handling.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# User Authentication Flow — Error Handling
|
||||
|
||||
Global exception mapping performed by `API.Core`'s `GlobalExceptionFilter`.
|
||||
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.
|
||||
|
||||
```mermaid
|
||||
--8<-- "web/diagrams/auth/error-handling.mmd"
|
||||
```
|
||||
12
docs/web/diagrams/auth/error-handling.mmd
Normal file
12
docs/web/diagrams/auth/error-handling.mmd
Normal file
@@ -0,0 +1,12 @@
|
||||
sequenceDiagram
|
||||
participant API as API Controller
|
||||
|
||||
Note over API: GlobalExceptionFilter catches:
|
||||
Note over API: FluentValidation.ValidationException → 400 Bad Request {message, errors} (grouped by property)
|
||||
Note over API: ConflictException → 409 Conflict
|
||||
Note over API: NotFoundException → 404 Not Found
|
||||
Note over API: UnauthorizedException → 401 Unauthorized
|
||||
Note over API: ForbiddenException → 403 Forbidden
|
||||
Note over API: Domain.Exceptions.ValidationException → 400 Bad Request
|
||||
Note over API: Microsoft.Data.SqlClient.SqlException → 503 Service Unavailable
|
||||
Note over API: All others → 500 Internal Server Error
|
||||
5
docs/web/diagrams/auth/login.md
Normal file
5
docs/web/diagrams/auth/login.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# User Authentication Flow — Login
|
||||
|
||||
```mermaid
|
||||
--8<-- "web/diagrams/auth/login.mmd"
|
||||
```
|
||||
93
docs/web/diagrams/auth/login.mmd
Normal file
93
docs/web/diagrams/auth/login.mmd
Normal file
@@ -0,0 +1,93 @@
|
||||
sequenceDiagram
|
||||
actor User
|
||||
participant API as API Controller
|
||||
participant Mediator as MediatR pipeline<br/>(ValidationBehavior)
|
||||
box Handler Layer
|
||||
participant LoginHandler
|
||||
participant UserMgr as UserManager(ApplicationUser)
|
||||
participant TokenSvc as TokenService
|
||||
end
|
||||
box Infrastructure Layer
|
||||
participant Argon2 as Argon2PasswordHasher
|
||||
participant JWT as JWT Infrastructure
|
||||
end
|
||||
box Repository / Cross-slice
|
||||
participant UserStore as DapperUserStore<br/>(IUserStore/IUserPasswordStore/IUserEmailStore)
|
||||
end
|
||||
participant DB as SQL Server
|
||||
|
||||
User->>API: POST /api/auth/login {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
|
||||
|
||||
alt Validation fails
|
||||
Mediator->>API: throws FluentValidation.ValidationException
|
||||
API->>User: 400 Bad Request {message, 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 ... FROM UserAccount WHERE Username = @Username
|
||||
activate DB
|
||||
DB-->>UserStore: UserAccount row or null
|
||||
deactivate DB
|
||||
UserStore->>UserStore: Hydrate (active UserCredential.Hash, UserVerification → EmailConfirmed)
|
||||
UserStore-->>UserMgr: ApplicationUser or null
|
||||
deactivate UserStore
|
||||
UserMgr-->>LoginHandler: ApplicationUser or null
|
||||
deactivate UserMgr
|
||||
|
||||
alt User not found
|
||||
LoginHandler->>Mediator: throw UnauthorizedException "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 credential hydrated) without calling the hasher — otherwise delegates to Argon2PasswordHasher
|
||||
UserMgr->>Argon2: VerifyHashedPassword(user, user.PasswordHash, password)
|
||||
activate Argon2
|
||||
Note right of Argon2: Parse stored PHC string ($argon2id$v=19$m=...,t=...,p=...$salt$hash), rehash password with the embedded salt/parameters, constant-time compare (CryptographicOperations.FixedTimeEquals)
|
||||
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(userAccountId, username, refreshToken, accessToken)
|
||||
deactivate LoginHandler
|
||||
Mediator-->>API: LoginPayload
|
||||
deactivate Mediator
|
||||
API->>User: 200 OK {message: "Logged in successfully.", payload: {...}}
|
||||
end
|
||||
end
|
||||
end
|
||||
deactivate API
|
||||
5
docs/web/diagrams/auth/refresh-token.md
Normal file
5
docs/web/diagrams/auth/refresh-token.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# User Authentication Flow — Refresh Token
|
||||
|
||||
```mermaid
|
||||
--8<-- "web/diagrams/auth/refresh-token.mmd"
|
||||
```
|
||||
77
docs/web/diagrams/auth/refresh-token.mmd
Normal file
77
docs/web/diagrams/auth/refresh-token.mmd
Normal file
@@ -0,0 +1,77 @@
|
||||
sequenceDiagram
|
||||
actor User
|
||||
participant API as API Controller
|
||||
participant Mediator as MediatR pipeline<br/>(ValidationBehavior)
|
||||
box Handler Layer
|
||||
participant RefreshHandler as RefreshTokenHandler
|
||||
participant UserMgr as UserManager(ApplicationUser)
|
||||
participant TokenSvc as TokenService
|
||||
end
|
||||
box Infrastructure Layer
|
||||
participant JWT as JWT Infrastructure
|
||||
end
|
||||
box Repository / Cross-slice
|
||||
participant UserStore as DapperUserStore<br/>(IUserStore/IUserPasswordStore/IUserEmailStore)
|
||||
end
|
||||
participant DB as SQL Server
|
||||
|
||||
User->>API: POST /api/auth/refresh<br/>Header: 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.
|
||||
|
||||
alt Validation fails
|
||||
Mediator->>API: throws FluentValidation.ValidationException
|
||||
API->>User: 400 Bad Request {message, 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
|
||||
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 ... FROM UserAccount WHERE 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 "User account not found"
|
||||
RefreshHandler->>Mediator: propagate
|
||||
Mediator->>API: propagate
|
||||
API->>User: 401 Unauthorized
|
||||
else User found
|
||||
TokenSvc->>JWT: GenerateAccessToken(user) + GenerateRefreshToken(user)
|
||||
JWT-->>TokenSvc: new access + refresh tokens
|
||||
TokenSvc-->>RefreshHandler: RefreshTokenResult
|
||||
deactivate TokenSvc
|
||||
|
||||
RefreshHandler-->>Mediator: LoginPayload(userId, username, refreshToken, accessToken)
|
||||
deactivate RefreshHandler
|
||||
Mediator-->>API: LoginPayload
|
||||
deactivate Mediator
|
||||
API->>User: 200 OK {message: "Token refreshed successfully.", payload: {...}}
|
||||
end
|
||||
end
|
||||
end
|
||||
deactivate API
|
||||
5
docs/web/diagrams/auth/registration.md
Normal file
5
docs/web/diagrams/auth/registration.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# User Authentication Flow — Registration
|
||||
|
||||
```mermaid
|
||||
--8<-- "web/diagrams/auth/registration.mmd"
|
||||
```
|
||||
165
docs/web/diagrams/auth/registration.mmd
Normal file
165
docs/web/diagrams/auth/registration.mmd
Normal file
@@ -0,0 +1,165 @@
|
||||
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
|
||||
5
docs/web/diagrams/auth/resend-confirmation.md
Normal file
5
docs/web/diagrams/auth/resend-confirmation.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# User Authentication Flow — Resend Confirmation Email
|
||||
|
||||
```mermaid
|
||||
--8<-- "web/diagrams/auth/resend-confirmation.mmd"
|
||||
```
|
||||
71
docs/web/diagrams/auth/resend-confirmation.mmd
Normal file
71
docs/web/diagrams/auth/resend-confirmation.mmd
Normal file
@@ -0,0 +1,71 @@
|
||||
sequenceDiagram
|
||||
actor User
|
||||
participant API as API Controller
|
||||
participant Mediator as MediatR pipeline<br/>(ValidationBehavior)
|
||||
box Handler Layer
|
||||
participant ResendHandler as ResendConfirmationEmailHandler
|
||||
participant UserMgr as UserManager(ApplicationUser)
|
||||
participant TokenSvc as TokenService
|
||||
end
|
||||
box Infrastructure Layer
|
||||
participant SMTP as Email Provider
|
||||
participant Template as Template Provider
|
||||
end
|
||||
box Repository / Cross-slice
|
||||
participant UserStore as DapperUserStore<br/>(IUserStore/IUserPasswordStore/IUserEmailStore)
|
||||
participant EmailDispatcher as EmailDispatcher<br/>(Features.Emails)
|
||||
end
|
||||
participant DB as SQL Server
|
||||
|
||||
User->>API: POST /api/auth/confirm/resend?userId={id} (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 ... FROM UserAccount WHERE 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 (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(firstName, email, confirmationToken)
|
||||
activate EmailDispatcher
|
||||
EmailDispatcher->>Template: Render email (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 {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
|
||||
deactivate API
|
||||
5
docs/web/diagrams/auth/update-email.md
Normal file
5
docs/web/diagrams/auth/update-email.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# User Authentication Flow — Update Email
|
||||
|
||||
```mermaid
|
||||
--8<-- "web/diagrams/auth/update-email.mmd"
|
||||
```
|
||||
64
docs/web/diagrams/auth/update-email.mmd
Normal file
64
docs/web/diagrams/auth/update-email.mmd
Normal file
@@ -0,0 +1,64 @@
|
||||
sequenceDiagram
|
||||
actor User
|
||||
participant API as API Controller
|
||||
participant Mediator as MediatR pipeline<br/>(ValidationBehavior)
|
||||
box Handler Layer
|
||||
participant UpdateEmailHandler
|
||||
participant UserMgr as UserManager(ApplicationUser)
|
||||
end
|
||||
box Repository / Cross-slice
|
||||
participant UserStore as DapperUserStore<br/>(IUserStore/IUserPasswordStore/IUserEmailStore)
|
||||
end
|
||||
participant DB as SQL Server
|
||||
|
||||
User->>API: PATCH /api/auth/email {newEmail} (requires Bearer access token)
|
||||
activate API
|
||||
API->>Mediator: Send(UpdateEmailCommand(GetAuthenticatedUserId(), newEmail))
|
||||
activate Mediator
|
||||
Note right of Mediator: ValidationBehavior runs UpdateEmailValidator (same format rules as registration)
|
||||
|
||||
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 — caller must re-confirm via POST /api/auth/confirm/resend
|
||||
UserMgr->>UserStore: UpdateAsync(user)
|
||||
UserStore->>DB: UPDATE UserAccount SET Email = @Email, ...
|
||||
UserStore->>DB: DELETE FROM UserVerification WHERE UserAccountID = @Id
|
||||
UserStore-->>UserMgr: IdentityResult
|
||||
UserMgr-->>UpdateEmailHandler: IdentityResult
|
||||
deactivate UserMgr
|
||||
|
||||
alt !result.Succeeded
|
||||
UpdateEmailHandler->>Mediator: throw ConflictException (DuplicateEmail → "Email address already exists")
|
||||
Mediator->>API: propagate
|
||||
API->>User: 409 Conflict
|
||||
else result.Succeeded
|
||||
UpdateEmailHandler-->>Mediator: UpdateEmailPayload(user.Id, user.Email, user.EmailConfirmed = false)
|
||||
deactivate UpdateEmailHandler
|
||||
Mediator-->>API: UpdateEmailPayload
|
||||
deactivate Mediator
|
||||
API->>User: 200 OK {message: "Email updated successfully.", payload: {...}}
|
||||
end
|
||||
end
|
||||
end
|
||||
deactivate API
|
||||
5
docs/web/diagrams/auth/update-password.md
Normal file
5
docs/web/diagrams/auth/update-password.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# User Authentication Flow — Update Password
|
||||
|
||||
```mermaid
|
||||
--8<-- "web/diagrams/auth/update-password.mmd"
|
||||
```
|
||||
84
docs/web/diagrams/auth/update-password.mmd
Normal file
84
docs/web/diagrams/auth/update-password.mmd
Normal file
@@ -0,0 +1,84 @@
|
||||
sequenceDiagram
|
||||
actor User
|
||||
participant API as API Controller
|
||||
participant Mediator as MediatR pipeline<br/>(ValidationBehavior)
|
||||
box Handler Layer
|
||||
participant UpdatePasswordHandler
|
||||
participant UserMgr as UserManager(ApplicationUser)
|
||||
end
|
||||
box Infrastructure Layer
|
||||
participant Argon2 as Argon2PasswordHasher
|
||||
end
|
||||
box Repository / Cross-slice
|
||||
participant UserStore as DapperUserStore<br/>(IUserStore/IUserPasswordStore/IUserEmailStore)
|
||||
end
|
||||
participant DB as SQL Server
|
||||
|
||||
User->>API: PATCH /api/auth/password {currentPassword, newPassword} (requires Bearer access token)
|
||||
activate API
|
||||
API->>Mediator: Send(UpdatePasswordCommand(GetAuthenticatedUserId(), currentPassword, newPassword))
|
||||
activate Mediator
|
||||
Note right of Mediator: ValidationBehavior runs UpdatePasswordValidator (same password strength rules as registration)
|
||||
|
||||
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(user, currentPassword, newPassword)
|
||||
activate UserMgr
|
||||
UserMgr->>Argon2: VerifyHashedPassword(user, user.PasswordHash, currentPassword)
|
||||
activate Argon2
|
||||
Argon2-->>UserMgr: Success/Failed
|
||||
deactivate Argon2
|
||||
|
||||
alt Current password mismatch
|
||||
UserMgr-->>UpdatePasswordHandler: IdentityResult.Failed (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 UserCredential SET IsRevoked = 1, RevokedAt = GETDATE() WHERE UserAccountId = @Id AND IsRevoked = 0
|
||||
UserStore->>DB: INSERT INTO UserCredential (UserAccountId, Hash)
|
||||
Note right of UserStore: Revoke-then-insert runs inside one DbTransaction (credential rotation)
|
||||
UserStore-->>UserMgr: (void)
|
||||
deactivate UserStore
|
||||
UserMgr-->>UpdatePasswordHandler: IdentityResult.Success
|
||||
end
|
||||
deactivate UserMgr
|
||||
|
||||
alt Current password is incorrect
|
||||
UpdatePasswordHandler->>Mediator: throw UnauthorizedException "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(user.Id, DateTime.UtcNow)
|
||||
deactivate UpdatePasswordHandler
|
||||
Mediator-->>API: UpdatePasswordPayload
|
||||
deactivate Mediator
|
||||
API->>User: 200 OK {message: "Password updated successfully.", payload: {...}}
|
||||
end
|
||||
end
|
||||
end
|
||||
deactivate API
|
||||
5
docs/web/diagrams/auth/update-profile.md
Normal file
5
docs/web/diagrams/auth/update-profile.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# User Authentication Flow — Update Profile
|
||||
|
||||
```mermaid
|
||||
--8<-- "web/diagrams/auth/update-profile.mmd"
|
||||
```
|
||||
63
docs/web/diagrams/auth/update-profile.mmd
Normal file
63
docs/web/diagrams/auth/update-profile.mmd
Normal file
@@ -0,0 +1,63 @@
|
||||
sequenceDiagram
|
||||
actor User
|
||||
participant API as API Controller
|
||||
participant Mediator as MediatR pipeline<br/>(ValidationBehavior)
|
||||
box Handler Layer
|
||||
participant UpdateProfileHandler
|
||||
participant UserMgr as UserManager(ApplicationUser)
|
||||
end
|
||||
box Repository / Cross-slice
|
||||
participant UserStore as DapperUserStore<br/>(IUserStore/IUserPasswordStore/IUserEmailStore)
|
||||
end
|
||||
participant DB as SQL Server
|
||||
|
||||
User->>API: PATCH /api/auth/profile {firstName, lastName, dateOfBirth} (requires Bearer access token)
|
||||
activate API
|
||||
API->>Mediator: Send(UpdateProfileCommand(GetAuthenticatedUserId(), firstName, lastName, dateOfBirth))
|
||||
activate Mediator
|
||||
Note right of Mediator: ValidationBehavior runs UpdateProfileValidator (same name/DateOfBirth rules as registration)
|
||||
|
||||
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 = ...<br/>user.LastName = ...<br/>user.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
|
||||
|
||||
UpdateProfileHandler->>UserMgr: UpdateAsync(user)
|
||||
activate UserMgr
|
||||
UserMgr->>UserStore: UpdateAsync(user)
|
||||
UserStore->>DB: UPDATE UserAccount SET FirstName = @FirstName, LastName = @LastName, 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(user.Id, firstName, lastName, dateOfBirth)
|
||||
deactivate UpdateProfileHandler
|
||||
Mediator-->>API: UpdateProfilePayload
|
||||
deactivate Mediator
|
||||
API->>User: 200 OK {message: "Profile updated successfully.", payload: {...}}
|
||||
end
|
||||
end
|
||||
end
|
||||
deactivate API
|
||||
5
docs/web/diagrams/auth/update-username.md
Normal file
5
docs/web/diagrams/auth/update-username.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# User Authentication Flow — Update Username
|
||||
|
||||
```mermaid
|
||||
--8<-- "web/diagrams/auth/update-username.mmd"
|
||||
```
|
||||
68
docs/web/diagrams/auth/update-username.mmd
Normal file
68
docs/web/diagrams/auth/update-username.mmd
Normal file
@@ -0,0 +1,68 @@
|
||||
sequenceDiagram
|
||||
actor User
|
||||
participant API as API Controller
|
||||
participant Mediator as MediatR pipeline<br/>(ValidationBehavior)
|
||||
box Handler Layer
|
||||
participant UpdateUsernameHandler
|
||||
participant UserMgr as UserManager(ApplicationUser)
|
||||
end
|
||||
box Repository / Cross-slice
|
||||
participant UserStore as DapperUserStore<br/>(IUserStore/IUserPasswordStore/IUserEmailStore)
|
||||
end
|
||||
participant DB as SQL Server
|
||||
|
||||
User->>API: PATCH /api/auth/username {newUsername} (requires Bearer access token)
|
||||
activate API
|
||||
API->>Mediator: Send(UpdateUsernameCommand(GetAuthenticatedUserId(), newUsername))
|
||||
activate Mediator
|
||||
Note right of Mediator: ValidationBehavior runs UpdateUsernameValidator (same format rules as registration)
|
||||
|
||||
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 ... FROM UserAccount WHERE 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
|
||||
UserMgr->>UserStore: UpdateAsync(user)
|
||||
UserStore->>DB: UPDATE UserAccount SET Username = @UserName, ...
|
||||
UserStore-->>UserMgr: IdentityResult
|
||||
UserMgr-->>UpdateUsernameHandler: IdentityResult
|
||||
deactivate UserMgr
|
||||
|
||||
alt !result.Succeeded
|
||||
UpdateUsernameHandler->>Mediator: throw ConflictException (DuplicateUserName → "Username already exists")
|
||||
Mediator->>API: propagate
|
||||
API->>User: 409 Conflict
|
||||
else result.Succeeded
|
||||
UpdateUsernameHandler-->>Mediator: UpdateUsernamePayload(user.Id, user.UserName)
|
||||
deactivate UpdateUsernameHandler
|
||||
Mediator-->>API: UpdateUsernamePayload
|
||||
deactivate Mediator
|
||||
API->>User: 200 OK {message: "Username updated successfully.", payload: {...}}
|
||||
end
|
||||
end
|
||||
end
|
||||
deactivate API
|
||||
Reference in New Issue
Block a user