panel-redesign: paths follow the menu; Account and Server settings split
Stage 1, step 3 (plan § Routes). BREAKING: every panel URL changes and the
old paths are removed without redirects.
Routes (web.go)
- /status -> /overview; /domains -> /outbound/domains with the domain's
settings actions under .../settings/ (reports, ratelimit, export);
/deliveries -> /outbound/log; /mail-queue -> /outbound/queue; /dmarc ->
/outbound/dmarc; /inbound -> /inbound/domains; /system-log ->
/server/log; /backup -> /server/backup (import under it); /users ->
/server/users; recheck and reload under /server/health/.
- One fragment name: <page>/fragment everywhere.
- Everything under /server/ is registered through server(), which wraps
the handler in globalOnly: the subtree is guarded once, where a new
route cannot forget it. Handlers keep their own requireGlobal.
- GET / leads to /overview, /outbound/domains or /inbound/domains.
- The /account -> /settings redirect is gone with /settings.
Account and Server > Settings
- handlers_account.go: GET /account and three POSTs - profile (username,
e-mail; no password asked, as designed), password (current password,
rate-limited like a sign-in, signs out other sessions), dmarc (the
user's default: hosted / account / custom / none, each validated).
This replaces step 1's bridge through the old Settings form.
- handlers_settings.go is the instance's page only: log retention and the
read-only rate limits, global role.
- One legacy template serves both pages by a flag until stage 2 gives each
its own: a new legacy page cannot be added to the ratchet.
Still to come inside stage 1, and why @routes stays on the ratchet:
- step 4: the application routes (one POST, under the domain);
- step 5: GET /server/health.
With @routes removed by hand TestRoutesFollowNavigation reports exactly
those and nothing else - no template links to a path the router lacks.
Old templates, tests and the e2e client are retargeted. The e2e module was
edited but not run here (no Docker, and its dependency could not be
downloaded on this machine).
Tests: Account (page, profile, password, DMARC default in every mode, a
domain administrator's own row only), Server settings (saved without a
password, range, global only), the /server/ subtree answered 404 by the
router for a domain administrator with All on both lists and for no
principal.
go test ./... green except the two Windows-only failures in internal/domain
and internal/logtail.
Roadmap 13/27.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
the download is then a <code>.spbk</code> file (SelfPost backup) that only
|
||||
the password opens.</p>
|
||||
{{if .BackupErr}}<p class="error">{{.BackupErr}}</p>{{end}}
|
||||
<form method="post" action="/backup">
|
||||
<form method="post" action="/server/backup">
|
||||
{{template "encryptfields" .}}
|
||||
<button type="submit">Download full backup</button>
|
||||
</form>
|
||||
@@ -30,7 +30,7 @@
|
||||
application passwords come across, so the published DNS record needs no
|
||||
change. The export file is a secret, like a full backup.</p>
|
||||
{{if .ImportErr}}<p class="error">{{.ImportErr}}</p>{{end}}
|
||||
<form method="post" action="/domains/import" enctype="multipart/form-data">
|
||||
<form method="post" action="/server/backup/import" enctype="multipart/form-data">
|
||||
<label for="importfile">Domain export file</label>
|
||||
<input id="importfile" name="file" type="file" accept=".json,.spde,application/json" required data-import-file>
|
||||
<div class="encrypt-fields" data-import-password-fields>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
{{if .IsGlobal}}
|
||||
<div class="card">
|
||||
<h2>Add a sending domain</h2>
|
||||
<form method="post" action="/domains">
|
||||
<form method="post" action="/outbound/domains">
|
||||
<label for="name">Domain</label>
|
||||
<div class="input-row">
|
||||
<input id="name" name="name" type="text" placeholder="example.com"
|
||||
@@ -29,11 +29,11 @@
|
||||
<tbody>
|
||||
{{range .Domains}}
|
||||
<tr>
|
||||
<td><a href="/domains/{{.ID}}">{{.Name}}</a></td>
|
||||
<td><a class="st st-{{.DNS}}" href="/domains/{{.ID}}#dns-status">{{.DNS}}</a></td>
|
||||
<td><a href="/outbound/domains/{{.ID}}">{{.Name}}</a></td>
|
||||
<td><a class="st st-{{.DNS}}" href="/outbound/domains/{{.ID}}#dns-status">{{.DNS}}</a></td>
|
||||
<td class="muted">{{.DKIMSelector}}</td>
|
||||
<td>{{.AppCount}}</td>
|
||||
{{if $.IsGlobal}}<td class="actions"><a class="danger" href="/domains/{{.ID}}/delete">Delete</a></td>{{else}}<td></td>{{end}}
|
||||
{{if $.IsGlobal}}<td class="actions"><a class="danger" href="/outbound/domains/{{.ID}}/delete">Delete</a></td>{{else}}<td></td>{{end}}
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
|
||||
<p class="muted">Rows older than {{.SendLogRetentionDays}} days are deleted
|
||||
automatically.{{if .IsGlobal}} Change retention on
|
||||
<a href="/settings">Settings</a>.{{end}}</p>
|
||||
<a href="/server/settings">Settings</a>.{{end}}</p>
|
||||
|
||||
<div class="card">
|
||||
<h2>Filter</h2>
|
||||
<form method="get" action="/deliveries">
|
||||
<form method="get" action="/outbound/log">
|
||||
<label for="domain">Domain</label>
|
||||
<select id="domain" name="domain">
|
||||
<option value="">All domains</option>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{define "deliveries_rows"}}
|
||||
<div id="deliveries-rows" data-poll aria-live="polite"
|
||||
hx-get="/deliveries/rows?domain={{.FilterDomain | urlquery}}&app={{.FilterApp | urlquery}}&p={{.Page}}"
|
||||
hx-get="/outbound/log/fragment?domain={{.FilterDomain | urlquery}}&app={{.FilterApp | urlquery}}&p={{.Page}}"
|
||||
hx-trigger="load" hx-swap="outerHTML">
|
||||
{{if .Rows}}
|
||||
<table>
|
||||
@@ -15,15 +15,15 @@
|
||||
<td>{{.To}}</td>
|
||||
<td class="subject"><span title="{{.Subject}}">{{.Subject}}</span></td>
|
||||
<td class="status"><span class="st st-{{.Level}}">{{.Status}}</span></td>
|
||||
<td class="actions"><a href="/deliveries/{{.ID}}?domain={{$.FilterDomain | urlquery}}&app={{$.FilterApp | urlquery}}&p={{$.Page}}">Details</a></td>
|
||||
<td class="actions"><a href="/outbound/log/{{.ID}}?domain={{$.FilterDomain | urlquery}}&app={{$.FilterApp | urlquery}}&p={{$.Page}}">Details</a></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="muted">
|
||||
Page {{.Page}}{{if .LastPage}} of {{.LastPage}}{{end}}
|
||||
{{if .HasPrev}} · <a href="/deliveries?domain={{.FilterDomain | urlquery}}&app={{.FilterApp | urlquery}}&p={{.PrevPage}}">← Newer</a>{{end}}
|
||||
{{if .HasNext}} · <a href="/deliveries?domain={{.FilterDomain | urlquery}}&app={{.FilterApp | urlquery}}&p={{.NextPage}}">Older →</a>{{end}}
|
||||
{{if .HasPrev}} · <a href="/outbound/log?domain={{.FilterDomain | urlquery}}&app={{.FilterApp | urlquery}}&p={{.PrevPage}}">← Newer</a>{{end}}
|
||||
{{if .HasNext}} · <a href="/outbound/log?domain={{.FilterDomain | urlquery}}&app={{.FilterApp | urlquery}}&p={{.NextPage}}">Older →</a>{{end}}
|
||||
</p>
|
||||
{{else}}
|
||||
<p class="muted">No messages logged yet.</p>
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
<code>mail.log</code>, oldest first — the connection to the receiving server,
|
||||
its reply, and the status that reply was filed as. Only lines carrying this
|
||||
message's queue id are shown; the whole log is on the
|
||||
<a href="/system-log">System log</a> page.</p>
|
||||
<a href="/server/log">System log</a> page.</p>
|
||||
|
||||
{{if .LogRows}}
|
||||
{{/* Two columns, the way the send log itself is a table: the times down one
|
||||
|
||||
@@ -34,12 +34,12 @@ XML — not a dashboard.</p>
|
||||
{{range .Reports}}
|
||||
<tr>
|
||||
<td class="time muted">{{.ReceivedLabel}}</td>
|
||||
<td>{{if .DomainID}}<a href="/dmarc/domains/{{.DomainID}}">{{.Domain}}</a>{{else}}{{.Domain}}{{end}}</td>
|
||||
<td>{{if .DomainID}}<a href="/outbound/dmarc/domains/{{.DomainID}}">{{.Domain}}</a>{{else}}{{.Domain}}{{end}}</td>
|
||||
<td class="muted">{{.Reporter}}</td>
|
||||
<td>{{.PeriodLabel}}</td>
|
||||
<td>{{.PassCount}}</td>
|
||||
<td>{{.FailCount}}</td>
|
||||
<td class="actions"><a href="/dmarc/reports/{{.ID}}">View</a></td>
|
||||
<td class="actions"><a href="/outbound/dmarc/reports/{{.ID}}">View</a></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
@@ -47,7 +47,7 @@ XML — not a dashboard.</p>
|
||||
<ul class="phone-list phone-only">
|
||||
{{range .Reports}}
|
||||
<li>
|
||||
<a href="/dmarc/reports/{{.ID}}">
|
||||
<a href="/outbound/dmarc/reports/{{.ID}}">
|
||||
<span class="st {{if gt .FailCount 0}}st-warn{{else}}st-ok{{end}}">{{.PassCount}} pass</span>
|
||||
<span class="when">{{.ReceivedLabel}}</span>
|
||||
<span class="meta">{{.Domain}} · {{.Reporter}}</span>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{{define "content"}}
|
||||
<h1>{{.Domain.Name}}</h1>
|
||||
{{template "back_link" (back "/dmarc" "All DMARC reports")}}
|
||||
{{template "back_link" (back (printf "/domains/%d" .Domain.ID) (printf "Back to %s" .Domain.Name))}}
|
||||
{{template "back_link" (back "/outbound/dmarc" "All DMARC reports")}}
|
||||
{{template "back_link" (back (printf "/outbound/domains/%d" .Domain.ID) (printf "Back to %s" .Domain.Name))}}
|
||||
|
||||
<div class="pair">
|
||||
<div class="card">
|
||||
<h2>Last {{.WindowDays}} days {{if and (gt .Pass7d 0) (eq .Fail7d 0)}}<span class="st st-ok">pass</span>{{else if gt .Fail7d 0}}<span class="st st-warn">review</span>{{end}}</h2>
|
||||
<p>{{.PolicyHint}}</p>
|
||||
<p class="muted"><a href="/domains/{{.Domain.ID}}#dmarc">Domain DNS and rua=</a></p>
|
||||
<p class="muted"><a href="/outbound/domains/{{.Domain.ID}}#dmarc">Domain DNS and rua=</a></p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Totals</h2>
|
||||
@@ -28,7 +28,7 @@
|
||||
<td>{{.PeriodLabel}}</td>
|
||||
<td>{{.PassCount}}</td>
|
||||
<td>{{.FailCount}}</td>
|
||||
<td class="actions"><a href="/dmarc/reports/{{.ID}}">View</a></td>
|
||||
<td class="actions"><a href="/outbound/dmarc/reports/{{.ID}}">View</a></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<span class="st st-ok">{{.Report.PassCount}} pass</span>
|
||||
{{if gt .Report.FailCount 0}}<span class="st st-warn">{{.Report.FailCount}} fail</span>{{end}}
|
||||
</p>
|
||||
{{template "back_link" (back (printf "/dmarc/domains/%d" .Domain.ID) (printf "Back to %s" .Domain.Name))}}
|
||||
{{template "back_link" (back (printf "/outbound/dmarc/domains/%d" .Domain.ID) (printf "Back to %s" .Domain.Name))}}
|
||||
|
||||
<div class="pair">
|
||||
<div class="card">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{define "content"}}
|
||||
<h1>Delete {{.Domain.Name}}</h1>
|
||||
|
||||
{{template "back_link" (back (printf "/domains/%d" .Domain.ID) (printf "Back to %s" .Domain.Name))}}
|
||||
{{template "back_link" (back (printf "/outbound/domains/%d" .Domain.ID) (printf "Back to %s" .Domain.Name))}}
|
||||
|
||||
<div class="card">
|
||||
<h2>Confirm deletion</h2>
|
||||
@@ -17,7 +17,7 @@
|
||||
<p class="muted">This cannot be undone. The published DKIM DNS record becomes
|
||||
invalid; remove it from DNS if you do not plan to re-add the domain.</p>
|
||||
|
||||
<form method="post" action="/domains/{{.Domain.ID}}/delete">
|
||||
<form method="post" action="/outbound/domains/{{.Domain.ID}}/delete">
|
||||
<button type="submit" class="danger">Delete {{.Domain.Name}}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
{{define "content"}}
|
||||
<h1>{{.Domain.Name}}</h1>
|
||||
|
||||
{{template "back_link" (back "/domains" "All domains")}}
|
||||
{{template "back_link" (back "/outbound/domains" "All domains")}}
|
||||
|
||||
{{if .Flash}}<div class="flash">{{.Flash}}</div>{{end}}
|
||||
{{if .RateLimitErr}}<div class="flash error">{{.RateLimitErr}}</div>{{end}}
|
||||
@@ -140,7 +140,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="inline" method="post" action="/domains/{{.Domain.ID}}/dns-recheck">
|
||||
<form class="inline" method="post" action="/outbound/domains/{{.Domain.ID}}/dns-recheck">
|
||||
<button type="submit">Re-check</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -186,7 +186,7 @@
|
||||
SelfPost does not receive inbound mail — use a mailbox on another domain.{{end}}</p>
|
||||
{{end}}
|
||||
{{if and .DMARCIngestEnabled .ResolvedDMARCEmail}}
|
||||
<p class="muted"><a href="/dmarc/domains/{{.Domain.ID}}">View DMARC reports</a> for this domain.</p>
|
||||
<p class="muted"><a href="/outbound/dmarc/domains/{{.Domain.ID}}">View DMARC reports</a> for this domain.</p>
|
||||
{{end}}
|
||||
|
||||
{{if .NeedsReportAuth}}
|
||||
@@ -228,7 +228,7 @@
|
||||
above the domains list. */}}
|
||||
<div class="card" id="add-application">
|
||||
<h2>Add an application</h2>
|
||||
<form method="post" action="/domains/{{.Domain.ID}}/applications">
|
||||
<form method="post" action="/outbound/domains/{{.Domain.ID}}/applications">
|
||||
<label for="login">Login</label>
|
||||
<input id="login" name="login" type="text" placeholder="prod-server"
|
||||
autocomplete="off" autocapitalize="none" spellcheck="false"
|
||||
@@ -418,10 +418,10 @@
|
||||
<div class="check-cols check-cols-rows">
|
||||
<div class="check-col">
|
||||
<p class="check-col-title">DMARC reports</p>
|
||||
<p class="muted">Default comes from <a href="/settings">Settings</a>;
|
||||
<p class="muted">Default comes from <a href="/account#dmarc">Account</a>;
|
||||
override per domain here.</p>
|
||||
<div class="check-col-fields">
|
||||
<form id="dmarc-domain" method="post" action="/domains/{{.Domain.ID}}/dmarc">
|
||||
<form id="dmarc-domain" method="post" action="/outbound/domains/{{.Domain.ID}}/settings/reports">
|
||||
<label for="dmarc_rua_mode">Aggregate reports (rua=)</label>
|
||||
<select id="dmarc_rua_mode" name="dmarc_rua_mode" data-custom-mode="custom">
|
||||
<option value="inherit"{{if eq .DMARCRuaMode "inherit"}} selected{{end}}>Same as Settings{{if .ProfileDMARCEmail}} ({{.ProfileDMARCEmail}}){{end}}</option>
|
||||
@@ -446,9 +446,9 @@
|
||||
<div class="check-col">
|
||||
<p class="check-col-title">Level-2 rate limit
|
||||
{{if .DomainHasRL}}<span class="st st-ok">active</span>{{else}}<span class="st st-unknown">inactive</span>{{end}}</p>
|
||||
<p class="muted">Level 1 backstop: {{.L1Messages}} messages / {{.L1Window}}s — <a href="/settings#rate-limits">Settings</a>.</p>
|
||||
<p class="muted">Level 1 backstop: {{.L1Messages}} messages / {{.L1Window}}s {{if .IsGlobal}} — <a href="/server/settings#rate-limits">Settings</a>{{end}}.</p>
|
||||
<div class="check-col-fields">
|
||||
<form id="rl-domain" method="post" action="/domains/{{.Domain.ID}}/ratelimit">
|
||||
<form id="rl-domain" method="post" action="/outbound/domains/{{.Domain.ID}}/settings/ratelimit">
|
||||
<label for="d_mode">Limit mode</label>
|
||||
<select id="d_mode" name="mode" data-ratelimit-mode>
|
||||
<option value="manual" {{if not .DomainRLAuto}}selected{{end}}>Manual</option>
|
||||
@@ -483,12 +483,12 @@
|
||||
<div class="check-col-actions">
|
||||
<button type="submit" form="rl-domain">Save limit</button>
|
||||
{{if .DomainRLAuto}}
|
||||
<form class="inline" method="post" action="/domains/{{.Domain.ID}}/ratelimit/recalc">
|
||||
<form class="inline" method="post" action="/outbound/domains/{{.Domain.ID}}/settings/ratelimit/recalc">
|
||||
<button type="submit">Recalculate now</button>
|
||||
</form>
|
||||
{{end}}
|
||||
{{if .DomainHasRL}}
|
||||
<form class="inline" method="post" action="/domains/{{.Domain.ID}}/ratelimit"
|
||||
<form class="inline" method="post" action="/outbound/domains/{{.Domain.ID}}/settings/ratelimit"
|
||||
data-confirm="Remove the domain rate limit? Only the global level-1 limit will apply.">
|
||||
<input type="hidden" name="clear" value="1">
|
||||
<button type="submit" class="danger">Remove limit</button>
|
||||
@@ -508,7 +508,7 @@
|
||||
<p class="muted"><strong>Secret file</strong> — transfer securely, or encrypt
|
||||
below as <code>.spde</code>.</p>
|
||||
{{if .ExportErr}}<p class="error">{{.ExportErr}}</p>{{end}}
|
||||
<form method="post" action="/domains/{{.Domain.ID}}/export">
|
||||
<form method="post" action="/outbound/domains/{{.Domain.ID}}/settings/export">
|
||||
{{template "encryptfields" .}}
|
||||
<button type="submit">Export domain</button>
|
||||
</form>
|
||||
@@ -517,7 +517,7 @@
|
||||
<div class="card" id="danger">
|
||||
<h2>Danger zone</h2>
|
||||
<p class="muted">Deletes the DKIM key and every application on this domain.</p>
|
||||
<a class="danger" href="/domains/{{.Domain.ID}}/delete">Delete domain</a>
|
||||
<a class="danger" href="/outbound/domains/{{.Domain.ID}}/delete">Delete domain</a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -12,10 +12,10 @@ on the card itself.</p>
|
||||
<ul>
|
||||
<li><label for="help-status">Status checks</label> — Machine, TLS, PTR,
|
||||
queue</li>
|
||||
<li><a href="/mail-queue">Mail queue retries</a> — time-based, no attempt
|
||||
<li><a href="/outbound/queue">Mail queue retries</a> — time-based, no attempt
|
||||
budget (on the queue page)</li>
|
||||
{{if .InboundEnabled}}
|
||||
<li><a href="/inbound">Inbound</a> — not mailboxes; listed recipients or any
|
||||
<li><a href="/inbound/domains">Inbound</a> — not mailboxes; listed recipients or any
|
||||
address at the domain</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
|
||||
@@ -14,7 +14,7 @@ Off by default in Compose.</p>
|
||||
{{/* Adding and deleting a domain stay with the global role; a domain
|
||||
administrator works with the inbound domains assigned to them. */}}
|
||||
{{if .IsGlobal}}
|
||||
<form method="post" action="/inbound">
|
||||
<form method="post" action="/inbound/domains">
|
||||
<label for="name">Add inbound domain</label>
|
||||
<div class="input-row">
|
||||
<input id="name" name="name" type="text" placeholder="lists.example.com"
|
||||
@@ -33,12 +33,12 @@ Off by default in Compose.</p>
|
||||
<tbody>
|
||||
{{range .Domains}}
|
||||
<tr>
|
||||
<td><a href="/inbound/{{.ID}}">{{.Name}}</a></td>
|
||||
<td><a class="st st-{{.DNS}}" href="/inbound/{{.ID}}">{{.DNS}}</a></td>
|
||||
<td><a href="/inbound/domains/{{.ID}}">{{.Name}}</a></td>
|
||||
<td><a class="st st-{{.DNS}}" href="/inbound/domains/{{.ID}}">{{.DNS}}</a></td>
|
||||
<td class="muted">{{.Upstream}}</td>
|
||||
<td>{{.RcptLabel}}</td>
|
||||
<td><span class="st st-{{if eq .TLSMode "encrypt"}}ok{{else}}unknown{{end}}">{{.TLSLabel}}</span></td>
|
||||
<td class="actions">{{if $.IsGlobal}}<a class="danger" href="/inbound/{{.ID}}/delete">Delete</a>{{end}}</td>
|
||||
<td class="actions">{{if $.IsGlobal}}<a class="danger" href="/inbound/domains/{{.ID}}/delete">Delete</a>{{end}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{define "content"}}
|
||||
<h1>Delete {{.Domain.Name}}</h1>
|
||||
|
||||
{{template "back_link" (back (printf "/inbound/%d" .Domain.ID) (printf "Back to %s" .Domain.Name))}}
|
||||
{{template "back_link" (back (printf "/inbound/domains/%d" .Domain.ID) (printf "Back to %s" .Domain.Name))}}
|
||||
|
||||
<div class="card">
|
||||
<h2>Confirm deletion</h2>
|
||||
@@ -13,7 +13,7 @@
|
||||
</ul>
|
||||
<p class="muted">This cannot be undone from a backup of inbound maps alone
|
||||
unless you restore one. Remove the MX if you do not plan to re-add the domain.</p>
|
||||
<form method="post" action="/inbound/{{.Domain.ID}}/delete">
|
||||
<form method="post" action="/inbound/domains/{{.Domain.ID}}/delete">
|
||||
<button type="submit" class="danger">Delete {{.Domain.Name}}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{{define "content"}}
|
||||
<h1>{{.Domain.Name}}</h1>
|
||||
|
||||
{{template "back_link" (back "/inbound" "All inbound domains")}}
|
||||
{{template "back_link" (back "/inbound/domains" "All inbound domains")}}
|
||||
|
||||
{{if .Flash}}<div class="flash">{{.Flash}}</div>{{end}}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
{{else}}
|
||||
<p class="error">{{.MX.Detail}}</p>
|
||||
{{end}}
|
||||
<form method="post" action="/inbound/{{.Domain.ID}}/dns-recheck">
|
||||
<form method="post" action="/inbound/domains/{{.Domain.ID}}/dns-recheck">
|
||||
<button type="submit">Re-check</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -43,7 +43,7 @@
|
||||
<div class="card">
|
||||
<h2>Upstream</h2>
|
||||
<p class="muted">Where accepted mail is handed off. Not a mailbox.</p>
|
||||
<form method="post" action="/inbound/{{.Domain.ID}}/upstream">
|
||||
<form method="post" action="/inbound/domains/{{.Domain.ID}}/upstream">
|
||||
<label for="host">Host</label>
|
||||
<input id="host" name="host" type="text" value="{{.Domain.Host}}"
|
||||
autocomplete="off" autocapitalize="none" spellcheck="false" required>
|
||||
@@ -89,7 +89,7 @@
|
||||
<h2>Valid recipients</h2>
|
||||
<p class="muted">Who this domain accepts on port 25. Same idea as an
|
||||
application's address mode: a list, or any address at the domain.</p>
|
||||
<form method="post" action="/inbound/{{.Domain.ID}}/recipients">
|
||||
<form method="post" action="/inbound/domains/{{.Domain.ID}}/recipients">
|
||||
<label for="recipient_mode">Who to accept</label>
|
||||
<select id="recipient_mode" name="recipient_mode" data-list-mode="list">
|
||||
<option value="list" {{if eq .Domain.RecipientMode "list"}}selected{{end}}>Listed addresses only</option>
|
||||
@@ -112,7 +112,7 @@
|
||||
<h2>Danger zone</h2>
|
||||
<p class="muted">Stops accepting mail for this domain. Does not touch
|
||||
outbound sending domains.</p>
|
||||
{{if .IsGlobal}}<p><a class="danger" href="/inbound/{{.Domain.ID}}/delete">Delete inbound domain</a></p>{{end}}
|
||||
{{if .IsGlobal}}<p><a class="danger" href="/inbound/domains/{{.Domain.ID}}/delete">Delete inbound domain</a></p>{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
@@ -68,38 +68,43 @@
|
||||
<nav class="nav">
|
||||
{{/* The order of the blocks is the order they are read in, top to bottom of
|
||||
the column: the mark, the panel's pages, and the session last. The mark
|
||||
goes to /status, the page the panel opens on — named outright rather
|
||||
goes to /overview, the page the panel opens on — named outright rather
|
||||
than as "/", which is only a redirect to it. */}}
|
||||
<a class="brand" href="{{if .IsGlobal}}/status{{else if .HasOutbound}}/domains{{else}}/inbound{{end}}"><img src="/static/logo-compact.svg" width="220" height="100" alt="SelfPost"></a>
|
||||
<a class="brand" href="{{if .IsGlobal}}/overview{{else if .HasOutbound}}/outbound/domains{{else}}/inbound/domains{{end}}"><img src="/static/logo-compact.svg" width="220" height="100" alt="SelfPost"></a>
|
||||
<div class="links">
|
||||
{{if .IsGlobal}}
|
||||
{{if eq .Active "status"}}<span aria-current="page">{{template "icon-status"}}Status</span>{{else}}<a href="/status">{{template "icon-status"}}Status</a>{{end}}
|
||||
{{if eq .Active "status"}}<span aria-current="page">{{template "icon-status"}}Status</span>{{else}}<a href="/overview">{{template "icon-status"}}Status</a>{{end}}
|
||||
{{end}}
|
||||
{{/* A group with nothing assigned is absent from that user's menu: the
|
||||
sending pages need outbound reach, Inbound needs inbound reach. */}}
|
||||
{{if or .IsGlobal .HasOutbound}}
|
||||
{{if eq .Active "domains"}}<span aria-current="page">{{template "icon-domains"}}Domains</span>{{else}}<a href="/domains">{{template "icon-domains"}}Domains</a>{{end}}
|
||||
{{if eq .Active "domains"}}<span aria-current="page">{{template "icon-domains"}}Domains</span>{{else}}<a href="/outbound/domains">{{template "icon-domains"}}Domains</a>{{end}}
|
||||
{{end}}
|
||||
{{if and .InboundEnabled (or .IsGlobal .HasInbound)}}
|
||||
{{if eq .Active "inbound"}}<span aria-current="page">{{template "icon-inbound"}}Inbound</span>{{else}}<a href="/inbound">{{template "icon-inbound"}}Inbound</a>{{end}}
|
||||
{{if eq .Active "inbound"}}<span aria-current="page">{{template "icon-inbound"}}Inbound</span>{{else}}<a href="/inbound/domains">{{template "icon-inbound"}}Inbound</a>{{end}}
|
||||
{{end}}
|
||||
{{if and .DMARCEnabled (or .IsGlobal .HasOutbound)}}
|
||||
{{if eq .Active "dmarc"}}<span aria-current="page">{{template "icon-dmarc"}}DMARC</span>{{else}}<a href="/dmarc">{{template "icon-dmarc"}}DMARC</a>{{end}}
|
||||
{{if eq .Active "dmarc"}}<span aria-current="page">{{template "icon-dmarc"}}DMARC</span>{{else}}<a href="/outbound/dmarc">{{template "icon-dmarc"}}DMARC</a>{{end}}
|
||||
{{end}}
|
||||
{{if eq .Active "help"}}<span aria-current="page">{{template "icon-help"}}Help</span>{{else}}<a href="/help">{{template "icon-help"}}Help</a>{{end}}
|
||||
{{if or .IsGlobal .HasOutbound}}
|
||||
{{if eq .Active "deliveries"}}<span aria-current="page">{{template "icon-deliveries"}}Deliveries</span>{{else}}<a href="/deliveries">{{template "icon-deliveries"}}Deliveries</a>{{end}}
|
||||
{{if eq .Active "deliveries"}}<span aria-current="page">{{template "icon-deliveries"}}Deliveries</span>{{else}}<a href="/outbound/log">{{template "icon-deliveries"}}Deliveries</a>{{end}}
|
||||
{{end}}
|
||||
{{if .IsGlobal}}
|
||||
{{if eq .Active "mail_queue"}}<span aria-current="page">{{template "icon-mail-queue"}}Mail queue</span>{{else}}<a href="/mail-queue">{{template "icon-mail-queue"}}Mail queue</a>{{end}}
|
||||
{{if eq .Active "system_log"}}<span aria-current="page">{{template "icon-system-log"}}System log</span>{{else}}<a href="/system-log">{{template "icon-system-log"}}System log</a>{{end}}
|
||||
{{if eq .Active "backup"}}<span aria-current="page">{{template "icon-backup"}}Backup</span>{{else}}<a href="/backup">{{template "icon-backup"}}Backup</a>{{end}}
|
||||
{{if eq .Active "users"}}<span aria-current="page">{{template "icon-users"}}Users</span>{{else}}<a href="/users">{{template "icon-users"}}Users</a>{{end}}
|
||||
{{if eq .Active "mail_queue"}}<span aria-current="page">{{template "icon-mail-queue"}}Mail queue</span>{{else}}<a href="/outbound/queue">{{template "icon-mail-queue"}}Mail queue</a>{{end}}
|
||||
{{if eq .Active "system_log"}}<span aria-current="page">{{template "icon-system-log"}}System log</span>{{else}}<a href="/server/log">{{template "icon-system-log"}}System log</a>{{end}}
|
||||
{{if eq .Active "backup"}}<span aria-current="page">{{template "icon-backup"}}Backup</span>{{else}}<a href="/server/backup">{{template "icon-backup"}}Backup</a>{{end}}
|
||||
{{if eq .Active "users"}}<span aria-current="page">{{template "icon-users"}}Users</span>{{else}}<a href="/server/users">{{template "icon-users"}}Users</a>{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="session">
|
||||
<span class="session-user muted">{{template "icon-account"}}User: {{.User}}</span>
|
||||
{{if eq .Active "settings"}}<span aria-current="page">{{template "icon-settings"}}Settings</span>{{else}}<a href="/settings">{{template "icon-settings"}}Settings</a>{{end}}
|
||||
{{/* Account is every user's own; Settings is the instance's and belongs
|
||||
to the global role. */}}
|
||||
{{if eq .Active "account"}}<span aria-current="page">{{template "icon-account"}}Account</span>{{else}}<a href="/account">{{template "icon-account"}}Account</a>{{end}}
|
||||
{{if .IsGlobal}}
|
||||
{{if eq .Active "settings"}}<span aria-current="page">{{template "icon-settings"}}Settings</span>{{else}}<a href="/server/settings">{{template "icon-settings"}}Settings</a>{{end}}
|
||||
{{end}}
|
||||
<form class="inline" method="post" action="/logout">
|
||||
<button type="submit" class="danger">{{template "icon-sign-out"}}Sign out</button>
|
||||
</form>
|
||||
@@ -116,7 +121,7 @@
|
||||
{{define "wide"}}{{end}}
|
||||
|
||||
{{/* back_link — up-navigation on drill-down pages. Invoke with the "back"
|
||||
function, e.g. {{template "back_link" (back "/users" "Back to users")}}.
|
||||
function, e.g. {{template "back_link" (back "/server/users" "Back to users")}}.
|
||||
Place it directly under the page <h1> and above the cards (see .back in
|
||||
legacy.css). TestDrillDownPagesPlaceBackLinkAboveContent guards this. */}}
|
||||
{{define "back_link"}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{define "mail_queue_body"}}
|
||||
<div id="mail-queue-body" data-poll aria-live="polite" hx-get="/mail-queue/body" hx-trigger="load" hx-swap="outerHTML">
|
||||
<div id="mail-queue-body" data-poll aria-live="polite" hx-get="/outbound/queue/fragment" hx-trigger="load" hx-swap="outerHTML">
|
||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||
<span class="code">{{if .Output}}{{.Output}}{{else}}Queue is empty.{{end}}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,86 +1,119 @@
|
||||
{{/* Wide enough for credentials and DMARC settings side by side (see .split
|
||||
in panel.css, as on a delivery's page). */}}
|
||||
{{/* One legacy template for two pages until stage 2 gives each its own:
|
||||
Account (GET /account, every role) — the user's name and e-mail, their
|
||||
password and their default DMARC report address, each its own form — and
|
||||
Server › Settings (GET /server/settings, global role) — what holds for the
|
||||
whole instance. .AccountPage says which one this is. */}}
|
||||
{{define "wide"}}wide{{end}}
|
||||
|
||||
{{/* The username/password fields are identical for a global administrator
|
||||
(split card, DMARC alongside) and a domain administrator (narrow card,
|
||||
no DMARC card) — only the surrounding form and card differ. */}}
|
||||
{{define "credentials_fields"}}
|
||||
<label for="username">Username</label>
|
||||
<input id="username" name="username" autocomplete="username"
|
||||
autocapitalize="none" spellcheck="false" value="{{.FormUsername}}" required>
|
||||
|
||||
<label for="current_password">Current password</label>
|
||||
<input id="current_password" name="current_password" type="password"
|
||||
autocomplete="current-password" required>
|
||||
|
||||
<label for="new_password">New password</label>
|
||||
<input id="new_password" name="new_password" type="password" autocomplete="new-password">
|
||||
|
||||
<label for="new_password_confirm">Confirm new password</label>
|
||||
<input id="new_password_confirm" name="new_password_confirm" type="password" autocomplete="new-password">
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<h1>Settings</h1>
|
||||
{{if .AccountPage}}
|
||||
<h1>Account</h1>
|
||||
|
||||
{{if .Flash}}<div class="flash">{{.Flash}}</div>{{end}}
|
||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||
|
||||
{{if .ShowDMARC}}
|
||||
<form method="post" action="/settings">
|
||||
<div class="split">
|
||||
<div class="card">
|
||||
<h2>Panel credentials</h2>
|
||||
<p class="muted">These are the credentials for this control panel only.
|
||||
Applications keep their own logins and passwords, which are not affected.</p>
|
||||
<div class="card" id="profile">
|
||||
<h2>Profile</h2>
|
||||
<p class="muted">Signed in as {{.User}} · {{.Role}}. These are the
|
||||
credentials for this control panel only. Applications keep their own logins
|
||||
and passwords, which are not affected.</p>
|
||||
<form method="post" action="/account/profile">
|
||||
<label for="username">Username</label>
|
||||
<input id="username" name="username" autocomplete="username"
|
||||
autocapitalize="none" spellcheck="false" value="{{.FormUsername}}" required>
|
||||
|
||||
{{template "credentials_fields" .}}
|
||||
<label for="email">E-mail</label>
|
||||
<input id="email" name="email" type="email" autocomplete="email"
|
||||
autocapitalize="none" spellcheck="false" value="{{.FormEmail}}">
|
||||
<p class="muted">Where the panel writes to you. It can also receive DMARC
|
||||
reports — your choice, under DMARC reports.</p>
|
||||
|
||||
<button type="submit">Save profile</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>DMARC aggregate reports</h2>
|
||||
<p class="muted">Optional default <code>rua=</code> address for every sending
|
||||
domain (can be overridden per domain).{{if .DMARCIngestEnabled}} When set to
|
||||
an address on <code>{{.HostedReportAddress}}</code>, SelfPost receives and
|
||||
parses aggregate reports on port 25.{{else}} Use a mailbox on a domain
|
||||
that receives inbound mail elsewhere.{{end}}</p>
|
||||
<div class="card" id="password">
|
||||
<h2>Password</h2>
|
||||
<form method="post" action="/account/password">
|
||||
<label for="current_password">Current password</label>
|
||||
<input id="current_password" name="current_password" type="password"
|
||||
autocomplete="current-password" required>
|
||||
|
||||
<label for="dmarc_report_email">Default report address</label>
|
||||
<input id="dmarc_report_email" name="dmarc_report_email" type="email"
|
||||
autocomplete="email" autocapitalize="none" spellcheck="false"
|
||||
value="{{.FormDMARCEmail}}" placeholder="{{if .DMARCIngestEnabled}}{{.HostedReportAddress}}{{else}}reports@your-mail-domain.com{{end}}">
|
||||
<label for="new_password">New password</label>
|
||||
<input id="new_password" name="new_password" type="password" autocomplete="new-password" required>
|
||||
|
||||
{{if .FormDMARCEmail}}
|
||||
<p class="muted">When <code>rua=</code> points at another domain, that hub
|
||||
domain must publish a report-authorisation record so receivers will deliver
|
||||
the XML aggregates.</p>
|
||||
<label for="new_password_confirm">Repeat</label>
|
||||
<input id="new_password_confirm" name="new_password_confirm" type="password" autocomplete="new-password" required>
|
||||
|
||||
<label>Report authorization — host / name</label>
|
||||
<div class="code-row">
|
||||
<span class="code">{{.ReportAuthName}}</span>
|
||||
<button type="button" class="copy">Copy</button>
|
||||
</div>
|
||||
|
||||
<label>Report authorization — value</label>
|
||||
<div class="code-row">
|
||||
<span class="code">{{.ReportAuthExample}}</span>
|
||||
<button type="button" class="copy">Copy</button>
|
||||
</div>
|
||||
|
||||
{{if .ReportAuthDNS.Status}}
|
||||
<label>Report authorization DNS <span class="st st-{{.ReportAuthDNS.Status}}">{{.ReportAuthDNS.Status}}</span></label>
|
||||
<p class="{{if eq .ReportAuthDNS.Status "ok"}}muted{{else}}error{{end}}">{{.ReportAuthDNS.Detail}}</p>
|
||||
{{if .ReportAuthDNS.Records}}<span class="code">{{range .ReportAuthDNS.Records}}{{.}}
|
||||
{{end}}</span>{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
<button type="submit">Change password</button>
|
||||
</form>
|
||||
<p class="muted">Changing it signs out every other session of this account;
|
||||
this one stays signed in.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="dmarc">
|
||||
<h2>DMARC reports</h2>
|
||||
<p class="muted">Your default report address. It is used by every outbound
|
||||
domain whose report address is set to follow your default; changing it
|
||||
changes the DMARC record those domains must publish.</p>
|
||||
<form method="post" action="/account/dmarc">
|
||||
<label for="dmarc_default">My default report address</label>
|
||||
<select id="dmarc_default" name="dmarc_default" data-custom-mode="custom">
|
||||
{{if .DMARCIngestEnabled}}<option value="hosted"{{if eq .FormDMARCMode "hosted"}} selected{{end}}>SelfPost hosted — read them under DMARC</option>{{end}}
|
||||
<option value="account"{{if eq .FormDMARCMode "account"}} selected{{end}}>My account e-mail{{if .AccountEmail}} — {{.AccountEmail}}{{end}}</option>
|
||||
<option value="custom"{{if eq .FormDMARCMode "custom"}} selected{{end}}>Another address…</option>
|
||||
<option value="none"{{if eq .FormDMARCMode "none"}} selected{{end}}>No reports</option>
|
||||
</select>
|
||||
<div data-custom-address>
|
||||
<label for="dmarc_default_address">Report address</label>
|
||||
<input id="dmarc_default_address" name="dmarc_default_address" type="email"
|
||||
autocomplete="off" autocapitalize="none" spellcheck="false"
|
||||
value="{{.FormDMARCAddress}}" placeholder="reports@your-mail-domain.com">
|
||||
</div>
|
||||
<button type="submit">Save report address</button>
|
||||
</form>
|
||||
|
||||
{{if .ReportAuthName}}
|
||||
<p class="muted">When <code>rua=</code> points at another domain, that hub
|
||||
domain must publish a report-authorisation record so receivers will deliver
|
||||
the XML aggregates.</p>
|
||||
|
||||
<label>Report authorization — host / name</label>
|
||||
<div class="code-row">
|
||||
<span class="code">{{.ReportAuthName}}</span>
|
||||
<button type="button" class="copy">Copy</button>
|
||||
</div>
|
||||
|
||||
<label>Report authorization — value</label>
|
||||
<div class="code-row">
|
||||
<span class="code">{{.ReportAuthExample}}</span>
|
||||
<button type="button" class="copy">Copy</button>
|
||||
</div>
|
||||
|
||||
{{if .ReportAuthDNS.Status}}
|
||||
<label>Report authorization DNS <span class="st st-{{.ReportAuthDNS.Status}}">{{.ReportAuthDNS.Status}}</span></label>
|
||||
<p class="{{if eq .ReportAuthDNS.Status "ok"}}muted{{else}}error{{end}}">{{.ReportAuthDNS.Detail}}</p>
|
||||
{{if .ReportAuthDNS.Records}}<span class="code">{{range .ReportAuthDNS.Records}}{{.}}
|
||||
{{end}}</span>{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{else}}
|
||||
<h1>Settings</h1>
|
||||
|
||||
<p class="muted">What holds for the whole instance, whoever is signed in. Your
|
||||
own name, e-mail, password and DMARC report address are under
|
||||
<a href="/account">Account</a>.</p>
|
||||
|
||||
{{if .Flash}}<div class="flash">{{.Flash}}</div>{{end}}
|
||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||
|
||||
<form method="post" action="/server/settings">
|
||||
<div class="card" id="deliveries-retention">
|
||||
<h2>Deliveries</h2>
|
||||
<p class="muted">How long rows on <a href="/deliveries">Deliveries</a> are
|
||||
<p class="muted">How long rows on <a href="/outbound/log">Deliveries</a> are
|
||||
kept before the background sweep deletes them — the main driver of
|
||||
<code>/data</code> growth over time. Does not affect <code>mail.log</code>
|
||||
rotation (fourteen daily files).</p>
|
||||
@@ -89,28 +122,9 @@
|
||||
<input id="send_log_retention_days" name="send_log_retention_days" type="number"
|
||||
min="7" max="365" step="1" required
|
||||
value="{{.FormSendLogRetentionDays}}">
|
||||
<button type="submit">Save settings</button>
|
||||
</div>
|
||||
|
||||
<button type="submit">Save changes</button>
|
||||
<p class="muted">Leave both new-password fields empty to change the username,
|
||||
DMARC address, or send log retention only. Changing the password signs out
|
||||
every other session; this one stays signed in.</p>
|
||||
</form>
|
||||
{{else}}
|
||||
<div class="card narrow">
|
||||
<h2>Panel credentials</h2>
|
||||
<p class="muted">These are the credentials for this control panel only.
|
||||
Applications keep their own logins and passwords, which are not affected.</p>
|
||||
<form method="post" action="/settings">
|
||||
{{template "credentials_fields" .}}
|
||||
|
||||
<button type="submit">Save changes</button>
|
||||
</form>
|
||||
<p class="muted">Leave both new-password fields empty to change the username
|
||||
only. Changing the password signs out every other session;
|
||||
this one stays signed in.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="card" id="rate-limits">
|
||||
<h2>Sending rate limits</h2>
|
||||
@@ -135,3 +149,4 @@ every other session; this one stays signed in.</p>
|
||||
allow-list.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
restored a backup, or the running configuration looks out of step with the
|
||||
domain and application lists. It does not touch the mail queue or the TLS
|
||||
certificate, and it is safe to run at any time.</p>
|
||||
<form class="inline" method="post" action="/reload">
|
||||
<form class="inline" method="post" action="/server/health/reload">
|
||||
<button type="submit">Reload configuration</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{define "status_body"}}
|
||||
<div id="status-body" data-poll aria-live="polite" hx-get="/status/fragment" hx-trigger="load" hx-swap="outerHTML">
|
||||
<div id="status-body" data-poll aria-live="polite" hx-get="/overview/fragment" hx-trigger="load" hx-swap="outerHTML">
|
||||
<div class="card" id="overall">
|
||||
<h2>Overall <span class="st st-{{.OverallStatus}}">{{.OverallStatus}}</span></h2>
|
||||
<p class="muted">{{.OverallHeading}}</p>
|
||||
@@ -77,7 +77,7 @@
|
||||
<p class="error">{{.QueueError}}</p>
|
||||
{{else}}
|
||||
<p>{{if .QueueSummary}}{{.QueueSummary}}{{else}}Mail queue is empty.{{end}}</p>
|
||||
<a class="btn" href="/mail-queue">View queue</a>
|
||||
<a class="btn" href="/outbound/queue">View queue</a>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
{{end}}</span>
|
||||
{{end}}
|
||||
|
||||
<form class="inline" method="post" action="/status/recheck">
|
||||
<form class="inline" method="post" action="/server/health/recheck">
|
||||
<button type="submit">Re-check DNS</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{define "system_log_body"}}
|
||||
<div id="system-log-body" data-poll aria-live="polite" hx-get="/system-log/body" hx-trigger="load" hx-swap="outerHTML">
|
||||
<div id="system-log-body" data-poll aria-live="polite" hx-get="/server/log/fragment" hx-trigger="load" hx-swap="outerHTML">
|
||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||
<span class="code">{{if .Lines}}{{range .Lines}}{{.}}
|
||||
{{end}}{{else}}No log lines yet.{{end}}</span>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{define "content"}}
|
||||
<h1>Delete {{.TargetUsername}}</h1>
|
||||
|
||||
{{template "back_link" (back (printf "/users/%d" .TargetID) (printf "Back to %s" .TargetUsername))}}
|
||||
{{template "back_link" (back (printf "/server/users/%d" .TargetID) (printf "Back to %s" .TargetUsername))}}
|
||||
|
||||
<div class="card narrow">
|
||||
<h2>Confirm deletion</h2>
|
||||
@@ -9,7 +9,7 @@
|
||||
This cannot be undone; a signed-in session for this user stops working
|
||||
immediately.</p>
|
||||
|
||||
<form method="post" action="/users/{{.TargetID}}/delete">
|
||||
<form method="post" action="/server/users/{{.TargetID}}/delete">
|
||||
<button type="submit" class="danger">Delete {{.TargetUsername}}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{{define "content"}}
|
||||
<h1>{{if .IsEdit}}Edit user{{else}}Create user{{end}}</h1>
|
||||
|
||||
{{template "back_link" (back "/users" "Back to users")}}
|
||||
{{template "back_link" (back "/server/users" "Back to users")}}
|
||||
|
||||
<div class="card narrow">
|
||||
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
|
||||
<form method="post" action="{{if .IsEdit}}/users/{{.UserID}}{{else}}/users/new{{end}}">
|
||||
<form method="post" action="{{if .IsEdit}}/server/users/{{.UserID}}{{else}}/server/users/new{{end}}">
|
||||
<label for="username">Username</label>
|
||||
<input id="username" name="username" autocomplete="username"
|
||||
autocapitalize="none" spellcheck="false" value="{{.FormUsername}}" required>
|
||||
@@ -62,7 +62,7 @@
|
||||
{{if .LastGlobalLocked}}
|
||||
<p class="muted">The only global administrator cannot be deleted.</p>
|
||||
{{else}}
|
||||
<a class="danger" href="/users/{{.UserID}}/delete">Delete user</a>
|
||||
<a class="danger" href="/server/users/{{.UserID}}/delete">Delete user</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{{if .Flash}}<div class="flash">{{.Flash}}</div>{{end}}
|
||||
|
||||
<div class="card">
|
||||
<p><a href="/users/new">Create user</a></p>
|
||||
<p><a href="/server/users/new">Create user</a></p>
|
||||
{{if .Users}}
|
||||
<table>
|
||||
<thead>
|
||||
@@ -17,7 +17,7 @@
|
||||
<td>{{.User.Role}}</td>
|
||||
<td class="muted">{{if or (eq .User.Role "global") .User.AllDomains}}All{{else}}{{range $i, $n := .DomainNames}}{{if $i}}, {{end}}{{$n}}{{end}}{{end}}</td>
|
||||
{{if $.InboundEnabled}}<td class="muted">{{if or (eq .User.Role "global") .User.AllInboundDomains}}All{{else}}{{range $i, $n := .InboundDomainNames}}{{if $i}}, {{end}}{{$n}}{{end}}{{end}}</td>{{end}}
|
||||
<td class="actions"><a href="/users/{{.User.ID}}">Edit</a></td>
|
||||
<td class="actions"><a href="/server/users/{{.User.ID}}">Edit</a></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user