feat(users): implement comprehensive user listing and control
This commit is contained in:
323
templates/account_user_edit.html
Normal file
323
templates/account_user_edit.html
Normal file
@@ -0,0 +1,323 @@
|
||||
{{ template "account_shell_start" . }}
|
||||
<main class="account-window" aria-labelledby="user-edit-title">
|
||||
{{ template "account_window_titlebar" . }}
|
||||
|
||||
<nav class="menu-bar" aria-label="User edit toolbar">
|
||||
<div class="menu-item">
|
||||
<button class="menu-button" type="button" aria-expanded="false">File</button>
|
||||
<div class="menu-popup" role="menu">
|
||||
<button class="menu-action" type="button" data-ue-command="save"><span>💾</span><span>Save user</span><span class="shortcut">Ctrl+S</span></button>
|
||||
<button class="menu-action" type="button" data-ue-command="discard"><span>↩</span><span>Discard changes</span><span class="shortcut">Esc</span></button>
|
||||
{{ if .CanManage }}
|
||||
<div class="menu-separator"></div>
|
||||
{{ if .IsPending }}
|
||||
<form method="post" action="/account/users/{{ .Target.ID }}/invite/resend" style="margin:0">
|
||||
{{ template "account_csrf_field" . }}
|
||||
<button class="menu-action" type="submit"><span>✉</span><span>Send invite again</span><span></span></button>
|
||||
</form>
|
||||
{{ end }}
|
||||
<button class="menu-action" type="button" data-ue-command="reset-password"><span>🔑</span><span>Reset password</span><span></span></button>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<button class="menu-button" type="button" aria-expanded="false">User</button>
|
||||
<div class="menu-popup" role="menu">
|
||||
{{ if .CanManage }}
|
||||
{{ if not .IsSelf }}
|
||||
<form method="post" action="/account/users/{{ .Target.ID }}/enable" style="margin:0">
|
||||
{{ template "account_csrf_field" . }}
|
||||
<button class="menu-action" type="submit"><span>✔</span><span>Enable user</span><span></span></button>
|
||||
</form>
|
||||
<form method="post" action="/account/users/{{ .Target.ID }}/disable" style="margin:0">
|
||||
{{ template "account_csrf_field" . }}
|
||||
<button class="menu-action" type="submit"><span>⛔</span><span>Disable user</span><span></span></button>
|
||||
</form>
|
||||
<div class="menu-separator"></div>
|
||||
{{ end }}
|
||||
<form method="post" action="/account/users/{{ .Target.ID }}/sessions/revoke" style="margin:0">
|
||||
{{ template "account_csrf_field" . }}
|
||||
<button class="menu-action" type="submit"><span>◌</span><span>Revoke all sessions</span><span></span></button>
|
||||
</form>
|
||||
{{ end }}
|
||||
<a class="menu-action" href="/account/users"><span>←</span><span>Back to users</span><span></span></a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="account-body-content">
|
||||
{{ if .Error }}
|
||||
<div class="account-error-banner">{{ .Error }}</div>
|
||||
{{ end }}
|
||||
{{ if .Success }}
|
||||
<div class="account-success-banner">{{ .Success }}</div>
|
||||
{{ end }}
|
||||
|
||||
<section class="stats-grid" aria-label="User summary">
|
||||
{{ if eq .Status "active" }}
|
||||
<article class="stat-card sunken-panel is-ok">
|
||||
{{ else if eq .Status "pending" }}
|
||||
<article class="stat-card sunken-panel is-warning">
|
||||
{{ else }}
|
||||
<article class="stat-card sunken-panel is-danger">
|
||||
{{ end }}
|
||||
<p class="stat-label">Status</p>
|
||||
<p class="stat-value">{{ .Status }}</p>
|
||||
<p class="stat-note">
|
||||
{{ if eq .Status "active" }}<span class="stat-note-pill">can sign in</span>
|
||||
{{ else if eq .Status "pending" }}<span class="stat-note-pill">invite not accepted</span>
|
||||
{{ else }}<span class="stat-note-pill">blocked</span>{{ end }}
|
||||
</p>
|
||||
</article>
|
||||
{{ if .IsAdmin }}
|
||||
<article class="stat-card sunken-panel is-info">
|
||||
{{ else }}
|
||||
<article class="stat-card sunken-panel">
|
||||
{{ end }}
|
||||
<p class="stat-label">Role</p>
|
||||
<p class="stat-value">{{ if .IsAdmin }}admin{{ else }}user{{ end }}</p>
|
||||
<p class="stat-note">
|
||||
{{ if .TagNames }}<span class="stat-note-pill">{{ .TagNames }}</span>{{ else }}<span class="stat-note-pill">no tags</span>{{ end }}
|
||||
</p>
|
||||
</article>
|
||||
<article class="stat-card sunken-panel">
|
||||
<p class="stat-label">Max file size</p>
|
||||
<p class="stat-value">{{ if .MaxFileSizeStr }}{{ .MaxFileSizeStr }}{{ else }}default{{ end }}</p>
|
||||
<p class="stat-note"><span class="stat-note-pill">bytes</span></p>
|
||||
</article>
|
||||
<article class="stat-card sunken-panel">
|
||||
<p class="stat-label">Max expiry</p>
|
||||
<p class="stat-value">{{ if .MaxExpiryStr }}{{ .MaxExpiryStr }}s{{ else }}default{{ end }}</p>
|
||||
<p class="stat-note"><span class="stat-note-pill">seconds</span></p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<form method="post" action="/account/users/{{ .Target.ID }}" id="user-edit-form" data-ue-form>
|
||||
{{ template "account_csrf_field" . }}
|
||||
|
||||
<div class="ue-content-grid">
|
||||
<div class="ue-column">
|
||||
|
||||
<section class="win98-window section-window">
|
||||
<div class="win98-titlebar">
|
||||
<div class="win98-titlebar-label">
|
||||
<span class="win98-titlebar-icon">A</span>
|
||||
<h2>Account <span class="ue-panel-sub">identity and basic state</span></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-body sunken-panel ue-panel-body">
|
||||
<div class="ue-form-grid">
|
||||
<div class="ue-field">
|
||||
<label for="ue-username">Username</label>
|
||||
<input class="win98-input" id="ue-username" name="username" type="text" value="{{ .Target.Username }}" {{ if not .CanManage }}disabled{{ end }} autocomplete="off">
|
||||
<span class="ue-help">Visible login name.</span>
|
||||
</div>
|
||||
<div class="ue-field">
|
||||
<label for="ue-email">Email</label>
|
||||
<input class="win98-input" id="ue-email" name="email" type="email" value="{{ .Target.Email }}" {{ if not .CanManage }}disabled{{ end }} autocomplete="off">
|
||||
<span class="ue-help">Account contact and invite destination.</span>
|
||||
</div>
|
||||
{{ if not .IsPending }}
|
||||
<div class="ue-field">
|
||||
<label for="ue-state">State</label>
|
||||
<select class="win98-select" id="ue-state" name="state" {{ if or (not .CanManage) .IsSelf }}disabled{{ end }}>
|
||||
<option value="active" {{ if eq .Status "active" }}selected{{ end }}>Active</option>
|
||||
<option value="disabled" {{ if eq .Status "disabled" }}selected{{ end }}>Disabled</option>
|
||||
</select>
|
||||
<span class="ue-help">{{ if .IsSelf }}Cannot disable yourself.{{ else }}Account state.{{ end }}</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="ue-field">
|
||||
<label for="ue-admin-note">Admin note</label>
|
||||
<input class="win98-input" id="ue-admin-note" name="admin_note" type="text" value="{{ .Target.AdminNote }}" {{ if not .CanManage }}disabled{{ end }}>
|
||||
<span class="ue-help">Private note. Not shown to the user.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="win98-window section-window">
|
||||
<div class="win98-titlebar">
|
||||
<div class="win98-titlebar-label">
|
||||
<span class="win98-titlebar-icon">R</span>
|
||||
<h2>Access rights <span class="ue-panel-sub">what this account can do</span></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-body sunken-panel ue-panel-body">
|
||||
<div class="ue-check-grid">
|
||||
<label class="ue-check-card">
|
||||
<input type="checkbox" name="upload_allowed" value="1" {{ if index .Check "upload_allowed" }}checked{{ end }} {{ if not .CanManage }}disabled{{ end }}>
|
||||
<span class="ue-check-copy"><strong>Create boxes</strong><span>Allow browser or API box creation.</span></span>
|
||||
</label>
|
||||
<label class="ue-check-card">
|
||||
<input type="checkbox" name="manage_own_boxes" value="1" {{ if index .Check "manage_own_boxes" }}checked{{ end }} {{ if not .CanManage }}disabled{{ end }}>
|
||||
<span class="ue-check-copy"><strong>Manage own boxes</strong><span>Edit sharing, password, or expiry for owned boxes.</span></span>
|
||||
</label>
|
||||
<label class="ue-check-card">
|
||||
<input type="checkbox" name="renewable_allowed" value="1" {{ if index .Check "renewable_allowed" }}checked{{ end }} {{ if not .CanManage }}disabled{{ end }}>
|
||||
<span class="ue-check-copy"><strong>Refresh own box expiry</strong><span>Permits time extension within limits.</span></span>
|
||||
</label>
|
||||
<label class="ue-check-card">
|
||||
<input type="checkbox" name="zip_download_allowed" value="1" {{ if index .Check "zip_download_allowed" }}checked{{ end }} {{ if not .CanManage }}disabled{{ end }}>
|
||||
<span class="ue-check-copy"><strong>Use ZIP downloads</strong><span>Allow ZIP generation on this user's boxes.</span></span>
|
||||
</label>
|
||||
<label class="ue-check-card">
|
||||
<input type="checkbox" name="one_time_download_allowed" value="1" {{ if index .Check "one_time_download_allowed" }}checked{{ end }} {{ if not .CanManage }}disabled{{ end }}>
|
||||
<span class="ue-check-copy"><strong>Use one-time boxes</strong><span>Permit one-time ZIP handoff boxes.</span></span>
|
||||
</label>
|
||||
<label class="ue-check-card">
|
||||
<input type="checkbox" name="is_admin" value="1" {{ if .IsAdmin }}checked{{ end }} {{ if or (not .CanManage) .IsSelf }}disabled{{ end }}>
|
||||
<span class="ue-check-copy"><strong>Administrator</strong><span>Grants full admin area access. Last admin is protected.</span></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="win98-window section-window">
|
||||
<div class="win98-titlebar">
|
||||
<div class="win98-titlebar-label">
|
||||
<span class="win98-titlebar-icon">L</span>
|
||||
<h2>Limits <span class="ue-panel-sub">0 = unlimited, empty = system default</span></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-body sunken-panel ue-panel-body">
|
||||
<div class="ue-form-grid">
|
||||
<div class="ue-field">
|
||||
<label for="ue-max-file">Max file size (bytes)</label>
|
||||
<input class="win98-input" id="ue-max-file" name="max_file_size_bytes" type="number" min="0"
|
||||
value="{{ .MaxFileSizeStr }}" {{ if not .CanManage }}disabled{{ end }}>
|
||||
<span class="ue-help">Per-file cap. Empty = system default.</span>
|
||||
</div>
|
||||
<div class="ue-field">
|
||||
<label for="ue-max-box">Max box size (bytes)</label>
|
||||
<input class="win98-input" id="ue-max-box" name="max_box_size_bytes" type="number" min="0"
|
||||
value="{{ .MaxBoxSizeStr }}" {{ if not .CanManage }}disabled{{ end }}>
|
||||
<span class="ue-help">Total size per box. Empty = system default.</span>
|
||||
</div>
|
||||
<div class="ue-field ue-field-full">
|
||||
<label for="ue-max-expiry">Max box expiry (seconds)</label>
|
||||
<input class="win98-input" id="ue-max-expiry" name="max_expiry_seconds" type="number" min="0"
|
||||
value="{{ .MaxExpiryStr }}" {{ if not .CanManage }}disabled{{ end }}>
|
||||
<span class="ue-help">Maximum expiry when creating or editing a box. Empty = system default.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="ue-column">
|
||||
|
||||
<section class="win98-window section-window">
|
||||
<div class="win98-titlebar">
|
||||
<div class="win98-titlebar-label">
|
||||
<span class="win98-titlebar-icon">O</span>
|
||||
<h2>Setting overrides <span class="ue-panel-sub">account-specific behavior</span></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-body sunken-panel ue-panel-body">
|
||||
<div class="ue-check-grid ue-check-grid-1col">
|
||||
<label class="ue-check-card">
|
||||
<input type="checkbox" name="allow_password_protected" value="1" {{ if index .Check "allow_password_protected" }}checked{{ end }} {{ if not .CanManage }}disabled{{ end }}>
|
||||
<span class="ue-check-copy"><strong>Allow password-protected boxes</strong><span>Overrides system default for this account.</span></span>
|
||||
</label>
|
||||
<label class="ue-check-card">
|
||||
<input type="checkbox" name="renew_on_access" value="1" {{ if index .Check "renew_on_access" }}checked{{ end }} {{ if not .CanManage }}disabled{{ end }}>
|
||||
<span class="ue-check-copy"><strong>Allow renew on access</strong><span>Only applies when the global feature is enabled.</span></span>
|
||||
</label>
|
||||
<label class="ue-check-card">
|
||||
<input type="checkbox" name="renew_on_download" value="1" {{ if index .Check "renew_on_download" }}checked{{ end }} {{ if not .CanManage }}disabled{{ end }}>
|
||||
<span class="ue-check-copy"><strong>Allow renew on download</strong><span>Only applies when the global feature is enabled.</span></span>
|
||||
</label>
|
||||
<label class="ue-check-card">
|
||||
<input type="checkbox" name="allow_owner_box_editing" value="1" {{ if index .Check "allow_owner_box_editing" }}checked{{ end }} {{ if not .CanManage }}disabled{{ end }}>
|
||||
<span class="ue-check-copy"><strong>Allow owner box editing</strong><span>Lets the user open the box edit page for owned boxes.</span></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="win98-window section-window">
|
||||
<div class="win98-titlebar">
|
||||
<div class="win98-titlebar-label">
|
||||
<span class="win98-titlebar-icon">P</span>
|
||||
<h2>Resolved policy <span class="ue-panel-sub">effective permissions after all overrides</span></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-body sunken-panel ue-panel-body">
|
||||
<pre class="ue-policy-pre">{{ .PolicyJSON }}</pre>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="win98-window section-window">
|
||||
<div class="win98-titlebar">
|
||||
<div class="win98-titlebar-label">
|
||||
<span class="win98-titlebar-icon">I</span>
|
||||
<h2>Account info <span class="ue-panel-sub">read-only details</span></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-body sunken-panel ue-panel-body">
|
||||
<ul class="ue-info-list">
|
||||
<li class="ue-info-item"><strong>User ID</strong><span>{{ .Target.ID }}</span></li>
|
||||
<li class="ue-info-item"><strong>Created</strong><span>{{ .CreatedAtStr }}</span></li>
|
||||
<li class="ue-info-item"><strong>Updated</strong><span>{{ .UpdatedAtStr }}</span></li>
|
||||
<li class="ue-info-item"><strong>Tags</strong><span>{{ if .TagNames }}{{ .TagNames }}{{ else }}none{{ end }}</span></li>
|
||||
<li class="ue-info-item"><strong>Password</strong><span>{{ if .IsPending }}pending invite{{ else }}set{{ end }}</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{ if .CanManage }}
|
||||
<section class="win98-window section-window">
|
||||
<div class="win98-titlebar">
|
||||
<div class="win98-titlebar-label">
|
||||
<span class="win98-titlebar-icon">!</span>
|
||||
<h2>Danger zone</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-body sunken-panel ue-panel-body">
|
||||
<div class="ue-danger-row">
|
||||
<form method="post" action="/account/users/{{ .Target.ID }}/password/reset">
|
||||
{{ template "account_csrf_field" . }}
|
||||
<button class="win98-button ue-danger-btn" type="submit">Reset password</button>
|
||||
</form>
|
||||
<form method="post" action="/account/users/{{ .Target.ID }}/sessions/revoke">
|
||||
{{ template "account_csrf_field" . }}
|
||||
<button class="win98-button" type="submit">Revoke sessions</button>
|
||||
</form>
|
||||
{{ if not .IsSelf }}
|
||||
<form method="post" action="/account/users/{{ .Target.ID }}/{{ if .Target.Disabled }}enable{{ else }}disable{{ end }}">
|
||||
{{ template "account_csrf_field" . }}
|
||||
<button class="win98-button ue-danger-btn" type="submit">{{ if .Target.Disabled }}Enable{{ else }}Disable{{ end }} user</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{ end }}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ue-footer">
|
||||
<div class="ue-footer-left">
|
||||
<span class="stat-note-pill" data-ue-dirty>No unsaved changes</span>
|
||||
<a class="stat-note-pill" href="/account/users">← Back to users</a>
|
||||
</div>
|
||||
<div class="ue-footer-right">
|
||||
{{ if .CanManage }}
|
||||
<button class="win98-button" type="button" data-ue-command="discard">Discard</button>
|
||||
<button class="win98-button" type="submit">Save user</button>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<footer class="win98-statusbar" aria-label="User edit status">
|
||||
<span>editing: {{ .Target.Username }}</span>
|
||||
<span>signed in: {{ .AccountNav.Username }}</span>
|
||||
<span>{{ .Status }}</span>
|
||||
</footer>
|
||||
</main>
|
||||
{{ template "account_shell_end" . }}
|
||||
257
templates/account_users.html
Normal file
257
templates/account_users.html
Normal file
@@ -0,0 +1,257 @@
|
||||
{{ template "account_shell_start" . }}
|
||||
<main class="account-window" aria-labelledby="account-users-title">
|
||||
{{ template "account_window_titlebar" . }}
|
||||
|
||||
<nav class="menu-bar" aria-label="Users toolbar">
|
||||
<div class="menu-item">
|
||||
<button class="menu-button" type="button" aria-expanded="false">File</button>
|
||||
<div class="menu-popup" role="menu">
|
||||
<a class="menu-action" href="/account/users"><span>R</span><span>Refresh list</span><span class="shortcut">F5</span></a>
|
||||
<div class="menu-separator"></div>
|
||||
<form action="/account/logout" method="post">
|
||||
{{ template "account_csrf_field" . }}
|
||||
<button class="menu-action" type="submit"><span>Q</span><span>Log out</span><span></span></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<button class="menu-button" type="button" aria-expanded="false">View</button>
|
||||
<div class="menu-popup" role="menu">
|
||||
<a class="menu-action" href="/account/users?status=active"><span>A</span><span>Show active</span><span></span></a>
|
||||
<a class="menu-action" href="/account/users?status=disabled"><span>D</span><span>Show disabled</span><span></span></a>
|
||||
<a class="menu-action" href="/account/users"><span>X</span><span>Clear filters</span><span></span></a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="account-body-content">
|
||||
<section class="dashboard-hero raised-panel" aria-label="Users overview">
|
||||
<div class="hero-copy">
|
||||
<h2 id="account-users-title">WarpBox Users</h2>
|
||||
<p>Accounts, invites, and access. Search, filter, and manage users with safe bulk actions.</p>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<button class="small-action is-primary" type="button" data-users-action="focus-create">Create / Invite</button>
|
||||
<button class="small-action" type="button" data-users-action="select-visible">Select visible</button>
|
||||
<button class="small-action" type="button" onclick="location.href='/account/users'">Refresh</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{ if .Error }}
|
||||
<div class="account-error-banner">{{ .Error }}</div>
|
||||
{{ end }}
|
||||
{{ if .Success }}
|
||||
<div class="account-success-banner">{{ .Success }}</div>
|
||||
{{ end }}
|
||||
|
||||
<section class="stats-grid" aria-label="User statistics">
|
||||
<article class="stat-card sunken-panel is-info">
|
||||
<p class="stat-label">Total users</p>
|
||||
<p class="stat-value">{{ .Stats.TotalUsers }}</p>
|
||||
<p class="stat-note"><span class="stat-note-pill">all</span></p>
|
||||
</article>
|
||||
<article class="stat-card sunken-panel is-ok">
|
||||
<p class="stat-label">Active</p>
|
||||
<p class="stat-value">{{ .Stats.ActiveUsers }}</p>
|
||||
<p class="stat-note"><span class="stat-note-pill">enabled</span></p>
|
||||
</article>
|
||||
<article class="stat-card sunken-panel is-warning">
|
||||
<p class="stat-label">Pending invites</p>
|
||||
<p class="stat-value">{{ .Stats.PendingInvites }}</p>
|
||||
<p class="stat-note"><span class="stat-note-pill">awaiting setup</span></p>
|
||||
</article>
|
||||
<article class="stat-card sunken-panel is-danger">
|
||||
<p class="stat-label">Disabled</p>
|
||||
<p class="stat-value">{{ .Stats.DisabledUsers }}</p>
|
||||
<p class="stat-note"><span class="stat-note-pill">blocked</span></p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="main-grid users-grid" aria-label="Users panel and form">
|
||||
<aside class="win98-window section-window users-form-window">
|
||||
<div class="win98-titlebar">
|
||||
<div class="win98-titlebar-label">
|
||||
<span class="win98-titlebar-icon">+</span>
|
||||
<h2>Create or Invite</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-body sunken-panel">
|
||||
<form class="form-grid" method="post" action="/account/users">
|
||||
{{ template "account_csrf_field" . }}
|
||||
<input type="hidden" name="action" value="create">
|
||||
|
||||
<div class="field-row">
|
||||
<label for="users-mode">Mode</label>
|
||||
<select class="win98-select" name="mode" id="users-mode">
|
||||
<option value="create">Create local user</option>
|
||||
<option value="invite">Send invite</option>
|
||||
</select>
|
||||
<div class="field-help">Invite creates a disabled account with a setup link. Create makes an active user immediately.</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label for="users-username">Username</label>
|
||||
<input class="win98-input" name="username" id="users-username" required placeholder="username" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label for="users-email">Email</label>
|
||||
<input class="win98-input" name="email" id="users-email" type="email" required placeholder="user@example.test" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label for="users-password">Password</label>
|
||||
<input class="win98-input" name="password" id="users-password" type="password" autocomplete="new-password" placeholder="Leave empty for auto-generated">
|
||||
<div class="field-help">If empty, a temporary password will be generated. Never prefill passwords.</div>
|
||||
</div>
|
||||
|
||||
<div class="field-row">
|
||||
<label for="users-role">Role</label>
|
||||
<select class="win98-select" name="role" id="users-role">
|
||||
<option value="all">No tag (default)</option>
|
||||
{{ range .Tags }}
|
||||
<option value="{{ .Name }}">{{ .Name }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
<div class="field-help">Assign an initial role tag. Permissions are resolved from tag settings.</div>
|
||||
</div>
|
||||
|
||||
<div class="button-row">
|
||||
<button class="small-action" type="reset">Clear</button>
|
||||
<button class="small-action is-primary" type="submit">Apply</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="win98-window section-window span-2 users-table-window">
|
||||
<div class="win98-titlebar">
|
||||
<div class="win98-titlebar-label">
|
||||
<span class="win98-titlebar-icon">U</span>
|
||||
<h2>Users</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="users-filters-bar">
|
||||
<form class="users-filters-form" method="get" action="/account/users" id="users-filters-form">
|
||||
<input class="win98-input" name="q" value="{{ .Filters.Query }}" placeholder="Search username or email">
|
||||
<select class="win98-select" name="status" onchange="this.form.submit()">
|
||||
<option value="" {{ if eq .Filters.Status "" }}selected{{ end }}>all statuses</option>
|
||||
<option value="active" {{ if eq .Filters.Status "active" }}selected{{ end }}>active</option>
|
||||
<option value="disabled" {{ if eq .Filters.Status "disabled" }}selected{{ end }}>disabled</option>
|
||||
</select>
|
||||
<select class="win98-select" name="role" onchange="this.form.submit()">
|
||||
<option value="" {{ if eq .Filters.Role "" }}selected{{ end }}>all roles</option>
|
||||
{{ range .Tags }}
|
||||
<option value="{{ .Name }}" {{ if eq $.Filters.Role .Name }}selected{{ end }}>{{ .Name }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
<select class="win98-select" name="sort" onchange="this.form.submit()">
|
||||
<option value="username" {{ if eq .Filters.Sort "username" }}selected{{ end }}>sort username</option>
|
||||
<option value="createdDesc" {{ if eq .Filters.Sort "createdDesc" }}selected{{ end }}>newest first</option>
|
||||
</select>
|
||||
<select class="win98-select" name="page_size" onchange="this.form.submit()">
|
||||
<option value="12" {{ if eq .Filters.PageSize 12 }}selected{{ end }}>12 rows</option>
|
||||
<option value="20" {{ if eq .Filters.PageSize 20 }}selected{{ end }}>20 rows</option>
|
||||
<option value="50" {{ if eq .Filters.PageSize 50 }}selected{{ end }}>50 rows</option>
|
||||
</select>
|
||||
<noscript><button class="small-action" type="submit">Filter</button></noscript>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<form id="users-bulk-form" method="post" action="/account/users/bulk/disable">
|
||||
{{ template "account_csrf_field" . }}
|
||||
<input type="hidden" name="selected_ids" value="" id="bulk-selected-ids">
|
||||
|
||||
<div class="users-bulk-strip">
|
||||
<button class="small-action" type="button" data-users-action="select-visible">Select visible</button>
|
||||
<button class="small-action" type="submit" data-users-action="bulk-disable" onclick="setBulkAction('/account/users/bulk/disable')">Disable</button>
|
||||
<button class="small-action" type="submit" data-users-action="bulk-enable" onclick="setBulkAction('/account/users/bulk/enable')">Enable</button>
|
||||
<button class="small-action" type="submit" data-users-action="bulk-revoke" onclick="setBulkAction('/account/users/bulk/revoke-sessions')">Revoke sessions</button>
|
||||
<span class="stat-note-pill" id="selected-count">0 selected</span>
|
||||
</div>
|
||||
|
||||
<div class="section-body sunken-panel table-body-panel">
|
||||
<div class="table-scroll">
|
||||
<table class="account-table" aria-label="Users">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="check-cell"><input type="checkbox" id="master-check" aria-label="Select current page"></th>
|
||||
<th>User</th>
|
||||
<th>Email</th>
|
||||
<th>Status</th>
|
||||
<th>Role</th>
|
||||
<th>Plan</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{ range .Rows }}
|
||||
<tr data-user-id="{{ .ID }}">
|
||||
<td class="check-cell">
|
||||
<input type="checkbox" class="row-check" value="{{ .ID }}" data-user-id="{{ .ID }}" aria-label="Select {{ .Username }}">
|
||||
</td>
|
||||
<td class="user-cell">
|
||||
<div class="user-main">
|
||||
<span class="username">{{ .Username }}{{ if .IsCurrent }} <span class="pill is-info">you</span>{{ end }}</span>
|
||||
<span class="subtle">id: {{ .ID }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="email-cell" title="{{ .Email }}">{{ .Email }}</td>
|
||||
<td>
|
||||
{{ if eq .Status "active" }}
|
||||
<span class="pill is-ok">active</span>
|
||||
{{ else }}
|
||||
<span class="pill is-danger">disabled</span>
|
||||
{{ end }}
|
||||
</td>
|
||||
<td><span class="pill is-info">{{ .Role }}</span></td>
|
||||
<td><span class="pill">{{ .Plan }}</span></td>
|
||||
<td>{{ .CreatedAt }}</td>
|
||||
<td class="actions-cell">
|
||||
<a class="tiny-button" href="/account/users/{{ .ID }}">Edit</a>
|
||||
{{ if and .IsInvite (not .IsCurrent) }}
|
||||
<form method="post" action="/account/users/{{ .ID }}/invite/resend" style="display:inline">
|
||||
{{ template "account_csrf_field" $ }}
|
||||
<button class="tiny-button" type="submit">Resend invite</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{ else }}
|
||||
<tr><td colspan="8">No users found.</td></tr>
|
||||
{{ end }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="pagination">
|
||||
<span class="pagination-info">
|
||||
Page {{ .Page }} of {{ .TotalPages }} — {{ .Total }} matching user(s)
|
||||
</span>
|
||||
<div class="pagination-controls">
|
||||
{{ if .HasPrev }}
|
||||
<a class="small-action" href="?q={{ .Filters.Query }}&status={{ .Filters.Status }}&role={{ .Filters.Role }}&sort={{ .Filters.Sort }}&page_size={{ .PageSize }}&page={{ .PrevPage }}">Prev</a>
|
||||
{{ else }}
|
||||
<button class="small-action" disabled>Prev</button>
|
||||
{{ end }}
|
||||
{{ if .HasNext }}
|
||||
<a class="small-action" href="?q={{ .Filters.Query }}&status={{ .Filters.Status }}&role={{ .Filters.Role }}&sort={{ .Filters.Sort }}&page_size={{ .PageSize }}&page={{ .NextPage }}">Next</a>
|
||||
{{ else }}
|
||||
<button class="small-action" disabled>Next</button>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer class="win98-statusbar" aria-label="Users status">
|
||||
<span>signed in: {{ .AccountNav.Username }}</span>
|
||||
<span>{{ if .AccountNav.IsAdmin }}admin{{ else }}account{{ end }}</span>
|
||||
<span>ready</span>
|
||||
</footer>
|
||||
</main>
|
||||
{{ template "account_shell_end" . }}
|
||||
Reference in New Issue
Block a user