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
import copy
import ipaddress
import json
from flask import Blueprint, request, redirect, flash
from auth import require_level
@ -62,6 +63,39 @@ def addvlan_add():
if not _hash_ok():
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()
vlans = cfg.setdefault('vlans', [])
@ -82,6 +116,7 @@ def addvlan_add():
'use_blocklists': use_blocklists,
'radius_default': radius_default,
'mdns_reflection': mdns_reflection,
'server_identities': new_identities,
}
if is_vpn:
entry['peers'] = []

View file

@ -217,8 +217,34 @@
"type": "hr"
},
{
"type": "identity_builder",
"label": "Router Identities on this VLAN:"
"type": "record_editor",
"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"

View file

@ -1205,65 +1205,76 @@ def _render_item(item, tokens, inherited_req=None):
value = e(apply_tokens(item.get('value', ''), tokens))
return f'<input type="hidden" name="{name}" value="{value}"/>'
if t == 'identity_builder':
label = e(item.get('label', 'Self Ident(s):'))
return (
'<div class="form-group identity-builder">'
f'<label class="form-label">{label}</label>'
'<div class="identity-builder-body">'
'<div class="identity-builder-list">'
'<table class="data-table identity-list-table">'
'<thead><tr><th>IP Address</th><th>Description</th><th>Hostname</th><th></th></tr></thead>'
'<tbody class="identity-builder-rows">'
'<tr class="identity-empty-row">'
'<td colspan="4" class="table-empty">No identities added.</td>'
'</tr>'
'</tbody>'
'</table>'
'</div>'
'<div class="identity-builder-form">'
'<table class="inline-edit-labeled-table">'
'<thead><tr><th></th><th></th></tr></thead>'
'<tbody>'
'<tr>'
'<td class="identity-input-label">IP Address:</td>'
'<td>'
if t == 'record_editor':
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">'
'<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"/>'
+ inp +
'<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>'
)
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 (
f'<div class="form-group record-editor" data-name="{name}" data-empty-message="{empty}">'
f'<label class="form-label">{label}</label>'
f'<div class="record-editor-body">'
f'<table class="data-table record-editor-table">'
f'<thead><tr>{ths}</tr></thead>'
f'<tbody class="record-editor-rows">'
f'<tr class="record-editor-empty-row">'
f'<td colspan="{col_count}" class="table-empty">{empty}</td>'
f'</tr>'
f'</tbody>'
f'</table>'
f'<div class="record-editor-form">'
f'<div class="{grid_class}">{form_rows}</div>'
f'<div style="margin-top:0.5rem">'
f'<button type="button" class="btn btn-secondary btn-sm record-editor-add-btn">Add</button>'
f'<button type="button" class="btn btn-ghost btn-sm record-editor-cancel-btn hidden" style="margin-left:0.5rem">Cancel</button>'
f'</div>'
f'</div>'
f'</div>'
f'<input type="hidden" name="{name}" class="record-editor-hidden" value="[]"/>'
f'</div>'
)
if t == 'field':