Development
This commit is contained in:
parent
eed1d295dc
commit
d9f3bd8289
45 changed files with 635 additions and 666 deletions
|
|
@ -4,8 +4,8 @@ from auth import require_level
|
|||
bp = Blueprint('accountlogout', __name__)
|
||||
|
||||
|
||||
@bp.route('/action/log_out', methods=['POST'])
|
||||
@bp.route('/action/accountlogout/logout', methods=['POST'])
|
||||
@require_level('viewer')
|
||||
def log_out():
|
||||
def logout():
|
||||
session.clear()
|
||||
return redirect('/view/view_overview')
|
||||
return redirect('/overview')
|
||||
|
|
@ -13,9 +13,9 @@ def require_level(minimum):
|
|||
if LEVEL_RANK.get(current, 0) < LEVEL_RANK.get(minimum, 0):
|
||||
if current == 'nothing':
|
||||
flash('Please log in to continue.', 'error')
|
||||
return redirect('/view/view_login')
|
||||
return redirect('/accountlogin')
|
||||
flash('You do not have permission to perform this action.', 'error')
|
||||
return redirect('/view/view_overview')
|
||||
return redirect('/overview')
|
||||
return f(*args, **kwargs)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
|
|
|||
|
|
@ -18,10 +18,9 @@ from pages.preferences.action import bp as preferences_bp
|
|||
from pages.accountverifyemail.action import bp as accountverifyemail_bp
|
||||
from pages.vpn.action import bp as vpn_bp
|
||||
from pages.accountcreate.action import bp as accountcreate_bp
|
||||
from pages.accountadd.action import bp as accountadd_bp
|
||||
from pages.accountdelete.action import bp as accountdelete_bp
|
||||
from pages.accountlogout.action import bp as accountlogout_bp
|
||||
from pages.accountmanage.action import bp as accountmanage_bp
|
||||
from pages.mdns.action import bp as mdns_bp
|
||||
from action_accountlogout import bp as accountlogout_bp
|
||||
from api_apply_health import bp as api_apply_health_bp
|
||||
|
||||
app = Flask(__name__)
|
||||
|
|
@ -43,8 +42,7 @@ app.register_blueprint(preferences_bp)
|
|||
app.register_blueprint(accountverifyemail_bp)
|
||||
app.register_blueprint(vpn_bp)
|
||||
app.register_blueprint(accountcreate_bp)
|
||||
app.register_blueprint(accountadd_bp)
|
||||
app.register_blueprint(accountdelete_bp)
|
||||
app.register_blueprint(accountmanage_bp)
|
||||
app.register_blueprint(accountlogout_bp)
|
||||
app.register_blueprint(mdns_bp)
|
||||
app.register_blueprint(api_apply_health_bp)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
{
|
||||
"type": "nav_item",
|
||||
"label": "Overview",
|
||||
"map_to": "view_overview",
|
||||
"map_to": "overview",
|
||||
"client_requirement": "client_is_nothing+"
|
||||
},
|
||||
{
|
||||
|
|
@ -11,23 +11,23 @@
|
|||
"label": "%MENU_LABEL%",
|
||||
"client_requirement": "client_is_viewer+",
|
||||
"items": [
|
||||
{ "type": "nav_item", "label": "Physical Interfaces", "map_to": "view_physicalinterfaces", "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_dnsblocking", "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_intervlan", "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": "DDNS", "map_to": "view_ddns" },
|
||||
{ "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": "Banned IPs", "map_to": "view_bannedips", "client_requirement": "client_is_administrator+" }
|
||||
{ "type": "nav_item", "label": "Physical Interfaces", "map_to": "physicalinterfaces", "client_requirement": "client_is_administrator+" },
|
||||
{ "type": "nav_item", "label": "DNS Server", "map_to": "dnsserver", "client_requirement": "client_is_administrator+" },
|
||||
{ "type": "nav_item", "label": "DNS Blocking", "map_to": "dnsblocking", "client_requirement": "client_is_administrator+" },
|
||||
{ "type": "nav_item", "label": "Network Layout", "map_to": "networklayout", "client_requirement": "client_is_administrator+" },
|
||||
{ "type": "nav_item", "label": "Inter-VLAN Exceptions", "map_to": "intervlan", "client_requirement": "client_is_administrator+" },
|
||||
{ "type": "nav_item", "label": "Port Forwarding", "map_to": "portforwarding", "client_requirement": "client_is_administrator+" },
|
||||
{ "type": "nav_item", "label": "DHCP", "map_to": "dhcp" },
|
||||
{ "type": "nav_item", "label": "DDNS", "map_to": "ddns" },
|
||||
{ "type": "nav_item", "label": "Host Overrides", "map_to": "hostoverrides", "client_requirement": "client_is_administrator+" },
|
||||
{ "type": "nav_item", "label": "VPN", "map_to": "vpn" },
|
||||
{ "type": "nav_item", "label": "Banned IPs", "map_to": "bannedips", "client_requirement": "client_is_administrator+" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "nav_item",
|
||||
"label": "Actions",
|
||||
"map_to": "view_actions",
|
||||
"map_to": "actions",
|
||||
"client_requirement": "client_is_administrator+"
|
||||
},
|
||||
{
|
||||
|
|
@ -36,22 +36,22 @@
|
|||
"align": "right",
|
||||
"client_requirement": "client_is_viewer+",
|
||||
"items": [
|
||||
{ "type": "nav_item", "label": "Preferences", "map_to": "view_preferences" },
|
||||
{ "type": "nav_item", "label": "Manage Accounts", "map_to": "view_manageaccounts", "client_requirement": "client_is_manager+" },
|
||||
{ "type": "nav_item", "label": "Preferences", "map_to": "preferences" },
|
||||
{ "type": "nav_item", "label": "Manage Accounts", "map_to": "accountmanage", "client_requirement": "client_is_manager+" },
|
||||
{ "type": "nav_action", "label": "Log Out", "action": "log_out" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "nav_item",
|
||||
"label": "Log In",
|
||||
"map_to": "view_login",
|
||||
"map_to": "accountlogin",
|
||||
"align": "right",
|
||||
"client_requirement": "client_is_nothing="
|
||||
},
|
||||
{
|
||||
"type": "nav_item",
|
||||
"label": "Create Account",
|
||||
"map_to": "view_createaccount",
|
||||
"map_to": "accountcreate",
|
||||
"align": "right",
|
||||
"client_requirement": "client_is_nothing="
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from pathlib import Path
|
||||
from flask import Blueprint, request, session, redirect, flash
|
||||
import json, os, bcrypt, secrets, smtplib
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
|
@ -6,7 +7,9 @@ from auth import require_level
|
|||
from config_utils import WEB_APP_DISPLAY_NAME, ACCOUNTS_FILE
|
||||
import sanitize
|
||||
|
||||
bp = Blueprint('accountcreate', __name__)
|
||||
_PAGE = Path(__file__).parent.name
|
||||
|
||||
bp = Blueprint(_PAGE, __name__)
|
||||
|
||||
CODE_TTL_MIN = 15
|
||||
|
||||
|
|
@ -48,12 +51,12 @@ def _send_verification_email(to_address, code):
|
|||
smtp.send_message(msg)
|
||||
|
||||
|
||||
@bp.route('/action/create_account', methods=['POST'])
|
||||
@bp.route('/action/accountcreate/form_create', methods=['POST'])
|
||||
@require_level('nothing')
|
||||
def create_account():
|
||||
def form_create():
|
||||
# Abort if already logged in
|
||||
if session.get('access_level', 'nothing') != 'nothing':
|
||||
return redirect('/view/view_overview')
|
||||
return redirect('/overview')
|
||||
|
||||
email = sanitize.email(request.form.get('email', ''))
|
||||
password = request.form.get('password', '')
|
||||
|
|
@ -62,26 +65,26 @@ def create_account():
|
|||
|
||||
if not email or not password or not password_confirm or not tz:
|
||||
flash('All fields are required.', 'error')
|
||||
return redirect('/view/view_createaccount')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if password != password_confirm:
|
||||
flash('Passwords do not match.', 'error')
|
||||
return redirect('/view/view_createaccount')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if len(password) < 8:
|
||||
flash('Password must be at least 8 characters.', 'error')
|
||||
return redirect('/view/view_createaccount')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
accounts = _load_accounts().get('accounts', [])
|
||||
account = next((a for a in accounts if a.get('email_address', '').lower() == email), None)
|
||||
|
||||
if account is None:
|
||||
flash('Email address not recognised. Contact your manager.', 'error')
|
||||
return redirect('/view/view_createaccount')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if account.get('hashed_password'):
|
||||
flash('This account is already set up. Please log in instead.', 'error')
|
||||
return redirect('/view/view_createaccount')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
salt = bcrypt.gensalt()
|
||||
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
|
||||
|
|
@ -92,7 +95,7 @@ def create_account():
|
|||
_send_verification_email(account['email_address'], code)
|
||||
except Exception as exc:
|
||||
flash(f'Could not send verification email: {exc}', 'error')
|
||||
return redirect('/view/view_createaccount')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
session['pending_create_account'] = {
|
||||
'email': account['email_address'],
|
||||
|
|
@ -102,4 +105,4 @@ def create_account():
|
|||
'expires': expires,
|
||||
}
|
||||
|
||||
return redirect('/view/view_verifyemail')
|
||||
return redirect('/accountverifyemail')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"id": "view_createaccount",
|
||||
"client_requirement": "client_is_nothing=",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -22,7 +21,7 @@
|
|||
},
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/create_account",
|
||||
"action": "/action/accountcreate/form_create",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -58,7 +57,7 @@
|
|||
},
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/create_account",
|
||||
"action": "/action/accountcreate/form_create",
|
||||
"method": "post",
|
||||
"text": "Create Account",
|
||||
"class": "btn-full"
|
||||
|
|
@ -69,7 +68,7 @@
|
|||
"type": "p",
|
||||
"text": "Already have an account?",
|
||||
"link": {
|
||||
"action": "/view/view_login",
|
||||
"action": "/accountlogin",
|
||||
"text": "Log In"
|
||||
}
|
||||
}
|
||||
|
|
@ -94,7 +93,7 @@
|
|||
},
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/view/overview",
|
||||
"action": "/overview",
|
||||
"text": "Go to Overview"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
from flask import Blueprint, request, session, redirect, flash
|
||||
import json
|
||||
from auth import require_level
|
||||
from config_utils import ACCOUNTS_FILE
|
||||
|
||||
bp = Blueprint('accountdelete', __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/delete_account', methods=['POST'])
|
||||
@require_level('manager')
|
||||
def delete_account():
|
||||
try:
|
||||
row_index = int(request.form.get('row_index', ''))
|
||||
except (ValueError, TypeError):
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect('/view/view_manageaccounts')
|
||||
|
||||
data = _load_accounts()
|
||||
accounts = data.get('accounts', [])
|
||||
|
||||
if row_index < 0 or row_index >= len(accounts):
|
||||
flash('Account not found.', 'error')
|
||||
return redirect('/view/view_manageaccounts')
|
||||
|
||||
target = accounts[row_index]
|
||||
|
||||
if target.get('email_address', '').lower() == session.get('email_address', '').lower():
|
||||
flash('You cannot remove your own account.', 'error')
|
||||
return redirect('/view/view_manageaccounts')
|
||||
|
||||
removed_email = target.get('email_address', '')
|
||||
accounts.pop(row_index)
|
||||
data['accounts'] = accounts
|
||||
_save_accounts(data)
|
||||
|
||||
flash(f'Account for {removed_email} has been removed.', 'success')
|
||||
return redirect('/view/view_manageaccounts')
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
from pathlib import Path
|
||||
from flask import Blueprint, request, session, redirect, flash
|
||||
import json, bcrypt
|
||||
from auth import require_level
|
||||
from config_utils import ACCOUNTS_FILE
|
||||
import sanitize
|
||||
|
||||
bp = Blueprint('accountlogin', __name__)
|
||||
_PAGE = Path(__file__).parent.name
|
||||
|
||||
bp = Blueprint(_PAGE, __name__)
|
||||
|
||||
|
||||
|
||||
|
|
@ -16,35 +19,35 @@ def _load_accounts():
|
|||
return {'accounts': []}
|
||||
|
||||
|
||||
@bp.route('/action/log_in', methods=['POST'])
|
||||
@bp.route('/action/accountlogin/form_login', methods=['POST'])
|
||||
@require_level('nothing')
|
||||
def log_in():
|
||||
def form_login():
|
||||
# Abort if already logged in
|
||||
if session.get('access_level', 'nothing') != 'nothing':
|
||||
return redirect('/view/view_overview')
|
||||
return redirect('/overview')
|
||||
|
||||
email = sanitize.email(request.form.get('email', ''))
|
||||
password = request.form.get('password', '')
|
||||
|
||||
if not email or not password:
|
||||
flash('Email address and password are required.', 'error')
|
||||
return redirect('/view/view_login')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
accounts = _load_accounts().get('accounts', [])
|
||||
account = next((a for a in accounts if a.get('email_address', '').lower() == email), None)
|
||||
|
||||
if account is None:
|
||||
flash('Email address not recognised.', 'error')
|
||||
return redirect('/view/view_login')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if not account.get('hashed_password'):
|
||||
flash('Account setup is not complete. Please use Create Account to set your password first.', 'error')
|
||||
return redirect('/view/view_login')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
stored_hash = account['hashed_password'].encode('utf-8')
|
||||
if not bcrypt.checkpw(password.encode('utf-8'), stored_hash):
|
||||
flash('Invalid email address or password.', 'error')
|
||||
return redirect('/view/view_login')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
session.clear()
|
||||
session['email_address'] = account['email_address']
|
||||
|
|
@ -53,4 +56,4 @@ def log_in():
|
|||
session['apply_changes_immediately'] = False
|
||||
session.permanent = True
|
||||
|
||||
return redirect('/view/view_overview')
|
||||
return redirect('/overview')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"id": "view_login",
|
||||
"client_requirement": "client_is_nothing=",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -22,7 +21,7 @@
|
|||
},
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/log_in",
|
||||
"action": "/action/accountlogin/form_login",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -41,7 +40,7 @@
|
|||
},
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/log_in",
|
||||
"action": "/action/accountlogin/form_login",
|
||||
"method": "post",
|
||||
"text": "Log In",
|
||||
"class": "btn-full"
|
||||
|
|
@ -52,7 +51,7 @@
|
|||
"type": "p",
|
||||
"text": "Need to complete your account?",
|
||||
"link": {
|
||||
"action": "/view/view_createaccount",
|
||||
"action": "/accountcreate",
|
||||
"text": "Create Account"
|
||||
}
|
||||
}
|
||||
|
|
@ -77,7 +76,7 @@
|
|||
},
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/view/overview",
|
||||
"action": "/overview",
|
||||
"text": "Go to Overview"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from pathlib import Path
|
||||
from flask import Blueprint, request, session, redirect, flash
|
||||
import json, re
|
||||
from datetime import datetime, timezone
|
||||
|
|
@ -5,8 +6,9 @@ from auth import require_level
|
|||
from config_utils import ACCOUNTS_FILE
|
||||
import sanitize
|
||||
|
||||
bp = Blueprint('accountadd', __name__)
|
||||
_PAGE = Path(__file__).parent.name
|
||||
|
||||
bp = Blueprint(_PAGE, __name__)
|
||||
|
||||
VALID_LEVELS = {'viewer', 'administrator', 'manager'}
|
||||
|
||||
|
|
@ -23,30 +25,30 @@ def _save_accounts(data):
|
|||
json.dump(data, f, indent=2)
|
||||
|
||||
|
||||
@bp.route('/action/add_account', methods=['POST'])
|
||||
@bp.route('/action/accountmanage/accounts_add', methods=['POST'])
|
||||
@require_level('manager')
|
||||
def add_account():
|
||||
def accounts_add():
|
||||
email = sanitize.email(request.form.get('email_address', ''))
|
||||
access_level = request.form.get('access_level', '').strip()
|
||||
|
||||
if not email:
|
||||
flash('Email address is required.', 'error')
|
||||
return redirect('/view/view_manageaccounts')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if not re.match(r'^[^@\s]+@[^@\s]+\.[^@\s]+$', email):
|
||||
flash('Email address does not appear to be valid.', 'error')
|
||||
return redirect('/view/view_manageaccounts')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if access_level not in VALID_LEVELS:
|
||||
flash('Invalid access level.', 'error')
|
||||
return redirect('/view/view_manageaccounts')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
data = _load_accounts()
|
||||
accounts = data.get('accounts', [])
|
||||
|
||||
if any(a.get('email_address', '').lower() == email for a in accounts):
|
||||
flash('An account with that email address already exists.', 'error')
|
||||
return redirect('/view/view_manageaccounts')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
now = datetime.now(tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
accounts.append({
|
||||
|
|
@ -61,4 +63,35 @@ def add_account():
|
|||
_save_accounts(data)
|
||||
|
||||
flash(f'Authorization added for {email}. User must complete account setup via the Create Account page.', 'success')
|
||||
return redirect('/view/view_manageaccounts')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/accountmanage/accounts_delete', methods=['POST'])
|
||||
@require_level('manager')
|
||||
def accounts_delete():
|
||||
try:
|
||||
row_index = int(request.form.get('row_index', ''))
|
||||
except (ValueError, TypeError):
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
data = _load_accounts()
|
||||
accounts = data.get('accounts', [])
|
||||
|
||||
if row_index < 0 or row_index >= len(accounts):
|
||||
flash('Account not found.', 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
target = accounts[row_index]
|
||||
|
||||
if target.get('email_address', '').lower() == session.get('email_address', '').lower():
|
||||
flash('You cannot remove your own account.', 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
removed_email = target.get('email_address', '')
|
||||
accounts.pop(row_index)
|
||||
data['accounts'] = accounts
|
||||
_save_accounts(data)
|
||||
|
||||
flash(f'Account for {removed_email} has been removed.', 'success')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"id": "view_manageaccounts",
|
||||
"client_requirement": "client_is_manager+",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -40,7 +39,7 @@
|
|||
],
|
||||
"row_actions": [
|
||||
{
|
||||
"action": "/action/delete_account",
|
||||
"action": "/action/accountmanage/accounts_delete",
|
||||
"method": "post",
|
||||
"text": "Remove",
|
||||
"class": "btn-danger btn-sm"
|
||||
|
|
@ -53,7 +52,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/add_account",
|
||||
"action": "/action/accountmanage/accounts_add",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -76,7 +75,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/add_account",
|
||||
"action": "/action/accountmanage/accounts_add",
|
||||
"method": "post",
|
||||
"text": "Authorize"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
from pathlib import Path
|
||||
from flask import Blueprint, request, session, redirect, flash
|
||||
import json, os, secrets
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from auth import require_level
|
||||
from config_utils import ACCOUNTS_FILE
|
||||
|
||||
bp = Blueprint('accountverifyemail', __name__)
|
||||
_PAGE = Path(__file__).parent.name
|
||||
|
||||
bp = Blueprint(_PAGE, __name__)
|
||||
|
||||
|
||||
|
||||
|
|
@ -20,29 +23,29 @@ def _save_accounts(data):
|
|||
json.dump(data, f, indent=2)
|
||||
|
||||
|
||||
@bp.route('/action/verify_email', methods=['POST'])
|
||||
@bp.route('/action/accountverifyemail/email_verify', methods=['POST'])
|
||||
@require_level('nothing')
|
||||
def verify_email():
|
||||
def email_verify():
|
||||
# Abort if already logged in
|
||||
if session.get('access_level', 'nothing') != 'nothing':
|
||||
return redirect('/view/view_overview')
|
||||
return redirect('/overview')
|
||||
|
||||
pending = session.get('pending_create_account')
|
||||
|
||||
if not pending:
|
||||
flash('No pending account creation found. Please start over.', 'error')
|
||||
return redirect('/view/view_createaccount')
|
||||
return redirect('/accountcreate')
|
||||
|
||||
expires = datetime.fromisoformat(pending['expires'])
|
||||
if datetime.now(tz=timezone.utc) > expires:
|
||||
session.pop('pending_create_account', None)
|
||||
flash('Verification code has expired. Please start over.', 'error')
|
||||
return redirect('/view/view_createaccount')
|
||||
return redirect('/accountcreate')
|
||||
|
||||
submitted = request.form.get('code', '').strip()
|
||||
if submitted != pending['code']:
|
||||
flash('Incorrect verification code.', 'error')
|
||||
return redirect('/view/view_verifyemail')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
data = _load_accounts()
|
||||
accounts = data.get('accounts', [])
|
||||
|
|
@ -54,12 +57,12 @@ def verify_email():
|
|||
if account is None:
|
||||
session.pop('pending_create_account', None)
|
||||
flash('Account no longer exists. Contact your manager.', 'error')
|
||||
return redirect('/view/view_createaccount')
|
||||
return redirect('/accountcreate')
|
||||
|
||||
if account.get('hashed_password'):
|
||||
session.pop('pending_create_account', None)
|
||||
flash('This account is already set up. Please log in.', 'error')
|
||||
return redirect('/view/view_login')
|
||||
return redirect('/accountlogin')
|
||||
|
||||
now = datetime.now(tz=timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||
account['hashed_password'] = pending['hashed_password']
|
||||
|
|
@ -77,15 +80,15 @@ def verify_email():
|
|||
session['timezone'] = pending['timezone']
|
||||
session.permanent = True
|
||||
|
||||
return redirect('/view/view_overview')
|
||||
return redirect('/overview')
|
||||
|
||||
|
||||
@bp.route('/action/resend_verification')
|
||||
@bp.route('/action/accountverifyemail/email_resend')
|
||||
@require_level('nothing')
|
||||
def resend_verification():
|
||||
def email_resend():
|
||||
# Abort if already logged in
|
||||
if session.get('access_level', 'nothing') != 'nothing':
|
||||
return redirect('/view/view_overview')
|
||||
return redirect('/overview')
|
||||
|
||||
from pages.accountcreate.action import _send_verification_email, CODE_TTL_MIN
|
||||
|
||||
|
|
@ -93,7 +96,7 @@ def resend_verification():
|
|||
|
||||
if not pending:
|
||||
flash('No pending account creation found. Please start over.', 'error')
|
||||
return redirect('/view/view_createaccount')
|
||||
return redirect('/accountcreate')
|
||||
|
||||
code = f'{secrets.randbelow(1000000):06d}'
|
||||
expires = (datetime.now(tz=timezone.utc) + timedelta(minutes=CODE_TTL_MIN)).isoformat()
|
||||
|
|
@ -102,11 +105,11 @@ def resend_verification():
|
|||
_send_verification_email(pending['email'], code)
|
||||
except Exception as exc:
|
||||
flash(f'Could not resend verification email: {exc}', 'error')
|
||||
return redirect('/view/view_verifyemail')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
pending['code'] = code
|
||||
pending['expires'] = expires
|
||||
session['pending_create_account'] = pending
|
||||
|
||||
flash('A new verification code has been sent.', 'success')
|
||||
return redirect('/view/view_verifyemail')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"id": "view_verifyemail",
|
||||
"client_requirement": "client_is_nothing=",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -19,7 +18,7 @@
|
|||
},
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/verify_email",
|
||||
"action": "/action/accountverifyemail/email_verify",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -32,7 +31,7 @@
|
|||
},
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/verify_email",
|
||||
"action": "/action/accountverifyemail/email_verify",
|
||||
"method": "post",
|
||||
"text": "Verify",
|
||||
"class": "btn-full"
|
||||
|
|
@ -43,7 +42,7 @@
|
|||
"type": "p",
|
||||
"text": "Didn't receive it?",
|
||||
"link": {
|
||||
"action": "/action/resend_verification",
|
||||
"action": "/action/accountverifyemail/email_resend",
|
||||
"text": "Resend code"
|
||||
}
|
||||
},
|
||||
|
|
@ -51,7 +50,7 @@
|
|||
"type": "p",
|
||||
"text": "Wrong email?",
|
||||
"link": {
|
||||
"action": "/view/view_createaccount",
|
||||
"action": "/accountcreate",
|
||||
"text": "Start over"
|
||||
}
|
||||
}
|
||||
|
|
@ -76,7 +75,7 @@
|
|||
},
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/view/view_overview",
|
||||
"action": "/overview",
|
||||
"text": "Go to Overview"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,41 +1,41 @@
|
|||
from pathlib import Path
|
||||
from flask import Blueprint, request, redirect, flash, session
|
||||
from auth import require_level
|
||||
from config_utils import (flush_pending_to_queue, get_dashboard_pending,
|
||||
revert_snapshot_to_config, queued_msg)
|
||||
|
||||
bp = Blueprint('actions', __name__)
|
||||
_PAGE = Path(__file__).parent.name
|
||||
|
||||
_VIEW = '/view/view_actions'
|
||||
bp = Blueprint(_PAGE, __name__)
|
||||
|
||||
|
||||
@bp.route('/action/actions_cardpending_save', methods=['POST'])
|
||||
@bp.route('/action/actions/pending_save', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def actions_cardpending_save():
|
||||
def pending_save():
|
||||
session['apply_changes_immediately'] = 'apply_changes_immediately' in request.form
|
||||
flash('Preference saved.', 'success')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/actions_cardpending_applynow', methods=['POST'])
|
||||
@bp.route('/action/actions/pending_apply', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def actions_cardpending_applynow():
|
||||
def pending_apply():
|
||||
pending = get_dashboard_pending()
|
||||
if not pending:
|
||||
flash('No pending changes to apply.', 'info')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
flush_pending_to_queue()
|
||||
if any(cmd != 'fix problems' for _, _, cmd, _ in pending):
|
||||
flash('Changes queued.', 'success')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/actions_cardhistory_revertselected', methods=['POST'])
|
||||
@bp.route('/action/actions/history_revert', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def actions_cardhistory_revertselected():
|
||||
def history_revert():
|
||||
selected_uuids = request.form.getlist('selected_uuids')
|
||||
if not selected_uuids:
|
||||
flash('No items selected.', 'info')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
succeeded, failed = 0, 0
|
||||
for uuid in selected_uuids:
|
||||
msg, ok = revert_snapshot_to_config(uuid)
|
||||
|
|
@ -47,4 +47,4 @@ def actions_cardhistory_revertselected():
|
|||
if succeeded:
|
||||
plural = 's' if succeeded != 1 else ''
|
||||
flash(f'{succeeded} change{plural} reverted.', 'success')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"id": "view_actions",
|
||||
"client_requirement": "client_is_viewer+",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -22,7 +21,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/actions_cardpending_applynow",
|
||||
"action": "/action/actions/pending_apply",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -50,7 +49,7 @@
|
|||
},
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/actions_cardpending_save",
|
||||
"action": "/action/actions/pending_save",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -66,7 +65,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/actions_cardpending_save",
|
||||
"action": "/action/actions/pending_save",
|
||||
"method": "post",
|
||||
"text": "Save"
|
||||
},
|
||||
|
|
@ -87,7 +86,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/actions_cardhistory_revertselected",
|
||||
"action": "/action/actions/history_revert",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from pathlib import Path
|
||||
import copy
|
||||
|
||||
from flask import Blueprint, request, redirect, flash
|
||||
|
|
@ -6,10 +7,9 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
|||
import sanitize
|
||||
import validation as validate
|
||||
|
||||
bp = Blueprint('bannedips', __name__)
|
||||
|
||||
VIEW = '/view/view_bannedips'
|
||||
_PAGE = Path(__file__).parent.name
|
||||
|
||||
bp = Blueprint(_PAGE, __name__)
|
||||
|
||||
def _row_index():
|
||||
try:
|
||||
|
|
@ -37,15 +37,15 @@ def _parse_ip():
|
|||
return ip
|
||||
|
||||
|
||||
@bp.route('/action/add_banned_ip', methods=['POST'])
|
||||
@bp.route('/action/bannedips/addip_add', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def add_banned_ip():
|
||||
def addip_add():
|
||||
description = sanitize.text(request.form.get('description', ''))
|
||||
ip = _parse_ip()
|
||||
if ip is None:
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
entry = {'description': description, 'ip': ip, 'enabled': True}
|
||||
|
|
@ -54,7 +54,7 @@ def add_banned_ip():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
flash(save_config_with_snapshot(
|
||||
cfg,
|
||||
|
|
@ -62,24 +62,24 @@ def add_banned_ip():
|
|||
before=None, after=entry,
|
||||
description=f'Added banned IP: {ip}',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/toggle_banned_ip', methods=['POST'])
|
||||
@bp.route('/action/bannedips/table_toggle', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def toggle_banned_ip():
|
||||
def table_toggle():
|
||||
idx = _row_index()
|
||||
if idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
items = cfg.get('banned_ips', [])
|
||||
if idx < 0 or idx >= len(items):
|
||||
flash('Entry not found.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
old_enabled = items[idx].get('enabled', True)
|
||||
items[idx]['enabled'] = not old_enabled
|
||||
|
|
@ -87,7 +87,7 @@ def toggle_banned_ip():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
action = 'Enabled' if not old_enabled else 'Disabled'
|
||||
flash(save_config_with_snapshot(
|
||||
|
|
@ -96,31 +96,31 @@ def toggle_banned_ip():
|
|||
before={'enabled': old_enabled}, after={'enabled': not old_enabled},
|
||||
description=f'{action} banned IP: {items[idx]["ip"]}',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/edit_banned_ip', methods=['POST'])
|
||||
@bp.route('/action/bannedips/table_edit', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def edit_banned_ip():
|
||||
def table_edit():
|
||||
idx = _row_index()
|
||||
if idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
description = sanitize.text(request.form.get('description', ''))
|
||||
ip = _parse_ip()
|
||||
if ip is None:
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
enabled = request.form.get('enabled') == 'on'
|
||||
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
items = cfg.get('banned_ips', [])
|
||||
if idx < 0 or idx >= len(items):
|
||||
flash('Entry not found.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
before = copy.deepcopy(items[idx])
|
||||
items[idx].update({'description': description, 'ip': ip, 'enabled': enabled})
|
||||
|
|
@ -128,7 +128,7 @@ def edit_banned_ip():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
flash(save_config_with_snapshot(
|
||||
cfg,
|
||||
|
|
@ -136,31 +136,31 @@ def edit_banned_ip():
|
|||
before=before, after=copy.deepcopy(items[idx]),
|
||||
description=f'Edited banned IP: {ip}',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/delete_banned_ip', methods=['POST'])
|
||||
@bp.route('/action/bannedips/table_delete', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def delete_banned_ip():
|
||||
def table_delete():
|
||||
idx = _row_index()
|
||||
if idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
items = cfg.get('banned_ips', [])
|
||||
if idx < 0 or idx >= len(items):
|
||||
flash('Entry not found.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
removed = items.pop(idx)
|
||||
errors = validate.validate_config(cfg)
|
||||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
flash(save_config_with_snapshot(
|
||||
cfg,
|
||||
|
|
@ -168,4 +168,4 @@ def delete_banned_ip():
|
|||
before=removed, after=None,
|
||||
description=f'Deleted banned IP: {removed["ip"]}',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"id": "view_bannedips",
|
||||
"client_requirement": "client_is_viewer+",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -43,7 +42,7 @@
|
|||
"row_actions": [
|
||||
{
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"action": "/action/edit_banned_ip",
|
||||
"action": "/action/bannedips/table_edit",
|
||||
"method": "inline_edit",
|
||||
"text": "Edit",
|
||||
"class": "btn-ghost btn-sm",
|
||||
|
|
@ -65,7 +64,7 @@
|
|||
},
|
||||
{
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"action": "/action/delete_banned_ip",
|
||||
"action": "/action/bannedips/table_delete",
|
||||
"method": "post",
|
||||
"text": "Delete",
|
||||
"class": "btn-danger btn-sm"
|
||||
|
|
@ -80,7 +79,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/add_banned_ip",
|
||||
"action": "/action/bannedips/addip_add",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -103,7 +102,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/add_banned_ip",
|
||||
"action": "/action/bannedips/addip_add",
|
||||
"method": "post",
|
||||
"text": "Add Banned IP"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from pathlib import Path
|
||||
import copy
|
||||
import os
|
||||
from flask import Blueprint, request, redirect, flash, send_file, abort
|
||||
|
|
@ -6,32 +7,33 @@ from config_utils import load_config, verify_config_hash, save_config_with_snaps
|
|||
import sanitize
|
||||
import validation as validate
|
||||
|
||||
bp = Blueprint('ddns', __name__)
|
||||
_PAGE = Path(__file__).parent.name
|
||||
|
||||
bp = Blueprint(_PAGE, __name__)
|
||||
|
||||
VIEW = '/view/view_ddns'
|
||||
LOG_FILE = f'{CONFIGS_DIR}/ddns.log'
|
||||
|
||||
|
||||
@bp.route('/action/ddns_cardaddaccount_add', methods=['POST'])
|
||||
@bp.route('/action/ddns/addaccount_add', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def ddns_cardaddaccount_add():
|
||||
def addaccount_add():
|
||||
provider_type = sanitize.filtervalue(request.form.get('provider', ''), validate.VALID_DDNS_PROVIDERS)
|
||||
description = sanitize.description(request.form.get('description', ''))
|
||||
hostnames = sanitize.domainlist(request.form.get('hostnames', '').splitlines())
|
||||
|
||||
if not description:
|
||||
flash('Description is required.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not hostnames:
|
||||
flash('At least one hostname is required.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not provider_type:
|
||||
flash('Unknown provider type.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
entry = {
|
||||
'description': description,
|
||||
|
|
@ -54,17 +56,17 @@ def ddns_cardaddaccount_add():
|
|||
cmd='ddns update',
|
||||
queue=False,
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/ddns_tableaccounts_rowedit', methods=['POST'])
|
||||
@bp.route('/action/ddns/accounts_edit', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def ddns_tableaccounts_rowedit():
|
||||
def accounts_edit():
|
||||
try:
|
||||
row_index = int(request.form.get('row_index', -1))
|
||||
except (TypeError, ValueError):
|
||||
flash('Invalid row index.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
provider_type = sanitize.filtervalue(request.form.get('provider', ''), validate.VALID_DDNS_PROVIDERS)
|
||||
description = sanitize.description(request.form.get('description', ''))
|
||||
|
|
@ -73,17 +75,17 @@ def ddns_tableaccounts_rowedit():
|
|||
|
||||
if not provider_type:
|
||||
flash('Unknown provider type.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
providers = cfg.setdefault('ddns', {}).setdefault('providers', [])
|
||||
if row_index < 0 or row_index >= len(providers):
|
||||
flash('Invalid provider index.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
before = copy.deepcopy(providers[row_index])
|
||||
entry = {
|
||||
|
|
@ -106,27 +108,27 @@ def ddns_tableaccounts_rowedit():
|
|||
cmd='ddns update',
|
||||
queue=False,
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/ddns_tableaccounts_rowdelete', methods=['POST'])
|
||||
@bp.route('/action/ddns/accounts_delete', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def ddns_tableaccounts_rowdelete():
|
||||
def accounts_delete():
|
||||
try:
|
||||
row_index = int(request.form.get('row_index', -1))
|
||||
except (TypeError, ValueError):
|
||||
flash('Invalid row index.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
providers = cfg.setdefault('ddns', {}).setdefault('providers', [])
|
||||
if row_index < 0 or row_index >= len(providers):
|
||||
flash('Invalid provider index.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
before = copy.deepcopy(providers[row_index])
|
||||
description = before.get('description', str(row_index))
|
||||
|
|
@ -138,12 +140,12 @@ def ddns_tableaccounts_rowdelete():
|
|||
cmd='ddns update',
|
||||
queue=False,
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/ddns_cardipcheckinterval_save', methods=['POST'])
|
||||
@bp.route('/action/ddns/ipcheckinterval_save', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def ddns_cardipcheckinterval_save():
|
||||
def ipcheckinterval_save():
|
||||
raw = request.form.get('timer_interval', '').strip()
|
||||
try:
|
||||
mins = int(raw)
|
||||
|
|
@ -151,12 +153,12 @@ def ddns_cardipcheckinterval_save():
|
|||
raise ValueError
|
||||
except ValueError:
|
||||
flash('Interval must be a whole number of minutes >= 1.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
timer_interval = f'{mins}m'
|
||||
|
||||
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
before = copy.deepcopy(cfg.get('ddns', {}).get('general', {}))
|
||||
|
|
@ -167,22 +169,22 @@ def ddns_cardipcheckinterval_save():
|
|||
description='Updated DDNS check interval',
|
||||
cmd='core apply',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/ddns_cardipcheckservices_save', methods=['POST'])
|
||||
@bp.route('/action/ddns/ipcheckservices_save', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def ddns_cardipcheckservices_save():
|
||||
def ipcheckservices_save():
|
||||
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
http_services = [u.strip() for u in request.form.getlist('http_services') if u.strip()]
|
||||
dig_services = [u.strip() for u in request.form.getlist('dig_services') if u.strip()]
|
||||
|
||||
if not http_services and not dig_services:
|
||||
flash('At least one IP check service is required.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
before = copy.deepcopy(cfg.get('ddns', {}).get('ip_check_services', []))
|
||||
|
|
@ -196,21 +198,21 @@ def ddns_cardipcheckservices_save():
|
|||
cmd='ddns update',
|
||||
queue=False,
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/ddns_cardlogging_save', methods=['POST'])
|
||||
@bp.route('/action/ddns/logging_save', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def ddns_cardlogging_save():
|
||||
def logging_save():
|
||||
log_max_kb = validate.int_range(request.form.get('log_max_kb', '').strip(), 64, None)
|
||||
if log_max_kb is None:
|
||||
flash('Max Log Size must be a number >= 64.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
log_errors_only = 'log_errors_only' in request.form
|
||||
|
||||
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
before = copy.deepcopy(cfg.get('ddns', {}).get('general', {}))
|
||||
|
|
@ -225,23 +227,23 @@ def ddns_cardlogging_save():
|
|||
cmd='ddns update',
|
||||
queue=False,
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/ddns_cardlogging_clear', methods=['POST'])
|
||||
@bp.route('/action/ddns/logging_clear', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def ddns_cardlogging_clear():
|
||||
def logging_clear():
|
||||
try:
|
||||
open(LOG_FILE, 'w').close()
|
||||
flash('DDNS log cleared.', 'success')
|
||||
except Exception as ex:
|
||||
flash(f'Could not clear log: {ex}', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/ddns_cardlogging_download', methods=['GET'])
|
||||
@bp.route('/action/ddns/logging_download', methods=['GET'])
|
||||
@require_level('administrator')
|
||||
def ddns_cardlogging_download():
|
||||
def logging_download():
|
||||
if not os.path.isfile(LOG_FILE):
|
||||
abort(404)
|
||||
return send_file(LOG_FILE, as_attachment=True, download_name='ddns.log', mimetype='text/plain')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"id": "view_ddns",
|
||||
"client_requirement": "client_is_viewer+",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -29,7 +28,7 @@
|
|||
"label": "IP Check Interval",
|
||||
"value": "%DDNS_TIMER_INTERVAL%",
|
||||
"sub": "%STAT_PUBLIC_IP_LAST_CHECKED%",
|
||||
"edit_action": "/action/ddns_cardipcheckinterval_save",
|
||||
"edit_action": "/action/ddns/ipcheckinterval_save",
|
||||
"edit_field": "timer_interval",
|
||||
"edit_input_type": "number",
|
||||
"edit_min": "1",
|
||||
|
|
@ -54,7 +53,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/ddns_cardipcheckservices_save",
|
||||
"action": "/action/ddns/ipcheckservices_save",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -78,7 +77,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/ddns_cardipcheckservices_save",
|
||||
"action": "/action/ddns/ipcheckservices_save",
|
||||
"method": "post",
|
||||
"text": "Save"
|
||||
},
|
||||
|
|
@ -125,7 +124,7 @@
|
|||
"row_actions": [
|
||||
{
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"action": "/action/ddns_tableaccounts_rowedit",
|
||||
"action": "/action/ddns/accounts_edit",
|
||||
"method": "inline_edit",
|
||||
"text": "Edit",
|
||||
"class": "btn-ghost btn-sm",
|
||||
|
|
@ -155,7 +154,7 @@
|
|||
},
|
||||
{
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"action": "/action/ddns_tableaccounts_rowdelete",
|
||||
"action": "/action/ddns/accounts_delete",
|
||||
"method": "post",
|
||||
"text": "Delete",
|
||||
"class": "btn-danger btn-sm"
|
||||
|
|
@ -169,7 +168,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/ddns_cardaddaccount_add",
|
||||
"action": "/action/ddns/addaccount_add",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -202,7 +201,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/ddns_cardaddaccount_add",
|
||||
"action": "/action/ddns/addaccount_add",
|
||||
"method": "post",
|
||||
"text": "Add Provider"
|
||||
},
|
||||
|
|
@ -236,12 +235,12 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_ghost",
|
||||
"action": "/action/ddns_cardlogging_download",
|
||||
"action": "/action/ddns/logging_download",
|
||||
"text": "Download Log"
|
||||
},
|
||||
{
|
||||
"type": "button_danger",
|
||||
"action": "/action/ddns_cardlogging_clear",
|
||||
"action": "/action/ddns/logging_clear",
|
||||
"method": "post",
|
||||
"text": "Clear Log"
|
||||
}
|
||||
|
|
@ -252,7 +251,7 @@
|
|||
},
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/ddns_cardlogging_save",
|
||||
"action": "/action/ddns/logging_save",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -277,7 +276,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/ddns_cardlogging_save",
|
||||
"action": "/action/ddns/logging_save",
|
||||
"method": "post",
|
||||
"text": "Save"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from pathlib import Path
|
||||
import copy
|
||||
import ipaddress
|
||||
|
||||
|
|
@ -7,10 +8,9 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
|||
import sanitize
|
||||
import validation as validate
|
||||
|
||||
bp = Blueprint('dhcp', __name__)
|
||||
|
||||
VIEW = '/view/view_dhcp'
|
||||
_PAGE = Path(__file__).parent.name
|
||||
|
||||
bp = Blueprint(_PAGE, __name__)
|
||||
|
||||
def _row_index():
|
||||
try:
|
||||
|
|
@ -66,9 +66,9 @@ def _check_ip_conflicts(ip, vlan):
|
|||
return None
|
||||
|
||||
|
||||
@bp.route('/action/add_dhcp_reservation', methods=['POST'])
|
||||
@bp.route('/action/dhcp/addreservation_add', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def add_dhcp_reservation():
|
||||
def addreservation_add():
|
||||
vlan_name = sanitize.name(request.form.get('vlan_name', ''))
|
||||
description = sanitize.text(request.form.get('description', ''))
|
||||
hostname = validate.domainname(request.form.get('hostname', ''))
|
||||
|
|
@ -77,27 +77,27 @@ def add_dhcp_reservation():
|
|||
radius_client = 'radius_client' in request.form
|
||||
|
||||
if ip is None:
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not vlan_name:
|
||||
flash('The configuration has not been saved because a VLAN is required.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not mac:
|
||||
flash('The configuration has not been saved because a MAC address is required.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
vlans = cfg.get('vlans', [])
|
||||
vlan = next((v for v in vlans if v.get('name') == vlan_name), None)
|
||||
if vlan is None:
|
||||
flash(f'The configuration has not been saved because VLAN "{vlan_name}" was not found.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
conflict = _check_ip_conflicts(ip, vlan)
|
||||
if conflict:
|
||||
flash(f'The configuration has not been saved because {conflict}', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
entry = {
|
||||
'description': description,
|
||||
|
|
@ -112,7 +112,7 @@ def add_dhcp_reservation():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
flash(save_config_with_snapshot(
|
||||
cfg,
|
||||
|
|
@ -120,25 +120,25 @@ def add_dhcp_reservation():
|
|||
before=None, after=entry,
|
||||
description=f'Added DHCP reservation: {hostname or mac} ({ip or "dynamic"})',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/toggle_dhcp_reservation', methods=['POST'])
|
||||
@bp.route('/action/dhcp/reservations_toggle', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def toggle_dhcp_reservation():
|
||||
def reservations_toggle():
|
||||
idx = _row_index()
|
||||
if idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
vlans = cfg.get('vlans', [])
|
||||
vi, ri = _flat_index_to_vlan_res(vlans, idx)
|
||||
if vi is None:
|
||||
flash('Entry not found.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
res = vlans[vi]['reservations'][ri]
|
||||
old_enabled = res.get('enabled', True)
|
||||
|
|
@ -147,7 +147,7 @@ def toggle_dhcp_reservation():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
vlan_name = vlans[vi]['name']
|
||||
action = 'Enabled' if not old_enabled else 'Disabled'
|
||||
|
|
@ -157,16 +157,16 @@ def toggle_dhcp_reservation():
|
|||
before={'enabled': old_enabled}, after={'enabled': not old_enabled},
|
||||
description=f'{action} DHCP reservation: {res.get("hostname") or res["mac"]}',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/edit_dhcp_reservation', methods=['POST'])
|
||||
@bp.route('/action/dhcp/reservations_edit', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def edit_dhcp_reservation():
|
||||
def reservations_edit():
|
||||
idx = _row_index()
|
||||
if idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
description = sanitize.text(request.form.get('description', ''))
|
||||
hostname = validate.domainname(request.form.get('hostname', ''))
|
||||
|
|
@ -175,24 +175,24 @@ def edit_dhcp_reservation():
|
|||
radius_client = 'radius_client' in request.form
|
||||
|
||||
if ip is None:
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not mac:
|
||||
flash('The configuration has not been saved because a MAC address is required.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
vlans = cfg.get('vlans', [])
|
||||
vi, ri = _flat_index_to_vlan_res(vlans, idx)
|
||||
if vi is None:
|
||||
flash('Entry not found.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
conflict = _check_ip_conflicts(ip, vlans[vi])
|
||||
if conflict:
|
||||
flash(f'The configuration has not been saved because {conflict}', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
res = vlans[vi]['reservations'][ri]
|
||||
before = copy.deepcopy(res)
|
||||
|
|
@ -208,7 +208,7 @@ def edit_dhcp_reservation():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
vlan_name = vlans[vi]['name']
|
||||
flash(save_config_with_snapshot(
|
||||
|
|
@ -217,25 +217,25 @@ def edit_dhcp_reservation():
|
|||
before=before, after=copy.deepcopy(res),
|
||||
description=f'Edited DHCP reservation: {hostname or mac} ({ip or "dynamic"})',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/delete_dhcp_reservation', methods=['POST'])
|
||||
@bp.route('/action/dhcp/reservations_delete', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def delete_dhcp_reservation():
|
||||
def reservations_delete():
|
||||
idx = _row_index()
|
||||
if idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
vlans = cfg.get('vlans', [])
|
||||
vi, ri = _flat_index_to_vlan_res(vlans, idx)
|
||||
if vi is None:
|
||||
flash('Entry not found.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
vlan_name = vlans[vi]['name']
|
||||
removed = vlans[vi]['reservations'].pop(ri)
|
||||
|
|
@ -243,7 +243,7 @@ def delete_dhcp_reservation():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
flash(save_config_with_snapshot(
|
||||
cfg,
|
||||
|
|
@ -251,4 +251,4 @@ def delete_dhcp_reservation():
|
|||
before=removed, after=None,
|
||||
description=f'Deleted DHCP reservation: {removed.get("hostname") or removed["mac"]}',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"id": "view_dhcp",
|
||||
"client_requirement": "client_is_viewer+",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -97,7 +96,7 @@
|
|||
"row_actions": [
|
||||
{
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"action": "/action/edit_dhcp_reservation",
|
||||
"action": "/action/dhcp/reservations_edit",
|
||||
"method": "inline_edit",
|
||||
"text": "Edit",
|
||||
"class": "btn-ghost btn-sm",
|
||||
|
|
@ -134,7 +133,7 @@
|
|||
},
|
||||
{
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"action": "/action/delete_dhcp_reservation",
|
||||
"action": "/action/dhcp/reservations_delete",
|
||||
"method": "post",
|
||||
"text": "Delete",
|
||||
"class": "btn-danger btn-sm"
|
||||
|
|
@ -149,7 +148,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/add_dhcp_reservation",
|
||||
"action": "/action/dhcp/addreservation_add",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -203,7 +202,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/add_dhcp_reservation",
|
||||
"action": "/action/dhcp/addreservation_add",
|
||||
"method": "post",
|
||||
"text": "Add"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from pathlib import Path
|
||||
import copy
|
||||
import re
|
||||
from flask import Blueprint, request, redirect, flash
|
||||
|
|
@ -6,9 +7,9 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
|||
import sanitize
|
||||
import validation as validate
|
||||
|
||||
bp = Blueprint('dnsblocking', __name__)
|
||||
_PAGE = Path(__file__).parent.name
|
||||
|
||||
VIEW = '/view/view_dnsblocking'
|
||||
bp = Blueprint(_PAGE, __name__)
|
||||
|
||||
_VALID_FORMATS_STR = ', '.join(sorted(validate.VALID_BLOCKLIST_FORMATS))
|
||||
|
||||
|
|
@ -52,22 +53,22 @@ def _parse_fields():
|
|||
return {'name': name, 'description': description, 'format': fmt, 'url': url}, None
|
||||
|
||||
|
||||
@bp.route('/action/dnsblocking_tableblocklists_rowdelete', methods=['POST'])
|
||||
@bp.route('/action/dnsblocking/blocklists_delete', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def dnsblocking_tableblocklists_rowdelete():
|
||||
def blocklists_delete():
|
||||
idx = _row_index()
|
||||
if idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
items = cfg.get('dns_blocking', {}).get('blocklists', [])
|
||||
if idx < 0 or idx >= len(items):
|
||||
flash('Entry not found.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
before = copy.deepcopy(items[idx])
|
||||
name = before.get('name', str(idx))
|
||||
|
|
@ -76,36 +77,36 @@ def dnsblocking_tableblocklists_rowdelete():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
flash(save_config_with_snapshot(
|
||||
cfg, path='dns_blocking', key=name, operation='delete',
|
||||
before=before, after=None,
|
||||
description=f'Deleted blocklist: {name}',
|
||||
queue=False,
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/dnsblocking_tableblocklists_rowedit', methods=['POST'])
|
||||
@bp.route('/action/dnsblocking/blocklists_edit', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def dnsblocking_tableblocklists_rowedit():
|
||||
def blocklists_edit():
|
||||
idx = _row_index()
|
||||
if idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
fields, err = _parse_fields()
|
||||
if err:
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
items = cfg.get('dns_blocking', {}).get('blocklists', [])
|
||||
if idx < 0 or idx >= len(items):
|
||||
flash('Entry not found.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
before = copy.deepcopy(items[idx])
|
||||
items[idx].update({
|
||||
|
|
@ -118,32 +119,32 @@ def dnsblocking_tableblocklists_rowedit():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
flash(save_config_with_snapshot(
|
||||
cfg, path='dns_blocking', key=fields['name'], operation='edit',
|
||||
before=before, after=copy.deepcopy(items[idx]),
|
||||
description=f'Edited blocklist: {fields["name"]}',
|
||||
queue=False,
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/dnsblocking_cardaddblocklist_add', methods=['POST'])
|
||||
@bp.route('/action/dnsblocking/addblocklist_add', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def dnsblocking_cardaddblocklist_add():
|
||||
def addblocklist_add():
|
||||
fields, err = _parse_fields()
|
||||
if err:
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
blocklists = cfg.setdefault('dns_blocking', {}).setdefault('blocklists', [])
|
||||
|
||||
if any(b.get('name', '').lower() == fields['name'].lower() for b in blocklists):
|
||||
flash('The configuration has not been saved because a blocklist with that name already exists.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
entry = {
|
||||
'name': fields['name'],
|
||||
|
|
@ -157,28 +158,28 @@ def dnsblocking_cardaddblocklist_add():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
flash(save_config_with_snapshot(
|
||||
cfg, path='dns_blocking', key=fields['name'], operation='add',
|
||||
before=None, after=copy.deepcopy(entry),
|
||||
description=f'Added blocklist: {fields["name"]}',
|
||||
queue=False,
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/dnsblocking_cardblocklistrefresh_save', methods=['POST'])
|
||||
@bp.route('/action/dnsblocking/blocklistrefresh_save', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def dnsblocking_cardblocklistrefresh_save():
|
||||
def blocklistrefresh_save():
|
||||
daily_execute_time = validate.time_24h(sanitize.time_24h(request.form.get('daily_execute_time_24hr_local', '')))
|
||||
|
||||
if not daily_execute_time:
|
||||
flash('Daily Refresh Time must be a valid 24-hour time (e.g. 02:30).', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
before = copy.deepcopy(cfg.get('dns_blocking', {}).get('general', {}))
|
||||
|
|
@ -189,30 +190,30 @@ def dnsblocking_cardblocklistrefresh_save():
|
|||
description='Updated daily blocklist refresh time',
|
||||
cmd='core apply',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/dnsblocking_cardblocklistrefresh_refreshnow', methods=['POST'])
|
||||
@bp.route('/action/dnsblocking/blocklistrefresh_refresh', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def dnsblocking_cardblocklistrefresh_refreshnow():
|
||||
def blocklistrefresh_refresh():
|
||||
flash(queued_msg('core update-blocklists', action_label='Blocklist refresh queued'), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/dnsblocking_cardlogging_save', methods=['POST'])
|
||||
@bp.route('/action/dnsblocking/logging_save', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def dnsblocking_cardlogging_save():
|
||||
def logging_save():
|
||||
log_max_kb_raw = request.form.get('log_max_kb', '').strip()
|
||||
log_errors_only = 'log_errors_only' in request.form
|
||||
|
||||
log_max_kb = validate.int_range(log_max_kb_raw, 64, None)
|
||||
if log_max_kb is None:
|
||||
flash('Max Log Size must be a number >= 64.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
before = copy.deepcopy(cfg.get('dns_blocking', {}).get('general', {}))
|
||||
|
|
@ -224,11 +225,11 @@ def dnsblocking_cardlogging_save():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
flash(save_config_with_snapshot(
|
||||
cfg, path='dns_blocking', key='general', operation='edit',
|
||||
before=before, after=copy.deepcopy(cfg['dns_blocking']['general']),
|
||||
description='Updated DNS blocking log settings',
|
||||
queue=False,
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"id": "view_dnsblocking",
|
||||
"client_requirement": "client_is_viewer+",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -42,7 +41,7 @@
|
|||
"row_actions": [
|
||||
{
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"action": "/action/dnsblocking_tableblocklists_rowedit",
|
||||
"action": "/action/dnsblocking/blocklists_edit",
|
||||
"method": "inline_edit",
|
||||
"text": "Edit",
|
||||
"class": "btn-ghost btn-sm",
|
||||
|
|
@ -70,7 +69,7 @@
|
|||
},
|
||||
{
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"action": "/action/dnsblocking_tableblocklists_rowdelete",
|
||||
"action": "/action/dnsblocking/blocklists_delete",
|
||||
"method": "post",
|
||||
"text": "Delete",
|
||||
"class": "btn-danger btn-sm"
|
||||
|
|
@ -85,7 +84,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/dnsblocking_cardaddblocklist_add",
|
||||
"action": "/action/dnsblocking/addblocklist_add",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -123,7 +122,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/dnsblocking_cardaddblocklist_add",
|
||||
"action": "/action/dnsblocking/addblocklist_add",
|
||||
"method": "post",
|
||||
"text": "Add Blocklist"
|
||||
},
|
||||
|
|
@ -154,7 +153,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_secondary",
|
||||
"action": "/action/dnsblocking_cardblocklistrefresh_refreshnow",
|
||||
"action": "/action/dnsblocking/blocklistrefresh_refresh",
|
||||
"method": "post",
|
||||
"text": "Refresh All Now"
|
||||
}
|
||||
|
|
@ -165,7 +164,7 @@
|
|||
},
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/dnsblocking_cardblocklistrefresh_save",
|
||||
"action": "/action/dnsblocking/blocklistrefresh_save",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -183,7 +182,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/dnsblocking_cardblocklistrefresh_save",
|
||||
"action": "/action/dnsblocking/blocklistrefresh_save",
|
||||
"method": "post",
|
||||
"text": "Save"
|
||||
},
|
||||
|
|
@ -204,7 +203,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/dnsblocking_cardlogging_save",
|
||||
"action": "/action/dnsblocking/logging_save",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -229,7 +228,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/dnsblocking_cardlogging_save",
|
||||
"action": "/action/dnsblocking/logging_save",
|
||||
"method": "post",
|
||||
"text": "Save"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from pathlib import Path
|
||||
import copy
|
||||
from flask import Blueprint, request, redirect, flash
|
||||
from auth import require_level
|
||||
|
|
@ -5,33 +6,32 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
|||
import sanitize
|
||||
import validation as validate
|
||||
|
||||
bp = Blueprint('dnsserver', __name__)
|
||||
_PAGE = Path(__file__).parent.name
|
||||
|
||||
_VIEW = '/view/view_dnsserver'
|
||||
bp = Blueprint(_PAGE, __name__)
|
||||
|
||||
|
||||
@bp.route('/action/dnsserver_cardupstreamdns_save', methods=['POST'])
|
||||
@bp.route('/action/dnsserver/upstreamdns_save', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def dnsserver_cardupstreamdns_save():
|
||||
def upstreamdns_save():
|
||||
strict_order = 'strict_order' in request.form
|
||||
submitted = request.form.getlist('upstream_servers')
|
||||
|
||||
for s in submitted:
|
||||
if not s.strip():
|
||||
flash('Remove blank server entries before saving.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
upstream_servers = []
|
||||
for s in submitted:
|
||||
clean = sanitize.ip(s.strip())
|
||||
if not clean:
|
||||
flash(f"'{s.strip()}' is not a valid IP address.", 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
upstream_servers.append(clean)
|
||||
|
||||
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
before = copy.deepcopy(cfg.get('upstream_dns', {}))
|
||||
|
|
@ -39,7 +39,7 @@ def dnsserver_cardupstreamdns_save():
|
|||
if (strict_order == bool(current.get('strict_order', False)) and
|
||||
upstream_servers == current.get('upstream_servers', [])):
|
||||
flash('No changes detected.', 'info')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg.setdefault('upstream_dns', {}).update({
|
||||
'strict_order': strict_order,
|
||||
|
|
@ -49,45 +49,45 @@ def dnsserver_cardupstreamdns_save():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
flash(save_config_with_snapshot(
|
||||
cfg, path='upstream_dns', key='global', operation='edit',
|
||||
before=before, after=copy.deepcopy(cfg['upstream_dns']),
|
||||
description='Updated upstream DNS servers',
|
||||
cmd='core apply',
|
||||
), 'success')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/dnsserver_carddnsforwarding_save', methods=['POST'])
|
||||
@bp.route('/action/dnsserver/dnsforwarding_save', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def dnsserver_carddnsforwarding_save():
|
||||
def dnsforwarding_save():
|
||||
cache_size = validate.int_range(request.form.get('cache_size', '').strip(), 0, None)
|
||||
if cache_size is None:
|
||||
flash('Cache Size must be a non-negative integer.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
before = copy.deepcopy(cfg.get('upstream_dns', {}))
|
||||
current = cfg.get('upstream_dns', {})
|
||||
if cache_size == int(current.get('cache_size', 0)):
|
||||
flash('No changes detected.', 'info')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg.setdefault('upstream_dns', {})['cache_size'] = cache_size
|
||||
errors = validate.validate_config(cfg)
|
||||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
flash(save_config_with_snapshot(
|
||||
cfg, path='upstream_dns', key='global', operation='edit',
|
||||
before=before, after=copy.deepcopy(cfg['upstream_dns']),
|
||||
description='Updated DNS cache size',
|
||||
cmd='core apply',
|
||||
), 'success')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"id": "view_dnsserver",
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -22,7 +21,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/dnsserver_cardupstreamdns_save",
|
||||
"action": "/action/dnsserver/upstreamdns_save",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -48,7 +47,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/dnsserver_cardupstreamdns_save",
|
||||
"action": "/action/dnsserver/upstreamdns_save",
|
||||
"method": "post",
|
||||
"text": "Save"
|
||||
},
|
||||
|
|
@ -69,7 +68,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/dnsserver_carddnsforwarding_save",
|
||||
"action": "/action/dnsserver/dnsforwarding_save",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -86,7 +85,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/dnsserver_carddnsforwarding_save",
|
||||
"action": "/action/dnsserver/dnsforwarding_save",
|
||||
"method": "post",
|
||||
"text": "Save"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from pathlib import Path
|
||||
import copy
|
||||
import ipaddress
|
||||
|
||||
|
|
@ -7,10 +8,9 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
|||
import sanitize
|
||||
import validation as validate
|
||||
|
||||
bp = Blueprint('hostoverrides', __name__)
|
||||
|
||||
VIEW = '/view/view_hostoverrides'
|
||||
_PAGE = Path(__file__).parent.name
|
||||
|
||||
bp = Blueprint(_PAGE, __name__)
|
||||
|
||||
def _vlan_networks(cfg):
|
||||
nets = []
|
||||
|
|
@ -48,23 +48,23 @@ def _hash_ok():
|
|||
return True
|
||||
|
||||
|
||||
@bp.route('/action/add_host_override', methods=['POST'])
|
||||
@bp.route('/action/hostoverrides/addoverride_add', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def add_host_override():
|
||||
def addoverride_add():
|
||||
description = sanitize.text(request.form.get('description', ''))
|
||||
host = validate.domainname(request.form.get('host', ''))
|
||||
ip = sanitize.ip(request.form.get('ip', ''))
|
||||
|
||||
if not host or not ip:
|
||||
flash('Hostname and IP address are required.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
if not _ip_in_vlan(ip, cfg):
|
||||
flash('IP address does not fall within any configured VLAN subnet.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
entry = {'description': description, 'host': host, 'ip': ip, 'enabled': True}
|
||||
cfg.setdefault('host_overrides', []).append(entry)
|
||||
|
|
@ -72,7 +72,7 @@ def add_host_override():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
flash(save_config_with_snapshot(
|
||||
cfg,
|
||||
|
|
@ -80,24 +80,24 @@ def add_host_override():
|
|||
before=None, after=entry,
|
||||
description=f'Added host override: {host} → {ip}',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/toggle_host_override', methods=['POST'])
|
||||
@bp.route('/action/hostoverrides/table_toggle', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def toggle_host_override():
|
||||
def table_toggle():
|
||||
idx = _row_index()
|
||||
if idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
items = cfg.get('host_overrides', [])
|
||||
if idx < 0 or idx >= len(items):
|
||||
flash('Entry not found.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
old_enabled = items[idx].get('enabled', True)
|
||||
items[idx]['enabled'] = not old_enabled
|
||||
|
|
@ -105,7 +105,7 @@ def toggle_host_override():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
action = 'Enabled' if not old_enabled else 'Disabled'
|
||||
flash(save_config_with_snapshot(
|
||||
|
|
@ -114,16 +114,16 @@ def toggle_host_override():
|
|||
before={'enabled': old_enabled}, after={'enabled': not old_enabled},
|
||||
description=f'{action} host override: {items[idx]["host"]}',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/edit_host_override', methods=['POST'])
|
||||
@bp.route('/action/hostoverrides/table_edit', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def edit_host_override():
|
||||
def table_edit():
|
||||
idx = _row_index()
|
||||
if idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
description = sanitize.text(request.form.get('description', ''))
|
||||
host = validate.domainname(request.form.get('host', ''))
|
||||
|
|
@ -132,19 +132,19 @@ def edit_host_override():
|
|||
|
||||
if not host or not ip:
|
||||
flash('Hostname and IP address are required.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
if not _ip_in_vlan(ip, cfg):
|
||||
flash('IP address does not fall within any configured VLAN subnet.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
items = cfg.get('host_overrides', [])
|
||||
if idx < 0 or idx >= len(items):
|
||||
flash('Entry not found.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
before = copy.deepcopy(items[idx])
|
||||
items[idx].update({'description': description, 'host': host, 'ip': ip, 'enabled': enabled})
|
||||
|
|
@ -152,7 +152,7 @@ def edit_host_override():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
flash(save_config_with_snapshot(
|
||||
cfg,
|
||||
|
|
@ -160,31 +160,31 @@ def edit_host_override():
|
|||
before=before, after=copy.deepcopy(items[idx]),
|
||||
description=f'Edited host override: {host} → {ip}',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/delete_host_override', methods=['POST'])
|
||||
@bp.route('/action/hostoverrides/table_delete', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def delete_host_override():
|
||||
def table_delete():
|
||||
idx = _row_index()
|
||||
if idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
items = cfg.get('host_overrides', [])
|
||||
if idx < 0 or idx >= len(items):
|
||||
flash('Entry not found.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
removed = items.pop(idx)
|
||||
errors = validate.validate_config(cfg)
|
||||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
flash(save_config_with_snapshot(
|
||||
cfg,
|
||||
|
|
@ -192,4 +192,4 @@ def delete_host_override():
|
|||
before=removed, after=None,
|
||||
description=f'Deleted host override: {removed["host"]}',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"id": "view_hostoverrides",
|
||||
"client_requirement": "client_is_viewer+",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -43,7 +42,7 @@
|
|||
"row_actions": [
|
||||
{
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"action": "/action/edit_host_override",
|
||||
"action": "/action/hostoverrides/table_edit",
|
||||
"method": "inline_edit",
|
||||
"text": "Edit",
|
||||
"class": "btn-ghost btn-sm",
|
||||
|
|
@ -71,7 +70,7 @@
|
|||
},
|
||||
{
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"action": "/action/delete_host_override",
|
||||
"action": "/action/hostoverrides/table_delete",
|
||||
"method": "post",
|
||||
"text": "Delete",
|
||||
"class": "btn-danger btn-sm"
|
||||
|
|
@ -86,7 +85,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/add_host_override",
|
||||
"action": "/action/hostoverrides/addoverride_add",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -117,7 +116,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/add_host_override",
|
||||
"action": "/action/hostoverrides/addoverride_add",
|
||||
"method": "post",
|
||||
"text": "Add Host Override"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from pathlib import Path
|
||||
import copy
|
||||
|
||||
from flask import Blueprint, request, redirect, flash
|
||||
|
|
@ -6,9 +7,9 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
|||
import sanitize
|
||||
import validation as validate
|
||||
|
||||
bp = Blueprint('intervlan', __name__)
|
||||
_PAGE = Path(__file__).parent.name
|
||||
|
||||
VIEW = '/view/view_intervlan'
|
||||
bp = Blueprint(_PAGE, __name__)
|
||||
|
||||
_VALID_PROTOS_STR = ', '.join(sorted(validate.VALID_PROTOCOLS))
|
||||
|
||||
|
|
@ -77,14 +78,14 @@ def _entry_key(entry):
|
|||
return f'{entry["protocol"]}:{entry["src_ip_or_subnet"]}→{entry["dst_ip_or_subnet"]}{port}'
|
||||
|
||||
|
||||
@bp.route('/action/add_inter_vlan', methods=['POST'])
|
||||
@bp.route('/action/intervlan/addexception_add', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def add_inter_vlan():
|
||||
def addexception_add():
|
||||
entry, err = _parse_entry()
|
||||
if err:
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
cfg.setdefault('inter_vlan_exceptions', []).append(entry)
|
||||
|
|
@ -92,7 +93,7 @@ def add_inter_vlan():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
key = _entry_key(entry)
|
||||
flash(save_config_with_snapshot(
|
||||
|
|
@ -101,24 +102,24 @@ def add_inter_vlan():
|
|||
before=None, after=entry,
|
||||
description=f'Added inter-VLAN rule: {key}',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/toggle_inter_vlan', methods=['POST'])
|
||||
@bp.route('/action/intervlan/table_toggle', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def toggle_inter_vlan():
|
||||
def table_toggle():
|
||||
idx = _row_index()
|
||||
if idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
items = cfg.get('inter_vlan_exceptions', [])
|
||||
if idx < 0 or idx >= len(items):
|
||||
flash('Entry not found.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
old_enabled = items[idx].get('enabled', True)
|
||||
items[idx]['enabled'] = not old_enabled
|
||||
|
|
@ -126,7 +127,7 @@ def toggle_inter_vlan():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
key = _entry_key(items[idx])
|
||||
action = 'Enabled' if not old_enabled else 'Disabled'
|
||||
|
|
@ -136,28 +137,28 @@ def toggle_inter_vlan():
|
|||
before={'enabled': old_enabled}, after={'enabled': not old_enabled},
|
||||
description=f'{action} inter-VLAN rule: {key}',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/edit_inter_vlan', methods=['POST'])
|
||||
@bp.route('/action/intervlan/table_edit', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def edit_inter_vlan():
|
||||
def table_edit():
|
||||
idx = _row_index()
|
||||
if idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
entry, err = _parse_entry()
|
||||
if err:
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
items = cfg.get('inter_vlan_exceptions', [])
|
||||
if idx < 0 or idx >= len(items):
|
||||
flash('Entry not found.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
before = copy.deepcopy(items[idx])
|
||||
items[idx] = entry
|
||||
|
|
@ -166,7 +167,7 @@ def edit_inter_vlan():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
key = _entry_key(entry)
|
||||
flash(save_config_with_snapshot(
|
||||
|
|
@ -175,31 +176,31 @@ def edit_inter_vlan():
|
|||
before=before, after=copy.deepcopy(items[idx]),
|
||||
description=f'Edited inter-VLAN rule: {key}',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/delete_inter_vlan', methods=['POST'])
|
||||
@bp.route('/action/intervlan/table_delete', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def delete_inter_vlan():
|
||||
def table_delete():
|
||||
idx = _row_index()
|
||||
if idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
items = cfg.get('inter_vlan_exceptions', [])
|
||||
if idx < 0 or idx >= len(items):
|
||||
flash('Entry not found.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
removed = items.pop(idx)
|
||||
errors = validate.validate_config(cfg)
|
||||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
key = _entry_key(removed)
|
||||
flash(save_config_with_snapshot(
|
||||
|
|
@ -208,4 +209,4 @@ def delete_inter_vlan():
|
|||
before=removed, after=None,
|
||||
description=f'Deleted inter-VLAN rule: {key}',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"id": "view_intervlan",
|
||||
"client_requirement": "client_is_viewer+",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -53,7 +52,7 @@
|
|||
"row_actions": [
|
||||
{
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"action": "/action/edit_inter_vlan",
|
||||
"action": "/action/intervlan/table_edit",
|
||||
"method": "inline_edit",
|
||||
"text": "Edit",
|
||||
"class": "btn-ghost btn-sm",
|
||||
|
|
@ -88,7 +87,7 @@
|
|||
},
|
||||
{
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"action": "/action/delete_inter_vlan",
|
||||
"action": "/action/intervlan/table_delete",
|
||||
"method": "post",
|
||||
"text": "Delete",
|
||||
"class": "btn-danger btn-sm"
|
||||
|
|
@ -103,7 +102,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/add_inter_vlan",
|
||||
"action": "/action/intervlan/addexception_add",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -149,7 +148,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/add_inter_vlan",
|
||||
"action": "/action/intervlan/addexception_add",
|
||||
"method": "post",
|
||||
"text": "Add Exception"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from pathlib import Path
|
||||
import copy
|
||||
|
||||
from flask import Blueprint, request, redirect, flash
|
||||
|
|
@ -6,17 +7,19 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
|||
import sanitize
|
||||
import validation as validate
|
||||
|
||||
bp = Blueprint('mdns', __name__)
|
||||
_PAGE = Path(__file__).parent.name
|
||||
|
||||
bp = Blueprint(_PAGE, __name__)
|
||||
|
||||
|
||||
@bp.route('/action/apply_mdns', methods=['POST'])
|
||||
@bp.route('/action/mdns/settings_apply', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def apply_mdns():
|
||||
def settings_apply():
|
||||
mdns_enabled = 'mdns_enabled' in request.form
|
||||
|
||||
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||
return redirect('/view/view_mdns')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
mdns_reflect_vlans = sanitize.filterlist(
|
||||
|
|
@ -33,7 +36,7 @@ def apply_mdns():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect('/view/view_mdns')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
flash(save_config_with_snapshot(
|
||||
cfg,
|
||||
|
|
@ -41,4 +44,4 @@ def apply_mdns():
|
|||
before=before or None, after=copy.deepcopy(cfg['mdns_reflection']),
|
||||
description='Updated mDNS reflection settings',
|
||||
), 'success')
|
||||
return redirect('/view/view_mdns')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from pathlib import Path
|
||||
import copy
|
||||
import ipaddress
|
||||
|
||||
|
|
@ -7,9 +8,9 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
|||
import sanitize
|
||||
import validation as validate
|
||||
|
||||
bp = Blueprint('networklayout', __name__)
|
||||
_PAGE = Path(__file__).parent.name
|
||||
|
||||
VIEW = '/view/view_networklayout'
|
||||
bp = Blueprint(_PAGE, __name__)
|
||||
|
||||
_VLAN_FIELDS = ['name', 'vlan_id', 'is_vpn', 'subnet', 'subnet_mask', 'dnsmasq_log_queries',
|
||||
'radius_default', 'mdns_reflection', 'use_blocklists']
|
||||
|
|
@ -29,9 +30,9 @@ def _hash_ok():
|
|||
return True
|
||||
|
||||
|
||||
@bp.route('/action/networklayout_cardaddvlan_addvlan', methods=['POST'])
|
||||
@bp.route('/action/networklayout/addvlan_add', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def networklayout_cardaddvlan_addvlan():
|
||||
def addvlan_add():
|
||||
name = sanitize.name(request.form.get('name', ''))
|
||||
vlan_id = sanitize.vlan_id(request.form.get('vlan_id', ''))
|
||||
is_vpn = 'is_vpn' in request.form
|
||||
|
|
@ -47,29 +48,29 @@ def networklayout_cardaddvlan_addvlan():
|
|||
|
||||
if not name:
|
||||
flash('Name is required.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if vlan_id is None:
|
||||
flash('VLAN ID must be an integer between 1 and 4094.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not subnet:
|
||||
flash('Subnet IP is required.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if subnet_mask is None:
|
||||
flash('Invalid subnet prefix (must be 1-30).', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
vlans = cfg.setdefault('vlans', [])
|
||||
|
||||
if any(v.get('vlan_id') == vlan_id for v in vlans):
|
||||
flash(f'VLAN ID {vlan_id} is already in use.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if radius_default and any(v.get('radius_default') for v in vlans):
|
||||
flash('Only one VLAN can be the RADIUS default.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
entry = {
|
||||
'name': name,
|
||||
|
|
@ -91,7 +92,7 @@ def networklayout_cardaddvlan_addvlan():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
flash(save_config_with_snapshot(
|
||||
cfg,
|
||||
|
|
@ -99,16 +100,16 @@ def networklayout_cardaddvlan_addvlan():
|
|||
before=None, after={k: entry[k] for k in _VLAN_FIELDS if k in entry},
|
||||
description=f'Added VLAN: {name} ({subnet}/{subnet_mask})',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/networklayout_tablevlans_edit', methods=['POST'])
|
||||
@bp.route('/action/networklayout/vlans_edit', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def networklayout_tablevlans_edit():
|
||||
def vlans_edit():
|
||||
idx = _row_index()
|
||||
if idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
name = sanitize.name(request.form.get('name', ''))
|
||||
vlan_id = sanitize.vlan_id(request.form.get('vlan_id', ''))
|
||||
|
|
@ -126,7 +127,7 @@ def networklayout_tablevlans_edit():
|
|||
clean = sanitize.ip(raw_ip)
|
||||
if not clean:
|
||||
flash(f"'{raw_ip}' is not a valid IP address.", 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
identity_ips.append(clean)
|
||||
identity_descs = request.form.get('server_identity_descriptions', '').splitlines()
|
||||
identity_hostnames = request.form.get('server_identity_hostnames', '').splitlines()
|
||||
|
|
@ -136,27 +137,27 @@ def networklayout_tablevlans_edit():
|
|||
subnet_mask = sanitize.subnet_mask(subnet_mask_raw)
|
||||
if subnet_mask is None:
|
||||
flash('Invalid subnet prefix (must be 1-30).', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
else:
|
||||
subnet_mask = None
|
||||
|
||||
if not name:
|
||||
flash('Name is required.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if vlan_id is None:
|
||||
flash('VLAN ID must be an integer between 1 and 4094.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not subnet:
|
||||
flash('Subnet IP is required.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
vlans = cfg.get('vlans', [])
|
||||
if idx < 0 or idx >= len(vlans):
|
||||
flash('VLAN not found.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
existing = vlans[idx]
|
||||
is_vpn = existing.get('is_vpn', False)
|
||||
|
|
@ -167,20 +168,20 @@ def networklayout_tablevlans_edit():
|
|||
for _ip in identity_ips:
|
||||
if ipaddress.IPv4Address(_ip) not in _vlan_net:
|
||||
flash(f"Server identity IP '{_ip}' is not in the VLAN subnet ({subnet}/{final_mask}).", 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
current_id = existing.get('vlan_id')
|
||||
if current_id == 1 and vlan_id != 1:
|
||||
flash('VLAN 1 is the physical interface and cannot change its ID.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if vlan_id != current_id and any(v.get('vlan_id') == vlan_id for i, v in enumerate(vlans) if i != idx):
|
||||
flash(f'VLAN ID {vlan_id} is already in use.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if radius_default and any(i != idx and v.get('radius_default') for i, v in enumerate(vlans)):
|
||||
flash('Only one VLAN can be the RADIUS default.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
old_identities = existing.get('server_identities', [])
|
||||
new_identities = []
|
||||
|
|
@ -197,7 +198,7 @@ def networklayout_tablevlans_edit():
|
|||
clean_hostname = sanitize.hostname(hostname_raw)
|
||||
if clean_hostname is None:
|
||||
flash(f"'{hostname_raw}' is not a valid hostname.", 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
entry['hostname'] = clean_hostname
|
||||
else:
|
||||
entry.pop('hostname', None)
|
||||
|
|
@ -206,7 +207,7 @@ def networklayout_tablevlans_edit():
|
|||
gateway_raw = sanitize.ip(request.form.get('gateway', ''))
|
||||
if gateway_raw and gateway_raw not in identity_ips:
|
||||
flash(f"Gateway '{gateway_raw}' must match one of the server identity IPs.", 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
inferred_gw = (min(identity_ips, key=lambda ip: int(ip.split('.')[-1]))
|
||||
if identity_ips else '')
|
||||
new_stored_gw = gateway_raw if (gateway_raw and gateway_raw != inferred_gw) else ''
|
||||
|
|
@ -221,17 +222,17 @@ def networklayout_tablevlans_edit():
|
|||
_clean = sanitize.ip(_line)
|
||||
if not _clean:
|
||||
flash(f"'{_line}' is not a valid DNS server IP.", 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
dns_ips.append(_clean)
|
||||
if dns_override and not dns_ips:
|
||||
flash('At least one DNS server IP is required when override is enabled.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if dns_override and dns_ips:
|
||||
_vlan_net = ipaddress.IPv4Network(f'{subnet}/{final_mask}', strict=False)
|
||||
for _ip in dns_ips:
|
||||
if ipaddress.IPv4Address(_ip) not in _vlan_net:
|
||||
flash(f"DNS server '{_ip}' is not in the VLAN subnet ({subnet}/{final_mask}).", 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
new_stored_dns = dns_ips if dns_override else []
|
||||
_existing_dns = existing.get('dhcp_information', {}).get('explicit_overrides', {}).get('dns_server', [])
|
||||
existing_dns = _existing_dns if isinstance(_existing_dns, list) else ([_existing_dns] if _existing_dns else [])
|
||||
|
|
@ -245,17 +246,17 @@ def networklayout_tablevlans_edit():
|
|||
_clean = sanitize.ip(_line)
|
||||
if not _clean:
|
||||
flash(f"'{_line}' is not a valid NTP server IP.", 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
ntp_ips.append(_clean)
|
||||
if ntp_override and not ntp_ips:
|
||||
flash('At least one NTP server IP is required when override is enabled.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if ntp_override and ntp_ips:
|
||||
_vlan_net = ipaddress.IPv4Network(f'{subnet}/{final_mask}', strict=False)
|
||||
for _ip in ntp_ips:
|
||||
if ipaddress.IPv4Address(_ip) not in _vlan_net:
|
||||
flash(f"NTP server '{_ip}' is not in the VLAN subnet ({subnet}/{final_mask}).", 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
new_stored_ntp = ntp_ips if ntp_override else []
|
||||
_existing_ntp = existing.get('dhcp_information', {}).get('explicit_overrides', {}).get('ntp_server', [])
|
||||
existing_ntp = _existing_ntp if isinstance(_existing_ntp, list) else ([_existing_ntp] if _existing_ntp else [])
|
||||
|
|
@ -282,7 +283,7 @@ def networklayout_tablevlans_edit():
|
|||
and new_stored_dns == existing_dns
|
||||
and new_stored_ntp == existing_ntp):
|
||||
flash('No changes were made.', 'info')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
before = {k: existing.get(k) for k in _VLAN_FIELDS}
|
||||
existing.update({
|
||||
|
|
@ -316,7 +317,7 @@ def networklayout_tablevlans_edit():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
flash(save_config_with_snapshot(
|
||||
cfg,
|
||||
|
|
@ -324,31 +325,31 @@ def networklayout_tablevlans_edit():
|
|||
before=before, after={k: existing.get(k) for k in _VLAN_FIELDS},
|
||||
description=f'Edited VLAN: {name}',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/networklayout_tablevlans_delete', methods=['POST'])
|
||||
@bp.route('/action/networklayout/vlans_delete', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def networklayout_tablevlans_delete():
|
||||
def vlans_delete():
|
||||
idx = _row_index()
|
||||
if idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
vlans = cfg.get('vlans', [])
|
||||
if idx < 0 or idx >= len(vlans):
|
||||
flash('VLAN not found.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
removed = vlans.pop(idx)
|
||||
errors = validate.validate_config(cfg)
|
||||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
flash(save_config_with_snapshot(
|
||||
cfg,
|
||||
|
|
@ -357,4 +358,4 @@ def networklayout_tablevlans_delete():
|
|||
after=None,
|
||||
description=f'Deleted VLAN: {removed["name"]}',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"id": "view_networklayout",
|
||||
"client_requirement": "client_is_viewer+",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -95,7 +94,7 @@
|
|||
"row_actions": [
|
||||
{
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"action": "/action/networklayout_tablevlans_edit",
|
||||
"action": "/action/networklayout/vlans_edit",
|
||||
"method": "inline_edit",
|
||||
"text": "Edit",
|
||||
"class": "btn-ghost btn-sm",
|
||||
|
|
@ -155,7 +154,7 @@
|
|||
},
|
||||
{
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"action": "/action/networklayout_tablevlans_delete",
|
||||
"action": "/action/networklayout/vlans_delete",
|
||||
"method": "post",
|
||||
"text": "Delete",
|
||||
"class": "btn-danger btn-sm",
|
||||
|
|
@ -174,7 +173,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/networklayout_cardaddvlan_addvlan",
|
||||
"action": "/action/networklayout/addvlan_add",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -263,7 +262,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/networklayout_cardaddvlan_addvlan",
|
||||
"action": "/action/networklayout/addvlan_add",
|
||||
"method": "post",
|
||||
"text": "Add VLAN",
|
||||
"class": "add-vlan-btn",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"id": "view_overview",
|
||||
"client_requirement": "client_is_nothing+",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -22,7 +21,7 @@
|
|||
},
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/view/view_login",
|
||||
"action": "/accountlogin",
|
||||
"text": "Log In"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from pathlib import Path
|
||||
import copy
|
||||
import os
|
||||
|
||||
|
|
@ -7,9 +8,9 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
|||
import sanitize
|
||||
import validation as validate
|
||||
|
||||
bp = Blueprint('physicalinterfaces', __name__)
|
||||
_PAGE = Path(__file__).parent.name
|
||||
|
||||
_VIEW = '/view/view_physicalinterfaces'
|
||||
bp = Blueprint(_PAGE, __name__)
|
||||
|
||||
_EXCLUDE_PREFIXES = ('lo', 'wg', 'docker', 'br-', 'veth',
|
||||
'tun', 'tap', 'ppp', 'virbr',
|
||||
|
|
@ -31,29 +32,29 @@ def _valid_interface(name):
|
|||
return name in _get_system_interfaces()
|
||||
|
||||
|
||||
@bp.route('/action/physicalinterfaces_cardphysicalinterface_save', methods=['POST'])
|
||||
@bp.route('/action/physicalinterfaces/physicalinterface_save', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def physicalinterfaces_cardphysicalinterface_save():
|
||||
def physicalinterface_save():
|
||||
wan = sanitize.interface_name(request.form.get('wan_interface', ''))
|
||||
lan = sanitize.interface_name(request.form.get('lan_interface', ''))
|
||||
|
||||
if not wan or not lan:
|
||||
flash('Both WAN and LAN interfaces are required.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if wan == lan:
|
||||
flash('WAN and LAN interfaces must be different.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
available = _get_system_interfaces()
|
||||
for iface in (wan, lan):
|
||||
if available and iface not in available:
|
||||
flash(f"Interface '{iface}' does not exist on this system.", 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
before = copy.deepcopy(cfg.get('network_interfaces', {}))
|
||||
|
|
@ -64,22 +65,22 @@ def physicalinterfaces_cardphysicalinterface_save():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
flash(save_config_with_snapshot(
|
||||
cfg, path='network_interfaces', key='global', operation='edit',
|
||||
before=before, after=copy.deepcopy(cfg['network_interfaces']),
|
||||
description='Updated network interfaces',
|
||||
cmd='core apply',
|
||||
), 'success')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/physicalinterfaces_cardinterfaceconfiguration_apply', methods=['POST'])
|
||||
@bp.route('/action/physicalinterfaces/ifaceconfig_apply', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def physicalinterfaces_cardinterfaceconfiguration_apply():
|
||||
def ifaceconfig_apply():
|
||||
if not verify_config_hash(request.form.get('config_hash', '')):
|
||||
flash('Configuration was modified by another session. Please refresh and try again.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
iface = sanitize.interface_name(request.form.get('iface', ''))
|
||||
mtu = request.form.get('mtu', '').strip()
|
||||
|
|
@ -89,27 +90,27 @@ def physicalinterfaces_cardinterfaceconfiguration_apply():
|
|||
|
||||
if not iface:
|
||||
flash('No interface specified.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if not _valid_interface(iface):
|
||||
flash(f"Interface '{iface}' does not exist on this system.", 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
mtu_int = None
|
||||
if mtu:
|
||||
mtu_int = validate.int_range(mtu, 68, 9000)
|
||||
if mtu_int is None:
|
||||
flash('MTU must be an integer between 68 and 9000.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
mac_raw = request.form.get('mac', '').strip()
|
||||
if mac_raw and not mac:
|
||||
flash('MAC address must be in the format aa:bb:cc:dd:ee:ff.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if not mtu_int and not mac:
|
||||
flash('No changes specified.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
queued = False
|
||||
if mtu_int and str(mtu_int) != original_mtu:
|
||||
|
|
@ -121,7 +122,7 @@ def physicalinterfaces_cardinterfaceconfiguration_apply():
|
|||
|
||||
if not queued:
|
||||
flash('No changes detected.', 'info')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
flash(queued_msg(action_label='Changes queued'), 'success')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"id": "view_physicalinterfaces",
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -22,7 +21,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/physicalinterfaces_cardphysicalinterface_save",
|
||||
"action": "/action/physicalinterfaces/physicalinterface_save",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -46,7 +45,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/physicalinterfaces_cardphysicalinterface_save",
|
||||
"action": "/action/physicalinterfaces/physicalinterface_save",
|
||||
"method": "post",
|
||||
"text": "Save"
|
||||
},
|
||||
|
|
@ -69,7 +68,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/physicalinterfaces_cardinterfaceconfiguration_apply",
|
||||
"action": "/action/physicalinterfaces/ifaceconfig_apply",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -142,7 +141,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/physicalinterfaces_cardinterfaceconfiguration_apply",
|
||||
"action": "/action/physicalinterfaces/ifaceconfig_apply",
|
||||
"method": "post",
|
||||
"text": "Apply"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from pathlib import Path
|
||||
import copy
|
||||
|
||||
from flask import Blueprint, request, redirect, flash
|
||||
|
|
@ -6,9 +7,9 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
|||
import sanitize
|
||||
import validation as validate
|
||||
|
||||
bp = Blueprint('portforwarding', __name__)
|
||||
_PAGE = Path(__file__).parent.name
|
||||
|
||||
VIEW = '/view/view_portforwarding'
|
||||
bp = Blueprint(_PAGE, __name__)
|
||||
|
||||
_VALID_PROTOS_STR = ', '.join(sorted(validate.VALID_PROTOCOLS))
|
||||
|
||||
|
|
@ -73,14 +74,14 @@ def _parse_entry():
|
|||
}, None
|
||||
|
||||
|
||||
@bp.route('/action/add_port_forward', methods=['POST'])
|
||||
@bp.route('/action/portforwarding/addrule_add', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def add_port_forward():
|
||||
def addrule_add():
|
||||
entry, err = _parse_entry()
|
||||
if err:
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
cfg.setdefault('port_forwarding', []).append(entry)
|
||||
|
|
@ -88,7 +89,7 @@ def add_port_forward():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
key = f'{entry["protocol"]}:{entry["dest_port"]}'
|
||||
flash(save_config_with_snapshot(
|
||||
|
|
@ -97,24 +98,24 @@ def add_port_forward():
|
|||
before=None, after=entry,
|
||||
description=f'Added port forward: {key} → {entry["nat_ip"]}:{entry["nat_port"]}',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/toggle_port_forward', methods=['POST'])
|
||||
@bp.route('/action/portforwarding/rules_toggle', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def toggle_port_forward():
|
||||
def rules_toggle():
|
||||
idx = _row_index()
|
||||
if idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
items = cfg.get('port_forwarding', [])
|
||||
if idx < 0 or idx >= len(items):
|
||||
flash('Entry not found.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
old_enabled = items[idx].get('enabled', True)
|
||||
items[idx]['enabled'] = not old_enabled
|
||||
|
|
@ -122,7 +123,7 @@ def toggle_port_forward():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
key = f'{items[idx]["protocol"]}:{items[idx]["dest_port"]}'
|
||||
action = 'Enabled' if not old_enabled else 'Disabled'
|
||||
|
|
@ -132,28 +133,28 @@ def toggle_port_forward():
|
|||
before={'enabled': old_enabled}, after={'enabled': not old_enabled},
|
||||
description=f'{action} port forward: {key}',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/edit_port_forward', methods=['POST'])
|
||||
@bp.route('/action/portforwarding/rules_edit', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def edit_port_forward():
|
||||
def rules_edit():
|
||||
idx = _row_index()
|
||||
if idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
entry, err = _parse_entry()
|
||||
if err:
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
items = cfg.get('port_forwarding', [])
|
||||
if idx < 0 or idx >= len(items):
|
||||
flash('Entry not found.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
before = copy.deepcopy(items[idx])
|
||||
items[idx] = entry
|
||||
|
|
@ -162,7 +163,7 @@ def edit_port_forward():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
key = f'{entry["protocol"]}:{entry["dest_port"]}'
|
||||
flash(save_config_with_snapshot(
|
||||
|
|
@ -171,31 +172,31 @@ def edit_port_forward():
|
|||
before=before, after=copy.deepcopy(items[idx]),
|
||||
description=f'Edited port forward: {key} → {entry["nat_ip"]}:{entry["nat_port"]}',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/delete_port_forward', methods=['POST'])
|
||||
@bp.route('/action/portforwarding/rules_delete', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def delete_port_forward():
|
||||
def rules_delete():
|
||||
idx = _row_index()
|
||||
if idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
items = cfg.get('port_forwarding', [])
|
||||
if idx < 0 or idx >= len(items):
|
||||
flash('Entry not found.', 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
removed = items.pop(idx)
|
||||
errors = validate.validate_config(cfg)
|
||||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
key = f'{removed["protocol"]}:{removed["dest_port"]}'
|
||||
flash(save_config_with_snapshot(
|
||||
|
|
@ -204,4 +205,4 @@ def delete_port_forward():
|
|||
before=removed, after=None,
|
||||
description=f'Deleted port forward: {key}',
|
||||
), 'success')
|
||||
return redirect(VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"id": "view_portforwarding",
|
||||
"client_requirement": "client_is_viewer+",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -53,7 +52,7 @@
|
|||
"row_actions": [
|
||||
{
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"action": "/action/edit_port_forward",
|
||||
"action": "/action/portforwarding/rules_edit",
|
||||
"method": "inline_edit",
|
||||
"text": "Edit",
|
||||
"class": "btn-ghost btn-sm",
|
||||
|
|
@ -88,7 +87,7 @@
|
|||
},
|
||||
{
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"action": "/action/delete_port_forward",
|
||||
"action": "/action/portforwarding/rules_delete",
|
||||
"method": "post",
|
||||
"text": "Delete",
|
||||
"class": "btn-danger btn-sm"
|
||||
|
|
@ -103,7 +102,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/add_port_forward",
|
||||
"action": "/action/portforwarding/addrule_add",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -149,7 +148,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/add_port_forward",
|
||||
"action": "/action/portforwarding/addrule_add",
|
||||
"method": "post",
|
||||
"text": "Add Rule"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
from pathlib import Path
|
||||
from flask import Blueprint, request, session, redirect, flash
|
||||
import json, bcrypt
|
||||
from auth import require_level
|
||||
from config_utils import ACCOUNTS_FILE
|
||||
import sanitize
|
||||
|
||||
bp = Blueprint('preferences', __name__)
|
||||
_PAGE = Path(__file__).parent.name
|
||||
|
||||
bp = Blueprint(_PAGE, __name__)
|
||||
|
||||
|
||||
|
||||
|
|
@ -20,14 +23,14 @@ def _save_accounts(data):
|
|||
json.dump(data, f, indent=2)
|
||||
|
||||
|
||||
@bp.route('/action/save_preferences', methods=['POST'])
|
||||
@bp.route('/action/preferences/accountdetails_save', methods=['POST'])
|
||||
@require_level('viewer')
|
||||
def save_preferences():
|
||||
def accountdetails_save():
|
||||
tz = sanitize.timezone(request.form.get('timezone', '').strip())
|
||||
|
||||
if not tz:
|
||||
flash('Timezone is required.', 'error')
|
||||
return redirect('/view/view_preferences')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
email = session.get('email_address', '').lower()
|
||||
data = _load_accounts()
|
||||
|
|
@ -36,7 +39,7 @@ def save_preferences():
|
|||
|
||||
if account is None:
|
||||
flash('Account not found. Please log in again.', 'error')
|
||||
return redirect('/view/view_login')
|
||||
return redirect('/accountlogin')
|
||||
|
||||
account['timezone'] = tz
|
||||
_save_accounts(data)
|
||||
|
|
@ -44,27 +47,27 @@ def save_preferences():
|
|||
session['timezone'] = tz
|
||||
|
||||
flash('Preferences saved.', 'success')
|
||||
return redirect('/view/view_preferences')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/change_password', methods=['POST'])
|
||||
@bp.route('/action/preferences/changepassword_save', methods=['POST'])
|
||||
@require_level('viewer')
|
||||
def change_password():
|
||||
def changepassword_save():
|
||||
current_password = request.form.get('current_password', '')
|
||||
new_password = request.form.get('new_password', '')
|
||||
confirm_password = request.form.get('confirm_password', '')
|
||||
|
||||
if not current_password or not new_password or not confirm_password:
|
||||
flash('All fields are required.', 'error')
|
||||
return redirect('/view/view_preferences')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if new_password != confirm_password:
|
||||
flash('New passwords do not match.', 'error')
|
||||
return redirect('/view/view_preferences')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if len(new_password) < 8:
|
||||
flash('New password must be at least 8 characters.', 'error')
|
||||
return redirect('/view/view_preferences')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
email = session.get('email_address', '').lower()
|
||||
data = _load_accounts()
|
||||
|
|
@ -73,12 +76,12 @@ def change_password():
|
|||
|
||||
if account is None:
|
||||
flash('Account not found. Please log in again.', 'error')
|
||||
return redirect('/view/view_login')
|
||||
return redirect('/accountlogin')
|
||||
|
||||
stored_hash = account.get('hashed_password', '').encode('utf-8')
|
||||
if not bcrypt.checkpw(current_password.encode('utf-8'), stored_hash):
|
||||
flash('Current password is incorrect.', 'error')
|
||||
return redirect('/view/view_preferences')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
salt = bcrypt.gensalt()
|
||||
hashed = bcrypt.hashpw(new_password.encode('utf-8'), salt)
|
||||
|
|
@ -88,4 +91,4 @@ def change_password():
|
|||
_save_accounts(data)
|
||||
|
||||
flash('Password changed successfully.', 'success')
|
||||
return redirect('/view/view_preferences')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"id": "view_preferences",
|
||||
"client_requirement": "client_is_viewer+",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -21,7 +20,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/save_preferences",
|
||||
"action": "/action/preferences/accountdetails_save",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -46,7 +45,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/save_preferences",
|
||||
"action": "/action/preferences/accountdetails_save",
|
||||
"method": "post",
|
||||
"text": "Save Preferences"
|
||||
}
|
||||
|
|
@ -62,7 +61,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/change_password",
|
||||
"action": "/action/preferences/changepassword_save",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -91,7 +90,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/change_password",
|
||||
"action": "/action/preferences/changepassword_save",
|
||||
"method": "post",
|
||||
"text": "Change Password"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
from pathlib import Path
|
||||
import base64
|
||||
import copy
|
||||
import ipaddress
|
||||
|
|
@ -9,9 +10,10 @@ from config_utils import load_config, save_config_with_snapshot, verify_config_h
|
|||
import sanitize
|
||||
import validation as validate
|
||||
|
||||
bp = Blueprint('vpn', __name__)
|
||||
_PAGE = Path(__file__).parent.name
|
||||
|
||||
bp = Blueprint(_PAGE, __name__)
|
||||
|
||||
_VIEW = '/view/view_vpn'
|
||||
_MTU_MIN = 576
|
||||
_MTU_MAX = 9000
|
||||
|
||||
|
|
@ -121,7 +123,7 @@ def _conf_response(vlan, peer_name, peer_ip, private_key):
|
|||
if not server_pub:
|
||||
flash('Peer saved. Run sudo python3 ~/routlin/core.py --apply to generate the server '
|
||||
'public key, then regenerate this peer to download the client config.', 'warning')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
conf = _build_client_conf(vlan, peer_name, peer_ip, private_key, server_pub)
|
||||
safe = re.sub(r'[^A-Za-z0-9_\-]', '_', peer_name)
|
||||
resp = make_response(conf)
|
||||
|
|
@ -130,9 +132,9 @@ def _conf_response(vlan, peer_name, peer_ip, private_key):
|
|||
return resp
|
||||
|
||||
|
||||
@bp.route('/action/apply_vpn', methods=['POST'])
|
||||
@bp.route('/action/vpn/wireguard_apply', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def apply_vpn():
|
||||
def wireguard_apply():
|
||||
listen_port_raw = request.form.get('vpn_listen_port', '').strip()
|
||||
server_endpoint = validate.domainname(request.form.get('vpn_server_endpoint', ''))
|
||||
domain = validate.domainname(request.form.get('vpn_domain', ''))
|
||||
|
|
@ -141,39 +143,39 @@ def apply_vpn():
|
|||
|
||||
if not listen_port_raw:
|
||||
flash('Listen port is required.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
listen_port = validate.int_range(listen_port_raw, 1, 65535)
|
||||
if listen_port is None:
|
||||
flash(f'"{listen_port_raw}" is not a valid port number (1-65535).', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
dns_server = ''
|
||||
if dns_raw:
|
||||
dns_server = validate.ip(dns_raw)
|
||||
if not dns_server:
|
||||
flash(f'"{dns_raw}" is not a valid IP address for DNS server.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
mtu = None
|
||||
if mtu_raw:
|
||||
mtu = validate.int_range(mtu_raw, _MTU_MIN, _MTU_MAX)
|
||||
if mtu is None:
|
||||
flash(f'"{mtu_raw}" is not a valid MTU (must be {_MTU_MIN}-{_MTU_MAX}).', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
if not _hash_ok():
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
vpn_vlan = _wg_vlan(cfg)
|
||||
if vpn_vlan is None:
|
||||
flash('No WireGuard VLAN found in configuration.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
for v in cfg.get('vlans', []):
|
||||
if v.get('is_vpn') and v is not vpn_vlan and v.get('vpn_information', {}).get('listen_port') == listen_port:
|
||||
flash(f'Listen port {listen_port} is already used by another VPN VLAN.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
before_info = copy.deepcopy(vpn_vlan.get('vpn_information', {}))
|
||||
info = vpn_vlan.setdefault('vpn_information', {})
|
||||
|
|
@ -195,7 +197,7 @@ def apply_vpn():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
vlan_name = vpn_vlan['name']
|
||||
flash(save_config_with_snapshot(
|
||||
|
|
@ -204,12 +206,12 @@ def apply_vpn():
|
|||
before=before_info or None, after=copy.deepcopy(info),
|
||||
description=f'Updated VPN configuration for {vlan_name}',
|
||||
), 'success')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/add_vpn_peer', methods=['POST'])
|
||||
@bp.route('/action/vpn/addpeer_add', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def add_vpn_peer():
|
||||
def addpeer_add():
|
||||
peer_name = sanitize.name(request.form.get('peer_name', ''))
|
||||
peer_vlan_nm = request.form.get('peer_vlan', '').strip()
|
||||
peer_ip_raw = request.form.get('peer_ip', '').strip()
|
||||
|
|
@ -218,41 +220,41 @@ def add_vpn_peer():
|
|||
|
||||
if not peer_name:
|
||||
flash('Peer name is required.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not peer_vlan_nm:
|
||||
flash('Assigned VLAN is required.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
peer_ip = validate.ip(peer_ip_raw)
|
||||
if not peer_ip:
|
||||
flash(f'"{peer_ip_raw}" is not a valid IP address.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
vpn_vlan = _wg_vlan_by_name(cfg, peer_vlan_nm)
|
||||
if vpn_vlan is None:
|
||||
flash(f'VPN VLAN "{peer_vlan_nm}" not found.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
try:
|
||||
network = ipaddress.IPv4Network(f"{vpn_vlan['subnet']}/{vpn_vlan['subnet_mask']}", strict=False)
|
||||
if ipaddress.IPv4Address(peer_ip) not in network:
|
||||
flash(f'{peer_ip} is not within the subnet {vpn_vlan["subnet"]}/{vpn_vlan["subnet_mask"]} of {peer_vlan_nm}.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
peers = vpn_vlan.setdefault('peers', [])
|
||||
if any(p.get('name') == peer_name for p in peers):
|
||||
flash(f'A peer named "{peer_name}" already exists.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
for v in cfg.get('vlans', []):
|
||||
if not v.get('is_vpn'):
|
||||
continue
|
||||
if any(p.get('ip') == peer_ip for p in v.get('peers', [])):
|
||||
flash(f'IP address {peer_ip} is already assigned to another peer.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
private_key, public_key = _generate_wg_keypair()
|
||||
entry = {
|
||||
|
|
@ -267,7 +269,7 @@ def add_vpn_peer():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
save_config_with_snapshot(
|
||||
cfg,
|
||||
|
|
@ -279,13 +281,13 @@ def add_vpn_peer():
|
|||
return _conf_response(vpn_vlan, peer_name, peer_ip, private_key)
|
||||
|
||||
|
||||
@bp.route('/action/edit_vpn_peer', methods=['POST'])
|
||||
@bp.route('/action/vpn/peers_edit', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def edit_vpn_peer():
|
||||
def peers_edit():
|
||||
flat_idx = _row_index()
|
||||
if flat_idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
peer_name = sanitize.name(request.form.get('name', ''))
|
||||
split_tunnel = request.form.get('split_tunnel') in ('true', '1', 'on', 'yes')
|
||||
|
|
@ -293,20 +295,20 @@ def edit_vpn_peer():
|
|||
|
||||
if not peer_name:
|
||||
flash('Peer name is required.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
vlan, peer_idx = _find_peer_by_flat_idx(cfg, flat_idx)
|
||||
if vlan is None:
|
||||
flash('Peer not found.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
peers = vlan.get('peers', [])
|
||||
if any(j != peer_idx and p.get('name') == peer_name for j, p in enumerate(peers)):
|
||||
flash(f'A peer named "{peer_name}" already exists.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
before = copy.deepcopy({k: peers[peer_idx].get(k) for k in ('name', 'split_tunnel', 'enabled')})
|
||||
peers[peer_idx].update({'name': peer_name, 'split_tunnel': split_tunnel, 'enabled': enabled})
|
||||
|
|
@ -314,7 +316,7 @@ def edit_vpn_peer():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
vlan_name = vlan['name']
|
||||
flash(save_config_with_snapshot(
|
||||
|
|
@ -323,24 +325,24 @@ def edit_vpn_peer():
|
|||
before=before, after={'name': peer_name, 'split_tunnel': split_tunnel, 'enabled': enabled},
|
||||
description=f'Edited VPN peer: {peer_name}',
|
||||
), 'success')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/toggle_vpn_peer', methods=['POST'])
|
||||
@bp.route('/action/vpn/peers_toggle', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def toggle_vpn_peer():
|
||||
def peers_toggle():
|
||||
flat_idx = _row_index()
|
||||
if flat_idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
vlan, peer_idx = _find_peer_by_flat_idx(cfg, flat_idx)
|
||||
if vlan is None:
|
||||
flash('Peer not found.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
peers = vlan.get('peers', [])
|
||||
old_enabled = peers[peer_idx].get('enabled', True)
|
||||
|
|
@ -349,7 +351,7 @@ def toggle_vpn_peer():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
peer_name = peers[peer_idx]['name']
|
||||
vlan_name = vlan['name']
|
||||
|
|
@ -360,24 +362,24 @@ def toggle_vpn_peer():
|
|||
before={'enabled': old_enabled}, after={'enabled': not old_enabled},
|
||||
description=f'{action} VPN peer: {peer_name}',
|
||||
), 'success')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/delete_vpn_peer', methods=['POST'])
|
||||
@bp.route('/action/vpn/peers_delete', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def delete_vpn_peer():
|
||||
def peers_delete():
|
||||
flat_idx = _row_index()
|
||||
if flat_idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
vlan, peer_idx = _find_peer_by_flat_idx(cfg, flat_idx)
|
||||
if vlan is None:
|
||||
flash('Peer not found.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
peers = vlan.get('peers', [])
|
||||
removed = peers.pop(peer_idx)
|
||||
|
|
@ -385,7 +387,7 @@ def delete_vpn_peer():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
vlan_name = vlan['name']
|
||||
flash(save_config_with_snapshot(
|
||||
|
|
@ -395,24 +397,24 @@ def delete_vpn_peer():
|
|||
after=None,
|
||||
description=f'Deleted VPN peer: {removed["name"]}',
|
||||
), 'success')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/regenerate_vpn_peer', methods=['POST'])
|
||||
@bp.route('/action/vpn/peers_regenerate', methods=['POST'])
|
||||
@require_level('administrator')
|
||||
def regenerate_vpn_peer():
|
||||
def peers_regenerate():
|
||||
flat_idx = _row_index()
|
||||
if flat_idx is None:
|
||||
flash('Invalid request.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
if not _hash_ok():
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
cfg = load_config()
|
||||
vlan, peer_idx = _find_peer_by_flat_idx(cfg, flat_idx)
|
||||
if vlan is None:
|
||||
flash('Peer not found.', 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
private_key, public_key = _generate_wg_keypair()
|
||||
peer = vlan['peers'][peer_idx]
|
||||
|
|
@ -422,7 +424,7 @@ def regenerate_vpn_peer():
|
|||
if errors:
|
||||
for msg in errors:
|
||||
flash(msg, 'error')
|
||||
return redirect(_VIEW)
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
vlan_name = vlan['name']
|
||||
save_config_with_snapshot(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"id": "view_vpn",
|
||||
"client_requirement": "client_is_viewer+",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -89,7 +88,7 @@
|
|||
"row_actions": [
|
||||
{
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"action": "/action/edit_vpn_peer",
|
||||
"action": "/action/vpn/peers_edit",
|
||||
"method": "inline_edit",
|
||||
"text": "Edit",
|
||||
"class": "btn-ghost btn-sm",
|
||||
|
|
@ -113,14 +112,14 @@
|
|||
},
|
||||
{
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"action": "/action/regenerate_vpn_peer",
|
||||
"action": "/action/vpn/peers_regenerate",
|
||||
"method": "post",
|
||||
"text": "Regen Conf",
|
||||
"class": "btn-ghost btn-sm"
|
||||
},
|
||||
{
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"action": "/action/delete_vpn_peer",
|
||||
"action": "/action/vpn/peers_delete",
|
||||
"method": "post",
|
||||
"text": "Delete",
|
||||
"class": "btn-danger btn-sm"
|
||||
|
|
@ -134,7 +133,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/add_vpn_peer",
|
||||
"action": "/action/vpn/addpeer_add",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -181,7 +180,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/add_vpn_peer",
|
||||
"action": "/action/vpn/addpeer_add",
|
||||
"method": "post",
|
||||
"text": "Add Peer & Download Conf"
|
||||
},
|
||||
|
|
@ -202,7 +201,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/apply_vpn",
|
||||
"action": "/action/vpn/wireguard_apply",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
|
|
@ -259,7 +258,7 @@
|
|||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"action": "/action/apply_vpn",
|
||||
"action": "/action/vpn/wireguard_apply",
|
||||
"method": "post",
|
||||
"text": "Save"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -66,23 +66,6 @@ def _load_icon(name):
|
|||
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 ======================================================
|
||||
|
|
@ -2040,7 +2023,7 @@ def render_layout(view_id, content_html, tokens):
|
|||
else 'Fix pending. The processing service is not running.')
|
||||
else:
|
||||
fix_suffix = ('Fix pending. Click <strong>Apply Now</strong> below to fix.'
|
||||
if view_id == 'view_actions' else
|
||||
if view_id == 'actions' else
|
||||
'Fix pending. Visit the <strong>Actions</strong> page ASAP to apply fix.')
|
||||
for sev, items in grouped.items():
|
||||
if not items:
|
||||
|
|
@ -2062,7 +2045,7 @@ def render_layout(view_id, content_html, tokens):
|
|||
pass
|
||||
|
||||
pending_bar = ''
|
||||
if has_pending_alert and not problem_bars and view_id != 'view_actions':
|
||||
if has_pending_alert and not problem_bars and view_id != 'actions':
|
||||
pending_bar = (
|
||||
'<div class="info-bar info-bar-warning">'
|
||||
'<span>You have actions pending. Please visit the <strong>Actions</strong> page.</span>'
|
||||
|
|
@ -2114,7 +2097,7 @@ def _render_nav_item(item, active_view, level, in_dropdown=False, inherited_req=
|
|||
map_to = item.get('map_to', '')
|
||||
action = item.get('action', '')
|
||||
is_active = ' active' if map_to and map_to == active_view else ''
|
||||
pending = ' nav-item-pending' if pending_alert and map_to == 'view_actions' else ''
|
||||
pending = ' nav-item-pending' if pending_alert and map_to == 'actions' else ''
|
||||
cls = f'dropdown-item{is_active}' if in_dropdown else f'nav-item{is_active}{pending}'
|
||||
if action:
|
||||
return (
|
||||
|
|
@ -2122,7 +2105,7 @@ def _render_nav_item(item, active_view, level, in_dropdown=False, inherited_req=
|
|||
f'<button type="submit" class="{cls}">{label}</button></form>'
|
||||
)
|
||||
if map_to:
|
||||
return f'<a href="/view/{e(map_to)}" class="{cls}">{label}</a>'
|
||||
return f'<a href="/{e(map_to)}" class="{cls}">{label}</a>'
|
||||
return f'<span class="{cls}">{label}</span>'
|
||||
|
||||
if t == 'nav_menu':
|
||||
|
|
@ -2161,15 +2144,14 @@ def _inline_js():
|
|||
|
||||
@bp.route('/')
|
||||
def index():
|
||||
return _serve_view('view_overview')
|
||||
return _serve_view('overview')
|
||||
|
||||
@bp.route('/view/<view_id>')
|
||||
def view(view_id):
|
||||
return _serve_view(view_id)
|
||||
@bp.route('/<page_name>')
|
||||
def view(page_name):
|
||||
return _serve_view(page_name)
|
||||
|
||||
def _serve_view(view_id):
|
||||
page_name = _VIEW_MAP.get(view_id)
|
||||
view_def = _load_json(_os.path.join(_PAGES_DIR, page_name, 'content.json')) if page_name else None
|
||||
def _serve_view(page_name):
|
||||
view_def = _load_json(_os.path.join(_PAGES_DIR, page_name, 'content.json'))
|
||||
|
||||
if view_def is None:
|
||||
from flask import abort
|
||||
|
|
@ -2178,7 +2160,7 @@ def _serve_view(view_id):
|
|||
view_req = view_def.get('client_requirement')
|
||||
level = _client_level()
|
||||
if not _passes(view_req, level):
|
||||
return redirect('/view/view_overview' if level > 0 else '/view/view_login')
|
||||
return redirect('/overview' if level > 0 else '/accountlogin')
|
||||
|
||||
tokens = collect_tokens()
|
||||
|
||||
|
|
@ -2189,4 +2171,4 @@ def _serve_view(view_id):
|
|||
flash_html += f'<div class="info-bar info-bar-{variant} info-bar-flash"><span>{msg_html}</span></div>'
|
||||
|
||||
content_html = flash_html + render_items(view_def.get('items', []), tokens, view_req)
|
||||
return render_layout(view_id, content_html, tokens)
|
||||
return render_layout(page_name, content_html, tokens)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue