308 lines
16 KiB
Plaintext
308 lines
16 KiB
Plaintext
<div class="space-y-8">
|
|
<div>
|
|
<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, active sessions, and connected applications.</p>
|
|
</div>
|
|
|
|
<!-- Account Information -->
|
|
<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">Account Information</h3>
|
|
<div class="mt-5 space-y-6">
|
|
<%= form_with model: @user, url: profile_path, method: :patch, class: "space-y-6" do |form| %>
|
|
<% if @user.errors.any? %>
|
|
<div class="rounded-md bg-red-50 p-4">
|
|
<h3 class="text-sm font-medium text-red-800">
|
|
<%= pluralize(@user.errors.count, "error") %> prohibited this from being saved:
|
|
</h3>
|
|
<ul class="mt-2 list-disc list-inside text-sm text-red-700">
|
|
<% @user.errors.each do |error| %>
|
|
<li><%= error.full_message %></li>
|
|
<% end %>
|
|
</ul>
|
|
</div>
|
|
<% end %>
|
|
|
|
<div>
|
|
<%= form.label :email_address, "Email Address", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= 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" %>
|
|
</div>
|
|
|
|
<div>
|
|
<%= form.submit "Update Email", class: "inline-flex justify-center rounded-md border border-transparent bg-blue-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Change Password -->
|
|
<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">Change Password</h3>
|
|
<div class="mt-5">
|
|
<%= form_with model: @user, url: profile_path, method: :patch, class: "space-y-6" do |form| %>
|
|
<div>
|
|
<%= form.label :current_password, "Current Password", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= 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" %>
|
|
</div>
|
|
|
|
<div>
|
|
<%= form.label :password, "New Password", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= 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" %>
|
|
<p class="mt-1 text-sm text-gray-500">Must be at least 8 characters</p>
|
|
</div>
|
|
|
|
<div>
|
|
<%= form.label :password_confirmation, "Confirm New Password", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= 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" %>
|
|
</div>
|
|
|
|
<div>
|
|
<%= form.submit "Update Password", class: "inline-flex justify-center rounded-md border border-transparent bg-blue-600 py-2 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Two-Factor Authentication -->
|
|
<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">Two-Factor Authentication</h3>
|
|
<div class="mt-2 max-w-xl text-sm text-gray-500">
|
|
<p>Add an extra layer of security to your account by enabling two-factor authentication.</p>
|
|
</div>
|
|
<div class="mt-5">
|
|
<% if @user.totp_enabled? %>
|
|
<div class="rounded-md bg-green-50 p-4">
|
|
<div class="flex">
|
|
<div class="flex-shrink-0">
|
|
<svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
<div class="ml-3 flex-1">
|
|
<p class="text-sm font-medium text-green-800">
|
|
Two-factor authentication is enabled
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 flex gap-3">
|
|
<button type="button" onclick="showDisable2FAModal()" 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">
|
|
Disable 2FA
|
|
</button>
|
|
<button type="button" onclick="showViewBackupCodesModal()" class="inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
|
View Backup Codes
|
|
</button>
|
|
</div>
|
|
<% 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 %>
|
|
Enable 2FA
|
|
<% end %>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Disable 2FA Modal -->
|
|
<div id="disable-2fa-modal" class="hidden fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50">
|
|
<div class="bg-white rounded-lg px-4 pt-5 pb-4 shadow-xl max-w-md w-full">
|
|
<div class="sm:flex sm:items-start">
|
|
<div class="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
|
|
<svg class="h-6 w-6 text-red-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
</svg>
|
|
</div>
|
|
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left flex-1">
|
|
<h3 class="text-lg font-medium leading-6 text-gray-900">Disable Two-Factor Authentication</h3>
|
|
<div class="mt-2">
|
|
<p class="text-sm text-gray-500">Enter your password to disable 2FA. This will make your account less secure.</p>
|
|
</div>
|
|
<%= form_with url: totp_path, method: :delete, class: "mt-4" do |form| %>
|
|
<div>
|
|
<%= password_field_tag :password, nil,
|
|
placeholder: "Enter your password",
|
|
autocomplete: "current-password",
|
|
required: true,
|
|
class: "block w-full rounded-md border-gray-300 shadow-sm focus:border-red-500 focus:ring-red-500 sm:text-sm" %>
|
|
</div>
|
|
<div class="mt-4 flex gap-3">
|
|
<%= form.submit "Disable 2FA",
|
|
class: "inline-flex justify-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2" %>
|
|
<button type="button" onclick="hideDisable2FAModal()" class="inline-flex justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- View Backup Codes Modal -->
|
|
<div id="view-backup-codes-modal" class="hidden fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50">
|
|
<div class="bg-white rounded-lg px-4 pt-5 pb-4 shadow-xl max-w-md w-full">
|
|
<div>
|
|
<h3 class="text-lg font-medium leading-6 text-gray-900">View Backup Codes</h3>
|
|
<div class="mt-2">
|
|
<p class="text-sm text-gray-500">Enter your password to view your backup codes.</p>
|
|
</div>
|
|
<%= form_with url: verify_password_totp_path, method: :post, class: "mt-4" do |form| %>
|
|
<div>
|
|
<%= password_field_tag :password, nil,
|
|
placeholder: "Enter your password",
|
|
autocomplete: "current-password",
|
|
required: true,
|
|
class: "block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
|
</div>
|
|
<div class="mt-4 flex gap-3">
|
|
<%= form.submit "View Codes",
|
|
class: "inline-flex justify-center rounded-md border border-transparent bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" %>
|
|
<button type="button" onclick="hideViewBackupCodesModal()" class="inline-flex justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function showDisable2FAModal() {
|
|
document.getElementById('disable-2fa-modal').classList.remove('hidden');
|
|
}
|
|
|
|
function hideDisable2FAModal() {
|
|
document.getElementById('disable-2fa-modal').classList.add('hidden');
|
|
}
|
|
|
|
function showViewBackupCodesModal() {
|
|
document.getElementById('view-backup-codes-modal').classList.remove('hidden');
|
|
}
|
|
|
|
function hideViewBackupCodesModal() {
|
|
document.getElementById('view-backup-codes-modal').classList.add('hidden');
|
|
}
|
|
</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>
|