summaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2026-07-31 17:05:05 +0200
committererdgeist <erdgeist@erdgeist.org>2026-07-31 17:05:05 +0200
commit8c6a6516e1dc5c1b4f12740a6f7b32765b530bb7 (patch)
treee40a1da656bedef0662f0e984b4e3b00b374bc1d /app/models
parent464dd4266bdc433805010b5dca428f4cb75c2a81 (diff)
Replace user deletion with deactivation
Deactivation adds the alumni role and leaves the others in place, so reactivation is lossless and nobody has to remember what an account held. login_from_session checks alumni? on every request, so a signed-in user is locked out on their next one without any session invalidation. Guards prevent deactivating yourself or the last active admin, and both verbs are witnessed in the action log.
Diffstat (limited to 'app/models')
-rw-r--r--app/models/node_action.rb5
-rw-r--r--app/models/user.rb20
2 files changed, 23 insertions, 2 deletions
diff --git a/app/models/node_action.rb b/app/models/node_action.rb
index fec5a062..d619aac5 100644
--- a/app/models/node_action.rb
+++ b/app/models/node_action.rb
@@ -98,8 +98,9 @@ class NodeAction < ApplicationRecord
98 # "detached_from" -- array of unique_names, only when any 98 # "detached_from" -- array of unique_names, only when any
99 # "headline_removed_from" -- array of unique_names, only when any 99 # "headline_removed_from" -- array of unique_names, only when any
100 # 100 #
101 # "otp_enroll" / "otp_disable" / "otp_reset" (second-factor lifecycle; 101 # "otp_enroll" / "otp_disable" / "otp_reset" / "user_deactivate" /
102 # node column nil; participants: the affected User -- the table's first 102 # "user_reactivate" (second-factor lifecycle; node column nil;
103 # participants: the affected User
103 # User-typed subject. otp_disable is self-service; otp_reset is an 104 # User-typed subject. otp_disable is self-service; otp_reset is an
104 # administrator clearing someone else's factor, where actor and 105 # administrator clearing someone else's factor, where actor and
105 # participant differ): 106 # participant differ):
diff --git a/app/models/user.rb b/app/models/user.rb
index 2e9da86c..1728521a 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -105,6 +105,26 @@ class User < ApplicationRecord
105 roles.map { |r| I18n.t("users.roles.#{r}", :default => r) } 105 roles.map { |r| I18n.t("users.roles.#{r}", :default => r) }
106 end 106 end
107 107
108 def deactivate!(actor:)
109 return false if alumni?
110 transaction do
111 update_column(:roles, (roles | ["alumni"]).sort)
112 NodeAction.record!(:participants => [self], :user => actor,
113 :action => "user_deactivate", :target_login => login)
114 end
115 true
116 end
117
118 def reactivate!(actor:)
119 return false unless alumni?
120 transaction do
121 update_column(:roles, (roles - ["alumni"]).sort)
122 NodeAction.record!(:participants => [self], :user => actor,
123 :action => "user_reactivate", :target_login => login)
124 end
125 true
126 end
127
108 # otp_secret present == enrolled. otp_pending_secret holds the secret 128 # otp_secret present == enrolled. otp_pending_secret holds the secret
109 # between QR display and first-code confirmation. otp_consumed_timestep 129 # between QR display and first-code confirmation. otp_consumed_timestep
110 # makes every accepted code single-use (replay guard within the drift 130 # makes every accepted code single-use (replay guard within the drift