Class: CommandTower::Services::Me::ChangePassword

Inherits:
ApplicationService show all
Defined in:
app/services/command_tower/services/me/change_password.rb

Constant Summary collapse

FIELD_KEYS =
{
  current_password: :currentPassword,
  password: :password,
  password_confirmation: :passwordConfirmation
}.freeze
SUCCESS_MESSAGE =
"Password has been successfully changed"

Constants inherited from CommandTower::ServiceBase

CommandTower::ServiceBase::ON_ARGUMENT_VALIDATION

Instance Method Summary collapse

Methods inherited from ApplicationService

call, inherited

Methods included from Transactional

#fail_transaction!, #transaction

Methods inherited from CommandTower::ServiceBase

#command_tower_lifecycle, inherited, #internal_validate, #service_lifecycle_error_codes, #service_lifecycle_log_level, #validate!

Methods included from Logging::LifecycleDeclaration

included

Methods included from Execution::ContextAccess

#audit, #execution_context, #log_debug, #log_error, #log_info, #log_warn, #publish_event

Methods included from ArgumentValidation

included

Methods included from CommandTower::ServiceLogging

included

Instance Method Details

#callObject



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'app/services/command_tower/services/me/change_password.rb', line 20

def call
  reject!(current_password: "Incorrect current password") unless user.authenticate(current_password)
  reject!(password_confirmation: "Password and confirmation do not match") unless password == password_confirmation

  config = CommandTower.config..plain_text
  if password.length < config.password_length_min || password.length > config.password_length_max
    reject!(
      password: "Password length must be between #{config.password_length_min} and #{config.password_length_max} characters"
    )
  end

  persistence_errors = nil
  infrastructure_failure = false

  ActiveRecord::Base.transaction do
    user.password = password
    user.password_confirmation = password_confirmation

    unless user.save
      persistence_errors = user.errors.to_hash.transform_values { Array(_1).join(", ") }
      raise ActiveRecord::Rollback
    end

    begin
      user.reset_verifier_token!
      CommandTower::Services::Impersonation::TerminateOpenSessions.call(
        actor_user_id: user.id,
        reason: "revoked"
      )
    rescue StandardError => e
      log_error("Failed to rotate session verifier for user [#{user.id}]: #{e.class}")
      infrastructure_failure = true
      raise ActiveRecord::Rollback
    end

    audit(
      :password_changed,
      subject: user,
      affected_user: user,
      changes: {},
      metadata: { mechanism: "authenticated_change" }
    )
  end

  if persistence_errors
    log_error("Failed to update password for user [#{user.id}]")
    reject!(persistence_errors)
  end

  if infrastructure_failure
    context.fail!(application_error: CommandTower::Errors::InternalError.new)
  end

  # Detect silent rollback (neither branch set) — should not occur in normal paths
  user.reload
  unless user.authenticate(password)
    log_error("Password change transaction did not persist for user [#{user.id}]")
    context.fail!(application_error: CommandTower::Errors::InternalError.new)
  end

  log_info("Password changed successfully for user [#{user.id}]")
  context.message = SUCCESS_MESSAGE
end