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