Add user admin
This commit is contained in:
83
app/controllers/admin/applications_controller.rb
Normal file
83
app/controllers/admin/applications_controller.rb
Normal file
@@ -0,0 +1,83 @@
|
||||
module Admin
|
||||
class ApplicationsController < BaseController
|
||||
before_action :set_application, only: [:show, :edit, :update, :destroy, :regenerate_credentials]
|
||||
|
||||
def index
|
||||
@applications = Application.order(created_at: :desc)
|
||||
end
|
||||
|
||||
def show
|
||||
@allowed_groups = @application.allowed_groups
|
||||
end
|
||||
|
||||
def new
|
||||
@application = Application.new
|
||||
@available_groups = Group.order(:name)
|
||||
end
|
||||
|
||||
def create
|
||||
@application = Application.new(application_params)
|
||||
|
||||
if @application.save
|
||||
# Handle group assignments
|
||||
if params[:application][:group_ids].present?
|
||||
group_ids = params[:application][:group_ids].reject(&:blank?)
|
||||
@application.allowed_groups = Group.where(id: group_ids)
|
||||
end
|
||||
|
||||
redirect_to admin_application_path(@application), notice: "Application created successfully."
|
||||
else
|
||||
@available_groups = Group.order(:name)
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@available_groups = Group.order(:name)
|
||||
end
|
||||
|
||||
def update
|
||||
if @application.update(application_params)
|
||||
# Handle group assignments
|
||||
if params[:application][:group_ids].present?
|
||||
group_ids = params[:application][:group_ids].reject(&:blank?)
|
||||
@application.allowed_groups = Group.where(id: group_ids)
|
||||
else
|
||||
@application.allowed_groups = []
|
||||
end
|
||||
|
||||
redirect_to admin_application_path(@application), notice: "Application updated successfully."
|
||||
else
|
||||
@available_groups = Group.order(:name)
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@application.destroy
|
||||
redirect_to admin_applications_path, notice: "Application deleted successfully."
|
||||
end
|
||||
|
||||
def regenerate_credentials
|
||||
if @application.oidc?
|
||||
@application.update!(
|
||||
client_id: SecureRandom.urlsafe_base64(32),
|
||||
client_secret: SecureRandom.urlsafe_base64(48)
|
||||
)
|
||||
redirect_to admin_application_path(@application), notice: "Credentials regenerated successfully. Make sure to update your application configuration."
|
||||
else
|
||||
redirect_to admin_application_path(@application), alert: "Only OIDC applications have credentials."
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_application
|
||||
@application = Application.find(params[:id])
|
||||
end
|
||||
|
||||
def application_params
|
||||
params.require(:application).permit(:name, :slug, :app_type, :active, :redirect_uris, :description, :metadata)
|
||||
end
|
||||
end
|
||||
end
|
||||
14
app/controllers/admin/base_controller.rb
Normal file
14
app/controllers/admin/base_controller.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
module Admin
|
||||
class BaseController < ApplicationController
|
||||
before_action :require_admin
|
||||
|
||||
private
|
||||
|
||||
def require_admin
|
||||
user = Current.session&.user
|
||||
unless user&.admin?
|
||||
redirect_to root_path, alert: "You must be an administrator to access this page."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
12
app/controllers/admin/dashboard_controller.rb
Normal file
12
app/controllers/admin/dashboard_controller.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
module Admin
|
||||
class DashboardController < BaseController
|
||||
def index
|
||||
@user_count = User.count
|
||||
@active_user_count = User.active.count
|
||||
@application_count = Application.count
|
||||
@active_application_count = Application.active.count
|
||||
@group_count = Group.count
|
||||
@recent_users = User.order(created_at: :desc).limit(5)
|
||||
end
|
||||
end
|
||||
end
|
||||
73
app/controllers/admin/groups_controller.rb
Normal file
73
app/controllers/admin/groups_controller.rb
Normal file
@@ -0,0 +1,73 @@
|
||||
module Admin
|
||||
class GroupsController < BaseController
|
||||
before_action :set_group, only: [:show, :edit, :update, :destroy]
|
||||
|
||||
def index
|
||||
@groups = Group.order(:name)
|
||||
end
|
||||
|
||||
def show
|
||||
@members = @group.users.order(:email_address)
|
||||
@applications = @group.applications.order(:name)
|
||||
@available_users = User.where.not(id: @members.pluck(:id)).order(:email_address)
|
||||
end
|
||||
|
||||
def new
|
||||
@group = Group.new
|
||||
@available_users = User.order(:email_address)
|
||||
end
|
||||
|
||||
def create
|
||||
@group = Group.new(group_params)
|
||||
|
||||
if @group.save
|
||||
# Handle user assignments
|
||||
if params[:group][:user_ids].present?
|
||||
user_ids = params[:group][:user_ids].reject(&:blank?)
|
||||
@group.users = User.where(id: user_ids)
|
||||
end
|
||||
|
||||
redirect_to admin_group_path(@group), notice: "Group created successfully."
|
||||
else
|
||||
@available_users = User.order(:email_address)
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
@available_users = User.order(:email_address)
|
||||
end
|
||||
|
||||
def update
|
||||
if @group.update(group_params)
|
||||
# Handle user assignments
|
||||
if params[:group][:user_ids].present?
|
||||
user_ids = params[:group][:user_ids].reject(&:blank?)
|
||||
@group.users = User.where(id: user_ids)
|
||||
else
|
||||
@group.users = []
|
||||
end
|
||||
|
||||
redirect_to admin_group_path(@group), notice: "Group updated successfully."
|
||||
else
|
||||
@available_users = User.order(:email_address)
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
@group.destroy
|
||||
redirect_to admin_groups_path, notice: "Group deleted successfully."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_group
|
||||
@group = Group.find(params[:id])
|
||||
end
|
||||
|
||||
def group_params
|
||||
params.require(:group).permit(:name, :description)
|
||||
end
|
||||
end
|
||||
end
|
||||
70
app/controllers/admin/users_controller.rb
Normal file
70
app/controllers/admin/users_controller.rb
Normal file
@@ -0,0 +1,70 @@
|
||||
module Admin
|
||||
class UsersController < BaseController
|
||||
before_action :set_user, only: [:show, :edit, :update, :destroy]
|
||||
|
||||
def index
|
||||
@users = User.order(created_at: :desc)
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def new
|
||||
@user = User.new
|
||||
end
|
||||
|
||||
def create
|
||||
@user = User.new(user_params)
|
||||
@user.password = SecureRandom.alphanumeric(16) if user_params[:password].blank?
|
||||
|
||||
if @user.save
|
||||
redirect_to admin_users_path, notice: "User created successfully."
|
||||
else
|
||||
render :new, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def edit
|
||||
end
|
||||
|
||||
def update
|
||||
# Prevent changing params for the current user's email and admin status
|
||||
# to avoid locking themselves out
|
||||
update_params = user_params.dup
|
||||
|
||||
if @user == Current.session.user
|
||||
update_params.delete(:admin)
|
||||
end
|
||||
|
||||
# Only update password if provided
|
||||
update_params.delete(:password) if update_params[:password].blank?
|
||||
|
||||
if @user.update(update_params)
|
||||
redirect_to admin_users_path, notice: "User updated successfully."
|
||||
else
|
||||
render :edit, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
# Prevent admin from deleting themselves
|
||||
if @user == Current.session.user
|
||||
redirect_to admin_users_path, alert: "You cannot delete your own account."
|
||||
return
|
||||
end
|
||||
|
||||
@user.destroy
|
||||
redirect_to admin_users_path, notice: "User deleted successfully."
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@user = User.find(params[:id])
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:email_address, :password, :admin, :status)
|
||||
end
|
||||
end
|
||||
end
|
||||
99
app/views/admin/applications/_form.html.erb
Normal file
99
app/views/admin/applications/_form.html.erb
Normal file
@@ -0,0 +1,99 @@
|
||||
<%= form_with(model: [:admin, application], class: "space-y-6") do |form| %>
|
||||
<% if application.errors.any? %>
|
||||
<div class="rounded-md bg-red-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">
|
||||
<%= pluralize(application.errors.count, "error") %> prohibited this application from being saved:
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-red-700">
|
||||
<ul class="list-disc pl-5 space-y-1">
|
||||
<% application.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<%= form.label :name, class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_field :name, required: true, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "My Application" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :slug, class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_field :slug, required: true, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm font-mono", placeholder: "my-app" %>
|
||||
<p class="mt-1 text-sm text-gray-500">Lowercase letters, numbers, and hyphens only. Used in URLs and API calls.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :description, class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_area :description, rows: 3, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "Optional description of this application" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :app_type, "Application Type", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.select :app_type, [["OpenID Connect (OIDC)", "oidc"], ["ForwardAuth (Trusted Headers)", "trusted_header"], ["SAML (Coming Soon)", "saml", { disabled: true }]], {}, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", disabled: application.persisted? %>
|
||||
<% if application.persisted? %>
|
||||
<p class="mt-1 text-sm text-gray-500">Application type cannot be changed after creation.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<!-- OIDC-specific fields -->
|
||||
<div id="oidc-fields" class="space-y-6 border-t border-gray-200 pt-6" style="<%= 'display: none;' unless application.oidc? || !application.persisted? %>">
|
||||
<h3 class="text-base font-semibold text-gray-900">OIDC Configuration</h3>
|
||||
|
||||
<div>
|
||||
<%= form.label :redirect_uris, "Redirect URIs", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_area :redirect_uris, rows: 4, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm font-mono", placeholder: "https://example.com/callback\nhttps://app.example.com/auth/callback" %>
|
||||
<p class="mt-1 text-sm text-gray-500">One URI per line. These are the allowed callback URLs for your application.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :group_ids, "Allowed Groups (Optional)", class: "block text-sm font-medium text-gray-700" %>
|
||||
<div class="mt-2 space-y-2 max-h-48 overflow-y-auto border border-gray-200 rounded-md p-3">
|
||||
<% if @available_groups.any? %>
|
||||
<% @available_groups.each do |group| %>
|
||||
<div class="flex items-center">
|
||||
<%= check_box_tag "application[group_ids][]", group.id, application.allowed_groups.include?(group), class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" %>
|
||||
<%= label_tag "application_group_ids_#{group.id}", group.name, class: "ml-2 text-sm text-gray-900" %>
|
||||
<span class="ml-2 text-xs text-gray-500">(<%= pluralize(group.users.count, "member") %>)</span>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<p class="text-sm text-gray-500">No groups available. Create groups first to restrict access.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-gray-500">If no groups are selected, all active users can access this application.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<%= form.check_box :active, class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" %>
|
||||
<%= form.label :active, "Active", class: "ml-2 block text-sm text-gray-900" %>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<%= form.submit application.persisted? ? "Update Application" : "Create Application", class: "rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" %>
|
||||
<%= link_to "Cancel", admin_applications_path, class: "rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<script>
|
||||
// Show/hide OIDC fields based on app type selection
|
||||
const appTypeSelect = document.querySelector('#application_app_type');
|
||||
const oidcFields = document.querySelector('#oidc-fields');
|
||||
|
||||
if (appTypeSelect && oidcFields) {
|
||||
appTypeSelect.addEventListener('change', function() {
|
||||
if (this.value === 'oidc') {
|
||||
oidcFields.style.display = 'block';
|
||||
} else {
|
||||
oidcFields.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
5
app/views/admin/applications/edit.html.erb
Normal file
5
app/views/admin/applications/edit.html.erb
Normal file
@@ -0,0 +1,5 @@
|
||||
<div class="max-w-3xl">
|
||||
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Edit Application</h1>
|
||||
<p class="text-sm text-gray-600 mb-6">Editing: <%= @application.name %></p>
|
||||
<%= render "form", application: @application %>
|
||||
</div>
|
||||
71
app/views/admin/applications/index.html.erb
Normal file
71
app/views/admin/applications/index.html.erb
Normal file
@@ -0,0 +1,71 @@
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-2xl font-semibold text-gray-900">Applications</h1>
|
||||
<p class="mt-2 text-sm text-gray-700">Manage OIDC and ForwardAuth applications.</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
||||
<%= link_to "New Application", new_admin_application_path, class: "block rounded-md bg-blue-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flow-root">
|
||||
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||
<table class="min-w-full divide-y divide-gray-300">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0">Name</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Slug</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Type</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Status</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Groups</th>
|
||||
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-0">
|
||||
<span class="sr-only">Actions</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
<% @applications.each do |application| %>
|
||||
<tr>
|
||||
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">
|
||||
<%= link_to application.name, admin_application_path(application), class: "text-blue-600 hover:text-blue-900" %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
<code class="text-xs bg-gray-100 px-2 py-1 rounded"><%= application.slug %></code>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
<% case application.app_type %>
|
||||
<% when "oidc" %>
|
||||
<span class="inline-flex items-center rounded-full bg-purple-100 px-2 py-1 text-xs font-medium text-purple-700">OIDC</span>
|
||||
<% when "trusted_header" %>
|
||||
<span class="inline-flex items-center rounded-full bg-indigo-100 px-2 py-1 text-xs font-medium text-indigo-700">ForwardAuth</span>
|
||||
<% when "saml" %>
|
||||
<span class="inline-flex items-center rounded-full bg-orange-100 px-2 py-1 text-xs font-medium text-orange-700">SAML</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
<% if application.active? %>
|
||||
<span class="inline-flex items-center rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-700">Active</span>
|
||||
<% else %>
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700">Inactive</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
<% if application.allowed_groups.empty? %>
|
||||
<span class="text-gray-400">All users</span>
|
||||
<% else %>
|
||||
<%= application.allowed_groups.count %>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
|
||||
<%= link_to "View", admin_application_path(application), class: "text-blue-600 hover:text-blue-900 mr-4" %>
|
||||
<%= link_to "Edit", edit_admin_application_path(application), class: "text-blue-600 hover:text-blue-900 mr-4" %>
|
||||
<%= button_to "Delete", admin_application_path(application), method: :delete, data: { turbo_confirm: "Are you sure you want to delete this application?" }, class: "text-red-600 hover:text-red-900" %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
4
app/views/admin/applications/new.html.erb
Normal file
4
app/views/admin/applications/new.html.erb
Normal file
@@ -0,0 +1,4 @@
|
||||
<div class="max-w-3xl">
|
||||
<h1 class="text-2xl font-semibold text-gray-900 mb-6">New Application</h1>
|
||||
<%= render "form", application: @application %>
|
||||
</div>
|
||||
122
app/views/admin/applications/show.html.erb
Normal file
122
app/views/admin/applications/show.html.erb
Normal file
@@ -0,0 +1,122 @@
|
||||
<div class="mb-6">
|
||||
<div class="sm:flex sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-gray-900"><%= @application.name %></h1>
|
||||
<p class="mt-1 text-sm text-gray-500"><%= @application.description %></p>
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-0 flex gap-3">
|
||||
<%= link_to "Edit", edit_admin_application_path(@application), class: "rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" %>
|
||||
<%= button_to "Delete", admin_application_path(@application), method: :delete, data: { turbo_confirm: "Are you sure?" }, class: "rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Basic Information -->
|
||||
<div class="bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 mb-4">Basic Information</h3>
|
||||
<dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Slug</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900"><code class="bg-gray-100 px-2 py-1 rounded"><%= @application.slug %></code></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Type</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<% case @application.app_type %>
|
||||
<% when "oidc" %>
|
||||
<span class="inline-flex items-center rounded-full bg-purple-100 px-2 py-1 text-xs font-medium text-purple-700">OIDC</span>
|
||||
<% when "trusted_header" %>
|
||||
<span class="inline-flex items-center rounded-full bg-indigo-100 px-2 py-1 text-xs font-medium text-indigo-700">ForwardAuth</span>
|
||||
<% when "saml" %>
|
||||
<span class="inline-flex items-center rounded-full bg-orange-100 px-2 py-1 text-xs font-medium text-orange-700">SAML</span>
|
||||
<% end %>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Status</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<% if @application.active? %>
|
||||
<span class="inline-flex items-center rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-700">Active</span>
|
||||
<% else %>
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700">Inactive</span>
|
||||
<% end %>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OIDC Configuration (only for OIDC apps) -->
|
||||
<% if @application.oidc? %>
|
||||
<div class="bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900">OIDC Credentials</h3>
|
||||
<%= button_to "Regenerate Credentials", regenerate_credentials_admin_application_path(@application), method: :post, data: { turbo_confirm: "This will invalidate the current credentials. Continue?" }, class: "text-sm text-red-600 hover:text-red-900" %>
|
||||
</div>
|
||||
<dl class="space-y-4">
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Client ID</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<code class="block bg-gray-100 px-3 py-2 rounded font-mono text-xs break-all"><%= @application.client_id %></code>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Client Secret</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<code class="block bg-gray-100 px-3 py-2 rounded font-mono text-xs break-all"><%= @application.client_secret %></code>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Redirect URIs</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<% if @application.redirect_uris.present? %>
|
||||
<% @application.parsed_redirect_uris.each do |uri| %>
|
||||
<code class="block bg-gray-100 px-3 py-2 rounded font-mono text-xs break-all mb-2"><%= uri %></code>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<span class="text-gray-400">No redirect URIs configured</span>
|
||||
<% end %>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<!-- Group Access Control -->
|
||||
<div class="bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 mb-4">Access Control</h3>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 mb-2">Allowed Groups</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<% if @allowed_groups.empty? %>
|
||||
<div class="rounded-md bg-blue-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-blue-700">
|
||||
No groups assigned - all active users can access this application.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<ul class="divide-y divide-gray-200 border border-gray-200 rounded-md">
|
||||
<% @allowed_groups.each do |group| %>
|
||||
<li class="px-4 py-3 flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900"><%= group.name %></p>
|
||||
<p class="text-xs text-gray-500"><%= pluralize(group.users.count, "member") %></p>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
</dd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
145
app/views/admin/dashboard/index.html.erb
Normal file
145
app/views/admin/dashboard/index.html.erb
Normal file
@@ -0,0 +1,145 @@
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900">Admin Dashboard</h1>
|
||||
<p class="mt-2 text-gray-600">System overview and quick actions</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<!-- Users Card -->
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-6 w-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">
|
||||
Total Users
|
||||
</dt>
|
||||
<dd class="flex items-baseline">
|
||||
<div class="text-2xl font-semibold text-gray-900">
|
||||
<%= @user_count %>
|
||||
</div>
|
||||
<div class="ml-2 text-sm text-gray-600">
|
||||
(<%= @active_user_count %> active)
|
||||
</div>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-5 py-3">
|
||||
<%= link_to "Manage users", admin_users_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Applications Card -->
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-6 w-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h4a1 1 0 010 2H6v10a1 1 0 001 1h10a1 1 0 001-1v-3a1 1 0 112 0v3a3 3 0 01-3 3H7a3 3 0 01-3-3V6a1 1 0 011-1zm9 1a1 1 0 10-2 0v3a1 1 0 102 0V6zm-4 8a1 1 0 100 2h.01a1 1 0 100-2H9zm4 0a1 1 0 100 2h.01a1 1 0 100-2H13z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">
|
||||
Applications
|
||||
</dt>
|
||||
<dd class="flex items-baseline">
|
||||
<div class="text-2xl font-semibold text-gray-900">
|
||||
<%= @application_count %>
|
||||
</div>
|
||||
<div class="ml-2 text-sm text-gray-600">
|
||||
(<%= @active_application_count %> active)
|
||||
</div>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-5 py-3">
|
||||
<%= link_to "Manage applications", admin_applications_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Groups Card -->
|
||||
<div class="bg-white overflow-hidden shadow rounded-lg">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-6 w-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-5 w-0 flex-1">
|
||||
<dl>
|
||||
<dt class="text-sm font-medium text-gray-500 truncate">
|
||||
Groups
|
||||
</dt>
|
||||
<dd class="text-2xl font-semibold text-gray-900">
|
||||
<%= @group_count %>
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 px-5 py-3">
|
||||
<%= link_to "Manage groups", admin_groups_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Users -->
|
||||
<div class="mt-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-4">Recent Users</h2>
|
||||
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||
<ul class="divide-y divide-gray-200">
|
||||
<% @recent_users.each do |user| %>
|
||||
<li class="px-6 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900"><%= user.email_address %></p>
|
||||
<p class="text-xs text-gray-500">
|
||||
Created <%= time_ago_in_words(user.created_at) %> ago
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<% if user.admin? %>
|
||||
<span class="inline-flex items-center rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-700">Admin</span>
|
||||
<% end %>
|
||||
<% if user.totp_enabled? %>
|
||||
<span class="inline-flex items-center rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-700">2FA</span>
|
||||
<% end %>
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700"><%= user.status.titleize %></span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="mt-8">
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-4">Quick Actions</h2>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<%= link_to new_admin_user_path, class: "block p-6 bg-white rounded-lg border border-gray-200 shadow-sm hover:bg-gray-50 hover:shadow-md transition" do %>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Create User</h3>
|
||||
<p class="text-sm text-gray-600">Add a new user to the system</p>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_admin_application_path, class: "block p-6 bg-white rounded-lg border border-gray-200 shadow-sm hover:bg-gray-50 hover:shadow-md transition" do %>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Register Application</h3>
|
||||
<p class="text-sm text-gray-600">Add a new OIDC or ForwardAuth app</p>
|
||||
<% end %>
|
||||
|
||||
<%= link_to new_admin_group_path, class: "block p-6 bg-white rounded-lg border border-gray-200 shadow-sm hover:bg-gray-50 hover:shadow-md transition" do %>
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Create Group</h3>
|
||||
<p class="text-sm text-gray-600">Organize users into a new group</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
56
app/views/admin/groups/_form.html.erb
Normal file
56
app/views/admin/groups/_form.html.erb
Normal file
@@ -0,0 +1,56 @@
|
||||
<%= form_with(model: [:admin, group], class: "space-y-6") do |form| %>
|
||||
<% if group.errors.any? %>
|
||||
<div class="rounded-md bg-red-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">
|
||||
<%= pluralize(group.errors.count, "error") %> prohibited this group from being saved:
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-red-700">
|
||||
<ul class="list-disc pl-5 space-y-1">
|
||||
<% group.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<%= form.label :name, class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_field :name, required: true, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "developers" %>
|
||||
<p class="mt-1 text-sm text-gray-500">Group names are automatically normalized to lowercase.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :description, class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.text_area :description, rows: 3, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "Optional description of this group" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :user_ids, "Group Members", class: "block text-sm font-medium text-gray-700" %>
|
||||
<div class="mt-2 space-y-2 max-h-64 overflow-y-auto border border-gray-200 rounded-md p-3">
|
||||
<% if @available_users.any? %>
|
||||
<% @available_users.each do |user| %>
|
||||
<div class="flex items-center">
|
||||
<%= check_box_tag "group[user_ids][]", user.id, group.users.include?(user), class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" %>
|
||||
<%= label_tag "group_user_ids_#{user.id}", user.email_address, class: "ml-2 text-sm text-gray-900" %>
|
||||
<% if user.admin? %>
|
||||
<span class="ml-2 inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700">Admin</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<p class="text-sm text-gray-500">No users available.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-gray-500">Select which users should be members of this group.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<%= form.submit group.persisted? ? "Update Group" : "Create Group", class: "rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" %>
|
||||
<%= link_to "Cancel", admin_groups_path, class: "rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" %>
|
||||
</div>
|
||||
<% end %>
|
||||
5
app/views/admin/groups/edit.html.erb
Normal file
5
app/views/admin/groups/edit.html.erb
Normal file
@@ -0,0 +1,5 @@
|
||||
<div class="max-w-2xl">
|
||||
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Edit Group</h1>
|
||||
<p class="text-sm text-gray-600 mb-6">Editing: <%= @group.name %></p>
|
||||
<%= render "form", group: @group %>
|
||||
</div>
|
||||
52
app/views/admin/groups/index.html.erb
Normal file
52
app/views/admin/groups/index.html.erb
Normal file
@@ -0,0 +1,52 @@
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-2xl font-semibold text-gray-900">Groups</h1>
|
||||
<p class="mt-2 text-sm text-gray-700">Organize users into groups for application access control.</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
||||
<%= link_to "New Group", new_admin_group_path, class: "block rounded-md bg-blue-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flow-root">
|
||||
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||
<table class="min-w-full divide-y divide-gray-300">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0">Name</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Description</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Members</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Applications</th>
|
||||
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-0">
|
||||
<span class="sr-only">Actions</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
<% @groups.each do |group| %>
|
||||
<tr>
|
||||
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">
|
||||
<%= link_to group.name, admin_group_path(group), class: "text-blue-600 hover:text-blue-900" %>
|
||||
</td>
|
||||
<td class="px-3 py-4 text-sm text-gray-500">
|
||||
<%= truncate(group.description, length: 80) || content_tag(:span, "No description", class: "text-gray-400") %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
<%= pluralize(group.users.count, "member") %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
<%= pluralize(group.applications.count, "app") %>
|
||||
</td>
|
||||
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
|
||||
<%= link_to "View", admin_group_path(group), class: "text-blue-600 hover:text-blue-900 mr-4" %>
|
||||
<%= link_to "Edit", edit_admin_group_path(group), class: "text-blue-600 hover:text-blue-900 mr-4" %>
|
||||
<%= button_to "Delete", admin_group_path(group), method: :delete, data: { turbo_confirm: "Are you sure you want to delete this group?" }, class: "text-red-600 hover:text-red-900" %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
4
app/views/admin/groups/new.html.erb
Normal file
4
app/views/admin/groups/new.html.erb
Normal file
@@ -0,0 +1,4 @@
|
||||
<div class="max-w-2xl">
|
||||
<h1 class="text-2xl font-semibold text-gray-900 mb-6">New Group</h1>
|
||||
<%= render "form", group: @group %>
|
||||
</div>
|
||||
88
app/views/admin/groups/show.html.erb
Normal file
88
app/views/admin/groups/show.html.erb
Normal file
@@ -0,0 +1,88 @@
|
||||
<div class="mb-6">
|
||||
<div class="sm:flex sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-gray-900"><%= @group.name %></h1>
|
||||
<% if @group.description.present? %>
|
||||
<p class="mt-1 text-sm text-gray-500"><%= @group.description %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-0 flex gap-3">
|
||||
<%= link_to "Edit", edit_admin_group_path(@group), class: "rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" %>
|
||||
<%= button_to "Delete", admin_group_path(@group), method: :delete, data: { turbo_confirm: "Are you sure?" }, class: "rounded-md bg-red-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-red-500" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- Members -->
|
||||
<div class="bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 mb-4">
|
||||
Members (<%= @members.count %>)
|
||||
</h3>
|
||||
<% if @members.any? %>
|
||||
<ul class="divide-y divide-gray-200 border border-gray-200 rounded-md">
|
||||
<% @members.each do |user| %>
|
||||
<li class="px-4 py-3 flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900"><%= user.email_address %></p>
|
||||
<div class="flex gap-2 mt-1">
|
||||
<% if user.admin? %>
|
||||
<span class="inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700">Admin</span>
|
||||
<% end %>
|
||||
<% if user.totp_enabled? %>
|
||||
<span class="inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700">2FA</span>
|
||||
<% end %>
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-700"><%= user.status.titleize %></span>
|
||||
</div>
|
||||
</div>
|
||||
<%= link_to "View", admin_user_path(user), class: "text-blue-600 hover:text-blue-900 text-sm" %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<div class="rounded-md bg-gray-50 p-4">
|
||||
<p class="text-sm text-gray-500">No members in this group yet.</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Applications -->
|
||||
<div class="bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900 mb-4">
|
||||
Assigned Applications (<%= @applications.count %>)
|
||||
</h3>
|
||||
<% if @applications.any? %>
|
||||
<ul class="divide-y divide-gray-200 border border-gray-200 rounded-md">
|
||||
<% @applications.each do |app| %>
|
||||
<li class="px-4 py-3 flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-900"><%= app.name %></p>
|
||||
<div class="flex gap-2 mt-1">
|
||||
<% case app.app_type %>
|
||||
<% when "oidc" %>
|
||||
<span class="inline-flex items-center rounded-full bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-700">OIDC</span>
|
||||
<% when "trusted_header" %>
|
||||
<span class="inline-flex items-center rounded-full bg-indigo-100 px-2 py-0.5 text-xs font-medium text-indigo-700">ForwardAuth</span>
|
||||
<% end %>
|
||||
<% if app.active? %>
|
||||
<span class="inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700">Active</span>
|
||||
<% else %>
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-700">Inactive</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<%= link_to "View", admin_application_path(app), class: "text-blue-600 hover:text-blue-900 text-sm" %>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% else %>
|
||||
<div class="rounded-md bg-gray-50 p-4">
|
||||
<p class="text-sm text-gray-500">This group is not assigned to any applications.</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
53
app/views/admin/users/_form.html.erb
Normal file
53
app/views/admin/users/_form.html.erb
Normal file
@@ -0,0 +1,53 @@
|
||||
<%= form_with(model: [:admin, user], class: "space-y-6") do |form| %>
|
||||
<% if user.errors.any? %>
|
||||
<div class="rounded-md bg-red-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">
|
||||
<%= pluralize(user.errors.count, "error") %> prohibited this user from being saved:
|
||||
</h3>
|
||||
<div class="mt-2 text-sm text-red-700">
|
||||
<ul class="list-disc pl-5 space-y-1">
|
||||
<% user.errors.full_messages.each do |message| %>
|
||||
<li><%= message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div>
|
||||
<%= form.label :email_address, class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.email_field :email_address, required: true, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "user@example.com" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :password, class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.password_field :password, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: user.persisted? ? "Leave blank to keep current password" : "Enter password" %>
|
||||
<% if user.persisted? %>
|
||||
<p class="mt-1 text-sm text-gray-500">Leave blank to keep the current password</p>
|
||||
<% else %>
|
||||
<p class="mt-1 text-sm text-gray-500">Leave blank to generate a random password</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :status, class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.select :status, User.statuses.keys.map { |s| [s.titleize, s] }, {}, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<%= form.check_box :admin, class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500", disabled: (user == Current.session.user) %>
|
||||
<%= form.label :admin, "Administrator", class: "ml-2 block text-sm text-gray-900" %>
|
||||
<% if user == Current.session.user %>
|
||||
<span class="ml-2 text-xs text-gray-500">(Cannot change your own admin status)</span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<%= form.submit user.persisted? ? "Update User" : "Create User", class: "rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" %>
|
||||
<%= link_to "Cancel", admin_users_path, class: "rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50" %>
|
||||
</div>
|
||||
<% end %>
|
||||
5
app/views/admin/users/edit.html.erb
Normal file
5
app/views/admin/users/edit.html.erb
Normal file
@@ -0,0 +1,5 @@
|
||||
<div class="max-w-2xl">
|
||||
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Edit User</h1>
|
||||
<p class="text-sm text-gray-600 mb-6">Editing: <%= @user.email_address %></p>
|
||||
<%= render "form", user: @user %>
|
||||
</div>
|
||||
78
app/views/admin/users/index.html.erb
Normal file
78
app/views/admin/users/index.html.erb
Normal file
@@ -0,0 +1,78 @@
|
||||
<div class="sm:flex sm:items-center">
|
||||
<div class="sm:flex-auto">
|
||||
<h1 class="text-2xl font-semibold text-gray-900">Users</h1>
|
||||
<p class="mt-2 text-sm text-gray-700">A list of all users in the system.</p>
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none">
|
||||
<%= link_to "New User", new_admin_user_path, class: "block rounded-md bg-blue-600 px-3 py-2 text-center text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flow-root">
|
||||
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div class="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||
<table class="min-w-full divide-y divide-gray-300">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0">Email</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Status</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Role</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">2FA</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Groups</th>
|
||||
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-0">
|
||||
<span class="sr-only">Actions</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
<% @users.each do |user| %>
|
||||
<tr>
|
||||
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">
|
||||
<%= user.email_address %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
<% if user.status.present? %>
|
||||
<% case user.status.to_sym %>
|
||||
<% when :active %>
|
||||
<span class="inline-flex items-center rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-700">Active</span>
|
||||
<% when :disabled %>
|
||||
<span class="inline-flex items-center rounded-full bg-red-100 px-2 py-1 text-xs font-medium text-red-700">Disabled</span>
|
||||
<% when :pending_invitation %>
|
||||
<span class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-1 text-xs font-medium text-yellow-700">Pending</span>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<span class="text-gray-400">-</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
<% if user.admin? %>
|
||||
<span class="inline-flex items-center rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-700">Admin</span>
|
||||
<% else %>
|
||||
<span class="text-gray-500">User</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
<% if user.totp_enabled? %>
|
||||
<svg class="h-5 w-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<% else %>
|
||||
<svg class="h-5 w-5 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
<%= user.groups.count %>
|
||||
</td>
|
||||
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
|
||||
<%= link_to "Edit", edit_admin_user_path(user), class: "text-blue-600 hover:text-blue-900 mr-4" %>
|
||||
<%= button_to "Delete", admin_user_path(user), method: :delete, data: { turbo_confirm: "Are you sure you want to delete this user?" }, class: "text-red-600 hover:text-red-900" %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
4
app/views/admin/users/new.html.erb
Normal file
4
app/views/admin/users/new.html.erb
Normal file
@@ -0,0 +1,4 @@
|
||||
<div class="max-w-2xl">
|
||||
<h1 class="text-2xl font-semibold text-gray-900 mb-6">New User</h1>
|
||||
<%= render "form", user: @user %>
|
||||
</div>
|
||||
37
bin/generate_oidc_key
Executable file
37
bin/generate_oidc_key
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
# Generate OIDC private key for Clinch
|
||||
# Usage: bin/generate_oidc_key
|
||||
|
||||
set -e
|
||||
|
||||
echo "Generating OIDC RSA private key..."
|
||||
echo
|
||||
|
||||
# Generate the key
|
||||
KEY=$(openssl genrsa 2048 2>/dev/null)
|
||||
|
||||
# Display the key
|
||||
echo "$KEY"
|
||||
echo
|
||||
echo "---"
|
||||
echo
|
||||
echo "✅ Key generated successfully!"
|
||||
echo
|
||||
echo "To use this key:"
|
||||
echo
|
||||
echo "1. Copy the entire key above (including BEGIN/END lines)"
|
||||
echo
|
||||
echo "2. Add to your .env file:"
|
||||
echo " OIDC_PRIVATE_KEY=\"-----BEGIN RSA PRIVATE KEY-----"
|
||||
echo " ...paste key here..."
|
||||
echo " -----END RSA PRIVATE KEY-----\""
|
||||
echo
|
||||
echo "3. Or save to file:"
|
||||
echo " bin/generate_oidc_key > oidc_private_key.pem"
|
||||
echo
|
||||
echo "⚠️ Important:"
|
||||
echo " - Generate this key ONCE and keep it forever"
|
||||
echo " - Backup the key securely"
|
||||
echo " - Don't commit .env to git (it's in .gitignore)"
|
||||
echo " - If you regenerate this key, all OIDC sessions become invalid"
|
||||
echo
|
||||
@@ -51,7 +51,11 @@ Rails.application.routes.draw do
|
||||
namespace :admin do
|
||||
root "dashboard#index"
|
||||
resources :users
|
||||
resources :applications
|
||||
resources :applications do
|
||||
member do
|
||||
post :regenerate_credentials
|
||||
end
|
||||
end
|
||||
resources :groups
|
||||
end
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ class AddAuthFieldsToUsers < ActiveRecord::Migration[8.1]
|
||||
add_column :users, :totp_secret, :string
|
||||
add_column :users, :totp_required, :boolean, default: false, null: false
|
||||
add_column :users, :backup_codes, :text
|
||||
add_column :users, :status, :string, default: "active", null: false
|
||||
add_column :users, :status, :integer, default: 0, null: false
|
||||
|
||||
add_index :users, :status
|
||||
end
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
class ChangeUserStatusToInteger < ActiveRecord::Migration[8.1]
|
||||
def change
|
||||
change_column :users, :status, :integer
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user