@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" 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 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). end note User -> API: POST /api/auth/confirm?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 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