696 lines
31 KiB
HTML
696 lines
31 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block content %}
|
|
<section class="section">
|
|
<div class="container">
|
|
<h1 class="title is-4">Security</h1>
|
|
|
|
<div class="columns is-desktop is-variable is-8">
|
|
<div class="column">
|
|
<div class="box">
|
|
<h2 class="title is-6">XMPP Channel</h2>
|
|
<table class="table is-fullwidth is-size-7">
|
|
<tbody>
|
|
<tr>
|
|
<th>Component JID</th>
|
|
<td>{{ xmpp_state.omemo_target_jid|default:"—" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>OMEMO</th>
|
|
<td>
|
|
{% if xmpp_state.omemo_enabled %}
|
|
<span class="tag is-success">Active</span>
|
|
{% else %}
|
|
<span class="tag is-light">{{ xmpp_state.omemo_status|default:"not configured" }}</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Status reason</th>
|
|
<td>{{ xmpp_state.omemo_status_reason|default:"—" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Component fingerprint</th>
|
|
<td><code>{{ xmpp_state.omemo_fingerprint|default:"—" }}</code></td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<h3 class="title is-7 mt-4 mb-2">Security Policy</h3>
|
|
<form method="post">
|
|
{% csrf_token %}
|
|
<div class="field">
|
|
<label class="checkbox">
|
|
<input type="checkbox" name="require_omemo"{% if security_settings.require_omemo %} checked{% endif %}>
|
|
Require OMEMO encryption — reject plaintext messages from your XMPP client
|
|
</label>
|
|
<p class="help is-size-7 has-text-grey mt-1">When enabled, any plaintext XMPP message to the gateway is rejected before command routing.</p>
|
|
<p class="help is-size-7 has-text-grey">This is separate from command-scope policy checks such as Require Trusted Fingerprint.</p>
|
|
</div>
|
|
<button class="button is-link is-small" type="submit">Save</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<div class="column">
|
|
<div class="box">
|
|
<h2 class="title is-6">Your XMPP Client</h2>
|
|
{% if omemo_row %}
|
|
<table class="table is-fullwidth is-size-7">
|
|
<tbody>
|
|
<tr>
|
|
<th>Status</th>
|
|
<td>
|
|
{% if omemo_row.status == "detected" %}
|
|
<span class="tag is-info">{{ omemo_row.status }}</span>
|
|
{% elif omemo_row.status == "no_omemo" %}
|
|
<span class="tag is-warning">no OMEMO observed</span>
|
|
{% elif omemo_row.status == "error" %}
|
|
<span class="tag is-danger">{{ omemo_row.status }}</span>
|
|
{% else %}
|
|
<span class="tag is-light">{{ omemo_row.status }}</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Contact key</th>
|
|
<td><code>{{ omemo_row.latest_client_key|default:"—" }}</code></td>
|
|
</tr>
|
|
<tr>
|
|
<th>Contact JID</th>
|
|
<td>{{ sender_jid.bare|default:"—" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Resource</th>
|
|
<td>{{ sender_jid.resource|default:"—" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>GIA component</th>
|
|
<td>{{ omemo_row.last_target_jid|default:"—" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Last seen</th>
|
|
<td>{{ omemo_row.last_seen_at|default:"—" }}</td>
|
|
</tr>
|
|
<tr>
|
|
<th>Updated</th>
|
|
<td>{{ omemo_row.updated_at }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<p class="is-size-7 has-text-grey">No OMEMO observation recorded yet. Send a message via XMPP to populate this.</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="box">
|
|
<h2 class="title is-6">Global Scope Override</h2>
|
|
<p class="is-size-7 has-text-grey mb-3">
|
|
This scope can force settings across all Command Security Scopes.
|
|
</p>
|
|
<div class="box" style="margin: 0; border: 1px solid rgba(60, 60, 60, 0.12);">
|
|
<form method="post">
|
|
{% csrf_token %}
|
|
<input type="hidden" name="scope_key" value="global.override">
|
|
<input type="hidden" name="global_scope_enabled" value="{{ global_override.values.scope_enabled }}" data-global-mode-input="scope_enabled">
|
|
<input type="hidden" name="global_require_omemo" value="{{ global_override.values.require_omemo }}" data-global-mode-input="require_omemo">
|
|
<input type="hidden" name="global_require_trusted_fingerprint" value="{{ global_override.values.require_trusted_fingerprint }}" data-global-mode-input="require_trusted_fingerprint">
|
|
<div class="is-flex is-justify-content-space-between is-align-items-center mb-2">
|
|
<div>
|
|
<p class="mb-1"><strong>Global Scope Override</strong></p>
|
|
<p class="is-size-7 has-text-grey">Remote controls for local scope security checkboxes.</p>
|
|
<p class="is-size-7 has-text-grey"><code>global.override</code></p>
|
|
</div>
|
|
<div class="tags has-addons">
|
|
<span class="tag is-dark">Scope Enabled</span>
|
|
{% if global_override.values.scope_enabled == "on" %}
|
|
<span class="tag is-success">Force On</span>
|
|
{% elif global_override.values.scope_enabled == "off" %}
|
|
<span class="tag is-light">Force Off</span>
|
|
{% else %}
|
|
<span class="tag is-info">Per Scope</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field mb-2">
|
|
<label class="label is-size-7">Allowed Services Preview</label>
|
|
<div class="scope-allowance-capsule" data-capsule>
|
|
{% for service in policy_services %}
|
|
<span class="scope-allowance-pill" data-service-pill="{{ service }}">{{ service }}</span>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field is-grouped is-grouped-multiline mb-2">
|
|
<label class="checkbox mr-4">
|
|
<input
|
|
type="checkbox"
|
|
disabled
|
|
data-global-visual-checkbox="scope_enabled"
|
|
data-global-visual-state="{{ global_override.values.scope_enabled }}"
|
|
{% if global_override.values.scope_enabled == "on" %} checked{% endif %}
|
|
>
|
|
Scope Enabled
|
|
</label>
|
|
<label class="checkbox mr-4">
|
|
<input
|
|
type="checkbox"
|
|
disabled
|
|
data-global-visual-checkbox="require_omemo"
|
|
data-global-visual-state="{{ global_override.values.require_omemo }}"
|
|
{% if global_override.values.require_omemo == "on" %} checked{% endif %}
|
|
>
|
|
Require OMEMO
|
|
</label>
|
|
<label class="checkbox">
|
|
<input
|
|
type="checkbox"
|
|
disabled
|
|
data-global-visual-checkbox="require_trusted_fingerprint"
|
|
{% if global_override.values.require_trusted_fingerprint == "on" %} checked{% endif %}
|
|
>
|
|
Require Trusted Fingerprint
|
|
</label>
|
|
</div>
|
|
<p class="help is-size-7 has-text-grey mb-3">Set each field to Per Scope to edit that field inside local scopes.</p>
|
|
|
|
<div class="field mb-2">
|
|
<div class="is-flex is-justify-content-space-between is-align-items-center">
|
|
<label class="label is-size-7 mb-1">Scope Enabled</label>
|
|
<div class="is-flex is-align-items-center" style="gap:0.5rem;">
|
|
<span class="tag is-size-7" data-global-mode-label="scope_enabled"></span>
|
|
<button type="button" class="button is-small is-light" data-global-change-toggle="scope_enabled">Change Global</button>
|
|
</div>
|
|
</div>
|
|
<div class="buttons has-addons is-hidden" data-global-mode-picker="scope_enabled">
|
|
<button class="button is-small" type="button" data-global-mode-set="scope_enabled" data-mode="per_scope">Per Scope</button>
|
|
<button class="button is-small" type="button" data-global-mode-set="scope_enabled" data-mode="on">Force On</button>
|
|
<button class="button is-small" type="button" data-global-mode-set="scope_enabled" data-mode="off">Force Off</button>
|
|
</div>
|
|
</div>
|
|
<div class="field mb-2">
|
|
<div class="is-flex is-justify-content-space-between is-align-items-center">
|
|
<label class="label is-size-7 mb-1">Require OMEMO</label>
|
|
<div class="is-flex is-align-items-center" style="gap:0.5rem;">
|
|
<span class="tag is-size-7" data-global-mode-label="require_omemo"></span>
|
|
<button type="button" class="button is-small is-light" data-global-change-toggle="require_omemo">Change Global</button>
|
|
</div>
|
|
</div>
|
|
<div class="buttons has-addons is-hidden" data-global-mode-picker="require_omemo">
|
|
<button class="button is-small" type="button" data-global-mode-set="require_omemo" data-mode="per_scope">Per Scope</button>
|
|
<button class="button is-small" type="button" data-global-mode-set="require_omemo" data-mode="on">Force On</button>
|
|
<button class="button is-small" type="button" data-global-mode-set="require_omemo" data-mode="off">Force Off</button>
|
|
</div>
|
|
</div>
|
|
<div class="field mb-3">
|
|
<div class="is-flex is-justify-content-space-between is-align-items-center">
|
|
<label class="label is-size-7 mb-1">Require Trusted Fingerprint</label>
|
|
<div class="is-flex is-align-items-center" style="gap:0.5rem;">
|
|
<span class="tag is-size-7" data-global-mode-label="require_trusted_fingerprint"></span>
|
|
<button type="button" class="button is-small is-light" data-global-change-toggle="require_trusted_fingerprint">Change Global</button>
|
|
</div>
|
|
</div>
|
|
<div class="buttons has-addons is-hidden" data-global-mode-picker="require_trusted_fingerprint">
|
|
<button class="button is-small" type="button" data-global-mode-set="require_trusted_fingerprint" data-mode="per_scope">Per Scope</button>
|
|
<button class="button is-small" type="button" data-global-mode-set="require_trusted_fingerprint" data-mode="on">Force On</button>
|
|
<button class="button is-small" type="button" data-global-mode-set="require_trusted_fingerprint" data-mode="off">Force Off</button>
|
|
</div>
|
|
</div>
|
|
<div class="field mb-2">
|
|
<label class="label is-size-7">Allowed Services</label>
|
|
<div class="is-flex is-flex-wrap-wrap" style="gap: 0.8rem;">
|
|
{% for service in policy_services %}
|
|
<label class="checkbox">
|
|
<input type="checkbox" name="allowed_services" value="{{ service }}"{% if service in global_override.allowed_services %} checked{% endif %}>
|
|
{{ service }}
|
|
</label>
|
|
{% endfor %}
|
|
</div>
|
|
<p class="help is-size-7 has-text-grey">Allowed Services: <code>xmpp</code>, <code>whatsapp</code>, <code>signal</code>, <code>instagram</code>, <code>web</code>.</p>
|
|
<p class="help is-size-7 has-text-grey">Leave all unchecked to allow all services.</p>
|
|
</div>
|
|
<div class="field mb-2">
|
|
<label class="label is-size-7">Allowed Channels</label>
|
|
<div id="channel-rules-global-override" class="channel-rules-list">
|
|
{% for rule in global_override.channel_rules %}
|
|
<div class="channel-rule-row is-flex is-align-items-center mb-2" style="gap: 0.5rem;">
|
|
<div class="select is-small">
|
|
<select name="allowed_channel_service">
|
|
<option value="*"{% if rule.service == "*" %} selected{% endif %}>any</option>
|
|
{% for service in policy_services %}
|
|
<option value="{{ service }}"{% if rule.service == service %} selected{% endif %}>{{ service }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<input class="input is-small" name="allowed_channel_pattern" value="{{ rule.pattern }}" placeholder="m@zm.is* or 1203*">
|
|
<button class="button is-small is-light is-danger channel-rule-remove" type="button">Remove</button>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
<button
|
|
class="button is-small is-light channel-rule-add"
|
|
type="button"
|
|
data-target="channel-rules-global-override"
|
|
>
|
|
Add Channel Rule
|
|
</button>
|
|
<p class="help is-size-7 has-text-grey">Leave pattern rows empty to allow all channels for allowed services.</p>
|
|
</div>
|
|
<button class="button is-link is-small" type="submit">Save Scope</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="box">
|
|
<h2 class="title is-6">Command Security Scopes</h2>
|
|
<p class="is-size-7 has-text-grey mb-3">
|
|
Choose a top-level category, expand a scope, then click <strong>Change</strong> to edit that scope.
|
|
</p>
|
|
<div class="tabs is-toggle is-toggle-rounded is-small mb-3">
|
|
<ul>
|
|
{% for group in policy_groups %}
|
|
<li class="{% if forloop.first %}is-active{% endif %}" data-policy-tab-button="{{ group.key }}">
|
|
<a>{{ group.label }}</a>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
{% for group in policy_groups %}
|
|
<div class="policy-tab-panel{% if forloop.first %} is-active{% endif %}" data-policy-tab-panel="{{ group.key }}">
|
|
<div class="is-flex is-flex-direction-column" style="gap: 1rem;">
|
|
{% for row in group.rows %}
|
|
<details class="box scope-editor-card" style="margin: 0; border: 1px solid rgba(60, 60, 60, 0.12);">
|
|
<summary class="scope-summary is-flex is-justify-content-space-between is-align-items-center">
|
|
<span>
|
|
<strong>{{ row.label }}</strong>
|
|
<span class="is-size-7 has-text-grey ml-2"><code>{{ row.scope_key }}</code></span>
|
|
</span>
|
|
<span class="tags has-addons">
|
|
<span class="tag is-dark">Scope Enabled</span>
|
|
{% if row.enabled %}
|
|
<span class="tag is-success">On</span>
|
|
{% else %}
|
|
<span class="tag is-light">Off</span>
|
|
{% endif %}
|
|
</span>
|
|
</summary>
|
|
<form method="post" class="mt-3" data-scope-form>
|
|
{% csrf_token %}
|
|
<input type="hidden" name="scope_key" value="{{ row.scope_key }}">
|
|
<input type="hidden" name="scope_change_mode" value="0" data-scope-change-mode>
|
|
<p class="is-size-7 has-text-grey mb-2">{{ row.description }}</p>
|
|
|
|
<div class="field mb-2">
|
|
<button class="button is-small is-light" type="button" data-scope-edit-toggle>Change</button>
|
|
</div>
|
|
|
|
<div class="field mb-2">
|
|
<label class="label is-size-7">Allowed Services Preview</label>
|
|
<div class="scope-allowance-capsule" data-capsule>
|
|
{% for service in policy_services %}
|
|
<span class="scope-allowance-pill" data-service-pill="{{ service }}">{{ service }}</span>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="field is-grouped is-grouped-multiline mb-2">
|
|
<label class="checkbox mr-4" title="{% if row.enabled_locked %}{{ row.lock_help }}{% endif %}">
|
|
<input class="scope-editable" data-lock-state="{% if row.enabled_locked %}locked{% else %}free{% endif %}" type="checkbox" name="policy_enabled"{% if row.enabled %} checked{% endif %}{% if row.enabled_locked %} disabled{% endif %}>
|
|
Scope Enabled
|
|
</label>
|
|
<label class="checkbox mr-4" title="{% if row.require_omemo_locked %}{{ row.lock_help }}{% endif %}">
|
|
<input class="scope-editable" data-lock-state="{% if row.require_omemo_locked %}locked{% else %}free{% endif %}" type="checkbox" name="policy_require_omemo"{% if row.require_omemo %} checked{% endif %}{% if row.require_omemo_locked %} disabled{% endif %}>
|
|
Require OMEMO
|
|
</label>
|
|
<label class="checkbox" title="{% if row.require_trusted_fingerprint_locked %}{{ row.lock_help }}{% endif %}">
|
|
<input class="scope-editable" data-lock-state="{% if row.require_trusted_fingerprint_locked %}locked{% else %}free{% endif %}" type="checkbox" name="policy_require_trusted_fingerprint"{% if row.require_trusted_fingerprint %} checked{% endif %}{% if row.require_trusted_fingerprint_locked %} disabled{% endif %}>
|
|
Require Trusted Fingerprint
|
|
</label>
|
|
</div>
|
|
|
|
<div class="field mb-2">
|
|
<label class="label is-size-7">Allowed Services</label>
|
|
<div class="is-flex is-flex-wrap-wrap" style="gap: 0.8rem;">
|
|
{% for service in policy_services %}
|
|
<label class="checkbox">
|
|
<input class="scope-editable" data-lock-state="free" type="checkbox" name="allowed_services" value="{{ service }}"{% if service in row.allowed_services %} checked{% endif %}>
|
|
{{ service }}
|
|
</label>
|
|
{% endfor %}
|
|
</div>
|
|
<p class="help is-size-7 has-text-grey">Allowed Services: <code>xmpp</code>, <code>whatsapp</code>, <code>signal</code>, <code>instagram</code>, <code>web</code>.</p>
|
|
<p class="help is-size-7 has-text-grey">Leave all unchecked to allow all services.</p>
|
|
</div>
|
|
|
|
<div class="field mb-2">
|
|
<label class="label is-size-7">Allowed Channels</label>
|
|
<div id="channel-rules-{{ row.scope_key|slugify }}" class="channel-rules-list">
|
|
{% for rule in row.channel_rules %}
|
|
<div class="channel-rule-row is-flex is-align-items-center mb-2" style="gap: 0.5rem;">
|
|
<div class="select is-small">
|
|
<select class="scope-editable" data-lock-state="free" name="allowed_channel_service">
|
|
<option value="*"{% if rule.service == "*" %} selected{% endif %}>any</option>
|
|
{% for service in policy_services %}
|
|
<option value="{{ service }}"{% if rule.service == service %} selected{% endif %}>{{ service }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<input class="input is-small scope-editable" data-lock-state="free" name="allowed_channel_pattern" value="{{ rule.pattern }}" placeholder="m@zm.is* or 1203*">
|
|
<button class="button is-small is-light is-danger channel-rule-remove scope-editable" data-lock-state="free" type="button">Remove</button>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
<button
|
|
class="button is-small is-light channel-rule-add scope-editable"
|
|
data-lock-state="free"
|
|
type="button"
|
|
data-target="channel-rules-{{ row.scope_key|slugify }}"
|
|
>
|
|
Add Channel Rule
|
|
</button>
|
|
<p class="help is-size-7 has-text-grey">Leave pattern rows empty to allow all channels for allowed services.</p>
|
|
</div>
|
|
|
|
<button class="button is-link is-small" type="submit" data-scope-save>Save Scope</button>
|
|
</form>
|
|
</details>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<div class="box">
|
|
<h2 class="title is-6">OMEMO Enablement Plan</h2>
|
|
<p class="is-size-7 has-text-grey mb-3">Complete each step to achieve end-to-end encrypted messaging with the gateway.</p>
|
|
<table class="table is-fullwidth is-size-7">
|
|
<tbody>
|
|
{% for step in omemo_plan %}
|
|
<tr>
|
|
<td style="width:2.5rem">
|
|
{% if step.done %}
|
|
<span class="tag is-success">✓</span>
|
|
{% else %}
|
|
<span class="tag is-warning">○</span>
|
|
{% endif %}
|
|
</td>
|
|
<td><strong>{{ step.label }}</strong></td>
|
|
<td class="has-text-grey">{{ step.hint }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
</div>
|
|
</section>
|
|
<template id="channel-rule-template">
|
|
<div class="channel-rule-row is-flex is-align-items-center mb-2" style="gap: 0.5rem;">
|
|
<div class="select is-small">
|
|
<select class="scope-editable" data-lock-state="free" name="allowed_channel_service">
|
|
<option value="*">any</option>
|
|
{% for service in policy_services %}
|
|
<option value="{{ service }}">{{ service }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<input class="input is-small scope-editable" data-lock-state="free" name="allowed_channel_pattern" value="" placeholder="m@zm.is* or 1203*">
|
|
<button class="button is-small is-light is-danger channel-rule-remove scope-editable" data-lock-state="free" type="button">Remove</button>
|
|
</div>
|
|
</template>
|
|
<script>
|
|
(function () {
|
|
function wireRemoveButtons(scope) {
|
|
scope.querySelectorAll(".channel-rule-remove").forEach(function (btn) {
|
|
btn.onclick = function () {
|
|
const row = btn.closest(".channel-rule-row");
|
|
if (row) {
|
|
row.remove();
|
|
}
|
|
};
|
|
});
|
|
}
|
|
|
|
function refreshScopeCapsule(form) {
|
|
if (!form) {
|
|
return;
|
|
}
|
|
const capsule = form.querySelector("[data-capsule]");
|
|
if (!capsule) {
|
|
return;
|
|
}
|
|
const enabledInput = form.querySelector('input[name="policy_enabled"]');
|
|
let enabled = !!(enabledInput && enabledInput.checked);
|
|
if (!enabledInput) {
|
|
const globalModeInput = form.querySelector('input[name="global_scope_enabled"]');
|
|
if (globalModeInput) {
|
|
enabled = String(globalModeInput.value || "").toLowerCase() !== "off";
|
|
} else {
|
|
enabled = true;
|
|
}
|
|
}
|
|
const serviceChecks = Array.from(
|
|
form.querySelectorAll('input[name="allowed_services"]')
|
|
);
|
|
const selected = serviceChecks
|
|
.filter(function (el) { return el.checked; })
|
|
.map(function (el) { return String(el.value || "").trim().toLowerCase(); })
|
|
.filter(Boolean);
|
|
const hasServiceRestriction = selected.length > 0;
|
|
|
|
capsule.querySelectorAll("[data-service-pill]").forEach(function (pill) {
|
|
const service = String(pill.getAttribute("data-service-pill") || "").trim().toLowerCase();
|
|
const serviceAllowed = !hasServiceRestriction || selected.includes(service);
|
|
const allowed = enabled && serviceAllowed;
|
|
pill.classList.remove("is-allowed", "is-blocked");
|
|
pill.classList.add(allowed ? "is-allowed" : "is-blocked");
|
|
pill.textContent = (service || "service") + (allowed ? " allowed" : " blocked");
|
|
});
|
|
}
|
|
|
|
wireRemoveButtons(document);
|
|
|
|
const template = document.getElementById("channel-rule-template");
|
|
document.querySelectorAll(".channel-rule-add").forEach(function (btn) {
|
|
btn.addEventListener("click", function () {
|
|
const targetId = btn.getAttribute("data-target");
|
|
const container = targetId ? document.getElementById(targetId) : null;
|
|
if (!container || !template) {
|
|
return;
|
|
}
|
|
const fragment = template.content.cloneNode(true);
|
|
container.appendChild(fragment);
|
|
wireRemoveButtons(container);
|
|
const scopeForm = btn.closest("form[data-scope-form]");
|
|
if (scopeForm) {
|
|
applyScopeEditState(scopeForm);
|
|
}
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll("form").forEach(function (form) {
|
|
form.querySelectorAll('input[name="policy_enabled"], input[name="allowed_services"]').forEach(function (input) {
|
|
input.addEventListener("change", function () {
|
|
refreshScopeCapsule(form);
|
|
});
|
|
});
|
|
refreshScopeCapsule(form);
|
|
});
|
|
|
|
function applyScopeEditState(form) {
|
|
if (!form || !form.matches("[data-scope-form]")) {
|
|
return;
|
|
}
|
|
const editing = String(form.getAttribute("data-editing") || "0") === "1";
|
|
form.querySelectorAll(".scope-editable").forEach(function (input) {
|
|
const locked = String(input.getAttribute("data-lock-state") || "").toLowerCase() === "locked";
|
|
input.disabled = locked || !editing;
|
|
});
|
|
const toggle = form.querySelector("[data-scope-edit-toggle]");
|
|
if (toggle) {
|
|
toggle.textContent = editing ? "Cancel Change" : "Change";
|
|
}
|
|
const changeMode = form.querySelector("[data-scope-change-mode]");
|
|
if (changeMode) {
|
|
changeMode.value = editing ? "1" : "0";
|
|
}
|
|
const saveButton = form.querySelector("[data-scope-save]");
|
|
if (saveButton) {
|
|
saveButton.disabled = !editing;
|
|
}
|
|
}
|
|
|
|
document.querySelectorAll("form[data-scope-form]").forEach(function (form) {
|
|
form.setAttribute("data-editing", "0");
|
|
applyScopeEditState(form);
|
|
const toggle = form.querySelector("[data-scope-edit-toggle]");
|
|
if (toggle) {
|
|
toggle.addEventListener("click", function () {
|
|
const editing = String(form.getAttribute("data-editing") || "0") === "1";
|
|
form.setAttribute("data-editing", editing ? "0" : "1");
|
|
applyScopeEditState(form);
|
|
});
|
|
}
|
|
});
|
|
|
|
function updateGlobalModeUI(field) {
|
|
const hiddenInput = document.querySelector('input[data-global-mode-input="' + field + '"]');
|
|
const checkbox = document.querySelector('input[data-global-visual-checkbox="' + field + '"]');
|
|
const label = document.querySelector('[data-global-mode-label="' + field + '"]');
|
|
if (!hiddenInput || !checkbox || !label) {
|
|
return;
|
|
}
|
|
const mode = String(hiddenInput.value || "").toLowerCase();
|
|
checkbox.checked = mode === "on";
|
|
checkbox.indeterminate = mode === "per_scope";
|
|
|
|
label.classList.remove("is-success", "is-light", "is-info");
|
|
if (mode === "on") {
|
|
label.classList.add("is-success");
|
|
label.textContent = "Force On";
|
|
} else if (mode === "off") {
|
|
label.classList.add("is-light");
|
|
label.textContent = "Force Off";
|
|
} else {
|
|
label.classList.add("is-info");
|
|
label.textContent = "Per Scope";
|
|
}
|
|
|
|
document.querySelectorAll('[data-global-mode-set="' + field + '"]').forEach(function (btn) {
|
|
const selected = String(btn.getAttribute("data-mode") || "").toLowerCase() === mode;
|
|
btn.classList.toggle("is-link", selected);
|
|
});
|
|
}
|
|
|
|
function setGlobalMode(field, mode) {
|
|
if (!field) {
|
|
return;
|
|
}
|
|
const hiddenInput = document.querySelector('input[data-global-mode-input="' + field + '"]');
|
|
if (!hiddenInput) {
|
|
return;
|
|
}
|
|
hiddenInput.value = mode;
|
|
updateGlobalModeUI(field);
|
|
const form = hiddenInput.closest("form");
|
|
refreshScopeCapsule(form);
|
|
}
|
|
|
|
document.querySelectorAll("[data-global-change-toggle]").forEach(function (btn) {
|
|
btn.addEventListener("click", function () {
|
|
const field = String(btn.getAttribute("data-global-change-toggle") || "");
|
|
const picker = document.querySelector('[data-global-mode-picker="' + field + '"]');
|
|
if (!picker) {
|
|
return;
|
|
}
|
|
const currentlyHidden = picker.classList.contains("is-hidden");
|
|
document.querySelectorAll("[data-global-mode-picker]").forEach(function (row) {
|
|
row.classList.add("is-hidden");
|
|
});
|
|
if (currentlyHidden) {
|
|
picker.classList.remove("is-hidden");
|
|
}
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll("[data-global-mode-set]").forEach(function (btn) {
|
|
btn.addEventListener("click", function () {
|
|
const field = String(btn.getAttribute("data-global-mode-set") || "");
|
|
const mode = String(btn.getAttribute("data-mode") || "");
|
|
setGlobalMode(field, mode);
|
|
const picker = document.querySelector('[data-global-mode-picker="' + field + '"]');
|
|
if (picker) {
|
|
picker.classList.add("is-hidden");
|
|
}
|
|
});
|
|
});
|
|
|
|
["scope_enabled", "require_omemo", "require_trusted_fingerprint"].forEach(function (field) {
|
|
updateGlobalModeUI(field);
|
|
});
|
|
|
|
document.querySelectorAll("[data-policy-tab-button]").forEach(function (tab) {
|
|
tab.addEventListener("click", function () {
|
|
const key = String(tab.getAttribute("data-policy-tab-button") || "");
|
|
if (!key) {
|
|
return;
|
|
}
|
|
document.querySelectorAll("[data-policy-tab-button]").forEach(function (node) {
|
|
node.classList.remove("is-active");
|
|
});
|
|
document.querySelectorAll("[data-policy-tab-panel]").forEach(function (panel) {
|
|
panel.classList.remove("is-active");
|
|
});
|
|
tab.classList.add("is-active");
|
|
const target = document.querySelector('[data-policy-tab-panel="' + key + '"]');
|
|
if (target) {
|
|
target.classList.add("is-active");
|
|
}
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll(".scope-editor-card").forEach(function (details) {
|
|
details.addEventListener("toggle", function () {
|
|
if (details.open) {
|
|
const form = details.querySelector("form[data-scope-form]");
|
|
if (form) {
|
|
refreshScopeCapsule(form);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
})();
|
|
</script>
|
|
<style>
|
|
.policy-tab-panel {
|
|
display: none;
|
|
}
|
|
.policy-tab-panel.is-active {
|
|
display: block;
|
|
}
|
|
.scope-summary {
|
|
cursor: pointer;
|
|
list-style: none;
|
|
}
|
|
.scope-summary::-webkit-details-marker {
|
|
display: none;
|
|
}
|
|
.scope-allowance-capsule {
|
|
display: grid;
|
|
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
gap: 0.4rem;
|
|
}
|
|
.scope-allowance-pill {
|
|
border-radius: 999px;
|
|
padding: 0.28rem 0.5rem;
|
|
text-align: center;
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
border: 1px solid transparent;
|
|
white-space: nowrap;
|
|
}
|
|
.scope-allowance-pill.is-allowed {
|
|
background: rgba(72, 199, 142, 0.18);
|
|
color: #1f684a;
|
|
border-color: rgba(72, 199, 142, 0.28);
|
|
}
|
|
.scope-allowance-pill.is-blocked {
|
|
background: rgba(241, 70, 104, 0.16);
|
|
color: #7d1f39;
|
|
border-color: rgba(241, 70, 104, 0.24);
|
|
}
|
|
[data-theme="dark"] .scope-allowance-pill.is-allowed {
|
|
background: rgba(72, 199, 142, 0.26);
|
|
color: #c7f3df;
|
|
border-color: rgba(72, 199, 142, 0.45);
|
|
}
|
|
[data-theme="dark"] .scope-allowance-pill.is-blocked {
|
|
background: rgba(241, 70, 104, 0.24);
|
|
color: #ffd0db;
|
|
border-color: rgba(241, 70, 104, 0.42);
|
|
}
|
|
</style>
|
|
{% endblock %}
|