The admin was a set of env credentials on its own loopback listener. That bought network isolation, and charged a second port to tunnel and proxy and a second credential in the password manager. It also sat outside the SameSite protection the member cookie already had, and left every ban and password reset with no actor to log. is_admin on users reuses what was already there: the session, the login rate limiter, ban-drops-sessions, CSRF. /admin is now a route on the member mux. A member without the flag gets 404 rather than 403 — the pages are none of their business, and "forbidden" confirms there is something to be forbidden from. Registration needs an invite and invites come from /admin, so an empty database cannot grow its first user. seedAdmin breaks that circle exactly once, from ADMIN_EMAIL and ADMIN_PASSWORD, and does nothing against a database that already has users. An admin cannot ban themselves: banning drops the target's sessions, and nothing would be left that could undo it. This reverses decision 8, which is rewritten rather than deleted, along with the admin entry in the CONTEXT.md vocabulary.
83 lines
3.5 KiB
HTML
83 lines
3.5 KiB
HTML
<!doctype html>
|
|
<html lang="fi">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>{{.Title}} — Levyraati</title>
|
|
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml">
|
|
<link rel="preload" href="/static/fonts/oswald.woff2" as="font" type="font/woff2" crossorigin>
|
|
<link rel="stylesheet" href="/static/style.css">
|
|
<script src="/static/htmx.min.js" defer></script>
|
|
<script src="/static/player.js" defer></script>
|
|
<script src="/static/lyrics.js" defer></script>
|
|
</head>
|
|
<body>
|
|
<header class="topbar">
|
|
<div class="topbar-inner">
|
|
<a class="brand" href="/">Levyraati{{if .Admin}} <span class="badge admin">ylläpito</span>{{end}}</a>
|
|
|
|
{{if .Member}}
|
|
<nav class="navlinks">
|
|
<a href="/" {{if eq .Path "/"}}aria-current="page"{{end}}>Jono{{if .Queued}} <span class="count">{{.Queued}}</span>{{end}}</a>
|
|
<a href="/songs" {{if eq .Path "/songs"}}aria-current="page"{{end}}>Kappaleet</a>
|
|
<a href="/submit" {{if eq .Path "/submit"}}aria-current="page"{{end}}>Lähetä</a>
|
|
<a href="/stats" {{if eq .Path "/stats"}}aria-current="page"{{end}}>Tilastot</a>
|
|
<!-- An admin is a member first: the same nav, with one link the others do not get. -->
|
|
{{if .Member.IsAdmin}}<a href="/admin" {{if .Admin}}aria-current="page"{{end}}>Ylläpito</a>{{end}}
|
|
</nav>
|
|
<div class="userblock">
|
|
<span class="lines">
|
|
<span class="name">{{.Member.Name}}</span>
|
|
<span class="email">{{.Member.Email}}</span>
|
|
</span>
|
|
<a href="/profile" title="{{.Member.Name}}">
|
|
{{if .Member.Avatar}}<img class="avatar" src="/avatars/{{.Member.ID}}" alt="Oma profiili">
|
|
{{else}}<span class="avatar">{{.Member.Initials}}</span>{{end}}
|
|
</a>
|
|
<form method="post" action="/logout"><button class="link">Kirjaudu ulos</button></form>
|
|
|
|
<!-- <details> is the mobile panel: no JS, and Esc/click-away come free. -->
|
|
<details class="mobilenav">
|
|
<summary aria-label="Valikko">☰</summary>
|
|
<div class="panel">
|
|
<a href="/">Jono{{if .Queued}} <span class="count">{{.Queued}}</span>{{end}}</a>
|
|
<a href="/songs">Kappaleet</a>
|
|
<a href="/submit">Lähetä</a>
|
|
<a href="/stats">Tilastot</a>
|
|
{{if .Member.IsAdmin}}<a href="/admin">Ylläpito</a>{{end}}
|
|
<a href="/profile">Oma profiili</a>
|
|
<form method="post" action="/logout"><button type="submit">Kirjaudu ulos</button></form>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
{{else}}
|
|
<span></span>
|
|
<nav class="navlinks"><a href="/login">Kirjaudu</a></nav>
|
|
{{end}}
|
|
</div>
|
|
</header>
|
|
|
|
<main {{if .Narrow}}class="narrow"{{end}}>{{template "content" .}}</main>
|
|
|
|
<footer class="sitefooter">
|
|
{{if .Member}}
|
|
<!-- The server already knows where they were, so the path travels in the link — no JS. -->
|
|
<a href="/report?from={{.Path}}">Anna palautetta</a> ·
|
|
{{end}}
|
|
<span class="slogan">We know good music, baby!</span>
|
|
<span class="copyright">© Kessinen</span>
|
|
<span class="version" title="Käytössä oleva versio">v{{.Version}}</span>
|
|
</footer>
|
|
|
|
{{with .Flash}}
|
|
<div class="toasts">
|
|
<div class="toast" role="status">
|
|
<p>{{.}}</p>
|
|
<button class="dismiss" aria-label="Sulje"
|
|
onclick="this.closest('.toast').remove()">×</button>
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
</body>
|
|
</html>
|