diff --git a/docker/routlin-dash/app/pages/captiveportal/action.py b/docker/routlin-dash/app/pages/captiveportal/action.py
index 8d6009c..63eb450 100644
--- a/docker/routlin-dash/app/pages/captiveportal/action.py
+++ b/docker/routlin-dash/app/pages/captiveportal/action.py
@@ -62,13 +62,22 @@ def portal_save():
except (ValueError, TypeError):
duration = 0
+ try:
+ exp_n = int(request.form.get('default_expiration_value', '0').strip() or '0')
+ exp_unit = request.form.get('default_expiration_unit', 'hours')
+ mult = {'hours': 3600, 'days': 86400}.get(exp_unit, 3600)
+ expiration = exp_n * mult if exp_n > 0 else 0
+ except (ValueError, TypeError):
+ expiration = 0
+
after = {
**existing,
- 'portal_splash_title': splash_title,
- 'portal_splash_text': splash_text,
- 'portal_terms': terms,
- 'require_username_password': require_upw,
- 'default_session_seconds': duration,
+ 'portal_splash_title': splash_title,
+ 'portal_splash_text': splash_text,
+ 'portal_terms': terms,
+ 'require_username_password': require_upw,
+ 'default_session_seconds': duration,
+ 'default_expiration_seconds': expiration,
}
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 38c9b98..dc23f39 100644
--- a/docker/routlin-dash/app/pages/captiveportal/content.json
+++ b/docker/routlin-dash/app/pages/captiveportal/content.json
@@ -177,7 +177,7 @@
"input_type": "number",
"min": 0,
"value": "0",
- "hint": "How long portal access lasts after authentication. 0 = no expiration."
+ "hint": "How long portal access lasts after authentication. 0 = no session limit."
},
{
"type": "field",
@@ -191,6 +191,31 @@
}
]
},
+ {
+ "type": "field_row",
+ "cols": 2,
+ "items": [
+ {
+ "type": "field",
+ "label": "Default Expiration Duration",
+ "name": "default_expiration_value",
+ "input_type": "number",
+ "min": 0,
+ "value": "0",
+ "hint": "How long after creation an account is valid before it permanently expires. 0 = never expires."
+ },
+ {
+ "type": "field",
+ "label": "Unit",
+ "name": "default_expiration_unit",
+ "input_type": "select",
+ "options": [
+ {"value": "hours", "label": "Hours"},
+ {"value": "days", "label": "Days"}
+ ]
+ }
+ ]
+ },
{
"type": "button_row",
"items": [
diff --git a/docker/routlin-dash/app/pages/captiveportal/view.py b/docker/routlin-dash/app/pages/captiveportal/view.py
index bf7c408..adda45a 100644
--- a/docker/routlin-dash/app/pages/captiveportal/view.py
+++ b/docker/routlin-dash/app/pages/captiveportal/view.py
@@ -38,18 +38,20 @@ 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_session_seconds', vlan.get('default_session_seconds', 0))
+ duration = cp.get('default_session_seconds', vlan.get('default_session_seconds', 0))
+ expiration = cp.get('default_expiration_seconds', vlan.get('default_expiration_seconds', 0))
n = len(terms)
display_rows.append({
- 'vlan_name': vlan['name'],
- 'portal_splash_title': title,
- 'portal_splash_text': text,
- 'portal_terms': terms,
- 'portal_terms_display': f'{n} term{"s" if n != 1 else ""}' if n else '--',
- 'require_upw': require_upw,
- 'require_username_password': require_upw,
- 'default_session_seconds': duration,
- 'session_display': _format_session(duration),
+ 'vlan_name': vlan['name'],
+ 'portal_splash_title': title,
+ 'portal_splash_text': text,
+ 'portal_terms': terms,
+ 'portal_terms_display': f'{n} term{"s" if n != 1 else ""}' if n else '--',
+ 'require_upw': require_upw,
+ 'require_username_password': require_upw,
+ 'default_session_seconds': duration,
+ 'default_expiration_seconds': expiration,
+ '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/action.py b/docker/routlin-dash/app/pages/clientcredentials/action.py
index d31bcd6..684201a 100644
--- a/docker/routlin-dash/app/pages/clientcredentials/action.py
+++ b/docker/routlin-dash/app/pages/clientcredentials/action.py
@@ -187,8 +187,12 @@ def addedit():
enabled = 'enabled' in request.form
session_seconds = _parse_session_seconds(
- request.form.get('session_seconds_value', ''),
- request.form.get('session_seconds_unit', 'hours'),
+ request.form.get('session_duration_value', ''),
+ request.form.get('session_duration_unit', 'hours'),
+ )
+ expires_seconds = _parse_session_seconds(
+ request.form.get('expiration_duration_value', ''),
+ request.form.get('expiration_duration_unit', 'hours'),
)
if not username:
@@ -223,10 +227,10 @@ def addedit():
conn.execute(
"""UPDATE credentials
SET username=?, password=?, description=?, user_type=?, digest_type=?,
- vlan=?, enabled=?, date_set=?, session_seconds=?
+ vlan=?, enabled=?, date_set=?, session_seconds=?, expires_seconds=?
WHERE id=?""",
(username, stored_password, description, user_type, stored_digest_type,
- vlan, int(enabled), date_set, session_seconds, existing['id']),
+ vlan, int(enabled), date_set, session_seconds, expires_seconds, existing['id']),
)
conn.commit()
except sqlite3.IntegrityError:
@@ -255,7 +259,7 @@ def addedit():
(username, password, description, user_type, digest_type, vlan, enabled, date_set, session_seconds, expires_seconds)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(username, hashed, description, user_type, digest_type,
- vlan, int(enabled), int(time.time()), session_seconds, 0),
+ vlan, int(enabled), int(time.time()), session_seconds, expires_seconds),
)
conn.commit()
except sqlite3.IntegrityError:
diff --git a/docker/routlin-dash/app/pages/clientcredentials/content.json b/docker/routlin-dash/app/pages/clientcredentials/content.json
index 5866dfb..ad961b5 100644
--- a/docker/routlin-dash/app/pages/clientcredentials/content.json
+++ b/docker/routlin-dash/app/pages/clientcredentials/content.json
@@ -76,7 +76,7 @@
{
"type": "card",
"id": "add-form",
- "label": "Add Credential",
+ "label": "Add User Account",
"client_requirement": "client_is_administrator+",
"items": [
{
@@ -91,13 +91,14 @@
},
{
"type": "field",
- "label": "Credential Active",
+ "label": "Account Active",
"name": "enabled",
- "input_type": "checkbox"
+ "input_type": "checkbox",
+ "value": "true"
},
{
"type": "raw_html",
- "html": ""
+ "html": ""
},
{
"type": "field",
@@ -170,17 +171,42 @@
"items": [
{
"type": "field",
- "label": "Valid For",
- "name": "session_seconds_value",
+ "label": "Session Duration",
+ "name": "session_duration_value",
"input_type": "number",
"min": 0,
"value": "0",
- "hint": "How long this credential is valid after creation. 0 = no expiration."
+ "hint": "0 = no session limit"
},
{
"type": "field",
"label": "Unit",
- "name": "session_seconds_unit",
+ "name": "session_duration_unit",
+ "input_type": "select",
+ "options": [
+ {"value": "hours", "label": "Hours"},
+ {"value": "days", "label": "Days"}
+ ]
+ }
+ ]
+ },
+ {
+ "type": "field_row",
+ "cols": 2,
+ "items": [
+ {
+ "type": "field",
+ "label": "Expiration Duration",
+ "name": "expiration_duration_value",
+ "input_type": "number",
+ "min": 0,
+ "value": "0",
+ "hint": "How long after creation an account is valid before it permanently expires. 0 = never expires."
+ },
+ {
+ "type": "field",
+ "label": "Unit",
+ "name": "expiration_duration_unit",
"input_type": "select",
"options": [
{"value": "hours", "label": "Hours"},
diff --git a/docker/routlin-dash/app/pages/clientcredentials/view.py b/docker/routlin-dash/app/pages/clientcredentials/view.py
index 08b86bb..4fb7b09 100644
--- a/docker/routlin-dash/app/pages/clientcredentials/view.py
+++ b/docker/routlin-dash/app/pages/clientcredentials/view.py
@@ -71,6 +71,9 @@ def collect_tokens(cfg):
tokens['RADIUS_DEFAULT_SESSION_SECONDS_JS'] = str(
cfg.get('radius', {}).get('options', {}).get('default_session_seconds', 0) or 0
)
+ tokens['RADIUS_DEFAULT_EXPIRATION_SECONDS_JS'] = str(
+ cfg.get('radius', {}).get('options', {}).get('default_expiration_seconds', 0) or 0
+ )
vlans = [v for v in cfg.get('vlans', []) if not v.get('is_vpn')]
tokens['VLAN_OPTIONS'] = json.dumps(
@@ -84,10 +87,12 @@ def collect_tokens(cfg):
{
'value': v['name'],
'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_session_seconds': v.get('captive_portal', {}).get('default_session_seconds',
- v.get('default_session_seconds', 0)),
+ 'require_upw': v.get('captive_portal', {}).get('require_username_password',
+ v.get('require_username_password', False)),
+ 'default_session_seconds': v.get('captive_portal', {}).get('default_session_seconds',
+ v.get('default_session_seconds', 0)),
+ 'default_expiration_seconds': v.get('captive_portal', {}).get('default_expiration_seconds',
+ v.get('default_expiration_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 ecb5581..45caa5c 100644
--- a/docker/routlin-dash/app/pages/radius/action.py
+++ b/docker/routlin-dash/app/pages/radius/action.py
@@ -101,20 +101,29 @@ def auth_mode_save():
after['default_session_seconds'] = dur_n * mult if dur_n > 0 else 0
except (ValueError, TypeError):
after['default_session_seconds'] = 0
+ try:
+ exp_n = int(request.form.get('default_expiration_value', '0').strip() or '0')
+ exp_unit = request.form.get('default_expiration_unit', 'hours')
+ mult = {'hours': 3600, 'days': 86400}.get(exp_unit, 3600)
+ after['default_expiration_seconds'] = exp_n * mult if exp_n > 0 else 0
+ except (ValueError, TypeError):
+ after['default_expiration_seconds'] = 0
elif auth_mode == 'eap_certificate':
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.pop('default_session_seconds', None)
+ after.pop('eap_protocol', None)
+ after.pop('tunneled_reply', None)
+ after.pop('inner_protocol', None)
+ after.pop('default_session_seconds', None)
+ after.pop('default_expiration_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('default_session_seconds', 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)
+ after.pop('default_expiration_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 9009500..3979286 100644
--- a/docker/routlin-dash/app/pages/radius/content.json
+++ b/docker/routlin-dash/app/pages/radius/content.json
@@ -225,7 +225,7 @@
"input_type": "number",
"min": 0,
"value": "%RADIUS_DEFAULT_SESSION_VALUE%",
- "hint": "How long a client session lasts before reauthentication is required. 0 = no expiration."
+ "hint": "How long a client session lasts before reauthentication is required. 0 = no session limit."
},
{
"type": "field",
@@ -240,6 +240,32 @@
}
]
},
+ {
+ "type": "field_row",
+ "cols": 2,
+ "items": [
+ {
+ "type": "field",
+ "label": "Default Expiration Duration",
+ "name": "default_expiration_value",
+ "input_type": "number",
+ "min": 0,
+ "value": "%RADIUS_DEFAULT_EXPIRATION_VALUE%",
+ "hint": "How long after creation an account is valid before it permanently expires. 0 = never expires."
+ },
+ {
+ "type": "field",
+ "label": "Unit",
+ "name": "default_expiration_unit",
+ "input_type": "select",
+ "value": "%RADIUS_DEFAULT_EXPIRATION_UNIT%",
+ "options": [
+ {"value": "hours", "label": "Hours"},
+ {"value": "days", "label": "Days"}
+ ]
+ }
+ ]
+ },
{
"type": "raw_html",
"html": ""
diff --git a/docker/routlin-dash/app/pages/radius/view.py b/docker/routlin-dash/app/pages/radius/view.py
index e3b05b7..beabd75 100644
--- a/docker/routlin-dash/app/pages/radius/view.py
+++ b/docker/routlin-dash/app/pages/radius/view.py
@@ -138,6 +138,17 @@ def collect_tokens(cfg):
tokens['RADIUS_DEFAULT_SESSION_VALUE'] = '0'
tokens['RADIUS_DEFAULT_SESSION_UNIT'] = 'hours'
+ exps = fr_opts.get('default_expiration_seconds', 0) or 0
+ if exps >= 86400 and exps % 86400 == 0:
+ tokens['RADIUS_DEFAULT_EXPIRATION_VALUE'] = str(exps // 86400)
+ tokens['RADIUS_DEFAULT_EXPIRATION_UNIT'] = 'days'
+ elif exps > 0:
+ tokens['RADIUS_DEFAULT_EXPIRATION_VALUE'] = str(max(1, round(exps / 3600)))
+ tokens['RADIUS_DEFAULT_EXPIRATION_UNIT'] = 'hours'
+ else:
+ tokens['RADIUS_DEFAULT_EXPIRATION_VALUE'] = '0'
+ tokens['RADIUS_DEFAULT_EXPIRATION_UNIT'] = 'hours'
+
vlans = cfg.get('vlans', [])
default_vlan = next((v['name'] for v in vlans if v.get('radius_default') is True), '')
vlan_options = [{'value': '', 'label': 'None (reject unknown devices)'}]
diff --git a/routlin/mod_radius.py b/routlin/mod_radius.py
index 66c61fa..0bc440c 100644
--- a/routlin/mod_radius.py
+++ b/routlin/mod_radius.py
@@ -52,7 +52,7 @@ def _load_supplicant_credentials():
conn = sqlite3.connect(str(CREDENTIALS_DB_FILE))
conn.row_factory = sqlite3.Row
rows = conn.execute(
- "SELECT username, password, digest_type, vlan, session_seconds, expires_seconds FROM credentials"
+ "SELECT username, password, digest_type, vlan, session_seconds, expires_seconds, date_set FROM credentials"
" WHERE user_type=? AND enabled=1",
(USER_TYPE_SUPPLICANT,)
).fetchall()
@@ -145,7 +145,7 @@ def _supplicant_reply_attrs(cred, default_session_seconds, vlan_id, vlan):
attrs.append(f" Session-Timeout = {session}")
expires = cred.get('expires_seconds') or 0
if expires:
- dt = datetime.datetime.fromtimestamp(expires)
+ dt = datetime.datetime.fromtimestamp((cred.get('date_set') or 0) + expires)
attrs.append(f" Expiration := \"{dt.strftime('%b %d %Y %H:%M:%S')}\"")
return attrs