diff --git a/app/controllers/active_sessions_controller.rb b/app/controllers/active_sessions_controller.rb new file mode 100644 index 0000000..7fa365b --- /dev/null +++ b/app/controllers/active_sessions_controller.rb @@ -0,0 +1,35 @@ +class ActiveSessionsController < ApplicationController + def show + @user = Current.session.user + @active_sessions = @user.sessions.active.order(last_activity_at: :desc) + @connected_applications = @user.oidc_user_consents.includes(:application).order(granted_at: :desc) + end + + def revoke_consent + @user = Current.session.user + application = Application.find(params[:application_id]) + + # Check if user has consent for this application + consent = @user.oidc_user_consents.find_by(application: application) + unless consent + redirect_to active_sessions_path, alert: "No consent found for this application." + return + end + + # Revoke the consent + consent.destroy + redirect_to active_sessions_path, notice: "Successfully revoked access to #{application.name}." + end + + def revoke_all_consents + @user = Current.session.user + count = @user.oidc_user_consents.count + + if count > 0 + @user.oidc_user_consents.destroy_all + redirect_to active_sessions_path, notice: "Successfully revoked access to #{count} applications." + else + redirect_to active_sessions_path, alert: "No applications to revoke." + end + end +end \ No newline at end of file diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 542dbae..2df6bc5 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -76,7 +76,7 @@ module Admin end def user_params - params.require(:user).permit(:email_address, :password, :admin, :status, custom_claims: {}) + params.require(:user).permit(:email_address, :name, :password, :admin, :status, custom_claims: {}) end end end diff --git a/app/controllers/oidc_controller.rb b/app/controllers/oidc_controller.rb index cd52552..1f8dd5c 100644 --- a/app/controllers/oidc_controller.rb +++ b/app/controllers/oidc_controller.rb @@ -291,7 +291,7 @@ class OidcController < ApplicationController email: user.email_address, email_verified: true, preferred_username: user.email_address, - name: user.email_address + name: user.name.presence || user.email_address } # Add groups if user has any diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index deeb1cc..30614d0 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -1,8 +1,6 @@ class ProfilesController < ApplicationController def show @user = Current.session.user - @active_sessions = @user.sessions.active.order(last_activity_at: :desc) - @connected_applications = @user.oidc_user_consents.includes(:application).order(granted_at: :desc) end def update @@ -12,7 +10,6 @@ class ProfilesController < ApplicationController # Updating password - requires current password unless @user.authenticate(params[:user][:current_password]) @user.errors.add(:current_password, "is incorrect") - @active_sessions = @user.sessions.active.order(last_activity_at: :desc) render :show, status: :unprocessable_entity return end @@ -20,7 +17,6 @@ class ProfilesController < ApplicationController if @user.update(password_params) redirect_to profile_path, notice: "Password updated successfully." else - @active_sessions = @user.sessions.active.order(last_activity_at: :desc) render :show, status: :unprocessable_entity end else @@ -28,40 +24,11 @@ class ProfilesController < ApplicationController if @user.update(email_params) redirect_to profile_path, notice: "Email updated successfully." else - @active_sessions = @user.sessions.active.order(last_activity_at: :desc) render :show, status: :unprocessable_entity end end end - def revoke_consent - @user = Current.session.user - application = Application.find(params[:application_id]) - - # Check if user has consent for this application - consent = @user.oidc_user_consents.find_by(application: application) - unless consent - redirect_to profile_path, alert: "No consent found for this application." - return - end - - # Revoke the consent - consent.destroy - redirect_to profile_path, notice: "Successfully revoked access to #{application.name}." - end - - def revoke_all_consents - @user = Current.session.user - count = @user.oidc_user_consents.count - - if count > 0 - @user.oidc_user_consents.destroy_all - redirect_to profile_path, notice: "Successfully revoked access to #{count} applications." - else - redirect_to profile_path, alert: "No applications to revoke." - end - end - private def email_params diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 20f389b..90b9041 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -115,7 +115,7 @@ class SessionsController < ApplicationController def destroy_other session = Current.session.user.sessions.find(params[:id]) session.destroy - redirect_to profile_path, notice: "Session revoked successfully." + redirect_to active_sessions_path, notice: "Session revoked successfully." end private diff --git a/app/models/application.rb b/app/models/application.rb index 159f291..3b94911 100644 --- a/app/models/application.rb +++ b/app/models/application.rb @@ -123,8 +123,10 @@ class Application < ApplicationRecord next unless header_name.present? # Skip disabled headers case key - when :user, :email, :name + when :user, :email headers[header_name] = user.email_address + when :name + headers[header_name] = user.name.presence || user.email_address when :groups headers[header_name] = user.groups.pluck(:name).join(",") if user.groups.any? when :admin diff --git a/app/services/oidc_jwt_service.rb b/app/services/oidc_jwt_service.rb index 00cee45..52f709e 100644 --- a/app/services/oidc_jwt_service.rb +++ b/app/services/oidc_jwt_service.rb @@ -13,7 +13,7 @@ class OidcJwtService email: user.email_address, email_verified: true, preferred_username: user.email_address, - name: user.email_address + name: user.name.presence || user.email_address } # Add nonce if provided (OIDC requires this for implicit flow) diff --git a/app/views/active_sessions/show.html.erb b/app/views/active_sessions/show.html.erb new file mode 100644 index 0000000..a66ee49 --- /dev/null +++ b/app/views/active_sessions/show.html.erb @@ -0,0 +1,114 @@ +
+
+

Sessions

+

Manage your active sessions and 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", + 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.

+ <% 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", + 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?" } } %> +
+
+
+ <% end %> +
+
+
+ + +
+
+

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", + form: { data: { turbo_confirm: "Are you sure you want to revoke this session?" } } %> + <% end %> +
    +
  • + <% end %> +
+ <% else %> +

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", + form: { data: { turbo_confirm: "This will sign you out from all other devices except this one. Are you sure?" } } %> +
+
+
+ <% end %> +
+
+
+ +
\ No newline at end of file diff --git a/app/views/admin/applications/_form.html.erb b/app/views/admin/applications/_form.html.erb index 7387bdd..2de21d9 100644 --- a/app/views/admin/applications/_form.html.erb +++ b/app/views/admin/applications/_form.html.erb @@ -65,8 +65,23 @@
<%= form.label :headers_config, "Custom Headers Configuration (JSON)", class: "block text-sm font-medium text-gray-700" %> - <%= form.text_area :headers_config, 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", placeholder: '{"user": "X-Remote-User", "email": "X-Remote-Email"}' %> -

Optional: Override default headers. Leave empty to use defaults: X-Remote-User, X-Remote-Email, X-Remote-Name, X-Remote-Groups, X-Remote-Admin

+ <%= form.text_area :headers_config, rows: 10, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm font-mono", placeholder: '{"user": "Remote-User", "groups": "Remote-Groups"}' %> +
+

Optional: Customize header names sent to your application.

+

Default headers: X-Remote-User, X-Remote-Email, X-Remote-Name, X-Remote-Groups, X-Remote-Admin

+
+ Show available header keys and what data they send +
+

user - User's email address

+

email - User's email address

+

name - User's display name (falls back to email if not set)

+

groups - Comma-separated list of group names (e.g., "admin,developers")

+

admin - "true" or "false" indicating admin status

+

Example: {"user": "Remote-User", "groups": "Remote-Groups"}

+

Need custom user fields? Add them to user's custom_claims for OIDC tokens

+
+
+
diff --git a/app/views/admin/users/_form.html.erb b/app/views/admin/users/_form.html.erb index 6f46488..3ed2485 100644 --- a/app/views/admin/users/_form.html.erb +++ b/app/views/admin/users/_form.html.erb @@ -23,6 +23,12 @@ <%= 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 :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: 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" %> diff --git a/app/views/profiles/show.html.erb b/app/views/profiles/show.html.erb index 58a283d..3c6b5b2 100644 --- a/app/views/profiles/show.html.erb +++ b/app/views/profiles/show.html.erb @@ -198,110 +198,4 @@ document.getElementById('view-backup-codes-modal').classList.add('hidden'); } - - -
-
-

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_profile_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", - 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.

- <% end %> -
-
-
- - -
-
-

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", - form: { data: { turbo_confirm: "Are you sure you want to revoke this session?" } } %> - <% end %> -
    -
  • - <% end %> -
- <% else %> -

No other active sessions.

- <% end %> -
-
-
- - -
-
-

Security Actions

-
-

Use these actions to quickly secure your account. Be careful - these actions cannot be undone.

-
-
- <% 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-4 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", - form: { data: { turbo_confirm: "This will sign you out from all other devices except this one. Are you sure?" } } %> - <% end %> - - <% if @connected_applications.any? %> - <%= button_to "Revoke All App Access", revoke_all_consents_profile_path, method: :delete, - 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", - 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?" } } %> - <% end %> -
-
-
diff --git a/app/views/shared/_sidebar.html.erb b/app/views/shared/_sidebar.html.erb index 3336732..e785cdc 100644 --- a/app/views/shared/_sidebar.html.erb +++ b/app/views/shared/_sidebar.html.erb @@ -78,6 +78,16 @@ <% end %> + +
  • + <%= 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 %> + + + + Sessions + <% end %> +
  • +
  • <%= link_to signout_path, data: { turbo_method: :delete }, 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 %> @@ -169,6 +179,14 @@ Profile <% end %>
  • +
  • + <%= link_to active_sessions_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 text-gray-700 hover:text-blue-600 hover:bg-gray-50" do %> + + + + Sessions + <% end %> +
  • <%= link_to signout_path, data: { turbo_method: :delete }, 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 %> diff --git a/config/routes.rb b/config/routes.rb index f6266b4..ef423ca 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -42,6 +42,12 @@ Rails.application.routes.draw do delete :revoke_all_consents end end + resource :active_sessions, only: [:show] do + member do + delete :revoke_consent + delete :revoke_all_consents + end + end resources :sessions, only: [] do member do delete :destroy, action: :destroy_other diff --git a/db/migrate/20251104022439_add_name_to_users.rb b/db/migrate/20251104022439_add_name_to_users.rb new file mode 100644 index 0000000..8ca60d1 --- /dev/null +++ b/db/migrate/20251104022439_add_name_to_users.rb @@ -0,0 +1,5 @@ +class AddNameToUsers < ActiveRecord::Migration[8.1] + def change + add_column :users, :name, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 06bc96d..8c66031 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2025_11_04_015104) do +ActiveRecord::Schema[8.1].define(version: 2025_11_04_022439) do create_table "application_groups", force: :cascade do |t| t.integer "application_id", null: false t.datetime "created_at", null: false @@ -128,6 +128,7 @@ ActiveRecord::Schema[8.1].define(version: 2025_11_04_015104) do t.json "custom_claims", default: {}, null: false t.string "email_address", null: false t.datetime "last_sign_in_at" + t.string "name" t.string "password_digest", null: false t.integer "status", default: 0, null: false t.boolean "totp_required", default: false, null: false