command_tower 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/command_tower/me/account_controller.rb +35 -0
  3. data/app/deserializers/command_tower/deserializers/me/delete_account_deserializer.rb +31 -0
  4. data/app/errors/command_tower/errors/auth/account_deleted_error.rb +21 -0
  5. data/app/models/user.rb +20 -2
  6. data/app/serializers/command_tower/serializers/me/delete_account_response_serializer.rb +13 -0
  7. data/app/services/command_tower/jwt/authenticate_user.rb +1 -1
  8. data/app/services/command_tower/services/admin/users.rb +2 -2
  9. data/app/services/command_tower/services/auth/email_availability.rb +1 -1
  10. data/app/services/command_tower/services/auth/plain_text/login.rb +1 -1
  11. data/app/services/command_tower/services/auth/purge_user_associated_data.rb +51 -0
  12. data/app/services/command_tower/services/auth/tombstone_user.rb +47 -0
  13. data/app/services/command_tower/services/me/capabilities.rb +6 -5
  14. data/app/services/command_tower/services/me/delete_account.rb +92 -0
  15. data/app/workflows/command_tower/workflows/me/delete_account_workflow.rb +34 -0
  16. data/config/routes.rb +1 -0
  17. data/db/migrate/20260826140000_add_deleted_at_to_users.rb +8 -0
  18. data/docs/upgrades/0.13.0.md +18 -0
  19. data/docs/upgrades/README.md +1 -0
  20. data/lib/command_tower/authorization/default.yml +3 -0
  21. data/lib/command_tower/configuration/account_deletion/config.rb +22 -0
  22. data/lib/command_tower/configuration/config.rb +6 -0
  23. data/lib/command_tower/configuration/registry/audit/config.rb +14 -0
  24. data/lib/command_tower/install/baseline.rb +1 -0
  25. data/lib/command_tower/version.rb +1 -1
  26. metadata +13 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 478979c1e8cabe23c8a2b666d7d42ec610abef048a302f4e8e7a5b5b7d9d74b0
4
- data.tar.gz: 82a5c9c35c78c0a22f1cddf038b203fd0d32433a78be139e386024971189fa05
3
+ metadata.gz: a7116094a133d9fc9edaf8cc0851bde09a3c2bebacded3ea64b1f4a7b4f445db
4
+ data.tar.gz: e4880f05a6c601fb07349d44d27d3a746e10051e8b4ef2b701dc961998862fc4
5
5
  SHA512:
6
- metadata.gz: 22fe0f30d9fdd950b976dfad3cb8818bd51eb7cb4877a550fa1c58de69092e8e99ca940dc0c244c9c0343fb8d875b6ea2730f929ac722da7bcc980dabc45d1f0
7
- data.tar.gz: b5db99a8faa23f2a9d872cae17354f4f005525e5105db924b338778912164fa52cab8c4456cc8c9ea0f6baab4a9a72de44243d713f0d4fa20abadd032294ecde
6
+ metadata.gz: 3633188b6f62b92945cf142ea65fff20a596420b9c0cc4f907d4688732b4d03f283843277e36ea662091b097f71d39d348f4bd06822604f2dace486b3484df38
7
+ data.tar.gz: 749c9b86dd4919c072e1a29af779a84bcd65892f1c95b80f06ded36b4e30844c9598eabf050902d13679cfe911c6b6214898236afd6472ef61fb17370a8b7757
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Me
5
+ class AccountController < CommandTower::ApplicationController
6
+ include CommandTower::Auth::AuthenticationBoundary
7
+ include CommandTower::Auth::AuthorizationBoundary
8
+
9
+ before_action :authenticate_request!
10
+ before_action :authorize_request!
11
+
12
+ def destroy
13
+ deserialized = CommandTower::Deserializers::Me::DeleteAccountDeserializer.call(params)
14
+ return render_deserializer_errors unless deserialized.success?
15
+
16
+ result = CommandTower::Workflows::Me::DeleteAccountWorkflow.call(
17
+ current_user: current_user,
18
+ password: deserialized.input.password
19
+ )
20
+ render_application_result(result)
21
+ end
22
+
23
+ private
24
+
25
+ def render_deserializer_errors
26
+ render_application_result(
27
+ CommandTower::Workflows::WorkflowResult.failure(
28
+ errors: [CommandTower::Errors::ValidationError.new(details: { base: "Missing required fields" })],
29
+ http_status: :unprocessable_entity
30
+ )
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Deserializers
5
+ module Me
6
+ class DeleteAccountDeserializer < CommandTower::Deserializers::ApplicationDeserializer
7
+ Input = Data.define(:password)
8
+
9
+ def call(params)
10
+ password = params[:password]
11
+
12
+ if blank?(password)
13
+ return failure(errors: { message: "missing_required_fields" })
14
+ end
15
+
16
+ unless password.is_a?(String)
17
+ return failure(errors: { message: "invalid_field_types" })
18
+ end
19
+
20
+ success(Input.new(password:))
21
+ end
22
+
23
+ private
24
+
25
+ def blank?(value)
26
+ value.nil? || (value.is_a?(String) && value.strip.empty?)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Errors
5
+ module Auth
6
+ class AccountDeletedError < CommandTower::Errors::UnauthorizedError
7
+ def code
8
+ "account_deleted"
9
+ end
10
+
11
+ def message
12
+ "Account has been deleted"
13
+ end
14
+
15
+ def log_level
16
+ :info
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
data/app/models/user.rb CHANGED
@@ -33,8 +33,14 @@ require "securerandom"
33
33
  class User < CommandTower::ApplicationRecord
34
34
  has_secure_password
35
35
 
36
- validates :username, uniqueness: true
37
- validates :email, uniqueness: true
36
+ TOMBSTONE_EMAIL_DOMAIN = "@invalid.local"
37
+ TOMBSTONE_EMAIL_PREFIX = "deleted+"
38
+ TOMBSTONE_USERNAME_PREFIX = "deleted_"
39
+
40
+ scope :not_deleted, -> { where(deleted_at: nil) }
41
+
42
+ validates :username, uniqueness: { conditions: -> { not_deleted } }
43
+ validates :email, uniqueness: { conditions: -> { not_deleted } }
38
44
 
39
45
  ###
40
46
  # Serialize the roles column to check for inclusion easily
@@ -49,6 +55,18 @@ class User < CommandTower::ApplicationRecord
49
55
  "#{first_name} #{last_name}"
50
56
  end
51
57
 
58
+ def deleted?
59
+ deleted_at.present?
60
+ end
61
+
62
+ def self.tombstone_email_for(id)
63
+ "#{TOMBSTONE_EMAIL_PREFIX}#{id}#{TOMBSTONE_EMAIL_DOMAIN}"
64
+ end
65
+
66
+ def self.tombstone_username_for(id)
67
+ "#{TOMBSTONE_USERNAME_PREFIX}#{id}"
68
+ end
69
+
52
70
  def reset_verifier_token!
53
71
  value = SecureRandom.alphanumeric(32)
54
72
  update!(verifier_token: value, verifier_token_last_reset: Time.now)
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Serializers
5
+ module Me
6
+ class DeleteAccountResponseSerializer
7
+ def self.serialize(message:)
8
+ { message: message }
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -27,7 +27,7 @@ module CommandTower::Jwt
27
27
  return invalid_token if expires_at.nil?
28
28
 
29
29
  user = User.find_by(id: payload[:user_id])
30
- if user.nil?
30
+ if user.nil? || user.deleted?
31
31
  log_warn("user_id [#{payload[:user_id]}] was not found. Cannot Continue")
32
32
  return invalid_token
33
33
  end
@@ -12,7 +12,7 @@ module CommandTower
12
12
  validate :scope_context, is_a: [CommandTower::AdminScope::ScopeContext, NilClass], required: false
13
13
 
14
14
  def call
15
- relation = ::User.order(id: :desc)
15
+ relation = ::User.not_deleted.order(id: :desc)
16
16
  relation = CommandTower::AdminScope::ApplyUsersNarrowing.call(
17
17
  relation:,
18
18
  scope_context:,
@@ -48,7 +48,7 @@ module CommandTower
48
48
  validate :scope_context, is_a: [CommandTower::AdminScope::ScopeContext, NilClass], required: false
49
49
 
50
50
  def call
51
- relation = ::User.where(id:)
51
+ relation = ::User.not_deleted.where(id:)
52
52
  relation = CommandTower::AdminScope::ApplyUsersNarrowing.call(
53
53
  relation:,
54
54
  scope_context:,
@@ -16,7 +16,7 @@ module CommandTower
16
16
  return
17
17
  end
18
18
 
19
- if ::User.exists?(email: normalized)
19
+ if ::User.not_deleted.exists?(email: normalized)
20
20
  context.valid = true
21
21
  context.available = false
22
22
  context.message = "Email is already registered"
@@ -9,7 +9,7 @@ module CommandTower
9
9
  validate :password, is_a: String, required: true, sensitive: true
10
10
 
11
11
  def call
12
- user = User.where(username: identifier).or(User.where(email: identifier)).first
12
+ user = User.not_deleted.where(username: identifier).or(User.not_deleted.where(email: identifier)).first
13
13
  if user.nil?
14
14
  audit_login_failed(affected_user: nil, outcome: "unknown_identifier")
15
15
  invalid_credentials!
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Services
5
+ module Auth
6
+ class PurgeUserAssociatedData < CommandTower::Services::ApplicationService
7
+ validate :user, is_a: User, required: true
8
+
9
+ def call
10
+ purge_user_secrets!
11
+ purge_messaging_inbox!
12
+ purge_messaging_endpoints!
13
+ revoke_impersonation_sessions!
14
+ end
15
+
16
+ private
17
+
18
+ def purge_user_secrets!
19
+ UserSecret.where(user_id: user.id).delete_all
20
+ end
21
+
22
+ def purge_messaging_inbox!
23
+ item_ids = CommandTower::Messaging::Inbox::Scope.for_recipient(user.id).pluck(:id)
24
+ return if item_ids.empty?
25
+
26
+ CommandTower::Messaging::Inbox.bulk_delete(recipient_id: user.id, inbox_item_ids: item_ids)
27
+ end
28
+
29
+ def purge_messaging_endpoints!
30
+ return unless defined?(CommandTower::Messaging::Endpoint)
31
+
32
+ CommandTower::Messaging::Endpoint.where(user_id: user.id).find_each(&:destroy!)
33
+ end
34
+
35
+ def revoke_impersonation_sessions!
36
+ CommandTower::Services::Impersonation::TerminateOpenSessions.call(
37
+ actor_user_id: user.id,
38
+ reason: "revoked"
39
+ )
40
+
41
+ CommandTower::Impersonation::Session.open.where(target_user_id: user.id).find_each do |session|
42
+ CommandTower::Services::Impersonation::End.call(
43
+ session_id: session.id,
44
+ reason: "revoked"
45
+ )
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Services
5
+ module Auth
6
+ class TombstoneUser < CommandTower::Services::ApplicationService
7
+ validate :user, is_a: User, required: true
8
+
9
+ def call
10
+ if user.deleted?
11
+ context.user = user
12
+ context.changed = false
13
+ return
14
+ end
15
+
16
+ user_id = user.id
17
+ scrub_password = SecureRandom.hex(32)
18
+ user.assign_attributes(
19
+ deleted_at: Time.current,
20
+ first_name: "",
21
+ last_name: "",
22
+ email: User.tombstone_email_for(user_id),
23
+ username: User.tombstone_username_for(user_id),
24
+ phone_number: nil,
25
+ phone_number_validated: false,
26
+ email_validated: false,
27
+ roles: []
28
+ )
29
+ user.password = scrub_password
30
+ user.password_confirmation = scrub_password
31
+
32
+ unless user.save(validate: false)
33
+ context.fail!(
34
+ application_error: CommandTower::Errors::InternalError.new
35
+ )
36
+ return
37
+ end
38
+
39
+ user.reset_verifier_token!
40
+
41
+ context.user = user.reload
42
+ context.changed = true
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -15,14 +15,15 @@ module CommandTower
15
15
 
16
16
  def project
17
17
  {
18
- editName: { enabled: true },
18
+ editName: { enabled: !user.deleted? },
19
19
  editUsername: { enabled: false },
20
20
  changeEmail: { enabled: false },
21
- changePassword: { enabled: true },
22
- editPhone: { enabled: sms_enabled? },
23
- editPushover: { enabled: pushover_enabled? },
21
+ changePassword: { enabled: !user.deleted? },
22
+ editPhone: { enabled: sms_enabled? && !user.deleted? },
23
+ editPushover: { enabled: pushover_enabled? && !user.deleted? },
24
24
  logoutAllDevices: { enabled: false },
25
- verifyEmail: { enabled: !user.email_validated }
25
+ verifyEmail: { enabled: !user.deleted? && !user.email_validated },
26
+ deleteAccount: { enabled: !user.deleted? }
26
27
  }
27
28
  end
28
29
 
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Services
5
+ module Me
6
+ class DeleteAccount < CommandTower::Services::ApplicationService
7
+ FIELD_KEYS = {
8
+ password: :password
9
+ }.freeze
10
+
11
+ validate :user, is_a: User, required: true
12
+ validate :password, is_a: String, required: true, sensitive: true
13
+
14
+ def call
15
+ if user.deleted?
16
+ context.fail!(
17
+ application_error: CommandTower::Errors::Auth::AccountDeletedError.new
18
+ )
19
+ return
20
+ end
21
+
22
+ reject!(password: "Incorrect password") unless user.authenticate(password)
23
+
24
+ infrastructure_failure = false
25
+
26
+ ActiveRecord::Base.transaction do
27
+ unless invoke_host_finalizer!
28
+ infrastructure_failure = true
29
+ raise ActiveRecord::Rollback
30
+ end
31
+
32
+ purge_result = CommandTower::Services::Auth::PurgeUserAssociatedData.call(user:)
33
+ unless purge_result.success?
34
+ infrastructure_failure = true
35
+ raise ActiveRecord::Rollback
36
+ end
37
+
38
+ tombstone_result = CommandTower::Services::Auth::TombstoneUser.call(user:)
39
+ unless tombstone_result.success?
40
+ infrastructure_failure = true
41
+ raise ActiveRecord::Rollback
42
+ end
43
+
44
+ user.reload
45
+ audit(
46
+ :account_deleted,
47
+ subject: user,
48
+ affected_user: user,
49
+ changes: {},
50
+ metadata: { mechanism: "self_service" }
51
+ )
52
+ end
53
+
54
+ if infrastructure_failure
55
+ context.fail!(application_error: CommandTower::Errors::InternalError.new)
56
+ return
57
+ end
58
+
59
+ user.reload
60
+ unless user.deleted?
61
+ log_error("Account deletion transaction did not persist for user [#{user.id}]")
62
+ context.fail!(application_error: CommandTower::Errors::InternalError.new)
63
+ end
64
+
65
+ log_info("Account deleted for user [#{user.id}]")
66
+ context.message = "Your account has been deleted"
67
+ end
68
+
69
+ private
70
+
71
+ def invoke_host_finalizer!
72
+ finalizer = CommandTower.config.account_deletion.host_finalizer
73
+ return true if finalizer.nil?
74
+
75
+ outcome = finalizer.call(user:)
76
+ return true if outcome.nil?
77
+ return true if outcome.respond_to?(:success?) && outcome.success?
78
+
79
+ false
80
+ end
81
+
82
+ def reject!(errors)
83
+ details = errors.each_with_object({}) do |(key, message), mapped|
84
+ mapped[FIELD_KEYS.fetch(key.to_sym, key)] = message
85
+ end
86
+
87
+ context.fail!(application_error: CommandTower::Errors::ValidationError.new(details:))
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CommandTower
4
+ module Workflows
5
+ module Me
6
+ class DeleteAccountWorkflow < CommandTower::Workflows::ApplicationWorkflow
7
+ retry_strategy :none
8
+
9
+ def call(current_user:, password:)
10
+ delete_result = CommandTower::Services::Me::DeleteAccount.call(
11
+ user: current_user,
12
+ password:
13
+ )
14
+
15
+ unless delete_result.success?
16
+ error = delete_result.errors.first
17
+ return failure(
18
+ errors: delete_result.errors,
19
+ http_status: CommandTower::Workflows::Me::ErrorStatus.http_status_for(error)
20
+ )
21
+ end
22
+
23
+ success(
24
+ payload: CommandTower::Serializers::Me::DeleteAccountResponseSerializer.serialize(
25
+ message: delete_result.data[:message]
26
+ ),
27
+ http_status: :ok,
28
+ response_effects: { clear_token: true }
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
data/config/routes.rb CHANGED
@@ -60,6 +60,7 @@ CommandTower::Engine.routes.draw do
60
60
  namespace :me do
61
61
  patch "name", to: "name#update"
62
62
  patch "password", to: "password#update"
63
+ delete "account", to: "account#destroy"
63
64
 
64
65
  resources :inbox, only: [:index, :show, :destroy], controller: "inbox" do
65
66
  collection do
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddDeletedAtToUsers < ActiveRecord::Migration[7.2]
4
+ def change
5
+ add_column :users, :deleted_at, :datetime
6
+ add_index :users, :deleted_at
7
+ end
8
+ end
@@ -0,0 +1,18 @@
1
+ # CommandTower 0.13.0
2
+
3
+ Account self-service deletion (CANON-020 / M1).
4
+
5
+ ## Host actions
6
+
7
+ 1. `rails command_tower:install:migrations` (adds `users.deleted_at`)
8
+ 2. `rails db:migrate`
9
+ 3. Grant `me_account` to authenticated member role in `config/rbac_groups.yml`
10
+ 4. Optional: register `config.account_deletion.host_finalizer` for product cleanup
11
+
12
+ ## API
13
+
14
+ - `DELETE /api/me/account` — body `{ "password": "..." }`; clears session on success
15
+
16
+ ## RBAC
17
+
18
+ New entity: `me_account` → `CommandTower::Me::AccountController#destroy`
@@ -3,6 +3,7 @@
3
3
  Host-facing upgrade / change summaries for CommandTower releases.
4
4
 
5
5
  | Version | Summary |
6
+ | [0.13.0](0.13.0.md) | Account self-service deletion (`DELETE /api/me/account`); tombstone + PII scrub; `me_account` RBAC; `users.deleted_at` migration |
6
7
  |---------|---------|
7
8
  | [0.12.0](0.12.0.md) | Intervention envelope serializers/deserializers; product-tool admin_scope without Users/Audit narrowing |
8
9
  | [0.11.1](0.11.1.md) | Audit Event `attribute :json` (MariaDB); audit migration without `utf8mb4_0900_ai_ci` |
@@ -25,6 +25,9 @@ entities:
25
25
  - name: me_password
26
26
  controller: CommandTower::Me::PasswordController
27
27
  only: update
28
+ - name: me_account
29
+ controller: CommandTower::Me::AccountController
30
+ only: destroy
28
31
  - name: me_inbox
29
32
  controller: CommandTower::Me::InboxController
30
33
  only:
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "class_composer"
4
+ require "command_tower/configuration/base"
5
+
6
+ module CommandTower
7
+ module Configuration
8
+ module AccountDeletion
9
+ class Config < ::CommandTower::Configuration::Base
10
+ include ClassComposer::Generator
11
+
12
+ add_composer :host_finalizer,
13
+ desc: "Optional host-owned callable invoked synchronously during account deletion " \
14
+ "before the user row is tombstoned. Receives keyword `user:` (non-deleted User). " \
15
+ "Return value is ignored; raise to abort the deletion transaction.",
16
+ allowed: [Proc, NilClass],
17
+ default: nil,
18
+ default_shown: "nil"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "singleton"
4
4
  require "class_composer"
5
+ require "command_tower/configuration/account_deletion/config"
5
6
  require "command_tower/configuration/admin/config"
6
7
  require "command_tower/configuration/admin_scope/config"
7
8
  require "command_tower/configuration/application/config"
@@ -91,6 +92,11 @@ module CommandTower
91
92
  allowed: Configuration::Messaging::Config,
92
93
  default: Configuration::Messaging::Config.new
93
94
 
95
+ add_composer :account_deletion,
96
+ desc: "Account self-service deletion host hooks",
97
+ allowed: Configuration::AccountDeletion::Config,
98
+ default: Configuration::AccountDeletion::Config.new
99
+
94
100
  add_composer :admin,
95
101
  desc: "Admin configuration for the app",
96
102
  allowed: Configuration::Admin::Config,
@@ -64,6 +64,20 @@ module CommandTower
64
64
  affected_user_required: true,
65
65
  global_visible_in_host_scope: true
66
66
  },
67
+ account_deleted: {
68
+ label: "Account deleted",
69
+ tags: %w[identity account deletion security],
70
+ enabled: true,
71
+ enablement_configurable: false,
72
+ user_history: true,
73
+ sensitive_fields: [],
74
+ allowed_changes: [],
75
+ retention: :permanent,
76
+ subject_required: true,
77
+ subject_type: "User",
78
+ affected_user_required: true,
79
+ global_visible_in_host_scope: true
80
+ },
67
81
  email_verified: {
68
82
  label: "Email verified",
69
83
  tags: %w[identity verification email],
@@ -14,6 +14,7 @@ module CommandTower
14
14
  20260816000001_create_command_tower_audit_events.rb
15
15
  20260817000001_add_scope_columns_to_command_tower_audit_events.rb
16
16
  20260817000003_create_command_tower_impersonation_sessions.rb
17
+ 20260826140000_add_deleted_at_to_users.rb
17
18
  ].freeze
18
19
 
19
20
  module_function
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CommandTower
4
- VERSION = "0.12.0"
4
+ VERSION = "0.13.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: command_tower
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - matt-taylor
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-24 00:00:00.000000000 Z
11
+ date: 2026-08-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: phonelib
@@ -209,6 +209,7 @@ files:
209
209
  - app/controllers/command_tower/auth/session_controller.rb
210
210
  - app/controllers/command_tower/auth/signup_session/create_controller.rb
211
211
  - app/controllers/command_tower/auth/username/availability_controller.rb
212
+ - app/controllers/command_tower/me/account_controller.rb
212
213
  - app/controllers/command_tower/me/audit_events_controller.rb
213
214
  - app/controllers/command_tower/me/inbox_controller.rb
214
215
  - app/controllers/command_tower/me/name_controller.rb
@@ -251,6 +252,7 @@ files:
251
252
  - app/deserializers/command_tower/deserializers/failure.rb
252
253
  - app/deserializers/command_tower/deserializers/intervention/envelope_deserializer.rb
253
254
  - app/deserializers/command_tower/deserializers/me/change_password_deserializer.rb
255
+ - app/deserializers/command_tower/deserializers/me/delete_account_deserializer.rb
254
256
  - app/deserializers/command_tower/deserializers/me/phone_verification/verify_deserializer.rb
255
257
  - app/deserializers/command_tower/deserializers/me/pushover/credentials_deserializer.rb
256
258
  - app/deserializers/command_tower/deserializers/me/update_name_deserializer.rb
@@ -272,6 +274,7 @@ files:
272
274
  - app/errors/command_tower/errors/account/pushover_verification_failed_error.rb
273
275
  - app/errors/command_tower/errors/account/sms_capability_unavailable_error.rb
274
276
  - app/errors/command_tower/errors/application_error.rb
277
+ - app/errors/command_tower/errors/auth/account_deleted_error.rb
275
278
  - app/errors/command_tower/errors/auth/admin_unavailable_during_impersonation_error.rb
276
279
  - app/errors/command_tower/errors/auth/csrf_mismatch_error.rb
277
280
  - app/errors/command_tower/errors/auth/csrf_missing_error.rb
@@ -359,6 +362,7 @@ files:
359
362
  - app/serializers/command_tower/serializers/intervention/remediation_serializer.rb
360
363
  - app/serializers/command_tower/serializers/me/account_serializer.rb
361
364
  - app/serializers/command_tower/serializers/me/change_password_response_serializer.rb
365
+ - app/serializers/command_tower/serializers/me/delete_account_response_serializer.rb
362
366
  - app/serializers/command_tower/serializers/me/pushover_serializer.rb
363
367
  - app/serializers/command_tower/serializers/messaging/inbox.rb
364
368
  - app/serializers/command_tower/serializers/messaging/preferences/catalog_serializer.rb
@@ -619,6 +623,7 @@ files:
619
623
  - app/services/command_tower/services/auth/password_reset/validate.rb
620
624
  - app/services/command_tower/services/auth/plain_text/login.rb
621
625
  - app/services/command_tower/services/auth/principal_capabilities/project.rb
626
+ - app/services/command_tower/services/auth/purge_user_associated_data.rb
622
627
  - app/services/command_tower/services/auth/register.rb
623
628
  - app/services/command_tower/services/auth/signup_rate_limits/check_availability.rb
624
629
  - app/services/command_tower/services/auth/signup_rate_limits/check_register.rb
@@ -628,6 +633,7 @@ files:
628
633
  - app/services/command_tower/services/auth/signup_session/decode.rb
629
634
  - app/services/command_tower/services/auth/signup_session/encode.rb
630
635
  - app/services/command_tower/services/auth/signup_session/validate.rb
636
+ - app/services/command_tower/services/auth/tombstone_user.rb
631
637
  - app/services/command_tower/services/auth/username_availability.rb
632
638
  - app/services/command_tower/services/impersonation/create.rb
633
639
  - app/services/command_tower/services/impersonation/end.rb
@@ -635,6 +641,7 @@ files:
635
641
  - app/services/command_tower/services/impersonation/terminate_open_sessions.rb
636
642
  - app/services/command_tower/services/me/capabilities.rb
637
643
  - app/services/command_tower/services/me/change_password.rb
644
+ - app/services/command_tower/services/me/delete_account.rb
638
645
  - app/services/command_tower/services/me/pushover_product_gate.rb
639
646
  - app/services/command_tower/services/me/sms_product_gate.rb
640
647
  - app/services/command_tower/services/me/update_name.rb
@@ -708,6 +715,7 @@ files:
708
715
  - app/workflows/command_tower/workflows/impersonation/stop_workflow.rb
709
716
  - app/workflows/command_tower/workflows/me/change_password_workflow.rb
710
717
  - app/workflows/command_tower/workflows/me/clear_phone_workflow.rb
718
+ - app/workflows/command_tower/workflows/me/delete_account_workflow.rb
711
719
  - app/workflows/command_tower/workflows/me/error_mapping.rb
712
720
  - app/workflows/command_tower/workflows/me/error_status.rb
713
721
  - app/workflows/command_tower/workflows/me/phone_verification/send_workflow.rb
@@ -739,6 +747,7 @@ files:
739
747
  - db/migrate/20260816000001_create_command_tower_audit_events.rb
740
748
  - db/migrate/20260817000001_add_scope_columns_to_command_tower_audit_events.rb
741
749
  - db/migrate/20260817000003_create_command_tower_impersonation_sessions.rb
750
+ - db/migrate/20260826140000_add_deleted_at_to_users.rb
742
751
  - docs/admin_workspace.md
743
752
  - docs/api_reference.md
744
753
  - docs/architecture.md
@@ -765,6 +774,7 @@ files:
765
774
  - docs/upgrades/0.11.0.md
766
775
  - docs/upgrades/0.11.1.md
767
776
  - docs/upgrades/0.12.0.md
777
+ - docs/upgrades/0.13.0.md
768
778
  - docs/upgrades/README.md
769
779
  - lib/command_tower.rb
770
780
  - lib/command_tower/admin_scope.rb
@@ -787,6 +797,7 @@ files:
787
797
  - lib/command_tower/authorization/effective_entity_grants.rb
788
798
  - lib/command_tower/authorization/entity.rb
789
799
  - lib/command_tower/authorization/role.rb
800
+ - lib/command_tower/configuration/account_deletion/config.rb
790
801
  - lib/command_tower/configuration/admin/config.rb
791
802
  - lib/command_tower/configuration/admin_scope/config.rb
792
803
  - lib/command_tower/configuration/admin_scope/tool_registration.rb