build-base
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
{{define "title"}}IP Bans{{end}}
|
||||
{{define "content"}}
|
||||
<h1 class="text-xl font-bold text-white mb-6">IP Bans</h1>
|
||||
|
||||
<div class="card mb-6">
|
||||
<div class="text-sm font-semibold text-gray-300 mb-3">Add ban</div>
|
||||
<form method="POST" action="/admin/bans">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||||
<div style="display:grid;grid-template-columns:1fr 2fr 1fr auto;gap:.75rem;align-items:end">
|
||||
<div class="field" style="margin:0">
|
||||
<label>IP address</label>
|
||||
<input type="text" name="ip" required placeholder="192.168.1.1" maxlength="45">
|
||||
</div>
|
||||
<div class="field" style="margin:0">
|
||||
<label>Reason</label>
|
||||
<input type="text" name="reason" maxlength="255" placeholder="Manual ban">
|
||||
</div>
|
||||
<div class="field" style="margin:0">
|
||||
<label>Duration (hours, 0 = permanent)</label>
|
||||
<input type="number" name="hours" value="24" min="0" max="87600">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-danger" style="white-space:nowrap">Add ban</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card" style="padding:0;overflow:hidden">
|
||||
{{if .Bans}}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IP address</th>
|
||||
<th>Reason</th>
|
||||
<th>Banned at</th>
|
||||
<th>Expires</th>
|
||||
<th>Active</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Bans}}
|
||||
<tr>
|
||||
<td class="font-mono text-sm">{{.IP}}</td>
|
||||
<td class="text-gray-300 text-xs">{{.Reason}}</td>
|
||||
<td class="text-gray-400 text-xs">{{shortTime .BannedAt}}</td>
|
||||
<td class="text-gray-400 text-xs">
|
||||
{{if .ExpiresAt.Valid}}{{shortTime .ExpiresAt.Time}}{{else}}permanent{{end}}
|
||||
</td>
|
||||
<td>
|
||||
{{if .ExpiresAt.Valid}}
|
||||
<span class="badge badge-gray">timed</span>
|
||||
{{else}}
|
||||
<span class="badge badge-red">permanent</span>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
<form method="POST" action="/admin/bans/{{.IP}}/remove"
|
||||
onsubmit="return confirm('Remove ban on {{.IP}}?')">
|
||||
<input type="hidden" name="_csrf" value="{{$.CSRF}}">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Remove</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="p-8 text-center text-gray-500 text-sm">No IP bans.</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,66 @@
|
||||
{{define "base"}}<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{template "title" .}} — mailgosend admin</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>tailwind.config={darkMode:'class'}</script>
|
||||
<style>
|
||||
body{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
|
||||
.nav-link{display:block;padding:.375rem .625rem;border-radius:.375rem;font-size:.875rem;color:#9ca3af;transition:background .15s}
|
||||
.nav-link:hover{background:#374151;color:#f9fafb}
|
||||
.nav-link.active{background:#1d4ed8;color:#fff}
|
||||
.btn{display:inline-block;padding:.375rem .875rem;border-radius:.375rem;font-size:.875rem;font-weight:500;cursor:pointer;border:none;text-decoration:none}
|
||||
.btn-primary{background:#1d4ed8;color:#fff}.btn-primary:hover{background:#1e40af}
|
||||
.btn-danger{background:#dc2626;color:#fff}.btn-danger:hover{background:#b91c1c}
|
||||
.btn-sm{padding:.25rem .625rem;font-size:.75rem}
|
||||
.card{background:#1f2937;border-radius:.5rem;padding:1.25rem;margin-bottom:1rem}
|
||||
table{width:100%;border-collapse:collapse;font-size:.875rem}
|
||||
th{text-align:left;padding:.5rem .75rem;color:#9ca3af;font-weight:500;border-bottom:1px solid #374151;white-space:nowrap}
|
||||
td{padding:.5rem .75rem;border-bottom:1px solid #1f2937;vertical-align:top}
|
||||
tr:hover td{background:#1f2937}
|
||||
input,select,textarea{background:#374151;border:1px solid #4b5563;border-radius:.375rem;color:#f9fafb;padding:.375rem .625rem;font-size:.875rem;width:100%;box-sizing:border-box}
|
||||
input:focus,select:focus,textarea:focus{outline:none;border-color:#3b82f6}
|
||||
label{display:block;font-size:.75rem;color:#9ca3af;margin-bottom:.25rem}
|
||||
.field{margin-bottom:.875rem}
|
||||
.badge{display:inline-block;padding:.125rem .375rem;border-radius:.25rem;font-size:.7rem;font-weight:600}
|
||||
.badge-green{background:#065f46;color:#6ee7b7}
|
||||
.badge-red{background:#7f1d1d;color:#fca5a5}
|
||||
.badge-yellow{background:#78350f;color:#fcd34d}
|
||||
.badge-gray{background:#374151;color:#9ca3af}
|
||||
.flash-ok{background:#064e3b;border:1px solid #065f46;color:#6ee7b7;padding:.75rem 1rem;border-radius:.375rem;margin-bottom:1rem;font-size:.875rem}
|
||||
.flash-err{background:#7f1d1d;border:1px solid #991b1b;color:#fca5a5;padding:.75rem 1rem;border-radius:.375rem;margin-bottom:1rem;font-size:.875rem}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-900 text-gray-100 min-h-screen flex">
|
||||
|
||||
{{if .Admin}}
|
||||
<nav class="w-44 bg-gray-800 min-h-screen p-3 fixed flex flex-col" style="border-right:1px solid #374151">
|
||||
<div class="text-sm font-bold text-white mb-1 px-2 py-1">mailgosend</div>
|
||||
<div class="text-xs text-gray-500 px-2 mb-2">admin panel</div>
|
||||
<hr style="border-color:#374151;margin-bottom:.75rem">
|
||||
<a href="/admin/" class="nav-link">Dashboard</a>
|
||||
<a href="/admin/domains" class="nav-link">Domains</a>
|
||||
<a href="/admin/users" class="nav-link">Users</a>
|
||||
<a href="/admin/queue" class="nav-link">Delivery Queue</a>
|
||||
<a href="/admin/bans" class="nav-link">IP Bans</a>
|
||||
<a href="/admin/events" class="nav-link">Security Events</a>
|
||||
<div class="mt-auto pt-4 border-t" style="border-color:#374151">
|
||||
<div class="text-xs text-gray-500 px-2 mb-1">{{.Admin.Email}}</div>
|
||||
<a href="/admin/logout" class="nav-link text-xs">Logout</a>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="flex-1 p-6" style="margin-left:11rem;min-width:0">
|
||||
{{else}}
|
||||
<main class="flex-1 p-6">
|
||||
{{end}}
|
||||
|
||||
{{if .Flash}}<div class="flash-ok">{{.Flash}}</div>{{end}}
|
||||
{{if .Error}}<div class="flash-err">{{.Error}}</div>{{end}}
|
||||
|
||||
{{template "content" .}}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
@@ -0,0 +1,58 @@
|
||||
{{define "title"}}Dashboard{{end}}
|
||||
{{define "content"}}
|
||||
<h1 class="text-xl font-bold text-white mb-6">Dashboard</h1>
|
||||
|
||||
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:1rem;margin-bottom:2rem">
|
||||
<div class="card" style="margin:0;text-align:center">
|
||||
<div class="text-3xl font-bold text-blue-400">{{.Stats.TotalDomains}}</div>
|
||||
<div class="text-xs text-gray-400 mt-1">Domains</div>
|
||||
</div>
|
||||
<div class="card" style="margin:0;text-align:center">
|
||||
<div class="text-3xl font-bold text-blue-400">{{.Stats.TotalUsers}}</div>
|
||||
<div class="text-xs text-gray-400 mt-1">Users</div>
|
||||
</div>
|
||||
<div class="card" style="margin:0;text-align:center">
|
||||
<div class="text-3xl font-bold text-blue-400">{{.Stats.TotalMessages}}</div>
|
||||
<div class="text-xs text-gray-400 mt-1">Messages</div>
|
||||
</div>
|
||||
<div class="card" style="margin:0;text-align:center">
|
||||
<div class="text-3xl font-bold {{if gt .Stats.QueuePending 0}}text-yellow-400{{else}}text-green-400{{end}}">{{.Stats.QueuePending}}</div>
|
||||
<div class="text-xs text-gray-400 mt-1">Queue Pending</div>
|
||||
</div>
|
||||
<div class="card" style="margin:0;text-align:center">
|
||||
<div class="text-3xl font-bold {{if gt .Stats.QueueFailed 0}}text-red-400{{else}}text-green-400{{end}}">{{.Stats.QueueFailed}}</div>
|
||||
<div class="text-xs text-gray-400 mt-1">Queue Failed</div>
|
||||
</div>
|
||||
<div class="card" style="margin:0;text-align:center">
|
||||
<div class="text-3xl font-bold {{if gt .Stats.ActiveBans 0}}text-red-400{{else}}text-gray-400{{end}}">{{.Stats.ActiveBans}}</div>
|
||||
<div class="text-xs text-gray-400 mt-1">Active Bans</div>
|
||||
</div>
|
||||
<div class="card" style="margin:0;text-align:center">
|
||||
<div class="text-3xl font-bold {{if gt .Stats.RecentEvents 0}}text-yellow-400{{else}}text-gray-400{{end}}">{{.Stats.RecentEvents}}</div>
|
||||
<div class="text-xs text-gray-400 mt-1">Events (24h)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.5rem">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold text-gray-300 mb-2">Quick links</h2>
|
||||
<div class="card" style="padding:.75rem">
|
||||
<a href="/admin/domains" class="block py-2 px-3 rounded hover:bg-gray-700 text-sm text-blue-400">Manage domains</a>
|
||||
<a href="/admin/users" class="block py-2 px-3 rounded hover:bg-gray-700 text-sm text-blue-400">Manage users</a>
|
||||
<a href="/admin/queue" class="block py-2 px-3 rounded hover:bg-gray-700 text-sm text-blue-400">Inspect delivery queue</a>
|
||||
<a href="/admin/bans" class="block py-2 px-3 rounded hover:bg-gray-700 text-sm text-blue-400">IP ban list</a>
|
||||
<a href="/admin/events" class="block py-2 px-3 rounded hover:bg-gray-700 text-sm text-blue-400">Security events</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold text-gray-300 mb-2">System</h2>
|
||||
<div class="card" style="padding:.75rem">
|
||||
<div class="text-xs text-gray-400 space-y-1">
|
||||
<div>Send SIGHUP to reload TLS certificates without restart.</div>
|
||||
<div>Send SIGTERM/SIGINT for graceful shutdown (10s drain).</div>
|
||||
<div class="pt-1 text-gray-500">Queue worker polls every 30 seconds.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,146 @@
|
||||
{{define "title"}}Domain — {{.Domain.Name}}{{end}}
|
||||
{{define "content"}}
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<a href="/admin/domains" class="text-gray-400 text-sm hover:text-white">Domains</a>
|
||||
<span class="text-gray-600">/</span>
|
||||
<h1 class="text-xl font-bold text-white">{{.Domain.Name}}</h1>
|
||||
{{if .Domain.Enabled}}<span class="badge badge-green">enabled</span>{{else}}<span class="badge badge-red">disabled</span>{{end}}
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.5rem">
|
||||
|
||||
<!-- Left column -->
|
||||
<div>
|
||||
<!-- Toggle enable -->
|
||||
<div class="card">
|
||||
<div class="text-sm font-semibold text-gray-300 mb-3">Enable / Disable</div>
|
||||
<form method="POST" action="/admin/domains/{{.Domain.ID}}/enable">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||||
{{if .Domain.Enabled}}
|
||||
<input type="hidden" name="enabled" value="0">
|
||||
<button type="submit" class="btn btn-danger">Disable domain</button>
|
||||
{{else}}
|
||||
<input type="hidden" name="enabled" value="1">
|
||||
<button type="submit" class="btn btn-primary">Enable domain</button>
|
||||
{{end}}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Limits -->
|
||||
<div class="card">
|
||||
<div class="text-sm font-semibold text-gray-300 mb-3">Limits</div>
|
||||
<form method="POST" action="/admin/domains/{{.Domain.ID}}/limits">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||||
<div class="field">
|
||||
<label>Max users (0 = unlimited)</label>
|
||||
<input type="number" name="max_users" value="{{.Domain.MaxUsers}}" min="0" max="100000">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Max quota per user (MB, 0 = unlimited)</label>
|
||||
<input type="number" name="max_quota_mb" value="{{mb .Domain.MaxQuotaBytes}}" min="0" max="1048576">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Save limits</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Delete -->
|
||||
<div class="card" style="border:1px solid #7f1d1d">
|
||||
<div class="text-sm font-semibold text-red-400 mb-2">Delete domain</div>
|
||||
<div class="text-xs text-gray-400 mb-3">Permanently deletes the domain and ALL users and messages within it. This cannot be undone.</div>
|
||||
<form method="POST" action="/admin/domains/{{.Domain.ID}}/delete"
|
||||
onsubmit="return confirm('Delete domain {{.Domain.Name}} and all its data?')">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Delete domain</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right column -->
|
||||
<div>
|
||||
<!-- DKIM -->
|
||||
<div class="card">
|
||||
<div class="text-sm font-semibold text-gray-300 mb-3">DKIM key</div>
|
||||
{{if .Domain.DKIMPublic}}
|
||||
<div class="text-xs text-gray-400 mb-1">
|
||||
Selector: <span class="text-white">{{.Domain.DKIMSelector}}</span> /
|
||||
Algorithm: <span class="text-white">{{.Domain.DKIMAlgo}}</span>
|
||||
</div>
|
||||
<div class="text-xs text-gray-300 mb-3 font-semibold">DNS TXT record:</div>
|
||||
<div style="background:#111827;border-radius:.375rem;padding:.625rem;font-size:.7rem;color:#6ee7b7;word-break:break-all;margin-bottom:.875rem;line-height:1.6">{{.DKIMRecord}}</div>
|
||||
{{else}}
|
||||
<div class="text-xs text-yellow-400 mb-3">No DKIM key generated yet.</div>
|
||||
{{end}}
|
||||
<form method="POST" action="/admin/domains/{{.Domain.ID}}/dkim">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||||
<div class="field">
|
||||
<label>Algorithm for new key</label>
|
||||
<select name="algo">
|
||||
<option value="rsa2048" {{if eq .Domain.DKIMAlgo "rsa2048"}}selected{{end}}>RSA-2048</option>
|
||||
<option value="ed25519" {{if eq .Domain.DKIMAlgo "ed25519"}}selected{{end}}>Ed25519</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm"
|
||||
onclick="return !{{if .Domain.DKIMPublic}}confirm('Regenerate DKIM key? Old signatures become invalid.'){{else}}false{{end}}">
|
||||
{{if .Domain.DKIMPublic}}Regenerate DKIM key{{else}}Generate DKIM key{{end}}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- DNS hints -->
|
||||
<div class="card">
|
||||
<div class="text-sm font-semibold text-gray-300 mb-3">Recommended DNS records</div>
|
||||
<div class="text-xs text-gray-400 mb-1">SPF</div>
|
||||
<div style="background:#111827;border-radius:.375rem;padding:.5rem;font-size:.7rem;color:#93c5fd;word-break:break-all;margin-bottom:.75rem">{{.SPFHint}}</div>
|
||||
<div class="text-xs text-gray-400 mb-1">DMARC</div>
|
||||
<div style="background:#111827;border-radius:.375rem;padding:.5rem;font-size:.7rem;color:#93c5fd;word-break:break-all">{{.DMARCHint}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Users -->
|
||||
<div class="mt-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h2 class="text-sm font-semibold text-gray-300">Users ({{len .Users}})</h2>
|
||||
<a href="/admin/users" class="btn btn-primary btn-sm">Add user</a>
|
||||
</div>
|
||||
<div class="card" style="padding:0;overflow:hidden">
|
||||
{{if .Users}}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Display name</th>
|
||||
<th>Status</th>
|
||||
<th>Role</th>
|
||||
<th>Quota used</th>
|
||||
<th>Last login</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Users}}
|
||||
<tr>
|
||||
<td><a href="/admin/users/{{.ID}}" class="text-blue-400 hover:underline">{{.Email}}</a></td>
|
||||
<td class="text-gray-300">{{.DisplayName}}</td>
|
||||
<td>
|
||||
{{if .Enabled}}<span class="badge badge-green">active</span>
|
||||
{{else}}<span class="badge badge-red">disabled</span>{{end}}
|
||||
</td>
|
||||
<td>
|
||||
{{if .Admin}}<span class="badge badge-yellow">admin</span>
|
||||
{{else if .DomainAdmin}}<span class="badge badge-gray">domain admin</span>
|
||||
{{else}}<span class="badge badge-gray">user</span>{{end}}
|
||||
</td>
|
||||
<td class="text-gray-400 text-xs">{{humanBytes .UsedBytes}} / {{if .QuotaBytes}}{{humanBytes .QuotaBytes}}{{else}}unlimited{{end}}</td>
|
||||
<td class="text-gray-400 text-xs">{{if isZero .LastLogin}}never{{else}}{{shortTime .LastLogin}}{{end}}</td>
|
||||
<td><a href="/admin/users/{{.ID}}" class="btn btn-primary btn-sm">Edit</a></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="p-6 text-center text-gray-500 text-sm">No users in this domain.</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,71 @@
|
||||
{{define "title"}}Domains{{end}}
|
||||
{{define "content"}}
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-xl font-bold text-white">Domains</h1>
|
||||
</div>
|
||||
|
||||
<div class="card mb-6">
|
||||
<div class="text-sm font-semibold text-gray-300 mb-3">Add domain</div>
|
||||
<form method="POST" action="/admin/domains">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||||
<div style="display:grid;grid-template-columns:2fr 1fr 1fr auto;gap:.75rem;align-items:end">
|
||||
<div class="field" style="margin:0">
|
||||
<label>Domain name</label>
|
||||
<input type="text" name="name" placeholder="example.com" required maxlength="253"
|
||||
pattern="[a-z0-9][a-z0-9\-\.]{1,252}">
|
||||
</div>
|
||||
<div class="field" style="margin:0">
|
||||
<label>DKIM selector</label>
|
||||
<input type="text" name="selector" value="mail" maxlength="63" pattern="[a-zA-Z0-9_\-]+">
|
||||
</div>
|
||||
<div class="field" style="margin:0">
|
||||
<label>DKIM algorithm</label>
|
||||
<select name="algo">
|
||||
<option value="rsa2048">RSA-2048</option>
|
||||
<option value="ed25519">Ed25519</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" style="white-space:nowrap">Add domain</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card" style="padding:0;overflow:hidden">
|
||||
{{if .Domains}}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Domain</th>
|
||||
<th>Status</th>
|
||||
<th>DKIM</th>
|
||||
<th>Max users</th>
|
||||
<th>Max quota</th>
|
||||
<th>Created</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Domains}}
|
||||
<tr>
|
||||
<td><a href="/admin/domains/{{.ID}}" class="text-blue-400 hover:underline">{{.Name}}</a></td>
|
||||
<td>
|
||||
{{if .Enabled}}<span class="badge badge-green">enabled</span>
|
||||
{{else}}<span class="badge badge-red">disabled</span>{{end}}
|
||||
</td>
|
||||
<td>
|
||||
{{if .DKIMPublic}}<span class="badge badge-green">{{.DKIMSelector}} / {{.DKIMAlgo}}</span>
|
||||
{{else}}<span class="badge badge-yellow">no key</span>{{end}}
|
||||
</td>
|
||||
<td class="text-gray-400">{{if .MaxUsers}}{{.MaxUsers}}{{else}}unlimited{{end}}</td>
|
||||
<td class="text-gray-400">{{if .MaxQuotaBytes}}{{humanBytes .MaxQuotaBytes}}{{else}}unlimited{{end}}</td>
|
||||
<td class="text-gray-400 text-xs">{{shortTime .CreatedAt}}</td>
|
||||
<td><a href="/admin/domains/{{.ID}}" class="btn btn-primary btn-sm">Manage</a></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="p-8 text-center text-gray-500 text-sm">No domains configured yet.</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,46 @@
|
||||
{{define "title"}}Security Events{{end}}
|
||||
{{define "content"}}
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-xl font-bold text-white">Security Events</h1>
|
||||
<div class="flex gap-2">
|
||||
<a href="/admin/events?limit=100" class="btn btn-primary btn-sm">100</a>
|
||||
<a href="/admin/events?limit=200" class="btn btn-primary btn-sm">200</a>
|
||||
<a href="/admin/events?limit=500" class="btn btn-primary btn-sm">500</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="padding:0;overflow:hidden">
|
||||
{{if .Events}}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Type</th>
|
||||
<th>IP</th>
|
||||
<th>User ID</th>
|
||||
<th>Detail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Events}}
|
||||
<tr>
|
||||
<td class="text-gray-400 text-xs white-space-nowrap">{{shortTime .CreatedAt}}</td>
|
||||
<td>
|
||||
{{$t := .Type}}
|
||||
{{if eq $t "login_failed"}}<span class="badge badge-red">{{$t}}</span>
|
||||
{{else if eq $t "ip_banned"}}<span class="badge badge-red">{{$t}}</span>
|
||||
{{else if eq $t "login_ok"}}<span class="badge badge-green">{{$t}}</span>
|
||||
{{else}}<span class="badge badge-gray">{{$t}}</span>{{end}}
|
||||
</td>
|
||||
<td class="font-mono text-xs">{{.IP}}</td>
|
||||
<td class="text-gray-400 text-xs">{{if .UserID.Valid}}{{.UserID.Int64}}{{end}}</td>
|
||||
<td class="text-gray-300 text-xs">{{.Detail}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="p-8 text-center text-gray-500 text-sm">No security events recorded.</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,26 @@
|
||||
{{define "title"}}Login{{end}}
|
||||
{{define "content"}}
|
||||
<div class="flex items-center justify-center min-h-screen -mt-6">
|
||||
<div style="width:22rem">
|
||||
<div class="text-center mb-8">
|
||||
<div class="text-2xl font-bold text-white">mailgosend</div>
|
||||
<div class="text-gray-400 text-sm mt-1">Admin Panel</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<form method="POST" action="/admin/login" autocomplete="off">
|
||||
<div class="field">
|
||||
<label for="email">Email address</label>
|
||||
<input type="email" id="email" name="email" required maxlength="254"
|
||||
autocomplete="username" placeholder="admin@example.com">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required maxlength="1024"
|
||||
autocomplete="current-password">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-full mt-2" style="width:100%">Sign in</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,32 @@
|
||||
{{define "title"}}Two-Factor Authentication{{end}}
|
||||
{{define "content"}}
|
||||
<div class="flex items-center justify-center min-h-screen -mt-6">
|
||||
<div style="width:22rem">
|
||||
<div class="text-center mb-8">
|
||||
<div class="text-2xl font-bold text-white">mailgosend</div>
|
||||
<div class="text-gray-400 text-sm mt-1">Admin — Two-Factor Authentication</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
{{if .Error}}<div class="alert alert-error mb-4">{{.Error}}</div>{{end}}
|
||||
{{if .Flash}}<div class="alert alert-success mb-4">{{.Flash}}</div>{{end}}
|
||||
<p style="color:#9ca3af;font-size:.8125rem;margin-bottom:1.25rem">
|
||||
Enter the 6-digit code from your authenticator app, or an 8-character backup code.
|
||||
</p>
|
||||
<form method="POST" action="/admin/login/mfa" autocomplete="off">
|
||||
<div class="field">
|
||||
<label for="code">Authentication Code</label>
|
||||
<input type="text" id="code" name="code" required
|
||||
minlength="6" maxlength="64" autofocus
|
||||
autocomplete="one-time-code" inputmode="numeric"
|
||||
placeholder="000000"
|
||||
style="letter-spacing:.15em;text-align:center;font-size:1.25rem">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mt-2" style="width:100%">Verify</button>
|
||||
</form>
|
||||
<div class="text-center mt-4">
|
||||
<a href="/admin/login" style="font-size:.8rem;color:#4b5563">Back to login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,58 @@
|
||||
{{define "title"}}Delivery Queue{{end}}
|
||||
{{define "content"}}
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h1 class="text-xl font-bold text-white">Delivery Queue</h1>
|
||||
<a href="/admin/queue" class="btn btn-primary btn-sm">Refresh</a>
|
||||
</div>
|
||||
|
||||
<div class="card" style="padding:0;overflow:hidden">
|
||||
{{if .Entries}}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Status</th>
|
||||
<th>From</th>
|
||||
<th>To</th>
|
||||
<th>Attempts</th>
|
||||
<th>Next attempt</th>
|
||||
<th>Expires</th>
|
||||
<th>Last error</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Entries}}
|
||||
<tr>
|
||||
<td class="text-gray-400 text-xs">{{.ID}}</td>
|
||||
<td>
|
||||
{{if eq .Status "failed"}}<span class="badge badge-red">failed</span>
|
||||
{{else if eq .Status "pending"}}<span class="badge badge-yellow">pending</span>
|
||||
{{else}}<span class="badge badge-gray">{{.Status}}</span>{{end}}
|
||||
</td>
|
||||
<td class="text-xs">{{.FromAddr}}</td>
|
||||
<td class="text-xs">{{.ToAddr}}</td>
|
||||
<td class="text-gray-400 text-xs">{{.Attempts}}</td>
|
||||
<td class="text-gray-400 text-xs">{{shortTime .NextAttempt}}</td>
|
||||
<td class="text-gray-400 text-xs">{{shortTime .ExpiresAt}}</td>
|
||||
<td class="text-xs text-red-300" style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{.ErrorLog}}</td>
|
||||
<td>
|
||||
<form method="POST" action="/admin/queue/{{.ID}}/retry" style="display:inline">
|
||||
<input type="hidden" name="_csrf" value="{{$.CSRF}}">
|
||||
<button type="submit" class="btn btn-primary btn-sm">Retry</button>
|
||||
</form>
|
||||
<form method="POST" action="/admin/queue/{{.ID}}/delete" style="display:inline;margin-left:.25rem"
|
||||
onsubmit="return confirm('Delete queue entry?')">
|
||||
<input type="hidden" name="_csrf" value="{{$.CSRF}}">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="p-8 text-center text-gray-500 text-sm">Queue is empty.</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,86 @@
|
||||
{{define "title"}}User — {{.U.Email}}{{end}}
|
||||
{{define "content"}}
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<a href="/admin/users" class="text-gray-400 text-sm hover:text-white">Users</a>
|
||||
<span class="text-gray-600">/</span>
|
||||
<h1 class="text-xl font-bold text-white">{{.U.Email}}</h1>
|
||||
{{if .U.Enabled}}<span class="badge badge-green">active</span>{{else}}<span class="badge badge-red">disabled</span>{{end}}
|
||||
{{if .U.Admin}}<span class="badge badge-yellow">admin</span>{{else if .U.DomainAdmin}}<span class="badge badge-gray">domain admin</span>{{end}}
|
||||
</div>
|
||||
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1.5rem">
|
||||
|
||||
<!-- Edit user -->
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="text-sm font-semibold text-gray-300 mb-3">User settings</div>
|
||||
<form method="POST" action="/admin/users/{{.U.ID}}/update">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||||
<div class="field">
|
||||
<label>Display name</label>
|
||||
<input type="text" name="display_name" value="{{.U.DisplayName}}" maxlength="255">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Quota (MB)</label>
|
||||
<input type="number" name="quota_mb" value="{{mb .U.QuotaBytes}}" min="0" max="1048576">
|
||||
</div>
|
||||
<div style="display:flex;gap:1.5rem;margin-bottom:.875rem">
|
||||
<label style="display:flex;align-items:center;gap:.375rem;cursor:pointer;margin:0">
|
||||
<input type="checkbox" name="enabled" value="1" {{if .U.Enabled}}checked{{end}} style="width:auto">
|
||||
<span class="text-sm">Enabled</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:.375rem;cursor:pointer;margin:0">
|
||||
<input type="checkbox" name="admin" value="1" {{if .U.Admin}}checked{{end}} style="width:auto">
|
||||
<span class="text-sm">Global admin</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:.375rem;cursor:pointer;margin:0">
|
||||
<input type="checkbox" name="domain_admin" value="1" {{if .U.DomainAdmin}}checked{{end}} style="width:auto">
|
||||
<span class="text-sm">Domain admin</span>
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Save changes</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Change password -->
|
||||
<div class="card">
|
||||
<div class="text-sm font-semibold text-gray-300 mb-3">Change password</div>
|
||||
<form method="POST" action="/admin/users/{{.U.ID}}/password">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||||
<div class="field">
|
||||
<label>New password (min 8 characters)</label>
|
||||
<input type="password" name="password" required minlength="8" maxlength="1024">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Set password</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info + danger -->
|
||||
<div>
|
||||
<div class="card">
|
||||
<div class="text-sm font-semibold text-gray-300 mb-3">Account info</div>
|
||||
<div class="text-xs space-y-1.5 text-gray-400">
|
||||
<div>Email: <span class="text-white">{{.U.Email}}</span></div>
|
||||
<div>Domain: <span class="text-white">{{.U.DomainName}}</span></div>
|
||||
<div>Used: <span class="text-white">{{humanBytes .U.UsedBytes}}</span>
|
||||
of <span class="text-white">{{if .U.QuotaBytes}}{{humanBytes .U.QuotaBytes}}{{else}}unlimited{{end}}</span></div>
|
||||
<div>Created: <span class="text-white">{{shortTime .U.CreatedAt}}</span></div>
|
||||
<div>Last login: <span class="text-white">{{if isZero .U.LastLogin}}never{{else}}{{shortTime .U.LastLogin}}{{end}}</span></div>
|
||||
<div>MFA: <span class="text-white">{{if .U.MFAEnabled}}enabled{{else}}disabled{{end}}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" style="border:1px solid #7f1d1d">
|
||||
<div class="text-sm font-semibold text-red-400 mb-2">Delete user</div>
|
||||
<div class="text-xs text-gray-400 mb-3">Permanently deletes the user account, all mailboxes, and all messages. Cannot be undone.</div>
|
||||
<form method="POST" action="/admin/users/{{.U.ID}}/delete"
|
||||
onsubmit="return confirm('Delete user {{.U.Email}} and all their data?')">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Delete user</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,88 @@
|
||||
{{define "title"}}Users{{end}}
|
||||
{{define "content"}}
|
||||
<h1 class="text-xl font-bold text-white mb-6">Users</h1>
|
||||
|
||||
<div class="card mb-6">
|
||||
<div class="text-sm font-semibold text-gray-300 mb-3">Create user</div>
|
||||
<form method="POST" action="/admin/users">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||||
<div style="display:grid;grid-template-columns:2fr 1fr 1fr 1fr auto;gap:.75rem;align-items:end">
|
||||
<div class="field" style="margin:0">
|
||||
<label>Domain</label>
|
||||
<select name="domain_id" required>
|
||||
<option value="">Select domain...</option>
|
||||
{{range .Domains}}
|
||||
<option value="{{.ID}}">{{.Name}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="field" style="margin:0">
|
||||
<label>Username (local part)</label>
|
||||
<input type="text" name="username" required maxlength="64" placeholder="alice"
|
||||
pattern="[a-z0-9._\-]+">
|
||||
</div>
|
||||
<div class="field" style="margin:0">
|
||||
<label>Display name</label>
|
||||
<input type="text" name="display_name" maxlength="255" placeholder="Alice Smith">
|
||||
</div>
|
||||
<div class="field" style="margin:0">
|
||||
<label>Password</label>
|
||||
<input type="password" name="password" required minlength="8" maxlength="1024">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" style="white-space:nowrap">Create</button>
|
||||
</div>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:.75rem;margin-top:.75rem">
|
||||
<div class="field" style="margin:0">
|
||||
<label>Quota (MB, default 1024)</label>
|
||||
<input type="number" name="quota_mb" value="1024" min="0" max="1048576">
|
||||
</div>
|
||||
<div class="field" style="margin:0;display:flex;align-items:center;gap:.5rem;padding-top:1.25rem">
|
||||
<input type="checkbox" name="domain_admin" value="1" id="da" style="width:auto">
|
||||
<label for="da" style="margin:0;cursor:pointer">Domain admin</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card" style="padding:0;overflow:hidden">
|
||||
{{if .Users}}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Domain</th>
|
||||
<th>Display name</th>
|
||||
<th>Status</th>
|
||||
<th>Role</th>
|
||||
<th>Quota used</th>
|
||||
<th>Last login</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Users}}
|
||||
<tr>
|
||||
<td><a href="/admin/users/{{.ID}}" class="text-blue-400 hover:underline">{{.Email}}</a></td>
|
||||
<td class="text-gray-400 text-xs">{{.DomainName}}</td>
|
||||
<td class="text-gray-300">{{.DisplayName}}</td>
|
||||
<td>
|
||||
{{if .Enabled}}<span class="badge badge-green">active</span>
|
||||
{{else}}<span class="badge badge-red">disabled</span>{{end}}
|
||||
</td>
|
||||
<td>
|
||||
{{if .Admin}}<span class="badge badge-yellow">admin</span>
|
||||
{{else if .DomainAdmin}}<span class="badge badge-gray">domain admin</span>
|
||||
{{else}}<span class="badge badge-gray">user</span>{{end}}
|
||||
</td>
|
||||
<td class="text-gray-400 text-xs">{{humanBytes .UsedBytes}} / {{if .QuotaBytes}}{{humanBytes .QuotaBytes}}{{else}}unlimited{{end}}</td>
|
||||
<td class="text-gray-400 text-xs">{{if isZero .LastLogin}}never{{else}}{{shortTime .LastLogin}}{{end}}</td>
|
||||
<td><a href="/admin/users/{{.ID}}" class="btn btn-primary btn-sm">Edit</a></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="p-8 text-center text-gray-500 text-sm">No users found.</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,85 @@
|
||||
{{define "base"}}<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{template "title" .}} — mailgosend</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>tailwind.config={darkMode:'class'}</script>
|
||||
<style>
|
||||
body{font-family:ui-sans-serif,system-ui,sans-serif;font-size:14px}
|
||||
.sidebar{width:200px;min-width:200px;background:#111827;border-right:1px solid #1f2937;min-height:100vh;display:flex;flex-direction:column;padding:.75rem 0}
|
||||
.sidebar-item{display:flex;align-items:center;justify-content:space-between;padding:.375rem .875rem;border-radius:.25rem;margin:0 .375rem;color:#9ca3af;cursor:pointer;text-decoration:none;font-size:.8125rem}
|
||||
.sidebar-item:hover{background:#1f2937;color:#f9fafb}
|
||||
.sidebar-item.active{background:#1e3a5f;color:#93c5fd}
|
||||
.sidebar-section{font-size:.65rem;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:#4b5563;padding:.5rem .875rem .25rem;margin-top:.5rem}
|
||||
.unread-badge{background:#1d4ed8;color:#fff;font-size:.65rem;padding:.1rem .35rem;border-radius:.75rem;font-weight:600;min-width:1.1rem;text-align:center}
|
||||
.btn{display:inline-flex;align-items:center;padding:.375rem .875rem;border-radius:.375rem;font-size:.8125rem;font-weight:500;cursor:pointer;border:none;text-decoration:none;transition:background .15s}
|
||||
.btn-primary{background:#1d4ed8;color:#fff}.btn-primary:hover{background:#1e40af}
|
||||
.btn-danger{background:#dc2626;color:#fff}.btn-danger:hover{background:#b91c1c}
|
||||
.btn-ghost{background:transparent;color:#9ca3af;border:1px solid #374151}.btn-ghost:hover{background:#1f2937;color:#f9fafb}
|
||||
.btn-sm{padding:.25rem .625rem;font-size:.75rem}
|
||||
.msg-row{display:grid;align-items:center;padding:.5rem .875rem;border-bottom:1px solid #1f2937;cursor:pointer;gap:.5rem;grid-template-columns:auto 1fr auto auto}
|
||||
.msg-row:hover{background:#111827}
|
||||
.msg-row.unread{background:#0a1628}
|
||||
.msg-row.unread .msg-subject{font-weight:600;color:#f9fafb}
|
||||
.msg-from{font-size:.75rem;color:#6b7280;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:140px}
|
||||
.msg-subject{color:#d1d5db;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.msg-date{font-size:.7rem;color:#4b5563;white-space:nowrap}
|
||||
.flash-ok{background:#064e3b;border:1px solid #065f46;color:#6ee7b7;padding:.625rem .875rem;border-radius:.375rem;margin-bottom:.875rem;font-size:.8125rem}
|
||||
.flash-err{background:#7f1d1d;border:1px solid #991b1b;color:#fca5a5;padding:.625rem .875rem;border-radius:.375rem;margin-bottom:.875rem;font-size:.8125rem}
|
||||
input,select,textarea{background:#1f2937;border:1px solid #374151;border-radius:.375rem;color:#f9fafb;padding:.375rem .625rem;font-size:.875rem;width:100%;box-sizing:border-box}
|
||||
input:focus,select:focus,textarea:focus{outline:none;border-color:#3b82f6}
|
||||
label{display:block;font-size:.75rem;color:#6b7280;margin-bottom:.2rem}
|
||||
.field{margin-bottom:.75rem}
|
||||
.tag{display:inline-block;padding:.1rem .3rem;border-radius:.2rem;font-size:.65rem;font-weight:600}
|
||||
.tag-green{background:#064e3b;color:#6ee7b7}
|
||||
.tag-red{background:#7f1d1d;color:#fca5a5}
|
||||
.tag-blue{background:#1e3a5f;color:#93c5fd}
|
||||
.tag-gray{background:#1f2937;color:#6b7280}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-950 text-gray-300 flex h-screen overflow-hidden" style="background:#0d1117">
|
||||
|
||||
{{if .User}}
|
||||
<!-- Sidebar -->
|
||||
<div class="sidebar flex-shrink-0">
|
||||
<div style="padding:.5rem .875rem .75rem;border-bottom:1px solid #1f2937;margin-bottom:.25rem">
|
||||
<div style="font-size:.75rem;font-weight:600;color:#f9fafb">{{.User.DisplayName}}</div>
|
||||
<div style="font-size:.7rem;color:#4b5563">{{.User.Email}}</div>
|
||||
</div>
|
||||
|
||||
<a href="/compose" class="btn btn-primary btn-sm" style="margin:.5rem .625rem .75rem;display:block;text-align:center">Compose</a>
|
||||
|
||||
<div class="sidebar-section">Folders</div>
|
||||
{{range .Mailboxes}}
|
||||
<a href="/mail/{{.ID}}" class="sidebar-item {{if $.CurrentBoxID}}{{if eq $.CurrentBoxID .ID}}active{{end}}{{end}}">
|
||||
<span>{{if mailboxLabel .Type}}{{mailboxLabel .Type}}{{else}}{{.Name}}{{end}}</span>
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
<div style="margin-top:auto;padding:.75rem .875rem 0;border-top:1px solid #1f2937">
|
||||
<a href="/settings" class="sidebar-item" style="margin:0;padding:.25rem 0;font-size:.75rem">Settings</a>
|
||||
<a href="/logout" class="sidebar-item" style="margin:0;padding:.25rem 0;font-size:.75rem;color:#4b5563">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main -->
|
||||
<div style="flex:1;overflow:hidden;display:flex;flex-direction:column;min-width:0">
|
||||
{{if .Flash}}<div class="flash-ok" style="margin:.625rem .875rem 0;border-radius:.375rem">{{.Flash}}</div>{{end}}
|
||||
{{if .Error}}<div class="flash-err" style="margin:.625rem .875rem 0;border-radius:.375rem">{{.Error}}</div>{{end}}
|
||||
{{template "content" .}}
|
||||
</div>
|
||||
|
||||
{{else}}
|
||||
<!-- Public pages (login) -->
|
||||
<div style="flex:1;display:flex;align-items:center;justify-content:center">
|
||||
{{if .Flash}}<div class="flash-ok" style="position:fixed;top:1rem;left:50%;transform:translateX(-50%)">{{.Flash}}</div>{{end}}
|
||||
{{if .Error}}<div class="flash-err" style="position:fixed;top:1rem;left:50%;transform:translateX(-50%)">{{.Error}}</div>{{end}}
|
||||
{{template "content" .}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
@@ -0,0 +1,47 @@
|
||||
{{define "title"}}Compose{{end}}
|
||||
{{define "content"}}
|
||||
<div style="display:flex;align-items:center;gap:.5rem;padding:.5rem .875rem;border-bottom:1px solid #1f2937;background:#0d1117;flex-shrink:0">
|
||||
<span style="font-weight:600;color:#f9fafb;font-size:.875rem">New Message</span>
|
||||
<div style="flex:1"></div>
|
||||
<a href="javascript:history.back()" class="btn btn-ghost btn-sm">Discard</a>
|
||||
</div>
|
||||
|
||||
<div style="flex:1;overflow-y:auto;padding:1rem 1.25rem">
|
||||
<form method="POST" action="/compose">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||||
<input type="hidden" name="in_reply_to" value="{{.InReplyTo}}">
|
||||
<input type="hidden" name="references" value="{{.References}}">
|
||||
|
||||
<div class="field">
|
||||
<label for="to">To</label>
|
||||
<input type="text" id="to" name="to" required value="{{.To}}"
|
||||
placeholder="recipient@example.com, another@example.com"
|
||||
maxlength="2048">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="cc">CC</label>
|
||||
<input type="text" id="cc" name="cc" value="{{.CC}}"
|
||||
placeholder="optional" maxlength="2048">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="bcc">BCC</label>
|
||||
<input type="text" id="bcc" name="bcc" placeholder="optional" maxlength="2048">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="subject">Subject</label>
|
||||
<input type="text" id="subject" name="subject" value="{{.Subject}}"
|
||||
placeholder="Subject" maxlength="998">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="body">Message</label>
|
||||
<textarea id="body" name="body" rows="20"
|
||||
style="font-family:ui-monospace,monospace;font-size:.8125rem;resize:vertical;min-height:320px"
|
||||
placeholder="Write your message here...">{{.BodyText}}</textarea>
|
||||
</div>
|
||||
<div style="display:flex;gap:.5rem">
|
||||
<button type="submit" class="btn btn-primary">Send</button>
|
||||
<a href="javascript:history.back()" class="btn btn-ghost">Discard</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,25 @@
|
||||
{{define "title"}}Sign in{{end}}
|
||||
{{define "content"}}
|
||||
<div style="width:22rem">
|
||||
<div style="text-align:center;margin-bottom:2rem">
|
||||
<div style="font-size:1.5rem;font-weight:700;color:#f9fafb">mailgosend</div>
|
||||
<div style="font-size:.875rem;color:#4b5563;margin-top:.25rem">Sign in to your account</div>
|
||||
</div>
|
||||
<div style="background:#161b22;border:1px solid #21262d;border-radius:.5rem;padding:1.5rem">
|
||||
{{if .Error}}<div class="flash-err" style="margin-bottom:1rem">{{.Error}}</div>{{end}}
|
||||
<form method="POST" action="/login">
|
||||
<div class="field">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" required maxlength="254"
|
||||
autocomplete="username" autofocus placeholder="you@example.com">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required maxlength="1024"
|
||||
autocomplete="current-password">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" style="width:100%;justify-content:center;margin-top:.5rem">Sign in</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,67 @@
|
||||
{{define "title"}}{{if .CurrentBox}}{{.CurrentBox.Name}}{{else}}Mail{{end}}{{end}}
|
||||
{{define "content"}}
|
||||
<!-- Toolbar -->
|
||||
<div style="display:flex;align-items:center;gap:.5rem;padding:.5rem .875rem;border-bottom:1px solid #1f2937;background:#0d1117;flex-shrink:0">
|
||||
<span style="font-weight:600;color:#f9fafb;font-size:.875rem">{{if .CurrentBox}}{{.CurrentBox.Name}}{{end}}</span>
|
||||
<span style="color:#4b5563;font-size:.75rem;margin-left:.25rem">{{.TotalCount}} messages</span>
|
||||
<div style="flex:1"></div>
|
||||
<!-- Search -->
|
||||
<form method="GET" style="display:flex;gap:.375rem">
|
||||
<input type="hidden" name="boxid" value="{{if .CurrentBox}}{{.CurrentBox.ID}}{{end}}">
|
||||
<input type="search" name="q" value="{{.Query}}" placeholder="Search..." style="width:200px;font-size:.75rem;padding:.25rem .5rem">
|
||||
<button type="submit" class="btn btn-ghost btn-sm">Search</button>
|
||||
{{if .Query}}<a href="/mail/{{if .CurrentBox}}{{.CurrentBox.ID}}{{end}}" class="btn btn-ghost btn-sm">Clear</a>{{end}}
|
||||
</form>
|
||||
{{if .CurrentBox}}
|
||||
<form method="POST" action="/mail/{{.CurrentBox.ID}}/expunge">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||||
<button type="submit" class="btn btn-ghost btn-sm" onclick="return confirm('Permanently delete all messages marked for deletion?')">Expunge</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- Message list -->
|
||||
<div style="flex:1;overflow-y:auto">
|
||||
{{if .Messages}}
|
||||
{{range .Messages}}
|
||||
<a href="/mail/{{$.CurrentBox.ID}}/{{.UID}}" style="text-decoration:none;color:inherit;display:block"
|
||||
class="msg-row {{if not .IsRead}}unread{{end}}">
|
||||
<!-- Star -->
|
||||
<form method="POST" action="/mail/{{$.CurrentBox.ID}}/{{.UID}}/flag" style="display:contents">
|
||||
<input type="hidden" name="_csrf" value="{{$.CSRF}}">
|
||||
<input type="hidden" name="flag" value="star">
|
||||
<input type="hidden" name="return" value="/mail/{{$.CurrentBox.ID}}">
|
||||
<button type="submit" style="background:none;border:none;cursor:pointer;padding:0;font-size:.875rem;color:{{if .IsStarred}}#eab308{{else}}#374151{{end}}">
|
||||
{{if .IsStarred}}*{{else}}.{{end}}
|
||||
</button>
|
||||
</form>
|
||||
<!-- From + Subject -->
|
||||
<div style="min-width:0">
|
||||
<div class="msg-from">{{if .FromName}}{{truncate .FromName 30}}{{else}}{{.FromEmail}}{{end}}</div>
|
||||
<div class="msg-subject">{{if .Subject}}{{truncate .Subject 80}}{{else}}(no subject){{end}}</div>
|
||||
</div>
|
||||
<!-- Attachments + Date -->
|
||||
<div style="display:flex;align-items:center;gap:.375rem">
|
||||
{{if .HasAttachment}}<span class="tag tag-gray" style="font-size:.6rem">att</span>{{end}}
|
||||
</div>
|
||||
<div class="msg-date">{{shortDate .Date}}</div>
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
<!-- Pagination -->
|
||||
<div style="display:flex;justify-content:space-between;padding:.625rem .875rem;border-top:1px solid #1f2937">
|
||||
{{if .PrevPage}}
|
||||
<a href="/mail/{{.CurrentBox.ID}}?before={{.PrevPage}}" class="btn btn-ghost btn-sm">Newer</a>
|
||||
{{else}}<span></span>{{end}}
|
||||
{{if .NextPage}}
|
||||
<a href="/mail/{{.CurrentBox.ID}}?before={{.NextPage}}" class="btn btn-ghost btn-sm">Older</a>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{else}}
|
||||
<div style="display:flex;align-items:center;justify-content:center;height:60%;color:#374151;flex-direction:column;gap:.5rem">
|
||||
<div style="font-size:.875rem">{{if .Query}}No messages match your search.{{else}}No messages.{{end}}</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,86 @@
|
||||
{{define "title"}}{{if .Message}}{{.Message.Subject}}{{else}}Message{{end}}{{end}}
|
||||
{{define "content"}}
|
||||
<!-- Message toolbar -->
|
||||
<div style="display:flex;align-items:center;gap:.5rem;padding:.5rem .875rem;border-bottom:1px solid #1f2937;background:#0d1117;flex-shrink:0">
|
||||
<a href="/mail/{{.CurrentBox.ID}}" class="btn btn-ghost btn-sm">Back</a>
|
||||
<div style="flex:1"></div>
|
||||
|
||||
<!-- Prev / Next -->
|
||||
{{if .NextUID}}<a href="/mail/{{.CurrentBox.ID}}/{{.NextUID}}" class="btn btn-ghost btn-sm">Newer</a>{{end}}
|
||||
{{if .PrevUID}}<a href="/mail/{{.CurrentBox.ID}}/{{.PrevUID}}" class="btn btn-ghost btn-sm">Older</a>{{end}}
|
||||
|
||||
<!-- Actions -->
|
||||
<a href="/compose?action=reply&boxid={{.CurrentBox.ID}}&uid={{.Message.UID}}" class="btn btn-primary btn-sm">Reply</a>
|
||||
<a href="/compose?action=forward&boxid={{.CurrentBox.ID}}&uid={{.Message.UID}}" class="btn btn-ghost btn-sm">Forward</a>
|
||||
|
||||
<form method="POST" action="/mail/{{.CurrentBox.ID}}/{{.Message.UID}}/trash">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||||
<button type="submit" class="btn btn-danger btn-sm">Delete</button>
|
||||
</form>
|
||||
|
||||
<!-- Star toggle -->
|
||||
<form method="POST" action="/mail/{{.CurrentBox.ID}}/{{.Message.UID}}/flag">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||||
<input type="hidden" name="flag" value="star">
|
||||
<button type="submit" class="btn btn-ghost btn-sm">{{if .Message.IsStarred}}Unstar{{else}}Star{{end}}</button>
|
||||
</form>
|
||||
|
||||
<!-- Move to folder -->
|
||||
{{if $.Mailboxes}}
|
||||
<form method="POST" action="/mail/{{.CurrentBox.ID}}/{{.Message.UID}}/move" style="display:flex;gap:.25rem">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||||
<select name="dest_box" style="font-size:.75rem;padding:.2rem .4rem;width:auto">
|
||||
{{range $.Mailboxes}}
|
||||
{{if ne .ID $.CurrentBox.ID}}
|
||||
<option value="{{.ID}}">{{if mailboxLabel .Type}}{{mailboxLabel .Type}}{{else}}{{.Name}}{{end}}</option>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</select>
|
||||
<button type="submit" class="btn btn-ghost btn-sm">Move</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- Message header -->
|
||||
<div style="padding:.875rem 1.25rem;border-bottom:1px solid #1f2937;background:#0d1117;flex-shrink:0">
|
||||
<h1 style="font-size:1.0625rem;font-weight:600;color:#f9fafb;margin:0 0 .625rem">
|
||||
{{if .Message.Subject}}{{.Message.Subject}}{{else}}(no subject){{end}}
|
||||
</h1>
|
||||
<div style="display:grid;grid-template-columns:auto 1fr;gap:.2rem .625rem;font-size:.8125rem">
|
||||
<span style="color:#4b5563">From</span>
|
||||
<span style="color:#d1d5db">{{if .Message.FromName}}{{.Message.FromName}} <{{.Message.FromEmail}}>{{else}}{{.Message.FromEmail}}{{end}}</span>
|
||||
<span style="color:#4b5563">To</span>
|
||||
<span style="color:#d1d5db">{{.Message.ToList}}</span>
|
||||
<span style="color:#4b5563">Date</span>
|
||||
<span style="color:#d1d5db">{{shortTime .Message.Date}}</span>
|
||||
{{if .Message.MessageID}}
|
||||
<span style="color:#4b5563">Message-ID</span>
|
||||
<span style="color:#4b5563;font-size:.7rem;font-family:monospace">{{.Message.MessageID}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
{{if .Body.Attachments}}
|
||||
<div style="margin-top:.625rem;display:flex;gap:.375rem;flex-wrap:wrap">
|
||||
{{range .Body.Attachments}}
|
||||
<span class="tag tag-blue">{{if .Filename}}{{truncate .Filename 40}}{{else}}{{.ContentType}}{{end}}</span>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<!-- Message body -->
|
||||
<div style="flex:1;overflow-y:auto">
|
||||
{{if .Body.HTML}}
|
||||
<iframe sandbox="allow-same-origin"
|
||||
srcdoc="{{.Body.HTML}}"
|
||||
style="width:100%;min-height:480px;border:none;background:#fff"
|
||||
onload="this.style.height=(this.contentDocument.documentElement.scrollHeight+32)+'px'">
|
||||
</iframe>
|
||||
{{else if .Body.Text}}
|
||||
<div style="padding:1.25rem;background:#0d1117">
|
||||
<pre style="font-family:ui-monospace,monospace;font-size:.8125rem;line-height:1.6;color:#d1d5db;white-space:pre-wrap;word-break:break-word;margin:0">{{.Body.Text}}</pre>
|
||||
</div>
|
||||
{{else}}
|
||||
<div style="padding:1.25rem;color:#4b5563;font-size:.875rem">(no content)</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,31 @@
|
||||
{{define "title"}}Two-Factor Authentication{{end}}
|
||||
{{define "content"}}
|
||||
<div style="width:22rem">
|
||||
<div style="text-align:center;margin-bottom:2rem">
|
||||
<div style="font-size:1.5rem;font-weight:700;color:#f9fafb">mailgosend</div>
|
||||
<div style="font-size:.875rem;color:#4b5563;margin-top:.25rem">Two-factor authentication required</div>
|
||||
</div>
|
||||
<div style="background:#161b22;border:1px solid #21262d;border-radius:.5rem;padding:1.5rem">
|
||||
{{if .Error}}<div class="flash-err" style="margin-bottom:1rem">{{.Error}}</div>{{end}}
|
||||
{{if .Flash}}<div class="flash-ok" style="margin-bottom:1rem">{{.Flash}}</div>{{end}}
|
||||
<p style="color:#9ca3af;font-size:.8125rem;margin-bottom:1.25rem">
|
||||
Enter the 6-digit code from your authenticator app, or an 8-character backup code.
|
||||
</p>
|
||||
<form method="POST" action="/login/mfa">
|
||||
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
||||
<div class="field">
|
||||
<label for="code">Authentication Code</label>
|
||||
<input type="text" id="code" name="code" required
|
||||
minlength="6" maxlength="64"
|
||||
autocomplete="one-time-code" autofocus
|
||||
inputmode="numeric" placeholder="000000"
|
||||
style="letter-spacing:.15em;text-align:center;font-size:1.25rem">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" style="width:100%;justify-content:center;margin-top:.5rem">Verify</button>
|
||||
</form>
|
||||
<div style="margin-top:1rem;text-align:center">
|
||||
<a href="/login" style="font-size:.8rem;color:#4b5563">Back to login</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,50 @@
|
||||
{{define "title"}}Set Up Two-Factor Authentication{{end}}
|
||||
{{define "content"}}
|
||||
<div style="max-width:32rem;width:100%">
|
||||
<div style="margin-bottom:1.25rem">
|
||||
<h1 style="font-size:1.125rem;font-weight:600;color:#f9fafb">Set Up Two-Factor Authentication</h1>
|
||||
<p style="color:#6b7280;font-size:.8125rem;margin-top:.25rem">Scan the QR code with your authenticator app, or enter the secret manually.</p>
|
||||
</div>
|
||||
|
||||
{{if .Error}}<div class="flash-err" style="margin-bottom:1rem">{{.Error}}</div>{{end}}
|
||||
{{if .Flash}}<div class="flash-ok" style="margin-bottom:1rem">{{.Flash}}</div>{{end}}
|
||||
|
||||
<div style="background:#161b22;border:1px solid #21262d;border-radius:.5rem;padding:1.5rem;margin-bottom:1.25rem">
|
||||
<h2 style="font-size:.875rem;font-weight:600;color:#d1d5db;margin-bottom:.875rem">Step 1 — Add to authenticator app</h2>
|
||||
|
||||
<!-- QR code via Google Charts API (no JS required) -->
|
||||
<div style="text-align:center;margin-bottom:1rem">
|
||||
<img src="https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl={{urlquery .OTPAuthURI}}&choe=UTF-8"
|
||||
alt="QR code for TOTP enrollment"
|
||||
style="border:6px solid #fff;border-radius:.25rem;display:inline-block"
|
||||
width="200" height="200">
|
||||
</div>
|
||||
|
||||
<p style="color:#6b7280;font-size:.75rem;margin-bottom:.5rem">Can't scan? Enter this secret manually in your app:</p>
|
||||
<div style="background:#0d1117;border:1px solid #30363d;border-radius:.25rem;padding:.625rem .875rem;font-family:monospace;font-size:.875rem;color:#58a6ff;word-break:break-all;letter-spacing:.1em">
|
||||
{{.Secret}}
|
||||
</div>
|
||||
<p style="color:#4b5563;font-size:.7rem;margin-top:.5rem">Algorithm: SHA-1 | Digits: 6 | Period: 30 seconds</p>
|
||||
</div>
|
||||
|
||||
<div style="background:#161b22;border:1px solid #21262d;border-radius:.5rem;padding:1.5rem">
|
||||
<h2 style="font-size:.875rem;font-weight:600;color:#d1d5db;margin-bottom:.875rem">Step 2 — Verify and enable</h2>
|
||||
<p style="color:#6b7280;font-size:.8125rem;margin-bottom:1rem">Enter the 6-digit code shown in your authenticator app to confirm setup.</p>
|
||||
<form method="POST" action="/settings/mfa/enroll">
|
||||
<input type="hidden" name="csrf" value="{{.CSRF}}">
|
||||
<div class="field">
|
||||
<label for="code">Verification Code</label>
|
||||
<input type="text" id="code" name="code" required
|
||||
minlength="6" maxlength="6" inputmode="numeric"
|
||||
autocomplete="one-time-code" autofocus
|
||||
placeholder="000000"
|
||||
style="letter-spacing:.2em;text-align:center;font-size:1.25rem">
|
||||
</div>
|
||||
<div style="display:flex;gap:.75rem;margin-top:.875rem">
|
||||
<button type="submit" class="btn btn-primary" style="flex:1;justify-content:center">Enable Two-Factor Auth</button>
|
||||
<a href="/settings" class="btn btn-ghost" style="flex:0 0 auto">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,82 @@
|
||||
{{define "title"}}Settings{{end}}
|
||||
{{define "content"}}
|
||||
<div style="padding:.5rem .875rem;border-bottom:1px solid #1f2937;background:#0d1117;flex-shrink:0">
|
||||
<span style="font-weight:600;color:#f9fafb;font-size:.875rem">Settings</span>
|
||||
</div>
|
||||
|
||||
<div style="flex:1;overflow-y:auto;padding:1.25rem;max-width:40rem">
|
||||
|
||||
<!-- Account info -->
|
||||
<div style="background:#161b22;border:1px solid #21262d;border-radius:.5rem;padding:1rem;margin-bottom:1rem">
|
||||
<div style="font-size:.75rem;font-weight:600;color:#6b7280;text-transform:uppercase;margin-bottom:.75rem">Account</div>
|
||||
<div style="font-size:.8125rem;color:#9ca3af;display:grid;grid-template-columns:auto 1fr;gap:.3rem .75rem">
|
||||
<span>Email</span><span style="color:#d1d5db">{{.AccountUser.Email}}</span>
|
||||
<span>Storage used</span><span style="color:#d1d5db">{{humanBytes .AccountUser.UsedBytes}} of {{if .AccountUser.QuotaBytes}}{{humanBytes .AccountUser.QuotaBytes}}{{else}}unlimited{{end}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Display name -->
|
||||
<div style="background:#161b22;border:1px solid #21262d;border-radius:.5rem;padding:1rem;margin-bottom:1rem">
|
||||
<div style="font-size:.75rem;font-weight:600;color:#6b7280;text-transform:uppercase;margin-bottom:.75rem">Display Name</div>
|
||||
<form method="POST" action="/settings/display">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||||
<div class="field">
|
||||
<label for="display_name">Display name shown in sent emails</label>
|
||||
<input type="text" id="display_name" name="display_name"
|
||||
value="{{.AccountUser.DisplayName}}" maxlength="255">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Change password -->
|
||||
<div style="background:#161b22;border:1px solid #21262d;border-radius:.5rem;padding:1rem;margin-bottom:1rem">
|
||||
<div style="font-size:.75rem;font-weight:600;color:#6b7280;text-transform:uppercase;margin-bottom:.75rem">Change Password</div>
|
||||
<form method="POST" action="/settings/password">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||||
<div class="field">
|
||||
<label>Current password</label>
|
||||
<input type="password" name="current_password" required maxlength="1024"
|
||||
autocomplete="current-password">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>New password (min 8 characters)</label>
|
||||
<input type="password" name="new_password" required minlength="8" maxlength="1024"
|
||||
autocomplete="new-password">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>Confirm new password</label>
|
||||
<input type="password" name="confirm_password" required maxlength="1024"
|
||||
autocomplete="new-password">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-sm"
|
||||
onclick="return confirm('Change your password? All other sessions will be logged out.')">
|
||||
Change password
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Two-factor authentication -->
|
||||
<div style="background:#161b22;border:1px solid #21262d;border-radius:.5rem;padding:1rem">
|
||||
<div style="font-size:.75rem;font-weight:600;color:#6b7280;text-transform:uppercase;margin-bottom:.75rem">Two-Factor Authentication</div>
|
||||
{{if .AccountUser.MFAEnabled}}
|
||||
<p style="color:#6ee7b7;font-size:.8125rem;margin-bottom:.875rem">Two-factor authentication is enabled on your account.</p>
|
||||
<form method="POST" action="/settings/mfa/disable"
|
||||
onsubmit="return confirm('Disable two-factor authentication? This reduces your account security.')">
|
||||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||||
<div class="field">
|
||||
<label>Current password (required to disable)</label>
|
||||
<input type="password" name="password" required maxlength="1024" autocomplete="current-password">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-danger btn-sm">Disable Two-Factor Auth</button>
|
||||
</form>
|
||||
{{else}}
|
||||
<p style="color:#9ca3af;font-size:.8125rem;margin-bottom:.875rem">
|
||||
Two-factor authentication is not enabled. Adding it significantly improves your account security.
|
||||
</p>
|
||||
<a href="/settings/mfa/enroll" class="btn btn-primary btn-sm">Set Up Two-Factor Auth</a>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user