Add functionality to remove and recover identity (#3377)
* Lint some quotation marks * Add BackupData table * Add identity and removal functionality * Test additional functionality * Remove dependent destroy for backup data * Add auth_data_dump column * Add challenge to reserved words * Add more shoulda matchers
This commit is contained in:
parent
3d81753285
commit
22e76bee84
13 changed files with 175 additions and 10 deletions
|
|
@ -75,6 +75,32 @@ class Internal::UsersController < Internal::ApplicationController
|
|||
redirect_to "/internal/users/#{@user.id}/edit"
|
||||
end
|
||||
|
||||
def remove_identity
|
||||
identity = Identity.find(user_params[:identity_id])
|
||||
@user = identity.user
|
||||
begin
|
||||
BackupData.backup!(identity)
|
||||
identity.delete
|
||||
@user.update("#{identity.provider}_username" => nil)
|
||||
flash[:success] = "The #{identity.provider.capitalize} identity was successfully deleted and backed up."
|
||||
rescue StandardError => e
|
||||
flash[:error] = e.message
|
||||
end
|
||||
redirect_to "/internal/users/#{@user.id}/edit"
|
||||
end
|
||||
|
||||
def recover_identity
|
||||
backup = BackupData.find(user_params[:backup_data_id])
|
||||
@user = backup.instance_user
|
||||
begin
|
||||
identity = backup.recover!
|
||||
flash[:success] = "The #{identity.provider} identity was successfully recovered, and the backup was removed."
|
||||
rescue StandardError => e
|
||||
flash[:error] = e.message
|
||||
end
|
||||
redirect_to "/internal/users/#{@user.id}/edit"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def manage_credits
|
||||
|
|
@ -121,7 +147,7 @@ class Internal::UsersController < Internal::ApplicationController
|
|||
new_note note_for_current_role user_status
|
||||
pro merge_user_id add_credits remove_credits
|
||||
add_org_credits remove_org_credits ghostify
|
||||
organization_id
|
||||
organization_id identity_id backup_data_id
|
||||
]
|
||||
params.require(:user).permit(allowed_params)
|
||||
end
|
||||
|
|
|
|||
15
app/models/backup_data.rb
Normal file
15
app/models/backup_data.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
class BackupData < ApplicationRecord
|
||||
belongs_to :instance, polymorphic: true
|
||||
belongs_to :instance_user, class_name: "User"
|
||||
validates :instance_id, :instance_type, :json_data, presence: true
|
||||
|
||||
def self.backup!(instance)
|
||||
BackupData.create!(instance_type: instance.class.name, instance_id: instance.id, instance_user_id: instance.user_id, json_data: instance.attributes)
|
||||
end
|
||||
|
||||
def recover!
|
||||
instance = instance_type.constantize.create!(json_data.to_h)
|
||||
destroy!
|
||||
instance
|
||||
end
|
||||
end
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
class Identity < ApplicationRecord
|
||||
belongs_to :user
|
||||
has_many :backup_data, as: :instance, class_name: "BackupData", dependent: :destroy
|
||||
validates :uid, :provider, presence: true
|
||||
validates :uid, uniqueness: { scope: :provider }, if: proc { |i| i.uid_changed? || i.provider_changed? }
|
||||
validates :user_id, uniqueness: { scope: :provider }, if: proc { |i| i.user_id_changed? || i.provider_changed? }
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class User < ApplicationRecord
|
|||
has_many :classified_listings
|
||||
has_many :poll_votes
|
||||
has_many :poll_skips
|
||||
has_many :backup_data, foreign_key: "instance_user_id", inverse_of: :instance_user, class_name: "BackupData"
|
||||
|
||||
mount_uploader :profile_image, ProfileImageUploader
|
||||
|
||||
|
|
|
|||
|
|
@ -39,6 +39,54 @@
|
|||
<% end %>
|
||||
</div>
|
||||
<%= render "notes" %>
|
||||
<div class="row">
|
||||
<h2>Remove Identity</h2>
|
||||
<p>Removing a social account identity can solve certain sign in issues, for example:</p>
|
||||
<ul>
|
||||
<li>
|
||||
They created an account on dev.to, but deleted their original social account and recreated it with the same Twitter/GitHub username.
|
||||
This will be true if their Twitter/GitHub account's UID does not match their identity's UID. You can use the following third party tools to check:
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://tweeterid.com/?username=<%= @user.twitter_username %>" target="_blank" rel="noopener nofollow">Tweeter ID for Twitter (username is in URL for reference)</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://caius.github.io/github_id/#<%= @user.github_username %>" target="_blank" rel="noopener nofollow">Caius's GitHub ID Lookup Tool</a>
|
||||
</li>
|
||||
</ul>
|
||||
Steps to check:
|
||||
<ol>
|
||||
<li>Click one of the links to check their social account's UID.</li>
|
||||
<li>Confirm whether or not it matches with the identity's UID.</li>
|
||||
<li>If it doesn't match, delete the respective identity.</li>
|
||||
<li>Ask the user to reauthorize their social account via https://dev.to/settings/account</li>
|
||||
</ol>
|
||||
</li>
|
||||
<% @user.identities.each do |identity| %>
|
||||
<%= form_for(@user, url: remove_identity_internal_user_path(@user), html: { method: :delete, onsubmit: "return confirm('Are you sure? This should only be done as a solution for the listed example(s).)" }) do |f| %>
|
||||
<%= f.hidden_field :identity_id, value: identity.id %>
|
||||
<p><b><%= identity.provider.capitalize %> UID: <%= identity.uid %> - Username: <%= identity.auth_data_dump["info"]["nickname"] %></b></p>
|
||||
<%= f.submit "Delete #{identity.provider.capitalize} Identity" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<h3>This should be done only do this if you are certain the user is having the specific problem(s) listed above.</h3>
|
||||
<hr>
|
||||
<h2>Recover a deleted identity:</h2>
|
||||
<% if @user.backup_data %>
|
||||
<ul>
|
||||
<% @user.backup_data.where(instance_type: "Identity").each do |data| %>
|
||||
<li>
|
||||
<%= "#{data.json_data['provider'].capitalize} #{data.instance_type} - UID: #{data.json_data['uid']}" %>
|
||||
<%= form_for(@user, url: recover_identity_internal_user_path(@user), html: { method: :post, onsubmit: "return confirm('Please confirm you want to recover the #{data.json_data['provider'].capitalize} identity.')" }) do |f| %>
|
||||
<%= f.hidden_field :backup_data_id, value: data.id %>
|
||||
<%= f.submit "Recover #{data.json_data['provider'].capitalize} Identity" %>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="row">
|
||||
<h2>Merge User</h2>
|
||||
<p>To merge a duplicate account, make sure you are currently on the page of the user you want to KEEP. Below, add the user id of the account to merge information from, and delete.</p>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<% unless @user.identities.exists?(provider: 'github') %>
|
||||
<% unless @user.identities.exists?(provider: "github") %>
|
||||
<div class="field">
|
||||
<a href="/users/auth/github" class="big-button cta" data-no-instant>
|
||||
<img src="<%= asset_path("github-logo.svg") %>" alt="github logo"> CONNECT GITHUB ACCOUNT
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
<hr />
|
||||
<% end %>
|
||||
|
||||
<% unless @user.identities.exists?(provider: 'twitter') %>
|
||||
<% unless @user.identities.exists?(provider: "twitter") %>
|
||||
<div class="field">
|
||||
<a href="/users/auth/twitter?callback_url=<%= ApplicationConfig["APP_PROTOCOL"] %><%= ApplicationConfig["APP_DOMAIN"] %>/users/auth/twitter/callback" class="big-button cta" data-no-instant>
|
||||
<img src="<%= asset_path("twitter-logo.svg") %> " alt="twitter logo"> CONNECT TWITTER ACCOUNT
|
||||
|
|
@ -37,14 +37,14 @@
|
|||
<% end %>
|
||||
|
||||
<% @user.api_secrets.order(created_at: :desc).each do |api_secret| %>
|
||||
<div class='api__secret__container'>
|
||||
<div class='api__secret__desc'>
|
||||
<p class='title'><%= api_secret.description %></p>
|
||||
<p class='content'><%= api_secret.secret %></p>
|
||||
<p class='subtitle'>Created <%= api_secret.created_at.to_date.to_s %></p>
|
||||
<div class="api__secret__container">
|
||||
<div class="api__secret__desc">
|
||||
<p class="title"><%= api_secret.description %></p>
|
||||
<p class="content"><%= api_secret.secret %></p>
|
||||
<p class="subtitle">Created <%= api_secret.created_at.to_date.to_s %></p>
|
||||
</div>
|
||||
|
||||
<div class='api__secret__revoke'>
|
||||
<div class="api__secret__revoke">
|
||||
<%= form_tag users_api_secret_path(api_secret.id), method: :delete do %>
|
||||
<%= button_tag "Revoke", class: "big-action danger-button" %>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ class ReservedWords
|
|||
rly
|
||||
connect
|
||||
chat_channels
|
||||
challenge
|
||||
email_subscriptions
|
||||
social_previews
|
||||
code-of-conduct
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ Rails.application.routes.draw do
|
|||
post "full_delete"
|
||||
patch "user_status"
|
||||
post "merge"
|
||||
delete "remove_identity"
|
||||
post "recover_identity"
|
||||
end
|
||||
end
|
||||
resources :welcome, only: %i[index create]
|
||||
|
|
|
|||
12
db/migrate/20190703003817_create_backup_data_table.rb
Normal file
12
db/migrate/20190703003817_create_backup_data_table.rb
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
class CreateBackupDataTable < ActiveRecord::Migration[5.2]
|
||||
def change
|
||||
create_table :backup_data do |t|
|
||||
t.bigint :instance_id, null: false
|
||||
t.string :instance_type, null: false
|
||||
t.bigint :instance_user_id
|
||||
t.jsonb :json_data, null: false
|
||||
|
||||
t.timestamps null: false
|
||||
end
|
||||
end
|
||||
end
|
||||
11
db/schema.rb
11
db/schema.rb
|
|
@ -12,7 +12,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 2019_06_28_123548) do
|
||||
ActiveRecord::Schema.define(version: 2019_07_03_003817) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
|
|
@ -144,6 +144,15 @@ ActiveRecord::Schema.define(version: 2019_06_28_123548) do
|
|||
t.index ["user_id"], name: "index_articles_on_user_id"
|
||||
end
|
||||
|
||||
create_table "backup_data", force: :cascade do |t|
|
||||
t.datetime "created_at", null: false
|
||||
t.bigint "instance_id", null: false
|
||||
t.string "instance_type", null: false
|
||||
t.bigint "instance_user_id"
|
||||
t.jsonb "json_data", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
end
|
||||
|
||||
create_table "badge_achievements", force: :cascade do |t|
|
||||
t.bigint "badge_id", null: false
|
||||
t.datetime "created_at", null: false
|
||||
|
|
|
|||
|
|
@ -6,5 +6,12 @@ FactoryBot.define do
|
|||
provider { "github" }
|
||||
token { rand(100_000) }
|
||||
secret { rand(100_000) }
|
||||
auth_data_dump do
|
||||
{
|
||||
"info" => {
|
||||
"nickname" => "something"
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
11
spec/models/backup_data_spec.rb
Normal file
11
spec/models/backup_data_spec.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe BackupData, type: :model do
|
||||
describe "validations" do
|
||||
it { is_expected.to validate_presence_of(:instance_type) }
|
||||
it { is_expected.to validate_presence_of(:instance_id) }
|
||||
it { is_expected.to validate_presence_of(:json_data) }
|
||||
it { is_expected.to belong_to(:instance) }
|
||||
it { is_expected.to belong_to(:instance_user).class_name("User") }
|
||||
end
|
||||
end
|
||||
|
|
@ -45,4 +45,36 @@ RSpec.describe "internal/users", type: :request do
|
|||
expect(user.reload.username).to include("spam")
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE internal/users/:id/remove_identity" do
|
||||
it "removes the given identity" do
|
||||
identity = user.identities.first
|
||||
delete "/internal/users/#{user.id}/remove_identity", params: { user: { identity_id: identity.id } }
|
||||
expect { identity.reload }.to raise_error ActiveRecord::RecordNotFound
|
||||
end
|
||||
|
||||
it "updates their social account's username to nil" do
|
||||
identity = user.identities.first
|
||||
delete "/internal/users/#{user.id}/remove_identity", params: { user: { identity_id: identity.id } }
|
||||
expect(user.reload.github_username).to eq nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST internal/users/:id/recover_identity" do
|
||||
it "recovers a deleted identity" do
|
||||
identity = user.identities.first
|
||||
backup = BackupData.backup!(identity)
|
||||
identity.delete
|
||||
post "/internal/users/#{user.id}/recover_identity", params: { user: { backup_data_id: backup.id } }
|
||||
expect(identity).to eq Identity.first
|
||||
end
|
||||
|
||||
it "deletes the backup data" do
|
||||
identity = user.identities.first
|
||||
backup = BackupData.backup!(identity)
|
||||
identity.delete
|
||||
post "/internal/users/#{user.id}/recover_identity", params: { user: { backup_data_id: backup.id } }
|
||||
expect { backup.reload }.to raise_error ActiveRecord::RecordNotFound
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue