Add backchannel logout, per application logout.
This commit is contained in:
@@ -17,6 +17,56 @@
|
||||
<%= 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 application" %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :icon, "Application Icon", class: "block text-sm font-medium text-gray-700" %>
|
||||
<% if application.icon.attached? %>
|
||||
<div class="mt-2 mb-3 flex items-center gap-4">
|
||||
<%= image_tag application.icon, class: "h-16 w-16 rounded-lg object-cover border border-gray-200", alt: "Current icon" %>
|
||||
<div class="text-sm text-gray-600">
|
||||
<p class="font-medium">Current icon</p>
|
||||
<p class="text-xs"><%= number_to_human_size(application.icon.blob.byte_size) %></p>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="mt-2" data-controller="file-drop">
|
||||
<div class="flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md hover:border-blue-400 transition-colors"
|
||||
data-file-drop-target="dropzone"
|
||||
data-action="dragover->file-drop#dragover dragleave->file-drop#dragleave drop->file-drop#drop">
|
||||
<div class="space-y-1 text-center">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
|
||||
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<div class="flex text-sm text-gray-600">
|
||||
<label for="<%= form.field_id(:icon) %>" class="relative cursor-pointer bg-white rounded-md font-medium text-blue-600 hover:text-blue-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-blue-500">
|
||||
<span>Upload a file</span>
|
||||
<%= form.file_field :icon,
|
||||
accept: "image/png,image/jpg,image/jpeg,image/gif,image/svg+xml",
|
||||
class: "sr-only",
|
||||
data: { file_drop_target: "input", action: "change->file-drop#handleFiles" } %>
|
||||
</label>
|
||||
<p class="pl-1">or drag and drop</p>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500">PNG, JPG, GIF, or SVG up to 2MB</p>
|
||||
</div>
|
||||
</div>
|
||||
<div data-file-drop-target="preview" class="mt-3 hidden">
|
||||
<div class="flex items-center gap-3 p-3 bg-blue-50 rounded-md border border-blue-200">
|
||||
<img data-file-drop-target="previewImage" class="h-12 w-12 rounded object-cover" alt="Preview">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900" data-file-drop-target="filename"></p>
|
||||
<p class="text-xs text-gray-500" data-file-drop-target="filesize"></p>
|
||||
</div>
|
||||
<button type="button" data-action="click->file-drop#clear" class="text-gray-400 hover:text-gray-600">
|
||||
<svg class="h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :landing_url, "Landing URL", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.url_field :landing_url, 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: "https://app.example.com" %>
|
||||
@@ -45,6 +95,16 @@
|
||||
<p class="mt-1 text-sm text-gray-500">One URI per line. These are the allowed callback URLs for your application.</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<%= form.label :backchannel_logout_uri, "Backchannel Logout URI (Optional)", class: "block text-sm font-medium text-gray-700" %>
|
||||
<%= form.url_field :backchannel_logout_uri, 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: "https://app.example.com/oidc/backchannel-logout" %>
|
||||
<p class="mt-1 text-sm text-gray-500">
|
||||
If the application supports OpenID Connect Backchannel Logout, enter the logout endpoint URL.
|
||||
When users log out, Clinch will send logout notifications to this endpoint for immediate session termination.
|
||||
Leave blank if the application doesn't support backchannel logout.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 pt-4 mt-4">
|
||||
<h4 class="text-sm font-semibold text-gray-900 mb-3">Token Expiration Settings</h4>
|
||||
<p class="text-sm text-gray-500 mb-4">Configure how long tokens remain valid. Shorter times are more secure but require more frequent refreshes.</p>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<table class="min-w-full divide-y divide-gray-300">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0">Name</th>
|
||||
<th scope="col" class="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-0">Application</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Slug</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Type</th>
|
||||
<th scope="col" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Status</th>
|
||||
@@ -28,7 +28,18 @@
|
||||
<% @applications.each do |application| %>
|
||||
<tr>
|
||||
<td class="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-0">
|
||||
<%= link_to application.name, admin_application_path(application), class: "text-blue-600 hover:text-blue-900" %>
|
||||
<div class="flex items-center gap-3">
|
||||
<% 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" %>
|
||||
<% else %>
|
||||
<div class="h-10 w-10 rounded-lg bg-gray-100 border border-gray-200 flex items-center justify-center flex-shrink-0">
|
||||
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= link_to application.name, admin_application_path(application), class: "text-blue-600 hover:text-blue-900" %>
|
||||
</div>
|
||||
</td>
|
||||
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||
<code class="text-xs bg-gray-100 px-2 py-1 rounded"><%= application.slug %></code>
|
||||
|
||||
@@ -16,10 +16,21 @@
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="sm:flex sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-gray-900"><%= @application.name %></h1>
|
||||
<p class="mt-1 text-sm text-gray-500"><%= @application.description %></p>
|
||||
<div class="sm:flex sm:items-start sm:justify-between">
|
||||
<div class="flex items-start gap-4">
|
||||
<% 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" %>
|
||||
<% else %>
|
||||
<div class="h-16 w-16 rounded-lg bg-gray-100 border border-gray-200 flex items-center justify-center shrink-0">
|
||||
<svg class="h-8 w-8 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<% end %>
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-gray-900"><%= @application.name %></h1>
|
||||
<p class="mt-1 text-sm text-gray-500"><%= @application.description %></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 sm:mt-0 flex gap-3">
|
||||
<%= 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" %>
|
||||
@@ -78,27 +89,29 @@
|
||||
<div class="bg-white shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900">OIDC Credentials</h3>
|
||||
<h3 class="text-base font-semibold leading-6 text-gray-900">OIDC Configuration</h3>
|
||||
<%= 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" %>
|
||||
</div>
|
||||
<dl class="space-y-4">
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Client ID</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<code class="block bg-gray-100 px-3 py-2 rounded font-mono text-xs break-all"><%= @application.client_id %></code>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Client Secret</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<div class="bg-gray-100 px-3 py-2 rounded text-xs text-gray-500 italic">
|
||||
🔒 Client secret is stored securely and cannot be displayed
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500">
|
||||
To get a new client secret, use the "Regenerate Credentials" button above.
|
||||
</p>
|
||||
</dd>
|
||||
</div>
|
||||
<% unless flash[:client_id] && flash[:client_secret] %>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Client ID</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<code class="block bg-gray-100 px-3 py-2 rounded font-mono text-xs break-all"><%= @application.client_id %></code>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Client Secret</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<div class="bg-gray-100 px-3 py-2 rounded text-xs text-gray-500 italic">
|
||||
🔒 Client secret is stored securely and cannot be displayed
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500">
|
||||
To get a new client secret, use the "Regenerate Credentials" button above.
|
||||
</p>
|
||||
</dd>
|
||||
</div>
|
||||
<% end %>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">Redirect URIs</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
@@ -111,6 +124,27 @@
|
||||
<% end %>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500">
|
||||
Backchannel Logout URI
|
||||
<% if @application.supports_backchannel_logout? %>
|
||||
<span class="ml-2 inline-flex items-center rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-700">Enabled</span>
|
||||
<% end %>
|
||||
</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900">
|
||||
<% if @application.backchannel_logout_uri.present? %>
|
||||
<code class="block bg-gray-100 px-3 py-2 rounded font-mono text-xs break-all"><%= @application.backchannel_logout_uri %></code>
|
||||
<p class="mt-2 text-xs text-gray-500">
|
||||
When users log out, Clinch will send logout notifications to this endpoint for immediate session termination.
|
||||
</p>
|
||||
<% else %>
|
||||
<span class="text-gray-400 italic">Not configured</span>
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
Backchannel logout is optional. Configure it if the application supports OpenID Connect Backchannel Logout.
|
||||
</p>
|
||||
<% end %>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -102,38 +102,56 @@
|
||||
<% @applications.each do |app| %>
|
||||
<div class="bg-white rounded-lg border border-gray-200 shadow-sm hover:shadow-md transition">
|
||||
<div class="p-6">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-lg font-semibold text-gray-900 truncate">
|
||||
<%= app.name %>
|
||||
</h3>
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
||||
<% if app.oidc? %>
|
||||
bg-blue-100 text-blue-800
|
||||
<% else %>
|
||||
bg-green-100 text-green-800
|
||||
<% end %>">
|
||||
<%= app.app_type.humanize %>
|
||||
</span>
|
||||
<div class="flex items-start gap-3 mb-4">
|
||||
<% 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" %>
|
||||
<% else %>
|
||||
<div class="h-12 w-12 rounded-lg bg-gray-100 border border-gray-200 flex items-center justify-center shrink-0">
|
||||
<svg class="h-6 w-6 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-start justify-between">
|
||||
<h3 class="text-lg font-semibold text-gray-900 truncate">
|
||||
<%= app.name %>
|
||||
</h3>
|
||||
<span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium shrink-0
|
||||
<% if app.oidc? %>
|
||||
bg-blue-100 text-blue-800
|
||||
<% else %>
|
||||
bg-green-100 text-green-800
|
||||
<% end %>">
|
||||
<%= app.app_type.humanize %>
|
||||
</span>
|
||||
</div>
|
||||
<% if app.description.present? %>
|
||||
<p class="text-sm text-gray-600 mt-1 line-clamp-2">
|
||||
<%= app.description %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-600 mb-4">
|
||||
<% if app.oidc? %>
|
||||
OIDC Application
|
||||
<div class="space-y-2">
|
||||
<% if app.landing_url.present? %>
|
||||
<%= 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" %>
|
||||
<% else %>
|
||||
ForwardAuth Protected Application
|
||||
<div class="text-sm text-gray-500 italic">
|
||||
No landing URL configured
|
||||
</div>
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
<% if app.landing_url.present? %>
|
||||
<%= 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" %>
|
||||
<% else %>
|
||||
<div class="text-sm text-gray-500 italic">
|
||||
No landing URL configured
|
||||
</div>
|
||||
<% end %>
|
||||
<% if app.user_has_active_session?(@user) %>
|
||||
<%= button_to "Logout", 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",
|
||||
form: { data: { turbo_confirm: "This will log you out of #{app.name}. You can sign back in without re-authorizing. Continue?" } } %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
<div class="mx-auto max-w-md">
|
||||
<div class="bg-white py-8 px-6 shadow rounded-lg sm:px-10">
|
||||
<div class="mb-8">
|
||||
<div class="mb-8 text-center">
|
||||
<% 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" %>
|
||||
<% else %>
|
||||
<div class="mx-auto h-20 w-20 rounded-xl bg-gray-100 border-2 border-gray-200 flex items-center justify-center mb-4">
|
||||
<svg class="h-10 w-10 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<% end %>
|
||||
<h2 class="text-2xl font-bold text-gray-900">Authorize Application</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">
|
||||
<strong><%= @application.name %></strong> is requesting access to your account.
|
||||
|
||||
Reference in New Issue
Block a user