diff --git a/docker/routlin-dash/app/navbar.json b/docker/routlin-dash/app/navbar.json index 953a487..27e3904 100644 --- a/docker/routlin-dash/app/navbar.json +++ b/docker/routlin-dash/app/navbar.json @@ -25,8 +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": "Captive Portal", "map_to": "captiveportal", "client_requirement": "client_is_administrator+" } + { "type": "nav_item", "label": "Captive Portal", "map_to": "captiveportal", "client_requirement": "client_is_administrator+" }, + { "type": "nav_item", "label": "Client Credentials", "map_to": "clientcredentials", "client_requirement": "client_is_administrator+" } ] }, { diff --git a/docker/routlin-dash/app/pages/captiveportal/action.py b/docker/routlin-dash/app/pages/captiveportal/action.py index 1b12b18..8d6009c 100644 --- a/docker/routlin-dash/app/pages/captiveportal/action.py +++ b/docker/routlin-dash/app/pages/captiveportal/action.py @@ -55,8 +55,8 @@ def portal_save(): require_upw = 'require_username_password' in request.form try: - dur_n = int(request.form.get('default_duration_value', '0').strip() or '0') - dur_unit = request.form.get('default_duration_unit', 'hours') + dur_n = int(request.form.get('default_session_value', '0').strip() or '0') + dur_unit = request.form.get('default_session_unit', 'hours') mult = {'hours': 3600, 'days': 86400}.get(dur_unit, 3600) duration = dur_n * mult if dur_n > 0 else 0 except (ValueError, TypeError): @@ -68,7 +68,7 @@ def portal_save(): 'portal_splash_text': splash_text, 'portal_terms': terms, 'require_username_password': require_upw, - 'default_duration_seconds': duration, + 'default_session_seconds': duration, } vlan['captive_portal'] = after diff --git a/docker/routlin-dash/app/pages/captiveportal/content.json b/docker/routlin-dash/app/pages/captiveportal/content.json index 640422d..38c9b98 100644 --- a/docker/routlin-dash/app/pages/captiveportal/content.json +++ b/docker/routlin-dash/app/pages/captiveportal/content.json @@ -91,6 +91,11 @@ "field": "portal_terms_display", "class": "col-narrow" }, + { + "label": "Session", + "field": "session_display", + "class": "col-narrow" + }, { "label": "U/P Required", "field": "require_upw", @@ -168,7 +173,7 @@ { "type": "field", "label": "Default Session Duration", - "name": "default_duration_value", + "name": "default_session_value", "input_type": "number", "min": 0, "value": "0", @@ -177,7 +182,7 @@ { "type": "field", "label": "Unit", - "name": "default_duration_unit", + "name": "default_session_unit", "input_type": "select", "options": [ {"value": "hours", "label": "Hours"}, diff --git a/docker/routlin-dash/app/pages/captiveportal/view.py b/docker/routlin-dash/app/pages/captiveportal/view.py index 6daf492..bf7c408 100644 --- a/docker/routlin-dash/app/pages/captiveportal/view.py +++ b/docker/routlin-dash/app/pages/captiveportal/view.py @@ -4,6 +4,16 @@ import config_utils import factory +def _format_session(secs): + if not secs or secs <= 0: + return 'No limit' + if secs % 86400 == 0: + d = secs // 86400 + return f'{d} day{"s" if d != 1 else ""}' + h = secs / 3600 + return f'{h:g} h' + + def collect_tokens(cfg): tokens = config_utils.collect_layout_tokens(cfg) cp = cfg.get('captive_portal', {}) @@ -28,7 +38,7 @@ def collect_tokens(cfg): text = cp.get('portal_splash_text', vlan.get('portal_splash_text', '')) terms = cp.get('portal_terms', vlan.get('portal_terms', [])) require_upw = cp.get('require_username_password', vlan.get('require_username_password', False)) - duration = cp.get('default_duration_seconds', vlan.get('default_duration_seconds', 0)) + duration = cp.get('default_session_seconds', vlan.get('default_session_seconds', 0)) n = len(terms) display_rows.append({ 'vlan_name': vlan['name'], @@ -38,7 +48,8 @@ def collect_tokens(cfg): 'portal_terms_display': f'{n} term{"s" if n != 1 else ""}' if n else '--', 'require_upw': require_upw, 'require_username_password': require_upw, - 'default_duration_seconds': duration, + 'default_session_seconds': duration, + 'session_display': _format_session(duration), }) content = factory.load_json(f'{factory.PAGES_DIR}/captiveportal/content.json') diff --git a/docker/routlin-dash/app/pages/clientcredentials/content.json b/docker/routlin-dash/app/pages/clientcredentials/content.json index 1991e71..b077456 100644 --- a/docker/routlin-dash/app/pages/clientcredentials/content.json +++ b/docker/routlin-dash/app/pages/clientcredentials/content.json @@ -97,7 +97,7 @@ }, { "type": "raw_html", - "html": "" + "html": "" }, { "type": "field", diff --git a/docker/routlin-dash/app/pages/clientcredentials/view.py b/docker/routlin-dash/app/pages/clientcredentials/view.py index 7c0d5a0..a37258f 100644 --- a/docker/routlin-dash/app/pages/clientcredentials/view.py +++ b/docker/routlin-dash/app/pages/clientcredentials/view.py @@ -69,6 +69,9 @@ def collect_tokens(cfg): 'Credentials can be viewed but not added or edited without a Pro license.' ) tokens['ADD_CREDENTIAL_DISABLED'] = 'true' + tokens['RADIUS_DEFAULT_SESSION_SECONDS_JS'] = str( + cfg.get('radius', {}).get('options', {}).get('default_session_seconds', 0) or 0 + ) vlans = [v for v in cfg.get('vlans', []) if not v.get('is_vpn')] tokens['VLAN_OPTIONS'] = json.dumps( @@ -84,8 +87,8 @@ def collect_tokens(cfg): 'label': f"{v['name']} (VLAN {v['vlan_id']})", 'require_upw': v.get('captive_portal', {}).get('require_username_password', v.get('require_username_password', False)), - 'default_duration_seconds': v.get('captive_portal', {}).get('default_duration_seconds', - v.get('default_duration_seconds', 0)), + 'default_session_seconds': v.get('captive_portal', {}).get('default_session_seconds', + v.get('default_session_seconds', 0)), } for v in captive_vlans ] diff --git a/docker/routlin-dash/app/pages/radius/action.py b/docker/routlin-dash/app/pages/radius/action.py index 49f5fdc..7548ca0 100644 --- a/docker/routlin-dash/app/pages/radius/action.py +++ b/docker/routlin-dash/app/pages/radius/action.py @@ -85,7 +85,7 @@ def auth_mode_save(): if auth_mode == 'eap_password': after['eap_protocol'] = eap_protocol after['tunneled_reply'] = tunneled_reply and eap_protocol in ('eap_peap', 'eap_ttls') - after['mab_first'] = mab_first + after['mab_first'] = mab_first if eap_protocol in _valid_inner and inner_protocol in _valid_inner[eap_protocol]: after['inner_protocol'] = inner_protocol else: @@ -94,18 +94,27 @@ def auth_mode_save(): after['include_length'] = include_length else: after.pop('include_length', None) + try: + dur_n = int(request.form.get('default_session_value', '0').strip() or '0') + dur_unit = request.form.get('default_session_unit', 'hours') + mult = {'hours': 3600, 'days': 86400}.get(dur_unit, 3600) + after['default_session_seconds'] = dur_n * mult if dur_n > 0 else 0 + except (ValueError, TypeError): + after['default_session_seconds'] = 0 elif auth_mode == 'eap_credential': after['include_length'] = include_length - after['mab_first'] = mab_first - after.pop('eap_protocol', None) - after.pop('tunneled_reply', None) - after.pop('inner_protocol', None) + after['mab_first'] = mab_first + after.pop('eap_protocol', None) + after.pop('tunneled_reply', None) + after.pop('inner_protocol', None) + after.pop('default_session_seconds', None) else: # mab - after.pop('eap_protocol', None) - after.pop('tunneled_reply', None) - after.pop('inner_protocol', None) - after.pop('include_length', None) - after.pop('mab_first', None) + after.pop('eap_protocol', None) + after.pop('tunneled_reply', None) + after.pop('inner_protocol', None) + after.pop('include_length', None) + after.pop('mab_first', None) + after.pop('default_session_seconds', None) cfg.setdefault('radius', {})['options'] = after changes = config_utils.diff_fields(before, after) diff --git a/docker/routlin-dash/app/pages/radius/content.json b/docker/routlin-dash/app/pages/radius/content.json index 676b87a..9009500 100644 --- a/docker/routlin-dash/app/pages/radius/content.json +++ b/docker/routlin-dash/app/pages/radius/content.json @@ -210,6 +210,40 @@ "type": "raw_html", "html": "" }, + { + "type": "raw_html", + "html": "
" + }, + { + "type": "field_row", + "cols": 2, + "items": [ + { + "type": "field", + "label": "Default Session Duration", + "name": "default_session_value", + "input_type": "number", + "min": 0, + "value": "%RADIUS_DEFAULT_SESSION_VALUE%", + "hint": "How long a client session lasts before reauthentication is required. 0 = no expiration." + }, + { + "type": "field", + "label": "Unit", + "name": "default_session_unit", + "input_type": "select", + "value": "%RADIUS_DEFAULT_SESSION_UNIT%", + "options": [ + {"value": "hours", "label": "Hours"}, + {"value": "days", "label": "Days"} + ] + } + ] + }, + { + "type": "raw_html", + "html": "
" + }, { "type": "button_row", "items": [ diff --git a/docker/routlin-dash/app/pages/radius/view.py b/docker/routlin-dash/app/pages/radius/view.py index 7ffe453..6c66d52 100644 --- a/docker/routlin-dash/app/pages/radius/view.py +++ b/docker/routlin-dash/app/pages/radius/view.py @@ -123,9 +123,20 @@ def collect_tokens(cfg): tokens['RADIUS_LOGGING_HINT'] = 'Unchecking will clear logs.' if fr_gen.get('logging', False) else '' tokens['RADIUS_GEN_LOG_MAX_KB'] = str(fr_gen.get('log_max_kb', 1024)) - tokens['RADIUS_TUNNELED_REPLY'] = 'true' if fr_opts.get('tunneled_reply', False) else '' - tokens['RADIUS_INCLUDE_LENGTH'] = 'true' if fr_opts.get('include_length', False) else '' - tokens['RADIUS_MAB_FIRST'] = 'true' if fr_opts.get('mab_first', True) else '' + tokens['RADIUS_TUNNELED_REPLY'] = 'true' if fr_opts.get('tunneled_reply', False) else '' + tokens['RADIUS_INCLUDE_LENGTH'] = 'true' if fr_opts.get('include_length', False) else '' + tokens['RADIUS_MAB_FIRST'] = 'true' if fr_opts.get('mab_first', True) else '' + + secs = fr_opts.get('default_session_seconds', 0) or 0 + if secs >= 86400 and secs % 86400 == 0: + tokens['RADIUS_DEFAULT_SESSION_VALUE'] = str(secs // 86400) + tokens['RADIUS_DEFAULT_SESSION_UNIT'] = 'days' + elif secs > 0: + tokens['RADIUS_DEFAULT_SESSION_VALUE'] = str(max(1, round(secs / 3600))) + tokens['RADIUS_DEFAULT_SESSION_UNIT'] = 'hours' + else: + tokens['RADIUS_DEFAULT_SESSION_VALUE'] = '0' + tokens['RADIUS_DEFAULT_SESSION_UNIT'] = 'hours' vlans = cfg.get('vlans', []) default_vlan = next((v['name'] for v in vlans if v.get('radius_default') is True), '')