Add GET /api/session, an unguarded probe that reports whether the request
carries valid credentials. It returns 200 with {"authenticated":bool} and never
sends WWW-Authenticate, so the static public pages can check login state without
triggering the browser's Basic-auth dialog. To keep it from being a
password-guessing oracle that bypasses the /upload limiter, a request with no
credentials is answered cheaply as false, while wrong credentials go through the
same failed-attempt limiter (429 once drained).
static/session.js, loaded by every page template, calls it and, only when
authenticated, builds a fixed toolbar: Upload / Manage links, an "Edit this"
link on photo pages (via <body data-slug>, deep-linking to /manage?edit=<slug>,
which scrolls to and focuses that row), a signed-in indicator, and a log-out
control. Logged-out visitors see nothing and get no dialog.
Log out is best-effort: HTTP Basic has no reliable sign-out (the browser caches
credentials until the window closes), so it overwrites the cached credential and
reloads, which works in some browsers but not all. A dependable logout would
need cookie sessions.
98 lines
3.1 KiB
HTML
98 lines
3.1 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>{{.Tag.Name}} · Photo Gallery</title>
|
||
<link rel="stylesheet" href="{{.FontsURL}}">
|
||
<link rel="stylesheet" href="/static/style.css">
|
||
<link rel="icon" href="/static/favicon.ico" sizes="any">
|
||
<link rel="icon" type="image/png" href="/static/web-32.png" sizes="32x32">
|
||
<link rel="apple-touch-icon" href="/static/web-192.png">
|
||
<style>:root { --serif: '{{.SerifFamily}}', Georgia, serif; --mj: '{{.MlFamily}}', sans-serif; }</style>
|
||
<link rel="alternate" type="application/atom+xml" href="/feed.xml" title="Photo Feed">
|
||
</head>
|
||
<body>
|
||
<header class="profile-hdr">
|
||
<div class="profile-inner">
|
||
<div class="avatar-ring">
|
||
<span class="avatar-initials">{{.Initial}}</span>
|
||
</div>
|
||
<div class="pinfo">
|
||
<h1>{{.Author}}</h1>
|
||
<p class="handle">{{.Handle}}</p>
|
||
<p class="pcount"><strong>{{.Total}}</strong> posts</p>
|
||
<p class="pbio">{{.Bio}}{{if .BioAlt}}<br><span class="ml">{{.BioAlt}}</span>{{end}}</p>
|
||
<a href="{{.TimelineURL}}" class="rss-a">Timeline</a>
|
||
<a href="/albums/" class="rss-a">← Albums</a>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main>
|
||
<div class="grid-wrap">
|
||
<section class="tl-day">
|
||
<h2 class="tl-date">{{.Tag.Name}}<span class="tl-count">{{.Count}}</span></h2>
|
||
<div class="pgrid">
|
||
{{range .Photos}}
|
||
<a href="/photo/{{.Slug}}/" class="ptile">
|
||
<img src="/{{.Thumb}}" alt="{{.Caption}}" loading="lazy">
|
||
</a>
|
||
{{end}}
|
||
</div>
|
||
</section>
|
||
|
||
{{if or .PrevURL .NextURL}}
|
||
<nav class="pager">
|
||
{{if .PrevURL}}<a class="pager-newer" href="{{.PrevURL}}">← Newer</a>{{end}}
|
||
{{if .NextURL}}<a class="pager-older" href="{{.NextURL}}" data-load-more>Older →</a>{{end}}
|
||
</nav>
|
||
{{end}}
|
||
</div>
|
||
</main>
|
||
|
||
<footer class="site-footer">
|
||
© 2000–{{.Year}} {{.Author}}. All rights reserved.
|
||
</footer>
|
||
|
||
<script>
|
||
(function () {
|
||
const btn = document.querySelector('[data-load-more]');
|
||
if (!btn) return;
|
||
const grid = document.querySelector('.pgrid');
|
||
let loading = false;
|
||
|
||
async function loadMore(url) {
|
||
if (loading) return;
|
||
loading = true;
|
||
try {
|
||
const res = await fetch(url);
|
||
if (!res.ok) return;
|
||
const html = await res.text();
|
||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||
doc.querySelectorAll('.pgrid > a').forEach(t => grid.appendChild(t));
|
||
const nextBtn = doc.querySelector('[data-load-more]');
|
||
if (nextBtn) btn.href = nextBtn.getAttribute('href');
|
||
else btn.remove();
|
||
} finally {
|
||
loading = false;
|
||
}
|
||
}
|
||
|
||
btn.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
loadMore(btn.href);
|
||
});
|
||
|
||
const io = new IntersectionObserver((entries) => {
|
||
if (entries[0].isIntersecting && document.body.contains(btn)) {
|
||
loadMore(btn.href);
|
||
}
|
||
}, { rootMargin: '400px' });
|
||
io.observe(btn);
|
||
})();
|
||
</script>
|
||
<script src="/static/session.js"></script>
|
||
</body>
|
||
</html>
|