DockFlare/dockflare/app/templates/access_policies.html
ChrispyBacon-dev b05aa8689e
Some checks failed
Docker Image Build and Push / build_self_hosted (push) Has been cancelled
Docker Image Build and Push / build_github_hosted_fallback (push) Has been cancelled
bugfix for friendly names user access policy
2025-10-06 19:33:01 +02:00

996 lines
56 KiB
HTML

{% extends "base.html" %}
{% block title %}Access Policies{% endblock %}
{% block content %}
<!-- 1. Access Groups Section -->
<section class="card bg-base-100 shadow-xl mb-8 sm:mb-12 transition-all duration-300 hover:shadow-2xl">
<div class="card-body overflow-visible">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center border-b border-base-300 pb-4 mb-4">
<div>
<h2 class="card-title text-2xl sm:text-3xl">
Advanced Access Policies
{% if ACCOUNT_ID_FOR_DISPLAY and ACCOUNT_ID_FOR_DISPLAY != "Not Configured" %}
<a href="https://one.dash.cloudflare.com/{{ ACCOUNT_ID_FOR_DISPLAY }}/access/apps" target="_blank" rel="noopener noreferrer" title="View Access Policies in Cloudflare Zero Trust" class="ml-2 inline-block align-middle transition-transform hover:scale-105">
<img src="{{ url_for('static', filename='images/cloudflare-icon.svg') }}" alt="Cloudflare" class="inline h-5 w-5" />
<span class="sr-only">Open in Cloudflare Zero Trust</span>
</a>
{% endif %}
</h2>
<p class="text-sm opacity-70 mt-1">Create reusable access policies to apply with a single label.</p>
<div class="flex items-center gap-2 mt-2">
<label class="text-xs opacity-60">Filter:</label>
<select id="policyFilter" class="select select-xs select-bordered">
<option value="all">All Policies</option>
<option value="dockflare">DockFlare-Managed</option>
<option value="external">External</option>
<option value="system">System</option>
</select>
</div>
</div>
<div class="flex gap-2 mt-4 sm:mt-0">
<button id="sync-cloudflare-btn" class="btn btn-sm btn-secondary">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" /></svg>
Sync from Cloudflare
</button>
<button id="create-access-group-btn" class="btn btn-sm btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
Create New Group
</button>
</div>
</div>
{% if access_groups and access_groups.items() %}
<div class="overflow-x-auto table-container">
<table class="table table-zebra policy-table w-full" id="access-groups-table">
<colgroup>
<col class="col-primary">
<col class="col-secondary">
<col class="col-tertiary">
<col class="col-status">
<col class="col-actions">
</colgroup>
<thead>
<tr>
<th class="px-4 py-3">Display Name</th>
<th class="px-4 py-3">Group ID (for label)</th>
<th class="px-4 py-3">Policy Summary</th>
<th class="px-4 py-3">Policy Type</th>
<th class="px-4 py-3 text-right">Actions</th>
</tr>
</thead>
<tbody>
{% for group_id, details in access_groups.items()|sort %}
{% set policy_type_label = 'dockflare' %}
{% if details.external_policy %}
{% set policy_type_label = 'external' %}
{% elif details.system_policy %}
{% set policy_type_label = 'system' %}
{% endif %}
<tr data-policy-type="{{ policy_type_label }}" data-group-id="{{ group_id }}">
<td class="px-4 py-3 cell-top">
<div class="font-medium flex items-center gap-2">
{{ details.display_name }}
</div>
{% if group_id in group_usage %}
<button class="btn btn-xs btn-ghost gap-1 mt-1 usage-toggle" data-group-id="{{ group_id }}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3 h-3">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
</svg>
In use by {{ group_usage[group_id]|length }} service{{ '' if group_usage[group_id]|length == 1 else 's' }}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3 h-3 chevron-icon">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</button>
{% endif %}
</td>
<td class="px-4 py-3 cell-top"><code class="badge badge-sm badge-outline">{{ group_id }}</code></td>
<td class="px-4 py-3 text-xs opacity-80 cell-top">
{% if details.policies %}
{{ details.policies | length }} rule(s) defined
{% else %}
<span class="italic opacity-60">No rules</span>
{% endif %}
</td>
<td class="px-4 py-3">
{% if details.external_policy %}
<span class="badge badge-sm badge-secondary df-badge">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" /></svg>
External
</span>
{% elif details.system_policy %}
<span class="badge badge-sm badge-success df-badge">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" /></svg>
System
</span>
{% else %}
<span class="badge badge-sm badge-warning df-badge">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" /></svg>
DockFlare
</span>
{% endif %}
</td>
<td class="px-4 py-3 text-right align-top">
<div class="dropdown dropdown-end">
<label tabindex="0" class="btn btn-ghost btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 12.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 18.75a.75.75 0 110-1.5.75.75 0 010 1.5z" />
</svg>
</label>
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-64">
<li>
<a class="edit-access-group-btn" data-group-id="{{ group_id }}" data-group-details="{{ details|tojson|forceescape }}" data-external="{{ 'true' if details.get('external_policy') else 'false' }}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10" /></svg>
Edit
</a>
</li>
{% if details.cloudflare_policy_id and ACCOUNT_ID_FOR_DISPLAY and ACCOUNT_ID_FOR_DISPLAY != "Not Configured" %}
<li>
<a class="flex items-center gap-2" href="https://one.dash.cloudflare.com/{{ ACCOUNT_ID_FOR_DISPLAY }}/access/policies/{{ details.cloudflare_policy_id }}/edit" target="_blank" rel="noopener noreferrer">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" /></svg>
View in Cloudflare
</a>
</li>
{% endif %}
<div class="divider my-1"></div>
{% set is_disabled = details.get('system_policy') or not details.get('deletable', True) or group_id in used_group_ids %}
<li class="{{ 'disabled' if is_disabled else '' }}">
<a class="text-error delete-access-group-btn {{ 'pointer-events-none cursor-not-allowed select-none' if is_disabled else '' }}" data-group-id="{{ group_id }}" data-group-name="{{ details.display_name }}" data-external="{{ 'true' if details.get('external_policy') else 'false' }}" data-disabled="{{ 'true' if is_disabled else 'false' }}" {{ 'title="Cannot delete: system policy"' if details.get('system_policy') or not details.get('deletable', True) else ('title="Cannot delete: group is in use"' if group_id in used_group_ids else '') }}>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" /></svg>
{% if details.get('system_policy') or not details.get('deletable', True) %}
<span class="opacity-50">Delete (system policy)</span>
{% elif group_id in used_group_ids %}
<span class="opacity-50">Delete (policy is in use)</span>
{% else %}
Delete
{% endif %}
</a>
</li>
{% if group_id in used_group_ids %}
<div class="divider my-1"></div>
<li class="menu-title">
<span class="text-xs opacity-70">Used by {{ group_usage[group_id]|length }} service{{ '' if group_usage[group_id]|length == 1 else 's' }}</span>
</li>
{% endif %}
</ul>
</div>
</td>
</tr>
{% if group_id in group_usage %}
<tr class="usage-details-row" id="usage-{{ group_id }}" data-group-id="{{ group_id }}" style="display: none;">
<td colspan="5" class="p-0">
<div class="bg-base-200/50 p-4 border-l-4 border-info">
<div class="text-sm font-semibold mb-2 opacity-70">
Used by these services:
</div>
<ul class="list-disc list-inside space-y-1 text-sm opacity-80">
{% for hostname in group_usage[group_id] %}
<li><code class="text-xs">{{ hostname }}</code></li>
{% endfor %}
</ul>
</div>
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center opacity-70 py-8">
<p>No Access Groups have been created yet.</p>
<p class="mt-2 text-sm">Click "Create New Group" to get started.</p>
</div>
{% endif %}
</div>
</section>
<!-- 2. Identity Providers Section -->
<section class="card bg-base-100 shadow-xl mb-8 sm:mb-12 transition-all duration-300 hover:shadow-2xl">
<div class="card-body overflow-visible">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center border-b border-base-300 pb-4 mb-4">
<div>
<h2 class="card-title text-2xl sm:text-3xl">
Identity Providers
{% if ACCOUNT_ID_FOR_DISPLAY and ACCOUNT_ID_FOR_DISPLAY != "Not Configured" %}
<a href="https://one.dash.cloudflare.com/{{ ACCOUNT_ID_FOR_DISPLAY }}/settings/authentication" target="_blank" rel="noopener noreferrer" title="View Identity Providers in Cloudflare Zero Trust" class="ml-2 inline-block align-middle transition-transform hover:scale-105">
<img src="{{ url_for('static', filename='images/cloudflare-icon.svg') }}" alt="Cloudflare" class="inline h-5 w-5" />
<span class="sr-only">Open in Cloudflare Zero Trust</span>
</a>
{% endif %}
</h2>
<p class="text-sm opacity-70 mt-1">Configure OAuth/OIDC providers for Zero Trust authentication.</p>
</div>
<div class="flex gap-2 mt-4 sm:mt-0">
<button id="sync-idps-btn" class="btn btn-sm btn-secondary">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" /></svg>
Sync from Cloudflare
</button>
<button id="create-idp-btn" class="btn btn-sm btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>
Add Provider
</button>
</div>
</div>
<div id="idp-table-container">
<p class="text-center opacity-70 py-8">Loading identity providers...</p>
</div>
</div>
</section>
<!-- 3. Zone Default Policies Section -->
<section class="card bg-base-100 shadow-xl mb-8 sm:mb-12 transition-all duration-300 hover:shadow-2xl">
<div class="card-body overflow-visible">
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center border-b border-base-300 pb-4 mb-4">
<div>
<h2 class="card-title text-2xl sm:text-3xl">
Zone Default Policies (*.tld Wildcards)
</h2>
<p class="text-sm opacity-70 mt-1">Protect all subdomains of your zones with a wildcard <code>*.domain.com</code> access policy.</p>
</div>
</div>
<div id="zone-policies-container">
<div class="text-center opacity-70 py-8">
<span class="loading loading-spinner loading-lg"></span>
<p class="mt-4">Loading zone policies...</p>
</div>
</div>
</div>
</section>
{% endblock %}
{% block modals %}
{{ super() }}
{% include 'modals/_access_group_modal.html' %}
<!-- Zone Default Policy Creation Modal -->
<dialog id="zone-policy-modal" class="modal">
<div class="modal-box max-w-2xl">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form>
<h3 class="font-bold text-lg mb-4">Create Zone Default Policy</h3>
<form method="POST" action="{{ url_for('web.create_zone_default_policy') }}" id="zone-policy-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="zone_name" id="zone-policy-zone-name"/>
<input type="hidden" name="zone_id" id="zone-policy-zone-id"/>
<div class="space-y-4">
<div class="alert alert-info">
<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>This will create a wildcard Access Application for <strong><code id="zone-policy-hostname-display"></code></strong> to protect all subdomains.</span>
</div>
<div class="form-control">
<label class="label" for="zone-policy-access-group">
<span class="label-text font-medium">Select Access Policy</span>
</label>
<select name="access_group_id" id="zone-policy-access-group" class="select select-bordered w-full" required>
<option value="" disabled selected>-- Select an Access Policy --</option>
{% for group_id, details in access_groups.items()|sort %}
<option value="{{ group_id }}">{{ details.display_name }}</option>
{% endfor %}
</select>
<div class="label">
<span class="label-text-alt">The access policy that will protect <code>*.zone.com</code></span>
</div>
</div>
</div>
<div class="modal-action mt-6">
<button type="button" class="btn btn-ghost" onclick="document.getElementById('zone-policy-modal').close()">Cancel</button>
<button type="submit" class="btn btn-primary">Create Zone Policy</button>
</div>
</form>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<!-- Sync from Cloudflare Modal -->
<dialog id="sync-cloudflare-modal" class="modal">
<div class="modal-box max-w-2xl">
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"></button>
</form>
<h3 class="font-bold text-lg mb-4">Sync Access Policies from Cloudflare</h3>
<form method="POST" action="{{ url_for('web.sync_access_groups_from_cloudflare') }}" id="sync-cloudflare-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="sync_all" id="sync-all-input" value="false"/>
<div class="space-y-4">
<div class="alert alert-info">
<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>Import reusable Access Policies from your Cloudflare account.</span>
</div>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-4">
<input type="radio" name="sync_mode" value="dockflare" class="radio radio-primary" checked />
<div class="flex-1">
<span class="label-text font-medium">DockFlare- prefix only (Recommended)</span>
<p class="text-xs opacity-70 mt-1">Import only policies with <code class="bg-base-300 px-1 rounded">DockFlare-</code> prefix. This keeps your policy list clean and focused on container infrastructure.</p>
</div>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-4">
<input type="radio" name="sync_mode" value="all" class="radio radio-primary" />
<div class="flex-1">
<span class="label-text font-medium">Sync all policies</span>
<p class="text-xs opacity-70 mt-1">Import ALL policies from Cloudflare, including those created manually or by other tools. External policies will be marked with a purple badge.</p>
</div>
</label>
</div>
<div class="alert alert-warning mt-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="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 class="text-xs"><strong>Pro tip:</strong> You can rename policies in Cloudflare to use the <code>DockFlare-</code> prefix. This allows you to organize which policies appear in DockFlare without enabling full sync.</span>
</div>
</div>
<div class="modal-action mt-6">
<button type="button" class="btn btn-ghost" onclick="document.getElementById('sync-cloudflare-modal').close()">Cancel</button>
<button type="submit" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 mr-2"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" /></svg>
Sync Now
</button>
</div>
</form>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
{% endblock %}
{% block scripts %}
<style>
.table-container,
.table-container .table {
overflow: visible !important;
}
.dropdown-end .dropdown-content {
position: absolute !important;
right: 0 !important;
top: 100% !important;
transform: none !important;
z-index: 50 !important;
}
.chevron-icon {
transition: transform 0.2s ease-in-out;
}
.usage-details-row td {
transition: all 0.3s ease-in-out;
}
:root {
--policy-col-primary: 32%;
--policy-col-secondary: 24%;
--policy-col-tertiary: 20%;
--policy-col-status: 12%;
--policy-col-actions: 12%;
}
.policy-table {
table-layout: fixed;
}
.policy-table col.col-primary {
width: var(--policy-col-primary);
}
.policy-table col.col-secondary {
width: var(--policy-col-secondary);
}
.policy-table col.col-tertiary {
width: var(--policy-col-tertiary);
}
.policy-table col.col-status {
width: var(--policy-col-status);
}
.policy-table col.col-actions {
width: var(--policy-col-actions);
}
.policy-table th,
.policy-table td {
vertical-align: middle;
font-size: 0.875rem;
}
.policy-table .cell-top {
vertical-align: top !important;
}
.policy-table code {
font-size: 0.75rem;
}
.df-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
line-height: 1;
}
.df-badge svg {
width: 0.75rem;
height: 0.75rem;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const policyFilter = document.getElementById('policyFilter');
if (policyFilter) {
policyFilter.addEventListener('change', function() {
const filterValue = this.value;
const rows = document.querySelectorAll('tbody tr');
rows.forEach(row => {
const badge = row.querySelector('.badge');
if (!badge) {
row.style.display = '';
return;
}
const badgeText = badge.textContent.trim().toLowerCase();
let show = false;
if (filterValue === 'all') {
show = true;
} else if (filterValue === 'external' && badgeText === 'external') {
show = true;
} else if (filterValue === 'system' && badgeText === 'system') {
show = true;
} else if (filterValue === 'dockflare' && badgeText === 'dockflare') {
show = true;
}
row.style.display = show ? '' : 'none';
});
});
}
const countrySelectEl = document.getElementById('group_countries');
if (countrySelectEl) {
const tomSelect = new TomSelect(countrySelectEl, {
plugins: {
'checkbox_options': {},
'remove_button': {
title: 'Remove this item',
}
},
create: false,
sortField: {
field: "text",
direction: "asc"
},
placeholder: "Search and select countries to block...",
maxOptions: null,
render: {
item: function(data, escape) {
return '<div class="item">' + escape(data.text) + '</div>';
}
}
});
const parentContainer = countrySelectEl.closest('.textarea');
if (parentContainer) {
const controlWrapper = tomSelect.control;
controlWrapper.style.height = '100%';
controlWrapper.style.maxHeight = 'none';
controlWrapper.style.border = 'none';
controlWrapper.style.background = 'transparent';
const observer = new ResizeObserver(() => {
const containerHeight = parentContainer.offsetHeight;
controlWrapper.style.height = containerHeight + 'px';
});
observer.observe(parentContainer);
}
const countryData = {
'africa': ['DZ', 'AO', 'BJ', 'BW', 'BF', 'BI', 'CM', 'CV', 'CF', 'TD', 'KM', 'CG', 'CD', 'CI', 'DJ', 'EG', 'GQ', 'ER', 'ET', 'GA', 'GM', 'GH', 'GN', 'GW', 'KE', 'LS', 'LR', 'LY', 'MG', 'MW', 'ML', 'MR', 'MU', 'MA', 'MZ', 'NA', 'NE', 'NG', 'RW', 'ST', 'SN', 'SC', 'SL', 'SO', 'ZA', 'SS', 'SD', 'SZ', 'TZ', 'TG', 'TN', 'UG', 'ZM', 'ZW'],
'asia': ['AF', 'AM', 'AZ', 'BH', 'BD', 'BT', 'BN', 'KH', 'CN', 'CY', 'GE', 'IN', 'ID', 'IR', 'IQ', 'IL', 'JP', 'JO', 'KZ', 'KW', 'KG', 'LA', 'LB', 'MY', 'MV', 'MN', 'MM', 'NP', 'KP', 'OM', 'PK', 'PS', 'PH', 'QA', 'SA', 'SG', 'KR', 'LK', 'SY', 'TJ', 'TH', 'TL', 'TR', 'TM', 'AE', 'UZ', 'VN', 'YE'],
'europe': ['AL', 'AD', 'AT', 'BY', 'BE', 'BA', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IS', 'IE', 'IT', 'XK', 'LV', 'LI', 'LT', 'LU', 'MK', 'MT', 'MD', 'MC', 'ME', 'NL', 'NO', 'PL', 'PT', 'RO', 'RU', 'SM', 'RS', 'SK', 'SI', 'ES', 'SE', 'CH', 'UA', 'GB', 'VA'],
'north-america': ['AG', 'BS', 'BB', 'BZ', 'CA', 'CR', 'CU', 'DM', 'DO', 'SV', 'GD', 'GT', 'HT', 'HN', 'JM', 'MX', 'NI', 'PA', 'KN', 'LC', 'VC', 'TT', 'US'],
'south-america': ['AR', 'BO', 'BR', 'CL', 'CO', 'EC', 'FK', 'GF', 'GY', 'PY', 'PE', 'SR', 'UY', 'VE'],
'oceania': ['AU', 'FJ', 'KI', 'MH', 'FM', 'NR', 'NZ', 'PW', 'PG', 'WS', 'SB', 'TO', 'TV', 'VU']
};
const euCountries = ['AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE'];
const natoCountries = ['US', 'CA', 'GB', 'FR', 'DE', 'IT', 'ES', 'PL', 'RO', 'NL', 'BE', 'GR', 'PT', 'CZ', 'HU', 'BG', 'SK', 'SI', 'HR', 'AL', 'LT', 'LV', 'EE', 'LU', 'IS', 'NO', 'DK', 'TR', 'ME', 'MK', 'FI', 'SE'];
const highRiskCountries = ['AF', 'IR', 'KP', 'SY', 'RU', 'BY', 'MM', 'VE', 'CU', 'SO', 'YE', 'SD', 'LY'];
function updateSelectionCounter() {
const selected = tomSelect.getValue().length;
const total = Object.keys(tomSelect.options).length;
const counter = document.getElementById('country-selection-counter');
const helpText = document.getElementById('country-policy-help-text');
if (counter) {
counter.textContent = `${selected} of ${total} countries selected`;
}
if (helpText) {
if (selected === 0) {
helpText.innerHTML = 'No countries blocked. <strong>All countries allowed</strong>.';
} else if (selected === total) {
helpText.innerHTML = '<strong>All countries blocked</strong>. No access allowed.';
} else if (selected === total - 1) {
helpText.innerHTML = `Only <strong>1 country allowed</strong>. ${total - 1} countries blocked.`;
} else {
const allowed = total - selected;
helpText.innerHTML = `<strong>${selected} countries blocked</strong>. ${allowed} countries allowed.`;
}
}
}
document.getElementById('select-all-countries')?.addEventListener('click', () => {
const allValues = Object.keys(tomSelect.options);
tomSelect.setValue(allValues);
updateSelectionCounter();
});
document.getElementById('select-none-countries')?.addEventListener('click', () => {
tomSelect.clear();
updateSelectionCounter();
});
document.getElementById('invert-selection')?.addEventListener('click', () => {
const allValues = Object.keys(tomSelect.options);
const currentValues = tomSelect.getValue();
const invertedValues = allValues.filter(val => !currentValues.includes(val));
tomSelect.setValue(invertedValues);
updateSelectionCounter();
});
const templatesDropdown = document.getElementById('templates-dropdown');
const templatesMenu = document.getElementById('templates-menu');
const regionsDropdown = document.getElementById('regions-dropdown');
const regionsMenu = document.getElementById('regions-menu');
templatesDropdown?.addEventListener('click', (e) => {
e.preventDefault();
templatesMenu.style.display = templatesMenu.style.display === 'none' ? 'block' : 'none';
regionsMenu.style.display = 'none';
});
regionsDropdown?.addEventListener('click', (e) => {
e.preventDefault();
regionsMenu.style.display = regionsMenu.style.display === 'none' ? 'block' : 'none';
templatesMenu.style.display = 'none';
});
document.addEventListener('click', (e) => {
if (!templatesDropdown?.contains(e.target) && !templatesMenu?.contains(e.target)) {
templatesMenu.style.display = 'none';
}
if (!regionsDropdown?.contains(e.target) && !regionsMenu?.contains(e.target)) {
regionsMenu.style.display = 'none';
}
});
const templates = {
'block-all-except-us': () => {
const allValues = Object.keys(tomSelect.options);
const blockValues = allValues.filter(val => val !== 'US');
tomSelect.setValue(blockValues);
},
'block-all-except-eu': () => {
const allValues = Object.keys(tomSelect.options);
const blockValues = allValues.filter(val => !euCountries.includes(val));
tomSelect.setValue(blockValues);
},
'block-high-risk': () => {
tomSelect.setValue(highRiskCountries);
},
'block-non-nato': () => {
const allValues = Object.keys(tomSelect.options);
const blockValues = allValues.filter(val => !natoCountries.includes(val));
tomSelect.setValue(blockValues);
}
};
document.querySelectorAll('[data-template]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
const template = e.target.dataset.template;
if (templates[template]) {
templates[template]();
updateSelectionCounter();
templatesMenu.style.display = 'none';
}
});
});
document.querySelectorAll('[data-region]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
const region = e.target.dataset.region;
if (countryData[region]) {
const currentValues = tomSelect.getValue();
const newValues = [...new Set([...currentValues, ...countryData[region]])];
tomSelect.setValue(newValues);
updateSelectionCounter();
regionsMenu.style.display = 'none';
}
});
});
const quickActions = {
'allow-us-only': () => {
const allValues = Object.keys(tomSelect.options);
const blockValues = allValues.filter(val => val !== 'US');
tomSelect.setValue(blockValues);
},
'allow-us-eu': () => {
const allowedCountries = ['US', ...euCountries];
const allValues = Object.keys(tomSelect.options);
const blockValues = allValues.filter(val => !allowedCountries.includes(val));
tomSelect.setValue(blockValues);
},
'block-high-risk': () => {
tomSelect.setValue(highRiskCountries);
}
};
document.querySelectorAll('[data-quick-action]').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
const action = e.target.dataset.quickAction;
if (quickActions[action]) {
quickActions[action]();
updateSelectionCounter();
}
});
});
tomSelect.on('change', updateSelectionCounter);
updateSelectionCounter();
window.enhancedCountrySelector = {
tomSelect: tomSelect,
updateSelectionCounter: updateSelectionCounter
};
}
const idpSelectEl = document.getElementById('group_identity_providers');
if (idpSelectEl) {
window.idpTomSelect = new TomSelect(idpSelectEl, {
plugins: {
'checkbox_options': {},
'remove_button': {
title: 'Remove this item',
}
},
create: false,
sortField: {
field: "text",
direction: "asc"
},
placeholder: "Select identity providers...",
maxOptions: null
});
fetch('/api/v2/idp/list')
.then(res => res.json())
.then(data => {
if (data.success && data.identity_providers) {
const options = [];
for (const [friendlyName, idpData] of Object.entries(data.identity_providers)) {
if (!idpData.system_managed) {
options.push({
value: friendlyName,
text: `${idpData.name} (${friendlyName})`
});
}
}
window.idpTomSelect.clearOptions();
window.idpTomSelect.addOptions(options);
}
})
.catch(err => console.error('Failed to load IdPs for selector:', err));
}
const tabAuthenticated = document.getElementById('tab-authenticated');
const tabPublic = document.getElementById('tab-public');
const publicModeInput = document.getElementById('public_mode');
const modeDescAuth = document.getElementById('mode-description-authenticated');
const modeDescPublic = document.getElementById('mode-description-public');
const emailFieldContainer = document.getElementById('email-field-container');
const idpFieldContainer = document.getElementById('idp-field-container');
const appSettingsContainer = document.getElementById('app-settings-container');
const emailField = document.getElementById('group_emails');
function switchToMode(mode) {
if (mode === 'public') {
tabPublic.classList.add('tab-active');
tabAuthenticated.classList.remove('tab-active');
publicModeInput.value = 'true';
modeDescPublic.style.display = 'flex';
modeDescAuth.style.display = 'none';
emailFieldContainer.style.display = 'none';
idpFieldContainer.style.display = 'none';
appSettingsContainer.style.display = 'none';
emailField.removeAttribute('required');
} else {
tabAuthenticated.classList.add('tab-active');
tabPublic.classList.remove('tab-active');
publicModeInput.value = 'false';
modeDescAuth.style.display = 'flex';
modeDescPublic.style.display = 'none';
emailFieldContainer.style.display = 'block';
idpFieldContainer.style.display = 'block';
appSettingsContainer.style.display = 'block';
emailField.removeAttribute('required');
}
}
tabAuthenticated?.addEventListener('click', (e) => {
e.preventDefault();
switchToMode('authenticated');
});
tabPublic?.addEventListener('click', (e) => {
e.preventDefault();
switchToMode('public');
});
document.querySelectorAll('.delete-access-group-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const isDisabled = btn.dataset.disabled === 'true' || btn.closest('li')?.classList.contains('disabled');
if (isDisabled) {
return;
}
const groupId = e.currentTarget.dataset.groupId;
const groupName = e.currentTarget.dataset.groupName;
const isExternal = e.currentTarget.dataset.external === 'true';
if (!groupId || !groupName) {
console.error('Missing group ID or name', e.currentTarget.dataset);
return;
}
let confirmMsg = `Are you sure you want to delete the Access Group '${groupName}'? This cannot be undone.`;
if (isExternal) {
confirmMsg = `⚠️ WARNING: This is an EXTERNAL policy not created by DockFlare.\n\nDeleting '${groupName}' may affect services outside of DockFlare.\n\nAre you absolutely sure you want to delete this external policy?`;
}
if (!confirm(confirmMsg)) {
return;
}
console.log('Deleting group:', groupId, 'Name:', groupName);
const form = document.createElement('form');
form.method = 'POST';
form.action = `/ui/access-groups/delete/${groupId}`;
console.log('Form action:', form.action);
const existingCsrfInput = document.querySelector('input[name="csrf_token"]');
if (!existingCsrfInput) {
console.error('CSRF token not found');
return;
}
const csrfInput = document.createElement('input');
csrfInput.type = 'hidden';
csrfInput.name = 'csrf_token';
csrfInput.value = existingCsrfInput.value;
form.appendChild(csrfInput);
document.body.appendChild(form);
form.submit();
});
});
document.querySelectorAll('.usage-toggle').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
const groupId = btn.getAttribute('data-group-id');
const usageRow = document.getElementById(`usage-${groupId}`);
const chevron = btn.querySelector('.chevron-icon');
if (usageRow.style.display === 'none') {
usageRow.style.display = 'table-row';
chevron.style.transform = 'rotate(180deg)';
} else {
usageRow.style.display = 'none';
chevron.style.transform = 'rotate(0deg)';
}
});
});
const accessGroupForm = document.getElementById('access_group_form');
if (accessGroupForm) {
accessGroupForm.addEventListener('submit', function(e) {
const publicMode = document.getElementById('public_mode').value;
if (publicMode === 'false') {
const emailField = document.getElementById('group_emails');
const emailValue = emailField ? emailField.value.trim() : '';
const selectedIdps = window.idpTomSelect ? window.idpTomSelect.getValue() : [];
if (selectedIdps.length > 0 && !emailValue) {
e.preventDefault();
alert('Security requirement: When using Identity Providers, you must specify allowed email addresses to prevent unauthorized access.');
return false;
}
}
});
}
async function loadZonePolicies() {
const container = document.getElementById('zone-policies-container');
if (!container) return;
try {
const response = await fetch('/api/v2/zone-policies');
const data = await response.json();
if (!data.success) {
container.innerHTML = '<div class="text-center opacity-70 py-8"><p class="text-error">Failed to load zone policies</p></div>';
return;
}
const zonePolicies = data.zone_policies || [];
if (zonePolicies.length === 0) {
container.innerHTML = '<div class="text-center opacity-70 py-8"><p>No DNS zones found in your Cloudflare account.</p></div>';
return;
}
let html = '<div class="overflow-x-auto table-container"><table class="table table-zebra policy-table w-full">';
html += '<colgroup>';
html += '<col class="col-primary">';
html += '<col class="col-secondary">';
html += '<col class="col-tertiary">';
html += '<col class="col-status">';
html += '<col class="col-actions">';
html += '</colgroup>';
html += '<thead><tr>';
html += '<th class="px-4 py-3">Zone Name</th>';
html += '<th class="px-4 py-3">Zone ID</th>';
html += '<th class="px-4 py-3">Wildcard Hostname</th>';
html += '<th class="px-4 py-3">Status</th>';
html += '<th class="px-4 py-3 text-right">Actions</th>';
html += '</tr></thead><tbody>';
zonePolicies.forEach(zone => {
html += '<tr>';
html += `<td class="px-4 py-3 font-medium">${zone.zone_name}</td>`;
if (zone.zone_id) {
html += `<td class="px-4 py-3 text-xs opacity-70"><span class=\"tooltip\" data-tip=\"${zone.zone_id}\"><code>${zone.zone_id.slice(0, 8)}...</code></span></td>`;
} else {
html += '<td class="px-4 py-3 text-xs opacity-70">-</td>';
}
html += `<td class="px-4 py-3"><code class="text-sm">*.${zone.zone_name}</code></td>`;
html += '<td class="px-4 py-3">';
if (zone.has_default_policy) {
html += '<span class="badge badge-sm badge-success df-badge">';
html += '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z" /></svg>Protected</span>';
} else {
html += '<span class="badge badge-sm badge-warning df-badge">';
html += '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" /></svg>Not Protected</span>';
}
html += '</td><td class="px-4 py-3 text-right">';
if (!zone.has_default_policy) {
html += `<div class="dropdown dropdown-end">
<label tabindex="0" class="btn btn-ghost btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 12.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 18.75a.75.75 0 110-1.5.75.75 0 010 1.5z" />
</svg>
</label>
<ul tabindex="0" class="dropdown-content menu menu-compact bg-base-100 shadow-lg rounded-box w-60 mt-2">
<li>
<a class="create-zone-policy-btn flex items-center gap-2" data-zone-name="${zone.zone_name}" data-zone-id="${zone.zone_id}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
Create Policy
</a>
</li>
</ul>
</div>`;
} else {
html += `<div class="dropdown dropdown-end">
<label tabindex="0" class="btn btn-ghost btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 12.75a.75.75 0 110-1.5.75.75 0 010 1.5zM12 18.75a.75.75 0 110-1.5.75.75 0 010 1.5z" />
</svg>
</label>
<ul tabindex="0" class="dropdown-content menu menu-compact bg-base-100 shadow-lg rounded-box w-60 mt-2">
<li>
<a href="https://one.dash.cloudflare.com/{{ ACCOUNT_ID_FOR_DISPLAY }}/access/apps" target="_blank" rel="noopener noreferrer" class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
</svg>
View in Cloudflare
</a>
</li>
</ul>
</div>`;
}
html += '</td></tr>';
});
html += '</tbody></table></div>';
container.innerHTML = html;
document.querySelectorAll('.create-zone-policy-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
const zoneName = btn.getAttribute('data-zone-name');
const zoneId = btn.getAttribute('data-zone-id');
document.getElementById('zone-policy-zone-name').value = zoneName;
document.getElementById('zone-policy-zone-id').value = zoneId;
document.getElementById('zone-policy-hostname-display').textContent = `*.${zoneName}`;
document.getElementById('zone-policy-modal').showModal();
});
});
} catch (error) {
console.error('Error loading zone policies:', error);
container.innerHTML = '<div class="text-center opacity-70 py-8"><p class="text-error">Failed to load zone policies</p></div>';
}
}
loadZonePolicies();
// Handle sync from Cloudflare modal
const syncCloudflareBtn = document.getElementById('sync-cloudflare-btn');
if (syncCloudflareBtn) {
syncCloudflareBtn.addEventListener('click', function(e) {
e.preventDefault();
document.getElementById('sync-cloudflare-modal').showModal();
});
}
// Handle radio button changes in sync modal
const syncModeRadios = document.querySelectorAll('input[name="sync_mode"]');
const syncAllInput = document.getElementById('sync-all-input');
syncModeRadios.forEach(radio => {
radio.addEventListener('change', function() {
if (this.value === 'all') {
syncAllInput.value = 'true';
} else {
syncAllInput.value = 'false';
}
});
});
});
</script>
{% include 'modals/_idp_modal.html' %}
{% endblock %}