Move sessions into their own view for easier management
This commit is contained in:
35
app/controllers/active_sessions_controller.rb
Normal file
35
app/controllers/active_sessions_controller.rb
Normal file
@@ -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
|
||||||
@@ -76,7 +76,7 @@ module Admin
|
|||||||
end
|
end
|
||||||
|
|
||||||
def user_params
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ class OidcController < ApplicationController
|
|||||||
email: user.email_address,
|
email: user.email_address,
|
||||||
email_verified: true,
|
email_verified: true,
|
||||||
preferred_username: user.email_address,
|
preferred_username: user.email_address,
|
||||||
name: user.email_address
|
name: user.name.presence || user.email_address
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add groups if user has any
|
# Add groups if user has any
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
class ProfilesController < ApplicationController
|
class ProfilesController < ApplicationController
|
||||||
def show
|
def show
|
||||||
@user = Current.session.user
|
@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
|
end
|
||||||
|
|
||||||
def update
|
def update
|
||||||
@@ -12,7 +10,6 @@ class ProfilesController < ApplicationController
|
|||||||
# Updating password - requires current password
|
# Updating password - requires current password
|
||||||
unless @user.authenticate(params[:user][:current_password])
|
unless @user.authenticate(params[:user][:current_password])
|
||||||
@user.errors.add(:current_password, "is incorrect")
|
@user.errors.add(:current_password, "is incorrect")
|
||||||
@active_sessions = @user.sessions.active.order(last_activity_at: :desc)
|
|
||||||
render :show, status: :unprocessable_entity
|
render :show, status: :unprocessable_entity
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -20,7 +17,6 @@ class ProfilesController < ApplicationController
|
|||||||
if @user.update(password_params)
|
if @user.update(password_params)
|
||||||
redirect_to profile_path, notice: "Password updated successfully."
|
redirect_to profile_path, notice: "Password updated successfully."
|
||||||
else
|
else
|
||||||
@active_sessions = @user.sessions.active.order(last_activity_at: :desc)
|
|
||||||
render :show, status: :unprocessable_entity
|
render :show, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
@@ -28,40 +24,11 @@ class ProfilesController < ApplicationController
|
|||||||
if @user.update(email_params)
|
if @user.update(email_params)
|
||||||
redirect_to profile_path, notice: "Email updated successfully."
|
redirect_to profile_path, notice: "Email updated successfully."
|
||||||
else
|
else
|
||||||
@active_sessions = @user.sessions.active.order(last_activity_at: :desc)
|
|
||||||
render :show, status: :unprocessable_entity
|
render :show, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def email_params
|
def email_params
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ class SessionsController < ApplicationController
|
|||||||
def destroy_other
|
def destroy_other
|
||||||
session = Current.session.user.sessions.find(params[:id])
|
session = Current.session.user.sessions.find(params[:id])
|
||||||
session.destroy
|
session.destroy
|
||||||
redirect_to profile_path, notice: "Session revoked successfully."
|
redirect_to active_sessions_path, notice: "Session revoked successfully."
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -123,8 +123,10 @@ class Application < ApplicationRecord
|
|||||||
next unless header_name.present? # Skip disabled headers
|
next unless header_name.present? # Skip disabled headers
|
||||||
|
|
||||||
case key
|
case key
|
||||||
when :user, :email, :name
|
when :user, :email
|
||||||
headers[header_name] = user.email_address
|
headers[header_name] = user.email_address
|
||||||
|
when :name
|
||||||
|
headers[header_name] = user.name.presence || user.email_address
|
||||||
when :groups
|
when :groups
|
||||||
headers[header_name] = user.groups.pluck(:name).join(",") if user.groups.any?
|
headers[header_name] = user.groups.pluck(:name).join(",") if user.groups.any?
|
||||||
when :admin
|
when :admin
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class OidcJwtService
|
|||||||
email: user.email_address,
|
email: user.email_address,
|
||||||
email_verified: true,
|
email_verified: true,
|
||||||
preferred_username: user.email_address,
|
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)
|
# Add nonce if provided (OIDC requires this for implicit flow)
|
||||||
|
|||||||
114
app/views/active_sessions/show.html.erb
Normal file
114
app/views/active_sessions/show.html.erb
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
<div class="space-y-8">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl font-bold text-gray-900">Sessions</h1>
|
||||||
|
<p class="mt-2 text-sm text-gray-600">Manage your active sessions and connected applications.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Connected Applications -->
|
||||||
|
<div class="bg-white shadow sm:rounded-lg">
|
||||||
|
<div class="px-4 py-5 sm:p-6">
|
||||||
|
<h3 class="text-lg font-medium leading-6 text-gray-900">Connected Applications</h3>
|
||||||
|
<div class="mt-2 max-w-xl text-sm text-gray-500">
|
||||||
|
<p>These applications have access to your account. You can revoke access at any time.</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5">
|
||||||
|
<% if @connected_applications.any? %>
|
||||||
|
<ul role="list" class="divide-y divide-gray-200">
|
||||||
|
<% @connected_applications.each do |consent| %>
|
||||||
|
<li class="py-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<p class="text-sm font-medium text-gray-900">
|
||||||
|
<%= consent.application.name %>
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
Access to: <%= consent.formatted_scopes %>
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 text-xs text-gray-400">
|
||||||
|
Authorized <%= time_ago_in_words(consent.granted_at) %> ago
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<%= 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." } } %>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
<% else %>
|
||||||
|
<p class="text-sm text-gray-500">No connected applications.</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if @connected_applications.any? %>
|
||||||
|
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<div class="inline-block">
|
||||||
|
<%= 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?" } } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Active Sessions -->
|
||||||
|
<div class="bg-white shadow sm:rounded-lg">
|
||||||
|
<div class="px-4 py-5 sm:p-6">
|
||||||
|
<h3 class="text-lg font-medium leading-6 text-gray-900">Active Sessions</h3>
|
||||||
|
<div class="mt-2 max-w-xl text-sm text-gray-500">
|
||||||
|
<p>These devices are currently signed in to your account. Revoke any sessions that you don't recognize.</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5">
|
||||||
|
<% if @active_sessions.any? %>
|
||||||
|
<ul role="list" class="divide-y divide-gray-200">
|
||||||
|
<% @active_sessions.each do |session| %>
|
||||||
|
<li class="py-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<p class="text-sm font-medium text-gray-900">
|
||||||
|
<%= session.device_name || "Unknown Device" %>
|
||||||
|
<% if session.id == Current.session.id %>
|
||||||
|
<span class="ml-2 inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800">
|
||||||
|
This device
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">
|
||||||
|
<%= session.ip_address %>
|
||||||
|
</p>
|
||||||
|
<p class="mt-1 text-xs text-gray-400">
|
||||||
|
Last active <%= time_ago_in_words(session.last_activity_at || session.updated_at) %> ago
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<% 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 %>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<% end %>
|
||||||
|
</ul>
|
||||||
|
<% else %>
|
||||||
|
<p class="text-sm text-gray-500">No other active sessions.</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if @active_sessions.count > 1 %>
|
||||||
|
<div class="mt-6 pt-6 border-t border-gray-200">
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<div class="inline-block">
|
||||||
|
<%= 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?" } } %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
@@ -65,8 +65,23 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<%= form.label :headers_config, "Custom Headers Configuration (JSON)", class: "block text-sm font-medium text-gray-700" %>
|
<%= 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"}' %>
|
<%= 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"}' %>
|
||||||
<p class="mt-1 text-sm text-gray-500">Optional: Override default headers. Leave empty to use defaults: X-Remote-User, X-Remote-Email, X-Remote-Name, X-Remote-Groups, X-Remote-Admin</p>
|
<div class="mt-2 text-sm text-gray-600 space-y-1">
|
||||||
|
<p class="font-medium">Optional: Customize header names sent to your application.</p>
|
||||||
|
<p><strong>Default headers:</strong> X-Remote-User, X-Remote-Email, X-Remote-Name, X-Remote-Groups, X-Remote-Admin</p>
|
||||||
|
<details class="mt-2">
|
||||||
|
<summary class="cursor-pointer text-blue-600 hover:text-blue-800">Show available header keys and what data they send</summary>
|
||||||
|
<div class="mt-2 ml-4 space-y-1 text-xs">
|
||||||
|
<p><code class="bg-gray-100 px-1 rounded">user</code> - User's email address</p>
|
||||||
|
<p><code class="bg-gray-100 px-1 rounded">email</code> - User's email address</p>
|
||||||
|
<p><code class="bg-gray-100 px-1 rounded">name</code> - User's display name (falls back to email if not set)</p>
|
||||||
|
<p><code class="bg-gray-100 px-1 rounded">groups</code> - Comma-separated list of group names (e.g., "admin,developers")</p>
|
||||||
|
<p><code class="bg-gray-100 px-1 rounded">admin</code> - "true" or "false" indicating admin status</p>
|
||||||
|
<p class="mt-2 italic">Example: <code class="bg-gray-100 px-1 rounded">{"user": "Remote-User", "groups": "Remote-Groups"}</code></p>
|
||||||
|
<p class="italic">Need custom user fields? Add them to user's custom_claims for OIDC tokens</p>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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.email_field :email_address, required: true, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: "user@example.com" %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<%= 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" %>
|
||||||
|
<p class="mt-1 text-sm text-gray-500">Optional: Name shown in applications. Defaults to email address if not set.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<%= form.label :password, class: "block text-sm font-medium text-gray-700" %>
|
<%= form.label :password, class: "block text-sm font-medium text-gray-700" %>
|
||||||
<%= form.password_field :password, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: user.persisted? ? "Leave blank to keep current password" : "Enter password" %>
|
<%= form.password_field :password, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm", placeholder: user.persisted? ? "Leave blank to keep current password" : "Enter password" %>
|
||||||
|
|||||||
@@ -198,110 +198,4 @@
|
|||||||
document.getElementById('view-backup-codes-modal').classList.add('hidden');
|
document.getElementById('view-backup-codes-modal').classList.add('hidden');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Connected Applications -->
|
|
||||||
<div class="bg-white shadow sm:rounded-lg">
|
|
||||||
<div class="px-4 py-5 sm:p-6">
|
|
||||||
<h3 class="text-lg font-medium leading-6 text-gray-900">Connected Applications</h3>
|
|
||||||
<div class="mt-2 max-w-xl text-sm text-gray-500">
|
|
||||||
<p>These applications have access to your account. You can revoke access at any time.</p>
|
|
||||||
</div>
|
|
||||||
<div class="mt-5">
|
|
||||||
<% if @connected_applications.any? %>
|
|
||||||
<ul role="list" class="divide-y divide-gray-200">
|
|
||||||
<% @connected_applications.each do |consent| %>
|
|
||||||
<li class="py-4">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<p class="text-sm font-medium text-gray-900">
|
|
||||||
<%= consent.application.name %>
|
|
||||||
</p>
|
|
||||||
<p class="mt-1 text-sm text-gray-500">
|
|
||||||
Access to: <%= consent.formatted_scopes %>
|
|
||||||
</p>
|
|
||||||
<p class="mt-1 text-xs text-gray-400">
|
|
||||||
Authorized <%= time_ago_in_words(consent.granted_at) %> ago
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<%= 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." } } %>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<% end %>
|
|
||||||
</ul>
|
|
||||||
<% else %>
|
|
||||||
<p class="text-sm text-gray-500">No connected applications.</p>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Active Sessions -->
|
|
||||||
<div class="bg-white shadow sm:rounded-lg">
|
|
||||||
<div class="px-4 py-5 sm:p-6">
|
|
||||||
<h3 class="text-lg font-medium leading-6 text-gray-900">Active Sessions</h3>
|
|
||||||
<div class="mt-2 max-w-xl text-sm text-gray-500">
|
|
||||||
<p>These devices are currently signed in to your account. Revoke any sessions that you don't recognize.</p>
|
|
||||||
</div>
|
|
||||||
<div class="mt-5">
|
|
||||||
<% if @active_sessions.any? %>
|
|
||||||
<ul role="list" class="divide-y divide-gray-200">
|
|
||||||
<% @active_sessions.each do |session| %>
|
|
||||||
<li class="py-4">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<p class="text-sm font-medium text-gray-900">
|
|
||||||
<%= session.device_name || "Unknown Device" %>
|
|
||||||
<% if session.id == Current.session.id %>
|
|
||||||
<span class="ml-2 inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800">
|
|
||||||
This device
|
|
||||||
</span>
|
|
||||||
<% end %>
|
|
||||||
</p>
|
|
||||||
<p class="mt-1 text-sm text-gray-500">
|
|
||||||
<%= session.ip_address %>
|
|
||||||
</p>
|
|
||||||
<p class="mt-1 text-xs text-gray-400">
|
|
||||||
Last active <%= time_ago_in_words(session.last_activity_at || session.updated_at) %> ago
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<% 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 %>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<% end %>
|
|
||||||
</ul>
|
|
||||||
<% else %>
|
|
||||||
<p class="text-sm text-gray-500">No other active sessions.</p>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Global Security Actions -->
|
|
||||||
<div class="bg-white shadow sm:rounded-lg">
|
|
||||||
<div class="px-4 py-5 sm:p-6">
|
|
||||||
<h3 class="text-lg font-medium leading-6 text-gray-900">Security Actions</h3>
|
|
||||||
<div class="mt-2 max-w-xl text-sm text-gray-500">
|
|
||||||
<p>Use these actions to quickly secure your account. Be careful - these actions cannot be undone.</p>
|
|
||||||
</div>
|
|
||||||
<div class="mt-5 flex flex-wrap gap-4">
|
|
||||||
<% 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 %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -78,6 +78,16 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<!-- Sessions -->
|
||||||
|
<li>
|
||||||
|
<%= link_to active_sessions_path, class: "group flex gap-x-3 rounded-md p-2 text-sm font-semibold leading-6 #{ current_path == '/active_sessions' ? 'bg-gray-50 text-blue-600' : 'text-gray-700 hover:text-blue-600 hover:bg-gray-50' }" do %>
|
||||||
|
<svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 18v-5.25m0 0a6.01 6.01 0 001.5-.189m-1.5.189a6.01 6.01 0 01-1.5-.189m3.75 7.478a12.06 12.06 0 01-4.5 0m3.75 2.383a14.406 14.406 0 01-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 10-7.517 0c.85.493 1.509 1.333 1.509 2.316V18" />
|
||||||
|
</svg>
|
||||||
|
Sessions
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
|
|
||||||
<!-- Sign Out -->
|
<!-- Sign Out -->
|
||||||
<li>
|
<li>
|
||||||
<%= 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 %>
|
<%= 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
|
Profile
|
||||||
<% end %>
|
<% end %>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<%= 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 %>
|
||||||
|
<svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 18v-5.25m0 0a6.01 6.01 0 001.5-.189m-1.5.189a6.01 6.01 0 01-1.5-.189m3.75 7.478a12.06 12.06 0 01-4.5 0m3.75 2.383a14.406 14.406 0 01-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 10-7.517 0c.85.493 1.509 1.333 1.509 2.316V18" />
|
||||||
|
</svg>
|
||||||
|
Sessions
|
||||||
|
<% end %>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<%= 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 %>
|
<%= 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 %>
|
||||||
<svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
<svg class="h-6 w-6 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ Rails.application.routes.draw do
|
|||||||
delete :revoke_all_consents
|
delete :revoke_all_consents
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
resource :active_sessions, only: [:show] do
|
||||||
|
member do
|
||||||
|
delete :revoke_consent
|
||||||
|
delete :revoke_all_consents
|
||||||
|
end
|
||||||
|
end
|
||||||
resources :sessions, only: [] do
|
resources :sessions, only: [] do
|
||||||
member do
|
member do
|
||||||
delete :destroy, action: :destroy_other
|
delete :destroy, action: :destroy_other
|
||||||
|
|||||||
5
db/migrate/20251104022439_add_name_to_users.rb
Normal file
5
db/migrate/20251104022439_add_name_to_users.rb
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
class AddNameToUsers < ActiveRecord::Migration[8.1]
|
||||||
|
def change
|
||||||
|
add_column :users, :name, :string
|
||||||
|
end
|
||||||
|
end
|
||||||
3
db/schema.rb
generated
3
db/schema.rb
generated
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# 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|
|
create_table "application_groups", force: :cascade do |t|
|
||||||
t.integer "application_id", null: false
|
t.integer "application_id", null: false
|
||||||
t.datetime "created_at", 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.json "custom_claims", default: {}, null: false
|
||||||
t.string "email_address", null: false
|
t.string "email_address", null: false
|
||||||
t.datetime "last_sign_in_at"
|
t.datetime "last_sign_in_at"
|
||||||
|
t.string "name"
|
||||||
t.string "password_digest", null: false
|
t.string "password_digest", null: false
|
||||||
t.integer "status", default: 0, null: false
|
t.integer "status", default: 0, null: false
|
||||||
t.boolean "totp_required", default: false, null: false
|
t.boolean "totp_required", default: false, null: false
|
||||||
|
|||||||
Reference in New Issue
Block a user