@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" 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 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: 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