feat(web): add embedded localized sync console
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
:root { color-scheme: dark; --bg:#10151a; --surface:#172128; --surface-2:#1e2b33; --line:#2b3b45; --text:#e5eef1; --muted:#9bb0b8; --accent:#4fd1b5; --accent-ink:#08231f; --danger:#f08080; --focus:#8ee7d5; }
|
||||
* { box-sizing:border-box; }
|
||||
body { min-width:320px; margin:0; background:var(--bg); color:var(--text); font:15px/1.5 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
|
||||
.site-header,.site-footer { display:flex; align-items:center; justify-content:space-between; gap:1rem; padding:1rem clamp(1rem,4vw,4rem); border-bottom:1px solid var(--line); }
|
||||
.site-footer { border-top:1px solid var(--line); border-bottom:0; color:var(--muted); font-size:.875rem; }
|
||||
.brand { display:inline-flex; align-items:center; gap:.65rem; color:var(--text); font-weight:700; text-decoration:none; }
|
||||
.site-nav { display:flex; gap:.9rem; }
|
||||
.site-nav a { color:var(--muted); text-decoration:none; }
|
||||
.site-nav a:hover,.site-nav a:focus-visible { color:var(--accent); }
|
||||
.locale-form { margin-left:auto; }
|
||||
.locale-form select { min-height:34px; padding:.3rem .55rem; border:1px solid var(--line); border-radius:7px; background:var(--surface-2); color:var(--text); }
|
||||
.page-shell { width:min(100% - 2rem,1100px); min-height:calc(100vh - 150px); margin:0 auto; padding:clamp(2rem,6vw,5rem) 0; }
|
||||
.card { border:1px solid var(--line); border-radius:16px; background:linear-gradient(145deg,var(--surface),#142027); box-shadow:0 14px 40px #0004; }
|
||||
.hero { max-width:720px; padding:clamp(2rem,7vw,5rem); }
|
||||
.auth-card,.message-card { width:min(100%,480px); margin:0 auto; padding:2rem; }
|
||||
.eyebrow { margin:0; color:var(--accent); font-size:.8rem; font-weight:700; letter-spacing:.12em; text-transform:uppercase; }
|
||||
.danger-text { color:var(--danger); }
|
||||
.hero h1 { max-width:14ch; margin:.6rem 0; font-size:clamp(2.2rem,6vw,4.6rem); line-height:1.05; }
|
||||
.lead { max-width:58ch; color:var(--muted); font-size:1.1rem; }
|
||||
.actions { display:flex; flex-wrap:wrap; gap:.75rem; margin-top:2rem; }
|
||||
.button { display:inline-flex; align-items:center; justify-content:center; min-height:42px; padding:.6rem 1rem; border:1px solid transparent; border-radius:8px; font:inherit; font-weight:650; text-decoration:none; cursor:pointer; }
|
||||
.button.primary { background:var(--accent); color:var(--accent-ink); }
|
||||
.button.secondary { border-color:var(--line); background:transparent; color:var(--text); }
|
||||
.button.danger { background:#4c2529; color:#ffd9d9; }
|
||||
.button:hover { filter:brightness(1.08); }
|
||||
:focus-visible { outline:3px solid var(--focus); outline-offset:3px; }
|
||||
.stack { display:grid; gap:1rem; }
|
||||
.stack label { display:grid; gap:.35rem; font-weight:650; }
|
||||
.stack input,.inline-form input { width:100%; min-height:42px; padding:.55rem .65rem; border:1px solid var(--line); border-radius:8px; background:#0d151a; color:var(--text); font:inherit; }
|
||||
.flash { padding:.75rem; border-radius:8px; }
|
||||
.flash.error { border:1px solid #6e343b; background:#3a2025; color:#ffd9d9; }
|
||||
.dashboard-head { display:flex; align-items:flex-start; justify-content:space-between; gap:1rem; margin-bottom:1.25rem; }
|
||||
.table-card { padding:1.25rem; }
|
||||
.table-scroll { overflow-x:auto; }
|
||||
table { width:100%; border-collapse:collapse; }
|
||||
th,td { padding:.75rem; border-bottom:1px solid var(--line); text-align:left; vertical-align:middle; }
|
||||
th { color:var(--muted); font-size:.8rem; letter-spacing:.06em; text-transform:uppercase; }
|
||||
.badge { display:inline-flex; padding:.2rem .55rem; border-radius:999px; font-size:.8rem; font-weight:700; }
|
||||
.badge.ok { background:#173d36; color:#91ecd9; }.badge.danger { background:#4c2529; color:#ffd9d9; }
|
||||
.inline-form { display:flex; min-width:250px; gap:.4rem; }.inline-form input { min-width:120px; }
|
||||
.link-button { padding:0; border:0; background:transparent; color:var(--muted); font:inherit; cursor:pointer; }
|
||||
.admin-shell { display:grid; grid-template-columns:220px minmax(0,1fr); gap:1.5rem; }
|
||||
.admin-nav { display:grid; align-content:start; gap:.35rem; padding:1rem; border:1px solid var(--line); border-radius:12px; background:var(--surface); }
|
||||
.admin-nav a { padding:.55rem .65rem; border-radius:7px; color:var(--muted); text-decoration:none; }.admin-nav a[aria-current="page"],.admin-nav a:hover { background:var(--surface-2); color:var(--accent); }
|
||||
.admin-content h1 { margin-top:0; }.panel { padding:1.25rem; }.section-heading { display:flex; align-items:center; justify-content:space-between; gap:1rem; margin-bottom:1rem; }
|
||||
.stat-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:1rem; margin:1rem 0; }.stat { display:grid; gap:.35rem; padding:1.2rem; }.stat strong { color:var(--accent); font-size:1.8rem; }.stat span { color:var(--muted); }
|
||||
.details { display:grid; grid-template-columns:minmax(150px,auto) 1fr; gap:.6rem 1rem; }.details dt { color:var(--muted); }.details dd { margin:0; overflow-wrap:anywhere; }
|
||||
details { margin-top:.6rem; } summary { cursor:pointer; color:var(--accent); }.compact { margin-top:.7rem; }.compact input { min-height:36px; }
|
||||
.mono { font-family:ui-monospace,SFMono-Regular,Menlo,monospace; }.muted,.empty { color:var(--muted); }.empty { padding:2rem; text-align:center; }
|
||||
.sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
|
||||
@media (max-width:760px) { .site-header,.site-footer,.dashboard-head,.section-heading { align-items:flex-start; flex-direction:column; }.locale-form { margin-left:0; }.page-shell { width:min(100% - 1.25rem,1100px); padding:2rem 0; }.hero { padding:1.5rem; }.inline-form { min-width:0; flex-wrap:wrap; }.admin-shell { grid-template-columns:1fr; }.admin-nav { grid-template-columns:repeat(2,minmax(0,1fr)); }.admin-nav .eyebrow { grid-column:1 / -1; } }
|
||||
@@ -0,0 +1,7 @@
|
||||
document.addEventListener("change", function (event) {
|
||||
if (event.target.matches("[data-auto-submit]")) event.target.form.requestSubmit();
|
||||
});
|
||||
document.addEventListener("click", function (event) {
|
||||
const button = event.target.closest("[data-confirm]");
|
||||
if (button && !window.confirm(button.dataset.confirm)) event.preventDefault();
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none"><path fill="#4fd1b5" d="M8 10h48v12H8zM14 26h36v12H14zM20 42h24v12H20z"/><path stroke="#e5eef1" stroke-width="4" d="M10 8h44v48H10z"/></svg>
|
||||
|
After Width: | Height: | Size: 213 B |
@@ -0,0 +1,27 @@
|
||||
{{define "admin"}}{{template "layout" .}}{{end}}
|
||||
{{define "content"}}
|
||||
<section class="admin-shell">
|
||||
<aside class="admin-nav" aria-label="{{t .Locale "admin.navigation"}}">
|
||||
<p class="eyebrow">{{t .Locale "admin.eyebrow"}}</p>
|
||||
<a href="/admin/dashboard" {{if eq .AdminPage "dashboard"}}aria-current="page"{{end}}>{{t .Locale "admin.overview"}}</a>
|
||||
<a href="/admin/users" {{if eq .AdminPage "users"}}aria-current="page"{{end}}>{{t .Locale "admin.users"}}</a>
|
||||
<a href="/admin/devices" {{if eq .AdminPage "devices"}}aria-current="page"{{end}}>{{t .Locale "admin.devices"}}</a>
|
||||
<a href="/admin/vaults" {{if eq .AdminPage "vaults"}}aria-current="page"{{end}}>{{t .Locale "admin.vaults"}}</a>
|
||||
<a href="/admin/storage" {{if eq .AdminPage "storage"}}aria-current="page"{{end}}>{{t .Locale "admin.storage"}}</a>
|
||||
<a href="/admin/audit" {{if eq .AdminPage "audit"}}aria-current="page"{{end}}>{{t .Locale "admin.audit"}}</a>
|
||||
<a href="/admin/settings" {{if eq .AdminPage "settings"}}aria-current="page"{{end}}>{{t .Locale "admin.settings"}}</a>
|
||||
<a href="/admin/diagnostics" {{if eq .AdminPage "diagnostics"}}aria-current="page"{{end}}>{{t .Locale "admin.diagnostics"}}</a>
|
||||
<form method="post" action="/admin/logout"><input type="hidden" name="csrf_token" value="{{.CSRF}}"><button class="link-button" type="submit">{{t .Locale "auth.logout"}}</button></form>
|
||||
</aside>
|
||||
<div class="admin-content">
|
||||
{{if eq .AdminPage "dashboard"}}<p class="eyebrow">{{t .Locale "admin.overview"}}</p><h1>{{t .Locale "admin.dashboard"}}</h1><div class="stat-grid"><article class="card stat"><strong>{{.Stats.Users}}</strong><span>{{t .Locale "admin.users"}}</span></article><article class="card stat"><strong>{{.Stats.ActiveDevices}}</strong><span>{{t .Locale "admin.activeDevices"}}</span></article><article class="card stat"><strong>{{.Stats.Vaults}}</strong><span>{{t .Locale "admin.vaults"}}</span></article><article class="card stat"><strong>{{.Stats.Operations}}</strong><span>{{t .Locale "admin.operations"}}</span></article></div><section class="card panel"><h2>{{t .Locale "admin.serviceHealth"}}</h2><dl class="details"><dt>{{t .Locale "admin.status"}}</dt><dd>{{.Health.Status}}</dd><dt>{{t .Locale "admin.version"}}</dt><dd>{{.Health.Version}} {{.Health.BuildCommit}}</dd><dt>{{t .Locale "admin.lastActivity"}}</dt><dd>{{.Stats.LastSyncAt}}</dd></dl></section>
|
||||
{{else if eq .AdminPage "users"}}<div class="section-heading"><div><p class="eyebrow">{{t .Locale "admin.access"}}</p><h1>{{t .Locale "admin.users"}}</h1></div><a class="button primary" href="/admin/create-user">{{t .Locale "admin.createUser"}}</a></div><section class="card table-card"><div class="table-scroll"><table><thead><tr><th>{{t .Locale "field.username"}}</th><th>{{t .Locale "field.email"}}</th><th>{{t .Locale "admin.devices"}}</th><th>{{t .Locale "device.status"}}</th><th>{{t .Locale "common.actions"}}</th></tr></thead><tbody>{{range .AdminUsers}}<tr><td>{{.Username}}</td><td>{{.Email}}</td><td>{{.Devices}}</td><td>{{if .Blocked}}<span class="badge danger">{{t $.Locale "admin.blocked"}}</span>{{else if .Confirmed}}<span class="badge ok">{{t $.Locale "device.active"}}</span>{{else}}<span class="badge">{{t $.Locale "admin.unconfirmed"}}</span>{{end}}</td><td><form class="inline-form" method="post" action="/admin/action"><input type="hidden" name="csrf_token" value="{{$.CSRF}}"><input type="hidden" name="action" value="toggle-user"><input type="hidden" name="id" value="{{.ID}}"><input name="password" type="password" required placeholder="{{t $.Locale "field.password"}}"><button class="button secondary" type="submit">{{if .Blocked}}{{t $.Locale "admin.unblock"}}{{else}}{{t $.Locale "admin.block"}}{{end}}</button></form><details><summary>{{t $.Locale "admin.manage"}}</summary><form method="post" action="/admin/action" class="stack compact"><input type="hidden" name="csrf_token" value="{{$.CSRF}}"><input type="hidden" name="id" value="{{.ID}}"><input type="hidden" name="action" value="edit-user"><label>{{t $.Locale "field.username"}}<input name="username" value="{{.Username}}" required></label><label>{{t $.Locale "field.email"}}<input name="email" type="email" value="{{.Email}}" required></label><button class="button secondary" type="submit">{{t $.Locale "admin.saveUser"}}</button></form><form method="post" action="/admin/action" class="stack compact"><input type="hidden" name="csrf_token" value="{{$.CSRF}}"><input type="hidden" name="id" value="{{.ID}}"><input type="hidden" name="action" value="reset-user-password"><label>{{t $.Locale "field.newPassword"}}<input name="new_password" type="password" required></label><label>{{t $.Locale "field.password"}}<input name="password" type="password" required></label><button class="button secondary" type="submit">{{t $.Locale "admin.resetPassword"}}</button></form><form method="post" action="/admin/action" class="stack compact"><input type="hidden" name="csrf_token" value="{{$.CSRF}}"><input type="hidden" name="id" value="{{.ID}}"><input type="hidden" name="action" value="delete-user"><label>{{t $.Locale "field.password"}}<input name="password" type="password" required></label><button class="button danger" type="submit">{{t $.Locale "admin.deleteUser"}}</button></form></details></td></tr>{{else}}<tr><td colspan="5" class="empty">{{t .Locale "admin.noUsers"}}</td></tr>{{end}}</tbody></table></div></section>
|
||||
{{else if eq .AdminPage "devices"}}<p class="eyebrow">{{t .Locale "admin.access"}}</p><h1>{{t .Locale "admin.devices"}}</h1><section class="card table-card"><div class="table-scroll"><table><thead><tr><th>{{t .Locale "device.name"}}</th><th>{{t .Locale "admin.user"}}</th><th>{{t .Locale "device.vault"}}</th><th>{{t .Locale "device.lastSeen"}}</th><th>{{t .Locale "device.status"}}</th><th>{{t .Locale "common.actions"}}</th></tr></thead><tbody>{{range .AdminDevices}}<tr><td>{{.Name}}</td><td>{{.User}}</td><td class="mono">{{short .Vault 16}}</td><td>{{.LastSeen}}</td><td>{{if .Revoked}}<span class="badge danger">{{t $.Locale "device.revoked"}}</span>{{else}}<span class="badge ok">{{t $.Locale "device.active"}}</span>{{end}}</td><td>{{if not .Revoked}}<form class="inline-form" method="post" action="/admin/action"><input type="hidden" name="csrf_token" value="{{$.CSRF}}"><input type="hidden" name="action" value="revoke-device"><input type="hidden" name="id" value="{{.ID}}"><input name="password" type="password" required placeholder="{{t $.Locale "field.password"}}"><button class="button danger" type="submit">{{t $.Locale "device.revoke"}}</button></form>{{end}}</td></tr>{{else}}<tr><td colspan="6" class="empty">{{t .Locale "admin.noDevices"}}</td></tr>{{end}}</tbody></table></div></section>
|
||||
{{else if eq .AdminPage "vaults"}}<p class="eyebrow">{{t .Locale "admin.storage"}}</p><h1>{{t .Locale "admin.vaults"}}</h1><section class="card table-card"><div class="table-scroll"><table><thead><tr><th>{{t .Locale "admin.user"}}</th><th>{{t .Locale "device.vault"}}</th><th>{{t .Locale "admin.devices"}}</th><th>{{t .Locale "admin.operations"}}</th><th>{{t .Locale "admin.lastActivity"}}</th></tr></thead><tbody>{{range .Vaults}}<tr><td>{{.User}}</td><td class="mono">{{short .Vault 24}}</td><td>{{.Devices}}</td><td>{{.Operations}}</td><td>{{.LastActivity}}</td></tr>{{else}}<tr><td colspan="5" class="empty">{{t .Locale "admin.noVaults"}}</td></tr>{{end}}</tbody></table></div></section>
|
||||
{{else if eq .AdminPage "storage"}}<p class="eyebrow">{{t .Locale "admin.storage"}}</p><h1>{{t .Locale "admin.storage"}}</h1><div class="stat-grid"><article class="card stat"><strong>{{.Stats.DatabaseBytes}}</strong><span>{{t .Locale "admin.databaseBytes"}}</span></article><article class="card stat"><strong>{{.Stats.BlobBytes}}</strong><span>{{t .Locale "admin.blobBytes"}}</span></article><article class="card stat"><strong>{{.Stats.Operations}}</strong><span>{{t .Locale "admin.operations"}}</span></article></div><p class="muted">{{t .Locale "admin.retentionNote"}}</p>
|
||||
{{else if eq .AdminPage "audit"}}<p class="eyebrow">{{t .Locale "admin.diagnostics"}}</p><h1>{{t .Locale "admin.audit"}}</h1><section class="card table-card"><div class="table-scroll"><table><thead><tr><th>{{t .Locale "admin.event"}}</th><th>{{t .Locale "admin.user"}}</th><th>{{t .Locale "device.name"}}</th><th>{{t .Locale "admin.time"}}</th></tr></thead><tbody>{{range .Audit}}<tr><td>{{.Event}}</td><td>{{.User}}</td><td>{{.Device}}</td><td>{{.At}}</td></tr>{{else}}<tr><td colspan="4" class="empty">{{t .Locale "admin.noAudit"}}</td></tr>{{end}}</tbody></table></div></section>
|
||||
{{else if eq .AdminPage "settings"}}<p class="eyebrow">{{t .Locale "admin.settings"}}</p><h1>{{t .Locale "admin.smtpTitle"}}</h1><section class="card panel"><form method="post" action="/admin/action" class="stack"><input type="hidden" name="csrf_token" value="{{.CSRF}}"><input type="hidden" name="action" value="smtp"><label>{{t .Locale "admin.smtpServer"}}<input name="smtp_host" value="{{.SMTP.Host}}"></label><label>{{t .Locale "admin.smtpPort"}}<input name="smtp_port" value="{{.SMTP.Port}}"></label><label>{{t .Locale "admin.smtpUsername"}}<input name="smtp_user" value="{{.SMTP.User}}"></label><label>{{t .Locale "admin.smtpPassword"}}<input name="smtp_pass" type="password" autocomplete="new-password"></label><label>{{t .Locale "admin.smtpType"}}<select name="smtp_security"><option value="none" {{if eq .SMTP.Security "none"}}selected{{end}}>{{t .Locale "admin.smtpNoEncryption"}}</option><option value="starttls" {{if eq .SMTP.Security "starttls"}}selected{{end}}>STARTTLS</option><option value="tls" {{if eq .SMTP.Security "tls"}}selected{{end}}>TLS</option></select></label><label>{{t .Locale "admin.smtpFrom"}}<input name="smtp_from" value="{{.SMTP.From}}"></label><label>{{t .Locale "admin.smtpServerURL"}}<input name="server_url" value="{{.SMTP.ServerURL}}"></label><label>{{t .Locale "field.password"}}<input name="password" type="password" autocomplete="current-password" required></label><button class="button primary" type="submit">{{t .Locale "admin.smtpSave"}}</button></form></section>
|
||||
{{else if eq .AdminPage "diagnostics"}}<p class="eyebrow">{{t .Locale "admin.diagnostics"}}</p><h1>{{t .Locale "admin.diagnostics"}}</h1><section class="card panel"><dl class="details"><dt>{{t .Locale "admin.status"}}</dt><dd>{{.Health.Status}}</dd><dt>{{t .Locale "admin.database"}}</dt><dd>{{.Health.DatabaseReachable}}</dd><dt>{{t .Locale "admin.blobStorage"}}</dt><dd>{{.Health.BlobStorageWritable}}</dd><dt>{{t .Locale "admin.schema"}}</dt><dd>{{.Health.SchemaVersion}}</dd><dt>{{t .Locale "admin.serverTime"}}</dt><dd>{{.Health.ServerTime}}</dd></dl><a class="button secondary" href="/api/v1/health">{{t .Locale "admin.healthJSON"}}</a></section>{{end}}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -0,0 +1,2 @@
|
||||
{{define "admin_create_user"}}{{template "layout" .}}{{end}}
|
||||
{{define "content"}}<section class="auth-card card"><p class="eyebrow">{{t .Locale "admin.access"}}</p><h1>{{t .Locale "admin.createUser"}}</h1>{{if .Flash}}<p class="flash error" role="alert">{{t .Locale .Flash}}</p>{{end}}<form method="post" action="/admin/create-user" class="stack"><input type="hidden" name="csrf_token" value="{{.CSRF}}"><label>{{t .Locale "field.username"}}<input name="username" autocomplete="username" required autofocus></label><label>{{t .Locale "field.email"}}<input name="email" type="email" autocomplete="email" required></label><label>{{t .Locale "field.password"}}<input name="password" type="password" autocomplete="new-password" minlength="8" maxlength="256" required></label><button class="button primary" type="submit">{{t .Locale "admin.createUser"}}</button></form><p class="muted"><a href="/admin/users">{{t .Locale "common.back"}}</a></p></section>{{end}}
|
||||
@@ -0,0 +1,2 @@
|
||||
{{define "admin_login"}}{{template "layout" .}}{{end}}
|
||||
{{define "content"}}<section class="auth-card card"><p class="eyebrow">{{t .Locale "admin.eyebrow"}}</p><h1>{{t .Locale "admin.loginTitle"}}</h1>{{if .Flash}}<p class="flash error" role="alert">{{t .Locale .Flash}}</p>{{end}}<form method="post" action="/admin/login" class="stack"><label>{{t .Locale "field.username"}}<input name="username" autocomplete="username" required autofocus></label><label>{{t .Locale "field.password"}}<input name="password" type="password" autocomplete="current-password" required></label><button class="button primary" type="submit">{{t .Locale "auth.login"}}</button></form></section>{{end}}
|
||||
@@ -0,0 +1,2 @@
|
||||
{{define "dashboard"}}{{template "layout" .}}{{end}}
|
||||
{{define "content"}}<section class="dashboard-head"><div><p class="eyebrow">{{t .Locale "user.account"}}</p><h1>{{.UserName}}</h1><p class="muted">{{.Email}}</p></div><form method="post" action="/logout"><input type="hidden" name="csrf_token" value="{{.CSRF}}"><button class="button secondary" type="submit">{{t .Locale "auth.logout"}}</button></form></section>{{if .Flash}}<p class="flash error" role="alert">{{t .Locale .Flash}}</p>{{end}}<section class="card table-card"><h2>{{t .Locale "user.devices"}}</h2>{{if .Devices}}<div class="table-scroll"><table><thead><tr><th>{{t .Locale "device.name"}}</th><th>{{t .Locale "device.vault"}}</th><th>{{t .Locale "device.version"}}</th><th>{{t .Locale "device.lastSeen"}}</th><th>{{t .Locale "device.status"}}</th><th>{{t .Locale "common.actions"}}</th></tr></thead><tbody>{{range .Devices}}<tr><td>{{.Name}}</td><td><span class="mono">{{short .Vault 18}}</span></td><td>{{.ClientVersion}}</td><td>{{.LastSeen}}</td><td>{{if .Revoked}}<span class="badge danger">{{t $.Locale "device.revoked"}}</span>{{else}}<span class="badge ok">{{t $.Locale "device.active"}}</span>{{end}}</td><td>{{if not .Revoked}}<form method="post" action="/api/v1/user/devices/{{.ID}}/revoke" class="inline-form"><input type="hidden" name="csrf_token" value="{{$.CSRF}}"><label class="sr-only">{{t $.Locale "field.password"}}</label><input name="password" type="password" autocomplete="current-password" required placeholder="{{t $.Locale "field.password"}}"><button class="button danger" type="submit" data-confirm="{{t $.Locale "device.revokeConfirm"}}">{{t $.Locale "device.revoke"}}</button></form>{{end}}</td></tr>{{end}}</tbody></table></div>{{else}}<p class="empty">{{t .Locale "user.noDevices"}}</p>{{end}}</section>{{end}}
|
||||
@@ -0,0 +1,2 @@
|
||||
{{define "error"}}{{template "layout" .}}{{end}}
|
||||
{{define "content"}}<section class="message-card card"><p class="eyebrow danger-text">{{t .Locale "error.label"}}</p><h1>{{t .Locale .Heading}}</h1><p class="lead">{{t .Locale .Message}}</p>{{if .BackURL}}<a class="button secondary" href="{{.BackURL}}">{{t .Locale "common.back"}}</a>{{end}}</section>{{end}}
|
||||
@@ -0,0 +1,5 @@
|
||||
{{define "forgot"}}{{template "layout" .}}{{end}}
|
||||
{{define "content"}}
|
||||
<section class="auth-card card"><p class="eyebrow">{{t .Locale "auth.account"}}</p><h1>{{t .Locale "auth.forgotTitle"}}</h1><p class="muted">{{t .Locale "auth.forgotDescription"}}</p>
|
||||
<form method="post" action="/forgot" class="stack"><label>{{t .Locale "field.email"}}<input name="email" type="email" autocomplete="email" required autofocus></label><button class="button primary" type="submit">{{t .Locale "auth.sendLink"}}</button></form><p class="muted"><a href="/login">{{t .Locale "auth.backLogin"}}</a></p></section>
|
||||
{{end}}
|
||||
@@ -0,0 +1,10 @@
|
||||
{{define "home"}}{{template "layout" .}}{{end}}
|
||||
{{define "content"}}
|
||||
<section class="hero card">
|
||||
<p class="eyebrow">{{t .Locale "home.eyebrow"}}</p>
|
||||
<h1>{{t .Locale "home.heading"}}</h1>
|
||||
<p class="lead">{{t .Locale "home.description"}}</p>
|
||||
<div class="actions"><a class="button primary" href="/login">{{t .Locale "home.login"}}</a>{{if .AllowRegistration}}<a class="button secondary" href="/register">{{t .Locale "home.register"}}</a>{{end}}</div>
|
||||
<p class="muted">{{t .Locale "home.version"}} {{.Version}} · {{.BuildCommit}} · <span class="badge ok">{{.Status}}</span></p>
|
||||
</section>
|
||||
{{end}}
|
||||
@@ -0,0 +1,35 @@
|
||||
{{define "layout"}}
|
||||
<!doctype html>
|
||||
<html lang="{{.Locale}}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{t .Locale .Title}} · {{.ServerName}}</title>
|
||||
<link rel="icon" href="/static/logo.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="/static/app.css">
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<a class="brand" href="/" aria-label="{{.ServerName}}"><img src="/static/logo.svg" alt="" width="28" height="28"><span>Verstak Sync</span></a>
|
||||
<nav class="site-nav" aria-label="{{t .Locale "nav.primary"}}">
|
||||
<a href="/login">{{t .Locale "nav.login"}}</a>
|
||||
{{if .AllowRegistration}}<a href="/register">{{t .Locale "nav.register"}}</a>{{end}}
|
||||
</nav>
|
||||
<form class="locale-form" action="/locale" method="post" aria-label="{{t .Locale "locale.label"}}">
|
||||
<input type="hidden" name="from" value="{{.CurrentURL}}">
|
||||
{{if .CSRF}}<input type="hidden" name="csrf_token" value="{{.CSRF}}">{{end}}
|
||||
<label class="sr-only" for="locale">{{t .Locale "locale.label"}}</label>
|
||||
<select id="locale" name="locale" data-auto-submit>
|
||||
<option value="system" {{if eq .LocalePreference "system"}}selected{{end}}>{{t .Locale "locale.system"}}</option>
|
||||
<option value="ru" {{if eq .LocalePreference "ru"}}selected{{end}}>Русский</option>
|
||||
<option value="en" {{if eq .LocalePreference "en"}}selected{{end}}>English</option>
|
||||
</select>
|
||||
<button class="sr-only" type="submit">{{t .Locale "locale.apply"}}</button>
|
||||
</form>
|
||||
</header>
|
||||
<main class="page-shell">{{template "content" .}}</main>
|
||||
<footer class="site-footer"><span>{{.ServerName}}</span><span>{{t .Locale "footer.localFirst"}}</span></footer>
|
||||
<script src="/static/app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
@@ -0,0 +1,7 @@
|
||||
{{define "login"}}{{template "layout" .}}{{end}}
|
||||
{{define "content"}}
|
||||
<section class="auth-card card"><p class="eyebrow">{{t .Locale "auth.welcome"}}</p><h1>{{t .Locale "auth.loginTitle"}}</h1>
|
||||
{{if .Flash}}<p class="flash error" role="alert">{{t .Locale .Flash}}</p>{{end}}
|
||||
<form method="post" action="/login" class="stack"><label>{{t .Locale "field.usernameOrEmail"}}<input name="username" autocomplete="username" required autofocus></label><label>{{t .Locale "field.password"}}<input name="password" type="password" autocomplete="current-password" required></label><button class="button primary" type="submit">{{t .Locale "auth.login"}}</button></form>
|
||||
<p class="muted"><a href="/forgot">{{t .Locale "auth.forgot"}}</a>{{if .AllowRegistration}} · <a href="/register">{{t .Locale "auth.register"}}</a>{{end}}</p></section>
|
||||
{{end}}
|
||||
@@ -0,0 +1,2 @@
|
||||
{{define "message"}}{{template "layout" .}}{{end}}
|
||||
{{define "content"}}<section class="message-card card"><p class="eyebrow">Verstak Sync</p><h1>{{t .Locale .Heading}}</h1><p class="lead">{{t .Locale .Message}}</p>{{if .BackURL}}<a class="button primary" href="{{.BackURL}}">{{t .Locale "common.continue"}}</a>{{end}}</section>{{end}}
|
||||
@@ -0,0 +1,7 @@
|
||||
{{define "register"}}{{template "layout" .}}{{end}}
|
||||
{{define "content"}}
|
||||
<section class="auth-card card"><p class="eyebrow">{{t .Locale "auth.account"}}</p><h1>{{t .Locale "auth.registerTitle"}}</h1>
|
||||
{{if .Flash}}<p class="flash error" role="alert">{{t .Locale .Flash}}</p>{{end}}
|
||||
<form method="post" action="/register" class="stack"><label>{{t .Locale "field.username"}}<input name="username" autocomplete="username" required autofocus></label><label>{{t .Locale "field.email"}}<input name="email" type="email" autocomplete="email" required></label><label>{{t .Locale "field.password"}}<input name="password" type="password" autocomplete="new-password" minlength="8" maxlength="256" required></label><button class="button primary" type="submit">{{t .Locale "auth.register"}}</button></form>
|
||||
<p class="muted"><a href="/login">{{t .Locale "auth.haveAccount"}}</a></p></section>
|
||||
{{end}}
|
||||
@@ -0,0 +1,6 @@
|
||||
{{define "reset"}}{{template "layout" .}}{{end}}
|
||||
{{define "content"}}
|
||||
<section class="auth-card card"><p class="eyebrow">{{t .Locale "auth.account"}}</p><h1>{{t .Locale "auth.resetTitle"}}</h1>
|
||||
{{if .Flash}}<p class="flash error" role="alert">{{t .Locale .Flash}}</p>{{end}}
|
||||
<form method="post" action="/reset" class="stack"><input type="hidden" name="token" value="{{.Token}}"><label>{{t .Locale "field.newPassword"}}<input name="password" type="password" autocomplete="new-password" minlength="8" maxlength="256" required autofocus></label><label>{{t .Locale "field.confirmPassword"}}<input name="confirm" type="password" autocomplete="new-password" minlength="8" maxlength="256" required></label><button class="button primary" type="submit">{{t .Locale "auth.savePassword"}}</button></form></section>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user