mirror of
https://github.com/System-End/identity-vault.git
synced 2026-04-19 19:45:08 +00:00
parent
7d0a98ab11
commit
ca58cc3bec
16 changed files with 297 additions and 262 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
module Backend
|
||||
class NoAuthController < ApplicationController
|
||||
class NoAuthController < Backend::ApplicationController
|
||||
skip_before_action :authenticate_user!
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
<div class="padding">
|
||||
<%= render Backend::Users::Form.new @user %>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
<div class="padding">
|
||||
<%= 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 %>
|
||||
</div>
|
||||
<div class="padding">
|
||||
<div class="list">
|
||||
<table class="detailed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Email</th>
|
||||
<th>Roles</th>
|
||||
<th>Active?</th>
|
||||
<th>View</th>
|
||||
|
|
@ -14,8 +21,12 @@
|
|||
<% @users.each do |user| %>
|
||||
<tr>
|
||||
<td>
|
||||
<%= render user %>
|
||||
<%= user.display_name %>
|
||||
<% if user.orphaned? %>
|
||||
<span class="badge badge-warning">⚠️ Not Linked</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td><%= user.email || "—" %></td>
|
||||
<td><%= user.pretty_roles %></td>
|
||||
<td><%= render_checkbox(user.active?) %></td>
|
||||
<td><%= link_to "go!", user, class: "link" %></td>
|
||||
|
|
@ -27,7 +38,7 @@
|
|||
</div>
|
||||
<div class="padding flex justify-end">
|
||||
<%= link_to new_backend_user_path, class: "button w-fit" do %>
|
||||
+ create user
|
||||
+ grant backend access
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
<div class="padding">
|
||||
<%= render Backend::Users::Form.new @user %>
|
||||
<h3>Search for an Identity</h3>
|
||||
<%= 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 %>
|
||||
</div>
|
||||
|
||||
<% if params[:query].present? %>
|
||||
<div class="padding">
|
||||
<% if @identities.any? %>
|
||||
<table class="detailed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Slack ID</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @identities.each do |identity| %>
|
||||
<tr>
|
||||
<td><%= "#{identity.first_name} #{identity.last_name}" %></td>
|
||||
<td><%= identity.primary_email %></td>
|
||||
<td><%= identity.slack_id || "—" %></td>
|
||||
<td>
|
||||
<%= link_to "Grant Access →", new_backend_user_path(identity_id: identity.id), class: "link" %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% else %>
|
||||
<p class="text-muted">No identities found matching "<%= params[:query] %>"</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if params[:identity_id].present? %>
|
||||
<% identity = Identity.find(params[:identity_id]) %>
|
||||
<div class="padding">
|
||||
<h3>Grant Access to <%= identity.first_name %> <%= identity.last_name %></h3>
|
||||
<p><strong>Email:</strong> <%= identity.primary_email %></p>
|
||||
<p><strong>Slack ID:</strong> <%= identity.slack_id || "Not linked" %></p>
|
||||
|
||||
<%= form_with url: backend_users_path, method: :post do |f| %>
|
||||
<%= hidden_field_tag :identity_id, identity.id %>
|
||||
|
||||
<div class="padding-top">
|
||||
<h4>Set Permissions</h4>
|
||||
<%= render Backend::Users::PermissionsForm.new(Backend::User.new) %>
|
||||
</div>
|
||||
|
||||
<div class="padding-top">
|
||||
<%= f.submit "Grant Backend Access", class: "button" %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
<div class="padding">
|
||||
<%= render Components::UserMention.new(@user) %>
|
||||
<b>Roles: </b> <%= @user.pretty_roles %>
|
||||
<br>
|
||||
<b>Organized Programs: </b>
|
||||
<% if @user.organized_programs.any? %>
|
||||
<%= @user.organized_programs.map(&:name).join(", ") %>
|
||||
<% if @user.orphaned? %>
|
||||
<div class="alert alert-warning">
|
||||
⚠️ <strong>Warning:</strong> This user is not linked to an Identity and cannot log in.
|
||||
</div>
|
||||
<% else %>
|
||||
<em>None</em>
|
||||
<div class="card">
|
||||
<h4>Linked Identity</h4>
|
||||
<p><strong>Name:</strong> <%= @user.first_name %> <%= @user.last_name %></p>
|
||||
<p><strong>Email:</strong> <%= @user.email %></p>
|
||||
<p><strong>Slack ID:</strong> <%= @user.slack_id || "Not linked" %></p>
|
||||
</div>
|
||||
<% 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} %>
|
||||
<i>(this will stop them from logging in)</i>
|
||||
<% else %>
|
||||
<%= button_to "activate this user", {action: :activate} %>
|
||||
<i>(this will allow them to log in again)</i>
|
||||
|
||||
<div class="padding-top">
|
||||
<b>Roles: </b> <%= @user.pretty_roles %>
|
||||
<br>
|
||||
<b>Organized Programs: </b>
|
||||
<% if @user.organized_programs.any? %>
|
||||
<%= @user.organized_programs.map(&:name).join(", ") %>
|
||||
<% else %>
|
||||
<em>None</em>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% super_admin_tool do %>
|
||||
<div class="padding-top">
|
||||
<%= link_to "edit this user", edit_backend_user_path(@user), class: "link" %>
|
||||
<% if @user.active? %>
|
||||
<%= button_to "deactivate this user", {action: :deactivate} %>
|
||||
<i>(this will stop them from logging in)</i>
|
||||
<% else %>
|
||||
<%= button_to "activate this user", {action: :activate} %>
|
||||
<i>(this will allow them to log in again)</i>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -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<br/> (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 <br/>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<br/>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
|
||||
|
|
|
|||
29
app/views/forms/backend/users/permissions_form.rb
Normal file
29
app/views/forms/backend/users/permissions_form.rb
Normal file
|
|
@ -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<br/> (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 <br/>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<br/>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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
22
db/migrate/20251120195531_link_backend_user_to_identity.rb
Normal file
22
db/migrate/20251120195531_link_backend_user_to_identity.rb
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
5
db/schema.rb
generated
5
db/schema.rb
generated
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue