thepracticaldev/dev.to

View on GitHub
app/controllers/users_controller.rb

Summary

Maintainability
C
1 day
Test Coverage
A
93%
class UsersController < ApplicationController
  before_action :set_no_cache_header
  before_action :raise_suspended, only: %i[update]
  before_action :set_user, only: %i[
    update update_twitch_username update_language_settings confirm_destroy request_destroy full_delete remove_identity
  ]
  after_action :verify_authorized, except: %i[index signout_confirm add_org_admin remove_org_admin remove_from_org]
  before_action :authenticate_user!, only: %i[onboarding_update onboarding_checkbox_update]
  before_action :set_suggested_users, only: %i[index]

  INDEX_ATTRIBUTES_FOR_SERIALIZATION = %i[id name username summary profile_image].freeze
  private_constant :INDEX_ATTRIBUTES_FOR_SERIALIZATION

  def index
    @users =
      case params[:state]
      when "follow_suggestions"
        determine_follow_suggestions(current_user)
      when "sidebar_suggestions"
        Suggester::Users::Sidebar.new(current_user, params[:tag]).suggest.sample(3)
      else
        User.none
      end
  end

  # GET /settings/@tab
  def edit
    unless current_user
      skip_authorization
      return redirect_to sign_up_path
    end
    set_user
    set_tabs(params["tab"] || "profile")
    handle_settings_tab
  end

  # PATCH/PUT /users/:id.:format
  def update
    set_tabs(params["user"]["tab"])

    unless valid_image?
      render :edit, status: :bad_request
      return
    end

    if @user.update(permitted_attributes(@user))
      RssReaderFetchUserWorker.perform_async(@user.id) if @user.feed_url.present?
      notice = "Your profile was successfully updated."
      if config_changed?
        notice = "Your config has been updated. Refresh to see all changes."
      end
      if @user.export_requested?
        notice += " The export will be emailed to you shortly."
        ExportContentWorker.perform_async(@user.id)
      end
      cookies.permanent[:user_experience_level] = @user.experience_level.to_s if @user.experience_level.present?
      follow_hiring_tag(@user)
      flash[:settings_notice] = notice
      @user.touch(:profile_updated_at)
      redirect_to "/settings/#{@tab}"
    else
      Honeycomb.add_field("error", @user.errors.messages.reject { |_, v| v.empty? })
      Honeycomb.add_field("errored", true)

      if @tab
        render :edit, status: :bad_request
      else
        flash[:error] = @user.errors.full_messages.join(", ")
        redirect_to "/settings"
      end
    end
  end

  def update_twitch_username
    set_tabs("integrations")
    new_twitch_username = params[:user][:twitch_username]
    if @user.twitch_username != new_twitch_username
      if @user.update(twitch_username: new_twitch_username)
        @user.touch(:profile_updated_at)
        Streams::TwitchWebhookRegistrationWorker.perform_async(@user.id) if @user.twitch_username?
      end
      flash[:settings_notice] = "Your Twitch username was successfully updated."
    end
    redirect_to "/settings/#{@tab}"
  end

  def update_language_settings
    set_tabs("misc")
    @user.language_settings["preferred_languages"] = Languages::LIST.keys & params[:user][:preferred_languages].to_a
    if @user.save
      flash[:settings_notice] = "Your language settings were successfully updated."
      @user.touch(:profile_updated_at)
      redirect_to "/settings/#{@tab}"
    else
      render :edit
    end
  end

  def request_destroy
    set_tabs("account")

    if destroy_request_in_progress?
      notice = "You have already requested account deletion. Please, check your email for further instructions."
      flash[:settings_notice] = notice
      redirect_to user_settings_path(@tab)
    elsif @user.email?
      Users::RequestDestroy.call(@user)
      notice = "You have requested account deletion. Please, check your email for further instructions."
      flash[:settings_notice] = notice
      redirect_to user_settings_path(@tab)
    else
      flash[:settings_notice] = "Please, provide an email to delete your account."
      redirect_to user_settings_path("account")
    end
  end

  def confirm_destroy
    destroy_token = Rails.cache.read("user-destroy-token-#{@user.id}")
    raise ActionController::RoutingError, "Not Found" unless destroy_token.present? && destroy_token == params[:token]

    set_tabs("account")
  end

  def full_delete
    set_tabs("account")
    if @user.email?
      Users::DeleteWorker.perform_async(@user.id)
      sign_out @user
      flash[:global_notice] = "Your account deletion is scheduled. You'll be notified when it's deleted."
      redirect_to root_path
    else
      flash[:settings_notice] = "Please, provide an email to delete your account"
      redirect_to user_settings_path("account")
    end
  end

  def remove_identity
    set_tabs("account")

    error_message = "An error occurred. Please try again or send an email to: #{SiteConfig.email_addresses[:default]}"
    unless Authentication::Providers.enabled?(params[:provider])
      flash[:error] = error_message
      redirect_to user_settings_path(@tab)
      return
    end

    provider = Authentication::Providers.get!(params[:provider])

    identity = @user.identities.find_by(provider: provider.provider_name)

    if identity && @user.identities.size > 1
      identity.destroy

      @user.update(
        provider.user_username_field => nil,
        :profile_updated_at => Time.current,
      )

      flash[:settings_notice] = "Your #{provider.official_name} account was successfully removed."
    else
      flash[:error] = error_message
    end

    redirect_to user_settings_path(@tab)
  end

  def onboarding_update
    if params[:user]
      sanitize_user_params
      permitted_params = %i[summary location employment_title employer_name last_onboarding_page]
      current_user.assign_attributes(params[:user].permit(permitted_params))
      current_user.profile_updated_at = Time.current
    end
    current_user.saw_onboarding = true
    authorize User
    render_update_response
  end

  def onboarding_checkbox_update
    if params[:user]
      permitted_params = %i[
        checked_code_of_conduct checked_terms_and_conditions email_newsletter email_digest_periodic
      ]
      current_user.assign_attributes(params[:user].permit(permitted_params))
    end

    current_user.saw_onboarding = true
    authorize User
    render_update_response
  end

  def join_org
    authorize User
    if (@organization = Organization.find_by(secret: params[:org_secret].strip))
      OrganizationMembership.create(user_id: current_user.id, organization_id: @organization.id, type_of_user: "member")
      flash[:settings_notice] = "You have joined the #{@organization.name} organization."
      redirect_to "/settings/organization/#{@organization.id}"
    else
      flash[:error] = "The given organization secret was invalid."
      redirect_to "/settings/organization/new"
    end
  end

  def leave_org
    org = Organization.find_by(id: params[:organization_id])
    authorize org
    OrganizationMembership.find_by(organization_id: org.id, user_id: current_user.id)&.delete
    flash[:settings_notice] = "You have left your organization."
    redirect_to "/settings/organization/new"
  end

  def add_org_admin
    adminable = User.find(params[:user_id])
    org = Organization.find_by(id: params[:organization_id])

    not_authorized unless current_user.org_admin?(org) && OrganizationMembership.exists?(user: adminable,
                                                                                         organization: org)

    OrganizationMembership.find_by(user_id: adminable.id, organization_id: org.id).update(type_of_user: "admin")
    flash[:settings_notice] = "#{adminable.name} is now an admin."
    redirect_to "/settings/organization/#{org.id}"
  end

  def remove_org_admin
    unadminable = User.find(params[:user_id])
    org = Organization.find_by(id: params[:organization_id])

    not_authorized unless current_user.org_admin?(org) && unadminable.org_admin?(org)

    OrganizationMembership.find_by(user_id: unadminable.id, organization_id: org.id).update(type_of_user: "member")
    flash[:settings_notice] = "#{unadminable.name} is no longer an admin."
    redirect_to "/settings/organization/#{org.id}"
  end

  def remove_from_org
    removable = User.find(params[:user_id])
    org = Organization.find_by(id: params[:organization_id])
    removable_org_membership = OrganizationMembership.find_by(user_id: removable.id, organization_id: org.id)

    not_authorized unless current_user.org_admin?(org) && removable_org_membership

    removable_org_membership.delete
    flash[:settings_notice] = "#{removable.name} is no longer part of your organization."
    redirect_to "/settings/organization/#{org.id}"
  end

  def signout_confirm; end

  def follow_hiring_tag(user)
    return unless user.looking_for_work?

    hiring_tag = Tag.find_by(name: "hiring")
    return if !hiring_tag || user.following?(hiring_tag)

    Users::FollowWorker.perform_async(user.id, hiring_tag.id, "Tag")
  end

  def handle_settings_tab
    return @tab = "profile" if @tab.blank?

    case @tab
    when "organization"
      handle_organization_tab
    when "integrations"
      handle_integrations_tab
    when "billing"
      handle_billing_tab
    when "account"
      handle_account_tab
    when "response-templates"
      handle_response_templates_tab
    else
      not_found unless @tab_list.map { |t| t.downcase.tr(" ", "-") }.include? @tab
    end
  end

  private

  def sanitize_user_params
    params[:user].delete_if { |_k, v| v.blank? }
  end

  def set_suggested_users
    @suggested_users = SiteConfig.suggested_users
  end

  def default_suggested_users
    @default_suggested_users ||= User.where(username: @suggested_users)
  end

  def determine_follow_suggestions(current_user)
    recent_suggestions = Suggester::Users::Recent.new(
      current_user,
      attributes_to_select: INDEX_ATTRIBUTES_FOR_SERIALIZATION,
    ).suggest

    recent_suggestions.presence || default_suggested_users
  end

  def render_update_response
    if current_user.save
      respond_to do |format|
        format.json { render json: { outcome: "updated successfully" } }
      end
    else
      respond_to do |format|
        format.json { render json: { outcome: "update failed" } }
      end
    end
  end

  def handle_organization_tab
    @organizations = @current_user.organizations.order(name: :asc)
    if params[:org_id] == "new" || params[:org_id].blank? && @organizations.size.zero?
      @organization = Organization.new
    elsif params[:org_id].blank? || params[:org_id].match?(/\d/)
      @organization = Organization.find_by(id: params[:org_id]) || @organizations.first
      authorize @organization, :part_of_org?

      @org_organization_memberships = @organization.organization_memberships.includes(:user)
      @organization_membership = OrganizationMembership.find_by(user_id: current_user.id,
                                                                organization_id: @organization.id)
    end
  end

  def handle_integrations_tab
    @github_repositories_show = current_user.authenticated_through?(:github)
  end

  def handle_billing_tab
    stripe_code = current_user.stripe_id_code
    return if stripe_code == "special"

    @customer = Payments::Customer.get(stripe_code) if stripe_code.present?
  end

  def handle_account_tab
    community_name = ApplicationConfig["COMMUNITY_NAME"]
    @email_body = <<~HEREDOC
      Hello #{community_name} Team,\n
      I would like to delete my account.\n
      You can keep any comments and discussion posts under the Ghost account.\n
      Regards,
      YOUR-#{community_name}-USERNAME-HERE
    HEREDOC
  end

  def handle_response_templates_tab
    @response_templates = current_user.response_templates
    @response_template = ResponseTemplate.find_or_initialize_by(id: params[:id], user: current_user)
  end

  def set_user
    @user = current_user
    not_found unless @user
    authorize @user
  end

  def set_tabs(current_tab = "profile")
    @tab_list = @user.settings_tab_list
    @tab = current_tab
  end

  def config_changed?
    params[:user].include?(:config_theme)
  end

  def less_than_one_day_old?(user)
    # we check all the `_created_at` fields for all available providers
    # we use `.available` and not `.enabled` to avoid a situation in which
    # an admin disables an authentication method after users have already
    # registered, risking that they would be flagged as new
    # the last one is a fallback in case all created_at fields are nil
    user_identity_age = Authentication::Providers.available.map do |provider|
      user.public_send("#{provider}_created_at")
    end.detect(&:present?)

    user_identity_age = user_identity_age.presence || 8.days.ago

    range = 1.day.ago.beginning_of_day..Time.current
    range.cover?(user_identity_age)
  end

  def valid_image?
    image = params.dig("user", "profile_image")
    return true unless image

    return true if valid_image_file?(image) && valid_filename?(image)

    Honeycomb.add_field("error", @user.errors.messages)
    Honeycomb.add_field("errored", true)

    false
  end

  def valid_image_file?(image)
    return true if file?(image)

    @user.errors.add(:profile_image, IS_NOT_FILE_MESSAGE)
    false
  end

  def valid_filename?(image)
    return true unless long_filename?(image)

    @user.errors.add(:profile_image, FILENAME_TOO_LONG_MESSAGE)
    false
  end

  def destroy_request_in_progress?
    Rails.cache.exist?("user-destroy-token-#{@user.id}")
  end
end