Development

This commit is contained in:
Matthew Grotke 2026-05-27 23:28:55 -04:00
parent 0c0589a0b1
commit a55d44f480
3 changed files with 131 additions and 59 deletions

View file

@ -1,6 +1,7 @@
from pathlib import Path from pathlib import Path
import copy import copy
import ipaddress import ipaddress
import json
from flask import Blueprint, request, redirect, flash from flask import Blueprint, request, redirect, flash
from auth import require_level from auth import require_level
@ -62,6 +63,39 @@ def addvlan_add():
if not _hash_ok(): if not _hash_ok():
return redirect(f'/{_PAGE}') return redirect(f'/{_PAGE}')
server_identities_raw = request.form.get('server_identities', '[]')
try:
raw_identities = json.loads(server_identities_raw)
if not isinstance(raw_identities, list):
raise ValueError
except (ValueError, TypeError):
flash('Invalid identity data.', 'error')
return redirect(f'/{_PAGE}')
new_identities = []
if raw_identities:
_vlan_net = ipaddress.IPv4Network(f'{subnet}/{subnet_mask}', strict=False)
for raw in raw_identities:
ip_clean = sanitize.ip(str(raw.get('ip', '')))
if not ip_clean:
flash('Invalid IP address in identity.', 'error')
return redirect(f'/{_PAGE}')
if ipaddress.IPv4Address(ip_clean) not in _vlan_net:
flash(f"Identity IP '{ip_clean}' is not in the VLAN subnet ({subnet}/{subnet_mask}).", 'error')
return redirect(f'/{_PAGE}')
ident = {'ip': ip_clean}
desc = str(raw.get('description', '')).strip()
if desc:
ident['description'] = desc
hostname_raw = str(raw.get('hostname', '')).strip()
if hostname_raw:
clean_hostname = sanitize.hostname(hostname_raw)
if clean_hostname is None:
flash(f"'{hostname_raw}' is not a valid hostname.", 'error')
return redirect(f'/{_PAGE}')
ident['hostname'] = clean_hostname
new_identities.append(ident)
cfg = load_config() cfg = load_config()
vlans = cfg.setdefault('vlans', []) vlans = cfg.setdefault('vlans', [])
@ -82,6 +116,7 @@ def addvlan_add():
'use_blocklists': use_blocklists, 'use_blocklists': use_blocklists,
'radius_default': radius_default, 'radius_default': radius_default,
'mdns_reflection': mdns_reflection, 'mdns_reflection': mdns_reflection,
'server_identities': new_identities,
} }
if is_vpn: if is_vpn:
entry['peers'] = [] entry['peers'] = []

View file

@ -217,8 +217,34 @@
"type": "hr" "type": "hr"
}, },
{ {
"type": "identity_builder", "type": "record_editor",
"label": "Router Identities on this VLAN:" "label": "Router Identities on this VLAN:",
"name": "server_identities",
"empty_message": "No identities added.",
"fields": [
{
"label": "IP Address",
"name": "ip",
"valtype": "address",
"attrs": {
"data-dep-subnet": "[name='subnet']",
"data-dep-mask": ".subnet-prefix-input"
},
"placeholder": "x.x.x.x",
"required": true
},
{
"label": "Description",
"name": "description",
"placeholder": "Optional label"
},
{
"label": "Hostname",
"name": "hostname",
"validate": "networkname",
"placeholder": "Optional"
}
]
}, },
{ {
"type": "hr" "type": "hr"

View file

@ -1205,65 +1205,76 @@ def _render_item(item, tokens, inherited_req=None):
value = e(apply_tokens(item.get('value', ''), tokens)) value = e(apply_tokens(item.get('value', ''), tokens))
return f'<input type="hidden" name="{name}" value="{value}"/>' return f'<input type="hidden" name="{name}" value="{value}"/>'
if t == 'identity_builder': if t == 'record_editor':
label = e(item.get('label', 'Self Ident(s):')) label = e(item.get('label', ''))
name = e(item.get('name', ''))
empty = e(item.get('empty_message', 'No records added.'))
fields = item.get('fields', [])
col_count = len(fields) + 1
ths = ''.join(f'<th>{e(f.get("label",""))}</th>' for f in fields) + '<th></th>'
form_rows = ''
for f in fields:
f_label = e(f.get('label', ''))
f_name = e(f.get('name', ''))
f_placeholder = e(f.get('placeholder', ''))
f_required = 'true' if f.get('required') else 'false'
f_validate = f.get('validate', '')
f_valtype = f.get('valtype', '')
f_attrs = f.get('attrs', {})
attr_str = f' data-field="{f_name}" data-required="{f_required}"'
if f_validate:
attr_str += f' data-validate="{e(f_validate)}"'
if f_valtype:
attr_str += f' data-valtype="{e(f_valtype)}"'
for ak, av in f_attrs.items():
attr_str += f' {e(ak)}="{e(str(av))}"'
inp = f'<input type="text" class="form-input"{attr_str} placeholder="{f_placeholder}"/>'
if f_validate or f_valtype:
field_inner = (
'<div class="field-wrap">'
+ inp +
'<p class="form-hint field-dyn-hint hidden"></p>'
'</div>'
)
else:
field_inner = inp
form_rows += (
f'<div class="form-group">'
f'<label class="form-label">{f_label}</label>'
f'{field_inner}'
f'</div>'
)
n = len(fields)
grid_class = f'form-row-{n}' if n in (2, 3, 4) else 'form-row-3'
return ( return (
'<div class="form-group identity-builder">' f'<div class="form-group record-editor" data-name="{name}" data-empty-message="{empty}">'
f'<label class="form-label">{label}</label>' f'<label class="form-label">{label}</label>'
'<div class="identity-builder-body">' f'<div class="record-editor-body">'
'<div class="identity-builder-list">' f'<table class="data-table record-editor-table">'
'<table class="data-table identity-list-table">' f'<thead><tr>{ths}</tr></thead>'
'<thead><tr><th>IP Address</th><th>Description</th><th>Hostname</th><th></th></tr></thead>' f'<tbody class="record-editor-rows">'
'<tbody class="identity-builder-rows">' f'<tr class="record-editor-empty-row">'
'<tr class="identity-empty-row">' f'<td colspan="{col_count}" class="table-empty">{empty}</td>'
'<td colspan="4" class="table-empty">No identities added.</td>' f'</tr>'
'</tr>' f'</tbody>'
'</tbody>' f'</table>'
'</table>' f'<div class="record-editor-form">'
'</div>' f'<div class="{grid_class}">{form_rows}</div>'
'<div class="identity-builder-form">' f'<div style="margin-top:0.5rem">'
'<table class="inline-edit-labeled-table">' f'<button type="button" class="btn btn-secondary btn-sm record-editor-add-btn">Add</button>'
'<thead><tr><th></th><th></th></tr></thead>' f'<button type="button" class="btn btn-ghost btn-sm record-editor-cancel-btn hidden" style="margin-left:0.5rem">Cancel</button>'
'<tbody>' f'</div>'
'<tr>' f'</div>'
'<td class="identity-input-label">IP Address:</td>' f'</div>'
'<td>' f'<input type="hidden" name="{name}" class="record-editor-hidden" value="[]"/>'
'<div class="field-wrap">' f'</div>'
'<input type="text" class="form-input inline-edit-input identity-ip-input"'
' placeholder="x.x.x.x"'
' data-valtype="address"'
' data-dep-subnet="[name=\'subnet\']"'
' data-dep-mask=".subnet-prefix-input"/>'
'<p class="form-hint field-dyn-hint hidden"></p>'
'</div>'
'</td>'
'</tr>'
'<tr>'
'<td class="identity-input-label">Description (Opt):</td>'
'<td><input type="text" class="form-input inline-edit-input identity-desc-input"/></td>'
'</tr>'
'<tr>'
'<td class="identity-input-label">Hostname (Opt):</td>'
'<td>'
'<div class="form-group field-wrap" style="margin:0">'
'<input type="text" class="form-input inline-edit-input identity-host-input"'
' data-validate="networkname"/>'
'<p class="form-hint field-dyn-hint hidden"></p>'
'</div>'
'</td>'
'</tr>'
'<tr>'
'<td></td>'
'<td><button type="button" class="btn btn-secondary btn-sm identity-add-btn">Add</button></td>'
'</tr>'
'</tbody>'
'</table>'
'</div>'
'</div>'
'<textarea name="server_identity_ips" class="hidden"></textarea>'
'<textarea name="server_identity_descriptions" class="hidden"></textarea>'
'<textarea name="server_identity_hostnames" class="hidden"></textarea>'
'</div>'
) )
if t == 'field': if t == 'field':