cf34710b4f
Tests · Go / test (push) Successful in 1m56s
Add the operator console's user-administration pages over the existing
*user.Service (no new business logic).
- GET /_gm/users paginated account list
- GET /_gm/users/{id} account detail: profile, entitlement, sanctions
- POST /_gm/users/{id}/block apply permanent_block (reason required)
- POST /_gm/users/{id}/entitlement set the entitlement tier
- POST /_gm/users/{id}/soft-delete soft-delete the account (cascades)
The console depends on a UserAdmin interface (satisfied by *user.Service) so the
pages render in tests without a database. All writes flow through the CSRF
guard, carry the operator as the audit actor, and answer with a 303 redirect;
a generic message page handles not-found, validation, and failure notices.
Unblock is intentionally absent — the admin API exposes no remove-sanction
endpoint.
Tests: list/detail render, not-found, block (with actor/scope/reason
assertions), missing-reason 400, bad-CSRF 403, entitlement, soft-delete
redirect, and the service-unavailable path.
Docs: backend/docs/admin-console.md gains the page inventory.
69 lines
2.6 KiB
Go Template
69 lines
2.6 KiB
Go Template
{{define "content" -}}
|
|
{{$csrf := .CSRFToken}}
|
|
{{with .Data}}
|
|
<p><a href="/_gm/users">« all users</a></p>
|
|
<h1>{{.Email}}</h1>
|
|
{{if .Deleted}}<p class="bad">This account is soft-deleted.</p>{{end}}
|
|
|
|
<section class="panel">
|
|
<h2>Account</h2>
|
|
<ul class="kv">
|
|
<li>User ID: <code>{{.UserID}}</code></li>
|
|
<li>User name: {{.UserName}}</li>
|
|
<li>Display name: {{.DisplayName}}</li>
|
|
<li>Preferred language: {{.PreferredLanguage}}</li>
|
|
<li>Time zone: {{.TimeZone}}</li>
|
|
<li>Declared country: {{.DeclaredCountry}}</li>
|
|
<li>Status: {{if .Blocked}}<span class="bad">blocked</span>{{else}}<span class="ok">active</span>{{end}}</li>
|
|
<li>Created: {{.CreatedAt}}</li>
|
|
<li>Updated: {{.UpdatedAt}}</li>
|
|
</ul>
|
|
</section>
|
|
|
|
<section class="panel">
|
|
<h2>Entitlement</h2>
|
|
<ul class="kv">
|
|
<li>Tier: <strong>{{.Tier}}</strong> ({{if .IsPaid}}paid{{else}}free{{end}})</li>
|
|
<li>Source: {{.EntitlementSource}}</li>
|
|
<li>Reason: {{.EntitlementReason}}</li>
|
|
<li>Ends: {{if .EntitlementEnds}}{{.EntitlementEnds}}{{else}}—{{end}}</li>
|
|
</ul>
|
|
<form method="post" action="/_gm/users/{{.UserID}}/entitlement" class="form">
|
|
<input type="hidden" name="_csrf" value="{{$csrf}}">
|
|
<label>Tier
|
|
<select name="tier">{{range .Tiers}}<option value="{{.}}">{{.}}</option>{{end}}</select>
|
|
</label>
|
|
<label>Source <input type="text" name="source" value="admin"></label>
|
|
<label>Reason <input type="text" name="reason_code" placeholder="optional"></label>
|
|
<button type="submit">Update entitlement</button>
|
|
</form>
|
|
</section>
|
|
|
|
<section class="panel">
|
|
<h2>Active sanctions</h2>
|
|
{{if .Sanctions}}
|
|
<table class="counts"><tbody>
|
|
{{range .Sanctions}}<tr><td>{{.SanctionCode}}</td><td>{{.Scope}}</td><td>{{.ReasonCode}}</td><td>{{.AppliedAt}}</td></tr>{{end}}
|
|
</tbody></table>
|
|
{{else}}<p class="note">none</p>{{end}}
|
|
{{if .Blocked}}
|
|
<p class="note">User is permanently blocked. Unblock is not available in the current admin API.</p>
|
|
{{else}}
|
|
<form method="post" action="/_gm/users/{{.UserID}}/block" class="form" onsubmit="return confirm('Permanently block this user?');">
|
|
<input type="hidden" name="_csrf" value="{{$csrf}}">
|
|
<label>Reason <input type="text" name="reason_code" required></label>
|
|
<button type="submit" class="danger">Permanently block</button>
|
|
</form>
|
|
{{end}}
|
|
</section>
|
|
|
|
<section class="panel">
|
|
<h2>Danger zone</h2>
|
|
<form method="post" action="/_gm/users/{{.UserID}}/soft-delete" class="form" onsubmit="return confirm('Soft-delete this account? This cascades to sessions, memberships, and owned games.');">
|
|
<input type="hidden" name="_csrf" value="{{$csrf}}">
|
|
<button type="submit" class="danger">Soft-delete account</button>
|
|
</form>
|
|
</section>
|
|
{{end}}
|
|
{{- end}}
|