Increase the thing
This commit is contained in:
@@ -2,6 +2,7 @@ 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)
|
@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
|
||||||
@@ -33,6 +34,34 @@ class ProfilesController < ApplicationController
|
|||||||
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
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
class SessionsController < ApplicationController
|
class SessionsController < ApplicationController
|
||||||
allow_unauthenticated_access only: %i[ new create verify_totp ]
|
allow_unauthenticated_access only: %i[ new create verify_totp ]
|
||||||
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to signin_path, alert: "Too many attempts. Try again later." }
|
rate_limit to: 20, within: 3.minutes, only: :create, with: -> { redirect_to signin_path, alert: "Too many attempts. Try again later." }
|
||||||
rate_limit to: 5, within: 3.minutes, only: :verify_totp, with: -> { redirect_to totp_verification_path, alert: "Too many attempts. Try again later." }
|
rate_limit to: 10, within: 3.minutes, only: :verify_totp, with: -> { redirect_to totp_verification_path, alert: "Too many attempts. Try again later." }
|
||||||
|
|
||||||
def new
|
def new
|
||||||
# Redirect to signup if this is first run
|
# Redirect to signup if this is first run
|
||||||
|
|||||||
@@ -26,6 +26,24 @@ class OidcUserConsent < ApplicationRecord
|
|||||||
(requested - granted).empty?
|
(requested - granted).empty?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Get a human-readable list of scopes
|
||||||
|
def formatted_scopes
|
||||||
|
scopes.map do |scope|
|
||||||
|
case scope
|
||||||
|
when 'openid'
|
||||||
|
'Basic authentication'
|
||||||
|
when 'profile'
|
||||||
|
'Profile information'
|
||||||
|
when 'email'
|
||||||
|
'Email address'
|
||||||
|
when 'groups'
|
||||||
|
'Group membership'
|
||||||
|
else
|
||||||
|
scope.humanize
|
||||||
|
end
|
||||||
|
end.join(', ')
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_granted_at
|
def set_granted_at
|
||||||
|
|||||||
@@ -80,6 +80,15 @@ class User < ApplicationRecord
|
|||||||
.find { |consent| consent.covers_scopes?(requested_scopes) }
|
.find { |consent| consent.covers_scopes?(requested_scopes) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def revoke_consent!(application)
|
||||||
|
consent = oidc_user_consents.find_by(application: application)
|
||||||
|
consent&.destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
def revoke_all_consents!
|
||||||
|
oidc_user_consents.destroy_all
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def generate_backup_codes
|
def generate_backup_codes
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-bold text-gray-900">Profile & Settings</h1>
|
<h1 class="text-3xl font-bold text-gray-900">Account Security</h1>
|
||||||
<p class="mt-2 text-sm text-gray-600">Manage your account settings and security preferences.</p>
|
<p class="mt-2 text-sm text-gray-600">Manage your account settings, active sessions, and connected applications.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Account Information -->
|
<!-- Account Information -->
|
||||||
@@ -199,6 +199,44 @@
|
|||||||
}
|
}
|
||||||
</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 -->
|
<!-- Active Sessions -->
|
||||||
<div class="bg-white shadow sm:rounded-lg">
|
<div class="bg-white shadow sm:rounded-lg">
|
||||||
<div class="px-4 py-5 sm:p-6">
|
<div class="px-4 py-5 sm:p-6">
|
||||||
@@ -243,4 +281,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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>
|
||||||
|
|||||||
@@ -34,7 +34,12 @@ Rails.application.routes.draw do
|
|||||||
|
|
||||||
# Authenticated routes
|
# Authenticated routes
|
||||||
root "dashboard#index"
|
root "dashboard#index"
|
||||||
resource :profile, only: [:show, :update]
|
resource :profile, only: [:show, :update] 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
|
||||||
|
|||||||
Reference in New Issue
Block a user