230 lines
16 KiB
Plaintext
230 lines
16 KiB
Plaintext
<%= form_with(model: [:admin, application], class: "space-y-6", data: { controller: "application-form form-errors" }) do |form| %>
|
|
<%= render "shared/form_errors", form: form %>
|
|
|
|
<div>
|
|
<%= form.label :name, class: "block text-sm font-medium text-gray-700" %>
|
|
<%= form.text_field :name, 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: "My Application" %>
|
|
</div>
|
|
|
|
<div>
|
|
<%= form.label :slug, class: "block text-sm font-medium text-gray-700" %>
|
|
<%= form.text_field :slug, 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 font-mono", placeholder: "my-app" %>
|
|
<p class="mt-1 text-sm text-gray-500">Lowercase letters, numbers, and hyphens only. Used in URLs and API calls.</p>
|
|
</div>
|
|
|
|
<div>
|
|
<%= form.label :description, class: "block text-sm font-medium text-gray-700" %>
|
|
<%= 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" %>
|
|
<p class="mt-1 text-sm text-gray-500">The main URL users will visit to access this application. This will be shown as a link on their dashboard.</p>
|
|
</div>
|
|
|
|
<div>
|
|
<%= form.label :app_type, "Application Type", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= form.select :app_type, [["OpenID Connect (OIDC)", "oidc"], ["Forward Auth (Reverse Proxy)", "forward_auth"]], {}, {
|
|
class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm",
|
|
disabled: application.persisted?,
|
|
data: { action: "change->application-form#updateFieldVisibility", application_form_target: "appTypeSelect" }
|
|
} %>
|
|
<% if application.persisted? %>
|
|
<p class="mt-1 text-sm text-gray-500">Application type cannot be changed after creation.</p>
|
|
<% end %>
|
|
</div>
|
|
|
|
<!-- OIDC-specific fields -->
|
|
<div id="oidc-fields" class="space-y-6 border-t border-gray-200 pt-6 <%= 'hidden' unless application.oidc? || !application.persisted? %>" data-application-form-target="oidcFields">
|
|
<h3 class="text-base font-semibold text-gray-900">OIDC Configuration</h3>
|
|
|
|
<div>
|
|
<%= form.label :redirect_uris, "Redirect URIs", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= form.text_area :redirect_uris, rows: 4, 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://example.com/callback\nhttps://app.example.com/auth/callback" %>
|
|
<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>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<%= form.label :access_token_ttl, "Access Token TTL (seconds)", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= form.number_field :access_token_ttl, value: application.access_token_ttl || 3600, min: 300, max: 86400, step: 60, 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-xs text-gray-500">
|
|
Range: 5 min - 24 hours
|
|
<br>Default: 1 hour (3600s)
|
|
<br>Current: <span class="font-medium"><%= application.access_token_ttl_human || "1 hour" %></span>
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<%= form.label :refresh_token_ttl, "Refresh Token TTL (seconds)", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= form.number_field :refresh_token_ttl, value: application.refresh_token_ttl || 2592000, min: 86400, max: 7776000, step: 86400, 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-xs text-gray-500">
|
|
Range: 1 day - 90 days
|
|
<br>Default: 30 days (2592000s)
|
|
<br>Current: <span class="font-medium"><%= application.refresh_token_ttl_human || "30 days" %></span>
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<%= form.label :id_token_ttl, "ID Token TTL (seconds)", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= form.number_field :id_token_ttl, value: application.id_token_ttl || 3600, min: 300, max: 86400, step: 60, 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-xs text-gray-500">
|
|
Range: 5 min - 24 hours
|
|
<br>Default: 1 hour (3600s)
|
|
<br>Current: <span class="font-medium"><%= application.id_token_ttl_human || "1 hour" %></span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<details class="mt-3">
|
|
<summary class="cursor-pointer text-sm text-blue-600 hover:text-blue-800">Understanding Token Types</summary>
|
|
<div class="mt-2 ml-4 space-y-2 text-sm text-gray-600">
|
|
<p><strong>Access Token:</strong> Used to access protected resources (APIs). Shorter lifetime = more secure. Users won't notice automatic refreshes.</p>
|
|
<p><strong>Refresh Token:</strong> Used to get new access tokens without re-authentication. Longer lifetime = better UX (less re-logins).</p>
|
|
<p><strong>ID Token:</strong> Contains user identity information (JWT). Should match access token lifetime in most cases.</p>
|
|
<p class="text-xs italic mt-2">💡 Tip: Banking apps use 5-15 min access tokens. Internal tools use 1-4 hours.</p>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Forward Auth-specific fields -->
|
|
<div id="forward-auth-fields" class="space-y-6 border-t border-gray-200 pt-6 <%= 'hidden' unless application.forward_auth? %>" data-application-form-target="forwardAuthFields">
|
|
<h3 class="text-base font-semibold text-gray-900">Forward Auth Configuration</h3>
|
|
|
|
<div>
|
|
<%= form.label :domain_pattern, "Domain Pattern", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= form.text_field :domain_pattern, 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: "*.example.com or app.example.com" %>
|
|
<p class="mt-1 text-sm text-gray-500">Domain pattern to match. Use * for wildcard subdomains (e.g., *.example.com matches app.example.com, api.example.com, etc.)</p>
|
|
</div>
|
|
|
|
<div data-controller="json-validator" data-json-validator-valid-class="border-green-500 focus:border-green-500 focus:ring-green-500" data-json-validator-invalid-class="border-red-500 focus:border-red-500 focus:ring-red-500" data-json-validator-valid-status-class="text-green-600" data-json-validator-invalid-status-class="text-red-600">
|
|
<%= form.label :headers_config, "Custom Headers Configuration (JSON)", class: "block text-sm font-medium text-gray-700" %>
|
|
<%= form.text_area :headers_config, value: (application.headers_config.present? && application.headers_config.any? ? JSON.pretty_generate(application.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"}',
|
|
data: {
|
|
action: "input->json-validator#validate blur->json-validator#format",
|
|
json_validator_target: "textarea"
|
|
} %>
|
|
<div class="mt-2 text-sm text-gray-600 space-y-1">
|
|
<div class="flex items-center justify-between">
|
|
<p class="font-medium">Optional: Customize header names sent to your application.</p>
|
|
<div class="flex items-center gap-2">
|
|
<button type="button" data-action="json-validator#format" class="text-xs bg-gray-100 hover:bg-gray-200 px-2 py-1 rounded">Format JSON</button>
|
|
<button type="button" data-action="json-validator#insertSample" data-json-sample='{"user": "Remote-User", "groups": "Remote-Groups", "email": "Remote-Email", "name": "Remote-Name", "admin": "Remote-Admin"}' class="text-xs bg-blue-100 hover:bg-blue-200 text-blue-700 px-2 py-1 rounded">Insert Example</button>
|
|
</div>
|
|
</div>
|
|
<p><strong>Default headers:</strong> X-Remote-User, X-Remote-Email, X-Remote-Name, X-Remote-Groups, X-Remote-Admin</p>
|
|
<div data-json-validator-target="status" class="text-xs font-medium"></div>
|
|
<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>
|
|
<%= form.label :group_ids, "Allowed Groups (Optional)", class: "block text-sm font-medium text-gray-700" %>
|
|
<div class="mt-2 space-y-2 max-h-48 overflow-y-auto border border-gray-200 rounded-md p-3">
|
|
<% if @available_groups.any? %>
|
|
<% @available_groups.each do |group| %>
|
|
<div class="flex items-center">
|
|
<%= check_box_tag "application[group_ids][]", group.id, application.allowed_groups.include?(group), class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" %>
|
|
<%= label_tag "application_group_ids_#{group.id}", group.name, class: "ml-2 text-sm text-gray-900" %>
|
|
<span class="ml-2 text-xs text-gray-500">(<%= pluralize(group.users.count, "member") %>)</span>
|
|
</div>
|
|
<% end %>
|
|
<% else %>
|
|
<p class="text-sm text-gray-500">No groups available. Create groups first to restrict access.</p>
|
|
<% end %>
|
|
</div>
|
|
<p class="mt-1 text-sm text-gray-500">If no groups are selected, all active users can access this application.</p>
|
|
</div>
|
|
|
|
<div class="flex items-center">
|
|
<%= form.check_box :active, class: "h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500" %>
|
|
<%= form.label :active, "Active", class: "ml-2 block text-sm text-gray-900" %>
|
|
</div>
|
|
|
|
<div class="flex gap-3">
|
|
<%= form.submit application.persisted? ? "Update Application" : "Create Application", class: "rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" %>
|
|
<%= link_to "Cancel", admin_applications_path, 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" %>
|
|
</div>
|
|
<% end %>
|
|
|