fix: keep MANGA_WEB_HOST unset in .env.example and make no-op saves inert
Re-review of the fix wave found the .env.example edit defeated the fix
it belonged to: shipping MANGA_WEB_HOST=manga.example.com re-supplied
the value that ${MANGA_WEB_HOST:?} exists to reject, so a fresh
`cp .env.example .env` started fine and Traefik published the UI router
on a domain the operator does not own. Left commented, matching
MANGA_API_HOST; DEPLOY.md 1 now lists it among the required variables.
Also:
- uiChapter leaves last_chapter too, not only last_chapter_url, when the
submitted number is unchanged. It used to rewrite the display string
("45.0" to "45") behind a frozen updated_at.
- Design spec 4.2 documents the two-secret key derivation.
- Corrected the pruneLocked aliasing rationale and the stale
sessionKeyPurpose comment.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+5
-4
@@ -18,7 +18,8 @@ const (
|
||||
// 60 days: long enough that a phone stays logged in between reading spells.
|
||||
sessionTTL = 60 * 24 * time.Hour
|
||||
// Domain separation, so the session key can never collide with any other
|
||||
// use of API_TOKEN. Changing this string logs everyone out.
|
||||
// use of the secrets it is derived from. Changing this string logs
|
||||
// everyone out.
|
||||
sessionKeyPurpose = "mangabm-web-session-v1"
|
||||
)
|
||||
|
||||
@@ -169,9 +170,9 @@ func (l *loginLimiter) reset(ip string) {
|
||||
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.
|
||||
// ranged over. Safe to alias because append writes at index len(kept),
|
||||
// which is always <= the range index i, and element i is read before
|
||||
// that write — the write cursor can never overtake the read cursor.
|
||||
kept := l.failures[ip][:0]
|
||||
for _, at := range l.failures[ip] {
|
||||
if at.After(cutoff) {
|
||||
|
||||
+11
-8
@@ -254,12 +254,15 @@ func (h *webHandler) uiFavorite(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// uiChapter forces the read chapter to a value the user typed.
|
||||
//
|
||||
// When that value actually changes the number, it also clears last_chapter_url:
|
||||
// that URL points at the chapter actually read, and once the number is forced
|
||||
// elsewhere it would send the reader backwards. ContinueURL then falls back to
|
||||
// the series page, which is always right. Resubmitting the same number — the
|
||||
// form is pre-filled, so a bare tap of Save is an easy accidental submit —
|
||||
// leaves last_chapter_url untouched instead of destroying it for no reason.
|
||||
// Writing the number also clears last_chapter_url: that URL points at the
|
||||
// chapter actually read, and once the number is forced elsewhere it would send
|
||||
// the reader backwards. ContinueURL then falls back to the series page, which
|
||||
// is always right.
|
||||
//
|
||||
// A submit that does not change the number touches nothing. The form is
|
||||
// pre-filled, so a bare tap of Save is an easy accidental submit; it must not
|
||||
// destroy last_chapter_url, nor rewrite the last_chapter display string ("45.0"
|
||||
// to "45") behind a frozen updated_at.
|
||||
func (h *webHandler) uiChapter(w http.ResponseWriter, r *http.Request) {
|
||||
b, ok := h.loadForMutation(w, r)
|
||||
if !ok {
|
||||
@@ -278,9 +281,9 @@ func (h *webHandler) uiChapter(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if num != b.LastChapterNum {
|
||||
b.LastChapterURL = ""
|
||||
b.LastChapter = raw
|
||||
b.LastChapterNum = num
|
||||
}
|
||||
b.LastChapter = raw
|
||||
b.LastChapterNum = num
|
||||
b.UpdatedAt = time.Now().UnixMilli()
|
||||
h.saveAndRenderCard(w, b)
|
||||
}
|
||||
|
||||
+9
-3
@@ -357,14 +357,16 @@ func TestChapterOverrideNoOpPreservesURLAndUpdatedAt(t *testing.T) {
|
||||
srv, store := newWebTestServer(t, cfg)
|
||||
before := seed(t, store, Bookmark{
|
||||
Key: "asura:solo", Site: "asura", SeriesID: "solo",
|
||||
Title: "Solo Leveling", LastChapter: "45", LastChapterNum: 45,
|
||||
Title: "Solo Leveling", LastChapter: "45.0", LastChapterNum: 45,
|
||||
LastChapterURL: "https://example.test/ch/45", SeriesURL: "https://example.test/solo",
|
||||
UpdatedAt: 1_000_000,
|
||||
})
|
||||
|
||||
// The chapter form is pre-filled with the current value, so tapping Save
|
||||
// without editing resubmits the unchanged number. That must be a no-op:
|
||||
// it must not silently clear last_chapter_url or move updated_at.
|
||||
// without editing resubmits the unchanged number. That must be a no-op: it
|
||||
// must not clear last_chapter_url, rewrite the last_chapter display string,
|
||||
// or move updated_at. The seed stores "45.0" against 45 so the display
|
||||
// string differs from what the form submits back.
|
||||
rr := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rr, uiRequest(t, cfg, http.MethodPost,
|
||||
"/ui/bookmarks/asura:solo/chapter", url.Values{"chapter": {"45"}}))
|
||||
@@ -380,6 +382,10 @@ func TestChapterOverrideNoOpPreservesURLAndUpdatedAt(t *testing.T) {
|
||||
t.Fatalf("LastChapterURL = %q, want preserved %q on a no-op save",
|
||||
after.LastChapterURL, before.LastChapterURL)
|
||||
}
|
||||
if after.LastChapter != before.LastChapter {
|
||||
t.Fatalf("LastChapter = %q, want preserved %q on a no-op save",
|
||||
after.LastChapter, before.LastChapter)
|
||||
}
|
||||
if after.UpdatedAt != before.UpdatedAt {
|
||||
t.Fatalf("UpdatedAt = %d, want unchanged %d on a no-op save",
|
||||
after.UpdatedAt, before.UpdatedAt)
|
||||
|
||||
Reference in New Issue
Block a user