diff --git a/docker/routlin-dash/app/action_apply_ddns_ip_check.py b/docker/routlin-dash/app/action_apply_ddns_ip_check.py new file mode 100644 index 0000000..2bfdc53 --- /dev/null +++ b/docker/routlin-dash/app/action_apply_ddns_ip_check.py @@ -0,0 +1,31 @@ +from flask import Blueprint, request, redirect, flash +from auth import require_level +from config_utils import load_core, save_core, verify_core_hash + +bp = Blueprint('action_apply_ddns_ip_check', __name__) + +VIEW = '/view/view_ddns' + + +@bp.route('/action/ddns_ip_check_save', methods=['POST']) +@require_level('administrator') +def ddns_ip_check_save(): + if not verify_core_hash(request.form.get('config_hash', '')): + flash('Configuration was modified by another session. Please refresh and try again.', 'error') + return redirect(VIEW) + + http_services = [u.strip() for u in request.form.getlist('http_services') if u.strip()] + dig_services = [u.strip() for u in request.form.getlist('dig_services') if u.strip()] + + if not http_services and not dig_services: + flash('At least one IP check service is required.', 'error') + return redirect(VIEW) + + services = [{'type': 'http', 'url': u} for u in http_services] + services += [{'type': 'dig', 'url': u} for u in dig_services] + + core = load_core() + core.setdefault('ddns', {})['ip_check_services'] = services + save_core(core) + flash('IP check services saved.', 'success') + return redirect(VIEW) diff --git a/docker/routlin-dash/app/main.py b/docker/routlin-dash/app/main.py index 364a081..7ade6cb 100644 --- a/docker/routlin-dash/app/main.py +++ b/docker/routlin-dash/app/main.py @@ -23,6 +23,7 @@ from action_save_preferences import bp as action_save_preferences_bp from action_change_password import bp as action_change_password_bp from action_clear_ddns_log import bp as action_clear_ddns_log_bp from action_apply_ddns_providers import bp as action_apply_ddns_providers_bp +from action_apply_ddns_ip_check import bp as action_apply_ddns_ip_check_bp from api_apply_status import bp as api_apply_status_bp app = Flask(__name__) @@ -50,6 +51,7 @@ app.register_blueprint(action_save_preferences_bp) app.register_blueprint(action_change_password_bp) app.register_blueprint(action_clear_ddns_log_bp) app.register_blueprint(action_apply_ddns_providers_bp) +app.register_blueprint(action_apply_ddns_ip_check_bp) app.register_blueprint(api_apply_status_bp) def _seed_initial_account(): diff --git a/docker/routlin-dash/app/view_page.py b/docker/routlin-dash/app/view_page.py index 1a79481..2cdd096 100644 --- a/docker/routlin-dash/app/view_page.py +++ b/docker/routlin-dash/app/view_page.py @@ -324,10 +324,10 @@ def _config_datasource(name): if ptype == 'noip': row['credentials'] = (f'
' f'U: {e(p.get("username", "-"))}
' - f'P: •••
') + f'P: ••••••') elif ptype in ('cloudflare', 'duckdns'): tok = p.get('api_token', '') - row['credentials'] = f'API Token: {e(tok[:24])}...' if tok else '(not set)' + row['credentials'] = f'API Token: {e(tok[:20])}...' if tok else '(not set)' else: row['credentials'] = '-' row['hostnames'] = json.dumps(p.get('hostnames', p.get('subdomains', []))) @@ -686,6 +686,13 @@ def collect_tokens(): tokens['DDNS_GEN_LOG_ERRORS_ONLY'] = 'true' if ddns_gen.get('log_errors_only') else 'false' enabled_p = [p for p in ddns.get('providers', []) if p.get('enabled', True)] tokens['STAT_DDNS_PROVIDER_COUNT'] = str(len(enabled_p)) + _ip_check = ddns.get('ip_check_services', []) + _http_svc = [s['url'] for s in _ip_check if s.get('type') == 'http'] + _dig_svc = [s['url'] for s in _ip_check if s.get('type') == 'dig'] + tokens['STAT_IP_CHECK_TOTAL'] = str(len(_ip_check)) + tokens['STAT_IP_CHECK_SUB'] = f'{len(_http_svc)} http and {len(_dig_svc)} dig' + tokens['IP_CHECK_HTTP_JSON'] = json.dumps(_http_svc) + tokens['IP_CHECK_DIG_JSON'] = json.dumps(_dig_svc) _ddns_labels = {'noip': 'No-IP', 'cloudflare': 'Cloudflare', 'duckdns': 'DuckDNS'} tokens['DDNS_PROVIDER_OPTIONS'] = json.dumps([ {'value': p, 'label': _ddns_labels.get(p, p.title())} @@ -872,8 +879,9 @@ def _render_item(item, tokens, inherited_req=None): return f'{text}' if t == 'button_cancel': - text = e(apply_tokens(item.get('text', 'Cancel'), tokens)) - return f'' + text = e(apply_tokens(item.get('text', 'Cancel'), tokens)) + extra_cls = (' ' + item['class']) if item.get('class') else '' + return f'' if t == 'page_header': return f'' @@ -902,6 +910,19 @@ def _render_item(item, tokens, inherited_req=None): edit_suffix = item.get('edit_suffix', '') edit_min = item.get('edit_min', '') edit_raw = apply_tokens(item.get('edit_value', item.get('value', '')), tokens) + reveal_card_id = item.get('reveal_card_id', '') + if reveal_card_id: + return ( + f'
' + f'
{label}
' + f'
' + f'{value}' + f'' + f'
' + f'
{sub}
' + f'
' + ) if edit_action and edit_field: min_attr = f' min="{e(edit_min)}"' if edit_min else '' suffix_html = f'{e(edit_suffix)}' if edit_suffix else '' @@ -2596,6 +2617,12 @@ function startApplyPoller(uuid, bar, mine) { document.querySelectorAll('.pre-block[data-scroll-bottom]').forEach(function(el) { el.scrollTop = el.scrollHeight; }); +document.querySelectorAll('[data-reveal-card]').forEach(function(btn) { + btn.addEventListener('click', function() { + var card = document.getElementById(btn.dataset.revealCard); + if (card) card.style.display = card.style.display === 'none' ? '' : 'none'; + }); +}); (function() { document.querySelectorAll('.stat-card-editable').forEach(function(card) { var form = card.querySelector('.stat-card-edit-form'); diff --git a/docker/routlin-dash/data/page_content.json b/docker/routlin-dash/data/page_content.json index 591926d..5e91f82 100644 --- a/docker/routlin-dash/data/page_content.json +++ b/docker/routlin-dash/data/page_content.json @@ -329,9 +329,56 @@ }, { "type": "stat_card", - "label": "Providers", - "value": "%STAT_DDNS_PROVIDER_COUNT%", - "sub": "configured" + "label": "IP Check Services", + "value": "%STAT_IP_CHECK_TOTAL%", + "sub": "%STAT_IP_CHECK_SUB%", + "reveal_card_id": "ip-check-services-edit" + } + ] + }, + { + "type": "card", + "id": "ip-check-services-edit", + "label": "IP Check Services", + "hidden": true, + "client_requirement": "client_is_administrator+", + "items": [ + { + "type": "form", + "action": "/action/ddns_ip_check_save", + "method": "post", + "items": [ + { + "type": "editable_list", + "label": "HTTP APIs", + "name": "http_services", + "item_placeholder": "https://...", + "add_label": "Add HTTP API", + "items": "%IP_CHECK_HTTP_JSON%" + }, + { + "type": "editable_list", + "label": "Dig APIs", + "name": "dig_services", + "item_placeholder": "e.g. @1.1.1.1 ch txt whoami.cloudflare", + "add_label": "Add Dig API", + "items": "%IP_CHECK_DIG_JSON%" + }, + { + "type": "button_row", + "items": [ + { + "type": "button_primary", + "text": "Save" + }, + { + "type": "button_cancel", + "text": "Cancel", + "class": "js-hide-card" + } + ] + } + ] } ] },