Files
infix/src/webui/templates/pages/configure-users.html
T
Joachim Wiberg 322811d712 webui: select password hash, mask inputs, hover help on user forms
Add a Hash dropdown to the add-user and change-password forms — yescrypt
(default), SHA-512, SHA-256, MD5 — passed through to mkpasswd. These are
exactly the four hashes the infix-system:crypt-hash YANG type accepts;
unknown values fall back to the default. Password inputs switch to the
CSS-masked type=text used by keystore secrets, which keeps the browser
password manager from offering to save device credentials. Each add-user
field gets a YANG description as a hover tooltip.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-06-17 10:32:46 +02:00

312 lines
14 KiB
HTML

{{define "configure-users.html"}}
{{template "base" .}}
{{end}}
{{define "content"}}
<div class="configure-page">
{{if .Loading}}
<div class="alert alert-info"
hx-get="/configure/users"
hx-trigger="every 3s"
hx-target="#content"
hx-swap="innerHTML">
Downloading YANG schema from device&hellip; this takes a moment on first login.
</div>
{{template "configure-toolbar" .}}
</div>
{{else}}
{{if .Error}}
<div class="alert alert-error">{{.Error}}</div>
{{end}}
<section class="info-grid">
{{/* ── Users ───────────────────────────────────────────────────────── */}}
<section class="info-card info-grid-span-2">
<div class="card-header">Users</div>
<div class="data-table-wrap">
<table class="data-table cfg-table">
<thead>
<tr>
<th>Username</th>
<th>Shell</th>
<th style="text-align:center">SSH Keys</th>
<th></th>
</tr>
</thead>
<tbody>
{{range $i, $u := .Users}}
<tr>
<td>
<button class="key-row-toggle" type="button"
aria-expanded="false" data-target="user-{{$i}}">
<span class="key-row-arrow" aria-hidden="true"></span>{{.Name}}
</button>
</td>
<td>{{.ShellLabel}}</td>
<td style="text-align:center">{{.KeyCount}}</td>
<td style="text-align:right">
<button type="button" class="btn-icon btn-icon-danger"
hx-delete="/configure/users/{{.Name}}"
hx-confirm="Delete user {{.Name}}?"
hx-swap="none"
title="Delete user">
{{template "icon-trash"}}
</button>
</td>
</tr>
<tr class="key-detail-row" id="key-detail-user-{{$i}}">
<td colspan="4" class="key-detail-cell">
<div class="key-detail-body">
{{/* Login shell — match the option's bare .Label against the
user's effective ShellLabel; .Value carries a module prefix
the stored shell does not. */}}
<form hx-post="/configure/users/{{.Name}}/shell" hx-swap="none">
<div class="section-subtitle">Login Shell</div>
<select class="cfg-input" name="shell" style="max-width:16rem">
{{$shell := .ShellLabel}}
{{range $.ShellOptions}}<option value="{{.Value}}" {{if eq .Label $shell}}selected{{end}}>{{.Label}}{{if .IsDefault}} (default){{end}}</option>{{end}}
</select>
<div class="cfg-card-footer" style="padding-left:0">
<button class="btn btn-primary btn-sm" type="submit">Save</button>
<span class="cfg-save-status"></span>
</div>
</form>
{{/* Change password. type=text + CSS mask (cfg-input-mask)
dodges the browser password-manager save prompt, same as
the keystore secret fields. */}}
<form hx-post="/configure/users/{{.Name}}/password" hx-swap="none" autocomplete="off">
<div class="section-subtitle">Change Password</div>
<table class="info-table">
<tr>
<th>New password{{template "field-info" (index $.Desc "password")}}</th>
<td>
<span class="cfg-secret-wrap">
<input class="cfg-input cfg-input-mask" type="text" name="password"
id="chpw-{{.Name}}" placeholder="Leave blank to keep current"
autocomplete="off" spellcheck="false"
data-1p-ignore="true" data-lpignore="true"
data-bwignore="true" data-form-type="other">
<button type="button" class="btn-icon cfg-secret-toggle"
data-toggle-mask="chpw-{{.Name}}"
aria-label="Show / hide password">👁</button>
</span>
</td>
</tr>
<tr>
<th>Hash{{template "field-info" (index $.Desc "crypt")}}</th>
<td><select class="cfg-input" name="crypt">
{{range $.CryptMethods}}<option value="{{.Value}}" {{if .Default}}selected{{end}}>{{.Label}}</option>{{end}}
</select></td>
</tr>
</table>
<div class="cfg-card-footer" style="padding-left:0">
<button class="btn btn-primary btn-sm" type="submit">Change Password</button>
<span class="cfg-save-status"></span>
</div>
</form>
{{/* Authorized SSH keys */}}
<div>
<div class="section-subtitle">Authorized SSH Keys ({{.KeyCount}})</div>
{{if .AuthorizedKeys}}
<div class="data-table-wrap">
<table class="data-table cfg-table">
<thead>
<tr><th>Name</th><th>Algorithm</th><th></th></tr>
</thead>
<tbody>
{{range .AuthorizedKeys}}
<tr>
<td>{{.Name}}</td>
<td>{{.Algorithm}}</td>
<td style="text-align:right">
<button type="button" class="btn-icon btn-icon-danger"
hx-delete="/configure/users/{{$u.Name}}/keys/{{.Name}}"
hx-confirm="Remove key {{.Name}}?"
hx-target="closest tr"
hx-swap="delete"
title="Remove key">
{{template "icon-trash"}}
</button>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
<form hx-post="/configure/users/{{.Name}}/keys" hx-swap="none"
style="margin-top:0.5rem">
<table class="info-table">
<tr>
<th>Key name</th>
<td><input class="cfg-input" type="text" name="key_name"
placeholder="e.g. laptop"></td>
</tr>
<tr>
<th>Public key</th>
<td><textarea class="cfg-input cfg-input-mono" name="key_data" rows="3"
placeholder="ssh-rsa AAAA… or ssh-ed25519 AAAA…"></textarea></td>
</tr>
</table>
<div class="cfg-card-footer" style="padding-left:0">
<button class="btn btn-primary btn-sm" type="submit">Add Key</button>
<span class="cfg-save-status"></span>
</div>
</form>
</div>
</div>
</td>
</tr>
{{end}}
{{/* Hidden add-user row — revealed by the + Add user button. The
password is a CSS-masked type=text (cfg-input-mask) to dodge the
browser password-manager save prompt, same as keystore secrets. */}}
<tr id="add-user-row" hidden>
<td colspan="4" class="key-detail-cell">
<div class="key-detail-body">
<form hx-post="/configure/users" hx-swap="none" autocomplete="off">
<div class="section-subtitle">Add User</div>
<table class="info-table">
<tr>
<th>Username{{template "field-info" (index $.Desc "username")}}</th>
<td><input class="cfg-input" type="text" name="username" required autocomplete="off"
pattern="[a-zA-Z0-9_][a-zA-Z0-9_.-]*" placeholder="Username"></td>
</tr>
<tr>
<th>Password{{template "field-info" (index $.Desc "password")}}</th>
<td>
<span class="cfg-secret-wrap">
<input class="cfg-input cfg-input-mask" type="text" name="password"
id="add-user-password" required placeholder="Password"
autocomplete="off" spellcheck="false"
data-1p-ignore="true" data-lpignore="true"
data-bwignore="true" data-form-type="other">
<button type="button" class="btn-icon cfg-secret-toggle"
data-toggle-mask="add-user-password"
aria-label="Show / hide password">👁</button>
</span>
</td>
</tr>
<tr>
<th>Hash{{template "field-info" (index $.Desc "crypt")}}</th>
<td><select class="cfg-input" name="crypt">
{{range $.CryptMethods}}<option value="{{.Value}}" {{if .Default}}selected{{end}}>{{.Label}}</option>{{end}}
</select></td>
</tr>
<tr>
<th>Shell{{template "field-info" (index $.Desc "shell")}}</th>
<td><select class="cfg-input" name="shell">
{{range $.ShellOptions}}<option value="{{.Value}}" {{if .IsDefault}}selected{{end}}>{{.Label}}{{if .IsDefault}} (default){{end}}</option>{{end}}
</select></td>
</tr>
</table>
<div class="cfg-card-footer" style="padding-left:0">
<button class="btn btn-primary btn-sm" type="submit">Add</button>
<button type="button" class="btn btn-sm" data-hide="add-user-row">Cancel</button>
<span class="cfg-save-status"></span>
</div>
</form>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="cfg-card-footer">
<button type="button" class="btn-add-row" data-show="add-user-row">+ Add user</button>
</div>
</section>
{{/* ── Groups ─────────────────────────────────────────────────────── */}}
<section class="info-card info-grid-span-2">
<div class="card-header">Groups</div>
<div class="data-table-wrap">
<table class="data-table cfg-table">
<thead>
<tr>
<th>Group</th>
<th>Members</th>
</tr>
</thead>
<tbody>
{{range $i, $g := .Groups}}
<tr>
<td>
<button class="key-row-toggle" type="button"
aria-expanded="false" data-target="group-{{$i}}">
<span class="key-row-arrow" aria-hidden="true"></span>{{.Name}}
</button>
</td>
<td>{{.MembersSummary}}</td>
</tr>
<tr class="key-detail-row" id="key-detail-group-{{$i}}">
<td colspan="2" class="key-detail-cell">
<div class="key-detail-body">
<div class="section-subtitle">Members</div>
{{if .UserNames}}
<div class="data-table-wrap">
<table class="data-table cfg-table">
<thead><tr><th>Username</th><th></th></tr></thead>
<tbody>
{{range .UserNames}}
<tr>
<td>{{.}}</td>
<td style="text-align:right">
<button type="button" class="btn-icon btn-icon-danger"
hx-delete="/configure/users/groups/{{$g.Name}}/members/{{.}}"
hx-confirm="Remove {{.}} from group {{$g.Name}}?"
hx-target="closest tr"
hx-swap="delete"
title="Remove from group">
{{template "icon-trash"}}
</button>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<p style="color:var(--text-muted);font-size:0.85rem">No members.</p>
{{end}}
{{if .Available}}
<form hx-post="/configure/users/groups/{{.Name}}/members" hx-swap="none"
style="margin-top:0.75rem">
<div class="section-subtitle">Add Members</div>
<select class="cfg-input" name="members" multiple size="4">
{{range .Available}}<option value="{{.}}">{{.}}</option>{{end}}
</select>
<div class="cfg-card-footer" style="padding-left:0">
<button class="btn btn-primary btn-sm" type="submit">Add Selected</button>
<span class="cfg-save-status"></span>
</div>
</form>
{{end}}
</div>
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</section>
</section>
{{template "configure-toolbar" .}}
</div>
{{end}}{{/* end {{else}} — schema ready */}}
{{end}}