Development
This commit is contained in:
parent
d8d1d46fd2
commit
eed1d295dc
69 changed files with 3355 additions and 3230 deletions
|
|
@ -1,47 +0,0 @@
|
||||||
from flask import Blueprint, request, session, redirect, flash
|
|
||||||
import json
|
|
||||||
from auth import require_level
|
|
||||||
from config_utils import ACCOUNTS_FILE
|
|
||||||
import sanitize
|
|
||||||
|
|
||||||
bp = Blueprint('action_save_preferences', __name__)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def _load_accounts():
|
|
||||||
try:
|
|
||||||
with open(ACCOUNTS_FILE) as f:
|
|
||||||
return json.load(f)
|
|
||||||
except Exception:
|
|
||||||
return {'accounts': []}
|
|
||||||
|
|
||||||
def _save_accounts(data):
|
|
||||||
with open(ACCOUNTS_FILE, 'w') as f:
|
|
||||||
json.dump(data, f, indent=2)
|
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/action/save_preferences', methods=['POST'])
|
|
||||||
@require_level('viewer')
|
|
||||||
def save_preferences():
|
|
||||||
tz = sanitize.timezone(request.form.get('timezone', '').strip())
|
|
||||||
|
|
||||||
if not tz:
|
|
||||||
flash('Timezone is required.', 'error')
|
|
||||||
return redirect('/view/view_preferences')
|
|
||||||
|
|
||||||
email = session.get('email_address', '').lower()
|
|
||||||
data = _load_accounts()
|
|
||||||
accounts = data.get('accounts', [])
|
|
||||||
account = next((a for a in accounts if a.get('email_address', '').lower() == email), None)
|
|
||||||
|
|
||||||
if account is None:
|
|
||||||
flash('Account not found. Please log in again.', 'error')
|
|
||||||
return redirect('/view/view_log_in')
|
|
||||||
|
|
||||||
account['timezone'] = tz
|
|
||||||
_save_accounts(data)
|
|
||||||
|
|
||||||
session['timezone'] = tz
|
|
||||||
|
|
||||||
flash('Preferences saved.', 'success')
|
|
||||||
return redirect('/view/view_preferences')
|
|
||||||
|
|
@ -13,7 +13,7 @@ def require_level(minimum):
|
||||||
if LEVEL_RANK.get(current, 0) < LEVEL_RANK.get(minimum, 0):
|
if LEVEL_RANK.get(current, 0) < LEVEL_RANK.get(minimum, 0):
|
||||||
if current == 'nothing':
|
if current == 'nothing':
|
||||||
flash('Please log in to continue.', 'error')
|
flash('Please log in to continue.', 'error')
|
||||||
return redirect('/view/view_log_in')
|
return redirect('/view/view_login')
|
||||||
flash('You do not have permission to perform this action.', 'error')
|
flash('You do not have permission to perform this action.', 'error')
|
||||||
return redirect('/view/view_overview')
|
return redirect('/view/view_overview')
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
import copy, json, subprocess, hashlib, os, uuid
|
import copy, json, subprocess, hashlib, os, uuid
|
||||||
|
import os as _os
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from flask import session
|
from flask import session
|
||||||
|
|
||||||
|
APP_DIR = _os.path.dirname(_os.path.abspath(__file__))
|
||||||
CONFIGS_DIR = '/routlin_location'
|
CONFIGS_DIR = '/routlin_location'
|
||||||
DATA_DIR = '/data'
|
DATA_DIR = '/data'
|
||||||
WWW_DIR = '/www'
|
WWW_DIR = '/www'
|
||||||
ACCOUNTS_FILE = f'{DATA_DIR}/authorized_accounts.json'
|
ACCOUNTS_FILE = f'{APP_DIR}/authorized_accounts.json'
|
||||||
CONFIG_FILE = f'{CONFIGS_DIR}/config.json'
|
CONFIG_FILE = f'{CONFIGS_DIR}/config.json'
|
||||||
DASHBOARD_QUEUE = f'{CONFIGS_DIR}/.dashboard-queue'
|
DASHBOARD_QUEUE = f'{CONFIGS_DIR}/.dashboard-queue'
|
||||||
DASHBOARD_DONE = f'{CONFIGS_DIR}/.dashboard-done'
|
DASHBOARD_DONE = f'{CONFIGS_DIR}/.dashboard-done'
|
||||||
|
|
|
||||||
|
|
@ -1,60 +1,59 @@
|
||||||
import os, json, sys
|
import os, json, sys
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
from config_utils import ACCOUNTS_FILE
|
||||||
from view_page import bp as view_page_bp
|
from view_page import bp as view_page_bp
|
||||||
from action_actions import bp as action_actions_bp
|
from pages.actions.action import bp as actions_bp
|
||||||
from action_physicalinterfaces import bp as action_physicalinterfaces_bp
|
from pages.bannedips.action import bp as bannedips_bp
|
||||||
from action_dnsserver import bp as action_dnsserver_bp
|
from pages.ddns.action import bp as ddns_bp
|
||||||
from action_apply_mdns import bp as action_apply_mdns_bp
|
from pages.dhcp.action import bp as dhcp_bp
|
||||||
from action_apply_vpn import bp as action_apply_vpn_bp
|
from pages.dnsblocking.action import bp as dnsblocking_bp
|
||||||
from action_apply_banned_ips import bp as action_apply_banned_ips_bp
|
from pages.dnsserver.action import bp as dnsserver_bp
|
||||||
from action_apply_host_overrides import bp as action_apply_host_overrides_bp
|
from pages.hostoverrides.action import bp as hostoverrides_bp
|
||||||
from action_dnsblocking import bp as action_dnsblocking_bp
|
from pages.intervlan.action import bp as intervlan_bp
|
||||||
from action_networklayout import bp as action_networklayout_bp
|
from pages.accountlogin.action import bp as accountlogin_bp
|
||||||
from action_apply_inter_vlan import bp as action_apply_inter_vlan_bp
|
from pages.networklayout.action import bp as networklayout_bp
|
||||||
from action_apply_port_forwarding import bp as action_apply_port_forwarding_bp
|
from pages.physicalinterfaces.action import bp as physicalinterfaces_bp
|
||||||
from action_apply_dhcp_reservations import bp as action_apply_dhcp_reservations_bp
|
from pages.portforwarding.action import bp as portforwarding_bp
|
||||||
from action_create_account import bp as action_create_account_bp
|
from pages.preferences.action import bp as preferences_bp
|
||||||
from action_log_in import bp as action_log_in_bp
|
from pages.accountverifyemail.action import bp as accountverifyemail_bp
|
||||||
from action_log_out import bp as action_log_out_bp
|
from pages.vpn.action import bp as vpn_bp
|
||||||
from action_verify_email import bp as action_verify_email_bp
|
from pages.accountcreate.action import bp as accountcreate_bp
|
||||||
from action_add_account import bp as action_add_account_bp
|
from pages.accountadd.action import bp as accountadd_bp
|
||||||
from action_delete_account import bp as action_delete_account_bp
|
from pages.accountdelete.action import bp as accountdelete_bp
|
||||||
from action_save_preferences import bp as action_save_preferences_bp
|
from pages.accountlogout.action import bp as accountlogout_bp
|
||||||
from action_change_password import bp as action_change_password_bp
|
from pages.mdns.action import bp as mdns_bp
|
||||||
from action_ddns import bp as action_ddns_bp
|
|
||||||
from api_apply_health import bp as api_apply_health_bp
|
from api_apply_health import bp as api_apply_health_bp
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.secret_key = os.environ.get('SECRET_KEY', os.urandom(24))
|
app.secret_key = os.environ.get('SECRET_KEY', os.urandom(24))
|
||||||
app.register_blueprint(view_page_bp)
|
app.register_blueprint(view_page_bp)
|
||||||
app.register_blueprint(action_actions_bp)
|
app.register_blueprint(actions_bp)
|
||||||
app.register_blueprint(action_physicalinterfaces_bp)
|
app.register_blueprint(bannedips_bp)
|
||||||
app.register_blueprint(action_dnsserver_bp)
|
app.register_blueprint(ddns_bp)
|
||||||
app.register_blueprint(action_apply_mdns_bp)
|
app.register_blueprint(dhcp_bp)
|
||||||
app.register_blueprint(action_apply_vpn_bp)
|
app.register_blueprint(dnsblocking_bp)
|
||||||
app.register_blueprint(action_apply_banned_ips_bp)
|
app.register_blueprint(dnsserver_bp)
|
||||||
app.register_blueprint(action_apply_host_overrides_bp)
|
app.register_blueprint(hostoverrides_bp)
|
||||||
app.register_blueprint(action_dnsblocking_bp)
|
app.register_blueprint(intervlan_bp)
|
||||||
app.register_blueprint(action_networklayout_bp)
|
app.register_blueprint(accountlogin_bp)
|
||||||
app.register_blueprint(action_apply_inter_vlan_bp)
|
app.register_blueprint(networklayout_bp)
|
||||||
app.register_blueprint(action_apply_port_forwarding_bp)
|
app.register_blueprint(physicalinterfaces_bp)
|
||||||
app.register_blueprint(action_apply_dhcp_reservations_bp)
|
app.register_blueprint(portforwarding_bp)
|
||||||
app.register_blueprint(action_create_account_bp)
|
app.register_blueprint(preferences_bp)
|
||||||
app.register_blueprint(action_log_in_bp)
|
app.register_blueprint(accountverifyemail_bp)
|
||||||
app.register_blueprint(action_log_out_bp)
|
app.register_blueprint(vpn_bp)
|
||||||
app.register_blueprint(action_verify_email_bp)
|
app.register_blueprint(accountcreate_bp)
|
||||||
app.register_blueprint(action_add_account_bp)
|
app.register_blueprint(accountadd_bp)
|
||||||
app.register_blueprint(action_delete_account_bp)
|
app.register_blueprint(accountdelete_bp)
|
||||||
app.register_blueprint(action_save_preferences_bp)
|
app.register_blueprint(accountlogout_bp)
|
||||||
app.register_blueprint(action_change_password_bp)
|
app.register_blueprint(mdns_bp)
|
||||||
app.register_blueprint(action_ddns_bp)
|
|
||||||
app.register_blueprint(api_apply_health_bp)
|
app.register_blueprint(api_apply_health_bp)
|
||||||
|
|
||||||
def _seed_initial_account():
|
def _seed_initial_account():
|
||||||
email = os.environ.get('INITIAL_MANAGER_EMAIL', '').strip().lower()
|
email = os.environ.get('INITIAL_MANAGER_EMAIL', '').strip().lower()
|
||||||
if not email:
|
if not email:
|
||||||
try:
|
try:
|
||||||
with open(accounts_file) as f:
|
with open(ACCOUNTS_FILE) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
except Exception:
|
except Exception:
|
||||||
data = {'accounts': []}
|
data = {'accounts': []}
|
||||||
|
|
@ -62,9 +61,8 @@ def _seed_initial_account():
|
||||||
print('[main] WARNING: No accounts exist and INITIAL_MANAGER_EMAIL is not set. '
|
print('[main] WARNING: No accounts exist and INITIAL_MANAGER_EMAIL is not set. '
|
||||||
'Set it in docker-compose.yml to seed the initial manager account.', file=sys.stderr)
|
'Set it in docker-compose.yml to seed the initial manager account.', file=sys.stderr)
|
||||||
return
|
return
|
||||||
accounts_file = '/data/authorized_accounts.json'
|
|
||||||
try:
|
try:
|
||||||
with open(accounts_file) as f:
|
with open(ACCOUNTS_FILE) as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
except Exception:
|
except Exception:
|
||||||
data = {'accounts': []}
|
data = {'accounts': []}
|
||||||
|
|
@ -76,7 +74,7 @@ def _seed_initial_account():
|
||||||
'hashed_password': '',
|
'hashed_password': '',
|
||||||
'timezone': '',
|
'timezone': '',
|
||||||
}]
|
}]
|
||||||
with open(accounts_file, 'w') as f:
|
with open(ACCOUNTS_FILE, 'w') as f:
|
||||||
json.dump(data, f, indent=2)
|
json.dump(data, f, indent=2)
|
||||||
print(f'[main] Seeded initial manager account: {email}', file=sys.stderr)
|
print(f'[main] Seeded initial manager account: {email}', file=sys.stderr)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,17 +11,17 @@
|
||||||
"label": "%MENU_LABEL%",
|
"label": "%MENU_LABEL%",
|
||||||
"client_requirement": "client_is_viewer+",
|
"client_requirement": "client_is_viewer+",
|
||||||
"items": [
|
"items": [
|
||||||
{ "type": "nav_item", "label": "Physical Interfaces", "map_to": "view_physical_interfaces", "client_requirement": "client_is_administrator+" },
|
{ "type": "nav_item", "label": "Physical Interfaces", "map_to": "view_physicalinterfaces", "client_requirement": "client_is_administrator+" },
|
||||||
{ "type": "nav_item", "label": "DNS Server", "map_to": "view_dns_server", "client_requirement": "client_is_administrator+" },
|
{ "type": "nav_item", "label": "DNS Server", "map_to": "view_dnsserver", "client_requirement": "client_is_administrator+" },
|
||||||
{ "type": "nav_item", "label": "DNS Blocking", "map_to": "view_dns_blocking", "client_requirement": "client_is_administrator+" },
|
{ "type": "nav_item", "label": "DNS Blocking", "map_to": "view_dnsblocking", "client_requirement": "client_is_administrator+" },
|
||||||
{ "type": "nav_item", "label": "Network Layout", "map_to": "view_network_layout", "client_requirement": "client_is_administrator+" },
|
{ "type": "nav_item", "label": "Network Layout", "map_to": "view_networklayout", "client_requirement": "client_is_administrator+" },
|
||||||
{ "type": "nav_item", "label": "Inter-VLAN Exceptions", "map_to": "view_inter_vlan", "client_requirement": "client_is_administrator+" },
|
{ "type": "nav_item", "label": "Inter-VLAN Exceptions", "map_to": "view_intervlan", "client_requirement": "client_is_administrator+" },
|
||||||
{ "type": "nav_item", "label": "Port Forwarding", "map_to": "view_port_forwarding", "client_requirement": "client_is_administrator+" },
|
{ "type": "nav_item", "label": "Port Forwarding", "map_to": "view_portforwarding", "client_requirement": "client_is_administrator+" },
|
||||||
{ "type": "nav_item", "label": "DHCP", "map_to": "view_dhcp" },
|
{ "type": "nav_item", "label": "DHCP", "map_to": "view_dhcp" },
|
||||||
{ "type": "nav_item", "label": "DDNS", "map_to": "view_ddns" },
|
{ "type": "nav_item", "label": "DDNS", "map_to": "view_ddns" },
|
||||||
{ "type": "nav_item", "label": "Host Overrides", "map_to": "view_host_overrides", "client_requirement": "client_is_administrator+" },
|
{ "type": "nav_item", "label": "Host Overrides", "map_to": "view_hostoverrides", "client_requirement": "client_is_administrator+" },
|
||||||
{ "type": "nav_item", "label": "VPN", "map_to": "view_vpn" },
|
{ "type": "nav_item", "label": "VPN", "map_to": "view_vpn" },
|
||||||
{ "type": "nav_item", "label": "Banned IPs", "map_to": "view_banned_ips", "client_requirement": "client_is_administrator+" }
|
{ "type": "nav_item", "label": "Banned IPs", "map_to": "view_bannedips", "client_requirement": "client_is_administrator+" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -37,21 +37,21 @@
|
||||||
"client_requirement": "client_is_viewer+",
|
"client_requirement": "client_is_viewer+",
|
||||||
"items": [
|
"items": [
|
||||||
{ "type": "nav_item", "label": "Preferences", "map_to": "view_preferences" },
|
{ "type": "nav_item", "label": "Preferences", "map_to": "view_preferences" },
|
||||||
{ "type": "nav_item", "label": "Manage Accounts", "map_to": "view_manage_accounts", "client_requirement": "client_is_manager+" },
|
{ "type": "nav_item", "label": "Manage Accounts", "map_to": "view_manageaccounts", "client_requirement": "client_is_manager+" },
|
||||||
{ "type": "nav_action", "label": "Log Out", "action": "log_out" }
|
{ "type": "nav_action", "label": "Log Out", "action": "log_out" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "nav_item",
|
"type": "nav_item",
|
||||||
"label": "Log In",
|
"label": "Log In",
|
||||||
"map_to": "view_log_in",
|
"map_to": "view_login",
|
||||||
"align": "right",
|
"align": "right",
|
||||||
"client_requirement": "client_is_nothing="
|
"client_requirement": "client_is_nothing="
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "nav_item",
|
"type": "nav_item",
|
||||||
"label": "Create Account",
|
"label": "Create Account",
|
||||||
"map_to": "view_create_account",
|
"map_to": "view_createaccount",
|
||||||
"align": "right",
|
"align": "right",
|
||||||
"client_requirement": "client_is_nothing="
|
"client_requirement": "client_is_nothing="
|
||||||
}
|
}
|
||||||
0
docker/routlin-dash/app/pages/__init__.py
Normal file
0
docker/routlin-dash/app/pages/__init__.py
Normal file
0
docker/routlin-dash/app/pages/accountadd/__init__.py
Normal file
0
docker/routlin-dash/app/pages/accountadd/__init__.py
Normal file
|
|
@ -5,7 +5,7 @@ from auth import require_level
|
||||||
from config_utils import ACCOUNTS_FILE
|
from config_utils import ACCOUNTS_FILE
|
||||||
import sanitize
|
import sanitize
|
||||||
|
|
||||||
bp = Blueprint('action_add_account', __name__)
|
bp = Blueprint('accountadd', __name__)
|
||||||
|
|
||||||
|
|
||||||
VALID_LEVELS = {'viewer', 'administrator', 'manager'}
|
VALID_LEVELS = {'viewer', 'administrator', 'manager'}
|
||||||
|
|
@ -31,22 +31,22 @@ def add_account():
|
||||||
|
|
||||||
if not email:
|
if not email:
|
||||||
flash('Email address is required.', 'error')
|
flash('Email address is required.', 'error')
|
||||||
return redirect('/view/view_manage_accounts')
|
return redirect('/view/view_manageaccounts')
|
||||||
|
|
||||||
if not re.match(r'^[^@\s]+@[^@\s]+\.[^@\s]+$', email):
|
if not re.match(r'^[^@\s]+@[^@\s]+\.[^@\s]+$', email):
|
||||||
flash('Email address does not appear to be valid.', 'error')
|
flash('Email address does not appear to be valid.', 'error')
|
||||||
return redirect('/view/view_manage_accounts')
|
return redirect('/view/view_manageaccounts')
|
||||||
|
|
||||||
if access_level not in VALID_LEVELS:
|
if access_level not in VALID_LEVELS:
|
||||||
flash('Invalid access level.', 'error')
|
flash('Invalid access level.', 'error')
|
||||||
return redirect('/view/view_manage_accounts')
|
return redirect('/view/view_manageaccounts')
|
||||||
|
|
||||||
data = _load_accounts()
|
data = _load_accounts()
|
||||||
accounts = data.get('accounts', [])
|
accounts = data.get('accounts', [])
|
||||||
|
|
||||||
if any(a.get('email_address', '').lower() == email for a in accounts):
|
if any(a.get('email_address', '').lower() == email for a in accounts):
|
||||||
flash('An account with that email address already exists.', 'error')
|
flash('An account with that email address already exists.', 'error')
|
||||||
return redirect('/view/view_manage_accounts')
|
return redirect('/view/view_manageaccounts')
|
||||||
|
|
||||||
now = datetime.now(tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
now = datetime.now(tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||||
accounts.append({
|
accounts.append({
|
||||||
|
|
@ -61,4 +61,4 @@ def add_account():
|
||||||
_save_accounts(data)
|
_save_accounts(data)
|
||||||
|
|
||||||
flash(f'Authorization added for {email}. User must complete account setup via the Create Account page.', 'success')
|
flash(f'Authorization added for {email}. User must complete account setup via the Create Account page.', 'success')
|
||||||
return redirect('/view/view_manage_accounts')
|
return redirect('/view/view_manageaccounts')
|
||||||
0
docker/routlin-dash/app/pages/accountcreate/__init__.py
Normal file
0
docker/routlin-dash/app/pages/accountcreate/__init__.py
Normal file
|
|
@ -6,7 +6,7 @@ from auth import require_level
|
||||||
from config_utils import WEB_APP_DISPLAY_NAME, ACCOUNTS_FILE
|
from config_utils import WEB_APP_DISPLAY_NAME, ACCOUNTS_FILE
|
||||||
import sanitize
|
import sanitize
|
||||||
|
|
||||||
bp = Blueprint('action_create_account', __name__)
|
bp = Blueprint('accountcreate', __name__)
|
||||||
|
|
||||||
CODE_TTL_MIN = 15
|
CODE_TTL_MIN = 15
|
||||||
|
|
||||||
|
|
@ -62,26 +62,26 @@ def create_account():
|
||||||
|
|
||||||
if not email or not password or not password_confirm or not tz:
|
if not email or not password or not password_confirm or not tz:
|
||||||
flash('All fields are required.', 'error')
|
flash('All fields are required.', 'error')
|
||||||
return redirect('/view/view_create_account')
|
return redirect('/view/view_createaccount')
|
||||||
|
|
||||||
if password != password_confirm:
|
if password != password_confirm:
|
||||||
flash('Passwords do not match.', 'error')
|
flash('Passwords do not match.', 'error')
|
||||||
return redirect('/view/view_create_account')
|
return redirect('/view/view_createaccount')
|
||||||
|
|
||||||
if len(password) < 8:
|
if len(password) < 8:
|
||||||
flash('Password must be at least 8 characters.', 'error')
|
flash('Password must be at least 8 characters.', 'error')
|
||||||
return redirect('/view/view_create_account')
|
return redirect('/view/view_createaccount')
|
||||||
|
|
||||||
accounts = _load_accounts().get('accounts', [])
|
accounts = _load_accounts().get('accounts', [])
|
||||||
account = next((a for a in accounts if a.get('email_address', '').lower() == email), None)
|
account = next((a for a in accounts if a.get('email_address', '').lower() == email), None)
|
||||||
|
|
||||||
if account is None:
|
if account is None:
|
||||||
flash('Email address not recognised. Contact your manager.', 'error')
|
flash('Email address not recognised. Contact your manager.', 'error')
|
||||||
return redirect('/view/view_create_account')
|
return redirect('/view/view_createaccount')
|
||||||
|
|
||||||
if account.get('hashed_password'):
|
if account.get('hashed_password'):
|
||||||
flash('This account is already set up. Please log in instead.', 'error')
|
flash('This account is already set up. Please log in instead.', 'error')
|
||||||
return redirect('/view/view_create_account')
|
return redirect('/view/view_createaccount')
|
||||||
|
|
||||||
salt = bcrypt.gensalt()
|
salt = bcrypt.gensalt()
|
||||||
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
|
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
|
||||||
|
|
@ -92,7 +92,7 @@ def create_account():
|
||||||
_send_verification_email(account['email_address'], code)
|
_send_verification_email(account['email_address'], code)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
flash(f'Could not send verification email: {exc}', 'error')
|
flash(f'Could not send verification email: {exc}', 'error')
|
||||||
return redirect('/view/view_create_account')
|
return redirect('/view/view_createaccount')
|
||||||
|
|
||||||
session['pending_create_account'] = {
|
session['pending_create_account'] = {
|
||||||
'email': account['email_address'],
|
'email': account['email_address'],
|
||||||
|
|
@ -102,4 +102,4 @@ def create_account():
|
||||||
'expires': expires,
|
'expires': expires,
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect('/view/view_verify_email')
|
return redirect('/view/view_verifyemail')
|
||||||
103
docker/routlin-dash/app/pages/accountcreate/content.json
Normal file
103
docker/routlin-dash/app/pages/accountcreate/content.json
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
{
|
||||||
|
"id": "view_createaccount",
|
||||||
|
"client_requirement": "client_is_nothing=",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "auth_wrapper",
|
||||||
|
"client_requirement": "client_is_nothing=",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "auth_card",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "h1",
|
||||||
|
"text": "Complete Your Account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "If your email has been pre-registered by a manager, setup your account below."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "hr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/create_account",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Email Address",
|
||||||
|
"name": "email",
|
||||||
|
"input_type": "text",
|
||||||
|
"placeholder": "you@example.com",
|
||||||
|
"hint": "Must match your pre-registered email address."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "New Password",
|
||||||
|
"name": "password",
|
||||||
|
"input_type": "password",
|
||||||
|
"placeholder": "Choose a strong password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Confirm Password",
|
||||||
|
"name": "password_confirm",
|
||||||
|
"input_type": "password",
|
||||||
|
"placeholder": "Repeat your password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Timezone",
|
||||||
|
"name": "timezone",
|
||||||
|
"input_type": "select",
|
||||||
|
"value": "",
|
||||||
|
"options": "%TIMEZONE_OPTIONS%",
|
||||||
|
"hint": "Used to display timestamps in your local time."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/create_account",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Create Account",
|
||||||
|
"class": "btn-full"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "Already have an account?",
|
||||||
|
"link": {
|
||||||
|
"action": "/view/view_login",
|
||||||
|
"text": "Log In"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "section",
|
||||||
|
"client_requirement": "client_is_viewer+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "h1",
|
||||||
|
"text": "Already logged in."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "Your account is already active."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spacer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/view/overview",
|
||||||
|
"text": "Go to Overview"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
0
docker/routlin-dash/app/pages/accountdelete/__init__.py
Normal file
0
docker/routlin-dash/app/pages/accountdelete/__init__.py
Normal file
|
|
@ -3,7 +3,7 @@ import json
|
||||||
from auth import require_level
|
from auth import require_level
|
||||||
from config_utils import ACCOUNTS_FILE
|
from config_utils import ACCOUNTS_FILE
|
||||||
|
|
||||||
bp = Blueprint('action_delete_account', __name__)
|
bp = Blueprint('accountdelete', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -26,20 +26,20 @@ def delete_account():
|
||||||
row_index = int(request.form.get('row_index', ''))
|
row_index = int(request.form.get('row_index', ''))
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
flash('Invalid request.', 'error')
|
flash('Invalid request.', 'error')
|
||||||
return redirect('/view/view_manage_accounts')
|
return redirect('/view/view_manageaccounts')
|
||||||
|
|
||||||
data = _load_accounts()
|
data = _load_accounts()
|
||||||
accounts = data.get('accounts', [])
|
accounts = data.get('accounts', [])
|
||||||
|
|
||||||
if row_index < 0 or row_index >= len(accounts):
|
if row_index < 0 or row_index >= len(accounts):
|
||||||
flash('Account not found.', 'error')
|
flash('Account not found.', 'error')
|
||||||
return redirect('/view/view_manage_accounts')
|
return redirect('/view/view_manageaccounts')
|
||||||
|
|
||||||
target = accounts[row_index]
|
target = accounts[row_index]
|
||||||
|
|
||||||
if target.get('email_address', '').lower() == session.get('email_address', '').lower():
|
if target.get('email_address', '').lower() == session.get('email_address', '').lower():
|
||||||
flash('You cannot remove your own account.', 'error')
|
flash('You cannot remove your own account.', 'error')
|
||||||
return redirect('/view/view_manage_accounts')
|
return redirect('/view/view_manageaccounts')
|
||||||
|
|
||||||
removed_email = target.get('email_address', '')
|
removed_email = target.get('email_address', '')
|
||||||
accounts.pop(row_index)
|
accounts.pop(row_index)
|
||||||
|
|
@ -47,4 +47,4 @@ def delete_account():
|
||||||
_save_accounts(data)
|
_save_accounts(data)
|
||||||
|
|
||||||
flash(f'Account for {removed_email} has been removed.', 'success')
|
flash(f'Account for {removed_email} has been removed.', 'success')
|
||||||
return redirect('/view/view_manage_accounts')
|
return redirect('/view/view_manageaccounts')
|
||||||
0
docker/routlin-dash/app/pages/accountlogin/__init__.py
Normal file
0
docker/routlin-dash/app/pages/accountlogin/__init__.py
Normal file
|
|
@ -4,7 +4,7 @@ from auth import require_level
|
||||||
from config_utils import ACCOUNTS_FILE
|
from config_utils import ACCOUNTS_FILE
|
||||||
import sanitize
|
import sanitize
|
||||||
|
|
||||||
bp = Blueprint('action_log_in', __name__)
|
bp = Blueprint('accountlogin', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -28,23 +28,23 @@ def log_in():
|
||||||
|
|
||||||
if not email or not password:
|
if not email or not password:
|
||||||
flash('Email address and password are required.', 'error')
|
flash('Email address and password are required.', 'error')
|
||||||
return redirect('/view/view_log_in')
|
return redirect('/view/view_login')
|
||||||
|
|
||||||
accounts = _load_accounts().get('accounts', [])
|
accounts = _load_accounts().get('accounts', [])
|
||||||
account = next((a for a in accounts if a.get('email_address', '').lower() == email), None)
|
account = next((a for a in accounts if a.get('email_address', '').lower() == email), None)
|
||||||
|
|
||||||
if account is None:
|
if account is None:
|
||||||
flash('Email address not recognised.', 'error')
|
flash('Email address not recognised.', 'error')
|
||||||
return redirect('/view/view_log_in')
|
return redirect('/view/view_login')
|
||||||
|
|
||||||
if not account.get('hashed_password'):
|
if not account.get('hashed_password'):
|
||||||
flash('Account setup is not complete. Please use Create Account to set your password first.', 'error')
|
flash('Account setup is not complete. Please use Create Account to set your password first.', 'error')
|
||||||
return redirect('/view/view_log_in')
|
return redirect('/view/view_login')
|
||||||
|
|
||||||
stored_hash = account['hashed_password'].encode('utf-8')
|
stored_hash = account['hashed_password'].encode('utf-8')
|
||||||
if not bcrypt.checkpw(password.encode('utf-8'), stored_hash):
|
if not bcrypt.checkpw(password.encode('utf-8'), stored_hash):
|
||||||
flash('Invalid email address or password.', 'error')
|
flash('Invalid email address or password.', 'error')
|
||||||
return redirect('/view/view_log_in')
|
return redirect('/view/view_login')
|
||||||
|
|
||||||
session.clear()
|
session.clear()
|
||||||
session['email_address'] = account['email_address']
|
session['email_address'] = account['email_address']
|
||||||
86
docker/routlin-dash/app/pages/accountlogin/content.json
Normal file
86
docker/routlin-dash/app/pages/accountlogin/content.json
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
{
|
||||||
|
"id": "view_login",
|
||||||
|
"client_requirement": "client_is_nothing=",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "auth_wrapper",
|
||||||
|
"client_requirement": "client_is_nothing=",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "auth_card",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "h1",
|
||||||
|
"text": "Log In"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "Enter your credentials to access the dashboard."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "hr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/log_in",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Email Address",
|
||||||
|
"name": "email",
|
||||||
|
"input_type": "text",
|
||||||
|
"placeholder": "you@example.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Password",
|
||||||
|
"name": "password",
|
||||||
|
"input_type": "password",
|
||||||
|
"placeholder": "Password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/log_in",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Log In",
|
||||||
|
"class": "btn-full"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "Need to complete your account?",
|
||||||
|
"link": {
|
||||||
|
"action": "/view/view_createaccount",
|
||||||
|
"text": "Create Account"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "section",
|
||||||
|
"client_requirement": "client_is_viewer+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "h1",
|
||||||
|
"text": "Already logged in."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "You are already authenticated."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spacer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/view/overview",
|
||||||
|
"text": "Go to Overview"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
0
docker/routlin-dash/app/pages/accountlogout/__init__.py
Normal file
0
docker/routlin-dash/app/pages/accountlogout/__init__.py
Normal file
|
|
@ -1,7 +1,7 @@
|
||||||
from flask import Blueprint, session, redirect
|
from flask import Blueprint, session, redirect
|
||||||
from auth import require_level
|
from auth import require_level
|
||||||
|
|
||||||
bp = Blueprint('action_log_out', __name__)
|
bp = Blueprint('accountlogout', __name__)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/action/log_out', methods=['POST'])
|
@bp.route('/action/log_out', methods=['POST'])
|
||||||
0
docker/routlin-dash/app/pages/accountmanage/__init__.py
Normal file
0
docker/routlin-dash/app/pages/accountmanage/__init__.py
Normal file
90
docker/routlin-dash/app/pages/accountmanage/content.json
Normal file
90
docker/routlin-dash/app/pages/accountmanage/content.json
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
{
|
||||||
|
"id": "view_manageaccounts",
|
||||||
|
"client_requirement": "client_is_manager+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "header_page_title",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "h1",
|
||||||
|
"text": "Manage Accounts"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"datasource": "config:accounts",
|
||||||
|
"empty_message": "No accounts configured.",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"label": "Email Address",
|
||||||
|
"field": "email_address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Access Level",
|
||||||
|
"field": "access_level"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Added By",
|
||||||
|
"field": "account_created_by"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Added",
|
||||||
|
"field": "account_created_utc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Status",
|
||||||
|
"field": "account_status",
|
||||||
|
"render": "badge_active_inactive"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"row_actions": [
|
||||||
|
{
|
||||||
|
"action": "/action/delete_account",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Remove",
|
||||||
|
"class": "btn-danger btn-sm"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"label": "Authorize New Account",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/add_account",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Email Address",
|
||||||
|
"name": "email_address",
|
||||||
|
"input_type": "text",
|
||||||
|
"placeholder": "user@example.com",
|
||||||
|
"hint": "The user will verify ownership of this address during account setup."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Access Level",
|
||||||
|
"name": "access_level",
|
||||||
|
"input_type": "select",
|
||||||
|
"options": "%ACCOUNT_LEVEL_OPTIONS%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/add_account",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Authorize"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ from datetime import datetime, timezone, timedelta
|
||||||
from auth import require_level
|
from auth import require_level
|
||||||
from config_utils import ACCOUNTS_FILE
|
from config_utils import ACCOUNTS_FILE
|
||||||
|
|
||||||
bp = Blueprint('action_verify_email', __name__)
|
bp = Blueprint('accountverifyemail', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -31,18 +31,18 @@ def verify_email():
|
||||||
|
|
||||||
if not pending:
|
if not pending:
|
||||||
flash('No pending account creation found. Please start over.', 'error')
|
flash('No pending account creation found. Please start over.', 'error')
|
||||||
return redirect('/view/view_create_account')
|
return redirect('/view/view_createaccount')
|
||||||
|
|
||||||
expires = datetime.fromisoformat(pending['expires'])
|
expires = datetime.fromisoformat(pending['expires'])
|
||||||
if datetime.now(tz=timezone.utc) > expires:
|
if datetime.now(tz=timezone.utc) > expires:
|
||||||
session.pop('pending_create_account', None)
|
session.pop('pending_create_account', None)
|
||||||
flash('Verification code has expired. Please start over.', 'error')
|
flash('Verification code has expired. Please start over.', 'error')
|
||||||
return redirect('/view/view_create_account')
|
return redirect('/view/view_createaccount')
|
||||||
|
|
||||||
submitted = request.form.get('code', '').strip()
|
submitted = request.form.get('code', '').strip()
|
||||||
if submitted != pending['code']:
|
if submitted != pending['code']:
|
||||||
flash('Incorrect verification code.', 'error')
|
flash('Incorrect verification code.', 'error')
|
||||||
return redirect('/view/view_verify_email')
|
return redirect('/view/view_verifyemail')
|
||||||
|
|
||||||
data = _load_accounts()
|
data = _load_accounts()
|
||||||
accounts = data.get('accounts', [])
|
accounts = data.get('accounts', [])
|
||||||
|
|
@ -54,12 +54,12 @@ def verify_email():
|
||||||
if account is None:
|
if account is None:
|
||||||
session.pop('pending_create_account', None)
|
session.pop('pending_create_account', None)
|
||||||
flash('Account no longer exists. Contact your manager.', 'error')
|
flash('Account no longer exists. Contact your manager.', 'error')
|
||||||
return redirect('/view/view_create_account')
|
return redirect('/view/view_createaccount')
|
||||||
|
|
||||||
if account.get('hashed_password'):
|
if account.get('hashed_password'):
|
||||||
session.pop('pending_create_account', None)
|
session.pop('pending_create_account', None)
|
||||||
flash('This account is already set up. Please log in.', 'error')
|
flash('This account is already set up. Please log in.', 'error')
|
||||||
return redirect('/view/view_log_in')
|
return redirect('/view/view_login')
|
||||||
|
|
||||||
now = datetime.now(tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
now = datetime.now(tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||||
account['hashed_password'] = pending['hashed_password']
|
account['hashed_password'] = pending['hashed_password']
|
||||||
|
|
@ -87,13 +87,13 @@ def resend_verification():
|
||||||
if session.get('access_level', 'nothing') != 'nothing':
|
if session.get('access_level', 'nothing') != 'nothing':
|
||||||
return redirect('/view/view_overview')
|
return redirect('/view/view_overview')
|
||||||
|
|
||||||
from action_create_account import _send_verification_email, CODE_TTL_MIN
|
from pages.accountcreate.action import _send_verification_email, CODE_TTL_MIN
|
||||||
|
|
||||||
pending = session.get('pending_create_account')
|
pending = session.get('pending_create_account')
|
||||||
|
|
||||||
if not pending:
|
if not pending:
|
||||||
flash('No pending account creation found. Please start over.', 'error')
|
flash('No pending account creation found. Please start over.', 'error')
|
||||||
return redirect('/view/view_create_account')
|
return redirect('/view/view_createaccount')
|
||||||
|
|
||||||
code = f'{secrets.randbelow(1000000):06d}'
|
code = f'{secrets.randbelow(1000000):06d}'
|
||||||
expires = (datetime.now(tz=timezone.utc) + timedelta(minutes=CODE_TTL_MIN)).isoformat()
|
expires = (datetime.now(tz=timezone.utc) + timedelta(minutes=CODE_TTL_MIN)).isoformat()
|
||||||
|
|
@ -102,11 +102,11 @@ def resend_verification():
|
||||||
_send_verification_email(pending['email'], code)
|
_send_verification_email(pending['email'], code)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
flash(f'Could not resend verification email: {exc}', 'error')
|
flash(f'Could not resend verification email: {exc}', 'error')
|
||||||
return redirect('/view/view_verify_email')
|
return redirect('/view/view_verifyemail')
|
||||||
|
|
||||||
pending['code'] = code
|
pending['code'] = code
|
||||||
pending['expires'] = expires
|
pending['expires'] = expires
|
||||||
session['pending_create_account'] = pending
|
session['pending_create_account'] = pending
|
||||||
|
|
||||||
flash('A new verification code has been sent.', 'success')
|
flash('A new verification code has been sent.', 'success')
|
||||||
return redirect('/view/view_verify_email')
|
return redirect('/view/view_verifyemail')
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
{
|
||||||
|
"id": "view_verifyemail",
|
||||||
|
"client_requirement": "client_is_nothing=",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "auth_wrapper",
|
||||||
|
"client_requirement": "client_is_nothing=",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "auth_card",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "h1",
|
||||||
|
"text": "Verify Your Email"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "A 6-digit code was sent to your email address. Enter it below to complete your account."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/verify_email",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Verification Code",
|
||||||
|
"name": "code",
|
||||||
|
"input_type": "text",
|
||||||
|
"placeholder": "000000",
|
||||||
|
"hint": "The code expires in 15 minutes."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/verify_email",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Verify",
|
||||||
|
"class": "btn-full"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "Didn't receive it?",
|
||||||
|
"link": {
|
||||||
|
"action": "/action/resend_verification",
|
||||||
|
"text": "Resend code"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "Wrong email?",
|
||||||
|
"link": {
|
||||||
|
"action": "/view/view_createaccount",
|
||||||
|
"text": "Start over"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "section",
|
||||||
|
"client_requirement": "client_is_viewer+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "h1",
|
||||||
|
"text": "Already logged in."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "Your account is already active."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spacer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/view/view_overview",
|
||||||
|
"text": "Go to Overview"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
0
docker/routlin-dash/app/pages/actions/__init__.py
Normal file
0
docker/routlin-dash/app/pages/actions/__init__.py
Normal file
|
|
@ -3,7 +3,7 @@ from auth import require_level
|
||||||
from config_utils import (flush_pending_to_queue, get_dashboard_pending,
|
from config_utils import (flush_pending_to_queue, get_dashboard_pending,
|
||||||
revert_snapshot_to_config, queued_msg)
|
revert_snapshot_to_config, queued_msg)
|
||||||
|
|
||||||
bp = Blueprint('action_actions', __name__)
|
bp = Blueprint('actions', __name__)
|
||||||
|
|
||||||
_VIEW = '/view/view_actions'
|
_VIEW = '/view/view_actions'
|
||||||
|
|
||||||
112
docker/routlin-dash/app/pages/actions/content.json
Normal file
112
docker/routlin-dash/app/pages/actions/content.json
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
{
|
||||||
|
"id": "view_actions",
|
||||||
|
"client_requirement": "client_is_viewer+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "header_page_title",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "h1",
|
||||||
|
"text": "Actions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "Apply or stage pending configuration changes."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"label": "Pending Actions",
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/actions_cardpending_applynow",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "raw_html",
|
||||||
|
"html": "%PENDING_ACTIONS_HTML%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"text": "Apply Now",
|
||||||
|
"disabled": "%NO_PENDING%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "raw_html",
|
||||||
|
"html": "%APPLY_WARNING%"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "hr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/actions_cardpending_save",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Apply Changes Immediately",
|
||||||
|
"name": "apply_changes_immediately",
|
||||||
|
"input_type": "checkbox",
|
||||||
|
"value": "%GENERAL_APPLY_ON_SAVE%",
|
||||||
|
"hint": "When enabled, saved changes are queued immediately. When disabled, changes accumulate in Pending Actions until you click Apply Now."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/actions_cardpending_save",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Save"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_cancel",
|
||||||
|
"text": "Cancel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"label": "Change History",
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/actions_cardhistory_revertselected",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "raw_html",
|
||||||
|
"html": "%CHANGE_HISTORY_HTML%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_danger",
|
||||||
|
"text": "Revert Selected",
|
||||||
|
"disabled": "%NO_HISTORY%"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
0
docker/routlin-dash/app/pages/bannedips/__init__.py
Normal file
0
docker/routlin-dash/app/pages/bannedips/__init__.py
Normal file
|
|
@ -6,9 +6,9 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
bp = Blueprint('action_apply_banned_ips', __name__)
|
bp = Blueprint('bannedips', __name__)
|
||||||
|
|
||||||
VIEW = '/view/view_banned_ips'
|
VIEW = '/view/view_bannedips'
|
||||||
|
|
||||||
|
|
||||||
def _row_index():
|
def _row_index():
|
||||||
121
docker/routlin-dash/app/pages/bannedips/content.json
Normal file
121
docker/routlin-dash/app/pages/bannedips/content.json
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
{
|
||||||
|
"id": "view_bannedips",
|
||||||
|
"client_requirement": "client_is_viewer+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "header_page_title",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "h1",
|
||||||
|
"text": "Banned IPs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "IPs and ranges blocked in both directions at the nftables firewall."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "info_bar",
|
||||||
|
"variant": "info",
|
||||||
|
"text": "Supports single IPs, CIDR (94.130.0.0/16), wildcards (94.130.*.*), and ranges (94.130.52.1-20). IPv4 and IPv6 are both supported."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"datasource": "config:banned_ips",
|
||||||
|
"empty_message": "No IP bans configured.",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"label": "Description",
|
||||||
|
"field": "description"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "IP / Range",
|
||||||
|
"field": "ip",
|
||||||
|
"class": "col-mono"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Status",
|
||||||
|
"field": "enabled",
|
||||||
|
"render": "badge_enabled_disabled"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"row_actions": [
|
||||||
|
{
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"action": "/action/edit_banned_ip",
|
||||||
|
"method": "inline_edit",
|
||||||
|
"text": "Edit",
|
||||||
|
"class": "btn-ghost btn-sm",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"col": "description",
|
||||||
|
"input_type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "ip",
|
||||||
|
"input_type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "enabled",
|
||||||
|
"input_type": "checkbox",
|
||||||
|
"checkbox_label": "Enabled"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"action": "/action/delete_banned_ip",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Delete",
|
||||||
|
"class": "btn-danger btn-sm"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"id": "add-form",
|
||||||
|
"label": "Add Banned IP",
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/add_banned_ip",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Description",
|
||||||
|
"name": "description",
|
||||||
|
"input_type": "text",
|
||||||
|
"placeholder": "e.g. Bad actor",
|
||||||
|
"hint": "Optional label for this entry."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "IP / Range",
|
||||||
|
"name": "ip",
|
||||||
|
"input_type": "text",
|
||||||
|
"placeholder": "e.g. 1.2.3.4 or 1.2.3.0/24"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/add_banned_ip",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Add Banned IP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_cancel",
|
||||||
|
"text": "Cancel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
0
docker/routlin-dash/app/pages/ddns/__init__.py
Normal file
0
docker/routlin-dash/app/pages/ddns/__init__.py
Normal file
|
|
@ -6,7 +6,7 @@ from config_utils import load_config, verify_config_hash, save_config_with_snaps
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
bp = Blueprint('action_ddns', __name__)
|
bp = Blueprint('ddns', __name__)
|
||||||
|
|
||||||
VIEW = '/view/view_ddns'
|
VIEW = '/view/view_ddns'
|
||||||
LOG_FILE = f'{CONFIGS_DIR}/ddns.log'
|
LOG_FILE = f'{CONFIGS_DIR}/ddns.log'
|
||||||
295
docker/routlin-dash/app/pages/ddns/content.json
Normal file
295
docker/routlin-dash/app/pages/ddns/content.json
Normal file
|
|
@ -0,0 +1,295 @@
|
||||||
|
{
|
||||||
|
"id": "view_ddns",
|
||||||
|
"client_requirement": "client_is_viewer+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "header_page_title",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "h1",
|
||||||
|
"text": "DDNS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "Dynamic DNS provider status and last known IP update."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "stat_card_grid",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "stat_card",
|
||||||
|
"label": "Current Public IP",
|
||||||
|
"value": "%STAT_PUBLIC_IP%",
|
||||||
|
"sub": "%STAT_PUBLIC_IP_LAST_OBTAINED%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "stat_card",
|
||||||
|
"label": "IP Check Interval",
|
||||||
|
"value": "%DDNS_TIMER_INTERVAL%",
|
||||||
|
"sub": "%STAT_PUBLIC_IP_LAST_CHECKED%",
|
||||||
|
"edit_action": "/action/ddns_cardipcheckinterval_save",
|
||||||
|
"edit_field": "timer_interval",
|
||||||
|
"edit_input_type": "number",
|
||||||
|
"edit_min": "1",
|
||||||
|
"edit_suffix": "minutes",
|
||||||
|
"edit_value": "%DDNS_TIMER_INTERVAL_MINS%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "stat_card",
|
||||||
|
"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_cardipcheckservices_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",
|
||||||
|
"action": "/action/ddns_cardipcheckservices_save",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Save"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_cancel",
|
||||||
|
"text": "Cancel",
|
||||||
|
"class": "js-hide-card"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"datasource": "config:ddns_providers",
|
||||||
|
"empty_message": "No DDNS providers configured.",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"label": "Description",
|
||||||
|
"field": "description"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Provider",
|
||||||
|
"field": "provider"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Hostname(s)",
|
||||||
|
"field": "hostnames",
|
||||||
|
"render": "tag_list"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Status",
|
||||||
|
"field": "enabled",
|
||||||
|
"render": "badge_enabled_disabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Credentials",
|
||||||
|
"field": "credentials",
|
||||||
|
"render": "raw_html"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"row_actions": [
|
||||||
|
{
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"action": "/action/ddns_tableaccounts_rowedit",
|
||||||
|
"method": "inline_edit",
|
||||||
|
"text": "Edit",
|
||||||
|
"class": "btn-ghost btn-sm",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"col": "description",
|
||||||
|
"input_type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "provider",
|
||||||
|
"input_type": "select",
|
||||||
|
"options": "%DDNS_PROVIDER_OPTIONS%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "hostnames",
|
||||||
|
"input_type": "textarea"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "enabled",
|
||||||
|
"input_type": "checkbox"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "credentials",
|
||||||
|
"input_type": "credentials"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"action": "/action/ddns_tableaccounts_rowdelete",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Delete",
|
||||||
|
"class": "btn-danger btn-sm"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"label": "Add DDNS Account",
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/ddns_cardaddaccount_add",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Description",
|
||||||
|
"name": "description",
|
||||||
|
"input_type": "text",
|
||||||
|
"placeholder": "e.g. My DuckDNS Account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Provider",
|
||||||
|
"name": "provider",
|
||||||
|
"input_type": "select",
|
||||||
|
"options": "%DDNS_PROVIDER_OPTIONS%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Hostnames (one per line)",
|
||||||
|
"name": "hostnames",
|
||||||
|
"input_type": "textarea",
|
||||||
|
"placeholder": "e.g. myhome.duckdns.org"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "credential_fields",
|
||||||
|
"provider_select": "provider"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/ddns_cardaddaccount_add",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Add Provider"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_cancel",
|
||||||
|
"text": "Cancel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"label": "Logging",
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "pre_block",
|
||||||
|
"text": "%DDNS_LOG_TAIL%",
|
||||||
|
"scroll_to_bottom": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "raw_html",
|
||||||
|
"html": "%DDNS_LOG_SUMMARY%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"justify": "space-between",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_ghost",
|
||||||
|
"action": "/action/ddns_cardlogging_download",
|
||||||
|
"text": "Download Log"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_danger",
|
||||||
|
"action": "/action/ddns_cardlogging_clear",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Clear Log"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "hr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/ddns_cardlogging_save",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Max Log Size (KB)",
|
||||||
|
"name": "log_max_kb",
|
||||||
|
"input_type": "number",
|
||||||
|
"layout": "inline",
|
||||||
|
"value": "%DDNS_GEN_LOG_MAX_KB%",
|
||||||
|
"min": "64"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "",
|
||||||
|
"name": "log_errors_only",
|
||||||
|
"input_type": "checkbox",
|
||||||
|
"checkbox_label": "Only record errors to log",
|
||||||
|
"value": "%DDNS_GEN_LOG_ERRORS_ONLY%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/ddns_cardlogging_save",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Save"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_cancel",
|
||||||
|
"text": "Cancel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
0
docker/routlin-dash/app/pages/dhcp/__init__.py
Normal file
0
docker/routlin-dash/app/pages/dhcp/__init__.py
Normal file
|
|
@ -7,7 +7,7 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
bp = Blueprint('action_apply_dhcp_reservations', __name__)
|
bp = Blueprint('dhcp', __name__)
|
||||||
|
|
||||||
VIEW = '/view/view_dhcp'
|
VIEW = '/view/view_dhcp'
|
||||||
|
|
||||||
221
docker/routlin-dash/app/pages/dhcp/content.json
Normal file
221
docker/routlin-dash/app/pages/dhcp/content.json
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
{
|
||||||
|
"id": "view_dhcp",
|
||||||
|
"client_requirement": "client_is_viewer+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "header_page_title",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "h1",
|
||||||
|
"text": "DHCP"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "Active leases, IP reservations, and VLAN authorizations."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"datasource": "live:dhcp_leases",
|
||||||
|
"empty_message": "No active DHCP leases found.",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"label": "Hostname",
|
||||||
|
"field": "hostname"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "IP Address",
|
||||||
|
"field": "ip_address",
|
||||||
|
"class": "col-mono"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "MAC Address",
|
||||||
|
"field": "mac_address",
|
||||||
|
"class": "col-mono"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "VLAN",
|
||||||
|
"field": "vlan_name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Expires",
|
||||||
|
"field": "expires"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"datasource": "config:dhcp_reservations",
|
||||||
|
"empty_message": "No DHCP reservations configured.",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"label": "Description",
|
||||||
|
"field": "description"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Hostname",
|
||||||
|
"field": "hostname",
|
||||||
|
"class": "col-mono"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "MAC",
|
||||||
|
"field": "mac",
|
||||||
|
"class": "col-mono"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "IP",
|
||||||
|
"field": "ip",
|
||||||
|
"class": "col-mono"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "VLAN",
|
||||||
|
"field": "vlan_name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "RADIUS",
|
||||||
|
"field": "radius_client",
|
||||||
|
"render": "badge_yes_no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Status",
|
||||||
|
"field": "enabled",
|
||||||
|
"render": "badge_enabled_disabled"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"toolbar": {
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "select",
|
||||||
|
"name": "vlan_filter",
|
||||||
|
"value": "all",
|
||||||
|
"options": "%VLAN_FILTER_OPTIONS%",
|
||||||
|
"filter_col": "vlan_name"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"row_actions": [
|
||||||
|
{
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"action": "/action/edit_dhcp_reservation",
|
||||||
|
"method": "inline_edit",
|
||||||
|
"text": "Edit",
|
||||||
|
"class": "btn-ghost btn-sm",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"col": "description",
|
||||||
|
"input_type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "hostname",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "networkname"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "mac",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "mac"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "ip",
|
||||||
|
"input_type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "radius_client",
|
||||||
|
"input_type": "checkbox",
|
||||||
|
"checkbox_label": "Enabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "enabled",
|
||||||
|
"input_type": "checkbox",
|
||||||
|
"checkbox_label": "Enabled"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"action": "/action/delete_dhcp_reservation",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Delete",
|
||||||
|
"class": "btn-danger btn-sm"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"id": "add-form",
|
||||||
|
"label": "Add Reservation/Authorization",
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/add_dhcp_reservation",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "VLAN",
|
||||||
|
"name": "vlan_name",
|
||||||
|
"input_type": "select",
|
||||||
|
"options": "%VLAN_NAMES_AS_OPTIONS%",
|
||||||
|
"hint": "VLAN this reservation belongs to."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Description",
|
||||||
|
"name": "description",
|
||||||
|
"input_type": "text",
|
||||||
|
"placeholder": "e.g. NAS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Hostname",
|
||||||
|
"name": "hostname",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "networkname",
|
||||||
|
"placeholder": "e.g. nas"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "MAC Address",
|
||||||
|
"name": "mac",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "mac",
|
||||||
|
"placeholder": "e.g. aa:bb:cc:dd:ee:ff"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "IP Address",
|
||||||
|
"name": "ip",
|
||||||
|
"input_type": "text",
|
||||||
|
"placeholder": "e.g. 192.168.10.50",
|
||||||
|
"hint": "Leave blank to authorize device on this VLAN dynamically."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "RADIUS Client",
|
||||||
|
"name": "radius_client",
|
||||||
|
"input_type": "checkbox",
|
||||||
|
"hint": "This device acts as a RADIUS authenticator, verifying credentials of other devices on the network."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/add_dhcp_reservation",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Add"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_cancel",
|
||||||
|
"text": "Cancel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
0
docker/routlin-dash/app/pages/dnsblocking/__init__.py
Normal file
0
docker/routlin-dash/app/pages/dnsblocking/__init__.py
Normal file
|
|
@ -6,9 +6,9 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
bp = Blueprint('action_dnsblocking', __name__)
|
bp = Blueprint('dnsblocking', __name__)
|
||||||
|
|
||||||
VIEW = '/view/view_dns_blocking'
|
VIEW = '/view/view_dnsblocking'
|
||||||
|
|
||||||
_VALID_FORMATS_STR = ', '.join(sorted(validate.VALID_BLOCKLIST_FORMATS))
|
_VALID_FORMATS_STR = ', '.join(sorted(validate.VALID_BLOCKLIST_FORMATS))
|
||||||
|
|
||||||
247
docker/routlin-dash/app/pages/dnsblocking/content.json
Normal file
247
docker/routlin-dash/app/pages/dnsblocking/content.json
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
{
|
||||||
|
"id": "view_dnsblocking",
|
||||||
|
"client_requirement": "client_is_viewer+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "header_page_title",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "h1",
|
||||||
|
"text": "DNS Blocking"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "Domain level blocking via dnsmasq."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"datasource": "config:blocklists",
|
||||||
|
"empty_message": "No blocklists configured.",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"label": "Name",
|
||||||
|
"field": "name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Description",
|
||||||
|
"field": "description"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Format",
|
||||||
|
"field": "format",
|
||||||
|
"class": "col-mono"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Source URL",
|
||||||
|
"field": "url",
|
||||||
|
"class": "col-mono"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"row_actions": [
|
||||||
|
{
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"action": "/action/dnsblocking_tableblocklists_rowedit",
|
||||||
|
"method": "inline_edit",
|
||||||
|
"text": "Edit",
|
||||||
|
"class": "btn-ghost btn-sm",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"col": "name",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "dashname"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "description",
|
||||||
|
"input_type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "format",
|
||||||
|
"input_type": "select",
|
||||||
|
"options": "%BLOCKLIST_FORMAT_OPTIONS%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "url",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "url"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"action": "/action/dnsblocking_tableblocklists_rowdelete",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Delete",
|
||||||
|
"class": "btn-danger btn-sm"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"id": "add-form",
|
||||||
|
"label": "Add Blocklist",
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/dnsblocking_cardaddblocklist_add",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Name",
|
||||||
|
"name": "name",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "dashname",
|
||||||
|
"placeholder": "e.g. steven-black"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Description",
|
||||||
|
"name": "description",
|
||||||
|
"input_type": "text",
|
||||||
|
"placeholder": "e.g. Steven Black (ads, malware, trackers)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Format",
|
||||||
|
"name": "format",
|
||||||
|
"input_type": "select",
|
||||||
|
"options": "%BLOCKLIST_FORMAT_OPTIONS%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Source URL",
|
||||||
|
"name": "url",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "url",
|
||||||
|
"placeholder": "https://..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/dnsblocking_cardaddblocklist_add",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Add Blocklist"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_cancel",
|
||||||
|
"text": "Cancel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"label": "Blocklist Refresh",
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "raw_html",
|
||||||
|
"html": "%BLOCKLIST_STATS_HTML%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "hr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_secondary",
|
||||||
|
"action": "/action/dnsblocking_cardblocklistrefresh_refreshnow",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Refresh All Now"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "hr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/dnsblocking_cardblocklistrefresh_save",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Daily Refresh Time",
|
||||||
|
"name": "daily_execute_time_24hr_local",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "time_24h",
|
||||||
|
"value": "%GENERAL_DAILY_EXECUTE_TIME%",
|
||||||
|
"placeholder": "e.g. 02:30",
|
||||||
|
"hint": "24-hour local time for the daily blocklist refresh."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/dnsblocking_cardblocklistrefresh_save",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Save"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_cancel",
|
||||||
|
"text": "Cancel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"label": "Logging",
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/dnsblocking_cardlogging_save",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Max Log Size (KB)",
|
||||||
|
"name": "log_max_kb",
|
||||||
|
"input_type": "number",
|
||||||
|
"value": "%GENERAL_LOG_MAX_KB%",
|
||||||
|
"min": 64,
|
||||||
|
"hint": "Log is cleared and restarted when it exceeds this size."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Only record errors to log",
|
||||||
|
"name": "log_errors_only",
|
||||||
|
"input_type": "checkbox",
|
||||||
|
"value": "%GENERAL_LOG_ERRORS_ONLY%",
|
||||||
|
"hint": "Only write error-level messages to the log."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/dnsblocking_cardlogging_save",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Save"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_cancel",
|
||||||
|
"text": "Cancel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
0
docker/routlin-dash/app/pages/dnsserver/__init__.py
Normal file
0
docker/routlin-dash/app/pages/dnsserver/__init__.py
Normal file
|
|
@ -5,9 +5,9 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
bp = Blueprint('action_dnsserver', __name__)
|
bp = Blueprint('dnsserver', __name__)
|
||||||
|
|
||||||
_VIEW = '/view/view_dns_server'
|
_VIEW = '/view/view_dnsserver'
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/action/dnsserver_cardupstreamdns_save', methods=['POST'])
|
@bp.route('/action/dnsserver_cardupstreamdns_save', methods=['POST'])
|
||||||
104
docker/routlin-dash/app/pages/dnsserver/content.json
Normal file
104
docker/routlin-dash/app/pages/dnsserver/content.json
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
{
|
||||||
|
"id": "view_dnsserver",
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "header_page_title",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "h1",
|
||||||
|
"text": "DNS Server"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "Upstream resolvers and forwarding DNS service settings."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"label": "Upstream DNS",
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/dnsserver_cardupstreamdns_save",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "editable_list",
|
||||||
|
"label": "DNS Providers",
|
||||||
|
"name": "upstream_servers",
|
||||||
|
"item_placeholder": "e.g. 1.1.1.1",
|
||||||
|
"add_label": "Add Provider",
|
||||||
|
"validate": "ip",
|
||||||
|
"hint": "DNS resolvers queried for external hostnames. Supports IPv4 and IPv6.",
|
||||||
|
"items": "%DNS_UPSTREAM_SERVERS_JSON%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Strict Order",
|
||||||
|
"name": "strict_order",
|
||||||
|
"input_type": "checkbox",
|
||||||
|
"value": "%DNS_STRICT_ORDER%",
|
||||||
|
"hint": "Query DNS providers in list order rather than in parallel."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/dnsserver_cardupstreamdns_save",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Save"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_cancel",
|
||||||
|
"text": "Cancel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"label": "DNS Forwarding",
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/dnsserver_carddnsforwarding_save",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Cache Size",
|
||||||
|
"name": "cache_size",
|
||||||
|
"input_type": "number",
|
||||||
|
"value": "%DNS_CACHE_SIZE%",
|
||||||
|
"min": 0,
|
||||||
|
"hint": "Max DNS responses to cache per instance. Set to 0 to disable caching."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/dnsserver_carddnsforwarding_save",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Save"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_cancel",
|
||||||
|
"text": "Cancel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
0
docker/routlin-dash/app/pages/hostoverrides/__init__.py
Normal file
0
docker/routlin-dash/app/pages/hostoverrides/__init__.py
Normal file
|
|
@ -7,9 +7,9 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
bp = Blueprint('action_apply_host_overrides', __name__)
|
bp = Blueprint('hostoverrides', __name__)
|
||||||
|
|
||||||
VIEW = '/view/view_host_overrides'
|
VIEW = '/view/view_hostoverrides'
|
||||||
|
|
||||||
|
|
||||||
def _vlan_networks(cfg):
|
def _vlan_networks(cfg):
|
||||||
135
docker/routlin-dash/app/pages/hostoverrides/content.json
Normal file
135
docker/routlin-dash/app/pages/hostoverrides/content.json
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
{
|
||||||
|
"id": "view_hostoverrides",
|
||||||
|
"client_requirement": "client_is_viewer+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "header_page_title",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "h1",
|
||||||
|
"text": "Host Overrides"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "Force a hostname to resolve to a specific internal IP."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"datasource": "config:host_overrides",
|
||||||
|
"empty_message": "No host overrides configured.",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"label": "Description",
|
||||||
|
"field": "description"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Hostname",
|
||||||
|
"field": "host",
|
||||||
|
"class": "col-mono"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Resolves To",
|
||||||
|
"field": "ip",
|
||||||
|
"class": "col-mono"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Status",
|
||||||
|
"field": "enabled",
|
||||||
|
"render": "badge_enabled_disabled"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"row_actions": [
|
||||||
|
{
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"action": "/action/edit_host_override",
|
||||||
|
"method": "inline_edit",
|
||||||
|
"text": "Edit",
|
||||||
|
"class": "btn-ghost btn-sm",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"col": "description",
|
||||||
|
"input_type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "host",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "domainname"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "ip",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "ip"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "enabled",
|
||||||
|
"input_type": "checkbox",
|
||||||
|
"checkbox_label": "Enabled"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"action": "/action/delete_host_override",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Delete",
|
||||||
|
"class": "btn-danger btn-sm"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"id": "add-form",
|
||||||
|
"label": "Add Host Override",
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/add_host_override",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Description",
|
||||||
|
"name": "description",
|
||||||
|
"input_type": "text",
|
||||||
|
"placeholder": "e.g. Local server"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Hostname",
|
||||||
|
"name": "host",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "domainname",
|
||||||
|
"placeholder": "e.g. server.home.local"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Resolves To",
|
||||||
|
"name": "ip",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "ip",
|
||||||
|
"placeholder": "e.g. 192.168.1.100"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/add_host_override",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Add Host Override"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_cancel",
|
||||||
|
"text": "Cancel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
0
docker/routlin-dash/app/pages/intervlan/__init__.py
Normal file
0
docker/routlin-dash/app/pages/intervlan/__init__.py
Normal file
|
|
@ -6,9 +6,9 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
bp = Blueprint('action_apply_inter_vlan', __name__)
|
bp = Blueprint('intervlan', __name__)
|
||||||
|
|
||||||
VIEW = '/view/view_inter_vlan'
|
VIEW = '/view/view_intervlan'
|
||||||
|
|
||||||
_VALID_PROTOS_STR = ', '.join(sorted(validate.VALID_PROTOCOLS))
|
_VALID_PROTOS_STR = ', '.join(sorted(validate.VALID_PROTOCOLS))
|
||||||
|
|
||||||
167
docker/routlin-dash/app/pages/intervlan/content.json
Normal file
167
docker/routlin-dash/app/pages/intervlan/content.json
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
{
|
||||||
|
"id": "view_intervlan",
|
||||||
|
"client_requirement": "client_is_viewer+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "header_page_title",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "h1",
|
||||||
|
"text": "Inter-VLAN Exceptions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "Firewall rules that permit specific traffic to cross VLAN boundaries."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"datasource": "config:inter_vlan_exceptions",
|
||||||
|
"empty_message": "No inter-VLAN exceptions configured. All cross-VLAN traffic is blocked by default.",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"label": "Description",
|
||||||
|
"field": "description"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Protocol",
|
||||||
|
"field": "protocol",
|
||||||
|
"class": "col-mono"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Source",
|
||||||
|
"field": "src_ip_or_subnet",
|
||||||
|
"class": "col-mono"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Destination",
|
||||||
|
"field": "dst_ip_or_subnet",
|
||||||
|
"class": "col-mono"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Dest Port",
|
||||||
|
"field": "dst_port",
|
||||||
|
"class": "col-mono"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Status",
|
||||||
|
"field": "enabled",
|
||||||
|
"render": "badge_enabled_disabled"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"row_actions": [
|
||||||
|
{
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"action": "/action/edit_inter_vlan",
|
||||||
|
"method": "inline_edit",
|
||||||
|
"text": "Edit",
|
||||||
|
"class": "btn-ghost btn-sm",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"col": "description",
|
||||||
|
"input_type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "protocol",
|
||||||
|
"input_type": "select",
|
||||||
|
"options": "%PROTOCOL_OPTIONS%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "src_ip_or_subnet",
|
||||||
|
"input_type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "dst_ip_or_subnet",
|
||||||
|
"input_type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "dst_port",
|
||||||
|
"input_type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "enabled",
|
||||||
|
"input_type": "checkbox",
|
||||||
|
"checkbox_label": "Enabled"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"action": "/action/delete_inter_vlan",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Delete",
|
||||||
|
"class": "btn-danger btn-sm"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"id": "add-form",
|
||||||
|
"label": "Add Exception",
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/add_inter_vlan",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Description",
|
||||||
|
"name": "description",
|
||||||
|
"input_type": "text",
|
||||||
|
"placeholder": "e.g. Allow Chromecast"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Protocol",
|
||||||
|
"name": "protocol",
|
||||||
|
"input_type": "select",
|
||||||
|
"options": "%PROTOCOL_OPTIONS%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Source",
|
||||||
|
"name": "src_ip_or_subnet",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "ipv4cidr",
|
||||||
|
"placeholder": "e.g. 192.168.20.0/24"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Destination",
|
||||||
|
"name": "dst_ip_or_subnet",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "ipv4",
|
||||||
|
"placeholder": "e.g. 192.168.10.100"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Dest Port",
|
||||||
|
"name": "dst_port",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "port",
|
||||||
|
"placeholder": "e.g. 8009"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/add_inter_vlan",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Add Exception"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_cancel",
|
||||||
|
"text": "Cancel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
0
docker/routlin-dash/app/pages/mdns/__init__.py
Normal file
0
docker/routlin-dash/app/pages/mdns/__init__.py
Normal file
|
|
@ -6,7 +6,7 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
bp = Blueprint('action_apply_mdns', __name__)
|
bp = Blueprint('mdns', __name__)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/action/apply_mdns', methods=['POST'])
|
@bp.route('/action/apply_mdns', methods=['POST'])
|
||||||
0
docker/routlin-dash/app/pages/networklayout/__init__.py
Normal file
0
docker/routlin-dash/app/pages/networklayout/__init__.py
Normal file
|
|
@ -7,9 +7,9 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
bp = Blueprint('action_networklayout', __name__)
|
bp = Blueprint('networklayout', __name__)
|
||||||
|
|
||||||
VIEW = '/view/view_network_layout'
|
VIEW = '/view/view_networklayout'
|
||||||
|
|
||||||
_VLAN_FIELDS = ['name', 'vlan_id', 'is_vpn', 'subnet', 'subnet_mask', 'dnsmasq_log_queries',
|
_VLAN_FIELDS = ['name', 'vlan_id', 'is_vpn', 'subnet', 'subnet_mask', 'dnsmasq_log_queries',
|
||||||
'radius_default', 'mdns_reflection', 'use_blocklists']
|
'radius_default', 'mdns_reflection', 'use_blocklists']
|
||||||
283
docker/routlin-dash/app/pages/networklayout/content.json
Normal file
283
docker/routlin-dash/app/pages/networklayout/content.json
Normal file
|
|
@ -0,0 +1,283 @@
|
||||||
|
{
|
||||||
|
"id": "view_networklayout",
|
||||||
|
"client_requirement": "client_is_viewer+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "header_page_title",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "h1",
|
||||||
|
"text": "Network Layout"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "Network segments managed by systemd-networkd, dnsmasq, nftables, and freeradius."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "info_bar",
|
||||||
|
"variant": "info",
|
||||||
|
"text": "For a basic flat network with no VLAN segmentation, only use VLAN 1 and delete the others."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"datasource": "config:vlans",
|
||||||
|
"empty_message": "No VLANs configured.",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"label": "VLAN ID",
|
||||||
|
"field": "vlan_id",
|
||||||
|
"class": "col-mono col-narrow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Name",
|
||||||
|
"field": "name",
|
||||||
|
"class": "col-narrow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Interface",
|
||||||
|
"field": "interface",
|
||||||
|
"class": "col-mono col-narrow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Subnet",
|
||||||
|
"field": "subnet",
|
||||||
|
"class": "col-mono col-narrow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Mask",
|
||||||
|
"field": "subnet_mask",
|
||||||
|
"class": "col-mono col-narrow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Self Ident(s)",
|
||||||
|
"field": "server_identity_ips",
|
||||||
|
"render": "tag_list"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Blocklists",
|
||||||
|
"field": "use_blocklists",
|
||||||
|
"class": "col-expand",
|
||||||
|
"render": "tag_list"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Default",
|
||||||
|
"field": "radius_default",
|
||||||
|
"class": "col-narrow",
|
||||||
|
"render": "badge_yes_no",
|
||||||
|
"render_options": {
|
||||||
|
"title_true": "RADIUS Default",
|
||||||
|
"title_false": "Not RADIUS Default"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "mDNS",
|
||||||
|
"field": "mdns_reflection",
|
||||||
|
"class": "col-narrow",
|
||||||
|
"render": "badge_yes_no",
|
||||||
|
"render_options": {
|
||||||
|
"title_true": "mDNS Reflection Enabled",
|
||||||
|
"title_false": "mDNS Reflection Disabled"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Record",
|
||||||
|
"field": "dnsmasq_log_queries",
|
||||||
|
"class": "col-narrow",
|
||||||
|
"render": "badge_yes_no",
|
||||||
|
"render_options": {
|
||||||
|
"title_true": "DNS Queries Recorded",
|
||||||
|
"title_false": "DNS Queries Not Recorded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"row_actions": [
|
||||||
|
{
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"action": "/action/networklayout_tablevlans_edit",
|
||||||
|
"method": "inline_edit",
|
||||||
|
"text": "Edit",
|
||||||
|
"class": "btn-ghost btn-sm",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"col": "name",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "dashname"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "subnet",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "subnet"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "subnet_mask",
|
||||||
|
"input_type": "number",
|
||||||
|
"min": 1,
|
||||||
|
"max": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "server_identity_ips",
|
||||||
|
"input_type": "textarea_pair",
|
||||||
|
"col_label": "IP Address",
|
||||||
|
"col_validate": "ip",
|
||||||
|
"pair_col": "server_identity_descriptions",
|
||||||
|
"pair_label": "Description (Opt)",
|
||||||
|
"pair_wide": true,
|
||||||
|
"pair_col2": "server_identity_hostnames",
|
||||||
|
"pair_label2": "Hostname (Opt)",
|
||||||
|
"pair_validate2": "networkname",
|
||||||
|
"gateway_col": "server_identity_gateway",
|
||||||
|
"dns_col": "server_identity_dns_server",
|
||||||
|
"ntp_col": "server_identity_ntp_server"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "radius_default",
|
||||||
|
"input_type": "checkbox",
|
||||||
|
"checkbox_label": "Enabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "mdns_reflection",
|
||||||
|
"input_type": "checkbox",
|
||||||
|
"checkbox_label": "Enabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "dnsmasq_log_queries",
|
||||||
|
"input_type": "checkbox",
|
||||||
|
"checkbox_label": "Record"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "use_blocklists",
|
||||||
|
"input_type": "checkbox_multi",
|
||||||
|
"options": "%BLOCKLIST_NAME_OPTIONS%"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"action": "/action/networklayout_tablevlans_delete",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Delete",
|
||||||
|
"class": "btn-danger btn-sm",
|
||||||
|
"disable_if": {
|
||||||
|
"field": "vlan_id",
|
||||||
|
"value": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"id": "add-form",
|
||||||
|
"label": "Add VLAN",
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/networklayout_cardaddvlan_addvlan",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field_row",
|
||||||
|
"cols": 4,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "VLAN ID",
|
||||||
|
"name": "vlan_id",
|
||||||
|
"input_type": "number",
|
||||||
|
"min": 1,
|
||||||
|
"max": 4094,
|
||||||
|
"validate": "vlan_id",
|
||||||
|
"hint": "Unique integer 1-4094. Sets the 802.1Q tag and interface name."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "VLAN Name",
|
||||||
|
"name": "name",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "dashname",
|
||||||
|
"hint": "Lowercase letters, digits, hyphens. E.g. iot"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "subnet_row",
|
||||||
|
"subnet_name": "subnet",
|
||||||
|
"prefix_name": "subnet_mask",
|
||||||
|
"subnet_placeholder": "e.g. 192.168.x.0",
|
||||||
|
"prefix_value": "24"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "VLAN Type",
|
||||||
|
"name": "is_vpn",
|
||||||
|
"input_type": "checkbox",
|
||||||
|
"checkbox_label": "Is VPN",
|
||||||
|
"hint": "Check if this VLAN uses a WireGuard interface (e.g. wg0, wg1, etc)."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "hr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "identity_builder",
|
||||||
|
"label": "Router Identities on this VLAN:"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "hr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Blocklists",
|
||||||
|
"name": "use_blocklists",
|
||||||
|
"input_type": "checkbox_group",
|
||||||
|
"options": "%BLOCKLIST_NAME_OPTIONS%",
|
||||||
|
"hint": "Note: Selected lists will be merged and de-duplicated prior to use."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "hr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "RADIUS Default",
|
||||||
|
"name": "radius_default",
|
||||||
|
"input_type": "checkbox",
|
||||||
|
"hint": "Wireless devices without a DHCP reservation will be placed into this VLAN. (Note: wired devices are not placed via RADIUS but rather by layer 3 switch policy.)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "mDNS Reflection",
|
||||||
|
"name": "mdns_reflection",
|
||||||
|
"input_type": "checkbox",
|
||||||
|
"hint": "Reflect mDNS traffic to/from this VLAN via avahi-daemon. Not supported on WireGuard interfaces."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Record DNS Queries",
|
||||||
|
"name": "dnsmasq_log_queries",
|
||||||
|
"input_type": "checkbox",
|
||||||
|
"hint": "Log every DNS query. High volume - enable for debugging only."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/networklayout_cardaddvlan_addvlan",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Add VLAN",
|
||||||
|
"class": "add-vlan-btn",
|
||||||
|
"disabled": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_cancel",
|
||||||
|
"text": "Cancel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
0
docker/routlin-dash/app/pages/overview/__init__.py
Normal file
0
docker/routlin-dash/app/pages/overview/__init__.py
Normal file
289
docker/routlin-dash/app/pages/overview/content.json
Normal file
289
docker/routlin-dash/app/pages/overview/content.json
Normal file
|
|
@ -0,0 +1,289 @@
|
||||||
|
{
|
||||||
|
"id": "view_overview",
|
||||||
|
"client_requirement": "client_is_nothing+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "auth_wrapper",
|
||||||
|
"client_requirement": "client_is_nothing=",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "auth_card",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "h1",
|
||||||
|
"text": "Routlin Dashboard"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "Log in to monitor and manage your network."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "spacer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/view/view_login",
|
||||||
|
"text": "Log In"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "header_page_title",
|
||||||
|
"client_requirement": "client_is_viewer+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "h1",
|
||||||
|
"text": "Overview"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "Current network status at a glance."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "stat_card_grid",
|
||||||
|
"client_requirement": "client_is_viewer+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "stat_card",
|
||||||
|
"label": "DHCP Leases",
|
||||||
|
"value": "%STAT_LEASE_COUNT%",
|
||||||
|
"sub": "active leases",
|
||||||
|
"variant": "accent"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "stat_card",
|
||||||
|
"label": "Queries Blocked",
|
||||||
|
"value": "%STAT_BLOCKED_TODAY%",
|
||||||
|
"sub": "since midnight",
|
||||||
|
"variant": "warning"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "stat_card",
|
||||||
|
"label": "Public IP",
|
||||||
|
"value": "%STAT_PUBLIC_IP%",
|
||||||
|
"sub": "%STAT_DDNS_HOSTNAME%"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"label": "Network",
|
||||||
|
"client_requirement": "client_is_viewer+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "grid",
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"type": "grid_label",
|
||||||
|
"text": "WAN Interface"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "grid_value",
|
||||||
|
"text": "%GENERAL_WAN_INTERFACE%"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"type": "grid_label",
|
||||||
|
"text": "VLANs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "grid_value",
|
||||||
|
"text": "%OVERVIEW_VLAN_NAMES%"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"type": "grid_label",
|
||||||
|
"text": "Firewall"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "grid_value",
|
||||||
|
"text": "%STAT_NFTABLES_STATUS%"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"type": "grid_label",
|
||||||
|
"text": "System Uptime"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "grid_value",
|
||||||
|
"text": "%STAT_UPTIME%"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"label": "DNS Blocking",
|
||||||
|
"client_requirement": "client_is_viewer+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "grid",
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"type": "grid_label",
|
||||||
|
"text": "Blocked Domains"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "grid_value",
|
||||||
|
"text": "%STAT_BLOCKED_DOMAINS%"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"type": "grid_label",
|
||||||
|
"text": "Active Blocklists"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "grid_value",
|
||||||
|
"text": "%STAT_BLOCKLIST_COUNT% lists"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"type": "grid_label",
|
||||||
|
"text": "Last Refreshed"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "grid_value",
|
||||||
|
"text": "%STAT_BL_LAST_UPDATE%"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"type": "grid_label",
|
||||||
|
"text": "Active IP Bans"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "grid_value",
|
||||||
|
"text": "%STAT_BANNED_IP_COUNT% rules"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"label": "DNS Caching",
|
||||||
|
"client_requirement": "client_is_viewer+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "grid",
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"type": "grid_label",
|
||||||
|
"text": "Total Queries"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "grid_value",
|
||||||
|
"text": "%DNS_STAT_QUERIES%"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"type": "grid_label",
|
||||||
|
"text": "Cache Hits"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "grid_value",
|
||||||
|
"text": "%DNS_STAT_HITS% (%DNS_STAT_HIT_RATE% hit rate)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"type": "grid_label",
|
||||||
|
"text": "Forwarded"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "grid_value",
|
||||||
|
"text": "%DNS_STAT_FORWARDED%"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"type": "grid_label",
|
||||||
|
"text": "Cache Capacity"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "grid_value",
|
||||||
|
"text": "%DNS_CACHE_SIZE% entries"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"type": "grid_label",
|
||||||
|
"text": "Authoritative Answers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "grid_value",
|
||||||
|
"text": "%DNS_STAT_AUTH%"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"type": "grid_label",
|
||||||
|
"text": "TCP Connections Peak"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "grid_value",
|
||||||
|
"text": "%DNS_STAT_TCP_PEAK%"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"type": "grid_label",
|
||||||
|
"text": "DNS Providers"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "grid_value",
|
||||||
|
"text": "%OVERVIEW_UPSTREAM_SERVERS%"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -7,9 +7,9 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
bp = Blueprint('action_physicalinterfaces', __name__)
|
bp = Blueprint('physicalinterfaces', __name__)
|
||||||
|
|
||||||
_VIEW = '/view/view_physical_interfaces'
|
_VIEW = '/view/view_physicalinterfaces'
|
||||||
|
|
||||||
_EXCLUDE_PREFIXES = ('lo', 'wg', 'docker', 'br-', 'veth',
|
_EXCLUDE_PREFIXES = ('lo', 'wg', 'docker', 'br-', 'veth',
|
||||||
'tun', 'tap', 'ppp', 'virbr',
|
'tun', 'tap', 'ppp', 'virbr',
|
||||||
166
docker/routlin-dash/app/pages/physicalinterfaces/content.json
Normal file
166
docker/routlin-dash/app/pages/physicalinterfaces/content.json
Normal file
|
|
@ -0,0 +1,166 @@
|
||||||
|
{
|
||||||
|
"id": "view_physicalinterfaces",
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "header_page_title",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "h1",
|
||||||
|
"text": "Physical Interfaces"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "WAN/LAN interface assignments and per-interface settings."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"label": "Physical Interfaces",
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/physicalinterfaces_cardphysicalinterface_save",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "WAN Interface",
|
||||||
|
"name": "wan_interface",
|
||||||
|
"input_type": "interface_picker",
|
||||||
|
"value": "%GENERAL_WAN_INTERFACE%",
|
||||||
|
"data": "%NETWORK_INTERFACE_DATA_JSON%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "LAN Interface",
|
||||||
|
"name": "lan_interface",
|
||||||
|
"input_type": "interface_picker",
|
||||||
|
"value": "%GENERAL_LAN_INTERFACE%",
|
||||||
|
"data": "%NETWORK_INTERFACE_DATA_JSON%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/physicalinterfaces_cardphysicalinterface_save",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Save"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_cancel",
|
||||||
|
"text": "Cancel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"id": "iface-config-card",
|
||||||
|
"label": "Interface Configuration",
|
||||||
|
"hidden": true,
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/physicalinterfaces_cardinterfaceconfiguration_apply",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "hidden",
|
||||||
|
"name": "original_mtu",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "hidden",
|
||||||
|
"name": "original_mac",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field_row",
|
||||||
|
"cols": 3,
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Interface",
|
||||||
|
"name": "iface",
|
||||||
|
"input_type": "text",
|
||||||
|
"readonly": true,
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "MTU",
|
||||||
|
"name": "mtu",
|
||||||
|
"input_type": "select",
|
||||||
|
"value": "",
|
||||||
|
"options": [
|
||||||
|
{
|
||||||
|
"label": "576",
|
||||||
|
"value": "576"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "1280",
|
||||||
|
"value": "1280"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "1492",
|
||||||
|
"value": "1492"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "1500",
|
||||||
|
"value": "1500"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "4096",
|
||||||
|
"value": "4096"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "9000",
|
||||||
|
"value": "9000"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "MAC Address",
|
||||||
|
"name": "mac",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "mac",
|
||||||
|
"value": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/physicalinterfaces_cardinterfaceconfiguration_apply",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Apply"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_secondary",
|
||||||
|
"action": "#",
|
||||||
|
"text": "Cancel",
|
||||||
|
"class": "iface-config-cancel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "raw_html",
|
||||||
|
"html": "<br /><br /><br /><br /><br />"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
0
docker/routlin-dash/app/pages/portforwarding/__init__.py
Normal file
0
docker/routlin-dash/app/pages/portforwarding/__init__.py
Normal file
|
|
@ -6,9 +6,9 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
bp = Blueprint('action_apply_port_forwarding', __name__)
|
bp = Blueprint('portforwarding', __name__)
|
||||||
|
|
||||||
VIEW = '/view/view_port_forwarding'
|
VIEW = '/view/view_portforwarding'
|
||||||
|
|
||||||
_VALID_PROTOS_STR = ', '.join(sorted(validate.VALID_PROTOCOLS))
|
_VALID_PROTOS_STR = ', '.join(sorted(validate.VALID_PROTOCOLS))
|
||||||
|
|
||||||
167
docker/routlin-dash/app/pages/portforwarding/content.json
Normal file
167
docker/routlin-dash/app/pages/portforwarding/content.json
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
{
|
||||||
|
"id": "view_portforwarding",
|
||||||
|
"client_requirement": "client_is_viewer+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "header_page_title",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "h1",
|
||||||
|
"text": "Port Forwarding"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "DNAT rules that forward inbound WAN traffic to internal hosts."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"datasource": "config:port_forwarding",
|
||||||
|
"empty_message": "No port forwarding rules configured.",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"label": "Description",
|
||||||
|
"field": "description"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Protocol",
|
||||||
|
"field": "protocol",
|
||||||
|
"class": "col-mono"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Ext Port",
|
||||||
|
"field": "dest_port",
|
||||||
|
"class": "col-mono"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "NAT IP",
|
||||||
|
"field": "nat_ip",
|
||||||
|
"class": "col-mono"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "NAT Port",
|
||||||
|
"field": "nat_port",
|
||||||
|
"class": "col-mono"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Status",
|
||||||
|
"field": "enabled",
|
||||||
|
"render": "badge_enabled_disabled"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"row_actions": [
|
||||||
|
{
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"action": "/action/edit_port_forward",
|
||||||
|
"method": "inline_edit",
|
||||||
|
"text": "Edit",
|
||||||
|
"class": "btn-ghost btn-sm",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"col": "description",
|
||||||
|
"input_type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "protocol",
|
||||||
|
"input_type": "select",
|
||||||
|
"options": "%PROTOCOL_OPTIONS%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "dest_port",
|
||||||
|
"input_type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "nat_ip",
|
||||||
|
"input_type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "nat_port",
|
||||||
|
"input_type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "enabled",
|
||||||
|
"input_type": "checkbox",
|
||||||
|
"checkbox_label": "Enabled"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"action": "/action/delete_port_forward",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Delete",
|
||||||
|
"class": "btn-danger btn-sm"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"id": "add-form",
|
||||||
|
"label": "Add Rule",
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/add_port_forward",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Description",
|
||||||
|
"name": "description",
|
||||||
|
"input_type": "text",
|
||||||
|
"placeholder": "e.g. Minecraft server"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Protocol",
|
||||||
|
"name": "protocol",
|
||||||
|
"input_type": "select",
|
||||||
|
"options": "%PROTOCOL_OPTIONS%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Ext Port",
|
||||||
|
"name": "dest_port",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "port",
|
||||||
|
"placeholder": "e.g. 25565"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "NAT IP",
|
||||||
|
"name": "nat_ip",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "ipv4",
|
||||||
|
"placeholder": "e.g. 192.168.1.50"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "NAT Port",
|
||||||
|
"name": "nat_port",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "port",
|
||||||
|
"placeholder": "e.g. 25565"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/add_port_forward",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Add Rule"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_cancel",
|
||||||
|
"text": "Cancel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
0
docker/routlin-dash/app/pages/preferences/__init__.py
Normal file
0
docker/routlin-dash/app/pages/preferences/__init__.py
Normal file
|
|
@ -2,8 +2,9 @@ from flask import Blueprint, request, session, redirect, flash
|
||||||
import json, bcrypt
|
import json, bcrypt
|
||||||
from auth import require_level
|
from auth import require_level
|
||||||
from config_utils import ACCOUNTS_FILE
|
from config_utils import ACCOUNTS_FILE
|
||||||
|
import sanitize
|
||||||
|
|
||||||
bp = Blueprint('action_change_password', __name__)
|
bp = Blueprint('preferences', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -19,6 +20,33 @@ def _save_accounts(data):
|
||||||
json.dump(data, f, indent=2)
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/action/save_preferences', methods=['POST'])
|
||||||
|
@require_level('viewer')
|
||||||
|
def save_preferences():
|
||||||
|
tz = sanitize.timezone(request.form.get('timezone', '').strip())
|
||||||
|
|
||||||
|
if not tz:
|
||||||
|
flash('Timezone is required.', 'error')
|
||||||
|
return redirect('/view/view_preferences')
|
||||||
|
|
||||||
|
email = session.get('email_address', '').lower()
|
||||||
|
data = _load_accounts()
|
||||||
|
accounts = data.get('accounts', [])
|
||||||
|
account = next((a for a in accounts if a.get('email_address', '').lower() == email), None)
|
||||||
|
|
||||||
|
if account is None:
|
||||||
|
flash('Account not found. Please log in again.', 'error')
|
||||||
|
return redirect('/view/view_login')
|
||||||
|
|
||||||
|
account['timezone'] = tz
|
||||||
|
_save_accounts(data)
|
||||||
|
|
||||||
|
session['timezone'] = tz
|
||||||
|
|
||||||
|
flash('Preferences saved.', 'success')
|
||||||
|
return redirect('/view/view_preferences')
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/action/change_password', methods=['POST'])
|
@bp.route('/action/change_password', methods=['POST'])
|
||||||
@require_level('viewer')
|
@require_level('viewer')
|
||||||
def change_password():
|
def change_password():
|
||||||
|
|
@ -45,7 +73,7 @@ def change_password():
|
||||||
|
|
||||||
if account is None:
|
if account is None:
|
||||||
flash('Account not found. Please log in again.', 'error')
|
flash('Account not found. Please log in again.', 'error')
|
||||||
return redirect('/view/view_log_in')
|
return redirect('/view/view_login')
|
||||||
|
|
||||||
stored_hash = account.get('hashed_password', '').encode('utf-8')
|
stored_hash = account.get('hashed_password', '').encode('utf-8')
|
||||||
if not bcrypt.checkpw(current_password.encode('utf-8'), stored_hash):
|
if not bcrypt.checkpw(current_password.encode('utf-8'), stored_hash):
|
||||||
105
docker/routlin-dash/app/pages/preferences/content.json
Normal file
105
docker/routlin-dash/app/pages/preferences/content.json
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
{
|
||||||
|
"id": "view_preferences",
|
||||||
|
"client_requirement": "client_is_viewer+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "header_page_title",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "h1",
|
||||||
|
"text": "Preferences"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "Your personal account settings."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"label": "Account Details",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/save_preferences",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Email Address",
|
||||||
|
"name": "email",
|
||||||
|
"input_type": "text",
|
||||||
|
"value": "%PREF_EMAIL%",
|
||||||
|
"hint": "Contact your manager to change your email address."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Timezone",
|
||||||
|
"name": "timezone",
|
||||||
|
"input_type": "select",
|
||||||
|
"value": "%PREF_TIMEZONE%",
|
||||||
|
"options": "%TIMEZONE_OPTIONS%",
|
||||||
|
"hint": "All timestamps will be displayed in this timezone."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/save_preferences",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Save Preferences"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"label": "Change Password",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/change_password",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Current Password",
|
||||||
|
"name": "current_password",
|
||||||
|
"input_type": "password",
|
||||||
|
"placeholder": "Current password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "New Password",
|
||||||
|
"name": "new_password",
|
||||||
|
"input_type": "password",
|
||||||
|
"placeholder": "New password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Confirm Password",
|
||||||
|
"name": "confirm_password",
|
||||||
|
"input_type": "password",
|
||||||
|
"placeholder": "Repeat new password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/change_password",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Change Password"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
0
docker/routlin-dash/app/pages/vpn/__init__.py
Normal file
0
docker/routlin-dash/app/pages/vpn/__init__.py
Normal file
|
|
@ -9,7 +9,7 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
|
|
||||||
bp = Blueprint('action_apply_vpn', __name__)
|
bp = Blueprint('vpn', __name__)
|
||||||
|
|
||||||
_VIEW = '/view/view_vpn'
|
_VIEW = '/view/view_vpn'
|
||||||
_MTU_MIN = 576
|
_MTU_MIN = 576
|
||||||
277
docker/routlin-dash/app/pages/vpn/content.json
Normal file
277
docker/routlin-dash/app/pages/vpn/content.json
Normal file
|
|
@ -0,0 +1,277 @@
|
||||||
|
{
|
||||||
|
"id": "view_vpn",
|
||||||
|
"client_requirement": "client_is_viewer+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "header_page_title",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "h1",
|
||||||
|
"text": "VPN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "p",
|
||||||
|
"text": "WireGuard peer management and server interface configuration."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"label": "Active Sessions",
|
||||||
|
"datasource": "live:vpn_sessions",
|
||||||
|
"empty_message": "No active VPN sessions.",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"label": "Peer",
|
||||||
|
"field": "peer_name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Tunnel IP",
|
||||||
|
"field": "tunnel_ip",
|
||||||
|
"class": "col-mono"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Endpoint",
|
||||||
|
"field": "endpoint",
|
||||||
|
"class": "col-mono"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Last Handshake",
|
||||||
|
"field": "last_handshake"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Received",
|
||||||
|
"field": "rx_bytes",
|
||||||
|
"class": "col-mono"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Sent",
|
||||||
|
"field": "tx_bytes",
|
||||||
|
"class": "col-mono"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "table",
|
||||||
|
"label": "Peers",
|
||||||
|
"datasource": "config:vpn_peers",
|
||||||
|
"empty_message": "No peers configured. Use Add Peer below.",
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"label": "Name",
|
||||||
|
"field": "name"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Assigned VLAN",
|
||||||
|
"field": "vlan_display",
|
||||||
|
"class": "col-mono"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Assigned IP",
|
||||||
|
"field": "ip",
|
||||||
|
"class": "col-mono"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Split Tunnel",
|
||||||
|
"field": "split_tunnel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Enabled",
|
||||||
|
"field": "enabled",
|
||||||
|
"render": "badge_enabled_disabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Public Key",
|
||||||
|
"field": "pubkey_short",
|
||||||
|
"class": "col-mono"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"row_actions": [
|
||||||
|
{
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"action": "/action/edit_vpn_peer",
|
||||||
|
"method": "inline_edit",
|
||||||
|
"text": "Edit",
|
||||||
|
"class": "btn-ghost btn-sm",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"col": "name",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "dashname"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "split_tunnel",
|
||||||
|
"input_type": "checkbox",
|
||||||
|
"checkbox_label": "Enabled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"col": "enabled",
|
||||||
|
"input_type": "checkbox",
|
||||||
|
"checkbox_label": "Enabled"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"action": "/action/regenerate_vpn_peer",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Regen Conf",
|
||||||
|
"class": "btn-ghost btn-sm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"action": "/action/delete_vpn_peer",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Delete",
|
||||||
|
"class": "btn-danger btn-sm"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"label": "Add Peer",
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/add_vpn_peer",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Name",
|
||||||
|
"name": "peer_name",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "dashname",
|
||||||
|
"placeholder": "e.g. laptop",
|
||||||
|
"hint": "Friendly name for this peer."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Assigned VLAN",
|
||||||
|
"name": "peer_vlan",
|
||||||
|
"input_type": "select",
|
||||||
|
"options": "%VPN_VLAN_OPTIONS%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Assigned IP",
|
||||||
|
"name": "peer_ip",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "ipv4",
|
||||||
|
"placeholder": "e.g. 192.168.40.2",
|
||||||
|
"hint": "Static IP assigned to this peer within the VPN subnet."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Split Tunnel",
|
||||||
|
"name": "split_tunnel",
|
||||||
|
"input_type": "checkbox",
|
||||||
|
"hint": "Route only VPN subnet traffic through the tunnel. When unchecked all traffic is routed through the VPN."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Enabled",
|
||||||
|
"name": "enabled",
|
||||||
|
"input_type": "checkbox",
|
||||||
|
"checked": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/add_vpn_peer",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Add Peer & Download Conf"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_cancel",
|
||||||
|
"text": "Cancel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "card",
|
||||||
|
"label": "WireGuard Interface",
|
||||||
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/apply_vpn",
|
||||||
|
"method": "post",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Listen Port",
|
||||||
|
"name": "vpn_listen_port",
|
||||||
|
"input_type": "number",
|
||||||
|
"value": "%VPN_LISTEN_PORT%",
|
||||||
|
"min": 1024,
|
||||||
|
"max": 65535,
|
||||||
|
"hint": "UDP port WireGuard listens on. Must match your port forwarding rule."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Server Endpoint",
|
||||||
|
"name": "vpn_server_endpoint",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "endpoint",
|
||||||
|
"value": "%VPN_SERVER_ENDPOINT%",
|
||||||
|
"placeholder": "e.g. vpn.example.com",
|
||||||
|
"hint": "Publicly reachable hostname or IP of this server, embedded in client config files."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "Domain",
|
||||||
|
"name": "vpn_domain",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "dashname",
|
||||||
|
"value": "%VPN_DOMAIN%",
|
||||||
|
"placeholder": "e.g. local",
|
||||||
|
"hint": "DNS search domain pushed to VPN clients."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "DNS Override",
|
||||||
|
"name": "vpn_dns_server",
|
||||||
|
"input_type": "text",
|
||||||
|
"validate": "ipv4",
|
||||||
|
"value": "%VPN_DNS_SERVER%",
|
||||||
|
"placeholder": "Leave blank to use gateway IP (%VPN_GATEWAY%)",
|
||||||
|
"hint": "Explicit DNS server pushed to peers. Defaults to the gateway IP."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "field",
|
||||||
|
"label": "MTU Override",
|
||||||
|
"name": "vpn_mtu",
|
||||||
|
"input_type": "number",
|
||||||
|
"value": "%VPN_MTU%",
|
||||||
|
"placeholder": "Leave blank for default",
|
||||||
|
"hint": "Override tunnel MTU. Leave blank for the system default."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_primary",
|
||||||
|
"action": "/action/apply_vpn",
|
||||||
|
"method": "post",
|
||||||
|
"text": "Save"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_cancel",
|
||||||
|
"text": "Cancel"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,9 @@ import json, re, subprocess, os, sys, html as html_mod
|
||||||
import sanitize
|
import sanitize
|
||||||
import validation as validate
|
import validation as validate
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from config_utils import config_hash, get_pending_entries, get_dashboard_pending, get_dashboard_done, load_snapshot_for_uuid, load_all_snapshots, get_done_timestamps, queue_command, _find_cmd_in_queues, _entry_ts_from_queue, _apply_changes_immediately, _seconds_until_next_run, _format_timing, _is_locked, _lock_mtime, WEB_APP_DISPLAY_NAME, CONFIGS_DIR, DATA_DIR, WWW_DIR
|
from config_utils import config_hash, get_pending_entries, get_dashboard_pending, get_dashboard_done, load_snapshot_for_uuid, load_all_snapshots, get_done_timestamps, queue_command, _find_cmd_in_queues, _entry_ts_from_queue, _apply_changes_immediately, _seconds_until_next_run, _format_timing, _is_locked, _lock_mtime, WEB_APP_DISPLAY_NAME, CONFIGS_DIR, DATA_DIR, WWW_DIR, ACCOUNTS_FILE, APP_DIR
|
||||||
|
import os as _os
|
||||||
|
_PAGES_DIR = _os.path.join(APP_DIR, 'pages')
|
||||||
|
|
||||||
bp = Blueprint('view_page', __name__)
|
bp = Blueprint('view_page', __name__)
|
||||||
|
|
||||||
|
|
@ -46,7 +48,7 @@ def _load_json(path):
|
||||||
|
|
||||||
def _load_config(): return _load_json(f'{CONFIGS_DIR}/config.json')
|
def _load_config(): return _load_json(f'{CONFIGS_DIR}/config.json')
|
||||||
def _load_ddns(): return _load_config().get('ddns', {})
|
def _load_ddns(): return _load_config().get('ddns', {})
|
||||||
def _load_accounts(): return _load_json(f'{DATA_DIR}/authorized_accounts.json')
|
def _load_accounts(): return _load_json(ACCOUNTS_FILE)
|
||||||
|
|
||||||
def _load_css():
|
def _load_css():
|
||||||
try:
|
try:
|
||||||
|
|
@ -64,6 +66,25 @@ def _load_icon(name):
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
|
||||||
|
def _build_view_map():
|
||||||
|
m = {}
|
||||||
|
if not _os.path.isdir(_PAGES_DIR):
|
||||||
|
return m
|
||||||
|
for name in _os.listdir(_PAGES_DIR):
|
||||||
|
cpath = _os.path.join(_PAGES_DIR, name, 'content.json')
|
||||||
|
if _os.path.isfile(cpath):
|
||||||
|
try:
|
||||||
|
with open(cpath) as f:
|
||||||
|
d = json.load(f)
|
||||||
|
vid = d.get('id')
|
||||||
|
if vid:
|
||||||
|
m[vid] = name
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return m
|
||||||
|
_VIEW_MAP = _build_view_map()
|
||||||
|
|
||||||
|
|
||||||
# Shell helper ======================================================
|
# Shell helper ======================================================
|
||||||
|
|
||||||
def _run(cmd):
|
def _run(cmd):
|
||||||
|
|
@ -1043,7 +1064,7 @@ def _render_item(item, tokens, inherited_req=None):
|
||||||
extra_cls = (' ' + item['class']) if item.get('class') else ''
|
extra_cls = (' ' + item['class']) if item.get('class') else ''
|
||||||
return f'<button type="button" class="btn btn-secondary btn-cancel{extra_cls}" disabled>{text}</button>'
|
return f'<button type="button" class="btn btn-secondary btn-cancel{extra_cls}" disabled>{text}</button>'
|
||||||
|
|
||||||
if t == 'page_header':
|
if t == 'header_page_title':
|
||||||
return f'<div class="page-header">{render_items(item.get("items", []), tokens, req)}</div>'
|
return f'<div class="page-header">{render_items(item.get("items", []), tokens, req)}</div>'
|
||||||
|
|
||||||
if t in ('section', 'auth_wrapper'):
|
if t in ('section', 'auth_wrapper'):
|
||||||
|
|
@ -1192,7 +1213,9 @@ def _render_item(item, tokens, inherited_req=None):
|
||||||
f'<input type="hidden" name="original_values" value="{e(json.dumps(originals))}"/>'
|
f'<input type="hidden" name="original_values" value="{e(json.dumps(originals))}"/>'
|
||||||
if originals else ''
|
if originals else ''
|
||||||
)
|
)
|
||||||
return f'<form action="{action}" method="{method}">{hash_field}{orig_field}{inner}</form>'
|
field_specs, submit_sel = _collect_form_specs(item.get('items', []))
|
||||||
|
script = _render_form_script(field_specs, submit_sel) if (field_specs and submit_sel) else ''
|
||||||
|
return f'<form action="{action}" method="{method}">{hash_field}{orig_field}{inner}</form>{script}'
|
||||||
|
|
||||||
if t == 'hidden':
|
if t == 'hidden':
|
||||||
name = e(item.get('name', ''))
|
name = e(item.get('name', ''))
|
||||||
|
|
@ -1331,7 +1354,6 @@ def _render_field(item, tokens):
|
||||||
placeholder = e(apply_tokens(item.get('placeholder', ''), tokens))
|
placeholder = e(apply_tokens(item.get('placeholder', ''), tokens))
|
||||||
hint = e(apply_tokens(item.get('hint', ''), tokens))
|
hint = e(apply_tokens(item.get('hint', ''), tokens))
|
||||||
hint_html = f'<p class="form-hint">{hint}</p>' if hint else ''
|
hint_html = f'<p class="form-hint">{hint}</p>' if hint else ''
|
||||||
extra_cls = f' {e(item["class"])}' if item.get('class') else ''
|
|
||||||
readonly = ' readonly' if item.get('readonly') else ''
|
readonly = ' readonly' if item.get('readonly') else ''
|
||||||
|
|
||||||
if input_type == 'hidden':
|
if input_type == 'hidden':
|
||||||
|
|
@ -1387,20 +1409,28 @@ def _render_field(item, tokens):
|
||||||
f'<option value="{e(o["value"])}"{" selected" if o["value"] == current else ""}>{e(o["label"])}</option>'
|
f'<option value="{e(o["value"])}"{" selected" if o["value"] == current else ""}>{e(o["label"])}</option>'
|
||||||
for o in options
|
for o in options
|
||||||
)
|
)
|
||||||
|
validate = item.get('validate', '')
|
||||||
|
depends = item.get('depends', [])
|
||||||
|
validate_attr = f' data-validate="{e(validate)}"' if validate else ''
|
||||||
|
depends_attr = f' data-depends="{e(",".join(depends))}"' if depends else ''
|
||||||
|
dyn_hint = '<p class="form-hint field-dyn-hint hidden"></p>' if validate else ''
|
||||||
return (
|
return (
|
||||||
f'<div class="form-group"><label class="form-label">{label}</label>'
|
f'<div class="form-group"><label class="form-label">{label}</label>'
|
||||||
f'<select name="{name}" class="form-select{extra_cls}">{opts_html}</select>'
|
f'<select name="{name}" class="form-select"{validate_attr}{depends_attr}>{opts_html}</select>'
|
||||||
f'{hint_html}</div>'
|
f'{hint_html}{dyn_hint}</div>'
|
||||||
)
|
)
|
||||||
|
|
||||||
if input_type == 'number':
|
if input_type == 'number':
|
||||||
min_attr = f' min="{item["min"]}"' if 'min' in item else ''
|
min_attr = f' min="{item["min"]}"' if 'min' in item else ''
|
||||||
max_attr = f' max="{item["max"]}"' if 'max' in item else ''
|
max_attr = f' max="{item["max"]}"' if 'max' in item else ''
|
||||||
|
validate = item.get('validate', 'positive_int')
|
||||||
|
depends = item.get('depends', [])
|
||||||
|
validate_attr = f' data-validate="{e(validate)}"'
|
||||||
|
depends_attr = f' data-depends="{e(",".join(depends))}"' if depends else ''
|
||||||
dyn_hint_html = '<p class="form-hint field-dyn-hint hidden"></p>'
|
dyn_hint_html = '<p class="form-hint field-dyn-hint hidden"></p>'
|
||||||
inp = (
|
inp = (
|
||||||
f'<input type="number" name="{name}" value="{e(value)}"{min_attr}{max_attr}'
|
f'<input type="number" name="{name}" value="{e(value)}"{min_attr}{max_attr}'
|
||||||
f' class="form-input{extra_cls}"{readonly}'
|
f' class="form-input form-input-mono"{readonly}{validate_attr}{depends_attr}/>'
|
||||||
' data-validate="positive_int" />'
|
|
||||||
)
|
)
|
||||||
if item.get('layout') == 'inline':
|
if item.get('layout') == 'inline':
|
||||||
return (
|
return (
|
||||||
|
|
@ -1533,16 +1563,125 @@ def _render_field(item, tokens):
|
||||||
'</div>'
|
'</div>'
|
||||||
)
|
)
|
||||||
|
|
||||||
validate = item.get('validate', '')
|
validate = item.get('validate', '')
|
||||||
|
depends = item.get('depends', [])
|
||||||
validate_attr = f' data-validate="{e(validate)}"' if validate else ''
|
validate_attr = f' data-validate="{e(validate)}"' if validate else ''
|
||||||
dyn_hint = '<p class="form-hint field-dyn-hint hidden"></p>' if (item.get('readonly') or item.get('dyn_hint') or validate) else ''
|
depends_attr = f' data-depends="{e(",".join(depends))}"' if depends else ''
|
||||||
|
dyn_hint = '<p class="form-hint field-dyn-hint hidden"></p>' if validate else ''
|
||||||
return (
|
return (
|
||||||
f'<div class="form-group"><label class="form-label">{label}</label>'
|
f'<div class="form-group"><label class="form-label">{label}</label>'
|
||||||
f'<input type="{e(input_type)}" name="{name}" value="{e(value)}"'
|
f'<input type="{e(input_type)}" name="{name}" value="{e(value)}"'
|
||||||
f' placeholder="{placeholder}" class="form-input{extra_cls}"{readonly}{validate_attr}/>{hint_html}{dyn_hint}</div>'
|
f' placeholder="{placeholder}" class="form-input"{readonly}{validate_attr}{depends_attr}/>'
|
||||||
|
f'{hint_html}{dyn_hint}</div>'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_form_specs(items):
|
||||||
|
"""Walk form items; return (field_specs, submit_sel) for factory script generation."""
|
||||||
|
fields = []
|
||||||
|
submit_sel = None
|
||||||
|
for item in items:
|
||||||
|
t = item.get('type', '')
|
||||||
|
if t == 'field':
|
||||||
|
itype = item.get('input_type', 'text')
|
||||||
|
if item.get('validate') or itype == 'checkbox' or itype == 'number':
|
||||||
|
fields.append(item)
|
||||||
|
elif t == 'subnet_row':
|
||||||
|
fields.append(item)
|
||||||
|
elif t == 'button_primary' and item.get('class'):
|
||||||
|
first_cls = item['class'].split()[0]
|
||||||
|
submit_sel = submit_sel or ('.' + first_cls)
|
||||||
|
elif t in ('field_row', 'button_row', 'section', 'form'):
|
||||||
|
sub, sub_btn = _collect_form_specs(item.get('items', []))
|
||||||
|
fields.extend(sub)
|
||||||
|
submit_sel = submit_sel or sub_btn
|
||||||
|
return fields, submit_sel
|
||||||
|
|
||||||
|
|
||||||
|
def _render_form_script(field_specs, submit_sel):
|
||||||
|
"""Generate an inline <script> for a form's validation and submit-gate wiring."""
|
||||||
|
import re
|
||||||
|
_safe = re.compile(r'^[a-zA-Z0-9_-]+$')
|
||||||
|
|
||||||
|
lines = ['(function() {']
|
||||||
|
lines.append(" var _prev = document.currentScript.previousElementSibling;")
|
||||||
|
lines.append(" var _card = _prev.closest('.card') || _prev.parentElement;")
|
||||||
|
lines.append(f" var _submit = _card ? _card.querySelector('{submit_sel}') : null;")
|
||||||
|
lines.append('')
|
||||||
|
|
||||||
|
# Classify each spec =================================================
|
||||||
|
subnet_items = [] # (subnet_var, prefix_var, subnet_name, prefix_name)
|
||||||
|
validate_items = [] # (js_var, field_name) — validated via validateEl
|
||||||
|
checkbox_only = [] # js_var — checkboxes that only need change→_upd
|
||||||
|
gate_vars = [] # JS boolean expressions that must all be true for submit
|
||||||
|
|
||||||
|
for spec in field_specs:
|
||||||
|
t = spec.get('type', '')
|
||||||
|
if t == 'subnet_row':
|
||||||
|
sn = spec.get('subnet_name', 'subnet')
|
||||||
|
pn = spec.get('prefix_name', 'subnet_mask')
|
||||||
|
if not (_safe.match(sn) and _safe.match(pn)):
|
||||||
|
continue
|
||||||
|
sv = '_' + sn.replace('-', '_')
|
||||||
|
pv = '_' + pn.replace('-', '_')
|
||||||
|
lines.append(f" var {sv} = _card.querySelector('[name=\"{sn}\"]');")
|
||||||
|
lines.append(f" var {pv} = _card.querySelector('[name=\"{pn}\"]');")
|
||||||
|
subnet_items.append((sv, pv))
|
||||||
|
gate_vars.append(f'{sv} && {sv}._valid')
|
||||||
|
elif t == 'field':
|
||||||
|
nm = spec.get('name', '')
|
||||||
|
itype = spec.get('input_type', 'text')
|
||||||
|
if not nm or not _safe.match(nm):
|
||||||
|
continue
|
||||||
|
vn = '_' + nm.replace('-', '_')
|
||||||
|
lines.append(f" var {vn} = _card.querySelector('[name=\"{nm}\"]');")
|
||||||
|
if itype == 'checkbox':
|
||||||
|
if spec.get('validate'):
|
||||||
|
validate_items.append((vn, nm))
|
||||||
|
gate_vars.append(f'{vn} && {vn}._valid')
|
||||||
|
else:
|
||||||
|
checkbox_only.append(vn)
|
||||||
|
else:
|
||||||
|
validate_items.append((vn, nm))
|
||||||
|
gate_vars.append(f'{vn} && {vn}._valid')
|
||||||
|
|
||||||
|
lines.append('')
|
||||||
|
|
||||||
|
# Submit gate =========================================================
|
||||||
|
gate_expr = ' && '.join(gate_vars) if gate_vars else 'true'
|
||||||
|
lines.append(' function _upd() {')
|
||||||
|
lines.append(' if (!_submit) return;')
|
||||||
|
lines.append(f' _submit.disabled = !({gate_expr});')
|
||||||
|
lines.append(' }')
|
||||||
|
lines.append('')
|
||||||
|
|
||||||
|
# validateEl listeners ================================================
|
||||||
|
for vn, _ in validate_items:
|
||||||
|
lines.append(f" if ({vn}) {vn}.addEventListener('input', function() {{ validateEl({vn}); _upd(); }});")
|
||||||
|
|
||||||
|
# subnet_row custom block =============================================
|
||||||
|
for sv, pv in subnet_items:
|
||||||
|
lines.append(f' function _chkSubnet() {{')
|
||||||
|
lines.append(f' if (!{sv} || !{pv}) return;')
|
||||||
|
lines.append(f" var res = _ipv4SubnetValid({sv}.value.trim(), {pv}.value.trim());")
|
||||||
|
lines.append(f" setFieldHint({sv}, res.ok ? '' : (res.msg||''), res.ok ? 'ok' : (res.partial ? 'warning' : 'error'));")
|
||||||
|
lines.append(f' {sv}._valid = res.ok;')
|
||||||
|
lines.append(f" var dot = {pv}.closest('.form-group').querySelector('.subnet-dotted');")
|
||||||
|
lines.append(f' var n = parseInt({pv}.value, 10);')
|
||||||
|
lines.append(f" if (dot) dot.textContent = (!isNaN(n) && n >= 1 && n <= 30) ? prefixToDotted(n) : '';")
|
||||||
|
lines.append(f' _upd();')
|
||||||
|
lines.append(f' }}')
|
||||||
|
lines.append(f" if ({sv}) {sv}.addEventListener('input', _chkSubnet);")
|
||||||
|
lines.append(f" if ({pv}) {pv}.addEventListener('input', _chkSubnet);")
|
||||||
|
|
||||||
|
# Checkbox change → _upd only =========================================
|
||||||
|
for vn in checkbox_only:
|
||||||
|
lines.append(f" if ({vn}) {vn}.addEventListener('change', _upd);")
|
||||||
|
|
||||||
|
lines.append('}());')
|
||||||
|
return '<script>' + '\n'.join(lines) + '</script>'
|
||||||
|
|
||||||
|
|
||||||
def _collect_form_originals(items, tokens):
|
def _collect_form_originals(items, tokens):
|
||||||
"""Walk form items and return {name: value} for all input fields (used for original_values)."""
|
"""Walk form items and return {name: value} for all input fields (used for original_values)."""
|
||||||
result = {}
|
result = {}
|
||||||
|
|
@ -1948,7 +2087,7 @@ def render_layout(view_id, content_html, tokens):
|
||||||
|
|
||||||
|
|
||||||
def _render_navbar(active_view, level, tokens, pending_alert=False):
|
def _render_navbar(active_view, level, tokens, pending_alert=False):
|
||||||
navbar_data = _load_json(f'{DATA_DIR}/navbar_content.json')
|
navbar_data = _load_json(f'{APP_DIR}/navbar_content.json')
|
||||||
left, right = [], []
|
left, right = [], []
|
||||||
for item in navbar_data.get('items', []):
|
for item in navbar_data.get('items', []):
|
||||||
req = item.get('client_requirement')
|
req = item.get('client_requirement')
|
||||||
|
|
@ -2029,8 +2168,8 @@ def view(view_id):
|
||||||
return _serve_view(view_id)
|
return _serve_view(view_id)
|
||||||
|
|
||||||
def _serve_view(view_id):
|
def _serve_view(view_id):
|
||||||
content_data = _load_json(f'{DATA_DIR}/page_content.json')
|
page_name = _VIEW_MAP.get(view_id)
|
||||||
view_def = next((v for v in content_data.get('views', []) if v.get('id') == view_id), None)
|
view_def = _load_json(_os.path.join(_PAGES_DIR, page_name, 'content.json')) if page_name else None
|
||||||
|
|
||||||
if view_def is None:
|
if view_def is None:
|
||||||
from flask import abort
|
from flask import abort
|
||||||
|
|
@ -2039,7 +2178,7 @@ def _serve_view(view_id):
|
||||||
view_req = view_def.get('client_requirement')
|
view_req = view_def.get('client_requirement')
|
||||||
level = _client_level()
|
level = _client_level()
|
||||||
if not _passes(view_req, level):
|
if not _passes(view_req, level):
|
||||||
return redirect('/view/view_overview' if level > 0 else '/view/view_log_in')
|
return redirect('/view/view_overview' if level > 0 else '/view/view_login')
|
||||||
|
|
||||||
tokens = collect_tokens()
|
tokens = collect_tokens()
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue