fix(backend): bind session key to both secrets and guard no-op chapter saves

Final-review fix wave over the web UI branch.

- sessionKey now derives from API_TOKEN and WEB_PASSWORD with a \x00
  separator, so rotating the password logs every browser out too.
- uiChapter only clears last_chapter_url when the number actually
  changes. The form is pre-filled, so a bare tap of Save resubmits the
  same value; that used to destroy the chapter URL silently while
  updated_at stayed put, degrading Continue to the series index page.
- MANGA_WEB_HOST is now required by the prod override rather than
  falling back to manga.example.com, matching MANGA_API_HOST.
- Comment fixes: static cache rationale, pruneLocked aliasing
  invariant, and the stale "3 routes" line in CLAUDE.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-26 03:18:00 +07:00
parent aac00ec01c
commit 22bf68f12f
8 changed files with 88 additions and 28 deletions
+11 -5
View File
@@ -22,11 +22,13 @@ const (
sessionKeyPurpose = "mangabm-web-session-v1"
)
// sessionKey derives the cookie-signing key from the API token. Sessions are
// stateless — there is no session table — so rotating API_TOKEN invalidates
// every outstanding cookie at once.
func sessionKey(apiToken string) []byte {
sum := sha256.Sum256([]byte(apiToken + sessionKeyPurpose))
// sessionKey derives the cookie-signing key from both secrets. Sessions are
// stateless — there is no session table — so rotating either API_TOKEN or
// WEB_PASSWORD invalidates every outstanding cookie at once. The \x00
// separator prevents the concatenation ambiguity a bare apiToken+webPassword
// would have (e.g. "ab"+"c" colliding with "a"+"bc").
func sessionKey(apiToken, webPassword string) []byte {
sum := sha256.Sum256([]byte(apiToken + "\x00" + webPassword + sessionKeyPurpose))
return sum[:]
}
@@ -166,6 +168,10 @@ func (l *loginLimiter) reset(ip string) {
// The caller must hold l.mu.
func (l *loginLimiter) pruneLocked(ip string, now time.Time) []time.Time {
cutoff := now.Add(-loginWindow)
// In-place filter: kept reuses the backing array of the slice being
// ranged over. The range expression captures the slice header once at
// the start, so the append cursor (kept) can never outrun the read
// cursor (the range index) — safe to alias.
kept := l.failures[ip][:0]
for _, at := range l.failures[ip] {
if at.After(cutoff) {