mirror of
https://github.com/ChrispyBacon-dev/DockFlare.git
synced 2026-04-26 10:50:43 +00:00
1052 lines
54 KiB
HTML
1052 lines
54 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Settings{% endblock %}
|
|
|
|
{% block content %}
|
|
<style>
|
|
html { scroll-behavior: smooth; }
|
|
section[id] { scroll-margin-top: 5.5rem; }
|
|
:target { scroll-margin-top: 5.5rem; }
|
|
</style>
|
|
|
|
<div class="flex flex-col sm:flex-row gap-8">
|
|
<aside class="w-full sm:w-1/3 md:w-1/4 flex-shrink-0">
|
|
<div class="sticky top-20">
|
|
<ul id="settings-left-nav" class="menu bg-base-200 rounded-box p-2">
|
|
<li><a href="{{ url_for('web.settings_page') }}#general-settings">General Settings</a></li>
|
|
<li><a href="{{ url_for('web.settings_page') }}#cloudflare-tunnels">All Cloudflare Tunnels</a></li>
|
|
<li><a href="{{ url_for('web.settings_page') }}#backup-restore">Backup & Restore</a></li>
|
|
<li><a href="{{ url_for('web.settings_page') }}#security">Security</a></li>
|
|
<li><a href="{{ url_for('web.settings_page') }}#security" class="pl-8 text-sm">OAuth Authentication</a></li>
|
|
<li><a href="{{ url_for('web.settings_page') }}#tunnel-agent-status">Tunnel & Agent Status</a></li>
|
|
</ul>
|
|
<div class="mt-4 px-2">
|
|
<button id="btn-check-version" class="btn btn-sm btn-outline w-full" type="button" title="Check DockFlare version">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 inline-block" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6 0a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
Check version
|
|
</button>
|
|
<div id="version-check-status" class="text-xs opacity-70 mt-2 hidden"></div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<main class="w-full sm:w-2/3 md:w-3/4">
|
|
<!-- 0. General Settings -->
|
|
<section id="general-settings" class="card bg-base-100 shadow-xl mb-8 sm:mb-12 transition-all duration-300 hover:shadow-2xl">
|
|
<div class="card-body">
|
|
<h2 class="card-title text-2xl sm:text-3xl border-b border-base-300 pb-3 mb-6">
|
|
General Settings
|
|
</h2>
|
|
<form method="POST" action="{{ url_for('web.settings_page') }}" class="space-y-4">
|
|
{{ settings_form.hidden_tag() }}
|
|
|
|
<div class="form-control w-full">
|
|
<label class="label" for="tunnel_name">
|
|
<span class="label-text">{{ settings_form.tunnel_name.label.text }}</span>
|
|
</label>
|
|
{{ settings_form.tunnel_name(class="input input-bordered w-full", **{'id': 'tunnel_name'}) }}
|
|
{% for error in settings_form.tunnel_name.errors %}
|
|
<label class="label">
|
|
<span class="label-text-alt text-error">{{ error }}</span>
|
|
</label>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<div class="form-control w-full">
|
|
<label class="label" for="cf_zone_id">
|
|
<span class="label-text">{{ settings_form.cf_zone_id.label.text }}</span>
|
|
</label>
|
|
{{ settings_form.cf_zone_id(class="input input-bordered w-full", **{'id': 'cf_zone_id'}) }}
|
|
<label class="label">
|
|
<span class="label-text-alt">Optional. The default Zone ID for new DNS records.</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-control w-full">
|
|
<label class="label" for="tunnel_dns_scan_zone_names">
|
|
<span class="label-text">{{ settings_form.tunnel_dns_scan_zone_names.label.text }}</span>
|
|
</label>
|
|
{{ settings_form.tunnel_dns_scan_zone_names(class="input input-bordered w-full", **{'id': 'tunnel_dns_scan_zone_names'}) }}
|
|
<label class="label">
|
|
<span class="label-text-alt">{{ settings_form.tunnel_dns_scan_zone_names.description }}</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-control w-full">
|
|
<label class="label" for="grace_period_seconds">
|
|
<span class="label-text">{{ settings_form.grace_period_seconds.label.text }}</span>
|
|
</label>
|
|
{{ settings_form.grace_period_seconds(class="input input-bordered w-full", **{'id': 'grace_period_seconds', 'type': 'number'}) }}
|
|
{% for error in settings_form.grace_period_seconds.errors %}
|
|
<label class="label">
|
|
<span class="label-text-alt text-error">{{ error }}</span>
|
|
</label>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<div class="card-actions pt-4">
|
|
{{ settings_form.submit_settings(class="btn btn-primary") }}
|
|
</div>
|
|
</form>
|
|
<div class="border-t border-base-300 mt-8 pt-8">
|
|
<h3 class="font-semibold text-lg mb-2">Update Cloudflare Credentials</h3>
|
|
<div role="alert" class="alert alert-info text-sm mb-4">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
|
<span>
|
|
Enter a new Account ID or API Token to update. For security, current values are not displayed.
|
|
<br>
|
|
Updating credentials require an DockFlare restart to take full effect.
|
|
</span>
|
|
</div>
|
|
<form method="POST" action="{{ url_for('web.settings_page') }}" class="space-y-4">
|
|
{{ cf_credentials_form.hidden_tag() }}
|
|
<div class="form-control">
|
|
<label class="label" for="cf_account_id">
|
|
<span class="label-text">Cloudflare Account ID</span>
|
|
</label>
|
|
{{ cf_credentials_form.cf_account_id(class="input input-bordered w-full", **{'id': 'cf_account_id', 'placeholder': 'Enter new 32-character Account ID'}) }}
|
|
{% for error in cf_credentials_form.cf_account_id.errors %}
|
|
<label class="label">
|
|
<span class="label-text-alt text-error">{{ error }}</span>
|
|
</label>
|
|
{% endfor %}
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label" for="cf_api_token">
|
|
<span class="label-text">Cloudflare API Token</span>
|
|
</label>
|
|
{{ cf_credentials_form.cf_api_token(class="input input-bordered w-full", **{'id': 'cf_api_token', 'placeholder': 'Enter new 40-character API Token'}) }}
|
|
{% for error in cf_credentials_form.cf_api_token.errors %}
|
|
<label class="label">
|
|
<span class="label-text-alt text-error">{{ error }}</span>
|
|
</label>
|
|
{% endfor %}
|
|
</div>
|
|
<div class="card-actions">
|
|
{{ cf_credentials_form.submit_cloudflare_credentials(class="btn btn-primary") }}
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 2. All Cloudflare Tunnels on Account Section -->
|
|
<section id="cloudflare-tunnels" class="card bg-base-100 shadow-xl mb-8 sm:mb-12 transition-all duration-300 hover:shadow-2xl">
|
|
<div class="card-body">
|
|
<h2 class="card-title text-2xl sm:text-3xl border-b border-base-300 pb-3 mb-6">All Cloudflare Tunnels on Account</h2>
|
|
{% if CF_ACCOUNT_ID_CONFIGURED %}
|
|
<p class="mb-4 text-sm opacity-70">
|
|
Displaying tunnels for Account ID: <code class="badge badge-ghost">{{ ACCOUNT_ID_FOR_DISPLAY | e }}</code>.
|
|
<br>
|
|
<em class="text-xs opacity-60">This list shows all tunnels on the account, not just the one managed by DockFlare. Click '+' to view associated DNS records.</em>
|
|
</p>
|
|
{% if all_account_tunnels is defined and all_account_tunnels %}
|
|
<div class="overflow-x-auto -mx-6 sm:-mx-8">
|
|
<table class="table table-sm w-full">
|
|
<thead>
|
|
<tr>
|
|
<th class="w-12">+/-</th>
|
|
<th>Tunnel Name</th>
|
|
<th>Tunnel ID</th>
|
|
<th>Status</th>
|
|
<th>Created At</th>
|
|
<th class="w-32">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for tunnel in all_account_tunnels %}
|
|
<tr>
|
|
<td class="text-center">
|
|
<button type="button" class="btn btn-xs btn-ghost btn-circle tunnel-dns-toggle"
|
|
data-tunnel-id="{{ tunnel.id | e }}" aria-expanded="false" aria-controls="dns-records-{{ tunnel.id | e }}" title="Toggle DNS records">
|
|
<svg class="w-4 h-4 expand-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
|
|
<svg class="w-4 h-4 collapse-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 12H6"></path></svg>
|
|
</button>
|
|
</td>
|
|
<td class="text-sm font-medium">{{ tunnel.name | e }}</td>
|
|
<td><code class="text-xs opacity-80">{{ tunnel.id | e }}</code></td>
|
|
<td>
|
|
{% set status_color = 'badge-success' if tunnel.status | lower == 'healthy' else ('badge-warning' if tunnel.status | lower == 'degraded' else ('badge-error' if tunnel.status | lower == 'down' else 'badge-ghost')) %}
|
|
<span class="badge {{ status_color }} badge-sm">{{ tunnel.status | capitalize | e }}</span>
|
|
</td>
|
|
<td class="text-xs opacity-70">{% if tunnel.created_at %}{{ tunnel.created_at.split('T')[0] | e }}{% else %}N/A{% endif %}</td>
|
|
<td class="text-xs">
|
|
<button type="button" class="btn btn-xs btn-error delete-tunnel-btn"
|
|
data-tunnel-id="{{ tunnel.id | e }}"
|
|
data-tunnel-name="{{ tunnel.name | e }}">
|
|
Delete
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
<tr class="dns-records-row hidden bg-base-200/30">
|
|
<td colspan="6">
|
|
<div id="dns-records-{{ tunnel.id | e }}" class="p-4 text-sm space-y-2">
|
|
<p class="opacity-60 italic">Click the '+' button to load DNS records for this tunnel.</p>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% elif all_account_tunnels is defined %}
|
|
<p class="text-center opacity-70 py-8">No Cloudflare Tunnels found for Account ID: <code class="badge badge-ghost">{{ ACCOUNT_ID_FOR_DISPLAY | e }}</code>.</p>
|
|
<p class="text-center text-xs opacity-60"><em>This could also mean an error occurred. Ensure your API Token has 'Account:Cloudflare Tunnel:Read' permission.</em></p>
|
|
{% else %}
|
|
<p class="alert alert-warning text-center py-8">Could not retrieve tunnel information.</p>
|
|
{% endif %}
|
|
{% else %}
|
|
<div role="alert" class="alert alert-warning">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><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>
|
|
<span>The <code>CF_ACCOUNT_ID</code> environment variable is not configured. This section cannot be displayed.</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</section>
|
|
|
|
<dialog id="delete_tunnel_modal" class="modal">
|
|
<div class="modal-box w-11/12 max-w-md bg-base-100/90 backdrop-blur">
|
|
<form method="dialog">
|
|
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" title="Close">✕</button>
|
|
</form>
|
|
<h3 class="font-bold text-lg mb-2">Delete Cloudflare Tunnel</h3>
|
|
<p id="delete_tunnel_warning_text" class="text-sm opacity-80 mb-4">
|
|
Deleting this tunnel will disconnect any agents currently using it.
|
|
</p>
|
|
<form id="delete_tunnel_form" action="{{ url_for('web.ui_delete_cloudflare_tunnel_route') }}" method="POST" class="space-y-4">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<input type="hidden" name="tunnel_id" id="delete_tunnel_id">
|
|
<label class="form-control w-full">
|
|
<span class="label-text text-sm">Type <span class="font-semibold">delete</span> to confirm</span>
|
|
<input type="text" name="confirm_text" id="delete_tunnel_confirm_input" class="input input-bordered w-full" autocomplete="off" required>
|
|
</label>
|
|
<div class="modal-action mt-4">
|
|
<button type="submit" id="delete_tunnel_confirm_button" class="btn btn-error" disabled>Delete Tunnel</button>
|
|
<button type="button" class="btn" onclick="document.getElementById('delete_tunnel_modal').close()">Cancel</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop"><button>Close</button></form>
|
|
</dialog>
|
|
|
|
<!-- 3. Backup & Restore Section -->
|
|
<section id="backup-restore" class="card bg-base-100 shadow-xl mb-8 sm:mb-12 transition-all duration-300 hover:shadow-2xl border-2 border-warning">
|
|
<div class="card-body">
|
|
<h2 class="card-title text-2xl sm:text-3xl border-b border-base-300 pb-3 mb-6">
|
|
Backup & Restore
|
|
</h2>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 items-start">
|
|
|
|
<!-- Backup Column -->
|
|
<div>
|
|
<h3 class="font-semibold text-lg mb-2">Create Backup</h3>
|
|
<p class="text-sm opacity-80 mb-4">
|
|
Download a full DockFlare backup archive (`.zip`) containing your encrypted configuration, agent keys, and state file. Store it safely with the `dockflare.key` included in the archive.
|
|
</p>
|
|
<a href="{{ url_for('web.download_state_backup') }}" class="btn btn-primary">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
|
</svg>
|
|
Download Backup Archive
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Restore Column -->
|
|
<div>
|
|
<h3 class="font-semibold text-lg mb-2">Restore from Backup</h3>
|
|
<div role="alert" class="alert alert-warning text-sm mb-4">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><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>
|
|
<span><strong>Warning:</strong> Restoring a backup overwrites configuration, credentials, agent keys, and state. This action cannot be undone.</span>
|
|
</div>
|
|
|
|
<form action="{{ url_for('web.restore_state_backup') }}" method="POST" enctype="multipart/form-data" class="protocol-aware-form space-y-3">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<input type="file" name="backup_file" class="file-input file-input-bordered w-full" accept=".zip,.json" required />
|
|
<label class="label">
|
|
<span class="label-text-alt text-sm">Upload a DockFlare backup archive (`.zip`). Legacy `state.json` files are still accepted but only restore rules/groups.</span>
|
|
</label>
|
|
<button type="submit" class="btn btn-error w-full" onclick="return confirm('Are you sure you want to overwrite your current settings with this backup? This cannot be undone.')">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
|
</svg>
|
|
Upload and Restore Backup
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 4. Security Section -->
|
|
<section id="security" class="card bg-base-100 shadow-xl mb-8 sm:mb-12 transition-all duration-300 hover:shadow-2xl">
|
|
<div class="card-body">
|
|
<h2 class="card-title text-2xl sm:text-3xl border-b border-base-300 pb-3 mb-6">
|
|
Security
|
|
</h2>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 items-start">
|
|
<div>
|
|
<h3 class="font-semibold text-lg mb-2">Change Password</h3>
|
|
<form method="POST" action="{{ url_for('web.change_password') }}" class="space-y-4">
|
|
{{ change_password_form.hidden_tag() }}
|
|
<div class="form-control">
|
|
<label class="label" for="current_password">
|
|
<span class="label-text">Current Password</span>
|
|
</label>
|
|
{{ change_password_form.current_password(class="input input-bordered w-full", **{'id': 'current_password'}) }}
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label" for="new_password">
|
|
<span class="label-text">New Password</span>
|
|
</label>
|
|
{{ change_password_form.new_password(class="input input-bordered w-full", **{'id': 'new_password'}) }}
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label" for="confirm_new_password">
|
|
<span class="label-text">Confirm New Password</span>
|
|
</label>
|
|
{{ change_password_form.confirm_new_password(class="input input-bordered w-full", **{'id': 'confirm_new_password'}) }}
|
|
</div>
|
|
<div class="card-actions">
|
|
<button type="submit" class="btn btn-primary">Change Password</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div>
|
|
<h3 class="font-semibold text-lg mb-2">Disable Password Login</h3>
|
|
<form method="POST" action="{{ url_for('web.settings_page') }}" class="space-y-4 protocol-aware-form">
|
|
{{ security_settings_form.hidden_tag() }}
|
|
|
|
<div class="form-control">
|
|
<label class="label cursor-pointer p-0">
|
|
<span class="label-text">{{ security_settings_form.disable_password_login.label.text }}</span>
|
|
{{ security_settings_form.disable_password_login(class="toggle toggle-primary") }}
|
|
</label>
|
|
</div>
|
|
|
|
<div role="alert" class="alert alert-warning text-sm">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><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>
|
|
<span><strong>Security Warning:</strong> When disabling password login, you become responsible for securing DockFlare access. Best practice is to use a Cloudflare Tunnel with an Access Policy and ensure Docker ports are not exposed, preventing access from the local network (LAN).</span>
|
|
</div>
|
|
|
|
<div class="card-actions">
|
|
{{ security_settings_form.submit_security_settings(class="btn btn-warning") }}
|
|
</div>
|
|
</form>
|
|
<div class="mt-6">
|
|
<h4 class="text-md font-semibold mb-2 opacity-80">Master API Key</h4>
|
|
<button type="button" class="btn btn-sm btn-outline" id="btn-show-master-key">Show Master API Key</button>
|
|
<span class="block text-xs opacity-70 mt-2">Keep this key secret; rotate it if exposed.</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- OAuth Authentication Section -->
|
|
<div class="mt-8">
|
|
<h3 class="font-semibold text-lg mb-4">OAuth Authentication</h3>
|
|
<p class="text-sm opacity-70 mb-4">Configure OAuth providers to allow users to log in with third-party services like Google, GitHub, or Microsoft.</p>
|
|
|
|
<!-- OAuth Settings Card -->
|
|
<div class="card bg-base-200 mb-6">
|
|
<div class="card-body">
|
|
<h4 class="card-title text-md">OAuth Settings</h4>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="form-control">
|
|
<label class="label" for="oauth_session_timeout">
|
|
<span class="label-text">Session Timeout (seconds)</span>
|
|
</label>
|
|
{{ security_settings_form.oauth_session_timeout(class="input input-bordered input-sm", **{'id': 'oauth_session_timeout'}) }}
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label cursor-pointer p-0">
|
|
<span class="label-text">{{ security_settings_form.oauth_audit_enabled.label.text }}</span>
|
|
{{ security_settings_form.oauth_audit_enabled(class="toggle toggle-primary toggle-sm") }}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- OAuth Providers Section -->
|
|
<div class="mb-6">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h4 class="text-md font-semibold">OAuth Providers</h4>
|
|
<button class="btn btn-primary btn-sm" onclick="showAddProviderModal()">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
|
</svg>
|
|
Add Provider
|
|
</button>
|
|
</div>
|
|
<div role="alert" class="alert alert-warning text-sm mb-4">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-5 w-5" fill="none" viewBox="0 0 24 24">
|
|
<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>
|
|
<span><strong>Security:</strong> OAuth providers you configure here will be trusted to authenticate users. Only add providers you control or fully trust.</span>
|
|
</div>
|
|
<div id="provider-list" class="space-y-3">
|
|
<div class="text-center text-sm opacity-60 py-8">
|
|
No OAuth providers configured. Click "Add Provider" to get started.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Authorized Users Section -->
|
|
<div class="mb-6">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h4 class="text-md font-semibold">Authorized Users</h4>
|
|
<button class="btn btn-secondary btn-sm" onclick="showAddUserModal()">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
|
</svg>
|
|
Add User
|
|
</button>
|
|
</div>
|
|
<p class="text-sm opacity-70 mb-4">Only users with email addresses listed here will be allowed to authenticate via OAuth.</p>
|
|
<div id="user-list" class="space-y-3">
|
|
<div class="text-center text-sm opacity-60 py-8">
|
|
No authorized users configured. Click "Add User" to authorize email addresses.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-8">
|
|
<h3 class="font-semibold text-lg mb-2">Password Reset</h3>
|
|
<div role="alert" class="alert alert-info text-sm">
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
|
<span>
|
|
To reset your password, you must have filesystem access to the DockFlare container.
|
|
<br>
|
|
1. Stop the DockFlare container.
|
|
<br>
|
|
2. Delete the `dockflare_config.dat` and `dockflare.key` files from your persistent data volume.
|
|
<br>
|
|
3. Restart the container. You will be prompted to go through the initial setup again.
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- 5. Tunnel & Agent Status Section -->
|
|
<section id="tunnel-agent-status" class="card bg-base-100 shadow-xl mb-8 sm:mb-12 transition-all duration-300 hover:shadow-2xl">
|
|
<div class="card-body">
|
|
<h2 class="card-title text-2xl sm:text-3xl border-b border-base-300 pb-3 mb-6">
|
|
Tunnel & Agent Status
|
|
</h2>
|
|
|
|
{% if agent_state.last_action_status or tunnel_state.get('error') %}
|
|
{% set alert_message = agent_state.last_action_status or tunnel_state.status_message %}
|
|
{% set is_error = 'Error:' in alert_message or tunnel_state.get('error') %}
|
|
{% set alert_type = 'alert-error' if is_error else 'alert-info' %}
|
|
<div role="alert" class="alert {{ alert_type }} shadow-sm text-sm mb-6">
|
|
<!-- ... (alert content) ... -->
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="mb-6">
|
|
<h4 class="text-md font-semibold mb-3 opacity-80">Tunnel Details</h4>
|
|
<div class="space-y-2 text-sm">
|
|
<p><strong class="inline-block w-36 opacity-70">Desired Name:</strong> <code class="badge badge-ghost">{{ tunnel_state.name }}</code></p>
|
|
<p><strong class="inline-block w-36 opacity-70">Tunnel ID:</strong> <code class="badge badge-ghost">{{ tunnel_state.id if tunnel_state.id else 'N/A' }}</code></p>
|
|
{% if not external_cloudflared %}
|
|
<p><strong class="inline-block w-36 opacity-70">Tunnel Token:</strong> <code class="badge badge-ghost">{{ display_token }}</code></p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-lg font-semibold mb-3 opacity-80">Agent Control</h3>
|
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 sm:gap-6 mt-2 p-4 bg-base-200/50 rounded-md">
|
|
{% if external_cloudflared %}
|
|
<div class="flex-grow space-y-1.5">
|
|
<strong class="text-sm font-medium">External Mode:</strong> <span class="badge badge-info badge-sm animate-pulse">Active</span>
|
|
</div>
|
|
{% else %}
|
|
<div class="flex-grow space-y-1.5">
|
|
<strong class="text-sm font-medium">Agent Status:</strong>
|
|
{% set indicator_color = 'bg-gray-400' %}
|
|
{% if agent_state.container_status == 'running' %} {% set indicator_color = 'badge-success' %}
|
|
{% elif agent_state.container_status in ['exited', 'dead', 'not_found'] %} {% set indicator_color = 'badge-error' %}
|
|
{% else %} {% set indicator_color = 'badge-warning' %} {% endif %}
|
|
<span class="badge {{ indicator_color }} badge-sm">{{ agent_state.container_status | replace('_',' ') }}</span>
|
|
</div>
|
|
<div class="w-full sm:w-auto flex-shrink-0">
|
|
{% if agent_state.container_status=='running' %}
|
|
<form action="{{ url_for('web.stop_tunnel_route') }}" method="post" class="protocol-aware-form">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="btn btn-sm btn-error w-full sm:w-auto">Stop Agent</button>
|
|
</form>
|
|
{% else %}
|
|
<form action="{{ url_for('web.start_tunnel_route') }}" method="post" class="protocol-aware-form">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="btn btn-sm btn-success w-full sm:w-auto">Start Agent</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</div>
|
|
|
|
<dialog id="master-key-modal" class="modal">
|
|
<div class="modal-box max-w-xl">
|
|
<h3 class="font-bold text-lg mb-2">Master API Key</h3>
|
|
<p class="text-sm opacity-70 mb-4">Treat this key like a password. Anyone with it can call the DockFlare API.</p>
|
|
<div class="bg-base-200 rounded-md p-4">
|
|
<code id="master-key-display" class="break-all text-sm">Loading...</code>
|
|
</div>
|
|
<div class="modal-action mt-6 gap-2">
|
|
<button type="button" class="btn btn-sm" id="btn-copy-master-key">Copy</button>
|
|
<button type="button" class="btn btn-sm btn-primary" id="btn-close-master-key">Close</button>
|
|
</div>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
|
</dialog>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
{{ super() }}
|
|
<script>
|
|
(function () {
|
|
|
|
const showMasterKeyBtn = document.getElementById('btn-show-master-key');
|
|
if (showMasterKeyBtn) {
|
|
showMasterKeyBtn.addEventListener('click', async () => {
|
|
try {
|
|
const response = await fetch("{{ url_for('web.reveal_master_key') }}", {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': '{{ csrf_token() }}'
|
|
},
|
|
credentials: 'same-origin',
|
|
body: JSON.stringify({})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorPayload = await response.json().catch(() => ({}));
|
|
const msg = errorPayload.message || 'Unable to reveal the master key. Check logs.';
|
|
alert(`Error: ${msg}`);
|
|
return;
|
|
}
|
|
|
|
const payload = await response.json();
|
|
const modal = document.getElementById('master-key-modal');
|
|
const keyDisplay = document.getElementById('master-key-display');
|
|
if (modal && keyDisplay) {
|
|
keyDisplay.textContent = payload.master_api_key || 'Unavailable';
|
|
modal.showModal();
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to fetch master API key:', err);
|
|
alert('Failed to contact the server.');
|
|
}
|
|
});
|
|
}
|
|
|
|
const closeMasterKeyBtn = document.getElementById('btn-close-master-key');
|
|
if (closeMasterKeyBtn) {
|
|
closeMasterKeyBtn.addEventListener('click', () => {
|
|
const modal = document.getElementById('master-key-modal');
|
|
if (modal) {
|
|
modal.close();
|
|
}
|
|
});
|
|
}
|
|
|
|
const copyMasterKeyBtn = document.getElementById('btn-copy-master-key');
|
|
if (copyMasterKeyBtn) {
|
|
copyMasterKeyBtn.addEventListener('click', async () => {
|
|
const keyDisplay = document.getElementById('master-key-display');
|
|
if (!keyDisplay) return;
|
|
try {
|
|
if (!navigator.clipboard) {
|
|
throw new Error('clipboard_unavailable');
|
|
}
|
|
await navigator.clipboard.writeText(keyDisplay.textContent || '');
|
|
copyMasterKeyBtn.textContent = 'Copied!';
|
|
setTimeout(() => { copyMasterKeyBtn.textContent = 'Copy'; }, 2000);
|
|
} catch (err) {
|
|
console.error('Clipboard copy failed', err);
|
|
alert('Could not copy to clipboard.');
|
|
}
|
|
});
|
|
}
|
|
|
|
try {
|
|
const nav = document.getElementById('settings-left-nav');
|
|
if (!nav) return;
|
|
|
|
const links = Array.from(nav.querySelectorAll('a[href^="#"]'));
|
|
const idToLink = {};
|
|
links.forEach(a => {
|
|
const id = a.getAttribute('href').slice(1);
|
|
idToLink[id] = a;
|
|
});
|
|
|
|
const sections = links.map(a => document.getElementById(a.getAttribute('href').slice(1))).filter(Boolean);
|
|
|
|
if ('IntersectionObserver' in window && sections.length) {
|
|
const observer = new IntersectionObserver(entries => {
|
|
entries.forEach(entry => {
|
|
const id = entry.target.id;
|
|
const link = idToLink[id];
|
|
if (!link) return;
|
|
if (entry.isIntersecting && entry.intersectionRatio >= 0.25) {
|
|
links.forEach(l => l.classList.remove('active'));
|
|
link.classList.add('active');
|
|
}
|
|
});
|
|
}, { threshold: [0.25, 0.5, 0.75] });
|
|
|
|
sections.forEach(s => observer.observe(s));
|
|
} else {
|
|
|
|
function onScroll() {
|
|
const scrollY = window.scrollY || window.pageYOffset;
|
|
let best = null;
|
|
sections.forEach(s => {
|
|
const rect = s.getBoundingClientRect();
|
|
const offset = Math.abs(rect.top - 100);
|
|
if (best === null || offset < best.offset) {
|
|
best = { el: s, offset };
|
|
}
|
|
});
|
|
if (best) {
|
|
links.forEach(l => l.classList.remove('active'));
|
|
const link = idToLink[best.el.id];
|
|
if (link) link.classList.add('active');
|
|
}
|
|
}
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
|
|
|
setTimeout(onScroll, 200);
|
|
}
|
|
|
|
|
|
links.forEach(a => {
|
|
a.addEventListener('click', (e) => {
|
|
|
|
const href = a.getAttribute('href');
|
|
const target = document.querySelector(href);
|
|
if (target) {
|
|
|
|
setTimeout(() => {
|
|
target.setAttribute('tabindex', '-1');
|
|
target.focus({ preventScroll: true });
|
|
}, 300);
|
|
}
|
|
});
|
|
});
|
|
} catch (err) {
|
|
console.debug('Settings left-nav script error', err);
|
|
}
|
|
})();
|
|
</script>
|
|
|
|
<script>
|
|
(function () {
|
|
try {
|
|
const btn = document.getElementById('btn-check-version');
|
|
const status = document.getElementById('version-check-status');
|
|
if (!btn || !status) return;
|
|
|
|
function renderMessage(html) {
|
|
status.innerHTML = html;
|
|
status.classList.remove('hidden');
|
|
}
|
|
|
|
btn.addEventListener('click', async function () {
|
|
btn.disabled = true;
|
|
const originalHtml = btn.innerHTML;
|
|
btn.innerHTML = '<span class="loading loading-spinner"></span> Checking...';
|
|
status.classList.add('hidden');
|
|
|
|
try {
|
|
const resp = await fetch('{{ url_for("web.version_check") }}', { credentials: 'same-origin' });
|
|
if (!resp.ok) throw new Error('Network response was not ok');
|
|
const data = await resp.json();
|
|
|
|
if (data.method === 'digest') {
|
|
if (data.up_to_date) {
|
|
renderMessage('<span class="text-success">Up to date — running the same image as {{ config.CLOUDFLARED_IMAGE if false else "" }}</span>');
|
|
} else {
|
|
let repoPart = data.repo ? `${data.repo}:${data.tag || 'stable'}` : '';
|
|
let cmd = repoPart ? `docker pull ${data.repo}:${data.tag || 'stable'} && docker compose restart dockflare` : 'docker pull alplat/dockflare:stable && docker compose restart dockflare';
|
|
let pushedAt = '';
|
|
if (data.remote_pushed_at) {
|
|
try {
|
|
const d = new Date(data.remote_pushed_at);
|
|
pushedAt = `<div class="mt-1 text-xs">Latest version from: ${d.toLocaleString()}</div>`;
|
|
} catch(e) {}
|
|
}
|
|
renderMessage(`<span class="text-warning">Update available</span><div class="mt-1">Run: <code class="text-xs">${cmd}</code></div>${pushedAt}`);
|
|
}
|
|
} else if (data.method === 'version') {
|
|
if (data.up_to_date) {
|
|
renderMessage('<span class="text-success">Up to date (APP_VERSION matches latest release).</span>');
|
|
} else if (data.latest) {
|
|
renderMessage(`<span class="text-warning">Latest: ${data.latest} · Current: ${data.current}</span><div class="mt-1">See the <a href="https://github.com/ChrispyBacon-dev/DockFlare/releases" target="_blank" rel="noopener noreferrer">release notes</a> for upgrade instructions.</div>`);
|
|
} else {
|
|
renderMessage('<span class="text-muted">Could not determine latest version.</span>');
|
|
}
|
|
} else if (data.error) {
|
|
renderMessage(`<span class="text-error">Version check error: ${String(data.error)}</span>`);
|
|
} else {
|
|
renderMessage('<span class="text-muted">Could not determine version status.</span>');
|
|
}
|
|
} catch (err) {
|
|
console.debug('Version check failed', err);
|
|
renderMessage('<span class="text-error">Version check failed (network or server error).</span>');
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.innerHTML = originalHtml;
|
|
}
|
|
});
|
|
} catch (e) {
|
|
console.debug('Version check script error', e);
|
|
}
|
|
})();
|
|
|
|
(function() {
|
|
let authData = null;
|
|
|
|
async function loadAuthSettings() {
|
|
try {
|
|
const response = await fetch('/api/v2/auth/settings', {
|
|
headers: {
|
|
'Authorization': `Bearer ${window.masterApiKey || ''}`,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
authData = await response.json();
|
|
renderProviders();
|
|
renderUsers();
|
|
} else {
|
|
console.error('Failed to load auth settings');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading auth settings:', error);
|
|
}
|
|
}
|
|
|
|
function renderProviders() {
|
|
const providerList = document.getElementById('provider-list');
|
|
if (!authData || !authData.providers || authData.providers.length === 0) {
|
|
providerList.innerHTML = '<div class="text-center text-sm opacity-60 py-8">No OAuth providers configured. Click "Add Provider" to get started.</div>';
|
|
return;
|
|
}
|
|
|
|
providerList.innerHTML = authData.providers.map(provider => `
|
|
<div class="card bg-base-100 border">
|
|
<div class="card-body p-4">
|
|
<div class="flex justify-between items-center">
|
|
<div class="flex items-center space-x-3">
|
|
<div class="badge badge-${provider.enabled ? 'success' : 'warning'}">${provider.enabled ? 'Enabled' : 'Disabled'}</div>
|
|
<div>
|
|
<h5 class="font-semibold">${escapeHtml(provider.name)}</h5>
|
|
<p class="text-sm opacity-70">${escapeHtml(provider.type)} · ID: ${escapeHtml(provider.id)}</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex space-x-2">
|
|
<button class="btn btn-sm btn-outline" onclick="editProvider('${escapeHtml(provider.id)}')">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
|
</svg>
|
|
</button>
|
|
<button class="btn btn-sm btn-error" onclick="deleteProvider('${escapeHtml(provider.id)}')">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function renderUsers() {
|
|
const userList = document.getElementById('user-list');
|
|
if (!authData || !authData.users || authData.users.length === 0) {
|
|
userList.innerHTML = '<div class="text-center text-sm opacity-60 py-8">No authorized users configured. Click "Add User" to authorize email addresses.</div>';
|
|
return;
|
|
}
|
|
|
|
userList.innerHTML = authData.users.map(user => `
|
|
<div class="card bg-base-100 border">
|
|
<div class="card-body p-4">
|
|
<div class="flex justify-between items-center">
|
|
<div>
|
|
<h5 class="font-semibold">${escapeHtml(user.email)}</h5>
|
|
${user.name ? `<p class="text-sm opacity-70">${escapeHtml(user.name)}</p>` : ''}
|
|
${user.added_date ? `<p class="text-xs opacity-50">Added: ${new Date(user.added_date).toLocaleDateString()}</p>` : ''}
|
|
</div>
|
|
<button class="btn btn-sm btn-error" onclick="deleteUser('${escapeHtml(user.email)}')">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function escapeHtml(unsafe) {
|
|
return unsafe
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
window.showAddProviderModal = function() {
|
|
document.getElementById('addProviderModal').showModal();
|
|
};
|
|
|
|
window.showAddUserModal = function() {
|
|
document.getElementById('addUserModal').showModal();
|
|
};
|
|
|
|
window.addProvider = async function() {
|
|
const form = document.getElementById('addProviderForm');
|
|
const formData = new FormData(form);
|
|
|
|
const providerData = {
|
|
id: formData.get('provider_id'),
|
|
name: formData.get('provider_name'),
|
|
type: formData.get('provider_type'),
|
|
client_id: formData.get('client_id'),
|
|
client_secret: formData.get('client_secret'),
|
|
enabled: formData.get('enabled') === 'on'
|
|
};
|
|
|
|
try {
|
|
const response = await fetch('/api/v2/auth/providers', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${window.masterApiKey || ''}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(providerData)
|
|
});
|
|
|
|
if (response.ok) {
|
|
document.getElementById('addProviderModal').close();
|
|
form.reset();
|
|
await loadAuthSettings();
|
|
showToast('Provider added successfully!', 'success');
|
|
} else {
|
|
const error = await response.json();
|
|
showToast(error.message || 'Failed to add provider', 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('Error adding provider', 'error');
|
|
}
|
|
};
|
|
|
|
window.addUser = async function() {
|
|
const form = document.getElementById('addUserForm');
|
|
const formData = new FormData(form);
|
|
|
|
const userData = {
|
|
email: formData.get('user_email'),
|
|
name: formData.get('user_name')
|
|
};
|
|
|
|
try {
|
|
const response = await fetch('/api/v2/auth/users', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${window.masterApiKey || ''}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(userData)
|
|
});
|
|
|
|
if (response.ok) {
|
|
document.getElementById('addUserModal').close();
|
|
form.reset();
|
|
await loadAuthSettings();
|
|
showToast('User added successfully!', 'success');
|
|
} else {
|
|
const error = await response.json();
|
|
showToast(error.message || 'Failed to add user', 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('Error adding user', 'error');
|
|
}
|
|
};
|
|
|
|
window.deleteProvider = async function(providerId) {
|
|
if (!confirm('Are you sure you want to delete this OAuth provider?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/v2/auth/providers/${providerId}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Authorization': `Bearer ${window.masterApiKey || ''}`,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
await loadAuthSettings();
|
|
showToast('Provider deleted successfully!', 'success');
|
|
} else {
|
|
showToast('Failed to delete provider', 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('Error deleting provider', 'error');
|
|
}
|
|
};
|
|
|
|
window.deleteUser = async function(userEmail) {
|
|
if (!confirm('Are you sure you want to remove this authorized user?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/v2/auth/users/${encodeURIComponent(userEmail)}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'Authorization': `Bearer ${window.masterApiKey || ''}`,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (response.ok) {
|
|
await loadAuthSettings();
|
|
showToast('User removed successfully!', 'success');
|
|
} else {
|
|
showToast('Failed to remove user', 'error');
|
|
}
|
|
} catch (error) {
|
|
showToast('Error removing user', 'error');
|
|
}
|
|
};
|
|
|
|
function showToast(message, type) {
|
|
const toast = document.createElement('div');
|
|
toast.className = `toast toast-top toast-end`;
|
|
toast.innerHTML = `
|
|
<div class="alert alert-${type}">
|
|
<span>${message}</span>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(toast);
|
|
setTimeout(() => toast.remove(), 3000);
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
if (window.masterApiKey) {
|
|
loadAuthSettings();
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
|
|
<!-- Add Provider Modal -->
|
|
<dialog id="addProviderModal" class="modal">
|
|
<div class="modal-box">
|
|
<h3 class="font-bold text-lg">Add OAuth Provider</h3>
|
|
<form id="addProviderForm" class="space-y-4 mt-4">
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Provider Type</span>
|
|
</label>
|
|
<select name="provider_type" class="select select-bordered" required>
|
|
<option value="">Select Provider Type</option>
|
|
<option value="google">Google</option>
|
|
<option value="github">GitHub</option>
|
|
<option value="microsoft">Microsoft</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Provider ID</span>
|
|
</label>
|
|
<input name="provider_id" type="text" placeholder="e.g. google, github-corp" class="input input-bordered" required />
|
|
<label class="label">
|
|
<span class="label-text-alt">Unique identifier for this provider</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Display Name</span>
|
|
</label>
|
|
<input name="provider_name" type="text" placeholder="e.g. Google, GitHub Corporate" class="input input-bordered" required />
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Client ID</span>
|
|
</label>
|
|
<input name="client_id" type="text" placeholder="OAuth Client ID" class="input input-bordered" required />
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Client Secret</span>
|
|
</label>
|
|
<input name="client_secret" type="password" placeholder="OAuth Client Secret" class="input input-bordered" required />
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label cursor-pointer">
|
|
<span class="label-text">Enable Provider</span>
|
|
<input name="enabled" type="checkbox" class="toggle" checked />
|
|
</label>
|
|
</div>
|
|
</form>
|
|
|
|
<div class="modal-action">
|
|
<form method="dialog">
|
|
<button class="btn">Cancel</button>
|
|
</form>
|
|
<button class="btn btn-primary" onclick="addProvider()">Add Provider</button>
|
|
</div>
|
|
</div>
|
|
</dialog>
|
|
|
|
<!-- Add User Modal -->
|
|
<dialog id="addUserModal" class="modal">
|
|
<div class="modal-box">
|
|
<h3 class="font-bold text-lg">Add Authorized User</h3>
|
|
<form id="addUserForm" class="space-y-4 mt-4">
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Email Address</span>
|
|
</label>
|
|
<input name="user_email" type="email" placeholder="user@example.com" class="input input-bordered" required />
|
|
<label class="label">
|
|
<span class="label-text-alt">This email must match exactly with the OAuth provider</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Display Name (Optional)</span>
|
|
</label>
|
|
<input name="user_name" type="text" placeholder="John Doe" class="input input-bordered" />
|
|
</div>
|
|
</form>
|
|
|
|
<div class="modal-action">
|
|
<form method="dialog">
|
|
<button class="btn">Cancel</button>
|
|
</form>
|
|
<button class="btn btn-primary" onclick="addUser()">Add User</button>
|
|
</div>
|
|
</div>
|
|
</dialog>
|
|
|
|
{% endblock %}
|