From ca58cc3becf0eb5c545287b992be4d83870a6f29 Mon Sep 17 00:00:00 2001 From: nora <163450896+24c02@users.noreply.github.com> Date: Wed, 3 Dec 2025 00:45:07 -0500 Subject: [PATCH] [Backend] Backend::User delenda est. (#66) kill me --- .../backend/application_controller.rb | 28 ++- app/controllers/backend/no_auth_controller.rb | 2 +- .../backend/sessions_controller.rb | 62 ------- app/controllers/backend/users_controller.rb | 35 +++- app/models/backend/user.rb | 171 ++++++------------ app/models/identity.rb | 10 + app/views/backend/users/edit.html.erb | 2 +- app/views/backend/users/index.html.erb | 17 +- app/views/backend/users/new.html.erb | 61 ++++++- app/views/backend/users/show.html.erb | 49 +++-- app/views/forms/backend/users/form.rb | 45 ++--- .../forms/backend/users/permissions_form.rb | 29 +++ config/routes.rb | 15 +- ...120195531_link_backend_user_to_identity.rb | 22 +++ ...2350_remove_slack_id_from_backend_users.rb | 6 + db/schema.rb | 5 +- 16 files changed, 297 insertions(+), 262 deletions(-) delete mode 100644 app/controllers/backend/sessions_controller.rb create mode 100644 app/views/forms/backend/users/permissions_form.rb create mode 100644 db/migrate/20251120195531_link_backend_user_to_identity.rb create mode 100644 db/migrate/20251120212350_remove_slack_id_from_backend_users.rb diff --git a/app/controllers/backend/application_controller.rb b/app/controllers/backend/application_controller.rb index 6a00c06..9f47877 100644 --- a/app/controllers/backend/application_controller.rb +++ b/app/controllers/backend/application_controller.rb @@ -2,6 +2,7 @@ module Backend class ApplicationController < ActionController::Base include PublicActivity::StoreController include Pundit::Authorization + include ::SessionsHelper layout "backend" @@ -10,15 +11,16 @@ module Backend helper_method :current_user, :user_signed_in? before_action :authenticate_user!, :set_honeybadger_context + before_action :require_2fa! before_action :set_paper_trail_whodunnit def current_user - @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id] + @current_user ||= current_identity&.backend_user end def current_impersonator - @current_impersonator ||= User.find_by(id: session[:impersonator_user_id]) if session[:impersonator_user_id] + @current_impersonator ||= Identity.find_by(id: session[:impersonator_user_id])&.backend_user if session[:impersonator_user_id] end alias_method :find_current_auditor, :current_user @@ -30,20 +32,28 @@ module Backend def user_signed_in? = !!current_user def authenticate_user! - unless user_signed_in? - return redirect_to backend_login_path, alert: ("you need to be logged in!") + unless current_identity + session[:return_to] = request.original_url + return redirect_to root_path, alert: "Please log in to access the backend." end - unless @current_user&.active? - session[:user_id] = nil - @current_user = nil - redirect_to backend_login_path, alert: ("you need to be logged in!") + + unless current_user&.active? + redirect_to root_path, alert: "You do not have access to the backend." + end + end + + def require_2fa! + unless current_identity&.use_two_factor_authentication? + redirect_to root_path, alert: "You must enable Two-Factor Authentication to access the backend." end end def set_honeybadger_context Honeybadger.context({ user_id: current_user&.id, - user_username: current_user&.username + user_username: current_user&.username, + identity_id: current_identity&.id, + identity_email: current_identity&.primary_email }) end diff --git a/app/controllers/backend/no_auth_controller.rb b/app/controllers/backend/no_auth_controller.rb index d42631d..eaee367 100644 --- a/app/controllers/backend/no_auth_controller.rb +++ b/app/controllers/backend/no_auth_controller.rb @@ -1,5 +1,5 @@ module Backend - class NoAuthController < ApplicationController + class NoAuthController < Backend::ApplicationController skip_before_action :authenticate_user! end end diff --git a/app/controllers/backend/sessions_controller.rb b/app/controllers/backend/sessions_controller.rb deleted file mode 100644 index b36b9e4..0000000 --- a/app/controllers/backend/sessions_controller.rb +++ /dev/null @@ -1,62 +0,0 @@ -module Backend - class SessionsController < ApplicationController - skip_before_action :authenticate_user!, only: [ :new, :create, :fake_slack_callback_for_dev ] - - skip_after_action :verify_authorized - - def new - redirect_uri = url_for(action: :create, only_path: false) - redirect_to User.authorize_url(redirect_uri), - host: "https://slack.com", - allow_other_host: true - end - - def create - redirect_uri = url_for(action: :create, only_path: false) - - if params[:error].present? - uuid = Honeybadger.notify("Slack OAuth error: #{params[:error]}") - redirect_to backend_login_path, alert: "failed to authenticate with Slack! (error: #{uuid})" - return - end - - begin - @user = User.from_slack_token(params[:code], redirect_uri) - rescue => e - uuid = Honeybadger.notify(e) - redirect_to backend_login_path, alert: "error authenticating! (error: #{uuid})" - return - end - - if @user&.persisted? - session[:user_id] = @user.id - flash[:success] = "welcome aboard!" - redirect_to backend_root_path - else - redirect_to backend_login_path, alert: "you haven't been provisioned an account on this service yet – this attempt been logged." - end - end - - def fake_slack_callback_for_dev - unless Rails.env.development? - Honeybadger.notify("Fake Slack callback attempted in non-development environment. WTF?!") - redirect_to backend_root_path, alert: "this is only available in development mode." - return - end - - @user = User.find_by(slack_id: params[:slack_id], active: true) - if @user.nil? - redirect_to backend_root_path, alert: "dunno who that is, sorry." - return - end - - session[:user_id] = @user.id - redirect_to backend_root_path, notice: "welcome aboard!" - end - - def destroy - session[:user_id] = nil - redirect_to backend_root_path, notice: "bye, see you next time!" - end - end -end diff --git a/app/controllers/backend/users_controller.rb b/app/controllers/backend/users_controller.rb index 2f58a55..ba7fbd1 100644 --- a/app/controllers/backend/users_controller.rb +++ b/app/controllers/backend/users_controller.rb @@ -5,11 +5,17 @@ module Backend def index authorize Backend::User @users = User.all + @users = @users.left_joins(:identity).where("identities.primary_email ILIKE :q OR identities.first_name ILIKE :q OR identities.last_name ILIKE :q OR users.username ILIKE :q", q: "%#{params[:search]}%") if params[:search].present? + @users = @users.includes(:identity, :organized_programs) end def new authorize User - @user = User.new + @identities = if params[:query].present? + Identity.search(params[:query]).where.not(id: User.linked.select(:identity_id)).limit(20) + else + Identity.none + end end def edit @@ -26,11 +32,28 @@ module Backend def create authorize User - @user = User.new(new_user_params.merge(active: true)) + + unless params[:identity_id].present? + redirect_to new_backend_user_path, alert: "No identity selected" + return + end + identity = Identity.find(params[:identity_id]) + + if User.exists?(identity_id: identity.id) + redirect_to backend_users_path, alert: "This identity already has backend access!" + return + end + + @user = User.new(user_params.merge( + identity: identity, + username: "#{identity.first_name} #{identity.last_name}".strip, + active: true + )) + if @user.save - redirect_to backend_users_path, notice: "User created!" + redirect_to backend_user_path(@user), notice: "Backend access granted to #{identity.primary_email}!" else - render :new + redirect_to new_backend_user_path, alert: @user.errors.full_messages.join(", ") end end @@ -65,9 +88,5 @@ module Backend def user_params params.require(:backend_user).permit(:username, :icon_url, :all_fields_access, :human_endorser, :program_manager, :manual_document_verifier, :super_admin, organized_program_ids: []) end - - def new_user_params - params.require(:backend_user).permit(:slack_id, :username, :all_fields_access, :human_endorser, :program_manager, :manual_document_verifier, :super_admin, organized_program_ids: []) - end end end diff --git a/app/models/backend/user.rb b/app/models/backend/user.rb index ae6ce93..e1b4aa4 100644 --- a/app/models/backend/user.rb +++ b/app/models/backend/user.rb @@ -1,136 +1,67 @@ -# == Schema Information -# -# Table name: backend_users -# -# id :bigint not null, primary key -# active :boolean -# all_fields_access :boolean -# can_break_glass :boolean -# human_endorser :boolean -# icon_url :string -# manual_document_verifier :boolean -# program_manager :boolean -# super_admin :boolean -# username :string -# created_at :datetime not null -# updated_at :datetime not null -# credential_id :string -# slack_id :string -# -# Indexes -# -# index_backend_users_on_slack_id (slack_id) -# -class Backend::User < ApplicationRecord - has_paper_trail +module Backend + class User < ApplicationRecord + self.table_name = "backend_users" - # Organizer positions - programs this backend user organizes - has_many :organizer_positions, class_name: "Backend::OrganizerPosition", foreign_key: "backend_user_id", dependent: :destroy - has_many :organized_programs, through: :organizer_positions, source: :program, class_name: "Program" + belongs_to :identity, optional: true - def self.authorize_url(redirect_uri) - params = { - client_id: ENV["SLACK_CLIENT_ID"], - redirect_uri: redirect_uri, - state: SecureRandom.hex(24), - user_scope: "users.profile:read,users:read,users:read.email" - } + has_many :organizer_positions, class_name: "Backend::OrganizerPosition", foreign_key: "backend_user_id", dependent: :destroy + has_many :organized_programs, through: :organizer_positions, source: :program, class_name: "Program" - URI.parse("https://slack.com/oauth/v2/authorize?#{params.to_query}") - end + validates :username, presence: true, uniqueness: true, if: :orphaned? - def self.from_slack_token(code, redirect_uri) - # Exchange code for token - response = HTTP.post("https://slack.com/api/oauth.v2.access", form: { - client_id: ENV["SLACK_CLIENT_ID"], - client_secret: ENV["SLACK_CLIENT_SECRET"], - code: code, - redirect_uri: redirect_uri - }) + delegate :first_name, :last_name, :slack_id, to: :identity, allow_nil: true - data = JSON.parse(response.body.to_s) + scope :orphaned, -> { where(identity_id: nil) } + scope :linked, -> { where.not(identity_id: nil) } - return nil unless data["ok"] + def orphaned? = identity_id.nil? - # Get users info - user_response = HTTP.auth("Bearer #{data["authed_user"]["access_token"]}") - .get("https://slack.com/api/users.info?user=#{data["authed_user"]["id"]}") + def email = identity&.primary_email - user_data = JSON.parse(user_response.body.to_s) - - return nil unless user_data["ok"] - - slack_id = data.dig("authed_user", "id") - - user = find_by(slack_id:) - - unless user - Honeybadger.notify("User #{slack_id} tried to sign into the backend without an account") - return nil + def display_name + return username if orphaned? + "#{first_name} #{last_name}".strip.presence || email || username || "Unknown User" end - unless user.active? - Honeybadger.notify("User #{slack_id} tried to sign into the backend while inactive") - return nil + def active? = active + def activate! = update!(active: true) + def deactivate! = update!(active: false) + + def super_admin? = super_admin + def program_manager? = program_manager + def manual_document_verifier? = manual_document_verifier + def human_endorser? = human_endorser + def all_fields_access? = all_fields_access + def can_break_glass? = can_break_glass + + # Returns a human-readable string of the user's roles + def pretty_roles + roles = [] + roles << "Super Admin" if super_admin? + roles << "Program Manager" if program_manager? + roles << "Manual Document Verifier" if manual_document_verifier? + roles << "Human Endorser" if human_endorser? + roles << "All Fields Access" if all_fields_access? + roles.presence&.join(", ") || "None" end - user.username ||= user_data.dig("user", "profile", "display_name_normalized") - user.username ||= user_data.dig("user", "profile", "real_name_normalized") - user.username ||= user_data.dig("user", "profile", "username") - user.icon_url = user_data.dig("user", "profile", "image_192") || user_data.dig("user", "profile", "image_72") - # Store the OAuth data - user.save! - user - end - - def activate! - update!(active: true) - end - - def deactivate! - update!(active: false) - end - - def pretty_roles - return "Super admin" if super_admin? - roles = [] - roles << "Program manager" if program_manager? - roles << "Document verifier" if manual_document_verifier? - roles << "Endorser" if human_endorser? - roles << "All fields" if all_fields_access? - roles.join(", ") - end - - # Handle organized program IDs for forms - def organized_program_ids - organized_programs.pluck(:id) - end - - def organized_program_ids=(program_ids) - @pending_program_ids = Array(program_ids).reject(&:blank?) - - # If the user is already persisted, update associations immediately - if persisted? - update_organized_programs - end - end - - # Callback to handle pending program IDs after save - after_save :update_organized_programs, if: -> { @pending_program_ids } - - private - - def update_organized_programs - return unless @pending_program_ids - - # Clear existing organizer positions - organizer_positions.destroy_all - - # Create new organizer positions for selected programs - @pending_program_ids.each do |program_id| - organizer_positions.create!(program_id: program_id) + # Returns an array of organized program IDs + def organized_program_ids + organized_programs.pluck(:id) end - @pending_program_ids = nil + # Sets the organized programs by IDs + def organized_program_ids=(ids) + ids = Array(ids).map(&:to_i).uniq + current_ids = organized_program_ids + # Add new organizer positions + (ids - current_ids).each do |id| + organizer_positions.create(program_id: id) + end + # Remove organizer positions not in the new list + (current_ids - ids).each do |id| + organizer_positions.where(program_id: id).destroy_all + end + end end end diff --git a/app/models/identity.rb b/app/models/identity.rb index 90ca20d..61ef71d 100644 --- a/app/models/identity.rb +++ b/app/models/identity.rb @@ -55,6 +55,16 @@ class Identity < ApplicationRecord has_many :totps, class_name: "Identity::TOTP", dependent: :destroy has_many :backup_codes, class_name: "Identity::BackupCode", dependent: :destroy + has_one :backend_user, class_name: "Backend::User", dependent: :destroy + + def active_for_backend? + backend_user&.active? + end + + + + + has_many :documents, class_name: "Identity::Document", dependent: :destroy has_many :verifications, class_name: "Verification", dependent: :destroy has_many :document_verifications, class_name: "Verification::DocumentVerification", dependent: :destroy diff --git a/app/views/backend/users/edit.html.erb b/app/views/backend/users/edit.html.erb index 7b290ea..d1ba537 100644 --- a/app/views/backend/users/edit.html.erb +++ b/app/views/backend/users/edit.html.erb @@ -1,4 +1,4 @@ -<%= render Components::Window.new("Edit user: #{@user.username}", close_url: backend_users_path, max_width: 500) do %> +<%= render Components::Window.new("Edit user: #{@user.display_name}", close_url: backend_users_path, max_width: 500) do %>
<%= render Backend::Users::Form.new @user %>
diff --git a/app/views/backend/users/index.html.erb b/app/views/backend/users/index.html.erb index 4b8ec99..497ca3c 100644 --- a/app/views/backend/users/index.html.erb +++ b/app/views/backend/users/index.html.erb @@ -1,10 +1,17 @@ -<%= render Components::Window.new("Users", close_url: backend_root_path, max_width: 600) do %> +<%= render Components::Window.new("Users", close_url: backend_root_path, max_width: 700) do %> +
+ <%= form_with url: backend_users_path, method: :get, class: "flex gap" do |f| %> + <%= f.text_field :search, placeholder: "Search by email or name...", value: params[:search], class: "input flex-1" %> + <%= f.submit "Search", class: "button" %> + <% end %> +
+ @@ -14,8 +21,12 @@ <% @users.each do |user| %> + @@ -27,7 +38,7 @@
<%= link_to new_backend_user_path, class: "button w-fit" do %> - + create user + + grant backend access <% end %>
<% end %> diff --git a/app/views/backend/users/new.html.erb b/app/views/backend/users/new.html.erb index de0d9ef..e7f94be 100644 --- a/app/views/backend/users/new.html.erb +++ b/app/views/backend/users/new.html.erb @@ -1,5 +1,62 @@ -<%= render Components::Window.new("New User", close_url: backend_users_path, max_width: 500) do %> +<%= render Components::Window.new("Grant Backend Access", close_url: backend_users_path, max_width: 600) do %>
- <%= render Backend::Users::Form.new @user %> +

Search for an Identity

+ <%= form_with url: new_backend_user_path, method: :get, class: "flex gap" do |f| %> + <%= f.text_field :query, placeholder: "Search by email, name, or Slack ID...", value: params[:query], class: "input flex-1", autofocus: true %> + <%= f.submit "Search", class: "button" %> + <% end %>
+ + <% if params[:query].present? %> +
+ <% if @identities.any? %> +
UserEmail Roles Active? View
- <%= render user %> + <%= user.display_name %> + <% if user.orphaned? %> + ⚠️ Not Linked + <% end %> <%= user.email || "—" %> <%= user.pretty_roles %> <%= render_checkbox(user.active?) %> <%= link_to "go!", user, class: "link" %>
+ + + + + + + + + + <% @identities.each do |identity| %> + + + + + + + <% end %> + +
NameEmailSlack IDAction
<%= "#{identity.first_name} #{identity.last_name}" %><%= identity.primary_email %><%= identity.slack_id || "—" %> + <%= link_to "Grant Access →", new_backend_user_path(identity_id: identity.id), class: "link" %> +
+ <% else %> +

No identities found matching "<%= params[:query] %>"

+ <% end %> +
+ <% end %> + + <% if params[:identity_id].present? %> + <% identity = Identity.find(params[:identity_id]) %> +
+

Grant Access to <%= identity.first_name %> <%= identity.last_name %>

+

Email: <%= identity.primary_email %>

+

Slack ID: <%= identity.slack_id || "Not linked" %>

+ + <%= form_with url: backend_users_path, method: :post do |f| %> + <%= hidden_field_tag :identity_id, identity.id %> + +
+

Set Permissions

+ <%= render Backend::Users::PermissionsForm.new(Backend::User.new) %> +
+ +
+ <%= f.submit "Grant Backend Access", class: "button" %> +
+ <% end %> +
+ <% end %> <% end %> diff --git a/app/views/backend/users/show.html.erb b/app/views/backend/users/show.html.erb index 2cc2dba..20542a1 100644 --- a/app/views/backend/users/show.html.erb +++ b/app/views/backend/users/show.html.erb @@ -1,23 +1,40 @@ -<%= render Components::Window.new("User: #{@user.username}", close_url: backend_users_path) do %> +<%= render Components::Window.new("User: #{@user.display_name}", close_url: backend_users_path) do %>
- <%= render Components::UserMention.new(@user) %> - Roles: <%= @user.pretty_roles %> -
- Organized Programs: - <% if @user.organized_programs.any? %> - <%= @user.organized_programs.map(&:name).join(", ") %> + <% if @user.orphaned? %> +
+ ⚠️ Warning: This user is not linked to an Identity and cannot log in. +
<% else %> - None +
+

Linked Identity

+

Name: <%= @user.first_name %> <%= @user.last_name %>

+

Email: <%= @user.email %>

+

Slack ID: <%= @user.slack_id || "Not linked" %>

+
<% end %> - <% super_admin_tool do %> - <%= link_to "edit this user", edit_backend_user_path(@user), class: "link" %> - <% if @user.active? %> - <%= button_to "deactivate this user", {action: :deactivate} %> - (this will stop them from logging in) - <% else %> - <%= button_to "activate this user", {action: :activate} %> - (this will allow them to log in again) + +
+ Roles: <%= @user.pretty_roles %> +
+ Organized Programs: + <% if @user.organized_programs.any? %> + <%= @user.organized_programs.map(&:name).join(", ") %> + <% else %> + None <% end %> +
+ + <% super_admin_tool do %> +
+ <%= link_to "edit this user", edit_backend_user_path(@user), class: "link" %> + <% if @user.active? %> + <%= button_to "deactivate this user", {action: :deactivate} %> + (this will stop them from logging in) + <% else %> + <%= button_to "activate this user", {action: :activate} %> + (this will allow them to log in again) + <% end %> +
<% end %>
<% end %> diff --git a/app/views/forms/backend/users/form.rb b/app/views/forms/backend/users/form.rb index aa8ec1d..ffc0b1b 100644 --- a/app/views/forms/backend/users/form.rb +++ b/app/views/forms/backend/users/form.rb @@ -1,37 +1,24 @@ -# params.require(:backend_user).permit(:slack_id, :username, :all_fields_access, :human_endorser, :program_manager, :manual_document_verifier, :super_admin) +# params.require(:backend_user).permit(:username, :icon_url, :all_fields_access, :human_endorser, :program_manager, :manual_document_verifier, :super_admin, :can_break_glass, organized_program_ids: []) class Backend::Users::Form < ApplicationForm def view_template(&) - div do - labeled field(:slack_id).input(disabled: !model.new_record?), "Slack ID: " - end - div do - labeled field(:username).input, "Display Name: " - end - b { "Roles: " } - div class: "grid gap align-center", style: "grid-template-columns: max-content auto;" do - check_box(field(:super_admin), "Allows this user access to all permissions
(this includes managing other users)") - check_box(field(:program_manager), "This user can provision API keys and program tags.") - check_box(field(:human_endorser), "This user can mark identities as
human-endorsed.") - check_box(field(:all_fields_access), "This user can view all fields on all identities.") - check_box(field(:manual_document_verifier), "This user can mark documents as
manually verified.") - check_box(field(:can_break_glass), "This user can view ID docs after they've been reviewed.") - end - - b { "Program Organizer Positions: " } - div class: "grid gap", style: "grid-template-columns: 1fr;" do - Program.all.each do |program| - is_organizer = model.organized_programs.include?(program) - - div class: "flex-column" do - div class: "checkbox-row" do - check_box_tag("backend_user[organized_program_ids][]", program.id, is_organizer, id: "organized_program_#{program.id}") - label(for: "organized_program_#{program.id}") { program.name } - end - end + unless model.orphaned? + div class: "card margin-bottom" do + h4 { "Linked Identity" } + p { b { "Name: " }; text "#{model.first_name} #{model.last_name}" } + p { b { "Email: " }; text model.email } + p { b { "Slack ID: " }; text model.slack_id || "Not linked" } end end - submit model.new_record? ? "create!" : "save" + if model.orphaned? + div do + labeled field(:username).input, "Display Name: " + end + end + + render Backend::Users::PermissionsForm.new(model) + + submit "save" end end diff --git a/app/views/forms/backend/users/permissions_form.rb b/app/views/forms/backend/users/permissions_form.rb new file mode 100644 index 0000000..517cbef --- /dev/null +++ b/app/views/forms/backend/users/permissions_form.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class Backend::Users::PermissionsForm < ApplicationForm + def view_template(&) + b { "Roles: " } + div class: "grid gap align-center", style: "grid-template-columns: max-content auto;" do + check_box(field(:super_admin), "Allows this user access to all permissions
(this includes managing other users)") + check_box(field(:program_manager), "This user can provision API keys and program tags.") + check_box(field(:human_endorser), "This user can mark identities as
human-endorsed.") + check_box(field(:all_fields_access), "This user can view all fields on all identities.") + check_box(field(:manual_document_verifier), "This user can mark documents as
manually verified.") + check_box(field(:can_break_glass), "This user can view ID docs after they've been reviewed.") + end + + b { "Program Organizer Positions: " } + div class: "grid gap", style: "grid-template-columns: 1fr;" do + Program.all.each do |program| + is_organizer = model.organized_programs.include?(program) + + div class: "flex-column" do + div class: "checkbox-row" do + check_box_tag("backend_user[organized_program_ids][]", program.id, is_organizer, id: "organized_program_#{program.id}") + label(for: "organized_program_#{program.id}") { program.name } + end + end + end + end + end +end diff --git a/config/routes.rb b/config/routes.rb index 9287646..d4c8fbb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -159,10 +159,13 @@ class SuperAdminConstraint def self.matches?(request) - return false unless request.session[:user_id] + session_token = request.cookie_jar.encrypted[:session_token] + return false unless session_token - user = Backend::User.find_by(id: request.session[:user_id]) - user&.super_admin? + session = IdentitySession.not_expired.find_by(session_token: session_token) + return false unless session&.identity + + session.identity.backend_user&.super_admin? end end @@ -189,12 +192,6 @@ Rails.application.routes.draw do get "login", to: "static_pages#login", as: :login get "session_dump", to: "static_pages#session_dump", as: :session_dump unless Rails.env.production? - get "/auth/slack", to: "sessions#new", as: :slack_auth - get "/auth/slack/callback", to: "sessions#create" - - if Rails.env.development? - post "/auth/slack/fake", to: "sessions#fake_slack_callback_for_dev", as: :fake_slack_callback_for_dev - end resources :users do member do diff --git a/db/migrate/20251120195531_link_backend_user_to_identity.rb b/db/migrate/20251120195531_link_backend_user_to_identity.rb new file mode 100644 index 0000000..1f49c53 --- /dev/null +++ b/db/migrate/20251120195531_link_backend_user_to_identity.rb @@ -0,0 +1,22 @@ +class LinkBackendUserToIdentity < ActiveRecord::Migration[8.0] + def up + add_reference :backend_users, :identity, foreign_key: true, null: true, index: false + + Backend::User.reset_column_information + + Backend::User.find_each do |user| + if user.slack_id.present? + identity = Identity.find_by(slack_id: user.slack_id) + if identity + user.update_column(:identity_id, identity.id) + end + end + end + + add_index :backend_users, :identity_id + end + + def down + remove_reference :backend_users, :identity + end +end diff --git a/db/migrate/20251120212350_remove_slack_id_from_backend_users.rb b/db/migrate/20251120212350_remove_slack_id_from_backend_users.rb new file mode 100644 index 0000000..b671926 --- /dev/null +++ b/db/migrate/20251120212350_remove_slack_id_from_backend_users.rb @@ -0,0 +1,6 @@ +class RemoveSlackIdFromBackendUsers < ActiveRecord::Migration[8.0] + def change + remove_index :backend_users, :slack_id + remove_column :backend_users, :slack_id, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index e8d377f..8b8b291 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -100,7 +100,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_02_173250) do end create_table "backend_users", force: :cascade do |t| - t.string "slack_id" t.string "username" t.string "icon_url" t.boolean "super_admin" @@ -113,7 +112,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_02_173250) do t.boolean "active" t.string "credential_id" t.boolean "can_break_glass" - t.index ["slack_id"], name: "index_backend_users_on_slack_id" + t.bigint "identity_id" + t.index ["identity_id"], name: "index_backend_users_on_identity_id" end create_table "break_glass_records", force: :cascade do |t| @@ -537,6 +537,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_02_173250) do add_foreign_key "addresses", "identities" add_foreign_key "backend_organizer_positions", "backend_users" add_foreign_key "backend_organizer_positions", "oauth_applications", column: "program_id" + add_foreign_key "backend_users", "identities" add_foreign_key "break_glass_records", "backend_users" add_foreign_key "identities", "addresses", column: "primary_address_id" add_foreign_key "identity_aadhaar_records", "identities"