PHP Classes

File: templates/admin/binkp_config.twig

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   templates/admin/binkp_config.twig   Download  
File: templates/admin/binkp_config.twig
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 53,228 bytes
 

Contents

Class file image Download
{% extends "base.twig" %} {% block title %}{{ t('ui.admin.binkp_config.page_title', {}, locale, ['common']) }}{% endblock %} {% block content %} <div class="container-fluid"> <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"> <h1 class="h2">{{ t('ui.admin.binkp_config.heading', {}, locale, ['common']) }}</h1> </div> <div class="alert alert-info"> <i class="fas fa-info-circle"></i> {{ t('ui.admin.binkp_config.restart_notice', {}, locale, ['common']) }} </div> <div id="binkpConfigAlert"></div> <div class="row g-4" id="binkpConfigCards"> <div class="col-xl-6"> <div class="card shadow h-100"> <div class="card-header"> <h6 class="mb-0">{{ t('ui.admin.binkp_config.system.title', {}, locale, ['common']) }}</h6> </div> <div class="card-body"> <div class="mb-3"> <label class="form-label" for="systemName">{{ t('ui.admin.binkp_config.system.system_name', {}, locale, ['common']) }}</label> <input type="text" class="form-control" id="systemName"> </div> <div class="mb-3"> <label class="form-label" for="systemAddress">{{ t('ui.admin.binkp_config.system.system_address', {}, locale, ['common']) }}</label> <input type="text" class="form-control" id="systemAddress"> </div> <div class="mb-3"> <label class="form-label" for="systemSysop">{{ t('ui.admin.binkp_config.system.sysop_name', {}, locale, ['common']) }}</label> <select class="form-select" id="systemSysop"></select> <small class="text-muted">{{ t('ui.admin.binkp_config.system.select_admin_user', {}, locale, ['common']) }}</small> </div> <div class="mb-3"> <label class="form-label" for="systemLocation">{{ t('ui.admin.binkp_config.system.location', {}, locale, ['common']) }}</label> <input type="text" class="form-control" id="systemLocation"> </div> <div class="mb-3"> <label class="form-label" for="systemHostname">{{ t('ui.common.hostname', {}, locale, ['common']) }}</label> <input type="text" class="form-control" id="systemHostname"> </div> <div class="mb-3"> <label class="form-label" for="systemTimezone">{{ t('ui.admin.binkp_config.system.timezone', {}, locale, ['common']) }}</label> <select class="form-select" id="systemTimezone"> {% for tz in timezone_list %} <option value="{{ tz }}">{{ tz }}</option> {% endfor %} </select> </div> <div class="mb-3"> <label class="form-label" for="systemOrigin">{{ t('ui.admin.binkp_config.system.origin_line', {}, locale, ['common']) }}</label> <input type="text" class="form-control" id="systemOrigin"> </div> </div> </div> </div> <div class="col-xl-6"> <div class="card shadow h-100"> <div class="card-header"> <h6 class="mb-0">{{ t('ui.admin.binkp_config.binkp.title', {}, locale, ['common']) }}</h6> </div> <div class="card-body"> <div class="mb-3"> <label class="form-label" for="binkpPort">{{ t('ui.common.port', {}, locale, ['common']) }}</label> <input type="number" class="form-control" id="binkpPort" min="1" max="65535"> </div> <div class="mb-3"> <label class="form-label" for="binkpTimeout">{{ t('ui.admin.binkp_config.binkp.timeout_seconds', {}, locale, ['common']) }}</label> <input type="number" class="form-control" id="binkpTimeout" min="30" max="3600"> </div> <div class="mb-3"> <label class="form-label" for="binkpMaxConnections">{{ t('ui.admin.binkp_config.binkp.max_connections', {}, locale, ['common']) }}</label> <input type="number" class="form-control" id="binkpMaxConnections" min="1" max="100"> </div> <div class="mb-3"> <label class="form-label" for="binkpOutboundQueueTimer">{{ t('ui.admin.binkp_config.binkp.outbound_queue_timer', {}, locale, ['common']) }}</label> <input type="number" class="form-control" id="binkpOutboundQueueTimer" min="1"> <div class="form-text">{{ t('ui.admin.binkp_config.binkp.outbound_queue_timer_help', {}, locale, ['common']) }}</div> </div> <div class="mb-3"> <label class="form-label" for="binkpBindAddress">{{ t('ui.admin.binkp_config.binkp.bind_address', {}, locale, ['common']) }}</label> <input type="text" class="form-control" id="binkpBindAddress"> </div> <div class="form-check mb-3"> <input class="form-check-input" type="checkbox" id="binkpPreserveProcessed"> <label class="form-check-label" for="binkpPreserveProcessed">{{ t('ui.admin.binkp_config.binkp.preserve_processed_packets', {}, locale, ['common']) }}</label> </div> <div class="form-check mb-3"> <input class="form-check-input" type="checkbox" id="binkpPreserveSent"> <label class="form-check-label" for="binkpPreserveSent">{{ t('ui.admin.binkp_config.binkp.preserve_sent_packets', {}, locale, ['common']) }}</label> </div> </div> </div> </div> <div class="col-xl-6"> <div class="card shadow h-100"> <div class="card-header"> <h6 class="mb-0">{{ t('ui.admin.binkp_config.security.title', {}, locale, ['common']) }}</h6> </div> <div class="card-body"> <div class="form-check mb-2"> <input class="form-check-input" type="checkbox" id="securityAllowInsecureInbound"> <label class="form-check-label" for="securityAllowInsecureInbound">{{ t('ui.admin.binkp_config.security.allow_insecure_inbound', {}, locale, ['common']) }}</label> </div> <div class="form-check mb-2"> <input class="form-check-input" type="checkbox" id="securityInsecureReceiveOnly"> <label class="form-check-label" for="securityInsecureReceiveOnly">{{ t('ui.admin.binkp_config.security.insecure_receive_only', {}, locale, ['common']) }}</label> </div> <div class="form-check mb-2"> <input class="form-check-input" type="checkbox" id="securityInsecureAllowFreq"> <label class="form-check-label" for="securityInsecureAllowFreq">{{ t('ui.admin.binkp_config.security.insecure_allow_freq', {}, locale, ['common']) }}</label> </div> <div class="form-check mb-2"> <input class="form-check-input" type="checkbox" id="securityRequireAllowlist"> <label class="form-check-label" for="securityRequireAllowlist">{{ t('ui.admin.binkp_config.security.require_allowlist_for_insecure', {}, locale, ['common']) }}</label> </div> <div class="mb-3"> <label class="form-label" for="securityMaxInsecureSessions">{{ t('ui.admin.binkp_config.security.max_insecure_sessions_per_hour', {}, locale, ['common']) }}</label> <input type="number" class="form-control" id="securityMaxInsecureSessions" min="1"> </div> <div class="form-check"> <input class="form-check-input" type="checkbox" id="securityAllowPlaintextFallback"> <label class="form-check-label" for="securityAllowPlaintextFallback">{{ t('ui.admin.binkp_config.security.allow_plaintext_fallback', {}, locale, ['common']) }}</label> </div> </div> </div> </div> <div class="col-xl-6"> <div class="card shadow h-100"> <div class="card-header"> <h6 class="mb-0">{{ t('ui.admin.binkp_config.crashmail.title', {}, locale, ['common']) }}</h6> </div> <div class="card-body"> <div class="form-check mb-2"> <input class="form-check-input" type="checkbox" id="crashmailEnabled"> <label class="form-check-label" for="crashmailEnabled">{{ t('ui.admin.binkp_config.crashmail.enable_crashmail', {}, locale, ['common']) }}</label> </div> <div class="mb-3"> <label class="form-label" for="crashmailMaxAttempts">{{ t('ui.admin.binkp_config.crashmail.max_attempts', {}, locale, ['common']) }}</label> <input type="number" class="form-control" id="crashmailMaxAttempts" min="1"> </div> <div class="mb-3"> <label class="form-label" for="crashmailRetryInterval">{{ t('ui.admin.binkp_config.crashmail.retry_interval_minutes', {}, locale, ['common']) }}</label> <input type="number" class="form-control" id="crashmailRetryInterval" min="1"> </div> <div class="form-check mb-2"> <input class="form-check-input" type="checkbox" id="crashmailUseNodelist"> <label class="form-check-label" for="crashmailUseNodelist">{{ t('ui.admin.binkp_config.crashmail.use_nodelist_for_routing', {}, locale, ['common']) }}</label> </div> <div class="mb-3"> <label class="form-label" for="crashmailFallbackPort">{{ t('ui.admin.binkp_config.crashmail.fallback_port', {}, locale, ['common']) }}</label> <input type="number" class="form-control" id="crashmailFallbackPort" min="1" max="65535"> </div> <div class="form-check"> <input class="form-check-input" type="checkbox" id="crashmailAllowInsecure"> <label class="form-check-label" for="crashmailAllowInsecure">{{ t('ui.admin.binkp_config.crashmail.allow_insecure_crash_delivery', {}, locale, ['common']) }}</label> </div> </div> </div> </div> </div> <div class="card shadow mt-4" id="binkpUplinksCard"> <div class="card-header d-flex justify-content-between align-items-center"> <h6 class="mb-0">{{ t('ui.admin.binkp_config.uplinks.title', {}, locale, ['common']) }}</h6> <button class="btn btn-sm btn-outline-primary" type="button" onclick="openUplinkModal()">{{ t('ui.admin.binkp_config.uplinks.add_uplink', {}, locale, ['common']) }}</button> </div> <div class="card-body"> <div class="table-responsive"> <table class="table table-striped" id="uplinksTable"> <thead> <tr> <th class="d-none d-sm-table-cell">{{ t('ui.admin.binkp_config.uplinks.table.me', {}, locale, ['common']) }}</th> <th>{{ t('ui.admin.binkp_config.uplinks.table.uplink', {}, locale, ['common']) }}</th> <th class="d-none d-sm-table-cell">{{ t('ui.admin.binkp_config.uplinks.table.domain', {}, locale, ['common']) }}</th> <th class="d-none d-md-table-cell">{{ t('ui.admin.binkp_config.uplinks.table.host', {}, locale, ['common']) }}</th> <th class="d-none d-lg-table-cell">{{ t('ui.admin.binkp_config.uplinks.table.schedule', {}, locale, ['common']) }}</th> <th>{{ t('ui.admin.binkp_config.uplinks.table.status', {}, locale, ['common']) }}</th> <th>{{ t('ui.admin.binkp_config.uplinks.table.actions', {}, locale, ['common']) }}</th> </tr> </thead> <tbody></tbody> </table> </div> </div> </div> <div class="mt-4"> <button class="btn btn-primary" type="button" onclick="saveBinkpConfig()">{{ t('ui.admin.binkp_config.save_configuration', {}, locale, ['common']) }}</button> <button class="btn btn-secondary ms-2" type="button" onclick="reloadBinkpConfig()" title="{{ t('ui.admin.binkp_config.reload_title', {}, locale, ['common']) }}"> <i class="fas fa-sync-alt"></i> {{ t('ui.admin.binkp_config.reload_config', {}, locale, ['common']) }} </button> </div> </div> <div class="modal fade" id="uplinkModal" tabindex="-1" aria-labelledby="uplinkModalLabel" aria-hidden="true"> <div class="modal-dialog modal-xl"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="uplinkModalLabel">{{ t('ui.admin.binkp_config.uplinks.modal.uplink', {}, locale, ['common']) }}</h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ t('ui.common.close', {}, locale, ['common']) }}"></button> </div> <div class="modal-body"> <input type="hidden" id="uplinkIndex"> <div class="row g-3"> <div class="col-lg-7"> <div class="row g-3"> <div class="col-md-6"> <label class="form-label" for="uplinkMe">{{ t('ui.admin.binkp_config.uplinks.modal.me_address', {}, locale, ['common']) }}</label> <input type="text" class="form-control" id="uplinkMe"> </div> <div class="col-md-6"> <label class="form-label" for="uplinkAddress">{{ t('ui.admin.binkp_config.uplinks.modal.uplink_address', {}, locale, ['common']) }}</label> <input type="text" class="form-control" id="uplinkAddress"> </div> <div class="col-md-6"> <label class="form-label" for="uplinkDomain">{{ t('ui.admin.binkp_config.uplinks.modal.domain', {}, locale, ['common']) }}</label> <div class="input-group"> <select class="form-select" id="uplinkDomain"> {% for network in networks %} <option value="{{ network.domain }}">{{ network.name }} ({{ network.domain }})</option> {% endfor %} </select> <a class="btn btn-outline-secondary" href="/admin/networks" title="{{ t('ui.admin.binkp_config.uplinks.modal.edit_network_settings', {}, locale, ['common']) }}" aria-label="{{ t('ui.admin.binkp_config.uplinks.modal.edit_network_settings', {}, locale, ['common']) }}"> <i class="fas fa-pen"></i> </a> </div> </div> <div class="col-md-6"> <label class="form-label" for="uplinkHostname">{{ t('ui.common.hostname', {}, locale, ['common']) }}</label> <input type="text" class="form-control" id="uplinkHostname"> </div> <div class="col-md-4"> <label class="form-label" for="uplinkPort">{{ t('ui.common.port', {}, locale, ['common']) }}</label> <input type="number" class="form-control" id="uplinkPort" min="1" max="65535"> </div> <div class="col-md-8"> <label class="form-label" for="uplinkPassword">{{ t('ui.admin.binkp_config.uplinks.modal.binkp_session_password', {}, locale, ['common']) }}</label> <div class="input-group"> <input type="password" class="form-control" id="uplinkPassword"> <button class="btn btn-outline-secondary" type="button" onclick="toggleUplinkPasswordVisibility('uplinkPassword', this)" title="{{ t('ui.common.show_password', {}, locale, ['common']) }}" aria-label="{{ t('ui.common.show_password', {}, locale, ['common']) }}"> <i class="fas fa-eye"></i> </button> </div> </div> <div class="col-md-6"> <label class="form-label" for="uplinkPktPassword">{{ t('ui.admin.binkp_config.uplinks.modal.packet_password', {}, locale, ['common']) }} <small class="text-muted">({{ t('ui.common.optional', {}, locale, ['common']) }})</small></label> <div class="input-group"> <input type="password" class="form-control" id="uplinkPktPassword" maxlength="8"> <button class="btn btn-outline-secondary" type="button" onclick="toggleUplinkPasswordVisibility('uplinkPktPassword', this)" title="{{ t('ui.common.show_password', {}, locale, ['common']) }}" aria-label="{{ t('ui.common.show_password', {}, locale, ['common']) }}"> <i class="fas fa-eye"></i> </button> </div> <div class="form-text">{{ t('ui.admin.binkp_config.uplinks.modal.packet_password_help', {}, locale, ['common']) }}</div> </div> <div class="col-md-6"> <label class="form-label" for="uplinkTicPassword">{{ t('ui.admin.binkp_config.uplinks.modal.tic_password', {}, locale, ['common']) }} <small class="text-muted">({{ t('ui.common.optional', {}, locale, ['common']) }})</small></label> <div class="input-group"> <input type="password" class="form-control" id="uplinkTicPassword"> <button class="btn btn-outline-secondary" type="button" onclick="toggleUplinkPasswordVisibility('uplinkTicPassword', this)" title="{{ t('ui.common.show_password', {}, locale, ['common']) }}" aria-label="{{ t('ui.common.show_password', {}, locale, ['common']) }}"> <i class="fas fa-eye"></i> </button> </div> <div class="form-text">{{ t('ui.admin.binkp_config.uplinks.modal.tic_password_help', {}, locale, ['common']) }}</div> </div> <div class="col-md-6"> <label class="form-label" for="uplinkAreafixPassword">{{ t('ui.admin.binkp_config.uplinks.modal.areafix_password', {}, locale, ['common']) }} <small class="text-muted">({{ t('ui.common.optional', {}, locale, ['common']) }})</small></label> <div class="input-group"> <input type="password" class="form-control" id="uplinkAreafixPassword"> <button class="btn btn-outline-secondary" type="button" onclick="toggleUplinkPasswordVisibility('uplinkAreafixPassword', this)" title="{{ t('ui.common.show_password', {}, locale, ['common']) }}" aria-label="{{ t('ui.common.show_password', {}, locale, ['common']) }}"> <i class="fas fa-eye"></i> </button> </div> <div class="form-text">{{ t('ui.admin.binkp_config.uplinks.modal.areafix_password_help', {}, locale, ['common']) }}</div> </div> <div class="col-md-6"> <label class="form-label" for="uplinkFilefixPassword">{{ t('ui.admin.binkp_config.uplinks.modal.filefix_password', {}, locale, ['common']) }} <small class="text-muted">({{ t('ui.common.optional', {}, locale, ['common']) }})</small></label> <div class="input-group"> <input type="password" class="form-control" id="uplinkFilefixPassword"> <button class="btn btn-outline-secondary" type="button" onclick="toggleUplinkPasswordVisibility('uplinkFilefixPassword', this)" title="{{ t('ui.common.show_password', {}, locale, ['common']) }}" aria-label="{{ t('ui.common.show_password', {}, locale, ['common']) }}"> <i class="fas fa-eye"></i> </button> </div> <div class="form-text">{{ t('ui.admin.binkp_config.uplinks.modal.filefix_password_help', {}, locale, ['common']) }}</div> </div> <div class="col-md-12"> <label class="form-label" for="uplinkSchedule">{{ t('ui.admin.binkp_config.uplinks.modal.poll_schedule', {}, locale, ['common']) }}</label> <input type="text" class="form-control" id="uplinkSchedule"> <div class="form-text">{{ t('ui.admin.binkp_config.uplinks.modal.poll_schedule_help', {}, locale, ['common']) }}</div> </div> <div class="col-md-12"> <label class="form-label" for="uplinkNetworks">{{ t('ui.admin.binkp_config.uplinks.modal.networks_one_per_line', {}, locale, ['common']) }}</label> <textarea class="form-control" id="uplinkNetworks" rows="4"></textarea> </div> </div> </div> <div class="col-lg-5"> <div class="border rounded bg-light p-3 h-100"> <h6 class="mb-3">{{ t('ui.admin.binkp_config.uplinks.modal.connection_options', {}, locale, ['common']) }}</h6> <div class="form-check mb-3"> <input class="form-check-input" type="checkbox" id="uplinkEnabled"> <label class="form-check-label" for="uplinkEnabled" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ t('ui.admin.binkp_config.uplinks.modal.enabled_help', {}, locale, ['common']) }}">{{ t('ui.admin.binkp_config.uplinks.table.enabled', {}, locale, ['common']) }}</label> </div> <div class="form-check mb-3"> <input class="form-check-input" type="checkbox" id="uplinkSendDomainInAddr"> <label class="form-check-label" for="uplinkSendDomainInAddr" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ t('ui.admin.binkp_config.uplinks.modal.send_domain_help', {}, locale, ['common']) }}">{{ t('ui.admin.binkp_config.uplinks.modal.send_domain', {}, locale, ['common']) }}</label> </div> <div class="form-check mb-3"> <input class="form-check-input" type="checkbox" id="uplinkCompression"> <label class="form-check-label" for="uplinkCompression" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ t('ui.admin.binkp_config.uplinks.modal.compression_help', {}, locale, ['common']) }}">{{ t('ui.admin.binkp_config.uplinks.modal.compression', {}, locale, ['common']) }}</label> </div> <div class="form-check mb-3"> <input class="form-check-input" type="checkbox" id="uplinkCrypt"> <label class="form-check-label" for="uplinkCrypt" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ t('ui.admin.binkp_config.uplinks.modal.crypt_help', {}, locale, ['common']) }}">{{ t('ui.admin.binkp_config.uplinks.modal.crypt', {}, locale, ['common']) }}</label> </div> <div class="form-check mb-3"> <input class="form-check-input" type="checkbox" id="uplinkDefault"> <label class="form-check-label" for="uplinkDefault" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ t('ui.admin.binkp_config.uplinks.modal.default_help', {}, locale, ['common']) }}">{{ t('ui.admin.binkp_config.uplinks.table.default', {}, locale, ['common']) }}</label> </div> <div class="form-check"> <input class="form-check-input" type="checkbox" id="uplinkAllowInsecureEchomail"> <label class="form-check-label" for="uplinkAllowInsecureEchomail" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ t('ui.admin.binkp_config.uplinks.modal.allow_insecure_echomail_help', {}, locale, ['common']) }}">{{ t('ui.admin.binkp_config.uplinks.modal.allow_insecure_echomail', {}, locale, ['common']) }}</label> </div> </div> </div> </div> <div id="uplinkModalAlert" class="mt-3"></div> </div> <div class="modal-footer"> <button class="btn btn-secondary" type="button" data-bs-dismiss="modal">{{ t('ui.common.cancel', {}, locale, ['common']) }}</button> <button class="btn btn-primary" type="button" onclick="saveUplink()">{{ t('ui.admin.binkp_config.uplinks.modal.save_uplink', {}, locale, ['common']) }}</button> </div> </div> </div> </div> {% endblock %} {% block scripts %} <script src="/js/cron-builder.js"></script> <script> let binkpConfig = null; let uplinkModal; let uplinkCheckStates = {}; const configuredNetworkDomains = {{ networks|map(n => n.domain)|json_encode|raw }}; const requestedUplinkAddress = new URLSearchParams(window.location.search).get('uplink'); function uiT(key, fallback, params = {}) { if (window.t) { return window.t(key, params, fallback); } return fallback; } document.addEventListener('DOMContentLoaded', function() { uplinkModal = new bootstrap.Modal(document.getElementById('uplinkModal')); document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(function(element) { new bootstrap.Tooltip(element); }); loadI18nNamespaces(['common', 'errors']).then(function() { CronBuilder.init('uplinkSchedule'); loadBinkpConfig(); loadAdminUsers('systemSysop'); }); document.getElementById('uplinkModal').addEventListener('shown.bs.modal', function () { CronBuilder.refresh('uplinkSchedule'); }); }); function loadBinkpConfig() { setSettingsLoading('#binkpConfigCards, #binkpUplinksCard', true); fetch('/admin/api/binkp-config') .then(response => response.json()) .then(data => { if (!data.success) { throw new Error(apiError(data, uiT('ui.admin.binkp_config.load_failed', 'Failed to load config'))); } binkpConfig = data.config || {}; populateForm(); renderUplinks(); openRequestedUplinkEditor(); setSettingsLoading('#binkpConfigCards, #binkpUplinksCard', false); }) .catch(error => { showBinkpAlert(error.message || uiT('ui.admin.binkp_config.load_failed', 'Failed to load config'), 'danger'); }); } function populateForm() { const system = binkpConfig.system || {}; document.getElementById('systemName').value = system.name || ''; document.getElementById('systemAddress').value = system.address || ''; document.getElementById('systemSysop').dataset.currentValue = system.sysop || ''; document.getElementById('systemLocation').value = system.location || ''; document.getElementById('systemHostname').value = system.hostname || ''; document.getElementById('systemTimezone').value = system.timezone || ''; document.getElementById('systemOrigin').value = system.origin || ''; syncSysopSelect('systemSysop'); const binkp = binkpConfig.binkp || {}; document.getElementById('binkpPort').value = binkp.port ?? ''; document.getElementById('binkpTimeout').value = binkp.timeout ?? ''; document.getElementById('binkpMaxConnections').value = binkp.max_connections ?? ''; document.getElementById('binkpOutboundQueueTimer').value = binkp.outbound_queue_timer_minutes ?? 30; document.getElementById('binkpBindAddress').value = binkp.bind_address || ''; document.getElementById('binkpPreserveProcessed').checked = !!binkp.preserve_processed_packets; document.getElementById('binkpPreserveSent').checked = !!binkp.preserve_sent_packets; const security = binkpConfig.security || {}; document.getElementById('securityAllowInsecureInbound').checked = !!security.allow_insecure_inbound; document.getElementById('securityInsecureReceiveOnly').checked = !!security.insecure_inbound_receive_only; document.getElementById('securityInsecureAllowFreq').checked = !!security.insecure_allow_freq; document.getElementById('securityRequireAllowlist').checked = !!security.require_allowlist_for_insecure; document.getElementById('securityMaxInsecureSessions').value = security.max_insecure_sessions_per_hour ?? ''; document.getElementById('securityAllowPlaintextFallback').checked = !!security.allow_plaintext_fallback; const crashmail = binkpConfig.crashmail || {}; document.getElementById('crashmailEnabled').checked = !!crashmail.enabled; document.getElementById('crashmailMaxAttempts').value = crashmail.max_attempts ?? ''; document.getElementById('crashmailRetryInterval').value = crashmail.retry_interval_minutes ?? ''; document.getElementById('crashmailUseNodelist').checked = !!crashmail.use_nodelist_for_routing; document.getElementById('crashmailFallbackPort').value = crashmail.fallback_port ?? ''; document.getElementById('crashmailAllowInsecure').checked = !!crashmail.allow_insecure_crash_delivery; } function gatherConfig() { return { system: { name: document.getElementById('systemName').value.trim(), address: document.getElementById('systemAddress').value.trim(), sysop: document.getElementById('systemSysop').value, location: document.getElementById('systemLocation').value.trim(), hostname: document.getElementById('systemHostname').value.trim(), timezone: document.getElementById('systemTimezone').value.trim(), origin: document.getElementById('systemOrigin').value.trim() }, binkp: { port: parseInt(document.getElementById('binkpPort').value, 10), timeout: parseInt(document.getElementById('binkpTimeout').value, 10), max_connections: parseInt(document.getElementById('binkpMaxConnections').value, 10), outbound_queue_timer_minutes: parseInt(document.getElementById('binkpOutboundQueueTimer').value, 10), bind_address: document.getElementById('binkpBindAddress').value.trim(), preserve_processed_packets: document.getElementById('binkpPreserveProcessed').checked, preserve_sent_packets: document.getElementById('binkpPreserveSent').checked }, uplinks: binkpConfig.uplinks || [], security: { allow_insecure_inbound: document.getElementById('securityAllowInsecureInbound').checked, insecure_inbound_receive_only: document.getElementById('securityInsecureReceiveOnly').checked, insecure_allow_freq: document.getElementById('securityInsecureAllowFreq').checked, require_allowlist_for_insecure: document.getElementById('securityRequireAllowlist').checked, max_insecure_sessions_per_hour: parseInt(document.getElementById('securityMaxInsecureSessions').value, 10), allow_plaintext_fallback: document.getElementById('securityAllowPlaintextFallback').checked }, crashmail: { enabled: document.getElementById('crashmailEnabled').checked, max_attempts: parseInt(document.getElementById('crashmailMaxAttempts').value, 10), retry_interval_minutes: parseInt(document.getElementById('crashmailRetryInterval').value, 10), use_nodelist_for_routing: document.getElementById('crashmailUseNodelist').checked, fallback_port: parseInt(document.getElementById('crashmailFallbackPort').value, 10), allow_insecure_crash_delivery: document.getElementById('crashmailAllowInsecure').checked }, }; } function validateConfig(config) { if (!config.system.name || !config.system.address || !config.system.sysop) { return uiT('ui.admin.binkp_config.validation.system_required', 'System name, address, and sysop are required.'); } if (!Number.isInteger(config.binkp.port) || config.binkp.port < 1 || config.binkp.port > 65535) { return uiT('ui.admin.binkp_config.validation.port_range', 'Binkp port must be between 1 and 65535.'); } if (!Number.isInteger(config.binkp.timeout) || config.binkp.timeout < 1) { return uiT('ui.admin.binkp_config.validation.timeout_positive', 'Binkp timeout must be a positive number.'); } if (!Number.isInteger(config.binkp.max_connections) || config.binkp.max_connections < 1) { return uiT('ui.admin.binkp_config.validation.max_connections_positive', 'Max connections must be a positive number.'); } if (!Number.isInteger(config.binkp.outbound_queue_timer_minutes) || config.binkp.outbound_queue_timer_minutes < 1) { return uiT('ui.admin.binkp_config.validation.outbound_queue_timer_positive', 'Outbound queue timer must be a positive number of minutes.'); } return null; } function saveBinkpConfig() { if (!binkpConfig) { showBinkpAlert(uiT('ui.admin.binkp_config.config_not_loaded', 'Config not loaded.'), 'danger'); return; } const config = gatherConfig(); const error = validateConfig(config); if (error) { showBinkpAlert(error, 'danger'); return; } fetch('/admin/api/binkp-config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ config }) }) .then(response => response.json()) .then(data => { if (!data.success) { throw new Error(apiError(data, uiT('ui.admin.binkp_config.save_failed', 'Failed to save config'))); } binkpConfig = data.config || config; renderUplinks(); showBinkpAlert(apiMessage(data, uiT('ui.admin.binkp_config.configuration_saved', 'Configuration saved.')), 'success'); }) .catch(error => { showBinkpAlert(error.message || uiT('ui.admin.binkp_config.save_failed', 'Failed to save config'), 'danger'); }); } function reloadBinkpConfig() { if (!confirm(uiT('ui.admin.binkp_config.reload_confirm', 'Reload binkp daemon configuration? This will send SIGHUP to the daemon.'))) { return; } showBinkpAlert(uiT('ui.admin.binkp_config.reloading', 'Reloading configuration...'), 'info'); fetch('/admin/api/binkp-reload', { method: 'POST', headers: { 'Content-Type': 'application/json' } }) .then(response => response.json()) .then(data => { if (!data.success) { throw new Error(apiError(data, uiT('ui.admin.binkp_config.reload_failed', 'Failed to reload config'))); } showBinkpAlert( apiMessage(data, uiT('ui.admin.binkp_config.reloaded_success', 'Configuration reloaded successfully. Daemon has picked up changes.')), 'success' ); }) .catch(error => { showBinkpAlert(error.message || uiT('ui.admin.binkp_config.reload_failed', 'Failed to reload config'), 'danger'); }); } function renderUplinks() { const tbody = document.querySelector('#uplinksTable tbody'); tbody.innerHTML = ''; const uplinks = binkpConfig.uplinks || []; if (uplinks.length === 0) { tbody.innerHTML = '<tr><td colspan="7" class="text-muted">' + escapeHtml(uiT('ui.admin.binkp_config.uplinks.none_configured', 'No uplinks configured.')) + '</td></tr>'; return; } uplinks.forEach((uplink, index) => { const row = document.createElement('tr'); const address = String(uplink.address || ''); const host = uplink.hostname ? escapeHtml(uplink.hostname) + (uplink.port ? ':' + uplink.port : '') : ''; const enabledBadge = uplink.enabled ? `<span class="badge bg-success">${uiT('ui.admin.binkp_config.uplinks.table.enabled', 'Enabled')}</span>` : `<span class="badge bg-secondary">${uiT('ui.admin.binkp_config.uplinks.table.disabled', 'Disabled')}</span>`; const defaultBadge = uplink.default ? ` <span class="badge bg-primary">${uiT('ui.admin.binkp_config.uplinks.table.default', 'Default')}</span>` : ''; row.innerHTML = ` <td class="d-none d-sm-table-cell">${escapeHtml(uplink.me || '')}</td> <td>${escapeHtml(address)}</td> <td class="d-none d-sm-table-cell">${escapeHtml(uplink.domain || '')}</td> <td class="d-none d-md-table-cell">${host}</td> <td class="d-none d-lg-table-cell"><code>${escapeHtml(uplink.poll_schedule || '')}</code></td> <td>${enabledBadge}${defaultBadge}</td> <td> <button class="btn btn-sm btn-outline-primary" type="button" onclick="openUplinkModal(${index})">${uiT('ui.common.edit', 'Edit')}</button> ${renderUplinkCheckButton(address)} <button class="btn btn-sm btn-outline-danger ms-1" type="button" onclick="deleteUplink(${index})">${uiT('ui.common.delete', 'Delete')}</button> </td> `; tbody.appendChild(row); }); } function openRequestedUplinkEditor() { if (!requestedUplinkAddress) { return; } const uplinks = binkpConfig.uplinks || []; const index = uplinks.findIndex(uplink => String(uplink.address || '').toLowerCase() === requestedUplinkAddress.toLowerCase()); if (index === -1) { showBinkpAlert(uiT('ui.admin.binkp_config.uplinks.not_found', 'Uplink not found.'), 'warning'); return; } openUplinkModal(index); } function renderUplinkCheckButton(address) { const disabled = address === ''; const state = uplinkCheckStates[address] || 'idle'; if (state === 'checking') { return `<button class="btn btn-sm btn-outline-secondary ms-1" type="button" disabled><span class="spinner-border spinner-border-sm" aria-hidden="true"></span><span class="visually-hidden">${escapeHtml(uiT('ui.binkp.checking', 'Checking...'))}</span></button>`; } if (state === 'success') { return `<button class="btn btn-sm btn-success ms-1" type="button" onclick="checkUplinkConnectivity('${escapeJsString(address)}')" title="${escapeHtmlAttr(uiT('ui.admin.binkp_config.uplinks.check_success', 'Connectivity check succeeded'))}" aria-label="${escapeHtmlAttr(uiT('ui.admin.binkp_config.uplinks.check_success', 'Connectivity check succeeded'))}"><i class="fas fa-check"></i></button>`; } return `<button class="btn btn-sm btn-outline-secondary ms-1" type="button" onclick="checkUplinkConnectivity('${escapeJsString(address)}')"${disabled ? ' disabled' : ''}>${escapeHtml(uiT('ui.binkp.check_status', 'Check'))}</button>`; } function checkUplinkConnectivity(address) { if (!address) { return; } uplinkCheckStates[address] = 'checking'; renderUplinks(); fetch(`/api/binkp/uplink-status?address=${encodeURIComponent(address)}`) .then(response => response.json().then(data => ({ response, data }))) .then(({ response, data }) => { if (!response.ok || !data.success) { throw new Error(apiError(data, uiT('ui.admin.binkp_config.uplinks.check_failed', 'Connectivity check failed'))); } uplinkCheckStates[address] = 'success'; renderUplinks(); }) .catch(() => { uplinkCheckStates[address] = 'idle'; renderUplinks(); window.alert(uiT('ui.admin.binkp_config.uplinks.check_failed', 'Connectivity check failed')); }); } function apiError(payload, fallback) { if (window.getApiErrorMessage) { return window.getApiErrorMessage(payload, fallback); } if (payload && payload.error) { return String(payload.error); } return fallback; } function apiMessage(payload, fallback) { if (window.getApiMessage) { return window.getApiMessage(payload, fallback); } if (payload && payload.message) { return String(payload.message); } return fallback; } function toggleUplinkPasswordVisibility(inputId, button) { const input = document.getElementById(inputId); if (!input || !button) { return; } const icon = button.querySelector('i'); const isHidden = input.type === 'password'; const nextLabel = isHidden ? uiT('ui.common.hide_password', 'Hide password') : uiT('ui.common.show_password', 'Show password'); input.type = isHidden ? 'text' : 'password'; button.setAttribute('title', nextLabel); button.setAttribute('aria-label', nextLabel); if (icon) { icon.classList.toggle('fa-eye', !isHidden); icon.classList.toggle('fa-eye-slash', isHidden); } } function resetUplinkPasswordVisibility() { ['uplinkPassword', 'uplinkPktPassword', 'uplinkTicPassword'].forEach(function(inputId) { const input = document.getElementById(inputId); if (!input) { return; } input.type = 'password'; const button = input.parentElement ? input.parentElement.querySelector('button') : null; const icon = button ? button.querySelector('i') : null; const label = uiT('ui.common.show_password', 'Show password'); if (button) { button.setAttribute('title', label); button.setAttribute('aria-label', label); } if (icon) { icon.classList.remove('fa-eye-slash'); icon.classList.add('fa-eye'); } }); } function openUplinkModal(index = null) { document.getElementById('uplinkModalAlert').innerHTML = ''; if (index === null) { document.getElementById('uplinkModalLabel').textContent = uiT('ui.admin.binkp_config.uplinks.add_uplink', 'Add Uplink'); document.getElementById('uplinkIndex').value = ''; document.getElementById('uplinkMe').value = ''; document.getElementById('uplinkAddress').value = ''; document.getElementById('uplinkDomain').value = configuredNetworkDomains[0] || ''; document.getElementById('uplinkHostname').value = ''; document.getElementById('uplinkPort').value = 24554; document.getElementById('uplinkPassword').value = ''; document.getElementById('uplinkPktPassword').value = ''; document.getElementById('uplinkTicPassword').value = ''; document.getElementById('uplinkAreafixPassword').value = ''; document.getElementById('uplinkFilefixPassword').value = ''; document.getElementById('uplinkSchedule').value = '0 */4 * * *'; document.getElementById('uplinkNetworks').value = ''; document.getElementById('uplinkSendDomainInAddr').checked = false; document.getElementById('uplinkEnabled').checked = true; document.getElementById('uplinkCompression').checked = false; document.getElementById('uplinkCrypt').checked = false; document.getElementById('uplinkDefault').checked = false; document.getElementById('uplinkAllowInsecureEchomail').checked = false; } else { const uplink = (binkpConfig.uplinks || [])[index]; if (!uplink) { return; } document.getElementById('uplinkModalLabel').textContent = uiT('ui.admin.binkp_config.uplinks.edit_uplink', 'Edit Uplink'); document.getElementById('uplinkIndex').value = index; document.getElementById('uplinkMe').value = uplink.me || ''; document.getElementById('uplinkAddress').value = uplink.address || ''; ensureUplinkDomainOption(uplink.domain || ''); document.getElementById('uplinkDomain').value = uplink.domain || ''; document.getElementById('uplinkHostname').value = uplink.hostname || ''; document.getElementById('uplinkPort').value = uplink.port ?? 24554; document.getElementById('uplinkPassword').value = uplink.password || ''; document.getElementById('uplinkPktPassword').value = uplink.pkt_password || ''; document.getElementById('uplinkTicPassword').value = uplink.tic_password || ''; document.getElementById('uplinkAreafixPassword').value = uplink.areafix_password || ''; document.getElementById('uplinkFilefixPassword').value = uplink.filefix_password || ''; document.getElementById('uplinkSchedule').value = uplink.poll_schedule || ''; document.getElementById('uplinkNetworks').value = Array.isArray(uplink.networks) ? uplink.networks.join('\n') : ''; document.getElementById('uplinkSendDomainInAddr').checked = !!uplink.send_domain_in_addr; document.getElementById('uplinkEnabled').checked = !!uplink.enabled; document.getElementById('uplinkAllowInsecureEchomail').checked = !!uplink.allow_insecure_echomail; document.getElementById('uplinkCompression').checked = !!uplink.compression; document.getElementById('uplinkCrypt').checked = !!uplink.crypt; document.getElementById('uplinkDefault').checked = !!uplink.default; } resetUplinkPasswordVisibility(); uplinkModal.show(); } function ensureUplinkDomainOption(domain) { if (!domain || configuredNetworkDomains.includes(domain)) { return; } const select = document.getElementById('uplinkDomain'); if (!select || Array.from(select.options).some(option => option.value === domain)) { return; } const option = document.createElement('option'); option.value = domain; option.textContent = `${domain} (${uiT('ui.admin.binkp_config.uplinks.modal.unconfigured_network', 'unconfigured')})`; select.appendChild(option); } function saveUplink() { const indexValue = document.getElementById('uplinkIndex').value; const uplink = { me: document.getElementById('uplinkMe').value.trim(), address: document.getElementById('uplinkAddress').value.trim(), domain: document.getElementById('uplinkDomain').value.trim(), hostname: document.getElementById('uplinkHostname').value.trim(), port: parseInt(document.getElementById('uplinkPort').value, 10), password: document.getElementById('uplinkPassword').value.trim(), pkt_password: document.getElementById('uplinkPktPassword').value.trim(), tic_password: document.getElementById('uplinkTicPassword').value.trim(), areafix_password: document.getElementById('uplinkAreafixPassword').value.trim(), filefix_password: document.getElementById('uplinkFilefixPassword').value.trim(), poll_schedule: document.getElementById('uplinkSchedule').value.trim(), networks: document.getElementById('uplinkNetworks').value.split('\n').map(v => v.trim()).filter(Boolean), send_domain_in_addr: document.getElementById('uplinkSendDomainInAddr').checked, enabled: document.getElementById('uplinkEnabled').checked, compression: document.getElementById('uplinkCompression').checked, crypt: document.getElementById('uplinkCrypt').checked, default: document.getElementById('uplinkDefault').checked, allow_insecure_echomail: document.getElementById('uplinkAllowInsecureEchomail').checked }; const error = validateUplink(uplink); if (error) { showUplinkModalAlert(error, 'danger'); return; } if (!Array.isArray(binkpConfig.uplinks)) { binkpConfig.uplinks = []; } if (uplink.default) { binkpConfig.uplinks = binkpConfig.uplinks.map(existing => ({ ...existing, default: false })); } if (indexValue === '') { binkpConfig.uplinks.push(uplink); } else { binkpConfig.uplinks[parseInt(indexValue, 10)] = uplink; } renderUplinks(); uplinkModal.hide(); } function deleteUplink(index) { if (!confirm(uiT('ui.admin.binkp_config.remove_uplink_confirm', 'Remove this uplink?'))) { return; } binkpConfig.uplinks.splice(index, 1); renderUplinks(); } function validateUplink(uplink) { if (!uplink.hostname) { return uiT('ui.admin.binkp_config.validation.uplink_hostname_required', 'Uplink hostname is required.'); } if (!uplink.domain) { return uiT('ui.admin.binkp_config.validation.uplink_domain_required', 'Network is required.'); } if (!configuredNetworkDomains.includes(uplink.domain)) { return uiT('ui.admin.binkp_config.validation.uplink_domain_unknown', 'Select a configured network.'); } if (!Number.isInteger(uplink.port) || uplink.port < 1 || uplink.port > 65535) { return uiT('ui.admin.binkp_config.validation.uplink_port_range', 'Uplink port must be between 1 and 65535.'); } return null; } function showUplinkModalAlert(message, type) { const container = document.getElementById('uplinkModalAlert'); container.innerHTML = ` <div class="alert alert-${type} alert-dismissible fade show" role="alert"> ${message} <button type="button" class="btn-close" data-bs-dismiss="alert"></button> </div> `; } function showBinkpAlert(message, type) { const container = document.getElementById('binkpConfigAlert'); container.innerHTML = ` <div class="alert alert-${type} alert-dismissible fade show" role="alert"> ${message} <button type="button" class="btn-close" data-bs-dismiss="alert"></button> </div> `; } function escapeHtml(value) { return String(value) .replaceAll('&', '&amp;') .replaceAll('<', '&lt;') .replaceAll('>', '&gt;') .replaceAll('"', '&quot;') .replaceAll("'", '&#039;'); } function escapeHtmlAttr(value) { return escapeHtml(value).replace(/"/g, '&quot;'); } function escapeJsString(value) { return String(value) .replaceAll('\\', '\\\\') .replaceAll("'", "\\'"); } function loadAdminUsers(selectId) { fetch('/admin/api/admin-users') .then(response => response.json()) .then(data => { const list = Array.isArray(data.admins) ? data.admins : []; const select = document.getElementById(selectId); select.innerHTML = ''; list.forEach(name => { const option = document.createElement('option'); option.value = name; option.textContent = name; select.appendChild(option); }); syncSysopSelect(selectId); }) .catch(() => {}); } function syncSysopSelect(selectId) { const select = document.getElementById(selectId); const current = select.dataset.currentValue || ''; if (!current) { return; } const exists = Array.from(select.options).some(opt => opt.value === current); if (!exists) { const option = document.createElement('option'); option.value = current; option.textContent = `${current} (${uiT('ui.admin.binkp_config.current', 'current')})`; select.insertBefore(option, select.firstChild); } select.value = current; } </script> {% endblock %}