Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions app/components/bootleg_turbo_button.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class Components::BootlegTurboButton < Components::Base
def initialize(path, text:, **opts)
@path = path
@text = text
@opts = opts
end

def view_template
container_id = @opts.delete(:id) || "btb-#{@path.parameterize}"
div(id: container_id, class: "btb-container") do
button(
class: "secondary small-btn",
hx_get: @path,
hx_target: "##{container_id}",
hx_swap: "innerHTML",
**@opts
) { @text }
div(class: "hx-loader") do
vite_image_tag "images/loader.gif", style: "image-rendering: pixelated;"
end
end
end
end
37 changes: 37 additions & 0 deletions app/components/identity_mention.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

class Components::IdentityMention < Components::Base
register_value_helper :current_identity

def initialize(identity)
@identity = identity
end

def view_template
if @identity.nil?
i { "System" }
return
end

span(class: "identity-mention") do
plain display_name

if @identity.backend_user
plain " "
abbr(title: "#{@identity.first_name} is an admin") { "⚡" }
end
end
end

private

def display_name
if @identity == current_identity
"You"
elsif current_identity&.backend_user
"#{@identity.first_name} #{@identity.last_name}"
else
@identity.first_name
end
end
end
25 changes: 15 additions & 10 deletions app/components/public_activity/snippet.rb
Original file line number Diff line number Diff line change
@@ -1,24 +1,29 @@
# frozen_string_literal: true

class Components::PublicActivity::Snippet < Components::Base
def initialize(activity, owner: nil)
def initialize(activity, owner: nil, owner_component: nil)
@activity = activity
@owner = owner
@owner_component = owner_component
end

def view_template
tr do
td do
owner = @owner || @activity.owner

if owner.nil?
i { "System" }
elsif owner.is_a?(::Backend::User)
render Components::UserMention.new(owner)
elsif owner.is_a?(::Identity)
render Components::UserMention.new(owner)
if @owner_component
render @owner_component
else
render owner
owner = @owner || @activity.owner

if owner.nil?
i { "System" }
elsif owner.is_a?(::Backend::User)
render Components::UserMention.new(owner)
elsif owner.is_a?(::Identity)
render Components::UserMention.new(owner)
else
render owner
end
end
end
td { yield }
Expand Down
10 changes: 6 additions & 4 deletions app/components/sidebar.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ def nav_items
items << { label: t("sidebar.addresses"), path: addresses_path, icon: "email" }
items << { label: t("sidebar.security"), path: security_path, icon: "private" }

# Add developer link if developer mode is enabled
if current_identity.present? && current_identity.developer_mode?
items << { label: t("sidebar.developer"), path: developer_apps_path, icon: "code" }
# Add developer link if developer mode is enabled or user is a program manager/super admin
if current_identity.present? && (current_identity.developer_mode? || current_identity.backend_user&.program_manager? || current_identity.backend_user&.super_admin?)
pending_count = current_identity.pending_collaboration_invitations.count
items << { label: t("sidebar.developer"), path: developer_apps_path, icon: "code", badge: pending_count }
end

items << { label: t("sidebar.docs"), path: docs_path, icon: "docs" }
Expand Down Expand Up @@ -95,12 +96,13 @@ def render_navigation
end
end

def render_nav_item(label:, path:, icon: nil)
def render_nav_item(label:, path:, icon: nil, badge: nil)
is_active = @current_path == path

link_to(path, class: [ "sidebar-nav-item", ("active" if is_active) ].compact.join(" ")) do
span(class: "nav-icon") { inline_icon(icon, size: 24) } if icon
span(class: "nav-label") { label }
span(class: "nav-badge") { badge.to_s } if badge && badge > 0
end
end

Expand Down
7 changes: 6 additions & 1 deletion app/controllers/backend/identities_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ def show

@all_programs = @identity.all_programs.distinct

@owned_apps = @identity.owned_developer_apps
@collaborated_apps = @identity.collaborated_programs

verification_ids = @identity.verifications.pluck(:id)
document_ids = @identity.documents.pluck(:id)
break_glass_record_ids = BreakGlassRecord.where(break_glassable_type: "Identity::Document", break_glassable_id: document_ids).pluck(:id)
Expand Down Expand Up @@ -182,7 +185,9 @@ def set_identity
end

def identity_params
params.require(:identity).permit(:first_name, :last_name, :legal_first_name, :legal_last_name, :primary_email, :phone_number, :birthday, :country, :hq_override, :ysws_eligible, :permabanned)
permitted = [ :first_name, :last_name, :legal_first_name, :legal_last_name, :primary_email, :phone_number, :birthday, :country, :hq_override, :ysws_eligible, :permabanned ]
permitted << :can_hq_officialize if current_user&.super_admin?
params.require(:identity).permit(permitted)
end

def vouch_params
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/backend/kbar_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def search_in_scope(scope, query)
id: app.id,
label: app.name,
sublabel: app.redirect_uri&.truncate(50),
path: "/backend/programs/#{app.id}"
path: "/developer/apps/#{app.id}"
}
end
end
Expand Down
94 changes: 0 additions & 94 deletions app/controllers/backend/programs_controller.rb

This file was deleted.

24 changes: 24 additions & 0 deletions app/controllers/concerns/identity_authorizable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

# Opt-in concern for frontend controllers that want Pundit authorization
# with Identity as the authorization subject (instead of Backend::User).
#
# This does NOT affect backend controllers — they continue using
# Backend::User via Backend::ApplicationController.
module IdentityAuthorizable
extend ActiveSupport::Concern

included do
include Pundit::Authorization
after_action :verify_authorized

rescue_from Pundit::NotAuthorizedError do |_e|
flash[:error] = "You're not authorized to do that."
redirect_to root_path
end

def pundit_user
current_identity
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
class DeveloperAppCollaboratorInvitationsController < ApplicationController
include IdentityAuthorizable

before_action :set_invitation

# Invitee accepts
def accept
authorize @invitation
@invitation.update!(identity: current_identity) if @invitation.identity_id.nil?
@invitation.accept!
@invitation.program.create_activity :collaborator_accepted, owner: current_identity
redirect_to developer_apps_path, notice: t(".success")
end

# Invitee declines
def decline
authorize @invitation
@invitation.decline!
@invitation.program.create_activity :collaborator_declined, owner: current_identity
redirect_to developer_apps_path, notice: t(".success")
end

# Owner cancels
def cancel
authorize @invitation
email = @invitation.invited_email
@invitation.cancel!
@invitation.program.create_activity :collaborator_cancelled, owner: current_identity, parameters: { cancelled_email: email }
redirect_to developer_app_path(@invitation.program), notice: t(".success")
end

private

def set_invitation
@invitation = ProgramCollaborator.find(params[:id])
end
end
60 changes: 60 additions & 0 deletions app/controllers/developer_app_collaborators_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
class DeveloperAppCollaboratorsController < ApplicationController
include IdentityAuthorizable

before_action :set_app

def create
authorize @app, :manage_collaborators?

email = params[:email].to_s.strip.downcase

if email == @app.owner_identity&.primary_email
redirect_to developer_app_path(@app), alert: t(".cannot_add_self")
return
end

identity = Identity.find_by(primary_email: email)

collaborator = @app.program_collaborators.find_or_create_by(invited_email: email) do |pc|
pc.identity = identity
end

unless collaborator.persisted?
alert_message = collaborator.errors.full_messages.to_sentence.presence || t(".invalid_email")
redirect_to developer_app_path(@app), alert: alert_message
return
end

reinvitable = collaborator.may_reinvite?
if reinvitable
collaborator.identity = identity
collaborator.reinvite!
end

if collaborator.previously_new_record? || reinvitable
@app.create_activity :collaborator_invited, owner: current_identity, parameters: { invited_email: email }
redirect_to developer_app_path(@app), notice: t(".invited")
else
redirect_to developer_app_path(@app), alert: t(".already_invited")
end
end

def destroy
collaborator = @app.program_collaborators.find(params[:id])
authorize collaborator, :remove?
email = collaborator.invited_email
collaborator.remove!
@app.create_activity :collaborator_removed, owner: current_identity, parameters: { removed_email: email }

redirect_to developer_app_path(@app), notice: t(".success")
end

private

def set_app
@app = Program.find(params[:developer_app_id])
rescue ActiveRecord::RecordNotFound
flash[:error] = t("developer_apps.set_app.not_found")
redirect_to developer_apps_path
end
end
Loading