Use a state machine for user status

The user status is a bit complex, since there are various states and
not all transitions between them make sense.

Using AASM means that we can name and restrict the transitions, which
hopefully makes them easier to reason about.
This commit is contained in:
Andy Allan 2022-01-05 18:44:46 +00:00
parent 786f28993a
commit 1a11c4dc19
12 changed files with 103 additions and 45 deletions

View file

@ -25,7 +25,7 @@ class ConfirmationsController < ApplicationController
render_unknown_user token.user.display_name
else
user = token.user
user.status = "active"
user.activate
user.email_valid = true
flash[:notice] = gravatar_status_message(user) if gravatar_enable(user)
user.save!

View file

@ -46,7 +46,7 @@ class PasswordsController < ApplicationController
if params[:user]
current_user.pass_crypt = params[:user][:pass_crypt]
current_user.pass_crypt_confirmation = params[:user][:pass_crypt_confirmation]
current_user.status = "active" if current_user.status == "pending"
current_user.activate if current_user.may_activate?
current_user.email_valid = true
if current_user.save

View file

@ -164,8 +164,6 @@ class UsersController < ApplicationController
Rails.logger.info "create: #{session[:referer]}"
current_user.status = "pending"
if current_user.auth_provider.present? && current_user.pass_crypt.empty?
# We are creating an account with external authentication and
# no password was specified so create a random one
@ -202,15 +200,17 @@ class UsersController < ApplicationController
##
# sets a user's status
def set_status
@user.status = params[:status]
@user.save
@user.activate! if params[:event] == "activate"
@user.confirm! if params[:event] == "confirm"
@user.hide! if params[:event] == "hide"
@user.unhide! if params[:event] == "unhide"
redirect_to user_path(:display_name => params[:display_name])
end
##
# destroy a user, marking them as deleted and removing personal data
def destroy
@user.destroy
@user.soft_destroy!
redirect_to user_path(:display_name => params[:display_name])
end

View file

@ -45,6 +45,7 @@
class User < ApplicationRecord
require "digest"
include AASM
has_many :traces, -> { where(:visible => true) }
has_many :diary_entries, -> { order(:created_at => :desc) }
@ -158,6 +159,51 @@ class User < ApplicationRecord
user
end
aasm :column => :status, :no_direct_assignment => true do
state :pending, :initial => true
state :active
state :confirmed
state :suspended
state :deleted
# A normal account is active
event :activate do
transitions :from => :pending, :to => :active
end
# Used in test suite, not something that we would normally need to do.
event :deactivate do
transitions :from => :active, :to => :pending
end
# To confirm an account is used to override the spam scoring
event :confirm do
transitions :from => [:pending, :active, :suspended], :to => :confirmed
end
event :suspend do
transitions :from => [:pending, :active], :to => :suspended
end
# Mark the account as deleted but keep all data intact
event :hide do
transitions :from => [:pending, :active, :confirmed, :suspended], :to => :deleted
end
event :unhide do
transitions :from => [:deleted], :to => :active
end
# Mark the account as deleted and remove personal data
event :soft_destroy do
before do
remove_personal_data
end
transitions :from => [:pending, :active, :confirmed, :suspended], :to => :deleted
end
end
def description
RichText.new(self[:description_format], self[:description])
end
@ -241,8 +287,8 @@ class User < ApplicationRecord
end
##
# destroy a user - leave the account but purge most personal data
def destroy
# remove personal data - leave the account but purge most personal data
def remove_personal_data
avatar.purge_later
self.display_name = "user_#{id}"
@ -253,7 +299,6 @@ class User < ApplicationRecord
self.new_email = nil
self.auth_provider = nil
self.auth_uid = nil
self.status = "deleted"
save
end
@ -279,7 +324,7 @@ class User < ApplicationRecord
##
# perform a spam check on a user
def spam_check
update(:status => "suspended") if status == "active" && spam_score > Settings.spam_threshold
suspend! if may_suspend? && spam_score > Settings.spam_threshold
end
##

View file

@ -139,30 +139,32 @@
<nav class='secondary-actions'>
<ul class='clearfix'>
<% if can? :set_status, User %>
<% if ["active", "confirmed"].include? @user.status %>
<% if @user.may_activate? %>
<li>
<%= link_to t(".deactivate_user"), set_status_user_path(:status => "pending", :display_name => @user.display_name), :method => :post, :data => { :confirm => t(".confirm") } %>
</li>
<% elsif ["pending"].include? @user.status %>
<li>
<%= link_to t(".activate_user"), set_status_user_path(:status => "active", :display_name => @user.display_name), :method => :post, :data => { :confirm => t(".confirm") } %>
<%= link_to t(".activate_user"), set_status_user_path(:event => "activate", :display_name => @user.display_name), :method => :post, :data => { :confirm => t(".confirm") } %>
</li>
<% end %>
<% if ["active", "suspended"].include? @user.status %>
<% if @user.may_confirm? %>
<li>
<%= link_to t(".confirm_user"), set_status_user_path(:status => "confirmed", :display_name => @user.display_name), :method => :post, :data => { :confirm => t(".confirm") } %>
<%= link_to t(".confirm_user"), set_status_user_path(:event => "confirm", :display_name => @user.display_name), :method => :post, :data => { :confirm => t(".confirm") } %>
</li>
<% end %>
<% if @user.may_hide? %>
<li>
<%= link_to t(".hide_user"), set_status_user_path(:event => "hide", :display_name => @user.display_name), :method => :post, :data => { :confirm => t(".confirm") } %>
</li>
<% end %>
<% if @user.may_unhide? %>
<li>
<%= link_to t(".unhide_user"), set_status_user_path(:event => "unhide", :display_name => @user.display_name), :method => :post, :data => { :confirm => t(".confirm") } %>
</li>
<% end %>
<li>
<% if ["pending", "active", "confirmed", "suspended"].include? @user.status %>
<%= link_to t(".hide_user"), set_status_user_path(:status => "deleted", :display_name => @user.display_name), :method => :post, :data => { :confirm => t(".confirm") } %>
<% else %>
<%= link_to t(".unhide_user"), set_status_user_path(:status => "active", :display_name => @user.display_name), :method => :post, :data => { :confirm => t(".confirm") } %>
<% end %>
</li>
<% end %>
<% if can? :destroy, User %>
<% if can?(:destroy, User) && @user.may_soft_destroy? %>
<li>
<%= link_to t(".delete_user"), user_path(:display_name => @user.display_name), :method => :delete, :data => { :confirm => t(".confirm") } %>
</li>