package identity import ( "context" "errors" "fmt" "strings" "time" ) type PasswordChanger interface { ChangeOwnPassword(context.Context, string, string, string, time.Time) (AuthorizationSnapshot, error) } type PasswordChangeCommand struct { AccountID, CurrentPassword, NewPassword string } type PasswordChangeFailure string const ( PasswordChangeInvalidInput PasswordChangeFailure = "invalid_input" PasswordChangeInvalidNewPassword PasswordChangeFailure = "invalid_new_password" PasswordChangeNotFound PasswordChangeFailure = "not_found" PasswordChangeCurrentIncorrect PasswordChangeFailure = "current_password_incorrect" ) var ErrPasswordChange = errors.New("password change failed") type PasswordChangeError struct{ Reason PasswordChangeFailure } func (e *PasswordChangeError) Error() string { return fmt.Sprintf("%s: %s", ErrPasswordChange, e.Reason) } func (e *PasswordChangeError) Unwrap() error { return ErrPasswordChange } func IsPasswordChangeFailure(err error, reason PasswordChangeFailure) bool { var target *PasswordChangeError return errors.As(err, &target) && target.Reason == reason } type PasswordChange struct { store PasswordChanger now func() time.Time } func NewPasswordChange(store PasswordChanger, now func() time.Time) *PasswordChange { if now == nil { now = time.Now } return &PasswordChange{store: store, now: now} } func (change *PasswordChange) Change(ctx context.Context, command PasswordChangeCommand) (Session, error) { command.AccountID = strings.TrimSpace(command.AccountID) command.CurrentPassword = strings.TrimSpace(command.CurrentPassword) command.NewPassword = strings.TrimSpace(command.NewPassword) if command.AccountID == "" || command.CurrentPassword == "" { return Session{}, &PasswordChangeError{Reason: PasswordChangeInvalidInput} } if len(command.NewPassword) < 8 { return Session{}, &PasswordChangeError{Reason: PasswordChangeInvalidNewPassword} } if change == nil || change.store == nil { return Session{}, fmt.Errorf("password change is not configured") } now := change.now() snapshot, err := change.store.ChangeOwnPassword(ctx, command.AccountID, command.CurrentPassword, command.NewPassword, now) if err != nil { return Session{}, err } return sessionFromSnapshot(snapshot, now) } func sessionFromSnapshot(snapshot AuthorizationSnapshot, now time.Time) (Session, error) { account := snapshot.Account if account.Status != "active" { return Session{}, &PasswordChangeError{Reason: PasswordChangeNotFound} } authMode, authorities, valid := roleClaims(account.Role) if !valid { return Session{}, NewPasswordLoginError(LoginFailureInvalidRole) } organizationName := "" if account.Role != "super_admin" { if account.OrganizationID == "" { return Session{}, NewPasswordLoginError(LoginFailureOrganizationRequired) } if snapshot.Organization == nil || snapshot.Organization.ID != account.OrganizationID || snapshot.Organization.Status != "active" { return Session{}, NewPasswordLoginError(LoginFailureOrganizationNotActive) } organizationName = snapshot.Organization.Name } else if snapshot.Organization != nil && snapshot.Organization.ID == account.OrganizationID { organizationName = snapshot.Organization.Name } version := account.SessionVersion return Session{Version: 1, AuthMode: authMode, IssuedAt: now.Unix(), ExpiresAt: now.Add(passwordSessionTTL).Unix(), SessionVersion: &version, User: User{ ID: account.ID, Subject: account.ID, Username: account.Phone, Phone: account.Phone, DisplayName: account.DisplayName, ClientID: "platform", OrganizationID: account.OrganizationID, OrganizationName: organizationName, Role: account.Role, Status: account.Status, Authorities: authorities, Scope: []string{}, }}, nil }