Development
This commit is contained in:
parent
99447b4987
commit
27f2356cd1
10 changed files with 241 additions and 6 deletions
|
|
@ -25,6 +25,7 @@ from pages.accountmanage.action import bp as accountmanage_bp
|
|||
from pages.mdns.action import bp as mdns_bp
|
||||
from pages.radius.action import bp as radius_bp
|
||||
from pages.clientcredentials.action import bp as clientcredentials_bp
|
||||
from pages.captiveportal.action import bp as captiveportal_bp
|
||||
from action_accountlogout import bp as accountlogout_bp
|
||||
from api_apply_health import bp as api_apply_health_bp
|
||||
|
||||
|
|
@ -138,6 +139,7 @@ app.register_blueprint(accountlogout_bp)
|
|||
app.register_blueprint(mdns_bp)
|
||||
app.register_blueprint(radius_bp)
|
||||
app.register_blueprint(clientcredentials_bp)
|
||||
app.register_blueprint(captiveportal_bp)
|
||||
app.register_blueprint(api_apply_health_bp)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@
|
|||
{ "type": "nav_item", "label": "VPN", "map_to": "vpn" },
|
||||
{ "type": "nav_item", "label": "Banned IPs", "map_to": "bannedips", "client_requirement": "client_is_administrator+" },
|
||||
{ "type": "nav_item", "label": "RADIUS", "map_to": "radius", "client_requirement": "client_is_administrator+" },
|
||||
{ "type": "nav_item", "label": "Client Credentials", "map_to": "clientcredentials", "client_requirement": "client_is_administrator+" }
|
||||
{ "type": "nav_item", "label": "Client Credentials", "map_to": "clientcredentials", "client_requirement": "client_is_administrator+" },
|
||||
{ "type": "nav_item", "label": "Captive Portal", "map_to": "captiveportal", "client_requirement": "client_is_administrator+" }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
|||
0
docker/routlin-dash/app/pages/captiveportal/__init__.py
Normal file
0
docker/routlin-dash/app/pages/captiveportal/__init__.py
Normal file
53
docker/routlin-dash/app/pages/captiveportal/action.py
Normal file
53
docker/routlin-dash/app/pages/captiveportal/action.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import copy
|
||||
|
||||
from flask import Blueprint, request, redirect, flash
|
||||
import auth
|
||||
import config_utils
|
||||
import sanitize
|
||||
|
||||
_PAGE = 'captiveportal'
|
||||
|
||||
bp = Blueprint(_PAGE, __name__)
|
||||
|
||||
|
||||
@bp.route('/action/captiveportal/options_save', methods=['POST'])
|
||||
@auth.require_level('administrator')
|
||||
def options_save():
|
||||
cfg = config_utils.load_config()
|
||||
before = copy.deepcopy(cfg.get('captive_portal', {}))
|
||||
|
||||
try:
|
||||
http_port = int(request.form.get('http_port', '8081'))
|
||||
if not (1024 <= http_port <= 65535):
|
||||
raise ValueError
|
||||
except (ValueError, TypeError):
|
||||
flash('HTTP port must be between 1024 and 65535.', 'error')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
https_domain = sanitize.description(request.form.get('https_domain', ''))
|
||||
|
||||
after = {**before, 'http_port': http_port, 'https_domain': https_domain}
|
||||
cfg.setdefault('captive_portal', {}).update(after)
|
||||
changes = config_utils.diff_fields(before, after)
|
||||
flash(config_utils.record_group(
|
||||
cfg, 'captive_portal', 'setting', 'captive_portal', changes, 'core apply'
|
||||
), 'success')
|
||||
return redirect(f'/{_PAGE}')
|
||||
|
||||
|
||||
@bp.route('/action/captiveportal/splash_save', methods=['POST'])
|
||||
@auth.require_level('administrator')
|
||||
def splash_save():
|
||||
cfg = config_utils.load_config()
|
||||
before = copy.deepcopy(cfg.get('captive_portal', {}))
|
||||
|
||||
splash_text = sanitize.description(request.form.get('splash_text', ''))
|
||||
terms = [t.strip() for t in request.form.getlist('terms') if t.strip()]
|
||||
|
||||
after = {**before, 'splash_text': splash_text, 'terms': terms}
|
||||
cfg.setdefault('captive_portal', {}).update(after)
|
||||
changes = config_utils.diff_fields(before, after)
|
||||
flash(config_utils.record_group(
|
||||
cfg, 'captive_portal', 'setting', 'captive_portal', changes, 'core apply'
|
||||
), 'success')
|
||||
return redirect(f'/{_PAGE}')
|
||||
112
docker/routlin-dash/app/pages/captiveportal/content.json
Normal file
112
docker/routlin-dash/app/pages/captiveportal/content.json
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
{
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"items": [
|
||||
{
|
||||
"type": "header_page_title",
|
||||
"items": [
|
||||
{
|
||||
"type": "h1",
|
||||
"text": "Captive Portal"
|
||||
},
|
||||
{
|
||||
"type": "p",
|
||||
"text": "Redirect unauthenticated guests on captive portal VLANs to a login page before allowing internet access."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "info_bar",
|
||||
"variant": "info",
|
||||
"text": "%CAPTIVE_STATUS_TEXT%"
|
||||
},
|
||||
{
|
||||
"type": "card",
|
||||
"label": "Options",
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/captiveportal/options_save",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
"type": "field",
|
||||
"label": "HTTP Port",
|
||||
"name": "http_port",
|
||||
"input_type": "number",
|
||||
"value": "%CAPTIVE_HTTP_PORT%",
|
||||
"min": 1024,
|
||||
"max": 65535,
|
||||
"hint": "Port the captive portal service listens on. nftables redirects unauthenticated HTTP traffic from captive portal VLANs to this port."
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"label": "HTTPS Domain",
|
||||
"name": "https_domain",
|
||||
"input_type": "text",
|
||||
"value": "%CAPTIVE_HTTPS_DOMAIN%",
|
||||
"hint": "Domain with an existing Caddy + Let's Encrypt certificate. The portal login page is served at https://<domain>/. Leave blank for HTTP-only."
|
||||
},
|
||||
{
|
||||
"type": "button_row",
|
||||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"text": "Save"
|
||||
},
|
||||
{
|
||||
"type": "button_cancel",
|
||||
"text": "Cancel"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "card",
|
||||
"label": "Splash Screen",
|
||||
"client_requirement": "client_is_administrator+",
|
||||
"items": [
|
||||
{
|
||||
"type": "form",
|
||||
"action": "/action/captiveportal/splash_save",
|
||||
"method": "post",
|
||||
"items": [
|
||||
{
|
||||
"type": "field",
|
||||
"label": "Welcome Text",
|
||||
"name": "splash_text",
|
||||
"input_type": "text",
|
||||
"value": "%CAPTIVE_SPLASH_TEXT%",
|
||||
"hint": "Welcome message shown at the top of the login page."
|
||||
},
|
||||
{
|
||||
"type": "editable_list",
|
||||
"label": "Terms",
|
||||
"name": "terms",
|
||||
"items": "%CAPTIVE_TERMS%",
|
||||
"add_label": "Add Term",
|
||||
"item_placeholder": "e.g. I agree to the acceptable use policy.",
|
||||
"hint": "Each term renders as a required checkbox the user must tick before submitting credentials."
|
||||
},
|
||||
{
|
||||
"type": "button_row",
|
||||
"items": [
|
||||
{
|
||||
"type": "button_primary",
|
||||
"text": "Save"
|
||||
},
|
||||
{
|
||||
"type": "button_cancel",
|
||||
"text": "Cancel"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
26
docker/routlin-dash/app/pages/captiveportal/view.py
Normal file
26
docker/routlin-dash/app/pages/captiveportal/view.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import json
|
||||
|
||||
import config_utils
|
||||
import factory
|
||||
|
||||
|
||||
def collect_tokens(cfg):
|
||||
tokens = config_utils.collect_layout_tokens(cfg)
|
||||
cp = cfg.get('captive_portal', {})
|
||||
captive_vlans = [v for v in cfg.get('vlans', []) if v.get('restricted_vlan') == 'c']
|
||||
|
||||
if captive_vlans:
|
||||
names = ', '.join(v['name'] for v in captive_vlans)
|
||||
tokens['CAPTIVE_STATUS_TEXT'] = f"Captive portal active on: {names}."
|
||||
else:
|
||||
tokens['CAPTIVE_STATUS_TEXT'] = (
|
||||
'No captive portal VLANs configured. '
|
||||
'Set Restricted VLAN = Captive Portal on the Network Layout page.'
|
||||
)
|
||||
|
||||
tokens['CAPTIVE_HTTP_PORT'] = str(cp.get('http_port', 8081))
|
||||
tokens['CAPTIVE_HTTPS_DOMAIN'] = cp.get('https_domain', '')
|
||||
tokens['CAPTIVE_SPLASH_TEXT'] = cp.get('splash_text', '')
|
||||
tokens['CAPTIVE_TERMS'] = json.dumps(cp.get('terms', []))
|
||||
|
||||
return tokens
|
||||
|
|
@ -2,7 +2,7 @@ import os
|
|||
|
||||
|
||||
def is_production():
|
||||
return os.environ.get('PRODUCTION_MODE', '').lower() in ('1', 'true', 'yes')
|
||||
return not os.environ.get('DEV_MODE', '').lower() in ('1', 'true', 'yes')
|
||||
|
||||
|
||||
def is_pro():
|
||||
|
|
|
|||
|
|
@ -26,4 +26,5 @@ services:
|
|||
- SMTP_PASSWORD=lfhrygyuwvlaczaw
|
||||
- SMTP_FROM=grotek.industries@gmail.com
|
||||
- LICENSE=asdf
|
||||
- DEV_MODE=true
|
||||
restart: unless-stopped
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue