Add dark mode with toggle and localStorage persistence

Uses Tailwind v4 class-based dark mode with a Stimulus controller for
toggling. Respects prefers-color-scheme as default, prevents FOUC with
an inline script, and persists the user's choice in localStorage. All
views updated with dark: variants for backgrounds, text, borders,
badges, buttons, and form inputs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dan Milne
2026-03-22 00:37:58 +11:00
parent 43958f50ce
commit 3d98261a51
38 changed files with 744 additions and 636 deletions

View File

@@ -1,3 +1,23 @@
@import "tailwindcss"; @import "tailwindcss";
@plugin "@tailwindcss/forms"; @plugin "@tailwindcss/forms";
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));
@layer base {
.dark input:where([type="text"], [type="email"], [type="password"], [type="number"], [type="url"], [type="tel"], [type="search"]),
.dark textarea,
.dark select {
background-color: var(--color-gray-800);
border-color: var(--color-gray-600);
color: var(--color-gray-100);
}
.dark input::placeholder,
.dark textarea::placeholder {
color: var(--color-gray-400);
}
.dark input:where([type="checkbox"], [type="radio"]) {
background-color: var(--color-gray-700);
border-color: var(--color-gray-500);
}
}

View File

@@ -22,11 +22,11 @@ module ApplicationHelper
def border_class_for(type) def border_class_for(type)
case type.to_s case type.to_s
when "notice" then "border-green-200" when "notice" then "border-green-200 dark:border-green-700"
when "alert", "error" then "border-red-200" when "alert", "error" then "border-red-200 dark:border-red-700"
when "warning" then "border-yellow-200" when "warning" then "border-yellow-200 dark:border-yellow-700"
when "info" then "border-blue-200" when "info" then "border-blue-200 dark:border-blue-700"
else "border-gray-200" else "border-gray-200 dark:border-gray-700"
end end
end end
end end

View File

@@ -0,0 +1,27 @@
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["icon"]
connect() {
this.updateIcon()
}
toggle() {
document.documentElement.classList.toggle("dark")
const isDark = document.documentElement.classList.contains("dark")
localStorage.setItem("theme", isDark ? "dark" : "light")
this.updateIcon()
}
updateIcon() {
const isDark = document.documentElement.classList.contains("dark")
this.iconTargets.forEach(icon => {
if (icon.dataset.mode === "dark") {
icon.classList.toggle("hidden", !isDark)
} else {
icon.classList.toggle("hidden", isDark)
}
})
}
}

View File

@@ -1,50 +1,50 @@
<div class="space-y-8"> <div class="space-y-8">
<div> <div>
<h1 class="text-3xl font-bold text-gray-900">Sessions</h1> <h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">Sessions</h1>
<p class="mt-2 text-sm text-gray-600">Manage your active sessions and connected applications.</p> <p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Manage your active sessions and connected applications.</p>
</div> </div>
<!-- Connected Applications --> <!-- Connected Applications -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900">Connected Applications</h3> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">Connected Applications</h3>
<div class="mt-2 max-w-xl text-sm text-gray-500"> <div class="mt-2 max-w-xl text-sm text-gray-500 dark:text-gray-400">
<p>These applications have access to your account. You can revoke access at any time.</p> <p>These applications have access to your account. You can revoke access at any time.</p>
</div> </div>
<div class="mt-5"> <div class="mt-5">
<% if @connected_applications.any? %> <% if @connected_applications.any? %>
<ul role="list" class="divide-y divide-gray-200"> <ul role="list" class="divide-y divide-gray-200 dark:divide-gray-700">
<% @connected_applications.each do |consent| %> <% @connected_applications.each do |consent| %>
<li class="py-4"> <li class="py-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex flex-col"> <div class="flex flex-col">
<p class="text-sm font-medium text-gray-900"> <p class="text-sm font-medium text-gray-900 dark:text-gray-100">
<%= consent.application.name %> <%= consent.application.name %>
</p> </p>
<p class="mt-1 text-sm text-gray-500"> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Access to: <%= consent.formatted_scopes %> Access to: <%= consent.formatted_scopes %>
</p> </p>
<p class="mt-1 text-xs text-gray-400"> <p class="mt-1 text-xs text-gray-400 dark:text-gray-500">
Authorized <%= time_ago_in_words(consent.granted_at) %> ago Authorized <%= time_ago_in_words(consent.granted_at) %> ago
</p> </p>
</div> </div>
<%= button_to "Revoke Access", revoke_consent_active_sessions_path(application_id: consent.application.id), method: :delete, <%= button_to "Revoke Access", revoke_consent_active_sessions_path(application_id: consent.application.id), method: :delete,
class: "inline-flex items-center rounded-md border border-red-300 bg-white px-3 py-2 text-sm font-medium text-red-700 shadow-sm hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2", class: "inline-flex items-center rounded-md border border-red-300 bg-white dark:bg-gray-700 dark:ring-gray-600 dark:text-gray-200 px-3 py-2 text-sm font-medium text-red-700 shadow-sm hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900",
form: { data: { turbo_confirm: "Are you sure you want to revoke access to #{consent.application.name}? You'll need to re-authorize this application to use it again." } } %> form: { data: { turbo_confirm: "Are you sure you want to revoke access to #{consent.application.name}? You'll need to re-authorize this application to use it again." } } %>
</div> </div>
</li> </li>
<% end %> <% end %>
</ul> </ul>
<% else %> <% else %>
<p class="text-sm text-gray-500">No connected applications.</p> <p class="text-sm text-gray-500 dark:text-gray-400">No connected applications.</p>
<% end %> <% end %>
<% if @connected_applications.any? %> <% if @connected_applications.any? %>
<div class="mt-6 pt-6 border-t border-gray-200"> <div class="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<div class="flex justify-end"> <div class="flex justify-end">
<div class="inline-block"> <div class="inline-block">
<%= button_to "Revoke All App Access", revoke_all_consents_active_sessions_path, method: :delete, <%= button_to "Revoke All App Access", revoke_all_consents_active_sessions_path, method: :delete,
class: "inline-flex items-center rounded-md border border-red-300 bg-white px-3 py-2 text-sm font-medium text-red-700 shadow-sm hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 whitespace-nowrap", class: "inline-flex items-center rounded-md border border-red-300 bg-white dark:bg-gray-700 dark:ring-gray-600 dark:text-gray-200 px-3 py-2 text-sm font-medium text-red-700 shadow-sm hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900 whitespace-nowrap",
form: { data: { turbo_confirm: "This will revoke access from all connected applications. You'll need to re-authorize each application to use them again. Are you sure?" } } %> form: { data: { turbo_confirm: "This will revoke access from all connected applications. You'll need to re-authorize each application to use them again. Are you sure?" } } %>
</div> </div>
</div> </div>
@@ -55,37 +55,37 @@
</div> </div>
<!-- Active Sessions --> <!-- Active Sessions -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900">Active Sessions</h3> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">Active Sessions</h3>
<div class="mt-2 max-w-xl text-sm text-gray-500"> <div class="mt-2 max-w-xl text-sm text-gray-500 dark:text-gray-400">
<p>These devices are currently signed in to your account. Revoke any sessions that you don't recognize.</p> <p>These devices are currently signed in to your account. Revoke any sessions that you don't recognize.</p>
</div> </div>
<div class="mt-5"> <div class="mt-5">
<% if @active_sessions.any? %> <% if @active_sessions.any? %>
<ul role="list" class="divide-y divide-gray-200"> <ul role="list" class="divide-y divide-gray-200 dark:divide-gray-700">
<% @active_sessions.each do |session| %> <% @active_sessions.each do |session| %>
<li class="py-4"> <li class="py-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex flex-col"> <div class="flex flex-col">
<p class="text-sm font-medium text-gray-900"> <p class="text-sm font-medium text-gray-900 dark:text-gray-100">
<%= session.device_name || "Unknown Device" %> <%= session.device_name || "Unknown Device" %>
<% if session.id == Current.session.id %> <% if session.id == Current.session.id %>
<span class="ml-2 inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800"> <span class="ml-2 inline-flex items-center rounded-full bg-green-100 dark:bg-green-900/50 px-2.5 py-0.5 text-xs font-medium text-green-800 dark:text-green-200">
This device This device
</span> </span>
<% end %> <% end %>
</p> </p>
<p class="mt-1 text-sm text-gray-500"> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
<%= session.ip_address %> <%= session.ip_address %>
</p> </p>
<p class="mt-1 text-xs text-gray-400"> <p class="mt-1 text-xs text-gray-400 dark:text-gray-500">
Last active <%= time_ago_in_words(session.last_activity_at || session.updated_at) %> ago Last active <%= time_ago_in_words(session.last_activity_at || session.updated_at) %> ago
</p> </p>
</div> </div>
<% if session.id != Current.session.id %> <% if session.id != Current.session.id %>
<%= button_to "Revoke", session_path(session), method: :delete, <%= button_to "Revoke", session_path(session), method: :delete,
class: "inline-flex items-center rounded-md border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2", class: "inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 dark:text-gray-200 px-3 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900",
form: { data: { turbo_confirm: "Are you sure you want to revoke this session?" } } %> form: { data: { turbo_confirm: "Are you sure you want to revoke this session?" } } %>
<% end %> <% end %>
</div> </div>
@@ -93,15 +93,15 @@
<% end %> <% end %>
</ul> </ul>
<% else %> <% else %>
<p class="text-sm text-gray-500">No other active sessions.</p> <p class="text-sm text-gray-500 dark:text-gray-400">No other active sessions.</p>
<% end %> <% end %>
<% if @active_sessions.count > 1 %> <% if @active_sessions.count > 1 %>
<div class="mt-6 pt-6 border-t border-gray-200"> <div class="mt-6 pt-6 border-t border-gray-200 dark:border-gray-700">
<div class="flex justify-end"> <div class="flex justify-end">
<div class="inline-block"> <div class="inline-block">
<%= button_to "Sign Out Everywhere Else", session_path(Current.session), method: :delete, <%= button_to "Sign Out Everywhere Else", session_path(Current.session), method: :delete,
class: "inline-flex items-center rounded-md border border-orange-300 bg-white px-3 py-2 text-sm font-medium text-orange-700 shadow-sm hover:bg-orange-50 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 whitespace-nowrap", class: "inline-flex items-center rounded-md border border-orange-300 bg-white dark:bg-gray-700 dark:ring-gray-600 dark:text-gray-200 px-3 py-2 text-sm font-medium text-orange-700 shadow-sm hover:bg-orange-50 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900 whitespace-nowrap",
form: { data: { turbo_confirm: "This will sign you out from all other devices except this one. Are you sure?" } } %> form: { data: { turbo_confirm: "This will sign you out from all other devices except this one. Are you sure?" } } %>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
<div class="max-w-3xl"> <div class="max-w-3xl">
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Edit Application</h1> <h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-6">Edit Application</h1>
<p class="text-sm text-gray-600 mb-6">Editing: <%= @application.name %></p> <p class="text-sm text-gray-600 dark:text-gray-400 mb-6">Editing: <%= @application.name %></p>
<%= render "form", application: @application %> <%= render "form", application: @application %>
</div> </div>

View File

@@ -1,7 +1,7 @@
<div class="sm:flex sm:items-center"> <div class="sm:flex sm:items-center">
<div class="sm:flex-auto"> <div class="sm:flex-auto">
<h1 class="text-2xl font-semibold text-gray-900">Applications</h1> <h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">Applications</h1>
<p class="mt-2 text-sm text-gray-700">Manage OIDC Clients.</p> <p class="mt-2 text-sm text-gray-700 dark:text-gray-300">Manage OIDC Clients.</p>
</div> </div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none"> <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" %> <%= 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" %>
@@ -11,29 +11,29 @@
<div class="mt-8 flow-root"> <div class="mt-8 flow-root">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"> <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"> <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"> <table class="min-w-full divide-y divide-gray-300 dark:divide-gray-600">
<thead> <thead>
<tr> <tr>
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0">Application</th> <th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100 sm:pl-0">Application</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 dark:text-gray-100">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 dark:text-gray-100">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 dark:text-gray-100">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="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Groups</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-0"> <th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-0">
<span class="sr-only">Actions</span> <span class="sr-only">Actions</span>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<% @applications.each do |application| %> <% @applications.each do |application| %>
<tr> <tr>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0"> <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 dark:text-gray-100 sm:pl-0">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<% if application.icon.attached? %> <% if application.icon.attached? %>
<%= image_tag application.icon, class: "h-10 w-10 rounded-lg object-cover border border-gray-200 flex-shrink-0", alt: "#{application.name} icon" %> <%= image_tag application.icon, class: "h-10 w-10 rounded-lg object-cover border border-gray-200 dark:border-gray-700 flex-shrink-0", alt: "#{application.name} icon" %>
<% else %> <% else %>
<div class="h-10 w-10 rounded-lg bg-gray-100 border border-gray-200 flex items-center justify-center flex-shrink-0"> <div class="h-10 w-10 rounded-lg bg-gray-100 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 flex items-center justify-center flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-6 w-6 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg> </svg>
</div> </div>
@@ -41,29 +41,29 @@
<%= link_to application.name, admin_application_path(application), class: "text-blue-600 hover:text-blue-900" %> <%= link_to application.name, admin_application_path(application), class: "text-blue-600 hover:text-blue-900" %>
</div> </div>
</td> </td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500"> <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
<code class="text-xs bg-gray-100 px-2 py-1 rounded"><%= application.slug %></code> <code class="text-xs bg-gray-100 dark:bg-gray-700 dark:text-gray-200 px-2 py-1 rounded"><%= application.slug %></code>
</td> </td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500"> <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
<% case application.app_type %> <% case application.app_type %>
<% when "oidc" %> <% 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> <span class="inline-flex items-center rounded-full bg-purple-100 dark:bg-purple-900/50 px-2 py-1 text-xs font-medium text-purple-700 dark:text-purple-300">OIDC</span>
<% when "forward_auth" %> <% when "forward_auth" %>
<span class="inline-flex items-center rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-700">Forward Auth</span> <span class="inline-flex items-center rounded-full bg-blue-100 dark:bg-blue-900/50 px-2 py-1 text-xs font-medium text-blue-700 dark:text-blue-300">Forward Auth</span>
<% when "saml" %> <% 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> <span class="inline-flex items-center rounded-full bg-orange-100 dark:bg-orange-900/50 px-2 py-1 text-xs font-medium text-orange-700 dark:text-orange-300">SAML</span>
<% end %> <% end %>
</td> </td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500"> <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
<% if application.active? %> <% 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> <span class="inline-flex items-center rounded-full bg-green-100 dark:bg-green-900/50 px-2 py-1 text-xs font-medium text-green-700 dark:text-green-300">Active</span>
<% else %> <% 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> <span class="inline-flex items-center rounded-full bg-gray-100 dark:bg-gray-700 px-2 py-1 text-xs font-medium text-gray-700 dark:text-gray-300">Inactive</span>
<% end %> <% end %>
</td> </td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500"> <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
<% if application.allowed_groups.empty? %> <% if application.allowed_groups.empty? %>
<span class="text-gray-400">All users</span> <span class="text-gray-400 dark:text-gray-500">All users</span>
<% else %> <% else %>
<%= application.allowed_groups.count %> <%= application.allowed_groups.count %>
<% end %> <% end %>

View File

@@ -1,4 +1,4 @@
<div class="max-w-3xl"> <div class="max-w-3xl">
<h1 class="text-2xl font-semibold text-gray-900 mb-6">New Application</h1> <h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-6">New Application</h1>
<%= render "form", application: @application %> <%= render "form", application: @application %>
</div> </div>

View File

@@ -1,27 +1,27 @@
<div class="mb-6"> <div class="mb-6">
<% if flash[:client_id] %> <% if flash[:client_id] %>
<div class="bg-yellow-50 border border-yellow-200 rounded-md p-4 mb-6"> <div class="bg-yellow-50 dark:bg-yellow-900/30 border border-yellow-200 dark:border-yellow-700 rounded-md p-4 mb-6">
<h4 class="text-sm font-medium text-yellow-800 mb-2">🔐 OIDC Client Credentials</h4> <h4 class="text-sm font-medium text-yellow-800 dark:text-yellow-200 mb-2">🔐 OIDC Client Credentials</h4>
<% if flash[:public_client] %> <% if flash[:public_client] %>
<p class="text-xs text-yellow-700 mb-3">This is a public client. Copy the client ID below.</p> <p class="text-xs text-yellow-700 dark:text-yellow-300 mb-3">This is a public client. Copy the client ID below.</p>
<% else %> <% else %>
<p class="text-xs text-yellow-700 mb-3">Copy these credentials now. The client secret will not be shown again.</p> <p class="text-xs text-yellow-700 dark:text-yellow-300 mb-3">Copy these credentials now. The client secret will not be shown again.</p>
<% end %> <% end %>
<div class="space-y-2"> <div class="space-y-2">
<div> <div>
<span class="text-xs font-medium text-yellow-700">Client ID:</span> <span class="text-xs font-medium text-yellow-700 dark:text-yellow-300">Client ID:</span>
</div> </div>
<code class="block bg-yellow-100 px-3 py-2 rounded font-mono text-xs break-all"><%= flash[:client_id] %></code> <code class="block bg-yellow-100 dark:bg-yellow-900/50 px-3 py-2 rounded font-mono text-xs break-all"><%= flash[:client_id] %></code>
<% if flash[:client_secret] %> <% if flash[:client_secret] %>
<div class="mt-3"> <div class="mt-3">
<span class="text-xs font-medium text-yellow-700">Client Secret:</span> <span class="text-xs font-medium text-yellow-700 dark:text-yellow-300">Client Secret:</span>
</div> </div>
<code class="block bg-yellow-100 px-3 py-2 rounded font-mono text-xs break-all"><%= flash[:client_secret] %></code> <code class="block bg-yellow-100 dark:bg-yellow-900/50 px-3 py-2 rounded font-mono text-xs break-all"><%= flash[:client_secret] %></code>
<% elsif flash[:public_client] %> <% elsif flash[:public_client] %>
<div class="mt-3"> <div class="mt-3">
<span class="text-xs font-medium text-yellow-700">Client Secret:</span> <span class="text-xs font-medium text-yellow-700 dark:text-yellow-300">Client Secret:</span>
</div> </div>
<div class="bg-yellow-100 px-3 py-2 rounded text-xs text-yellow-600"> <div class="bg-yellow-100 dark:bg-yellow-900/50 px-3 py-2 rounded text-xs text-yellow-600 dark:text-yellow-400">
Public clients do not have a client secret. PKCE is required. Public clients do not have a client secret. PKCE is required.
</div> </div>
<% end %> <% end %>
@@ -32,21 +32,21 @@
<div class="sm:flex sm:items-start sm:justify-between"> <div class="sm:flex sm:items-start sm:justify-between">
<div class="flex items-start gap-4"> <div class="flex items-start gap-4">
<% if @application.icon.attached? %> <% if @application.icon.attached? %>
<%= image_tag @application.icon, class: "h-16 w-16 rounded-lg object-cover border border-gray-200 shrink-0", alt: "#{@application.name} icon" %> <%= image_tag @application.icon, class: "h-16 w-16 rounded-lg object-cover border border-gray-200 dark:border-gray-700 shrink-0", alt: "#{@application.name} icon" %>
<% else %> <% else %>
<div class="h-16 w-16 rounded-lg bg-gray-100 border border-gray-200 flex items-center justify-center shrink-0"> <div class="h-16 w-16 rounded-lg bg-gray-100 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 flex items-center justify-center shrink-0">
<svg class="h-8 w-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-8 w-8 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg> </svg>
</div> </div>
<% end %> <% end %>
<div> <div>
<h1 class="text-2xl font-semibold text-gray-900"><%= @application.name %></h1> <h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100"><%= @application.name %></h1>
<p class="mt-1 text-sm text-gray-500"><%= @application.description %></p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400"><%= @application.description %></p>
</div> </div>
</div> </div>
<div class="mt-4 sm:mt-0 flex gap-3"> <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" %> <%= link_to "Edit", edit_admin_application_path(@application), class: "rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-200 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600" %>
<%= 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" %> <%= 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>
@@ -54,42 +54,42 @@
<div class="space-y-6"> <div class="space-y-6">
<!-- Basic Information --> <!-- Basic Information -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <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> <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-gray-100 mb-4">Basic Information</h3>
<dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2"> <dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
<div> <div>
<dt class="text-sm font-medium text-gray-500">Slug</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">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> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100"><code class="bg-gray-100 dark:bg-gray-700 dark:text-gray-200 px-2 py-1 rounded"><%= @application.slug %></code></dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500">Type</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Type</dt>
<dd class="mt-1 text-sm text-gray-900"> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
<% case @application.app_type %> <% case @application.app_type %>
<% when "oidc" %> <% 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> <span class="inline-flex items-center rounded-full bg-purple-100 dark:bg-purple-900/50 px-2 py-1 text-xs font-medium text-purple-700 dark:text-purple-300">OIDC</span>
<% when "forward_auth" %> <% when "forward_auth" %>
<span class="inline-flex items-center rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-700">Forward Auth</span> <span class="inline-flex items-center rounded-full bg-blue-100 dark:bg-blue-900/50 px-2 py-1 text-xs font-medium text-blue-700 dark:text-blue-300">Forward Auth</span>
<% end %> <% end %>
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500">Status</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Status</dt>
<dd class="mt-1 text-sm text-gray-900"> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
<% if @application.active? %> <% 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> <span class="inline-flex items-center rounded-full bg-green-100 dark:bg-green-900/50 px-2 py-1 text-xs font-medium text-green-700 dark:text-green-300">Active</span>
<% else %> <% 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> <span class="inline-flex items-center rounded-full bg-gray-100 dark:bg-gray-700 px-2 py-1 text-xs font-medium text-gray-700 dark:text-gray-300">Inactive</span>
<% end %> <% end %>
</dd> </dd>
</div> </div>
<div class="sm:col-span-2"> <div class="sm:col-span-2">
<dt class="text-sm font-medium text-gray-500">Landing URL</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Landing URL</dt>
<dd class="mt-1 text-sm text-gray-900"> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
<% if @application.landing_url.present? %> <% if @application.landing_url.present? %>
<%= link_to @application.landing_url, @application.landing_url, target: "_blank", rel: "noopener noreferrer", class: "text-blue-600 hover:text-blue-800 underline" %> <%= link_to @application.landing_url, @application.landing_url, target: "_blank", rel: "noopener noreferrer", class: "text-blue-600 hover:text-blue-800 underline" %>
<% else %> <% else %>
<span class="text-gray-400 italic">Not configured</span> <span class="text-gray-400 dark:text-gray-500 italic">Not configured</span>
<% end %> <% end %>
</dd> </dd>
</div> </div>
@@ -99,59 +99,59 @@
<!-- OIDC Configuration (only for OIDC apps) --> <!-- OIDC Configuration (only for OIDC apps) -->
<% if @application.oidc? %> <% if @application.oidc? %>
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h3 class="text-base font-semibold leading-6 text-gray-900">OIDC Configuration</h3> <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-gray-100">OIDC Configuration</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" %> <%= 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> </div>
<dl class="space-y-4"> <dl class="space-y-4">
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div> <div>
<dt class="text-sm font-medium text-gray-500">Client Type</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Client Type</dt>
<dd class="mt-1 text-sm text-gray-900"> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
<% if @application.public_client? %> <% if @application.public_client? %>
<span class="inline-flex items-center rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-700">Public</span> <span class="inline-flex items-center rounded-full bg-blue-100 dark:bg-blue-900/50 px-2 py-1 text-xs font-medium text-blue-700 dark:text-blue-300">Public</span>
<% else %> <% else %>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700">Confidential</span> <span class="inline-flex items-center rounded-full bg-gray-100 dark:bg-gray-700 px-2 py-1 text-xs font-medium text-gray-700 dark:text-gray-300">Confidential</span>
<% end %> <% end %>
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500">PKCE</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">PKCE</dt>
<dd class="mt-1 text-sm text-gray-900"> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
<% if @application.requires_pkce? %> <% if @application.requires_pkce? %>
<span class="inline-flex items-center rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-700">Required</span> <span class="inline-flex items-center rounded-full bg-green-100 dark:bg-green-900/50 px-2 py-1 text-xs font-medium text-green-700 dark:text-green-300">Required</span>
<% else %> <% else %>
<span class="inline-flex items-center rounded-full bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700">Optional</span> <span class="inline-flex items-center rounded-full bg-gray-100 dark:bg-gray-700 px-2 py-1 text-xs font-medium text-gray-700 dark:text-gray-300">Optional</span>
<% end %> <% end %>
</dd> </dd>
</div> </div>
</div> </div>
<% unless flash[:client_id] %> <% unless flash[:client_id] %>
<div> <div>
<dt class="text-sm font-medium text-gray-500">Client ID</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Client ID</dt>
<dd class="mt-1 text-sm text-gray-900"> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
<code class="block bg-gray-100 px-3 py-2 rounded font-mono text-xs break-all"><%= @application.client_id %></code> <code class="block bg-gray-100 dark:bg-gray-700 dark:text-gray-200 px-3 py-2 rounded font-mono text-xs break-all"><%= @application.client_id %></code>
</dd> </dd>
</div> </div>
<% if @application.confidential_client? %> <% if @application.confidential_client? %>
<div> <div>
<dt class="text-sm font-medium text-gray-500">Client Secret</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Client Secret</dt>
<dd class="mt-1 text-sm text-gray-900"> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
<div class="bg-gray-100 px-3 py-2 rounded text-xs text-gray-500 italic"> <div class="bg-gray-100 dark:bg-gray-700 px-3 py-2 rounded text-xs text-gray-500 dark:text-gray-400 italic">
🔒 Client secret is stored securely and cannot be displayed 🔒 Client secret is stored securely and cannot be displayed
</div> </div>
<p class="mt-2 text-xs text-gray-500"> <p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
To get a new client secret, use the "Regenerate Credentials" button above. To get a new client secret, use the "Regenerate Credentials" button above.
</p> </p>
</dd> </dd>
</div> </div>
<% else %> <% else %>
<div> <div>
<dt class="text-sm font-medium text-gray-500">Client Secret</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Client Secret</dt>
<dd class="mt-1 text-sm text-gray-900"> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
<div class="bg-blue-50 px-3 py-2 rounded text-xs text-blue-600"> <div class="bg-blue-50 dark:bg-blue-900/30 px-3 py-2 rounded text-xs text-blue-600 dark:text-blue-400">
Public clients do not use a client secret. PKCE is required for authorization. Public clients do not use a client secret. PKCE is required for authorization.
</div> </div>
</dd> </dd>
@@ -159,33 +159,33 @@
<% end %> <% end %>
<% end %> <% end %>
<div> <div>
<dt class="text-sm font-medium text-gray-500">Redirect URIs</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Redirect URIs</dt>
<dd class="mt-1 text-sm text-gray-900"> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
<% if @application.redirect_uris.present? %> <% if @application.redirect_uris.present? %>
<% @application.parsed_redirect_uris.each do |uri| %> <% @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> <code class="block bg-gray-100 dark:bg-gray-700 dark:text-gray-200 px-3 py-2 rounded font-mono text-xs break-all mb-2"><%= uri %></code>
<% end %> <% end %>
<% else %> <% else %>
<span class="text-gray-400">No redirect URIs configured</span> <span class="text-gray-400 dark:text-gray-500">No redirect URIs configured</span>
<% end %> <% end %>
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500"> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
Backchannel Logout URI Backchannel Logout URI
<% if @application.supports_backchannel_logout? %> <% if @application.supports_backchannel_logout? %>
<span class="ml-2 inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700">Enabled</span> <span class="ml-2 inline-flex items-center rounded-full bg-green-100 dark:bg-green-900/50 px-2 py-0.5 text-xs font-medium text-green-700 dark:text-green-300">Enabled</span>
<% end %> <% end %>
</dt> </dt>
<dd class="mt-1 text-sm text-gray-900"> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
<% if @application.backchannel_logout_uri.present? %> <% if @application.backchannel_logout_uri.present? %>
<code class="block bg-gray-100 px-3 py-2 rounded font-mono text-xs break-all"><%= @application.backchannel_logout_uri %></code> <code class="block bg-gray-100 dark:bg-gray-700 dark:text-gray-200 px-3 py-2 rounded font-mono text-xs break-all"><%= @application.backchannel_logout_uri %></code>
<p class="mt-2 text-xs text-gray-500"> <p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
When users log out, Clinch will send logout notifications to this endpoint for immediate session termination. When users log out, Clinch will send logout notifications to this endpoint for immediate session termination.
</p> </p>
<% else %> <% else %>
<span class="text-gray-400 italic">Not configured</span> <span class="text-gray-400 dark:text-gray-500 italic">Not configured</span>
<p class="mt-1 text-xs text-gray-500"> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Backchannel logout is optional. Configure it if the application supports OpenID Connect Backchannel Logout. Backchannel logout is optional. Configure it if the application supports OpenID Connect Backchannel Logout.
</p> </p>
<% end %> <% end %>
@@ -198,23 +198,23 @@
<!-- Forward Auth Configuration (only for Forward Auth apps) --> <!-- Forward Auth Configuration (only for Forward Auth apps) -->
<% if @application.forward_auth? %> <% if @application.forward_auth? %>
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold leading-6 text-gray-900 mb-4">Forward Auth Configuration</h3> <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-gray-100 mb-4">Forward Auth Configuration</h3>
<dl class="space-y-4"> <dl class="space-y-4">
<div> <div>
<dt class="text-sm font-medium text-gray-500">Domain Pattern</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Domain Pattern</dt>
<dd class="mt-1 text-sm text-gray-900"> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
<code class="block bg-gray-100 px-3 py-2 rounded font-mono text-xs"><%= @application.domain_pattern %></code> <code class="block bg-gray-100 dark:bg-gray-700 dark:text-gray-200 px-3 py-2 rounded font-mono text-xs"><%= @application.domain_pattern %></code>
</dd> </dd>
</div> </div>
<div> <div>
<dt class="text-sm font-medium text-gray-500">Headers Configuration</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Headers Configuration</dt>
<dd class="mt-1 text-sm text-gray-900"> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
<% if @application.headers_config.present? && @application.headers_config.any? %> <% if @application.headers_config.present? && @application.headers_config.any? %>
<code class="block bg-gray-100 px-3 py-2 rounded font-mono text-xs whitespace-pre-wrap"><%= JSON.pretty_generate(@application.headers_config) %></code> <code class="block bg-gray-100 dark:bg-gray-700 dark:text-gray-200 px-3 py-2 rounded font-mono text-xs whitespace-pre-wrap"><%= JSON.pretty_generate(@application.headers_config) %></code>
<% else %> <% else %>
<div class="bg-gray-100 px-3 py-2 rounded text-xs text-gray-500"> <div class="bg-gray-100 dark:bg-gray-700 px-3 py-2 rounded text-xs text-gray-500 dark:text-gray-400">
Using default headers: X-Remote-User, X-Remote-Email, X-Remote-Name, X-Remote-Username, X-Remote-Groups, X-Remote-Admin Using default headers: X-Remote-User, X-Remote-Email, X-Remote-Name, X-Remote-Username, X-Remote-Groups, X-Remote-Admin
</div> </div>
<% end %> <% end %>
@@ -226,29 +226,29 @@
<% end %> <% end %>
<!-- Group Access Control --> <!-- Group Access Control -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <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> <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-gray-100 mb-4">Access Control</h3>
<div> <div>
<dt class="text-sm font-medium text-gray-500 mb-2">Allowed Groups</dt> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2">Allowed Groups</dt>
<dd class="mt-1 text-sm text-gray-900"> <dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">
<% if @allowed_groups.empty? %> <% if @allowed_groups.empty? %>
<div class="rounded-md bg-blue-50 p-4"> <div class="rounded-md bg-blue-50 dark:bg-blue-900/30 p-4">
<div class="flex"> <div class="flex">
<div class="ml-3"> <div class="ml-3">
<p class="text-sm text-blue-700"> <p class="text-sm text-blue-700 dark:text-blue-300">
No groups assigned - all active users can access this application. No groups assigned - all active users can access this application.
</p> </p>
</div> </div>
</div> </div>
</div> </div>
<% else %> <% else %>
<ul class="divide-y divide-gray-200 border border-gray-200 rounded-md"> <ul class="divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-md">
<% @allowed_groups.each do |group| %> <% @allowed_groups.each do |group| %>
<li class="px-4 py-3 flex items-center justify-between"> <li class="px-4 py-3 flex items-center justify-between">
<div> <div>
<p class="text-sm font-medium text-gray-900"><%= group.name %></p> <p class="text-sm font-medium text-gray-900 dark:text-gray-100"><%= group.name %></p>
<p class="text-xs text-gray-500"><%= pluralize(group.users.count, "member") %></p> <p class="text-xs text-gray-500 dark:text-gray-400"><%= pluralize(group.users.count, "member") %></p>
</div> </div>
</li> </li>
<% end %> <% end %>

View File

@@ -1,28 +1,28 @@
<div class="mb-8"> <div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">Admin Dashboard</h1> <h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">Admin Dashboard</h1>
<p class="mt-2 text-gray-600">System overview and quick actions</p> <p class="mt-2 text-gray-600 dark:text-gray-400">System overview and quick actions</p>
</div> </div>
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
<!-- Users Card --> <!-- Users Card -->
<div class="bg-white overflow-hidden shadow rounded-lg"> <div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-6 w-6 text-gray-400 dark:text-gray-500" 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> <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> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="text-sm font-medium text-gray-500 truncate"> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">
Total Users Total Users
</dt> </dt>
<dd class="flex items-baseline"> <dd class="flex items-baseline">
<div class="text-2xl font-semibold text-gray-900"> <div class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
<%= @user_count %> <%= @user_count %>
</div> </div>
<div class="ml-2 text-sm text-gray-600"> <div class="ml-2 text-sm text-gray-600 dark:text-gray-400">
(<%= @active_user_count %> active) (<%= @active_user_count %> active)
</div> </div>
</dd> </dd>
@@ -30,30 +30,30 @@
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-5 py-3"> <div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<%= link_to "Manage users", admin_users_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %> <%= link_to "Manage users", admin_users_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
</div> </div>
</div> </div>
<!-- Applications Card --> <!-- Applications Card -->
<div class="bg-white overflow-hidden shadow rounded-lg"> <div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-6 w-6 text-gray-400 dark:text-gray-500" 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> <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> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="text-sm font-medium text-gray-500 truncate"> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">
Applications Applications
</dt> </dt>
<dd class="flex items-baseline"> <dd class="flex items-baseline">
<div class="text-2xl font-semibold text-gray-900"> <div class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
<%= @application_count %> <%= @application_count %>
</div> </div>
<div class="ml-2 text-sm text-gray-600"> <div class="ml-2 text-sm text-gray-600 dark:text-gray-400">
(<%= @active_application_count %> active) (<%= @active_application_count %> active)
</div> </div>
</dd> </dd>
@@ -61,33 +61,33 @@
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-5 py-3"> <div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<%= link_to "Manage applications", admin_applications_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %> <%= link_to "Manage applications", admin_applications_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
</div> </div>
</div> </div>
<!-- Groups Card --> <!-- Groups Card -->
<div class="bg-white overflow-hidden shadow rounded-lg"> <div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-6 w-6 text-gray-400 dark:text-gray-500" 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> <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> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="text-sm font-medium text-gray-500 truncate"> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">
Groups Groups
</dt> </dt>
<dd class="text-2xl font-semibold text-gray-900"> <dd class="text-2xl font-semibold text-gray-900 dark:text-gray-100">
<%= @group_count %> <%= @group_count %>
</dd> </dd>
</dl> </dl>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-5 py-3"> <div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<%= link_to "Manage groups", admin_groups_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %> <%= link_to "Manage groups", admin_groups_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
</div> </div>
</div> </div>
@@ -95,26 +95,26 @@
<!-- Recent Users --> <!-- Recent Users -->
<div class="mt-8"> <div class="mt-8">
<h2 class="text-xl font-semibold text-gray-900 mb-4">Recent Users</h2> <h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">Recent Users</h2>
<div class="bg-white shadow overflow-hidden sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-lg">
<ul class="divide-y divide-gray-200"> <ul class="divide-y divide-gray-200 dark:divide-gray-700">
<% @recent_users.each do |user| %> <% @recent_users.each do |user| %>
<li class="px-6 py-4"> <li class="px-6 py-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
<p class="text-sm font-medium text-gray-900"><%= user.email_address %></p> <p class="text-sm font-medium text-gray-900 dark:text-gray-100"><%= user.email_address %></p>
<p class="text-xs text-gray-500"> <p class="text-xs text-gray-500 dark:text-gray-400">
Created <%= time_ago_in_words(user.created_at) %> ago Created <%= time_ago_in_words(user.created_at) %> ago
</p> </p>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<% if user.admin? %> <% 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> <span class="inline-flex items-center rounded-full bg-blue-100 dark:bg-blue-900/50 px-2 py-1 text-xs font-medium text-blue-700 dark:text-blue-300">Admin</span>
<% end %> <% end %>
<% if user.totp_enabled? %> <% 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> <span class="inline-flex items-center rounded-full bg-green-100 dark:bg-green-900/50 px-2 py-1 text-xs font-medium text-green-700 dark:text-green-300">2FA</span>
<% end %> <% 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> <span class="inline-flex items-center rounded-full bg-gray-100 dark:bg-gray-700 px-2 py-1 text-xs font-medium text-gray-700 dark:text-gray-300"><%= user.status.titleize %></span>
</div> </div>
</div> </div>
</li> </li>
@@ -125,21 +125,21 @@
<!-- Quick Actions --> <!-- Quick Actions -->
<div class="mt-8"> <div class="mt-8">
<h2 class="text-xl font-semibold text-gray-900 mb-4">Quick Actions</h2> <h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">Quick Actions</h2>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3"> <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 %> <%= link_to new_admin_user_path, class: "block p-6 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 hover:shadow-md transition" do %>
<h3 class="text-lg font-semibold text-gray-900 mb-2">Create User</h3> <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">Create User</h3>
<p class="text-sm text-gray-600">Add a new user to the system</p> <p class="text-sm text-gray-600 dark:text-gray-400">Add a new user to the system</p>
<% end %> <% 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 %> <%= link_to new_admin_application_path, class: "block p-6 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 hover:shadow-md transition" do %>
<h3 class="text-lg font-semibold text-gray-900 mb-2">Register Application</h3> <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">Register Application</h3>
<p class="text-sm text-gray-600">Add a new OIDC or ForwardAuth app</p> <p class="text-sm text-gray-600 dark:text-gray-400">Add a new OIDC or ForwardAuth app</p>
<% end %> <% 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 %> <%= link_to new_admin_group_path, class: "block p-6 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 hover:shadow-md transition" do %>
<h3 class="text-lg font-semibold text-gray-900 mb-2">Create Group</h3> <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">Create Group</h3>
<p class="text-sm text-gray-600">Organize users into a new group</p> <p class="text-sm text-gray-600 dark:text-gray-400">Organize users into a new group</p>
<% end %> <% end %>
</div> </div>
</div> </div>

View File

@@ -2,51 +2,51 @@
<%= render "shared/form_errors", form: form %> <%= render "shared/form_errors", form: form %>
<div> <div>
<%= form.label :name, class: "block text-sm font-medium text-gray-700" %> <%= form.label :name, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= 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" %> <%= form.text_field :name, required: true, class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 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> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Group names are automatically normalized to lowercase.</p>
</div> </div>
<div> <div>
<%= form.label :description, class: "block text-sm font-medium text-gray-700" %> <%= form.label :description, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= 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" %> <%= form.text_area :description, rows: 3, class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "Optional description of this group" %>
</div> </div>
<div> <div>
<%= form.label :user_ids, "Group Members", class: "block text-sm font-medium text-gray-700" %> <%= form.label :user_ids, "Group Members", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<div class="mt-2 space-y-2 max-h-64 overflow-y-auto border border-gray-200 rounded-md p-3"> <div class="mt-2 space-y-2 max-h-64 overflow-y-auto border border-gray-200 dark:border-gray-700 rounded-md p-3">
<% if @available_users.any? %> <% if @available_users.any? %>
<% @available_users.each do |user| %> <% @available_users.each do |user| %>
<div class="flex items-center"> <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" %> <%= check_box_tag "group[user_ids][]", user.id, group.users.include?(user), class: "h-4 w-4 rounded border-gray-300 dark:border-gray-600 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" %> <%= label_tag "group_user_ids_#{user.id}", user.email_address, class: "ml-2 text-sm text-gray-900 dark:text-gray-100" %>
<% if user.admin? %> <% 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> <span class="ml-2 inline-flex items-center rounded-full bg-blue-100 dark:bg-blue-900/50 px-2 py-0.5 text-xs font-medium text-blue-700 dark:text-blue-300">Admin</span>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<% else %> <% else %>
<p class="text-sm text-gray-500">No users available.</p> <p class="text-sm text-gray-500 dark:text-gray-400">No users available.</p>
<% end %> <% end %>
</div> </div>
<p class="mt-1 text-sm text-gray-500">Select which users should be members of this group.</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Select which users should be members of this group.</p>
</div> </div>
<div data-controller="json-validator" data-json-validator-valid-class="border-green-500 focus:border-green-500 focus:ring-green-500" data-json-validator-invalid-class="border-red-500 focus:border-red-500 focus:ring-red-500" data-json-validator-valid-status-class="text-green-600" data-json-validator-invalid-status-class="text-red-600"> <div data-controller="json-validator" data-json-validator-valid-class="border-green-500 focus:border-green-500 focus:ring-green-500" data-json-validator-invalid-class="border-red-500 focus:border-red-500 focus:ring-red-500" data-json-validator-valid-status-class="text-green-600" data-json-validator-invalid-status-class="text-red-600">
<%= form.label :custom_claims, "Custom Claims (JSON)", class: "block text-sm font-medium text-gray-700" %> <%= form.label :custom_claims, "Custom Claims (JSON)", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_area :custom_claims, value: (group.custom_claims.present? ? JSON.pretty_generate(group.custom_claims) : ""), rows: 8, <%= form.text_area :custom_claims, value: (group.custom_claims.present? ? JSON.pretty_generate(group.custom_claims) : ""), rows: 8,
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", class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm font-mono",
placeholder: '{"roles": ["admin", "editor"]}', placeholder: '{"roles": ["admin", "editor"]}',
data: { data: {
action: "input->json-validator#validate blur->json-validator#format", action: "input->json-validator#validate blur->json-validator#format",
json_validator_target: "textarea" json_validator_target: "textarea"
} %> } %>
<div class="mt-2 text-sm text-gray-600 space-y-1"> <div class="mt-2 text-sm text-gray-600 dark:text-gray-400 space-y-1">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<p>Optional: Custom claims to add to OIDC tokens for all members. These will be merged with user-level claims.</p> <p>Optional: Custom claims to add to OIDC tokens for all members. These will be merged with user-level claims.</p>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button type="button" data-action="json-validator#format" class="text-xs bg-gray-100 hover:bg-gray-200 px-2 py-1 rounded">Format JSON</button> <button type="button" data-action="json-validator#format" class="text-xs bg-gray-100 dark:bg-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-600 px-2 py-1 rounded">Format JSON</button>
<button type="button" data-action="json-validator#insertSample" data-json-sample='{"roles": ["admin", "editor"], "permissions": ["read", "write"], "team": "backend"}' class="text-xs bg-blue-100 hover:bg-blue-200 text-blue-700 px-2 py-1 rounded">Insert Example</button> <button type="button" data-action="json-validator#insertSample" data-json-sample='{"roles": ["admin", "editor"], "permissions": ["read", "write"], "team": "backend"}' class="text-xs bg-blue-100 dark:bg-blue-900/50 hover:bg-blue-200 dark:hover:bg-blue-900 text-blue-700 dark:text-blue-300 px-2 py-1 rounded">Insert Example</button>
</div> </div>
</div> </div>
<div data-json-validator-target="status" class="text-xs font-medium"></div> <div data-json-validator-target="status" class="text-xs font-medium"></div>
@@ -55,6 +55,6 @@
<div class="flex gap-3"> <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" %> <%= 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" %> <%= link_to "Cancel", admin_groups_path, class: "rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-200 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600" %>
</div> </div>
<% end %> <% end %>

View File

@@ -1,5 +1,5 @@
<div class="max-w-2xl"> <div class="max-w-2xl">
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Edit Group</h1> <h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-6">Edit Group</h1>
<p class="text-sm text-gray-600 mb-6">Editing: <%= @group.name %></p> <p class="text-sm text-gray-600 dark:text-gray-400 mb-6">Editing: <%= @group.name %></p>
<%= render "form", group: @group %> <%= render "form", group: @group %>
</div> </div>

View File

@@ -1,7 +1,7 @@
<div class="sm:flex sm:items-center"> <div class="sm:flex sm:items-center">
<div class="sm:flex-auto"> <div class="sm:flex-auto">
<h1 class="text-2xl font-semibold text-gray-900">Groups</h1> <h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">Groups</h1>
<p class="mt-2 text-sm text-gray-700">Organize users into groups for application access control.</p> <p class="mt-2 text-sm text-gray-700 dark:text-gray-300">Organize users into groups for application access control.</p>
</div> </div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none"> <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" %> <%= 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" %>
@@ -11,31 +11,31 @@
<div class="mt-8 flow-root"> <div class="mt-8 flow-root">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"> <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"> <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"> <table class="min-w-full divide-y divide-gray-300 dark:divide-gray-600">
<thead> <thead>
<tr> <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="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100 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 dark:text-gray-100">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 dark:text-gray-100">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="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Applications</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-0"> <th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-0">
<span class="sr-only">Actions</span> <span class="sr-only">Actions</span>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<% @groups.each do |group| %> <% @groups.each do |group| %>
<tr> <tr>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0"> <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 dark:text-gray-100 sm:pl-0">
<%= link_to group.name, admin_group_path(group), class: "text-blue-600 hover:text-blue-900" %> <%= link_to group.name, admin_group_path(group), class: "text-blue-600 hover:text-blue-900" %>
</td> </td>
<td class="px-3 py-4 text-sm text-gray-500"> <td class="px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
<%= truncate(group.description, length: 80) || content_tag(:span, "No description", class: "text-gray-400") %> <%= truncate(group.description, length: 80) || content_tag(:span, "No description", class: "text-gray-400 dark:text-gray-500") %>
</td> </td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500"> <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
<%= pluralize(group.users.count, "member") %> <%= pluralize(group.users.count, "member") %>
</td> </td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500"> <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
<%= pluralize(group.applications.count, "app") %> <%= pluralize(group.applications.count, "app") %>
</td> </td>
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0"> <td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">

View File

@@ -1,4 +1,4 @@
<div class="max-w-2xl"> <div class="max-w-2xl">
<h1 class="text-2xl font-semibold text-gray-900 mb-6">New Group</h1> <h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-6">New Group</h1>
<%= render "form", group: @group %> <%= render "form", group: @group %>
</div> </div>

View File

@@ -1,13 +1,13 @@
<div class="mb-6"> <div class="mb-6">
<div class="sm:flex sm:items-center sm:justify-between"> <div class="sm:flex sm:items-center sm:justify-between">
<div> <div>
<h1 class="text-2xl font-semibold text-gray-900"><%= @group.name %></h1> <h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100"><%= @group.name %></h1>
<% if @group.description.present? %> <% if @group.description.present? %>
<p class="mt-1 text-sm text-gray-500"><%= @group.description %></p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400"><%= @group.description %></p>
<% end %> <% end %>
</div> </div>
<div class="mt-4 sm:mt-0 flex gap-3"> <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" %> <%= link_to "Edit", edit_admin_group_path(@group), class: "rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-200 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600" %>
<%= 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" %> <%= 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>
@@ -15,25 +15,25 @@
<div class="space-y-6"> <div class="space-y-6">
<!-- Members --> <!-- Members -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold leading-6 text-gray-900 mb-4"> <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-gray-100 mb-4">
Members (<%= @members.count %>) Members (<%= @members.count %>)
</h3> </h3>
<% if @members.any? %> <% if @members.any? %>
<ul class="divide-y divide-gray-200 border border-gray-200 rounded-md"> <ul class="divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-md">
<% @members.each do |user| %> <% @members.each do |user| %>
<li class="px-4 py-3 flex items-center justify-between"> <li class="px-4 py-3 flex items-center justify-between">
<div> <div>
<p class="text-sm font-medium text-gray-900"><%= user.email_address %></p> <p class="text-sm font-medium text-gray-900 dark:text-gray-100"><%= user.email_address %></p>
<div class="flex gap-2 mt-1"> <div class="flex gap-2 mt-1">
<% if user.admin? %> <% 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> <span class="inline-flex items-center rounded-full bg-blue-100 dark:bg-blue-900/50 px-2 py-0.5 text-xs font-medium text-blue-700 dark:text-blue-300">Admin</span>
<% end %> <% end %>
<% if user.totp_enabled? %> <% 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> <span class="inline-flex items-center rounded-full bg-green-100 dark:bg-green-900/50 px-2 py-0.5 text-xs font-medium text-green-700 dark:text-green-300">2FA</span>
<% end %> <% 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> <span class="inline-flex items-center rounded-full bg-gray-100 dark:bg-gray-700 px-2 py-0.5 text-xs font-medium text-gray-700 dark:text-gray-300"><%= user.status.titleize %></span>
</div> </div>
</div> </div>
<%= link_to "View", admin_user_path(user), class: "text-blue-600 hover:text-blue-900 text-sm" %> <%= link_to "View", admin_user_path(user), class: "text-blue-600 hover:text-blue-900 text-sm" %>
@@ -41,36 +41,36 @@
<% end %> <% end %>
</ul> </ul>
<% else %> <% else %>
<div class="rounded-md bg-gray-50 p-4"> <div class="rounded-md bg-gray-50 dark:bg-gray-700 p-4">
<p class="text-sm text-gray-500">No members in this group yet.</p> <p class="text-sm text-gray-500 dark:text-gray-400">No members in this group yet.</p>
</div> </div>
<% end %> <% end %>
</div> </div>
</div> </div>
<!-- Applications --> <!-- Applications -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-base font-semibold leading-6 text-gray-900 mb-4"> <h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-gray-100 mb-4">
Assigned Applications (<%= @applications.count %>) Assigned Applications (<%= @applications.count %>)
</h3> </h3>
<% if @applications.any? %> <% if @applications.any? %>
<ul class="divide-y divide-gray-200 border border-gray-200 rounded-md"> <ul class="divide-y divide-gray-200 dark:divide-gray-700 border border-gray-200 dark:border-gray-700 rounded-md">
<% @applications.each do |app| %> <% @applications.each do |app| %>
<li class="px-4 py-3 flex items-center justify-between"> <li class="px-4 py-3 flex items-center justify-between">
<div> <div>
<p class="text-sm font-medium text-gray-900"><%= app.name %></p> <p class="text-sm font-medium text-gray-900 dark:text-gray-100"><%= app.name %></p>
<div class="flex gap-2 mt-1"> <div class="flex gap-2 mt-1">
<% case app.app_type %> <% case app.app_type %>
<% when "oidc" %> <% 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> <span class="inline-flex items-center rounded-full bg-purple-100 dark:bg-purple-900/50 px-2 py-0.5 text-xs font-medium text-purple-700 dark:text-purple-300">OIDC</span>
<% when "trusted_header" %> <% 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> <span class="inline-flex items-center rounded-full bg-indigo-100 dark:bg-indigo-900/50 px-2 py-0.5 text-xs font-medium text-indigo-700 dark:text-indigo-300">ForwardAuth</span>
<% end %> <% end %>
<% if app.active? %> <% 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> <span class="inline-flex items-center rounded-full bg-green-100 dark:bg-green-900/50 px-2 py-0.5 text-xs font-medium text-green-700 dark:text-green-300">Active</span>
<% else %> <% 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> <span class="inline-flex items-center rounded-full bg-gray-100 dark:bg-gray-700 px-2 py-0.5 text-xs font-medium text-gray-700 dark:text-gray-300">Inactive</span>
<% end %> <% end %>
</div> </div>
</div> </div>
@@ -79,8 +79,8 @@
<% end %> <% end %>
</ul> </ul>
<% else %> <% else %>
<div class="rounded-md bg-gray-50 p-4"> <div class="rounded-md bg-gray-50 dark:bg-gray-700 p-4">
<p class="text-sm text-gray-500">This group is not assigned to any applications.</p> <p class="text-sm text-gray-500 dark:text-gray-400">This group is not assigned to any applications.</p>
</div> </div>
<% end %> <% end %>
</div> </div>

View File

@@ -3,29 +3,29 @@
<!-- OIDC Apps: Custom Claims --> <!-- OIDC Apps: Custom Claims -->
<% if oidc_apps.any? %> <% if oidc_apps.any? %>
<div class="mt-12 border-t pt-8"> <div class="mt-12 border-t dark:border-gray-700 pt-8">
<h2 class="text-xl font-semibold text-gray-900 mb-4">OIDC App-Specific Claims</h2> <h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">OIDC App-Specific Claims</h2>
<p class="text-sm text-gray-600 mb-6"> <p class="text-sm text-gray-600 dark:text-gray-400 mb-6">
Configure custom claims that apply only to specific OIDC applications. These override both group and user global claims and are included in ID tokens. Configure custom claims that apply only to specific OIDC applications. These override both group and user global claims and are included in ID tokens.
</p> </p>
<div class="space-y-6"> <div class="space-y-6">
<% oidc_apps.each do |app| %> <% oidc_apps.each do |app| %>
<% app_claim = user.application_user_claims.find_by(application: app) %> <% app_claim = user.application_user_claims.find_by(application: app) %>
<details class="border rounded-lg" <%= "open" if app_claim&.custom_claims&.any? %>> <details class="border dark:border-gray-700 rounded-lg" <%= "open" if app_claim&.custom_claims&.any? %>>
<summary class="cursor-pointer bg-gray-50 px-4 py-3 hover:bg-gray-100 rounded-t-lg flex items-center justify-between"> <summary class="cursor-pointer bg-gray-50 dark:bg-gray-800 px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-t-lg flex items-center justify-between">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span class="font-medium text-gray-900"><%= app.name %></span> <span class="font-medium text-gray-900 dark:text-gray-100"><%= app.name %></span>
<span class="text-xs px-2 py-1 rounded-full bg-blue-100 text-blue-700"> <span class="text-xs px-2 py-1 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300">
OIDC OIDC
</span> </span>
<% if app_claim&.custom_claims&.any? %> <% if app_claim&.custom_claims&.any? %>
<span class="text-xs px-2 py-1 rounded-full bg-amber-100 text-amber-700"> <span class="text-xs px-2 py-1 rounded-full bg-amber-100 dark:bg-amber-900/50 text-amber-700 dark:text-amber-300">
<%= app_claim.custom_claims.keys.count %> claim(s) <%= app_claim.custom_claims.keys.count %> claim(s)
</span> </span>
<% end %> <% end %>
</div> </div>
<svg class="h-5 w-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-5 w-5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
</summary> </summary>
@@ -35,22 +35,22 @@
<%= hidden_field_tag :application_id, app.id %> <%= hidden_field_tag :application_id, app.id %>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-2">Custom Claims (JSON)</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Custom Claims (JSON)</label>
<%= text_area_tag :custom_claims, <%= text_area_tag :custom_claims,
(app_claim&.custom_claims.present? ? JSON.pretty_generate(app_claim.custom_claims) : ""), (app_claim&.custom_claims.present? ? JSON.pretty_generate(app_claim.custom_claims) : ""),
rows: 8, rows: 8,
class: "w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm font-mono", class: "w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm font-mono",
placeholder: '{"kavita_groups": ["admin"], "library_access": "all"}', placeholder: '{"kavita_groups": ["admin"], "library_access": "all"}',
data: { data: {
action: "input->json-validator#validate blur->json-validator#format", action: "input->json-validator#validate blur->json-validator#format",
json_validator_target: "textarea" json_validator_target: "textarea"
} %> } %>
<div class="mt-2 space-y-1"> <div class="mt-2 space-y-1">
<p class="text-xs text-gray-600"> <p class="text-xs text-gray-600 dark:text-gray-400">
Example for <%= app.name %>: Add claims that this app specifically needs to read. Example for <%= app.name %>: Add claims that this app specifically needs to read.
</p> </p>
<p class="text-xs text-amber-600"> <p class="text-xs text-amber-600">
<strong>Note:</strong> Do not use reserved claim names (<code class="bg-amber-50 px-1 rounded">groups</code>, <code class="bg-amber-50 px-1 rounded">email</code>, <code class="bg-amber-50 px-1 rounded">name</code>, etc.). Use app-specific names like <code class="bg-amber-50 px-1 rounded">kavita_groups</code> instead. <strong>Note:</strong> Do not use reserved claim names (<code class="bg-amber-50 dark:bg-amber-900/30 px-1 rounded">groups</code>, <code class="bg-amber-50 dark:bg-amber-900/30 px-1 rounded">email</code>, <code class="bg-amber-50 dark:bg-amber-900/30 px-1 rounded">name</code>, etc.). Use app-specific names like <code class="bg-amber-50 dark:bg-amber-900/30 px-1 rounded">kavita_groups</code> instead.
</p> </p>
<div data-json-validator-target="status" class="text-xs font-medium"></div> <div data-json-validator-target="status" class="text-xs font-medium"></div>
</div> </div>
@@ -66,27 +66,27 @@
delete_application_claims_admin_user_path(user, application_id: app.id), delete_application_claims_admin_user_path(user, application_id: app.id),
method: :delete, method: :delete,
data: { turbo_confirm: "Remove app-specific claims for #{app.name}?" }, data: { turbo_confirm: "Remove app-specific claims for #{app.name}?" },
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" %> class: "rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-200 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600" %>
<% end %> <% end %>
</div> </div>
<% end %> <% end %>
<!-- Preview merged claims --> <!-- Preview merged claims -->
<div class="mt-4 border-t pt-4"> <div class="mt-4 border-t dark:border-gray-700 pt-4">
<h4 class="text-sm font-medium text-gray-700 mb-2">Preview: Final ID Token Claims for <%= app.name %></h4> <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Preview: Final ID Token Claims for <%= app.name %></h4>
<div class="bg-gray-50 rounded-lg p-3"> <div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-3">
<pre class="text-xs font-mono text-gray-800 overflow-x-auto"><%= JSON.pretty_generate(preview_user_claims(user, app)) %></pre> <pre class="text-xs font-mono text-gray-800 dark:text-gray-200 overflow-x-auto"><%= JSON.pretty_generate(preview_user_claims(user, app)) %></pre>
</div> </div>
<details class="mt-2"> <details class="mt-2">
<summary class="cursor-pointer text-xs text-gray-600 hover:text-gray-900">Show claim sources</summary> <summary class="cursor-pointer text-xs text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100">Show claim sources</summary>
<div class="mt-2 space-y-1"> <div class="mt-2 space-y-1">
<% claim_sources(user, app).each do |source| %> <% claim_sources(user, app).each do |source| %>
<div class="flex gap-2 items-start text-xs"> <div class="flex gap-2 items-start text-xs">
<span class="px-2 py-1 rounded <%= source[:type] == :group ? 'bg-blue-100 text-blue-700' : (source[:type] == :user ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700') %>"> <span class="px-2 py-1 rounded <%= source[:type] == :group ? 'bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300' : (source[:type] == :user ? 'bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300' : 'bg-amber-100 dark:bg-amber-900/50 text-amber-700 dark:text-amber-300') %>">
<%= source[:name] %> <%= source[:name] %>
</span> </span>
<code class="text-gray-700"><%= source[:claims].to_json %></code> <code class="text-gray-700 dark:text-gray-300"><%= source[:claims].to_json %></code>
</div> </div>
<% end %> <% end %>
</div> </div>
@@ -101,32 +101,32 @@
<!-- ForwardAuth Apps: Headers Preview --> <!-- ForwardAuth Apps: Headers Preview -->
<% if forward_auth_apps.any? %> <% if forward_auth_apps.any? %>
<div class="mt-12 border-t pt-8"> <div class="mt-12 border-t dark:border-gray-700 pt-8">
<h2 class="text-xl font-semibold text-gray-900 mb-4">ForwardAuth Headers Preview</h2> <h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">ForwardAuth Headers Preview</h2>
<p class="text-sm text-gray-600 mb-6"> <p class="text-sm text-gray-600 dark:text-gray-400 mb-6">
ForwardAuth applications receive HTTP headers (not OIDC tokens). Headers are based on user's email, name, groups, and admin status. ForwardAuth applications receive HTTP headers (not OIDC tokens). Headers are based on user's email, name, groups, and admin status.
</p> </p>
<div class="space-y-6"> <div class="space-y-6">
<% forward_auth_apps.each do |app| %> <% forward_auth_apps.each do |app| %>
<details class="border rounded-lg"> <details class="border dark:border-gray-700 rounded-lg">
<summary class="cursor-pointer bg-gray-50 px-4 py-3 hover:bg-gray-100 rounded-t-lg flex items-center justify-between"> <summary class="cursor-pointer bg-gray-50 dark:bg-gray-800 px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-t-lg flex items-center justify-between">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span class="font-medium text-gray-900"><%= app.name %></span> <span class="font-medium text-gray-900 dark:text-gray-100"><%= app.name %></span>
<span class="text-xs px-2 py-1 rounded-full bg-green-100 text-green-700"> <span class="text-xs px-2 py-1 rounded-full bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300">
FORWARD AUTH FORWARD AUTH
</span> </span>
<span class="text-xs text-gray-500"> <span class="text-xs text-gray-500 dark:text-gray-400">
<%= app.domain_pattern %> <%= app.domain_pattern %>
</span> </span>
</div> </div>
<svg class="h-5 w-5 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-5 w-5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg> </svg>
</summary> </summary>
<div class="p-4 space-y-4"> <div class="p-4 space-y-4">
<div class="bg-blue-50 border border-blue-200 rounded-lg p-3"> <div class="bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-700 rounded-lg p-3">
<div class="flex items-start"> <div class="flex items-start">
<svg class="h-5 w-5 text-blue-400 mr-2 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20"> <svg class="h-5 w-5 text-blue-400 mr-2 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
@@ -135,33 +135,33 @@
</div> </div>
<div> <div>
<h4 class="text-sm font-medium text-gray-700 mb-2">Headers Sent to <%= app.name %></h4> <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Headers Sent to <%= app.name %></h4>
<div class="bg-gray-50 rounded-lg p-3 border"> <div class="bg-gray-50 dark:bg-gray-800 rounded-lg p-3 border dark:border-gray-700">
<% headers = app.headers_for_user(user) %> <% headers = app.headers_for_user(user) %>
<% if headers.any? %> <% if headers.any? %>
<dl class="space-y-2 text-xs font-mono"> <dl class="space-y-2 text-xs font-mono">
<% headers.each do |header_name, value| %> <% headers.each do |header_name, value| %>
<div class="flex"> <div class="flex">
<dt class="text-blue-600 font-semibold w-48"><%= header_name %>:</dt> <dt class="text-blue-600 dark:text-blue-400 font-semibold w-48"><%= header_name %>:</dt>
<dd class="text-gray-800 flex-1"><%= value %></dd> <dd class="text-gray-800 dark:text-gray-200 flex-1"><%= value %></dd>
</div> </div>
<% end %> <% end %>
</dl> </dl>
<% else %> <% else %>
<p class="text-xs text-gray-500 italic">All headers disabled for this application.</p> <p class="text-xs text-gray-500 dark:text-gray-400 italic">All headers disabled for this application.</p>
<% end %> <% end %>
</div> </div>
<p class="mt-2 text-xs text-gray-500"> <p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
These headers are configured in the application settings and sent by your reverse proxy (Caddy/Traefik) to the upstream application. These headers are configured in the application settings and sent by your reverse proxy (Caddy/Traefik) to the upstream application.
</p> </p>
</div> </div>
<% if user.groups.any? %> <% if user.groups.any? %>
<div> <div>
<h4 class="text-sm font-medium text-gray-700 mb-2">User's Groups</h4> <h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">User's Groups</h4>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<% user.groups.each do |group| %> <% user.groups.each do |group| %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"> <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200">
<%= group.name %> <%= group.name %>
</span> </span>
<% end %> <% end %>
@@ -176,10 +176,10 @@
<% end %> <% end %>
<% if oidc_apps.empty? && forward_auth_apps.empty? %> <% if oidc_apps.empty? && forward_auth_apps.empty? %>
<div class="mt-12 border-t pt-8"> <div class="mt-12 border-t dark:border-gray-700 pt-8">
<div class="text-center py-12 bg-gray-50 rounded-lg"> <div class="text-center py-12 bg-gray-50 dark:bg-gray-800 rounded-lg">
<p class="text-gray-500">No active applications found.</p> <p class="text-gray-500 dark:text-gray-400">No active applications found.</p>
<p class="text-sm text-gray-400 mt-1">Create applications in the Admin panel first.</p> <p class="text-sm text-gray-400 dark:text-gray-500 mt-1">Create applications in the Admin panel first.</p>
</div> </div>
</div> </div>
<% end %> <% end %>

View File

@@ -2,49 +2,49 @@
<%= render "shared/form_errors", form: form %> <%= render "shared/form_errors", form: form %>
<div> <div>
<%= form.label :email_address, class: "block text-sm font-medium text-gray-700" %> <%= form.label :email_address, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= 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" %> <%= form.email_field :email_address, required: true, class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "user@example.com" %>
</div> </div>
<div> <div>
<%= form.label :username, "Username (Optional)", class: "block text-sm font-medium text-gray-700" %> <%= form.label :username, "Username (Optional)", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :username, 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: "jsmith" %> <%= form.text_field :username, class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "jsmith" %>
<p class="mt-1 text-sm text-gray-500">Optional: Short username/handle for login. Can only contain letters, numbers, underscores, and hyphens.</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Optional: Short username/handle for login. Can only contain letters, numbers, underscores, and hyphens.</p>
</div> </div>
<div> <div>
<%= form.label :name, "Display Name (Optional)", class: "block text-sm font-medium text-gray-700" %> <%= form.label :name, "Display Name (Optional)", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_field :name, 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: "John Smith" %> <%= form.text_field :name, class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "John Smith" %>
<p class="mt-1 text-sm text-gray-500">Optional: Full name shown in applications. Defaults to email address if not set.</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Optional: Full name shown in applications. Defaults to email address if not set.</p>
</div> </div>
<div> <div>
<%= form.label :password, class: "block text-sm font-medium text-gray-700" %> <%= form.label :password, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= 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" %> <%= form.password_field :password, class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 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? %> <% if user.persisted? %>
<p class="mt-1 text-sm text-gray-500">Leave blank to keep the current password</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Leave blank to keep the current password</p>
<% else %> <% else %>
<p class="mt-1 text-sm text-gray-500">Leave blank to generate a random password</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Leave blank to generate a random password</p>
<% end %> <% end %>
</div> </div>
<div> <div>
<%= form.label :status, class: "block text-sm font-medium text-gray-700" %> <%= form.label :status, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= 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" %> <%= form.select :status, User.statuses.keys.map { |s| [s.titleize, s] }, {}, class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
</div> </div>
<div class="flex items-center"> <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.check_box :admin, class: "h-4 w-4 rounded border-gray-300 dark:border-gray-600 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" %> <%= form.label :admin, "Administrator", class: "ml-2 block text-sm text-gray-900 dark:text-gray-100" %>
<% if user == Current.session.user %> <% if user == Current.session.user %>
<span class="ml-2 text-xs text-gray-500">(Cannot change your own admin status)</span> <span class="ml-2 text-xs text-gray-500 dark:text-gray-400">(Cannot change your own admin status)</span>
<% end %> <% end %>
</div> </div>
<div> <div>
<div class="flex items-center"> <div class="flex items-center">
<%= form.check_box :totp_required, class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" %> <%= form.check_box :totp_required, class: "h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500" %>
<%= form.label :totp_required, "Require Two-Factor Authentication", class: "ml-2 block text-sm text-gray-900" %> <%= form.label :totp_required, "Require Two-Factor Authentication", class: "ml-2 block text-sm text-gray-900 dark:text-gray-100" %>
<% if user.totp_required? && !user.totp_enabled? %> <% if user.totp_required? && !user.totp_enabled? %>
<span class="ml-2 text-xs text-amber-600">(User has not set up 2FA yet)</span> <span class="ml-2 text-xs text-amber-600">(User has not set up 2FA yet)</span>
<% end %> <% end %>
@@ -57,24 +57,24 @@
Warning: This user will be prompted to set up 2FA on their next login. Warning: This user will be prompted to set up 2FA on their next login.
</p> </p>
<% end %> <% end %>
<p class="mt-1 text-sm text-gray-500">When enabled, this user must use two-factor authentication to sign in.</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">When enabled, this user must use two-factor authentication to sign in.</p>
</div> </div>
<div data-controller="json-validator" data-json-validator-valid-class="border-green-500 focus:border-green-500 focus:ring-green-500" data-json-validator-invalid-class="border-red-500 focus:border-red-500 focus:ring-red-500" data-json-validator-valid-status-class="text-green-600" data-json-validator-invalid-status-class="text-red-600"> <div data-controller="json-validator" data-json-validator-valid-class="border-green-500 focus:border-green-500 focus:ring-green-500" data-json-validator-invalid-class="border-red-500 focus:border-red-500 focus:ring-red-500" data-json-validator-valid-status-class="text-green-600" data-json-validator-invalid-status-class="text-red-600">
<%= form.label :custom_claims, "Custom Claims (JSON)", class: "block text-sm font-medium text-gray-700" %> <%= form.label :custom_claims, "Custom Claims (JSON)", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.text_area :custom_claims, value: (user.custom_claims.present? ? JSON.pretty_generate(user.custom_claims) : ""), rows: 8, <%= form.text_area :custom_claims, value: (user.custom_claims.present? ? JSON.pretty_generate(user.custom_claims) : ""), rows: 8,
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", class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm font-mono",
placeholder: '{"department": "engineering", "level": "senior"}', placeholder: '{"department": "engineering", "level": "senior"}',
data: { data: {
action: "input->json-validator#validate blur->json-validator#format", action: "input->json-validator#validate blur->json-validator#format",
json_validator_target: "textarea" json_validator_target: "textarea"
} %> } %>
<div class="mt-2 text-sm text-gray-600 space-y-1"> <div class="mt-2 text-sm text-gray-600 dark:text-gray-400 space-y-1">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<p>Optional: User-specific custom claims to add to OIDC tokens. These override group-level claims.</p> <p>Optional: User-specific custom claims to add to OIDC tokens. These override group-level claims.</p>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button type="button" data-action="json-validator#format" class="text-xs bg-gray-100 hover:bg-gray-200 px-2 py-1 rounded">Format JSON</button> <button type="button" data-action="json-validator#format" class="text-xs bg-gray-100 dark:bg-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-600 px-2 py-1 rounded">Format JSON</button>
<button type="button" data-action="json-validator#insertSample" data-json-sample='{"department": "engineering", "level": "senior", "location": "remote"}' class="text-xs bg-blue-100 hover:bg-blue-200 text-blue-700 px-2 py-1 rounded">Insert Example</button> <button type="button" data-action="json-validator#insertSample" data-json-sample='{"department": "engineering", "level": "senior", "location": "remote"}' class="text-xs bg-blue-100 dark:bg-blue-900/50 hover:bg-blue-200 dark:hover:bg-blue-900 text-blue-700 dark:text-blue-300 px-2 py-1 rounded">Insert Example</button>
</div> </div>
</div> </div>
<div data-json-validator-target="status" class="text-xs font-medium"></div> <div data-json-validator-target="status" class="text-xs font-medium"></div>
@@ -83,6 +83,6 @@
<div class="flex gap-3"> <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" %> <%= 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" %> <%= link_to "Cancel", admin_users_path, class: "rounded-md bg-white dark:bg-gray-700 px-3 py-2 text-sm font-semibold text-gray-900 dark:text-gray-200 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600" %>
</div> </div>
<% end %> <% end %>

View File

@@ -1,6 +1,6 @@
<div class="max-w-4xl"> <div class="max-w-4xl">
<h1 class="text-2xl font-semibold text-gray-900 mb-6">Edit User</h1> <h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-6">Edit User</h1>
<p class="text-sm text-gray-600 mb-6">Editing: <%= @user.email_address %></p> <p class="text-sm text-gray-600 dark:text-gray-400 mb-6">Editing: <%= @user.email_address %></p>
<div class="max-w-2xl"> <div class="max-w-2xl">
<%= render "form", user: @user %> <%= render "form", user: @user %>

View File

@@ -1,7 +1,7 @@
<div class="sm:flex sm:items-center"> <div class="sm:flex sm:items-center">
<div class="sm:flex-auto"> <div class="sm:flex-auto">
<h1 class="text-2xl font-semibold text-gray-900">Users</h1> <h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">Users</h1>
<p class="mt-2 text-sm text-gray-700">A list of all users in the system.</p> <p class="mt-2 text-sm text-gray-700 dark:text-gray-300">A list of all users in the system.</p>
</div> </div>
<div class="mt-4 sm:mt-0 sm:ml-16 sm:flex-none"> <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" %> <%= 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" %>
@@ -9,7 +9,7 @@
</div> </div>
<% unless smtp_configured? %> <% unless smtp_configured? %>
<div class="mt-6 rounded-md bg-yellow-50 p-4"> <div class="mt-6 rounded-md bg-yellow-50 dark:bg-yellow-900/30 p-4">
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> <svg class="h-5 w-5 text-yellow-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
@@ -17,10 +17,10 @@
</svg> </svg>
</div> </div>
<div class="ml-3"> <div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800"> <h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
Email delivery not configured Email delivery not configured
</h3> </h3>
<div class="mt-2 text-sm text-yellow-700"> <div class="mt-2 text-sm text-yellow-700 dark:text-yellow-300">
<p> <p>
<% if Rails.env.development? %> <% if Rails.env.development? %>
Emails are being delivered using <span class="font-mono"><%= email_delivery_method %></span> and will open in your browser. Emails are being delivered using <span class="font-mono"><%= email_delivery_method %></span> and will open in your browser.
@@ -44,63 +44,63 @@
<div class="mt-8 flow-root"> <div class="mt-8 flow-root">
<div class="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"> <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"> <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"> <table class="min-w-full divide-y divide-gray-300 dark:divide-gray-600">
<thead> <thead>
<tr> <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="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 dark:text-gray-100 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 dark:text-gray-100">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 dark:text-gray-100">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 dark:text-gray-100">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="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 dark:text-gray-100">Groups</th>
<th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-0"> <th scope="col" class="relative py-3.5 pl-3 pr-4 sm:pr-0">
<span class="sr-only">Actions</span> <span class="sr-only">Actions</span>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-200"> <tbody class="divide-y divide-gray-200 dark:divide-gray-700">
<% @users.each do |user| %> <% @users.each do |user| %>
<tr> <tr>
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0"> <td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 dark:text-gray-100 sm:pl-0">
<%= user.email_address %> <%= user.email_address %>
</td> </td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500"> <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
<% if user.status.present? %> <% if user.status.present? %>
<% case user.status.to_sym %> <% case user.status.to_sym %>
<% when :active %> <% 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> <span class="inline-flex items-center rounded-full bg-green-100 dark:bg-green-900/50 px-2 py-1 text-xs font-medium text-green-700 dark:text-green-300">Active</span>
<% when :disabled %> <% 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> <span class="inline-flex items-center rounded-full bg-red-100 dark:bg-red-900/50 px-2 py-1 text-xs font-medium text-red-700 dark:text-red-300">Disabled</span>
<% when :pending_invitation %> <% 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> <span class="inline-flex items-center rounded-full bg-yellow-100 dark:bg-yellow-900/50 px-2 py-1 text-xs font-medium text-yellow-700 dark:text-yellow-300">Pending</span>
<% end %> <% end %>
<% else %> <% else %>
<span class="text-gray-400">-</span> <span class="text-gray-400 dark:text-gray-500">-</span>
<% end %> <% end %>
</td> </td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500"> <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
<% if user.admin? %> <% 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> <span class="inline-flex items-center rounded-full bg-blue-100 dark:bg-blue-900/50 px-2 py-1 text-xs font-medium text-blue-700 dark:text-blue-300">Admin</span>
<% else %> <% else %>
<span class="text-gray-500">User</span> <span class="text-gray-500 dark:text-gray-400">User</span>
<% end %> <% end %>
</td> </td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500"> <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<% if user.totp_enabled? %> <% if user.totp_enabled? %>
<svg class="h-5 w-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" title="2FA Enabled"> <svg class="h-5 w-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" title="2FA Enabled">
<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> <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> </svg>
<% else %> <% else %>
<svg class="h-5 w-5 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24" title="2FA Not Enabled"> <svg class="h-5 w-5 text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" title="2FA Not Enabled">
<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> <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> </svg>
<% end %> <% end %>
<% if user.totp_required? %> <% if user.totp_required? %>
<span class="inline-flex items-center rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-700" title="2FA Required by Admin">Required</span> <span class="inline-flex items-center rounded-full bg-blue-100 dark:bg-blue-900/50 px-2 py-1 text-xs font-medium text-blue-700 dark:text-blue-300" title="2FA Required by Admin">Required</span>
<% end %> <% end %>
</div> </div>
</td> </td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500"> <td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500 dark:text-gray-400">
<%= user.groups.count %> <%= user.groups.count %>
</td> </td>
<td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0"> <td class="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">

View File

@@ -1,4 +1,4 @@
<div class="max-w-2xl"> <div class="max-w-2xl">
<h1 class="text-2xl font-semibold text-gray-900 mb-6">New User</h1> <h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100 mb-6">New User</h1>
<%= render "form", user: @user %> <%= render "form", user: @user %>
</div> </div>

View File

@@ -1,44 +1,44 @@
<div class="max-w-4xl mx-auto"> <div class="max-w-4xl mx-auto">
<div class="mb-8 flex items-center justify-between"> <div class="mb-8 flex items-center justify-between">
<div> <div>
<h1 class="text-3xl font-bold text-gray-900">API Keys</h1> <h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">API Keys</h1>
<p class="mt-2 text-sm text-gray-600"> <p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Bearer tokens for server-to-server access to forward auth applications. Bearer tokens for server-to-server access to forward auth applications.
</p> </p>
</div> </div>
<%= link_to "New API Key", new_api_key_path, <%= link_to "New API Key", new_api_key_path,
class: "inline-flex items-center rounded-md border border-transparent bg-blue-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" %> class: "inline-flex items-center rounded-md border border-transparent bg-blue-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900" %>
</div> </div>
<% if @api_keys.any? %> <% if @api_keys.any? %>
<div class="bg-white shadow overflow-hidden sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow overflow-hidden sm:rounded-lg">
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50"> <thead class="bg-gray-50 dark:bg-gray-700">
<tr> <tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Application</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Application</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Created</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Last Used</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Last Used</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Expires</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Expires</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th> <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"></th> <th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"></th>
</tr> </tr>
</thead> </thead>
<tbody class="bg-white divide-y divide-gray-200"> <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
<% @api_keys.each do |key| %> <% @api_keys.each do |key| %>
<tr> <tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900"><%= key.name %></td> <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100"><%= key.name %></td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"><%= key.application.name %></td> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= key.application.name %></td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"><%= key.created_at.strftime("%b %d, %Y") %></td> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= key.created_at.strftime("%b %d, %Y") %></td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"><%= key.last_used_at ? time_ago_in_words(key.last_used_at) + " ago" : "Never" %></td> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= key.last_used_at ? time_ago_in_words(key.last_used_at) + " ago" : "Never" %></td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"><%= key.expires_at ? key.expires_at.strftime("%b %d, %Y") : "Never" %></td> <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"><%= key.expires_at ? key.expires_at.strftime("%b %d, %Y") : "Never" %></td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<% if key.revoked? %> <% if key.revoked? %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">Revoked</span> <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 dark:bg-red-900/50 text-red-800 dark:text-red-200">Revoked</span>
<% elsif key.expired? %> <% elsif key.expired? %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">Expired</span> <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-200">Expired</span>
<% else %> <% else %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">Active</span> <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-200">Active</span>
<% end %> <% end %>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
@@ -54,12 +54,12 @@
</table> </table>
</div> </div>
<% else %> <% else %>
<div class="bg-gray-50 rounded-lg border border-gray-200 p-8 text-center"> <div class="bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-8 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"></path>
</svg> </svg>
<h3 class="mt-4 text-lg font-medium text-gray-900">No API keys</h3> <h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-gray-100">No API keys</h3>
<p class="mt-2 text-sm text-gray-500"> <p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
Create an API key to authenticate server-to-server requests. Create an API key to authenticate server-to-server requests.
</p> </p>
<div class="mt-6"> <div class="mt-6">

View File

@@ -1,17 +1,17 @@
<div class="max-w-lg mx-auto"> <div class="max-w-lg mx-auto">
<div class="mb-8"> <div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">New API Key</h1> <h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">New API Key</h1>
<p class="mt-2 text-sm text-gray-600"> <p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Create a bearer token for server-to-server access to a forward auth application. Create a bearer token for server-to-server access to a forward auth application.
</p> </p>
</div> </div>
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<%= form_with(model: @api_key, class: "space-y-6") do |f| %> <%= form_with(model: @api_key, class: "space-y-6") do |f| %>
<% if @api_key.errors.any? %> <% if @api_key.errors.any? %>
<div class="rounded-md bg-red-50 p-4"> <div class="rounded-md bg-red-50 dark:bg-red-900/30 p-4">
<div class="text-sm text-red-700"> <div class="text-sm text-red-700 dark:text-red-300">
<ul class="list-disc pl-5 space-y-1"> <ul class="list-disc pl-5 space-y-1">
<% @api_key.errors.full_messages.each do |msg| %> <% @api_key.errors.full_messages.each do |msg| %>
<li><%= msg %></li> <li><%= msg %></li>
@@ -22,32 +22,32 @@
<% end %> <% end %>
<div> <div>
<%= f.label :name, class: "block text-sm font-medium text-gray-700" %> <%= f.label :name, class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= f.text_field :name, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", <%= f.text_field :name, class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
placeholder: "e.g., Video Player WebDAV" %> placeholder: "e.g., Video Player WebDAV" %>
</div> </div>
<div> <div>
<%= f.label :application_id, "Application", class: "block text-sm font-medium text-gray-700" %> <%= f.label :application_id, "Application", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<% if @applications.any? %> <% if @applications.any? %>
<%= f.collection_select :application_id, @applications, :id, :name, <%= f.collection_select :application_id, @applications, :id, :name,
{ prompt: "Select an application" }, { prompt: "Select an application" },
{ class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" } %> { class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" } %>
<% else %> <% else %>
<p class="mt-1 text-sm text-gray-500">No forward auth applications available.</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">No forward auth applications available.</p>
<% end %> <% end %>
</div> </div>
<div> <div>
<%= f.label :expires_at, "Expiration (optional)", class: "block text-sm font-medium text-gray-700" %> <%= f.label :expires_at, "Expiration (optional)", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= f.datetime_local_field :expires_at, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %> <%= f.datetime_local_field :expires_at, class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
<p class="mt-1 text-xs text-gray-500">Leave blank for no expiration.</p> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Leave blank for no expiration.</p>
</div> </div>
<div class="flex items-center justify-end gap-3"> <div class="flex items-center justify-end gap-3">
<%= link_to "Cancel", api_keys_path, class: "text-sm font-medium text-gray-700 hover:text-gray-500" %> <%= link_to "Cancel", api_keys_path, class: "text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-500 dark:hover:text-gray-400" %>
<%= f.submit "Create API Key", <%= f.submit "Create API Key",
class: "inline-flex justify-center rounded-md border border-transparent bg-blue-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" %> class: "inline-flex justify-center rounded-md border border-transparent bg-blue-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900" %>
</div> </div>
<% end %> <% end %>
</div> </div>

View File

@@ -1,19 +1,19 @@
<div class="max-w-2xl mx-auto" data-controller="clipboard"> <div class="max-w-2xl mx-auto" data-controller="clipboard">
<div class="mb-8"> <div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">API Key Created</h1> <h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">API Key Created</h1>
<p class="mt-2 text-sm text-gray-600"> <p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Copy your API key now. You won't be able to see it again. Copy your API key now. You won't be able to see it again.
</p> </p>
</div> </div>
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<div class="rounded-md bg-yellow-50 p-4 mb-6"> <div class="rounded-md bg-yellow-50 dark:bg-yellow-900/30 p-4 mb-6">
<div class="flex"> <div class="flex">
<svg class="h-5 w-5 text-yellow-400 mr-3 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5 text-yellow-400 mr-3 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
</svg> </svg>
<div class="text-sm text-yellow-800"> <div class="text-sm text-yellow-800 dark:text-yellow-200">
<p class="font-medium">Save this key now!</p> <p class="font-medium">Save this key now!</p>
<p class="mt-1">This is the only time you'll see the full API key. Store it securely.</p> <p class="mt-1">This is the only time you'll see the full API key. Store it securely.</p>
</div> </div>
@@ -21,14 +21,14 @@
</div> </div>
<div> <div>
<label class="block text-sm font-medium text-gray-700 mb-1">API Key</label> <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">API Key</label>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<input type="text" readonly value="<%= @plaintext_token %>" <input type="text" readonly value="<%= @plaintext_token %>"
data-clipboard-target="source" data-clipboard-target="source"
class="flex-1 rounded-md border-gray-300 bg-gray-50 font-mono text-sm shadow-sm focus:border-blue-500 focus:ring-blue-500" /> class="flex-1 rounded-md border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700 dark:text-gray-100 font-mono text-sm shadow-sm focus:border-blue-500 focus:ring-blue-500" />
<button data-action="click->clipboard#copy" <button data-action="click->clipboard#copy"
data-clipboard-target="button" data-clipboard-target="button"
class="inline-flex items-center rounded-md border border-gray-300 bg-white py-2 px-3 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"> class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 py-2 px-3 text-sm font-medium text-gray-700 dark:text-gray-200 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900">
<svg class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
</svg> </svg>
@@ -37,22 +37,22 @@
</div> </div>
</div> </div>
<div class="mt-6 space-y-2 text-sm text-gray-600"> <div class="mt-6 space-y-2 text-sm text-gray-600 dark:text-gray-400">
<p><strong>Name:</strong> <%= @api_key.name %></p> <p><strong>Name:</strong> <%= @api_key.name %></p>
<p><strong>Application:</strong> <%= @api_key.application.name %></p> <p><strong>Application:</strong> <%= @api_key.application.name %></p>
<p><strong>Expires:</strong> <%= @api_key.expires_at ? @api_key.expires_at.strftime("%b %d, %Y %H:%M") : "Never" %></p> <p><strong>Expires:</strong> <%= @api_key.expires_at ? @api_key.expires_at.strftime("%b %d, %Y %H:%M") : "Never" %></p>
</div> </div>
<div class="mt-6 rounded-md bg-gray-50 p-4"> <div class="mt-6 rounded-md bg-gray-50 dark:bg-gray-700 p-4">
<p class="text-sm font-medium text-gray-700 mb-2">Usage example:</p> <p class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Usage example:</p>
<pre class="text-xs text-gray-600 overflow-x-auto">curl -H "Authorization: Bearer <%= @plaintext_token %>" \ <pre class="text-xs text-gray-600 dark:text-gray-200 overflow-x-auto">curl -H "Authorization: Bearer <%= @plaintext_token %>" \
-H "X-Forwarded-Host: your-app.example.com" \ -H "X-Forwarded-Host: your-app.example.com" \
<%= request.base_url %>/api/verify</pre> <%= request.base_url %>/api/verify</pre>
</div> </div>
<div class="mt-8"> <div class="mt-8">
<%= link_to "Done", api_keys_path, <%= link_to "Done", api_keys_path,
class: "inline-flex justify-center rounded-md border border-transparent bg-blue-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" %> class: "inline-flex justify-center rounded-md border border-transparent bg-blue-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900" %>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,8 +1,8 @@
<div class="mb-8"> <div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900"> <h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">
Welcome, <%= @user.email_address %> Welcome, <%= @user.email_address %>
</h1> </h1>
<p class="mt-2 text-gray-600"> <p class="mt-2 text-gray-600 dark:text-gray-400">
<% if @user.admin? %> <% if @user.admin? %>
Administrator Administrator
<% else %> <% else %>
@@ -13,34 +13,34 @@
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
<!-- Active Sessions Card --> <!-- Active Sessions Card -->
<div class="bg-white overflow-hidden shadow rounded-lg"> <div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-6 w-6 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="text-sm font-medium text-gray-500 truncate"> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">
Active Sessions Active Sessions
</dt> </dt>
<dd class="text-lg font-semibold text-gray-900"> <dd class="text-lg font-semibold text-gray-900 dark:text-gray-100">
<%= @user.sessions.active.count %> <%= @user.sessions.active.count %>
</dd> </dd>
</dl> </dl>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-5 py-3"> <div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<%= link_to "View all sessions", profile_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %> <%= link_to "View all sessions", profile_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
</div> </div>
</div> </div>
<% if @user.totp_enabled? %> <% if @user.totp_enabled? %>
<!-- 2FA Status Card --> <!-- 2FA Status Card -->
<div class="bg-white overflow-hidden shadow rounded-lg"> <div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
@@ -50,7 +50,7 @@
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="text-sm font-medium text-gray-500 truncate"> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">
Two-Factor Authentication Two-Factor Authentication
</dt> </dt>
<dd class="text-lg font-semibold text-green-600"> <dd class="text-lg font-semibold text-green-600">
@@ -60,13 +60,13 @@
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-5 py-3"> <div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<%= link_to "Manage 2FA", profile_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %> <%= link_to "Manage 2FA", profile_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
</div> </div>
</div> </div>
<% else %> <% else %>
<!-- 2FA Disabled Card --> <!-- 2FA Disabled Card -->
<div class="bg-white overflow-hidden shadow rounded-lg border-2 border-yellow-200"> <div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg border-2 border-yellow-200">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
@@ -76,7 +76,7 @@
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="text-sm font-medium text-gray-500 truncate"> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">
Two-Factor Authentication Two-Factor Authentication
</dt> </dt>
<dd class="text-lg font-semibold text-yellow-600"> <dd class="text-lg font-semibold text-yellow-600">
@@ -86,34 +86,34 @@
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-5 py-3"> <div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<%= link_to "Enable 2FA", profile_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %> <%= link_to "Enable 2FA", profile_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
</div> </div>
</div> </div>
<% end %> <% end %>
<!-- API Keys Card --> <!-- API Keys Card -->
<div class="bg-white overflow-hidden shadow rounded-lg"> <div class="bg-white dark:bg-gray-800 overflow-hidden shadow rounded-lg">
<div class="p-5"> <div class="p-5">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-6 w-6 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"></path>
</svg> </svg>
</div> </div>
<div class="ml-5 w-0 flex-1"> <div class="ml-5 w-0 flex-1">
<dl> <dl>
<dt class="text-sm font-medium text-gray-500 truncate"> <dt class="text-sm font-medium text-gray-500 dark:text-gray-400 truncate">
API Keys API Keys
</dt> </dt>
<dd class="text-lg font-semibold text-gray-900"> <dd class="text-lg font-semibold text-gray-900 dark:text-gray-100">
<%= @user.api_keys.active.count %> <%= @user.api_keys.active.count %>
</dd> </dd>
</dl> </dl>
</div> </div>
</div> </div>
</div> </div>
<div class="bg-gray-50 px-5 py-3"> <div class="bg-gray-50 dark:bg-gray-700 px-5 py-3">
<%= link_to "Manage API Keys", api_keys_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %> <%= link_to "Manage API Keys", api_keys_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
</div> </div>
</div> </div>
@@ -121,39 +121,39 @@
<!-- Your Applications Section --> <!-- Your Applications Section -->
<div class="mt-8"> <div class="mt-8">
<h2 class="text-xl font-semibold text-gray-900 mb-4">Your Applications</h2> <h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">Your Applications</h2>
<% if @applications.any? %> <% if @applications.any? %>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
<% @applications.each do |app| %> <% @applications.each do |app| %>
<div class="bg-white rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition"> <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm hover:shadow-md transition">
<div class="p-6"> <div class="p-6">
<div class="flex items-start gap-3 mb-4"> <div class="flex items-start gap-3 mb-4">
<% if app.icon.attached? %> <% if app.icon.attached? %>
<%= image_tag app.icon, class: "h-12 w-12 rounded-lg object-cover border border-gray-200 shrink-0", alt: "#{app.name} icon" %> <%= image_tag app.icon, class: "h-12 w-12 rounded-lg object-cover border border-gray-200 dark:border-gray-700 shrink-0", alt: "#{app.name} icon" %>
<% else %> <% else %>
<div class="h-12 w-12 rounded-lg bg-gray-100 border border-gray-200 flex items-center justify-center shrink-0"> <div class="h-12 w-12 rounded-lg bg-gray-100 dark:bg-gray-700 border border-gray-200 dark:border-gray-700 flex items-center justify-center shrink-0">
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-6 w-6 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg> </svg>
</div> </div>
<% end %> <% end %>
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<div class="flex items-start justify-between"> <div class="flex items-start justify-between">
<h3 class="text-lg font-semibold text-gray-900 truncate"> <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 truncate">
<%= app.name %> <%= app.name %>
</h3> </h3>
<span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium shrink-0 <span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium shrink-0
<% if app.oidc? %> <% if app.oidc? %>
bg-blue-100 text-blue-800 bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-200
<% else %> <% else %>
bg-green-100 text-green-800 bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-200
<% end %>"> <% end %>">
<%= app.app_type.humanize %> <%= app.app_type.humanize %>
</span> </span>
</div> </div>
<% if app.description.present? %> <% if app.description.present? %>
<p class="text-sm text-gray-600 mt-1 line-clamp-2"> <p class="text-sm text-gray-600 dark:text-gray-400 mt-1 line-clamp-2">
<%= app.description %> <%= app.description %>
</p> </p>
<% end %> <% end %>
@@ -165,30 +165,40 @@
<%= link_to "Open Application", app.landing_url, <%= link_to "Open Application", app.landing_url,
target: "_blank", target: "_blank",
rel: "noopener noreferrer", rel: "noopener noreferrer",
class: "w-full flex justify-center items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition" %> class: "w-full flex justify-center items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-900 focus:ring-blue-500 transition" %>
<% else %> <% else %>
<div class="text-sm text-gray-500 italic"> <div class="text-sm text-gray-500 dark:text-gray-400 italic">
No landing URL configured No landing URL configured
</div> </div>
<% end %> <% end %>
<% if app.user_has_active_session?(@user) %> <% if app.user_has_active_session?(@user) %>
<%= button_to "Require Re-Auth", logout_from_app_active_sessions_path(application_id: app.id), method: :delete, <%= button_to "Require Re-Auth", logout_from_app_active_sessions_path(application_id: app.id), method: :delete,
class: "w-full flex justify-center items-center px-4 py-2 border border-orange-300 text-sm font-medium rounded-md text-orange-700 bg-white hover:bg-orange-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 transition", class: "w-full flex justify-center items-center px-4 py-2 border border-orange-300 text-sm font-medium rounded-md text-orange-700 bg-white dark:bg-gray-700 dark:ring-gray-600 dark:text-gray-200 hover:bg-orange-50 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-900 focus:ring-orange-500 transition",
form: { data: { turbo_confirm: "This will revoke #{app.name}'s access tokens. The next time #{app.name} needs to authenticate, you'll sign in again (no re-authorization needed). Continue?" } } %> form: { data: { turbo_confirm: "This will revoke #{app.name}'s access tokens. The next time #{app.name} needs to authenticate, you'll sign in again (no re-authorization needed). Continue?" } } %>
<% end %> <% end %>
<% if @user.admin? %>
<div class="flex gap-2 mt-1">
<%= link_to "View", admin_application_path(app),
class: "text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-blue-600 transition" %>
<span class="text-gray-300 dark:text-gray-600">|</span>
<%= link_to "Edit", edit_admin_application_path(app),
class: "text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-blue-600 transition" %>
</div>
<% end %>
</div> </div>
</div> </div>
</div> </div>
<% end %> <% end %>
</div> </div>
<% else %> <% else %>
<div class="bg-gray-50 rounded-lg border border-gray-200 p-8 text-center"> <div class="bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-8 text-center">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
</svg> </svg>
<h3 class="mt-4 text-lg font-medium text-gray-900">No applications available</h3> <h3 class="mt-4 text-lg font-medium text-gray-900 dark:text-gray-100">No applications available</h3>
<p class="mt-2 text-sm text-gray-500"> <p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
You don't have access to any applications yet. Contact your administrator if you think this is an error. You don't have access to any applications yet. Contact your administrator if you think this is an error.
</p> </p>
</div> </div>
@@ -197,21 +207,21 @@
<% if @user.admin? %> <% if @user.admin? %>
<div class="mt-8"> <div class="mt-8">
<h2 class="text-xl font-semibold text-gray-900 mb-4">Admin Quick Actions</h2> <h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100 mb-4">Admin Quick Actions</h2>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3"> <div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<%= link_to admin_users_path, class: "block p-6 bg-white rounded-lg border border-gray-200 shadow-sm hover:bg-gray-50 hover:shadow-md transition" do %> <%= link_to admin_users_path, class: "block p-6 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 hover:shadow-md transition" do %>
<h3 class="text-lg font-semibold text-gray-900 mb-2">Manage Users</h3> <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">Manage Users</h3>
<p class="text-sm text-gray-600">View, edit, and invite users</p> <p class="text-sm text-gray-600 dark:text-gray-400">View, edit, and invite users</p>
<% end %> <% end %>
<%= link_to admin_applications_path, class: "block p-6 bg-white rounded-lg border border-gray-200 shadow-sm hover:bg-gray-50 hover:shadow-md transition" do %> <%= link_to admin_applications_path, class: "block p-6 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 hover:shadow-md transition" do %>
<h3 class="text-lg font-semibold text-gray-900 mb-2">Manage Applications</h3> <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">Manage Applications</h3>
<p class="text-sm text-gray-600">Register and configure applications</p> <p class="text-sm text-gray-600 dark:text-gray-400">Register and configure applications</p>
<% end %> <% end %>
<%= link_to admin_groups_path, class: "block p-6 bg-white rounded-lg border border-gray-200 shadow-sm hover:bg-gray-50 hover:shadow-md transition" do %> <%= link_to admin_groups_path, class: "block p-6 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 hover:shadow-md transition" do %>
<h3 class="text-lg font-semibold text-gray-900 mb-2">Manage Groups</h3> <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2">Manage Groups</h3>
<p class="text-sm text-gray-600">Create and organize user groups</p> <p class="text-sm text-gray-600 dark:text-gray-400">Create and organize user groups</p>
<% end %> <% end %>
</div> </div>
</div> </div>

View File

@@ -4,15 +4,15 @@
<% end %> <% end %>
<h1 class="font-bold text-4xl">Welcome to Clinch!</h1> <h1 class="font-bold text-4xl">Welcome to Clinch!</h1>
<p class="mt-2 text-gray-600">You've been invited to join Clinch. Please create your password to complete your account setup.</p> <p class="mt-2 text-gray-600 dark:text-gray-400">You've been invited to join Clinch. Please create your password to complete your account setup.</p>
<%= form_with url: invitation_path(params[:token]), method: :put, class: "contents" do |form| %> <%= form_with url: invitation_path(params[:token]), method: :put, class: "contents" do |form| %>
<div class="my-5"> <div class="my-5">
<%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter your password", maxlength: 72, class: "block shadow-sm rounded-md border border-gray-400 focus:outline-solid focus:outline-blue-600 px-3 py-2 mt-2 w-full" %> <%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter your password", maxlength: 72, class: "block shadow-sm rounded-md border border-gray-400 focus:outline-solid focus:outline-blue-600 px-3 py-2 mt-2 w-full dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100" %>
</div> </div>
<div class="my-5"> <div class="my-5">
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Confirm your password", maxlength: 72, class: "block shadow-sm rounded-md border border-gray-400 focus:outline-solid focus:outline-blue-600 px-3 py-2 mt-2 w-full" %> <%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Confirm your password", maxlength: 72, class: "block shadow-sm rounded-md border border-gray-400 focus:outline-solid focus:outline-blue-600 px-3 py-2 mt-2 w-full dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100" %>
</div> </div>
<div class="inline"> <div class="inline">

View File

@@ -9,6 +9,15 @@
<%= csrf_meta_tags %> <%= csrf_meta_tags %>
<%= csp_meta_tag %> <%= csp_meta_tag %>
<script>
(function() {
var theme = localStorage.getItem('theme');
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
})();
</script>
<%= yield :head %> <%= yield :head %>
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %> <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
@@ -23,15 +32,15 @@
<%= javascript_importmap_tags %> <%= javascript_importmap_tags %>
</head> </head>
<body> <body class="dark:bg-gray-900 dark:text-gray-100">
<% if authenticated? %> <% if authenticated? %>
<div data-controller="mobile-sidebar"> <div data-controller="mobile-sidebar">
<%= render "shared/sidebar" %> <%= render "shared/sidebar" %>
<div class="lg:pl-64"> <div class="lg:pl-64">
<!-- Mobile menu button --> <!-- Mobile menu button -->
<div class="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-x-4 border-b border-gray-200 bg-white px-4 shadow-sm sm:gap-x-6 sm:px-6 lg:hidden"> <div class="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-x-4 border-b border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-4 shadow-sm sm:gap-x-6 sm:px-6 lg:hidden">
<button type="button" <button type="button"
class="-m-2.5 p-2.5 text-gray-700" class="-m-2.5 p-2.5 text-gray-700 dark:text-gray-300"
id="mobile-menu-button" id="mobile-menu-button"
data-action="click->mobile-sidebar#openSidebar"> data-action="click->mobile-sidebar#openSidebar">
<span class="sr-only">Open sidebar</span> <span class="sr-only">Open sidebar</span>
@@ -51,6 +60,16 @@
</div> </div>
<% else %> <% else %>
<!-- Public layout (signup/signin) --> <!-- Public layout (signup/signin) -->
<div class="absolute top-4 right-4" data-controller="dark-mode">
<button type="button" data-action="click->dark-mode#toggle" class="rounded-lg p-2 text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800">
<svg data-dark-mode-target="icon" data-mode="light" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
</svg>
<svg data-dark-mode-target="icon" data-mode="dark" class="hidden h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
</svg>
</button>
</div>
<main class="container mx-auto mt-28 px-5"> <main class="container mx-auto mt-28 px-5">
<%= render "shared/flash" %> <%= render "shared/flash" %>
<%= yield %> <%= yield %>

View File

@@ -1,30 +1,30 @@
<div class="mx-auto max-w-md"> <div class="mx-auto max-w-md">
<div class="bg-white py-8 px-6 shadow rounded-lg sm:px-10"> <div class="bg-white dark:bg-gray-800 py-8 px-6 shadow rounded-lg sm:px-10">
<div class="mb-8 text-center"> <div class="mb-8 text-center">
<% if @application.icon.attached? %> <% if @application.icon.attached? %>
<%= image_tag @application.icon, class: "mx-auto h-20 w-20 rounded-xl object-cover border-2 border-gray-200 shadow-sm mb-4", alt: "#{@application.name} icon" %> <%= image_tag @application.icon, class: "mx-auto h-20 w-20 rounded-xl object-cover border-2 border-gray-200 dark:border-gray-700 shadow-sm mb-4", alt: "#{@application.name} icon" %>
<% else %> <% else %>
<div class="mx-auto h-20 w-20 rounded-xl bg-gray-100 border-2 border-gray-200 flex items-center justify-center mb-4"> <div class="mx-auto h-20 w-20 rounded-xl bg-gray-100 dark:bg-gray-700 border-2 border-gray-200 dark:border-gray-700 flex items-center justify-center mb-4">
<svg class="h-10 w-10 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-10 w-10 text-gray-400 dark:text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg> </svg>
</div> </div>
<% end %> <% end %>
<h2 class="text-2xl font-bold text-gray-900">Authorize Application</h2> <h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Authorize Application</h2>
<p class="mt-2 text-sm text-gray-600"> <p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
<strong><%= @application.name %></strong> is requesting access to your account. <strong><%= @application.name %></strong> is requesting access to your account.
</p> </p>
</div> </div>
<div class="mb-6"> <div class="mb-6">
<h3 class="text-sm font-medium text-gray-900 mb-3">This application will be able to:</h3> <h3 class="text-sm font-medium text-gray-900 dark:text-gray-100 mb-3">This application will be able to:</h3>
<ul class="space-y-2"> <ul class="space-y-2">
<% if @scopes.include?("openid") %> <% if @scopes.include?("openid") %>
<li class="flex items-start"> <li class="flex items-start">
<svg class="h-5 w-5 text-green-500 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"> <svg class="h-5 w-5 text-green-500 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg> </svg>
<span class="text-sm text-gray-700">Verify your identity</span> <span class="text-sm text-gray-700 dark:text-gray-300">Verify your identity</span>
</li> </li>
<% end %> <% end %>
<% if @scopes.include?("email") %> <% if @scopes.include?("email") %>
@@ -32,7 +32,7 @@
<svg class="h-5 w-5 text-green-500 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"> <svg class="h-5 w-5 text-green-500 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg> </svg>
<span class="text-sm text-gray-700">Access your email address (<%= Current.session.user.email_address %>)</span> <span class="text-sm text-gray-700 dark:text-gray-300">Access your email address (<%= Current.session.user.email_address %>)</span>
</li> </li>
<% end %> <% end %>
<% if @scopes.include?("profile") %> <% if @scopes.include?("profile") %>
@@ -40,7 +40,7 @@
<svg class="h-5 w-5 text-green-500 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"> <svg class="h-5 w-5 text-green-500 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg> </svg>
<span class="text-sm text-gray-700">Access your profile information</span> <span class="text-sm text-gray-700 dark:text-gray-300">Access your profile information</span>
</li> </li>
<% end %> <% end %>
<% if @scopes.include?("groups") %> <% if @scopes.include?("groups") %>
@@ -48,18 +48,18 @@
<svg class="h-5 w-5 text-green-500 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"> <svg class="h-5 w-5 text-green-500 mr-2 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg> </svg>
<span class="text-sm text-gray-700">Access your group memberships</span> <span class="text-sm text-gray-700 dark:text-gray-300">Access your group memberships</span>
</li> </li>
<% end %> <% end %>
</ul> </ul>
</div> </div>
<div class="rounded-md bg-blue-50 p-4 mb-6"> <div class="rounded-md bg-blue-50 dark:bg-blue-900/30 p-4 mb-6">
<div class="flex"> <div class="flex">
<svg class="h-5 w-5 text-blue-400 mr-3 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5 text-blue-400 mr-3 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/> <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
</svg> </svg>
<div class="text-sm text-blue-700"> <div class="text-sm text-blue-700 dark:text-blue-300">
<p>You'll be redirected to:</p> <p>You'll be redirected to:</p>
<p class="mt-1 font-mono text-xs break-all"><%= @redirect_uri %></p> <p class="mt-1 font-mono text-xs break-all"><%= @redirect_uri %></p>
</div> </div>
@@ -68,13 +68,13 @@
<%= form_with url: "/oauth/authorize/consent", method: :post, class: "space-y-3", data: { turbo: false }, local: true do |form| %> <%= form_with url: "/oauth/authorize/consent", method: :post, class: "space-y-3", data: { turbo: false }, local: true do |form| %>
<%= form.submit "Authorize", <%= form.submit "Authorize",
class: "w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %> class: "w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-900 focus:ring-blue-500" %>
<%= button_tag "Deny", <%= button_tag "Deny",
type: :submit, type: :submit,
name: :deny, name: :deny,
value: "1", value: "1",
class: "w-full flex justify-center py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %> class: "w-full flex justify-center py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 dark:ring-gray-600 hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-900 focus:ring-blue-500" %>
<% end %> <% end %>
</div> </div>
</div> </div>

View File

@@ -7,11 +7,11 @@
<%= form_with url: password_path(params[:token]), method: :put, class: "contents" do |form| %> <%= form_with url: password_path(params[:token]), method: :put, class: "contents" do |form| %>
<div class="my-5"> <div class="my-5">
<%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter new password", maxlength: 72, class: "block shadow-sm rounded-md border border-gray-400 focus:outline-solid focus:outline-blue-600 px-3 py-2 mt-2 w-full" %> <%= form.password_field :password, required: true, autocomplete: "new-password", placeholder: "Enter new password", maxlength: 72, class: "block shadow-sm rounded-md border border-gray-400 focus:outline-solid focus:outline-blue-600 px-3 py-2 mt-2 w-full dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100" %>
</div> </div>
<div class="my-5"> <div class="my-5">
<%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72, class: "block shadow-sm rounded-md border border-gray-400 focus:outline-solid focus:outline-blue-600 px-3 py-2 mt-2 w-full" %> <%= form.password_field :password_confirmation, required: true, autocomplete: "new-password", placeholder: "Repeat new password", maxlength: 72, class: "block shadow-sm rounded-md border border-gray-400 focus:outline-solid focus:outline-blue-600 px-3 py-2 mt-2 w-full dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100" %>
</div> </div>
<div class="inline"> <div class="inline">

View File

@@ -7,7 +7,7 @@
<%= form_with url: passwords_path, class: "contents", data: { controller: "form-errors" } do |form| %> <%= form_with url: passwords_path, class: "contents", data: { controller: "form-errors" } do |form| %>
<div class="my-5"> <div class="my-5">
<%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address], class: "block shadow-sm rounded-md border border-gray-400 focus:outline-solid focus:outline-blue-600 px-3 py-2 mt-2 w-full" %> <%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address], class: "block shadow-sm rounded-md border border-gray-400 focus:outline-solid focus:outline-blue-600 px-3 py-2 mt-2 w-full dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100" %>
</div> </div>
<div class="inline"> <div class="inline">

View File

@@ -1,21 +1,21 @@
<div class="space-y-8" data-controller="modal"> <div class="space-y-8" data-controller="modal">
<div> <div>
<h1 class="text-3xl font-bold text-gray-900">Account Security</h1> <h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">Account Security</h1>
<p class="mt-2 text-sm text-gray-600">Manage your account settings, active sessions, and connected applications.</p> <p class="mt-2 text-sm text-gray-600 dark:text-gray-400">Manage your account settings, active sessions, and connected applications.</p>
</div> </div>
<!-- Account Information --> <!-- Account Information -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900">Account Information</h3> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">Account Information</h3>
<div class="mt-5 space-y-6"> <div class="mt-5 space-y-6">
<%= form_with model: @user, url: profile_path, method: :patch, class: "space-y-6" do |form| %> <%= form_with model: @user, url: profile_path, method: :patch, class: "space-y-6" do |form| %>
<% if @user.errors.any? %> <% if @user.errors.any? %>
<div class="rounded-md bg-red-50 p-4"> <div class="rounded-md bg-red-50 dark:bg-red-900/30 p-4">
<h3 class="text-sm font-medium text-red-800"> <h3 class="text-sm font-medium text-red-800 dark:text-red-200">
<%= pluralize(@user.errors.count, "error") %> prohibited this from being saved: <%= pluralize(@user.errors.count, "error") %> prohibited this from being saved:
</h3> </h3>
<ul class="mt-2 list-disc list-inside text-sm text-red-700"> <ul class="mt-2 list-disc list-inside text-sm text-red-700 dark:text-red-300">
<% @user.errors.each do |error| %> <% @user.errors.each do |error| %>
<li><%= error.full_message %></li> <li><%= error.full_message %></li>
<% end %> <% end %>
@@ -24,24 +24,24 @@
<% end %> <% end %>
<div> <div>
<%= form.label :email_address, "Email Address", class: "block text-sm font-medium text-gray-700" %> <%= form.label :email_address, "Email Address", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.email_field :email_address, <%= form.email_field :email_address,
required: true, required: true,
autocomplete: "email", autocomplete: "email",
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %> class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
</div> </div>
<div> <div>
<%= form.label :current_password, "Current Password", class: "block text-sm font-medium text-gray-700" %> <%= form.label :current_password, "Current Password", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.password_field :current_password, <%= form.password_field :current_password,
autocomplete: "current-password", autocomplete: "current-password",
placeholder: "Required to change email", placeholder: "Required to change email",
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %> class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
<p class="mt-1 text-sm text-gray-500">Enter your current password to confirm this change</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Enter your current password to confirm this change</p>
</div> </div>
<div> <div>
<%= form.submit "Update Email", class: "inline-flex justify-center rounded-md border border-transparent bg-blue-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" %> <%= form.submit "Update Email", class: "inline-flex justify-center rounded-md border border-transparent bg-blue-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900" %>
</div> </div>
<% end %> <% end %>
</div> </div>
@@ -49,38 +49,38 @@
</div> </div>
<!-- Change Password --> <!-- Change Password -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900">Change Password</h3> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">Change Password</h3>
<div class="mt-5"> <div class="mt-5">
<%= form_with model: @user, url: profile_path, method: :patch, class: "space-y-6" do |form| %> <%= form_with model: @user, url: profile_path, method: :patch, class: "space-y-6" do |form| %>
<div> <div>
<%= form.label :current_password, "Current Password", class: "block text-sm font-medium text-gray-700" %> <%= form.label :current_password, "Current Password", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.password_field :current_password, <%= form.password_field :current_password,
autocomplete: "current-password", autocomplete: "current-password",
placeholder: "Enter current password", placeholder: "Enter current 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" %> class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
</div> </div>
<div> <div>
<%= form.label :password, "New Password", class: "block text-sm font-medium text-gray-700" %> <%= form.label :password, "New Password", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.password_field :password, <%= form.password_field :password,
autocomplete: "new-password", autocomplete: "new-password",
placeholder: "Enter new password", placeholder: "Enter new 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" %> class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
<p class="mt-1 text-sm text-gray-500">Must be at least 8 characters</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Must be at least 8 characters</p>
</div> </div>
<div> <div>
<%= form.label :password_confirmation, "Confirm New Password", class: "block text-sm font-medium text-gray-700" %> <%= form.label :password_confirmation, "Confirm New Password", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= form.password_field :password_confirmation, <%= form.password_field :password_confirmation,
autocomplete: "new-password", autocomplete: "new-password",
placeholder: "Confirm new password", placeholder: "Confirm new 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" %> class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
</div> </div>
<div> <div>
<%= form.submit "Update Password", class: "inline-flex justify-center rounded-md border border-transparent bg-blue-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" %> <%= form.submit "Update Password", class: "inline-flex justify-center rounded-md border border-transparent bg-blue-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900" %>
</div> </div>
<% end %> <% end %>
</div> </div>
@@ -88,15 +88,15 @@
</div> </div>
<!-- Two-Factor Authentication --> <!-- Two-Factor Authentication -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900">Two-Factor Authentication</h3> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">Two-Factor Authentication</h3>
<div class="mt-2 max-w-xl text-sm text-gray-500"> <div class="mt-2 max-w-xl text-sm text-gray-500 dark:text-gray-400">
<p>Add an extra layer of security to your account by enabling two-factor authentication.</p> <p>Add an extra layer of security to your account by enabling two-factor authentication.</p>
</div> </div>
<div class="mt-5"> <div class="mt-5">
<% if @user.totp_enabled? %> <% if @user.totp_enabled? %>
<div class="rounded-md bg-green-50 p-4"> <div class="rounded-md bg-green-50 dark:bg-green-900/30 p-4">
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
@@ -104,11 +104,11 @@
</svg> </svg>
</div> </div>
<div class="ml-3 flex-1"> <div class="ml-3 flex-1">
<p class="text-sm font-medium text-green-800"> <p class="text-sm font-medium text-green-800 dark:text-green-200">
Two-factor authentication is enabled Two-factor authentication is enabled
</p> </p>
<% if @user.totp_required? %> <% if @user.totp_required? %>
<p class="mt-1 text-sm text-green-700"> <p class="mt-1 text-sm text-green-700 dark:text-green-300">
<svg class="inline h-4 w-4" fill="currentColor" viewBox="0 0 20 20"> <svg class="inline h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd" />
</svg> </svg>
@@ -119,12 +119,12 @@
</div> </div>
</div> </div>
<% if @user.totp_required? %> <% if @user.totp_required? %>
<div class="mt-4 rounded-md bg-blue-50 p-4"> <div class="mt-4 rounded-md bg-blue-50 dark:bg-blue-900/30 p-4">
<div class="flex"> <div class="flex">
<svg class="h-5 w-5 text-blue-400 mr-2 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5 text-blue-400 mr-2 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg> </svg>
<p class="text-sm text-blue-800"> <p class="text-sm text-blue-800 dark:text-blue-200">
Your administrator requires two-factor authentication. You cannot disable it. Your administrator requires two-factor authentication. You cannot disable it.
</p> </p>
</div> </div>
@@ -133,7 +133,7 @@
<button type="button" <button type="button"
data-action="click->modal#show" data-action="click->modal#show"
data-modal-id="view-backup-codes-modal" data-modal-id="view-backup-codes-modal"
class="inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"> class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900">
View Backup Codes View Backup Codes
</button> </button>
</div> </div>
@@ -142,19 +142,19 @@
<button type="button" <button type="button"
data-action="click->modal#show" data-action="click->modal#show"
data-modal-id="disable-2fa-modal" data-modal-id="disable-2fa-modal"
class="inline-flex items-center rounded-md border border-red-300 bg-white px-4 py-2 text-sm font-medium text-red-700 shadow-sm hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"> class="inline-flex items-center rounded-md border border-red-300 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-red-700 shadow-sm hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900">
Disable 2FA Disable 2FA
</button> </button>
<button type="button" <button type="button"
data-action="click->modal#show" data-action="click->modal#show"
data-modal-id="view-backup-codes-modal" data-modal-id="view-backup-codes-modal"
class="inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"> class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900">
View Backup Codes View Backup Codes
</button> </button>
</div> </div>
<% end %> <% end %>
<% else %> <% else %>
<%= link_to new_totp_path, class: "inline-flex items-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" do %> <%= link_to new_totp_path, class: "inline-flex items-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900" do %>
Enable 2FA Enable 2FA
<% end %> <% end %>
<% end %> <% end %>
@@ -166,17 +166,17 @@
<div id="disable-2fa-modal" <div id="disable-2fa-modal"
data-action="click->modal#closeOnBackdrop keyup@window->modal#closeOnEscape" data-action="click->modal#closeOnBackdrop keyup@window->modal#closeOnEscape"
class="hidden fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50"> class="hidden fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50">
<div class="bg-white rounded-lg px-4 pt-5 pb-4 shadow-xl max-w-md w-full"> <div class="bg-white dark:bg-gray-800 rounded-lg px-4 pt-5 pb-4 shadow-xl max-w-md w-full">
<div class="sm:flex sm:items-start"> <div class="sm:flex sm:items-start">
<div class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10"> <div class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 dark:bg-red-900/50 sm:mx-0 sm:h-10 sm:w-10">
<svg class="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg> </svg>
</div> </div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left flex-1"> <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left flex-1">
<h3 class="text-lg font-medium leading-6 text-gray-900">Disable Two-Factor Authentication</h3> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">Disable Two-Factor Authentication</h3>
<div class="mt-2"> <div class="mt-2">
<p class="text-sm text-gray-500">Enter your password to disable 2FA. This will make your account less secure.</p> <p class="text-sm text-gray-500 dark:text-gray-400">Enter your password to disable 2FA. This will make your account less secure.</p>
</div> </div>
<%= form_with url: totp_path, method: :delete, class: "mt-4" do |form| %> <%= form_with url: totp_path, method: :delete, class: "mt-4" do |form| %>
<div> <div>
@@ -184,14 +184,14 @@
placeholder: "Enter your password", placeholder: "Enter your password",
autocomplete: "current-password", autocomplete: "current-password",
required: true, required: true,
class: "block w-full rounded-md border-gray-300 shadow-sm focus:border-red-500 focus:ring-red-500 sm:text-sm" %> class: "block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-red-500 focus:ring-red-500 sm:text-sm" %>
</div> </div>
<div class="mt-4 flex gap-3"> <div class="mt-4 flex gap-3">
<%= form.submit "Disable 2FA", <%= form.submit "Disable 2FA",
class: "inline-flex justify-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2" %> class: "inline-flex justify-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900" %>
<button type="button" <button type="button"
data-action="click->modal#hide" data-action="click->modal#hide"
class="inline-flex justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"> class="inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900">
Cancel Cancel
</button> </button>
</div> </div>
@@ -205,18 +205,18 @@
<div id="view-backup-codes-modal" <div id="view-backup-codes-modal"
data-action="click->modal#closeOnBackdrop keyup@window->modal#closeOnEscape" data-action="click->modal#closeOnBackdrop keyup@window->modal#closeOnEscape"
class="hidden fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50"> class="hidden fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50">
<div class="bg-white rounded-lg px-4 pt-5 pb-4 shadow-xl max-w-md w-full"> <div class="bg-white dark:bg-gray-800 rounded-lg px-4 pt-5 pb-4 shadow-xl max-w-md w-full">
<div> <div>
<h3 class="text-lg font-medium leading-6 text-gray-900">Generate New Backup Codes</h3> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">Generate New Backup Codes</h3>
<div class="mt-2"> <div class="mt-2">
<p class="text-sm text-gray-500">Due to security improvements, you need to generate new backup codes. Your old codes have been invalidated.</p> <p class="text-sm text-gray-500 dark:text-gray-400">Due to security improvements, you need to generate new backup codes. Your old codes have been invalidated.</p>
</div> </div>
<div class="mt-3 p-3 bg-yellow-50 rounded-md"> <div class="mt-3 p-3 bg-yellow-50 dark:bg-yellow-900/30 rounded-md">
<div class="flex"> <div class="flex">
<svg class="h-5 w-5 text-yellow-400 mr-2 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5 text-yellow-400 mr-2 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
</svg> </svg>
<p class="text-sm text-yellow-800"> <p class="text-sm text-yellow-800 dark:text-yellow-200">
<strong>Important:</strong> Save the new codes immediately after generation. You won't be able to see them again without regenerating. <strong>Important:</strong> Save the new codes immediately after generation. You won't be able to see them again without regenerating.
</p> </p>
</div> </div>
@@ -227,14 +227,14 @@
placeholder: "Enter your password", placeholder: "Enter your password",
autocomplete: "current-password", autocomplete: "current-password",
required: true, required: true,
class: "block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %> class: "block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
</div> </div>
<div class="mt-4 flex gap-3"> <div class="mt-4 flex gap-3">
<%= form.submit "Generate New Codes", <%= form.submit "Generate New Codes",
class: "inline-flex justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" %> class: "inline-flex justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900" %>
<button type="button" <button type="button"
data-action="click->modal#hide" data-action="click->modal#hide"
class="inline-flex justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"> class="inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900">
Cancel Cancel
</button> </button>
</div> </div>
@@ -244,10 +244,10 @@
</div> </div>
<!-- Passkeys (WebAuthn) --> <!-- Passkeys (WebAuthn) -->
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6" data-controller="webauthn" data-webauthn-challenge-url-value="/webauthn/challenge" data-webauthn-create-url-value="/webauthn/create"> <div class="px-4 py-5 sm:p-6" data-controller="webauthn" data-webauthn-challenge-url-value="/webauthn/challenge" data-webauthn-create-url-value="/webauthn/create">
<h3 class="text-lg font-medium leading-6 text-gray-900">Passkeys</h3> <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">Passkeys</h3>
<div class="mt-2 max-w-xl text-sm text-gray-500"> <div class="mt-2 max-w-xl text-sm text-gray-500 dark:text-gray-400">
<p>Use your fingerprint, face recognition, or security key to sign in without passwords.</p> <p>Use your fingerprint, face recognition, or security key to sign in without passwords.</p>
</div> </div>
@@ -255,20 +255,20 @@
<div class="mt-5"> <div class="mt-5">
<div id="add-passkey-form" class="space-y-4"> <div id="add-passkey-form" class="space-y-4">
<div> <div>
<label for="passkey-nickname" class="block text-sm font-medium text-gray-700">Passkey Name</label> <label for="passkey-nickname" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Passkey Name</label>
<input type="text" <input type="text"
id="passkey-nickname" id="passkey-nickname"
data-webauthn-target="nickname" data-webauthn-target="nickname"
placeholder="e.g., MacBook Touch ID, iPhone Face ID" placeholder="e.g., MacBook Touch ID, iPhone Face ID"
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"> class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm">
<p class="mt-1 text-sm text-gray-500">Give this passkey a memorable name so you can identify it later.</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Give this passkey a memorable name so you can identify it later.</p>
</div> </div>
<div> <div>
<button type="button" <button type="button"
data-action="click->webauthn#register" data-action="click->webauthn#register"
data-webauthn-target="submitButton" data-webauthn-target="submitButton"
class="inline-flex items-center rounded-md border border-transparent bg-green-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"> class="inline-flex items-center rounded-md border border-transparent bg-green-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
</svg> </svg>
@@ -284,11 +284,11 @@
<!-- Existing Passkeys List --> <!-- Existing Passkeys List -->
<div class="mt-8"> <div class="mt-8">
<h4 class="text-md font-medium text-gray-900 mb-4">Your Passkeys</h4> <h4 class="text-md font-medium text-gray-900 dark:text-gray-100 mb-4">Your Passkeys</h4>
<% if @user.webauthn_credentials.exists? %> <% if @user.webauthn_credentials.exists? %>
<div class="space-y-3"> <div class="space-y-3">
<% @user.webauthn_credentials.order(created_at: :desc).each do |credential| %> <% @user.webauthn_credentials.order(created_at: :desc).each do |credential| %>
<div class="flex items-center justify-between p-4 bg-gray-50 rounded-lg"> <div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded-lg">
<div class="flex items-center space-x-3"> <div class="flex items-center space-x-3">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<% if credential.platform_authenticator? %> <% if credential.platform_authenticator? %>
@@ -304,10 +304,10 @@
<% end %> <% end %>
</div> </div>
<div> <div>
<div class="text-sm font-medium text-gray-900"> <div class="text-sm font-medium text-gray-900 dark:text-gray-100">
<%= credential.nickname %> <%= credential.nickname %>
</div> </div>
<div class="text-sm text-gray-500"> <div class="text-sm text-gray-500 dark:text-gray-400">
<%= credential.authenticator_type.humanize %> • <%= credential.authenticator_type.humanize %> •
Last used <%= credential.last_used_ago %> Last used <%= credential.last_used_ago %>
<% if credential.backed_up? %> <% if credential.backed_up? %>
@@ -318,7 +318,7 @@
</div> </div>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<% if credential.created_recently? %> <% if credential.created_recently? %>
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800"> <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-200">
New New
</span> </span>
<% end %> <% end %>
@@ -338,7 +338,7 @@
<% end %> <% end %>
</div> </div>
<div class="mt-4 p-3 bg-blue-50 rounded-lg"> <div class="mt-4 p-3 bg-blue-50 dark:bg-blue-900/30 rounded-lg">
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5 text-blue-400" viewBox="0 0 20 20" fill="currentColor">
@@ -346,7 +346,7 @@
</svg> </svg>
</div> </div>
<div class="ml-3"> <div class="ml-3">
<p class="text-sm text-blue-800"> <p class="text-sm text-blue-800 dark:text-blue-200">
<strong>Tip:</strong> Add passkeys on multiple devices for easy access. Platform authenticators (like Touch ID) are synced across your devices if you use iCloud Keychain or Google Password Manager. <strong>Tip:</strong> Add passkeys on multiple devices for easy access. Platform authenticators (like Touch ID) are synced across your devices if you use iCloud Keychain or Google Password Manager.
</p> </p>
</div> </div>
@@ -354,11 +354,11 @@
</div> </div>
<% else %> <% else %>
<div class="text-center py-8"> <div class="text-center py-8">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"></path> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"></path>
</svg> </svg>
<h3 class="mt-2 text-sm font-medium text-gray-900">No passkeys</h3> <h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-gray-100">No passkeys</h3>
<p class="mt-1 text-sm text-gray-500">Get started by adding your first passkey for passwordless sign-in.</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Get started by adding your first passkey for passwordless sign-in.</p>
</div> </div>
<% end %> <% end %>
</div> </div>

View File

@@ -6,7 +6,7 @@
<%= form_with url: signin_path, class: "contents", data: { controller: "form-errors" } do |form| %> <%= form_with url: signin_path, class: "contents", data: { controller: "form-errors" } do |form| %>
<%= hidden_field_tag :rd, params[:rd] if params[:rd].present? %> <%= hidden_field_tag :rd, params[:rd] if params[:rd].present? %>
<div class="my-5"> <div class="my-5">
<%= form.label :email_address, "Email Address", class: "block font-medium text-sm text-gray-700" %> <%= form.label :email_address, "Email Address", class: "block font-medium text-sm text-gray-700 dark:text-gray-300" %>
<%= form.email_field :email_address, <%= form.email_field :email_address,
required: true, required: true,
autofocus: true, autofocus: true,
@@ -14,17 +14,17 @@
placeholder: "your@email.com", placeholder: "your@email.com",
value: @login_hint || params[:email_address], value: @login_hint || params[:email_address],
data: { action: "blur->webauthn#checkWebAuthnSupport change->webauthn#checkWebAuthnSupport" }, data: { action: "blur->webauthn#checkWebAuthnSupport change->webauthn#checkWebAuthnSupport" },
class: "block shadow-sm rounded-md border border-gray-400 focus:outline-blue-600 px-3 py-2 mt-2 w-full" %> class: "block shadow-sm rounded-md border border-gray-400 focus:outline-blue-600 px-3 py-2 mt-2 w-full dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100" %>
</div> </div>
<!-- WebAuthn section - initially hidden --> <!-- WebAuthn section - initially hidden -->
<div id="webauthn-section" data-login-form-target="webauthnSection" class="my-5 hidden"> <div id="webauthn-section" data-login-form-target="webauthnSection" class="my-5 hidden">
<div class="bg-green-50 border border-green-200 rounded-lg p-4 mb-4"> <div class="bg-green-50 border border-green-200 rounded-lg p-4 mb-4 dark:bg-green-900/30 dark:border-green-700">
<div class="flex items-center"> <div class="flex items-center">
<svg class="w-5 h-5 text-green-600 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="w-5 h-5 text-green-600 mr-2" 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> <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> </svg>
<p class="text-sm text-green-800"> <p class="text-sm text-green-800 dark:text-green-200">
<strong>Passkey detected!</strong> You can sign in without a password. <strong>Passkey detected!</strong> You can sign in without a password.
</p> </p>
</div> </div>
@@ -44,13 +44,13 @@
<!-- Password section - shown by default, hidden if WebAuthn is required --> <!-- Password section - shown by default, hidden if WebAuthn is required -->
<div id="password-section" data-login-form-target="passwordSection"> <div id="password-section" data-login-form-target="passwordSection">
<div class="my-5"> <div class="my-5">
<%= form.label :password, class: "block font-medium text-sm text-gray-700" %> <%= form.label :password, class: "block font-medium text-sm text-gray-700 dark:text-gray-300" %>
<%= form.password_field :password, <%= form.password_field :password,
required: true, required: true,
autocomplete: "current-password", autocomplete: "current-password",
placeholder: "Enter your password", placeholder: "Enter your password",
maxlength: 72, maxlength: 72,
class: "block shadow-sm rounded-md border border-gray-400 focus:outline-blue-600 px-3 py-2 mt-2 w-full" %> class: "block shadow-sm rounded-md border border-gray-400 focus:outline-blue-600 px-3 py-2 mt-2 w-full dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100" %>
</div> </div>
<div class="my-5"> <div class="my-5">
@@ -59,14 +59,16 @@
</div> </div>
</div> </div>
<div class="mt-4 text-sm text-gray-600 text-center"> <div class="mt-4 text-sm text-gray-600 dark:text-gray-400 text-center">
<%= link_to "Forgot your password?", new_password_path, class: "text-blue-600 hover:text-blue-500 underline" %> <%= link_to "Forgot your password?", new_password_path, class: "text-blue-600 hover:text-blue-500 underline" %>
</div> </div>
<% end %> <% end %>
<!-- Loading overlay --> <!-- Loading overlay -->
<div id="loading-overlay" data-login-form-target="loadingOverlay" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center z-50"> <div id="loading-overlay" data-login-form-target="loadingOverlay"
<div class="bg-white rounded-lg p-6 flex items-center"> data-action="click->login-form#hideLoading"
class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 flex items-center justify-center z-50 cursor-pointer">
<div class="bg-white rounded-lg p-6 flex items-center dark:bg-gray-900">
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-blue-600" fill="none" viewBox="0 0 24 24"> <svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-blue-600" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>

View File

@@ -1,8 +1,8 @@
<div class="mx-auto max-w-md"> <div class="mx-auto max-w-md">
<div class="bg-white py-8 px-6 shadow rounded-lg sm:px-10"> <div class="bg-white py-8 px-6 shadow rounded-lg sm:px-10 dark:bg-gray-900">
<div class="mb-8"> <div class="mb-8">
<h2 class="text-2xl font-bold text-gray-900">Two-Factor Authentication</h2> <h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Two-Factor Authentication</h2>
<p class="mt-2 text-sm text-gray-600"> <p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Enter the 6-digit code from your authenticator app to complete sign in. Enter the 6-digit code from your authenticator app to complete sign in.
</p> </p>
</div> </div>
@@ -13,7 +13,7 @@
} do |form| %> } do |form| %>
<%= hidden_field_tag :rd, params[:rd] if params[:rd].present? %> <%= hidden_field_tag :rd, params[:rd] if params[:rd].present? %>
<div> <div>
<%= label_tag :code, "Verification Code", class: "block text-sm font-medium text-gray-700" %> <%= label_tag :code, "Verification Code", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= text_field_tag :code, <%= text_field_tag :code,
nil, nil,
placeholder: "000000", placeholder: "000000",
@@ -21,8 +21,8 @@
required: true, required: true,
autofocus: true, autofocus: true,
autocomplete: "off", autocomplete: "off",
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-center text-2xl tracking-widest font-mono sm:text-sm" %> class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-center text-2xl tracking-widest font-mono sm:text-sm dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100" %>
<p class="mt-2 text-xs text-gray-500"> <p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
Enter your 6-digit authenticator code or an 8-character backup code Enter your 6-digit authenticator code or an 8-character backup code
</p> </p>
</div> </div>
@@ -30,7 +30,7 @@
<div> <div>
<%= form.submit "Verify", <%= form.submit "Verify",
data: { form_submit_protection_target: "submit" }, data: { form_submit_protection_target: "submit" },
class: "w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" %> class: "w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-offset-gray-900" %>
</div> </div>
<% end %> <% end %>
@@ -40,10 +40,10 @@
<div class="mt-4"> <div class="mt-4">
<div class="relative my-4"> <div class="relative my-4">
<div class="absolute inset-0 flex items-center"> <div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300"></div> <div class="w-full border-t border-gray-300 dark:border-gray-600"></div>
</div> </div>
<div class="relative flex justify-center text-sm"> <div class="relative flex justify-center text-sm">
<span class="px-2 bg-white text-gray-500">Or</span> <span class="px-2 bg-white text-gray-500 dark:bg-gray-900 dark:text-gray-400">Or</span>
</div> </div>
</div> </div>
<button type="button" <button type="button"
@@ -62,18 +62,18 @@
<div class="mt-6"> <div class="mt-6">
<div class="relative"> <div class="relative">
<div class="absolute inset-0 flex items-center"> <div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300"></div> <div class="w-full border-t border-gray-300 dark:border-gray-600"></div>
</div> </div>
<div class="relative flex justify-center text-sm"> <div class="relative flex justify-center text-sm">
<span class="px-2 bg-white text-gray-500">Need help?</span> <span class="px-2 bg-white text-gray-500 dark:bg-gray-900 dark:text-gray-400">Need help?</span>
</div> </div>
</div> </div>
<div class="mt-4 text-center"> <div class="mt-4 text-center">
<p class="text-sm text-gray-600"> <p class="text-sm text-gray-600 dark:text-gray-400">
Lost access to your authenticator? Lost access to your authenticator?
</p> </p>
<p class="mt-1 text-xs text-gray-500"> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Contact an administrator for assistance. Contact an administrator for assistance.
</p> </p>
</div> </div>

View File

@@ -8,34 +8,34 @@
# Map flash types to styling # Map flash types to styling
case type.to_s case type.to_s
when 'notice' when 'notice'
bg_class = 'bg-green-50' bg_class = 'bg-green-50 dark:bg-green-900/30'
text_class = 'text-green-800' text_class = 'text-green-800 dark:text-green-200'
icon_class = 'text-green-400' icon_class = 'text-green-400 dark:text-green-300'
icon_path = 'M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z' icon_path = 'M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z'
auto_dismiss = true auto_dismiss = true
when 'alert', 'error' when 'alert', 'error'
bg_class = 'bg-red-50' bg_class = 'bg-red-50 dark:bg-red-900/30'
text_class = 'text-red-800' text_class = 'text-red-800 dark:text-red-200'
icon_class = 'text-red-400' icon_class = 'text-red-400 dark:text-red-300'
icon_path = 'M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z' icon_path = 'M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z'
auto_dismiss = false auto_dismiss = false
when 'warning' when 'warning'
bg_class = 'bg-yellow-50' bg_class = 'bg-yellow-50 dark:bg-yellow-900/30'
text_class = 'text-yellow-800' text_class = 'text-yellow-800 dark:text-yellow-200'
icon_class = 'text-yellow-400' icon_class = 'text-yellow-400 dark:text-yellow-300'
icon_path = 'M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z' icon_path = 'M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z'
auto_dismiss = false auto_dismiss = false
when 'info' when 'info'
bg_class = 'bg-blue-50' bg_class = 'bg-blue-50 dark:bg-blue-900/30'
text_class = 'text-blue-800' text_class = 'text-blue-800 dark:text-blue-200'
icon_class = 'text-blue-400' icon_class = 'text-blue-400 dark:text-blue-300'
icon_path = 'M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z' icon_path = 'M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z'
auto_dismiss = true auto_dismiss = true
else else
# Default styling for unknown types # Default styling for unknown types
bg_class = 'bg-gray-50' bg_class = 'bg-gray-50 dark:bg-gray-800'
text_class = 'text-gray-800' text_class = 'text-gray-800 dark:text-gray-200'
icon_class = 'text-gray-400' icon_class = 'text-gray-400 dark:text-gray-500'
icon_path = 'M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z' icon_path = 'M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z'
auto_dismiss = false auto_dismiss = false
end end
@@ -60,7 +60,7 @@
<div class="-mx-1.5 -my-1.5"> <div class="-mx-1.5 -my-1.5">
<button type="button" <button type="button"
data-action="click->flash#dismiss" data-action="click->flash#dismiss"
class="inline-flex rounded-md <%= bg_class %> p-1.5 <%= icon_class %> hover:bg-opacity-70 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-<%= bg_class.gsub('bg-', '') %>" class="inline-flex rounded-md <%= bg_class %> p-1.5 <%= icon_class %> hover:bg-opacity-70 focus:outline-none focus:ring-2 focus:ring-offset-2"
aria-label="Dismiss"> aria-label="Dismiss">
<span class="sr-only">Dismiss</span> <span class="sr-only">Dismiss</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">

View File

@@ -3,19 +3,19 @@
<% form_object = form.respond_to?(:object) ? form.object : (object || form) %> <% form_object = form.respond_to?(:object) ? form.object : (object || form) %>
<% if form_object&.errors&.any? %> <% if form_object&.errors&.any? %>
<div class="rounded-md bg-red-50 p-4 mb-6 border border-red-200" role="alert" aria-labelledby="form-errors-title" data-form-errors-target="container"> <div class="rounded-md bg-red-50 dark:bg-red-900/30 p-4 mb-6 border border-red-200 dark:border-red-700" role="alert" aria-labelledby="form-errors-title" data-form-errors-target="container">
<div class="flex"> <div class="flex">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> <svg class="h-5 w-5 text-red-400 dark:text-red-300" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/> <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg> </svg>
</div> </div>
<div class="ml-3 flex-1"> <div class="ml-3 flex-1">
<h3 id="form-errors-title" class="text-sm font-medium text-red-800"> <h3 id="form-errors-title" class="text-sm font-medium text-red-800 dark:text-red-200">
<%= pluralize(form_object.errors.count, "error") %> prohibited this <%= form_object.class.name.downcase.gsub(/^admin::/, '') %> from being saved: <%= pluralize(form_object.errors.count, "error") %> prohibited this <%= form_object.class.name.downcase.gsub(/^admin::/, '') %> from being saved:
</h3> </h3>
<div class="mt-2"> <div class="mt-2">
<ul class="list-disc space-y-1 pl-5 text-sm text-red-700"> <ul class="list-disc space-y-1 pl-5 text-sm text-red-700 dark:text-red-300">
<% form_object.errors.full_messages.each do |message| %> <% form_object.errors.full_messages.each do |message| %>
<li><%= message %></li> <li><%= message %></li>
<% end %> <% end %>
@@ -24,7 +24,7 @@
</div> </div>
<div class="ml-auto pl-3"> <div class="ml-auto pl-3">
<div class="-mx-1.5 -my-1.5"> <div class="-mx-1.5 -my-1.5">
<button type="button" data-action="click->form-errors#dismiss" class="inline-flex rounded-md bg-red-50 p-1.5 text-red-500 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-red-600 focus:ring-offset-2 focus:ring-offset-red-50" aria-label="Dismiss"> <button type="button" data-action="click->form-errors#dismiss" class="inline-flex rounded-md bg-red-50 dark:bg-red-900/30 p-1.5 text-red-500 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/50 focus:outline-none focus:ring-2 focus:ring-red-600 focus:ring-offset-2 focus:ring-offset-red-50 dark:focus:ring-offset-gray-900" aria-label="Dismiss">
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" /> <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
</svg> </svg>

View File

@@ -6,16 +6,16 @@
<!-- Desktop sidebar --> <!-- Desktop sidebar -->
<div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-64 lg:flex-col"> <div class="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-64 lg:flex-col">
<div class="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 bg-white px-6 pb-4"> <div class="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 px-6 pb-4">
<!-- Branding and User Info --> <!-- Branding and User Info -->
<div class="flex flex-col pt-5 pb-4 border-b border-gray-200"> <div class="flex flex-col pt-5 pb-4 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center"> <div class="flex items-center">
<h1 class="text-2xl font-bold text-gray-900">Clinch</h1> <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Clinch</h1>
</div> </div>
<div class="mt-2"> <div class="mt-2">
<p class="text-sm text-gray-600 truncate"><%= user.email_address %></p> <p class="text-sm text-gray-600 dark:text-gray-400 truncate"><%= user.email_address %></p>
<% if user.admin? %> <% 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-800 mt-1"> <span class="inline-flex items-center rounded-full bg-blue-100 dark:bg-blue-900/50 px-2 py-0.5 text-xs font-medium text-blue-800 dark:text-blue-200 mt-1">
Administrator Administrator
</span> </span>
<% end %> <% end %>
@@ -28,7 +28,7 @@
<ul role="list" class="-mx-2 space-y-1"> <ul role="list" class="-mx-2 space-y-1">
<!-- Dashboard --> <!-- Dashboard -->
<li> <li>
<%= link_to root_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path == '/' ? 'bg-gray-50 text-blue-600' : 'text-gray-700 hover:text-blue-600 hover:bg-gray-50' }" do %> <%= link_to root_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path == '/' ? 'bg-gray-50 dark:bg-gray-800 text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-50 dark:hover:bg-gray-800' }" do %>
<svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" /> <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
</svg> </svg>
@@ -39,7 +39,7 @@
<% if user.admin? %> <% if user.admin? %>
<!-- Admin: Users --> <!-- Admin: Users -->
<li> <li>
<%= link_to admin_users_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path.start_with?('/admin/users') ? 'bg-gray-50 text-blue-600' : 'text-gray-700 hover:text-blue-600 hover:bg-gray-50' }" do %> <%= link_to admin_users_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path.start_with?('/admin/users') ? 'bg-gray-50 dark:bg-gray-800 text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-50 dark:hover:bg-gray-800' }" do %>
<svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
</svg> </svg>
@@ -49,7 +49,7 @@
<!-- Admin: Applications --> <!-- Admin: Applications -->
<li> <li>
<%= link_to admin_applications_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path.start_with?('/admin/applications') ? 'bg-gray-50 text-blue-600' : 'text-gray-700 hover:text-blue-600 hover:bg-gray-50' }" do %> <%= link_to admin_applications_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path.start_with?('/admin/applications') ? 'bg-gray-50 dark:bg-gray-800 text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-50 dark:hover:bg-gray-800' }" do %>
<svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
</svg> </svg>
@@ -59,7 +59,7 @@
<!-- Admin: Groups --> <!-- Admin: Groups -->
<li> <li>
<%= link_to admin_groups_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path.start_with?('/admin/groups') ? 'bg-gray-50 text-blue-600' : 'text-gray-700 hover:text-blue-600 hover:bg-gray-50' }" do %> <%= link_to admin_groups_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path.start_with?('/admin/groups') ? 'bg-gray-50 dark:bg-gray-800 text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-50 dark:hover:bg-gray-800' }" do %>
<svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z" />
</svg> </svg>
@@ -70,7 +70,7 @@
<!-- Profile --> <!-- Profile -->
<li> <li>
<%= link_to profile_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path == '/profile' ? 'bg-gray-50 text-blue-600' : 'text-gray-700 hover:text-blue-600 hover:bg-gray-50' }" do %> <%= link_to profile_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path == '/profile' ? 'bg-gray-50 dark:bg-gray-800 text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-50 dark:hover:bg-gray-800' }" do %>
<svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
</svg> </svg>
@@ -80,7 +80,7 @@
<!-- Sessions --> <!-- Sessions -->
<li> <li>
<%= link_to active_sessions_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path == '/active_sessions' ? 'bg-gray-50 text-blue-600' : 'text-gray-700 hover:text-blue-600 hover:bg-gray-50' }" do %> <%= link_to active_sessions_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path == '/active_sessions' ? 'bg-gray-50 dark:bg-gray-800 text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-50 dark:hover:bg-gray-800' }" do %>
<svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 18v-5.25m0 0a6.01 6.01 0 001.5-.189m-1.5.189a6.01 6.01 0 01-1.5-.189m3.75 7.478a12.06 12.06 0 01-4.5 0m3.75 2.383a14.406 14.406 0 01-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 10-7.517 0c.85.493 1.509 1.333 1.509 2.316V18" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 18v-5.25m0 0a6.01 6.01 0 001.5-.189m-1.5.189a6.01 6.01 0 01-1.5-.189m3.75 7.478a12.06 12.06 0 01-4.5 0m3.75 2.383a14.406 14.406 0 01-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 10-7.517 0c.85.493 1.509 1.333 1.509 2.316V18" />
</svg> </svg>
@@ -88,9 +88,25 @@
<% end %> <% end %>
</li> </li>
<!-- Dark Mode Toggle -->
<li data-controller="dark-mode">
<button type="button" data-action="click->dark-mode#toggle" class="group flex w-full gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-50 dark:hover:bg-gray-800">
<!-- Moon icon (shown in light mode) -->
<svg data-dark-mode-target="icon" data-mode="light" class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
</svg>
<!-- Sun icon (shown in dark mode) -->
<svg data-dark-mode-target="icon" data-mode="dark" class="hidden h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
</svg>
<span data-dark-mode-target="icon" data-mode="light">Dark Mode</span>
<span data-dark-mode-target="icon" data-mode="dark" class="hidden">Light Mode</span>
</button>
</li>
<!-- Sign Out --> <!-- Sign Out -->
<li> <li>
<%= link_to signout_path, data: { turbo_method: :delete, action: "click->mobile-sidebar#closeSidebar" }, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-red-600 hover:text-red-700 hover:bg-red-50" do %> <%= link_to signout_path, data: { turbo_method: :delete, action: "click->mobile-sidebar#closeSidebar" }, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300 hover:bg-red-50 dark:hover:bg-red-900/20" do %>
<svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
</svg> </svg>
@@ -124,16 +140,16 @@
</button> </button>
</div> </div>
<div class="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 pb-2"> <div class="flex grow flex-col gap-y-5 overflow-y-auto bg-white dark:bg-gray-900 px-6 pb-2">
<!-- Branding and User Info --> <!-- Branding and User Info -->
<div class="flex flex-col pt-5 pb-4 border-b border-gray-200"> <div class="flex flex-col pt-5 pb-4 border-b border-gray-200 dark:border-gray-700">
<div class="flex items-center"> <div class="flex items-center">
<h1 class="text-2xl font-bold text-gray-900">Clinch</h1> <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Clinch</h1>
</div> </div>
<div class="mt-2"> <div class="mt-2">
<p class="text-sm text-gray-600 truncate"><%= user.email_address %></p> <p class="text-sm text-gray-600 dark:text-gray-400 truncate"><%= user.email_address %></p>
<% if user.admin? %> <% 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-800 mt-1"> <span class="inline-flex items-center rounded-full bg-blue-100 dark:bg-blue-900/50 px-2 py-0.5 text-xs font-medium text-blue-800 dark:text-blue-200 mt-1">
Administrator Administrator
</span> </span>
<% end %> <% end %>
@@ -144,7 +160,7 @@
<!-- Same nav items as desktop --> <!-- Same nav items as desktop -->
<ul role="list" class="-mx-2 space-y-1"> <ul role="list" class="-mx-2 space-y-1">
<li> <li>
<%= link_to root_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path == '/' ? 'bg-gray-50 text-blue-600' : 'text-gray-700 hover:text-blue-600 hover:bg-gray-50' }", data: { action: "click->mobile-sidebar#closeSidebar" } do %> <%= link_to root_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path == '/' ? 'bg-gray-50 dark:bg-gray-800 text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-50 dark:hover:bg-gray-800' }", data: { action: "click->mobile-sidebar#closeSidebar" } do %>
<svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" /> <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
</svg> </svg>
@@ -153,7 +169,7 @@
</li> </li>
<% if user.admin? %> <% if user.admin? %>
<li> <li>
<%= link_to admin_users_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path.start_with?('/admin/users') ? 'bg-gray-50 text-blue-600' : 'text-gray-700 hover:text-blue-600 hover:bg-gray-50' }", data: { action: "click->mobile-sidebar#closeSidebar" } do %> <%= link_to admin_users_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path.start_with?('/admin/users') ? 'bg-gray-50 dark:bg-gray-800 text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-50 dark:hover:bg-gray-800' }", data: { action: "click->mobile-sidebar#closeSidebar" } do %>
<svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />
</svg> </svg>
@@ -161,7 +177,7 @@
<% end %> <% end %>
</li> </li>
<li> <li>
<%= link_to admin_applications_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path.start_with?('/admin/applications') ? 'bg-gray-50 text-blue-600' : 'text-gray-700 hover:text-blue-600 hover:bg-gray-50' }", data: { action: "click->mobile-sidebar#closeSidebar" } do %> <%= link_to admin_applications_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path.start_with?('/admin/applications') ? 'bg-gray-50 dark:bg-gray-800 text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-50 dark:hover:bg-gray-800' }", data: { action: "click->mobile-sidebar#closeSidebar" } do %>
<svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />
</svg> </svg>
@@ -169,7 +185,7 @@
<% end %> <% end %>
</li> </li>
<li> <li>
<%= link_to admin_groups_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path.start_with?('/admin/groups') ? 'bg-gray-50 text-blue-600' : 'text-gray-700 hover:text-blue-600 hover:bg-gray-50' }", data: { action: "click->mobile-sidebar#closeSidebar" } do %> <%= link_to admin_groups_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path.start_with?('/admin/groups') ? 'bg-gray-50 dark:bg-gray-800 text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-50 dark:hover:bg-gray-800' }", data: { action: "click->mobile-sidebar#closeSidebar" } do %>
<svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z" />
</svg> </svg>
@@ -178,7 +194,7 @@
</li> </li>
<% end %> <% end %>
<li> <li>
<%= link_to profile_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path == '/profile' ? 'bg-gray-50 text-blue-600' : 'text-gray-700 hover:text-blue-600 hover:bg-gray-50' }", data: { action: "click->mobile-sidebar#closeSidebar" } do %> <%= link_to profile_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path == '/profile' ? 'bg-gray-50 dark:bg-gray-800 text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-50 dark:hover:bg-gray-800' }", data: { action: "click->mobile-sidebar#closeSidebar" } do %>
<svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" /> <path stroke-linecap="round" stroke-linejoin="round" d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z" />
</svg> </svg>
@@ -186,15 +202,30 @@
<% end %> <% end %>
</li> </li>
<li> <li>
<%= link_to active_sessions_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path == '/active_sessions' ? 'bg-gray-50 text-blue-600' : 'text-gray-700 hover:text-blue-600 hover:bg-gray-50' }", data: { action: "click->mobile-sidebar#closeSidebar" } do %> <%= link_to active_sessions_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path == '/active_sessions' ? 'bg-gray-50 dark:bg-gray-800 text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-50 dark:hover:bg-gray-800' }", data: { action: "click->mobile-sidebar#closeSidebar" } do %>
<svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 18v-5.25m0 0a6.01 6.01 0 001.5-.189m-1.5.189a6.01 6.01 0 01-1.5-.189m3.75 7.478a12.06 12.06 0 01-4.5 0m3.75 2.383a14.406 14.406 0 01-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 10-7.517 0c.85.493 1.509 1.333 1.509 2.316V18" /> <path stroke-linecap="round" stroke-linejoin="round" d="M12 18v-5.25m0 0a6.01 6.01 0 001.5-.189m-1.5.189a6.01 6.01 0 01-1.5-.189m3.75 7.478a12.06 12.06 0 01-4.5 0m3.75 2.383a14.406 14.406 0 01-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 10-7.517 0c.85.493 1.509 1.333 1.509 2.316V18" />
</svg> </svg>
Sessions Sessions
<% end %> <% end %>
</li> </li>
<!-- Dark Mode Toggle (mobile) -->
<li data-controller="dark-mode">
<button type="button" data-action="click->dark-mode#toggle" class="group flex w-full gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-gray-50 dark:hover:bg-gray-800">
<svg data-dark-mode-target="icon" data-mode="light" class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
</svg>
<svg data-dark-mode-target="icon" data-mode="dark" class="hidden h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
</svg>
<span data-dark-mode-target="icon" data-mode="light">Dark Mode</span>
<span data-dark-mode-target="icon" data-mode="dark" class="hidden">Light Mode</span>
</button>
</li>
<li> <li>
<%= link_to signout_path, data: { turbo_method: :delete, action: "click->mobile-sidebar#closeSidebar" }, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-red-600 hover:text-red-700 hover:bg-red-50" do %> <%= link_to signout_path, data: { turbo_method: :delete, action: "click->mobile-sidebar#closeSidebar" }, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300 hover:bg-red-50 dark:hover:bg-red-900/20" do %>
<svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"> <svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" /> <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
</svg> </svg>

View File

@@ -1,42 +1,42 @@
<div class="max-w-2xl mx-auto" data-controller="backup-codes" data-backup-codes-codes-value="<%= @backup_codes.to_json %>"> <div class="max-w-2xl mx-auto" data-controller="backup-codes" data-backup-codes-codes-value="<%= @backup_codes.to_json %>">
<div class="mb-8"> <div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">Backup Codes</h1> <h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">Backup Codes</h1>
<p class="mt-2 text-sm text-gray-600"> <p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Save these backup codes in a safe place. Each code can only be used once. Save these backup codes in a safe place. Each code can only be used once.
</p> </p>
</div> </div>
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<div class="rounded-md bg-yellow-50 p-4 mb-6"> <div class="rounded-md bg-yellow-50 dark:bg-yellow-900/30 p-4 mb-6">
<div class="flex"> <div class="flex">
<svg class="h-5 w-5 text-yellow-400 mr-3 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5 text-yellow-400 mr-3 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
</svg> </svg>
<div class="text-sm text-yellow-800"> <div class="text-sm text-yellow-800 dark:text-yellow-200">
<p class="font-medium">Save these codes now!</p> <p class="font-medium">Save these codes now!</p>
<p class="mt-1">Store them somewhere safe. You won't be able to see them again without re-entering your password.</p> <p class="mt-1">Store them somewhere safe. You won't be able to see them again without re-entering your password.</p>
</div> </div>
</div> </div>
</div> </div>
<div class="grid grid-cols-2 gap-4 p-6 bg-gray-50 rounded-lg font-mono"> <div class="grid grid-cols-2 gap-4 p-6 bg-gray-50 dark:bg-gray-700 rounded-lg font-mono">
<% @backup_codes.each do |code| %> <% @backup_codes.each do |code| %>
<div class="text-center text-lg tracking-wider py-2 px-4 bg-white rounded border border-gray-200"> <div class="text-center text-lg tracking-wider py-2 px-4 bg-white dark:bg-gray-800 rounded border border-gray-200 dark:border-gray-700 dark:text-gray-100">
<%= code %> <%= code %>
</div> </div>
<% end %> <% end %>
</div> </div>
<div class="mt-6 flex gap-3"> <div class="mt-6 flex gap-3">
<button data-action="click->backup-codes#download" class="inline-flex items-center rounded-md border border-gray-300 bg-white py-2 px-4 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"> <button data-action="click->backup-codes#download" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 py-2 px-4 text-sm font-medium text-gray-700 dark:text-gray-200 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900">
<svg class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg> </svg>
Download Codes Download Codes
</button> </button>
<button data-action="click->backup-codes#print" class="inline-flex items-center rounded-md border border-gray-300 bg-white py-2 px-4 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"> <button data-action="click->backup-codes#print" class="inline-flex items-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 py-2 px-4 text-sm font-medium text-gray-700 dark:text-gray-200 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900">
<svg class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
</svg> </svg>
@@ -47,13 +47,12 @@
<div class="mt-8"> <div class="mt-8">
<% if @auto_signin_pending %> <% if @auto_signin_pending %>
<%= button_to "Continue to Sign In", complete_totp_setup_path, method: :post, <%= button_to "Continue to Sign In", complete_totp_setup_path, method: :post,
class: "inline-flex justify-center rounded-md border border-transparent bg-blue-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" %> class: "inline-flex justify-center rounded-md border border-transparent bg-blue-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900" %>
<% else %> <% else %>
<%= link_to "Done", profile_path, <%= link_to "Done", profile_path,
class: "inline-flex justify-center rounded-md border border-transparent bg-blue-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" %> class: "inline-flex justify-center rounded-md border border-transparent bg-blue-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900" %>
<% end %> <% end %>
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,17 +1,17 @@
<div class="max-w-2xl mx-auto"> <div class="max-w-2xl mx-auto">
<div class="mb-8"> <div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">Enable Two-Factor Authentication</h1> <h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">Enable Two-Factor Authentication</h1>
<p class="mt-2 text-sm text-gray-600"> <p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Scan the QR code below with your authenticator app, then enter the verification code to confirm. Scan the QR code below with your authenticator app, then enter the verification code to confirm.
</p> </p>
</div> </div>
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<!-- Step 1: Scan QR Code --> <!-- Step 1: Scan QR Code -->
<div class="mb-8"> <div class="mb-8">
<h3 class="text-lg font-medium text-gray-900 mb-4">Step 1: Scan QR Code</h3> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">Step 1: Scan QR Code</h3>
<div class="flex justify-center p-6 bg-gray-50 rounded-lg"> <div class="flex justify-center p-6 bg-gray-50 dark:bg-gray-700 rounded-lg">
<%= @qr_code.as_svg( <%= @qr_code.as_svg(
module_size: 4, module_size: 4,
color: "000", color: "000",
@@ -19,26 +19,26 @@
standalone: true standalone: true
).html_safe %> ).html_safe %>
</div> </div>
<p class="mt-4 text-sm text-gray-600 text-center"> <p class="mt-4 text-sm text-gray-600 dark:text-gray-400 text-center">
Use an authenticator app like Google Authenticator, Authy, or 1Password to scan this code. Use an authenticator app like Google Authenticator, Authy, or 1Password to scan this code.
</p> </p>
</div> </div>
<!-- Manual Entry Option --> <!-- Manual Entry Option -->
<div class="mb-8 p-4 bg-blue-50 rounded-lg"> <div class="mb-8 p-4 bg-blue-50 dark:bg-blue-900/30 rounded-lg">
<p class="text-sm font-medium text-blue-900 mb-2">Can't scan the QR code?</p> <p class="text-sm font-medium text-blue-900 dark:text-blue-200 mb-2">Can't scan the QR code?</p>
<p class="text-sm text-blue-800">Enter this key manually in your authenticator app:</p> <p class="text-sm text-blue-800 dark:text-blue-300">Enter this key manually in your authenticator app:</p>
<code class="mt-2 block p-2 bg-white rounded text-sm font-mono break-all"><%= @totp_secret %></code> <code class="mt-2 block p-2 bg-white dark:bg-gray-700 dark:text-gray-200 rounded text-sm font-mono break-all"><%= @totp_secret %></code>
</div> </div>
<!-- Step 2: Verify --> <!-- Step 2: Verify -->
<div> <div>
<h3 class="text-lg font-medium text-gray-900 mb-4">Step 2: Verify</h3> <h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">Step 2: Verify</h3>
<%= form_with url: totp_path, method: :post, class: "space-y-4" do |form| %> <%= form_with url: totp_path, method: :post, class: "space-y-4" do |form| %>
<%= hidden_field_tag :totp_secret, @totp_secret %> <%= hidden_field_tag :totp_secret, @totp_secret %>
<div> <div>
<%= label_tag :code, "Verification Code", class: "block text-sm font-medium text-gray-700" %> <%= label_tag :code, "Verification Code", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<%= text_field_tag :code, <%= text_field_tag :code,
nil, nil,
placeholder: "000000", placeholder: "000000",
@@ -46,27 +46,27 @@
required: true, required: true,
autofocus: true, autofocus: true,
autocomplete: "off", autocomplete: "off",
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-center text-2xl tracking-widest font-mono" %> class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-center text-2xl tracking-widest font-mono" %>
<p class="mt-1 text-sm text-gray-500">Enter the 6-digit code from your authenticator app</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Enter the 6-digit code from your authenticator app</p>
</div> </div>
<div class="flex gap-3"> <div class="flex gap-3">
<%= form.submit "Verify and Enable 2FA", <%= form.submit "Verify and Enable 2FA",
class: "inline-flex justify-center rounded-md border border-transparent bg-blue-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" %> class: "inline-flex justify-center rounded-md border border-transparent bg-blue-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900" %>
<%= link_to "Cancel", profile_path, <%= link_to "Cancel", profile_path,
class: "inline-flex justify-center rounded-md border border-gray-300 bg-white py-2 px-4 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" %> class: "inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 py-2 px-4 text-sm font-medium text-gray-700 dark:text-gray-200 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900" %>
</div> </div>
<% end %> <% end %>
</div> </div>
</div> </div>
</div> </div>
<div class="mt-6 p-4 bg-yellow-50 rounded-lg"> <div class="mt-6 p-4 bg-yellow-50 dark:bg-yellow-900/30 rounded-lg">
<div class="flex"> <div class="flex">
<svg class="h-5 w-5 text-yellow-400 mr-3 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5 text-yellow-400 mr-3 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
</svg> </svg>
<div class="text-sm text-yellow-800"> <div class="text-sm text-yellow-800 dark:text-yellow-200">
<p class="font-medium">Important: Save your backup codes</p> <p class="font-medium">Important: Save your backup codes</p>
<p class="mt-1">After verifying, you'll be shown backup codes. Save these in a safe place - they can be used to access your account if you lose your device.</p> <p class="mt-1">After verifying, you'll be shown backup codes. Save these in a safe place - they can be used to access your account if you lose your device.</p>
</div> </div>

View File

@@ -1,19 +1,19 @@
<div class="max-w-2xl mx-auto"> <div class="max-w-2xl mx-auto">
<div class="mb-8"> <div class="mb-8">
<h1 class="text-3xl font-bold text-gray-900">Regenerate Backup Codes</h1> <h1 class="text-3xl font-bold text-gray-900 dark:text-gray-100">Regenerate Backup Codes</h1>
<p class="mt-2 text-sm text-gray-600"> <p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
This will invalidate all existing backup codes and generate new ones. This will invalidate all existing backup codes and generate new ones.
</p> </p>
</div> </div>
<div class="bg-white shadow sm:rounded-lg"> <div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6"> <div class="px-4 py-5 sm:p-6">
<div class="rounded-md bg-yellow-50 p-4 mb-6"> <div class="rounded-md bg-yellow-50 dark:bg-yellow-900/30 p-4 mb-6">
<div class="flex"> <div class="flex">
<svg class="h-5 w-5 text-yellow-400 mr-3 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-5 w-5 text-yellow-400 mr-3 flex-shrink-0" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z" clip-rule="evenodd" />
</svg> </svg>
<div class="text-sm text-yellow-800"> <div class="text-sm text-yellow-800 dark:text-yellow-200">
<p class="font-medium">Important Security Notice</p> <p class="font-medium">Important Security Notice</p>
<p class="mt-1">All your current backup codes will become invalid after this action. Make sure you're ready to save the new codes.</p> <p class="mt-1">All your current backup codes will become invalid after this action. Make sure you're ready to save the new codes.</p>
</div> </div>
@@ -22,22 +22,22 @@
<%= form_with(url: create_new_backup_codes_totp_path, method: :post, class: "space-y-6") do |form| %> <%= form_with(url: create_new_backup_codes_totp_path, method: :post, class: "space-y-6") do |form| %>
<div> <div>
<%= form.label :password, "Enter your password to confirm", class: "block text-sm font-medium text-gray-700" %> <%= form.label :password, "Enter your password to confirm", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
<div class="mt-1"> <div class="mt-1">
<%= form.password_field :password, required: true, <%= form.password_field :password, required: true,
class: "block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 placeholder-gray-400 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 sm:text-sm" %> class: "block w-full appearance-none rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 px-3 py-2 placeholder-gray-400 dark:placeholder-gray-500 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 sm:text-sm" %>
</div> </div>
<p class="mt-2 text-sm text-gray-500"> <p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
This is required to verify your identity before regenerating backup codes. This is required to verify your identity before regenerating backup codes.
</p> </p>
</div> </div>
<div class="flex gap-3"> <div class="flex gap-3">
<%= form.submit "Generate New Backup Codes", <%= form.submit "Generate New Backup Codes",
class: "inline-flex justify-center rounded-md border border-transparent bg-blue-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" %> class: "inline-flex justify-center rounded-md border border-transparent bg-blue-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900" %>
<%= link_to "Cancel", profile_path, <%= link_to "Cancel", profile_path,
class: "inline-flex justify-center rounded-md border border-gray-300 bg-white py-2 px-4 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" %> class: "inline-flex justify-center rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 py-2 px-4 text-sm font-medium text-gray-700 dark:text-gray-200 shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-900" %>
</div> </div>
<% end %> <% end %>
</div> </div>

View File

@@ -1,41 +1,41 @@
<div class="mx-auto md:w-2/3 w-full"> <div class="mx-auto md:w-2/3 w-full">
<div class="mb-8"> <div class="mb-8">
<h1 class="font-bold text-4xl">Welcome to Clinch</h1> <h1 class="font-bold text-4xl">Welcome to Clinch</h1>
<p class="mt-2 text-gray-600">Create your admin account to get started</p> <p class="mt-2 text-gray-600 dark:text-gray-400">Create your admin account to get started</p>
</div> </div>
<%= form_with model: @user, url: signup_path, class: "contents", data: { controller: "form-errors" } do |form| %> <%= form_with model: @user, url: signup_path, class: "contents", data: { controller: "form-errors" } do |form| %>
<%= render "shared/form_errors", form: form %> <%= render "shared/form_errors", form: form %>
<div class="my-5"> <div class="my-5">
<%= form.label :email_address, class: "block font-medium text-sm text-gray-700" %> <%= form.label :email_address, class: "block font-medium text-sm text-gray-700 dark:text-gray-300" %>
<%= form.email_field :email_address, <%= form.email_field :email_address,
required: true, required: true,
autofocus: true, autofocus: true,
autocomplete: "email", autocomplete: "email",
placeholder: "admin@example.com", placeholder: "admin@example.com",
class: "block shadow-sm rounded-md border border-gray-400 focus:outline-blue-600 px-3 py-2 mt-2 w-full" %> class: "block shadow-sm rounded-md border border-gray-400 focus:outline-blue-600 px-3 py-2 mt-2 w-full dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100" %>
</div> </div>
<div class="my-5"> <div class="my-5">
<%= form.label :password, class: "block font-medium text-sm text-gray-700" %> <%= form.label :password, class: "block font-medium text-sm text-gray-700 dark:text-gray-300" %>
<%= form.password_field :password, <%= form.password_field :password,
required: true, required: true,
autocomplete: "new-password", autocomplete: "new-password",
placeholder: "Enter a strong password", placeholder: "Enter a strong password",
maxlength: 72, maxlength: 72,
class: "block shadow-sm rounded-md border border-gray-400 focus:outline-blue-600 px-3 py-2 mt-2 w-full" %> class: "block shadow-sm rounded-md border border-gray-400 focus:outline-blue-600 px-3 py-2 mt-2 w-full dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100" %>
<p class="mt-1 text-sm text-gray-500">Must be at least 8 characters</p> <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Must be at least 8 characters</p>
</div> </div>
<div class="my-5"> <div class="my-5">
<%= form.label :password_confirmation, "Confirm Password", class: "block font-medium text-sm text-gray-700" %> <%= form.label :password_confirmation, "Confirm Password", class: "block font-medium text-sm text-gray-700 dark:text-gray-300" %>
<%= form.password_field :password_confirmation, <%= form.password_field :password_confirmation,
required: true, required: true,
autocomplete: "new-password", autocomplete: "new-password",
placeholder: "Re-enter your password", placeholder: "Re-enter your password",
maxlength: 72, maxlength: 72,
class: "block shadow-sm rounded-md border border-gray-400 focus:outline-blue-600 px-3 py-2 mt-2 w-full" %> class: "block shadow-sm rounded-md border border-gray-400 focus:outline-blue-600 px-3 py-2 mt-2 w-full dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100" %>
</div> </div>
<div class="my-5"> <div class="my-5">
@@ -43,8 +43,8 @@
class: "w-full rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white font-medium cursor-pointer" %> class: "w-full rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white font-medium cursor-pointer" %>
</div> </div>
<div class="mt-4 p-4 bg-blue-50 rounded-lg"> <div class="mt-4 p-4 bg-blue-50 rounded-lg dark:bg-blue-900/30">
<p class="text-sm text-blue-900"> <p class="text-sm text-blue-900 dark:text-blue-200">
<strong>Note:</strong> This is a first-run setup. You're creating the initial administrator account. <strong>Note:</strong> This is a first-run setup. You're creating the initial administrator account.
After this, you'll be able to invite other users from the admin dashboard. After this, you'll be able to invite other users from the admin dashboard.
</p> </p>