From 3d98261a51ea227a5b6db44320fb385c266d882f Mon Sep 17 00:00:00 2001 From: Dan Milne Date: Sun, 22 Mar 2026 00:37:58 +1100 Subject: [PATCH] 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) --- app/assets/tailwind/application.css | 20 +++ app/helpers/application_helper.rb | 10 +- .../controllers/dark_mode_controller.js | 27 +++ app/views/active_sessions/show.html.erb | 52 +++--- app/views/admin/applications/edit.html.erb | 4 +- app/views/admin/applications/index.html.erb | 48 +++--- app/views/admin/applications/new.html.erb | 2 +- app/views/admin/applications/show.html.erb | 160 +++++++++--------- app/views/admin/dashboard/index.html.erb | 74 ++++---- app/views/admin/groups/_form.html.erb | 36 ++-- app/views/admin/groups/edit.html.erb | 4 +- app/views/admin/groups/index.html.erb | 26 +-- app/views/admin/groups/new.html.erb | 2 +- app/views/admin/groups/show.html.erb | 44 ++--- .../admin/users/_application_claims.html.erb | 86 +++++----- app/views/admin/users/_form.html.erb | 52 +++--- app/views/admin/users/edit.html.erb | 4 +- app/views/admin/users/index.html.erb | 50 +++--- app/views/admin/users/new.html.erb | 2 +- app/views/api_keys/index.html.erb | 50 +++--- app/views/api_keys/new.html.erb | 30 ++-- app/views/api_keys/show.html.erb | 26 +-- app/views/dashboard/index.html.erb | 98 ++++++----- app/views/invitations/show.html.erb | 8 +- app/views/layouts/application.html.erb | 25 ++- app/views/oidc/consent.html.erb | 30 ++-- app/views/passwords/edit.html.erb | 4 +- app/views/passwords/new.html.erb | 2 +- app/views/profiles/show.html.erb | 134 +++++++-------- app/views/sessions/new.html.erb | 20 ++- app/views/sessions/verify_totp.html.erb | 26 +-- app/views/shared/_flash.html.erb | 32 ++-- app/views/shared/_form_errors.html.erb | 12 +- app/views/shared/_sidebar.html.erb | 79 ++++++--- app/views/totp/backup_codes.html.erb | 23 ++- app/views/totp/new.html.erb | 36 ++-- .../totp/regenerate_backup_codes.html.erb | 22 +-- app/views/users/new.html.erb | 20 +-- 38 files changed, 744 insertions(+), 636 deletions(-) create mode 100644 app/javascript/controllers/dark_mode_controller.js diff --git a/app/assets/tailwind/application.css b/app/assets/tailwind/application.css index 4b1a3f8..65792d6 100644 --- a/app/assets/tailwind/application.css +++ b/app/assets/tailwind/application.css @@ -1,3 +1,23 @@ @import "tailwindcss"; @plugin "@tailwindcss/forms"; @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); + } +} diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index f28c936..ffc1ea4 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -22,11 +22,11 @@ module ApplicationHelper def border_class_for(type) case type.to_s - when "notice" then "border-green-200" - when "alert", "error" then "border-red-200" - when "warning" then "border-yellow-200" - when "info" then "border-blue-200" - else "border-gray-200" + when "notice" then "border-green-200 dark:border-green-700" + when "alert", "error" then "border-red-200 dark:border-red-700" + when "warning" then "border-yellow-200 dark:border-yellow-700" + when "info" then "border-blue-200 dark:border-blue-700" + else "border-gray-200 dark:border-gray-700" end end end diff --git a/app/javascript/controllers/dark_mode_controller.js b/app/javascript/controllers/dark_mode_controller.js new file mode 100644 index 0000000..4c11033 --- /dev/null +++ b/app/javascript/controllers/dark_mode_controller.js @@ -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) + } + }) + } +} diff --git a/app/views/active_sessions/show.html.erb b/app/views/active_sessions/show.html.erb index a66ee49..0eb0cb6 100644 --- a/app/views/active_sessions/show.html.erb +++ b/app/views/active_sessions/show.html.erb @@ -1,50 +1,50 @@
-

Sessions

-

Manage your active sessions and connected applications.

+

Sessions

+

Manage your active sessions and connected applications.

-
+
-

Connected Applications

-
+

Connected Applications

+

These applications have access to your account. You can revoke access at any time.

<% if @connected_applications.any? %> -
    +
      <% @connected_applications.each do |consent| %>
    • -

      +

      <%= consent.application.name %>

      -

      +

      Access to: <%= consent.formatted_scopes %>

      -

      +

      Authorized <%= time_ago_in_words(consent.granted_at) %> ago

      <%= 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." } } %>
    • <% end %>
    <% else %> -

    No connected applications.

    +

    No connected applications.

    <% end %> <% if @connected_applications.any? %> -
    +
    <%= 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?" } } %>
    @@ -55,37 +55,37 @@
    -
    +
    -

    Active Sessions

    -
    +

    Active Sessions

    +

    These devices are currently signed in to your account. Revoke any sessions that you don't recognize.

    <% if @active_sessions.any? %> -
      +
        <% @active_sessions.each do |session| %>
      • -

        +

        <%= session.device_name || "Unknown Device" %> <% if session.id == Current.session.id %> - + This device <% end %>

        -

        +

        <%= session.ip_address %>

        -

        +

        Last active <%= time_ago_in_words(session.last_activity_at || session.updated_at) %> ago

        <% if session.id != Current.session.id %> <%= 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?" } } %> <% end %>
        @@ -93,15 +93,15 @@ <% end %>
      <% else %> -

      No other active sessions.

      +

      No other active sessions.

      <% end %> <% if @active_sessions.count > 1 %> -
      +
      <%= 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?" } } %>
      @@ -111,4 +111,4 @@
      -
    \ No newline at end of file +
    diff --git a/app/views/admin/applications/edit.html.erb b/app/views/admin/applications/edit.html.erb index 2bf0e1b..1bee2f1 100644 --- a/app/views/admin/applications/edit.html.erb +++ b/app/views/admin/applications/edit.html.erb @@ -1,5 +1,5 @@
    -

    Edit Application

    -

    Editing: <%= @application.name %>

    +

    Edit Application

    +

    Editing: <%= @application.name %>

    <%= render "form", application: @application %>
    diff --git a/app/views/admin/applications/index.html.erb b/app/views/admin/applications/index.html.erb index 8d7b650..cbb62b2 100644 --- a/app/views/admin/applications/index.html.erb +++ b/app/views/admin/applications/index.html.erb @@ -1,7 +1,7 @@
    -

    Applications

    -

    Manage OIDC Clients.

    +

    Applications

    +

    Manage OIDC Clients.

    <%= 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 @@
    - +
    - - - - - + + + + + - + <% @applications.each do |application| %> - - - - -
    ApplicationSlugTypeStatusGroupsApplicationSlugTypeStatusGroups Actions
    +
    <% 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 %> -
    - +
    +
    @@ -41,29 +41,29 @@ <%= link_to application.name, admin_application_path(application), class: "text-blue-600 hover:text-blue-900" %>
    - <%= application.slug %> + + <%= application.slug %> + <% case application.app_type %> <% when "oidc" %> - OIDC + OIDC <% when "forward_auth" %> - Forward Auth + Forward Auth <% when "saml" %> - SAML + SAML <% end %> + <% if application.active? %> - Active + Active <% else %> - Inactive + Inactive <% end %> + <% if application.allowed_groups.empty? %> - All users + All users <% else %> <%= application.allowed_groups.count %> <% end %> diff --git a/app/views/admin/applications/new.html.erb b/app/views/admin/applications/new.html.erb index 5b402bb..879894b 100644 --- a/app/views/admin/applications/new.html.erb +++ b/app/views/admin/applications/new.html.erb @@ -1,4 +1,4 @@
    -

    New Application

    +

    New Application

    <%= render "form", application: @application %>
    diff --git a/app/views/admin/applications/show.html.erb b/app/views/admin/applications/show.html.erb index d22f231..e5681f7 100644 --- a/app/views/admin/applications/show.html.erb +++ b/app/views/admin/applications/show.html.erb @@ -1,27 +1,27 @@
    <% if flash[:client_id] %> -
    -

    🔐 OIDC Client Credentials

    +
    +

    🔐 OIDC Client Credentials

    <% if flash[:public_client] %> -

    This is a public client. Copy the client ID below.

    +

    This is a public client. Copy the client ID below.

    <% else %> -

    Copy these credentials now. The client secret will not be shown again.

    +

    Copy these credentials now. The client secret will not be shown again.

    <% end %>
    - Client ID: + Client ID:
    - <%= flash[:client_id] %> + <%= flash[:client_id] %> <% if flash[:client_secret] %>
    - Client Secret: + Client Secret:
    - <%= flash[:client_secret] %> + <%= flash[:client_secret] %> <% elsif flash[:public_client] %>
    - Client Secret: + Client Secret:
    -
    +
    Public clients do not have a client secret. PKCE is required.
    <% end %> @@ -32,21 +32,21 @@
    <% 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 %> -
    - +
    +
    <% end %>
    -

    <%= @application.name %>

    -

    <%= @application.description %>

    +

    <%= @application.name %>

    +

    <%= @application.description %>

    - <%= 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" %>
    @@ -54,42 +54,42 @@
    -
    +
    -

    Basic Information

    +

    Basic Information

    -
    Slug
    -
    <%= @application.slug %>
    +
    Slug
    +
    <%= @application.slug %>
    -
    Type
    -
    +
    Type
    +
    <% case @application.app_type %> <% when "oidc" %> - OIDC + OIDC <% when "forward_auth" %> - Forward Auth + Forward Auth <% end %>
    -
    Status
    -
    +
    Status
    +
    <% if @application.active? %> - Active + Active <% else %> - Inactive + Inactive <% end %>
    -
    Landing URL
    -
    +
    Landing URL
    +
    <% 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" %> <% else %> - Not configured + Not configured <% end %>
    @@ -99,59 +99,59 @@ <% if @application.oidc? %> -
    +
    -

    OIDC Configuration

    +

    OIDC Configuration

    <%= 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" %>
    -
    Client Type
    -
    +
    Client Type
    +
    <% if @application.public_client? %> - Public + Public <% else %> - Confidential + Confidential <% end %>
    -
    PKCE
    -
    +
    PKCE
    +
    <% if @application.requires_pkce? %> - Required + Required <% else %> - Optional + Optional <% end %>
    <% unless flash[:client_id] %>
    -
    Client ID
    -
    - <%= @application.client_id %> +
    Client ID
    +
    + <%= @application.client_id %>
    <% if @application.confidential_client? %>
    -
    Client Secret
    -
    -
    +
    Client Secret
    +
    +
    🔒 Client secret is stored securely and cannot be displayed
    -

    +

    To get a new client secret, use the "Regenerate Credentials" button above.

    <% else %>
    -
    Client Secret
    -
    -
    +
    Client Secret
    +
    +
    Public clients do not use a client secret. PKCE is required for authorization.
    @@ -159,33 +159,33 @@ <% end %> <% end %>
    -
    Redirect URIs
    -
    +
    Redirect URIs
    +
    <% if @application.redirect_uris.present? %> <% @application.parsed_redirect_uris.each do |uri| %> - <%= uri %> + <%= uri %> <% end %> <% else %> - No redirect URIs configured + No redirect URIs configured <% end %>
    -
    +
    Backchannel Logout URI <% if @application.supports_backchannel_logout? %> - Enabled + Enabled <% end %>
    -
    +
    <% if @application.backchannel_logout_uri.present? %> - <%= @application.backchannel_logout_uri %> -

    + <%= @application.backchannel_logout_uri %> +

    When users log out, Clinch will send logout notifications to this endpoint for immediate session termination.

    <% else %> - Not configured -

    + Not configured +

    Backchannel logout is optional. Configure it if the application supports OpenID Connect Backchannel Logout.

    <% end %> @@ -198,23 +198,23 @@ <% if @application.forward_auth? %> -
    +
    -

    Forward Auth Configuration

    +

    Forward Auth Configuration

    -
    Domain Pattern
    -
    - <%= @application.domain_pattern %> +
    Domain Pattern
    +
    + <%= @application.domain_pattern %>
    -
    Headers Configuration
    -
    +
    Headers Configuration
    +
    <% if @application.headers_config.present? && @application.headers_config.any? %> - <%= JSON.pretty_generate(@application.headers_config) %> + <%= JSON.pretty_generate(@application.headers_config) %> <% else %> -
    +
    Using default headers: X-Remote-User, X-Remote-Email, X-Remote-Name, X-Remote-Username, X-Remote-Groups, X-Remote-Admin
    <% end %> @@ -226,29 +226,29 @@ <% end %> -
    +
    -

    Access Control

    +

    Access Control

    -
    Allowed Groups
    -
    +
    Allowed Groups
    +
    <% if @allowed_groups.empty? %> -
    +
    -

    +

    No groups assigned - all active users can access this application.

    <% else %> -
      +
        <% @allowed_groups.each do |group| %>
      • -

        <%= group.name %>

        -

        <%= pluralize(group.users.count, "member") %>

        +

        <%= group.name %>

        +

        <%= pluralize(group.users.count, "member") %>

      • <% end %> diff --git a/app/views/admin/dashboard/index.html.erb b/app/views/admin/dashboard/index.html.erb index d664852..596456e 100644 --- a/app/views/admin/dashboard/index.html.erb +++ b/app/views/admin/dashboard/index.html.erb @@ -1,28 +1,28 @@
        -

        Admin Dashboard

        -

        System overview and quick actions

        +

        Admin Dashboard

        +

        System overview and quick actions

        -
        +
        - +
        -
        +
        Total Users
        -
        +
        <%= @user_count %>
        -
        +
        (<%= @active_user_count %> active)
        @@ -30,30 +30,30 @@
        -
        +
        <%= link_to "Manage users", admin_users_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
        -
        +
        - +
        -
        +
        Applications
        -
        +
        <%= @application_count %>
        -
        +
        (<%= @active_application_count %> active)
        @@ -61,33 +61,33 @@
        -
        +
        <%= link_to "Manage applications", admin_applications_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
        -
        +
        - +
        -
        +
        Groups
        -
        +
        <%= @group_count %>
        -
        +
        <%= link_to "Manage groups", admin_groups_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
        @@ -95,26 +95,26 @@
        -

        Recent Users

        -
        -
          +

          Recent Users

          +
          +
            <% @recent_users.each do |user| %>
          • -

            <%= user.email_address %>

            -

            +

            <%= user.email_address %>

            +

            Created <%= time_ago_in_words(user.created_at) %> ago

            <% if user.admin? %> - Admin + Admin <% end %> <% if user.totp_enabled? %> - 2FA + 2FA <% end %> - <%= user.status.titleize %> + <%= user.status.titleize %>
          • @@ -125,21 +125,21 @@
            -

            Quick Actions

            +

            Quick Actions

            - <%= 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 %> -

            Create User

            -

            Add a new user to the system

            + <%= 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 %> +

            Create User

            +

            Add a new user to the system

            <% 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 %> -

            Register Application

            -

            Add a new OIDC or ForwardAuth app

            + <%= 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 %> +

            Register Application

            +

            Add a new OIDC or ForwardAuth app

            <% 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 %> -

            Create Group

            -

            Organize users into a new group

            + <%= 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 %> +

            Create Group

            +

            Organize users into a new group

            <% end %>
            diff --git a/app/views/admin/groups/_form.html.erb b/app/views/admin/groups/_form.html.erb index ba64adb..cf2b371 100644 --- a/app/views/admin/groups/_form.html.erb +++ b/app/views/admin/groups/_form.html.erb @@ -2,51 +2,51 @@ <%= render "shared/form_errors", form: form %>
            - <%= form.label :name, class: "block text-sm font-medium text-gray-700" %> - <%= form.text_field :name, required: true, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "developers" %> -

            Group names are automatically normalized to lowercase.

            + <%= 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 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" %> +

            Group names are automatically normalized to lowercase.

            - <%= form.label :description, class: "block text-sm font-medium text-gray-700" %> - <%= form.text_area :description, rows: 3, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "Optional description of this group" %> + <%= 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 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" %>
            - <%= 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" %> +
            <% if @available_users.any? %> <% @available_users.each do |user| %>
            - <%= check_box_tag "group[user_ids][]", user.id, group.users.include?(user), class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" %> - <%= label_tag "group_user_ids_#{user.id}", user.email_address, class: "ml-2 text-sm text-gray-900" %> + <%= 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 dark:text-gray-100" %> <% if user.admin? %> - Admin + Admin <% end %>
            <% end %> <% else %> -

            No users available.

            +

            No users available.

            <% end %>
            -

            Select which users should be members of this group.

            +

            Select which users should be members of this group.

            - <%= 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, - 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"]}', data: { action: "input->json-validator#validate blur->json-validator#format", json_validator_target: "textarea" } %> -
            +

            Optional: Custom claims to add to OIDC tokens for all members. These will be merged with user-level claims.

            - - + +
            @@ -55,6 +55,6 @@
            <%= 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" %>
            <% end %> diff --git a/app/views/admin/groups/edit.html.erb b/app/views/admin/groups/edit.html.erb index ba04d03..b7a25a1 100644 --- a/app/views/admin/groups/edit.html.erb +++ b/app/views/admin/groups/edit.html.erb @@ -1,5 +1,5 @@
            -

            Edit Group

            -

            Editing: <%= @group.name %>

            +

            Edit Group

            +

            Editing: <%= @group.name %>

            <%= render "form", group: @group %>
            diff --git a/app/views/admin/groups/index.html.erb b/app/views/admin/groups/index.html.erb index f6cb3a7..22477e3 100644 --- a/app/views/admin/groups/index.html.erb +++ b/app/views/admin/groups/index.html.erb @@ -1,7 +1,7 @@
            -

            Groups

            -

            Organize users into groups for application access control.

            +

            Groups

            +

            Organize users into groups for application access control.

            <%= 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 @@
            - +
            - - - - + + + + - + <% @groups.each do |group| %> - - - -
            NameDescriptionMembersApplicationsNameDescriptionMembersApplications Actions
            + <%= link_to group.name, admin_group_path(group), class: "text-blue-600 hover:text-blue-900" %> - <%= 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") %> + <%= pluralize(group.users.count, "member") %> + <%= pluralize(group.applications.count, "app") %> diff --git a/app/views/admin/groups/new.html.erb b/app/views/admin/groups/new.html.erb index ebc8a7f..12b8d02 100644 --- a/app/views/admin/groups/new.html.erb +++ b/app/views/admin/groups/new.html.erb @@ -1,4 +1,4 @@
            -

            New Group

            +

            New Group

            <%= render "form", group: @group %>
            diff --git a/app/views/admin/groups/show.html.erb b/app/views/admin/groups/show.html.erb index 4f34ad2..e1c8d3e 100644 --- a/app/views/admin/groups/show.html.erb +++ b/app/views/admin/groups/show.html.erb @@ -1,13 +1,13 @@
            -

            <%= @group.name %>

            +

            <%= @group.name %>

            <% if @group.description.present? %> -

            <%= @group.description %>

            +

            <%= @group.description %>

            <% end %>
            - <%= 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" %>
            @@ -15,25 +15,25 @@
            -
            +
            -

            +

            Members (<%= @members.count %>)

            <% if @members.any? %> -
              +
                <% @members.each do |user| %>
              • -

                <%= user.email_address %>

                +

                <%= user.email_address %>

                <% if user.admin? %> - Admin + Admin <% end %> <% if user.totp_enabled? %> - 2FA + 2FA <% end %> - <%= user.status.titleize %> + <%= user.status.titleize %>
                <%= link_to "View", admin_user_path(user), class: "text-blue-600 hover:text-blue-900 text-sm" %> @@ -41,36 +41,36 @@ <% end %>
              <% else %> -
              -

              No members in this group yet.

              +
              +

              No members in this group yet.

              <% end %>
            -
            +
            -

            +

            Assigned Applications (<%= @applications.count %>)

            <% if @applications.any? %> -
              +
                <% @applications.each do |app| %>
              • -

                <%= app.name %>

                +

                <%= app.name %>

                <% case app.app_type %> <% when "oidc" %> - OIDC + OIDC <% when "trusted_header" %> - ForwardAuth + ForwardAuth <% end %> <% if app.active? %> - Active + Active <% else %> - Inactive + Inactive <% end %>
                @@ -79,8 +79,8 @@ <% end %>
              <% else %> -
              -

              This group is not assigned to any applications.

              +
              +

              This group is not assigned to any applications.

              <% end %>
              diff --git a/app/views/admin/users/_application_claims.html.erb b/app/views/admin/users/_application_claims.html.erb index f477ee0..c88524b 100644 --- a/app/views/admin/users/_application_claims.html.erb +++ b/app/views/admin/users/_application_claims.html.erb @@ -3,29 +3,29 @@ <% if oidc_apps.any? %> -
              -

              OIDC App-Specific Claims

              -

              +

              +

              OIDC App-Specific Claims

              +

              Configure custom claims that apply only to specific OIDC applications. These override both group and user global claims and are included in ID tokens.

              <% oidc_apps.each do |app| %> <% app_claim = user.application_user_claims.find_by(application: app) %> -
              > - +
              > +
              - <%= app.name %> - + <%= app.name %> + OIDC <% if app_claim&.custom_claims&.any? %> - + <%= app_claim.custom_claims.keys.count %> claim(s) <% end %>
              - +
              @@ -35,22 +35,22 @@ <%= hidden_field_tag :application_id, app.id %>
              - + <%= text_area_tag :custom_claims, (app_claim&.custom_claims.present? ? JSON.pretty_generate(app_claim.custom_claims) : ""), 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"}', data: { action: "input->json-validator#validate blur->json-validator#format", json_validator_target: "textarea" } %>
              -

              +

              Example for <%= app.name %>: Add claims that this app specifically needs to read.

              - Note: Do not use reserved claim names (groups, email, name, etc.). Use app-specific names like kavita_groups instead. + Note: Do not use reserved claim names (groups, email, name, etc.). Use app-specific names like kavita_groups instead.

              @@ -66,27 +66,27 @@ delete_application_claims_admin_user_path(user, application_id: app.id), method: :delete, 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 %> -
              -

              Preview: Final ID Token Claims for <%= app.name %>

              -
              -
              <%= JSON.pretty_generate(preview_user_claims(user, app)) %>
              +
              +

              Preview: Final ID Token Claims for <%= app.name %>

              +
              +
              <%= JSON.pretty_generate(preview_user_claims(user, app)) %>
              - Show claim sources + Show claim sources
              <% claim_sources(user, app).each do |source| %>
              - + <%= source[:name] %> - <%= source[:claims].to_json %> + <%= source[:claims].to_json %>
              <% end %>
              @@ -101,32 +101,32 @@ <% if forward_auth_apps.any? %> -
              -

              ForwardAuth Headers Preview

              -

              +

              +

              ForwardAuth Headers Preview

              +

              ForwardAuth applications receive HTTP headers (not OIDC tokens). Headers are based on user's email, name, groups, and admin status.

              <% forward_auth_apps.each do |app| %> -
              - +
              +
              - <%= app.name %> - + <%= app.name %> + FORWARD AUTH - + <%= app.domain_pattern %>
              - +
              -
              +
              @@ -135,33 +135,33 @@
              -

              Headers Sent to <%= app.name %>

              -
              +

              Headers Sent to <%= app.name %>

              +
              <% headers = app.headers_for_user(user) %> <% if headers.any? %>
              <% headers.each do |header_name, value| %>
              -
              <%= header_name %>:
              -
              <%= value %>
              +
              <%= header_name %>:
              +
              <%= value %>
              <% end %>
              <% else %> -

              All headers disabled for this application.

              +

              All headers disabled for this application.

              <% end %>
              -

              +

              These headers are configured in the application settings and sent by your reverse proxy (Caddy/Traefik) to the upstream application.

              <% if user.groups.any? %>
              -

              User's Groups

              +

              User's Groups

              <% user.groups.each do |group| %> - + <%= group.name %> <% end %> @@ -176,10 +176,10 @@ <% end %> <% if oidc_apps.empty? && forward_auth_apps.empty? %> -
              -
              -

              No active applications found.

              -

              Create applications in the Admin panel first.

              +
              +
              +

              No active applications found.

              +

              Create applications in the Admin panel first.

              <% end %> diff --git a/app/views/admin/users/_form.html.erb b/app/views/admin/users/_form.html.erb index 4cc066d..8fb7668 100644 --- a/app/views/admin/users/_form.html.erb +++ b/app/views/admin/users/_form.html.erb @@ -2,49 +2,49 @@ <%= render "shared/form_errors", form: form %>
              - <%= form.label :email_address, class: "block text-sm font-medium text-gray-700" %> - <%= form.email_field :email_address, required: true, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "user@example.com" %> + <%= 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 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" %>
              - <%= form.label :username, "Username (Optional)", class: "block text-sm font-medium text-gray-700" %> - <%= 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" %> -

              Optional: Short username/handle for login. Can only contain letters, numbers, underscores, and hyphens.

              + <%= 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 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" %> +

              Optional: Short username/handle for login. Can only contain letters, numbers, underscores, and hyphens.

              - <%= form.label :name, "Display Name (Optional)", class: "block text-sm font-medium text-gray-700" %> - <%= 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" %> -

              Optional: Full name shown in applications. Defaults to email address if not set.

              + <%= 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 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" %> +

              Optional: Full name shown in applications. Defaults to email address if not set.

              - <%= form.label :password, class: "block text-sm font-medium text-gray-700" %> - <%= form.password_field :password, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: user.persisted? ? "Leave blank to keep current password" : "Enter password" %> + <%= 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 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? %> -

              Leave blank to keep the current password

              +

              Leave blank to keep the current password

              <% else %> -

              Leave blank to generate a random password

              +

              Leave blank to generate a random password

              <% end %>
              - <%= form.label :status, class: "block text-sm font-medium text-gray-700" %> - <%= form.select :status, User.statuses.keys.map { |s| [s.titleize, s] }, {}, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %> + <%= 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 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" %>
              - <%= form.check_box :admin, class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500", disabled: (user == Current.session.user) %> - <%= form.label :admin, "Administrator", class: "ml-2 block text-sm text-gray-900" %> + <%= 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 dark:text-gray-100" %> <% if user == Current.session.user %> - (Cannot change your own admin status) + (Cannot change your own admin status) <% end %>
              - <%= form.check_box :totp_required, class: "h-4 w-4 rounded border-gray-300 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.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 dark:text-gray-100" %> <% if user.totp_required? && !user.totp_enabled? %> (User has not set up 2FA yet) <% end %> @@ -57,24 +57,24 @@ Warning: This user will be prompted to set up 2FA on their next login.

              <% end %> -

              When enabled, this user must use two-factor authentication to sign in.

              +

              When enabled, this user must use two-factor authentication to sign in.

              - <%= 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, - 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"}', data: { action: "input->json-validator#validate blur->json-validator#format", json_validator_target: "textarea" } %> -
              +

              Optional: User-specific custom claims to add to OIDC tokens. These override group-level claims.

              - - + +
              @@ -83,6 +83,6 @@
              <%= 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" %>
              <% end %> diff --git a/app/views/admin/users/edit.html.erb b/app/views/admin/users/edit.html.erb index 8c50c8e..965fad2 100644 --- a/app/views/admin/users/edit.html.erb +++ b/app/views/admin/users/edit.html.erb @@ -1,6 +1,6 @@
              -

              Edit User

              -

              Editing: <%= @user.email_address %>

              +

              Edit User

              +

              Editing: <%= @user.email_address %>

              <%= render "form", user: @user %> diff --git a/app/views/admin/users/index.html.erb b/app/views/admin/users/index.html.erb index 2c1f6d5..8d50a5a 100644 --- a/app/views/admin/users/index.html.erb +++ b/app/views/admin/users/index.html.erb @@ -1,7 +1,7 @@
              -

              Users

              -

              A list of all users in the system.

              +

              Users

              +

              A list of all users in the system.

              <%= 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 @@
              <% unless smtp_configured? %> -
              +
              -

              +

              Email delivery not configured

              -
              +

              <% if Rails.env.development? %> Emails are being delivered using <%= email_delivery_method %> and will open in your browser. @@ -44,63 +44,63 @@

              - +
              - - - - - + + + + + - + <% @users.each do |user| %> - - - - -
              EmailStatusRole2FAGroupsEmailStatusRole2FAGroups Actions
              + <%= user.email_address %> + <% if user.status.present? %> <% case user.status.to_sym %> <% when :active %> - Active + Active <% when :disabled %> - Disabled + Disabled <% when :pending_invitation %> - Pending + Pending <% end %> <% else %> - - + - <% end %> + <% if user.admin? %> - Admin + Admin <% else %> - User + User <% end %> +
              <% if user.totp_enabled? %> <% else %> - + <% end %> <% if user.totp_required? %> - Required + Required <% end %>
              + <%= user.groups.count %> diff --git a/app/views/admin/users/new.html.erb b/app/views/admin/users/new.html.erb index 7b19648..c86c53b 100644 --- a/app/views/admin/users/new.html.erb +++ b/app/views/admin/users/new.html.erb @@ -1,4 +1,4 @@
              -

              New User

              +

              New User

              <%= render "form", user: @user %>
              diff --git a/app/views/api_keys/index.html.erb b/app/views/api_keys/index.html.erb index e58aaea..06bd13e 100644 --- a/app/views/api_keys/index.html.erb +++ b/app/views/api_keys/index.html.erb @@ -1,44 +1,44 @@
              -

              API Keys

              -

              +

              API Keys

              +

              Bearer tokens for server-to-server access to forward auth applications.

              <%= 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" %>
              <% if @api_keys.any? %> -
              - - +
              +
              + - - - - - - - + + + + + + + - + <% @api_keys.each do |key| %> - - - - - + + + + +
              NameApplicationCreatedLast UsedExpiresStatusNameApplicationCreatedLast UsedExpiresStatus
              <%= key.name %><%= key.application.name %><%= key.created_at.strftime("%b %d, %Y") %><%= key.last_used_at ? time_ago_in_words(key.last_used_at) + " ago" : "Never" %><%= key.expires_at ? key.expires_at.strftime("%b %d, %Y") : "Never" %><%= key.name %><%= key.application.name %><%= key.created_at.strftime("%b %d, %Y") %><%= key.last_used_at ? time_ago_in_words(key.last_used_at) + " ago" : "Never" %><%= key.expires_at ? key.expires_at.strftime("%b %d, %Y") : "Never" %> <% if key.revoked? %> - Revoked + Revoked <% elsif key.expired? %> - Expired + Expired <% else %> - Active + Active <% end %> @@ -54,12 +54,12 @@
              <% else %> -
              +
              -

              No API keys

              -

              +

              No API keys

              +

              Create an API key to authenticate server-to-server requests.

              diff --git a/app/views/api_keys/new.html.erb b/app/views/api_keys/new.html.erb index d9ace45..b5b5201 100644 --- a/app/views/api_keys/new.html.erb +++ b/app/views/api_keys/new.html.erb @@ -1,17 +1,17 @@
              -

              New API Key

              -

              +

              New API Key

              +

              Create a bearer token for server-to-server access to a forward auth application.

              -
              +
              <%= form_with(model: @api_key, class: "space-y-6") do |f| %> <% if @api_key.errors.any? %> -
              -
              +
              +
                <% @api_key.errors.full_messages.each do |msg| %>
              • <%= msg %>
              • @@ -22,32 +22,32 @@ <% end %>
                - <%= f.label :name, class: "block text-sm font-medium text-gray-700" %> - <%= 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.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 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" %>
                - <%= 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? %> <%= f.collection_select :application_id, @applications, :id, :name, { 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 %> -

                No forward auth applications available.

                +

                No forward auth applications available.

                <% end %>
                - <%= f.label :expires_at, "Expiration (optional)", class: "block text-sm font-medium text-gray-700" %> - <%= 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" %> -

                Leave blank for no expiration.

                + <%= 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 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" %> +

                Leave blank for no expiration.

                - <%= 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", - 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 %>
              diff --git a/app/views/api_keys/show.html.erb b/app/views/api_keys/show.html.erb index b4fd8a7..4f5be5c 100644 --- a/app/views/api_keys/show.html.erb +++ b/app/views/api_keys/show.html.erb @@ -1,19 +1,19 @@
              -

              API Key Created

              -

              +

              API Key Created

              +

              Copy your API key now. You won't be able to see it again.

              -
              +
              -
              +
              -
              +

              Save this key now!

              This is the only time you'll see the full API key. Store it securely.

              @@ -21,14 +21,14 @@
              - +
              + 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" />
              -
              +

              Name: <%= @api_key.name %>

              Application: <%= @api_key.application.name %>

              Expires: <%= @api_key.expires_at ? @api_key.expires_at.strftime("%b %d, %Y %H:%M") : "Never" %>

              -
              -

              Usage example:

              -
              curl -H "Authorization: Bearer <%= @plaintext_token %>" \
              +      
              +

              Usage example:

              +
              curl -H "Authorization: Bearer <%= @plaintext_token %>" \
                    -H "X-Forwarded-Host: your-app.example.com" \
                    <%= request.base_url %>/api/verify
              <%= 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" %>
              diff --git a/app/views/dashboard/index.html.erb b/app/views/dashboard/index.html.erb index 825977e..c509ab6 100644 --- a/app/views/dashboard/index.html.erb +++ b/app/views/dashboard/index.html.erb @@ -1,8 +1,8 @@
              -

              +

              Welcome, <%= @user.email_address %>

              -

              +

              <% if @user.admin? %> Administrator <% else %> @@ -13,34 +13,34 @@

              -
              +
              - +
              -
              +
              Active Sessions
              -
              +
              <%= @user.sessions.active.count %>
              -
              +
              <%= link_to "View all sessions", profile_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
              <% if @user.totp_enabled? %> -
              +
              @@ -50,7 +50,7 @@
              -
              +
              Two-Factor Authentication
              @@ -60,13 +60,13 @@
              -
              +
              <%= link_to "Manage 2FA", profile_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
              <% else %> -
              +
              @@ -76,7 +76,7 @@
              -
              +
              Two-Factor Authentication
              @@ -86,34 +86,34 @@
              -
              +
              <%= link_to "Enable 2FA", profile_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
              <% end %> -
              +
              - +
              -
              +
              API Keys
              -
              +
              <%= @user.api_keys.active.count %>
              -
              +
              <%= link_to "Manage API Keys", api_keys_path, class: "text-sm font-medium text-blue-600 hover:text-blue-500" %>
              @@ -121,39 +121,39 @@
              -

              Your Applications

              +

              Your Applications

              <% if @applications.any? %>
              <% @applications.each do |app| %> -
              +
              <% 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 %> -
              - +
              +
              <% end %>
              -

              +

              <%= app.name %>

              <%= app.app_type.humanize %>
              <% if app.description.present? %> -

              +

              <%= app.description %>

              <% end %> @@ -165,30 +165,40 @@ <%= link_to "Open Application", app.landing_url, target: "_blank", 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 %> -
              +
              No landing URL configured
              <% end %> <% if app.user_has_active_session?(@user) %> <%= 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?" } } %> <% end %> + + <% if @user.admin? %> +
              + <%= link_to "View", admin_application_path(app), + class: "text-sm font-medium text-gray-600 dark:text-gray-400 hover:text-blue-600 transition" %> + | + <%= 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" %> +
              + <% end %>
              <% end %>
              <% else %> -
              - +
              + -

              No applications available

              -

              +

              No applications available

              +

              You don't have access to any applications yet. Contact your administrator if you think this is an error.

              @@ -197,21 +207,21 @@ <% if @user.admin? %>
              -

              Admin Quick Actions

              +

              Admin Quick Actions

              - <%= 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 %> -

              Manage Users

              -

              View, edit, and invite users

              + <%= 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 %> +

              Manage Users

              +

              View, edit, and invite users

              <% 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 %> -

              Manage Applications

              -

              Register and configure applications

              + <%= 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 %> +

              Manage Applications

              +

              Register and configure applications

              <% 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 %> -

              Manage Groups

              -

              Create and organize user groups

              + <%= 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 %> +

              Manage Groups

              +

              Create and organize user groups

              <% end %>
              diff --git a/app/views/invitations/show.html.erb b/app/views/invitations/show.html.erb index 63af659..fb19da4 100644 --- a/app/views/invitations/show.html.erb +++ b/app/views/invitations/show.html.erb @@ -4,19 +4,19 @@ <% end %>

              Welcome to Clinch!

              -

              You've been invited to join Clinch. Please create your password to complete your account setup.

              +

              You've been invited to join Clinch. Please create your password to complete your account setup.

              <%= form_with url: invitation_path(params[:token]), method: :put, class: "contents" do |form| %>
              - <%= 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" %>
              - <%= 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" %>
              <%= form.submit "Create Account", class: "w-full sm:w-auto text-center rounded-md px-3.5 py-2.5 bg-blue-600 hover:bg-blue-500 text-white inline-block font-medium cursor-pointer" %>
              <% end %> -
              \ No newline at end of file +
              diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 31b7ff6..12d391c 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -9,6 +9,15 @@ <%= csrf_meta_tags %> <%= csp_meta_tag %> + + <%= yield :head %> <%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %> @@ -23,15 +32,15 @@ <%= javascript_importmap_tags %> - + <% if authenticated? %>
              <%= render "shared/sidebar" %>
              -
              +
              <% else %> +
              + +
              <%= render "shared/flash" %> <%= yield %> diff --git a/app/views/oidc/consent.html.erb b/app/views/oidc/consent.html.erb index 1cc0c15..97404e3 100644 --- a/app/views/oidc/consent.html.erb +++ b/app/views/oidc/consent.html.erb @@ -1,30 +1,30 @@
              -
              +
              <% 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 %> -
              - +
              +
              <% end %> -

              Authorize Application

              -

              +

              Authorize Application

              +

              <%= @application.name %> is requesting access to your account.

              -

              This application will be able to:

              +

              This application will be able to:

                <% if @scopes.include?("openid") %>
              • - Verify your identity + Verify your identity
              • <% end %> <% if @scopes.include?("email") %> @@ -32,7 +32,7 @@ - Access your email address (<%= Current.session.user.email_address %>) + Access your email address (<%= Current.session.user.email_address %>) <% end %> <% if @scopes.include?("profile") %> @@ -40,7 +40,7 @@ - Access your profile information + Access your profile information <% end %> <% if @scopes.include?("groups") %> @@ -48,18 +48,18 @@ - Access your group memberships + Access your group memberships <% end %>
              -
              +
              -
              +

              You'll be redirected to:

              <%= @redirect_uri %>

              @@ -68,13 +68,13 @@ <%= form_with url: "/oauth/authorize/consent", method: :post, class: "space-y-3", data: { turbo: false }, local: true do |form| %> <%= 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", type: :submit, name: :deny, 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 %>
              diff --git a/app/views/passwords/edit.html.erb b/app/views/passwords/edit.html.erb index 65798f8..638b6c4 100644 --- a/app/views/passwords/edit.html.erb +++ b/app/views/passwords/edit.html.erb @@ -7,11 +7,11 @@ <%= form_with url: password_path(params[:token]), method: :put, class: "contents" do |form| %>
              - <%= 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" %>
              - <%= 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" %>
              diff --git a/app/views/passwords/new.html.erb b/app/views/passwords/new.html.erb index c12f7d9..e8db738 100644 --- a/app/views/passwords/new.html.erb +++ b/app/views/passwords/new.html.erb @@ -7,7 +7,7 @@ <%= form_with url: passwords_path, class: "contents", data: { controller: "form-errors" } do |form| %>
              - <%= 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" %>
              diff --git a/app/views/profiles/show.html.erb b/app/views/profiles/show.html.erb index 4384fc1..f35bfa4 100644 --- a/app/views/profiles/show.html.erb +++ b/app/views/profiles/show.html.erb @@ -1,21 +1,21 @@
              -

              Account Security

              -

              Manage your account settings, active sessions, and connected applications.

              +

              Account Security

              +

              Manage your account settings, active sessions, and connected applications.

              -
              +
              -

              Account Information

              +

              Account Information

              <%= form_with model: @user, url: profile_path, method: :patch, class: "space-y-6" do |form| %> <% if @user.errors.any? %> -
              -

              +
              +

              <%= pluralize(@user.errors.count, "error") %> prohibited this from being saved:

              -
                +
                  <% @user.errors.each do |error| %>
                • <%= error.full_message %>
                • <% end %> @@ -24,24 +24,24 @@ <% end %>
                  - <%= 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, required: true, 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" %>
                  - <%= 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, autocomplete: "current-password", 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" %> -

                  Enter your current password to confirm this change

                  + 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" %> +

                  Enter your current password to confirm this change

                  - <%= 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" %>
                  <% end %>
              @@ -49,38 +49,38 @@

              -
              +
              -

              Change Password

              +

              Change Password

              <%= form_with model: @user, url: profile_path, method: :patch, class: "space-y-6" do |form| %>
              - <%= 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, autocomplete: "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" %>
              - <%= 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, autocomplete: "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" %> -

              Must be at least 8 characters

              + 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" %> +

              Must be at least 8 characters

              - <%= 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, autocomplete: "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" %>
              - <%= 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" %>
              <% end %>
              @@ -88,15 +88,15 @@
              -
              +
              -

              Two-Factor Authentication

              -
              +

              Two-Factor Authentication

              +

              Add an extra layer of security to your account by enabling two-factor authentication.

              <% if @user.totp_enabled? %> -
              +
              @@ -104,11 +104,11 @@
              -

              +

              Two-factor authentication is enabled

              <% if @user.totp_required? %> -

              +

              @@ -119,12 +119,12 @@

              <% if @user.totp_required? %> -
              +
              -

              +

              Your administrator requires two-factor authentication. You cannot disable it.

              @@ -133,7 +133,7 @@
              @@ -142,19 +142,19 @@
              <% end %> <% 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 <% end %> <% end %> @@ -166,17 +166,17 @@