feat(backend): per-Reader userscript credential with UI install and rotation (#24)
Each Reader's userscript credential is derived from TOKEN_KEY, their Discord id and a token epoch (HMAC-SHA256, hex); only its SHA-256 sits in readers.token_sha256, so install URLs survive restarts while a database leak yields nothing but hashes. One credential authenticates the script download path and the API bearer header. - internal/token: derivation + hashing; migration 0006 adds token_epoch - seed refreshes the owner's epoch-0 hash only before first rotation - httpmw.Auth resolves the acting Reader from the credential hash and stashes it in the request context; the retired API_TOKEN resolves to the owner until API_TOKEN_GRACE_UNTIL, logged per use, on both the bearer and script-download paths - userscript handler renders the bindmounted file with the resolved Reader's credential substituted for __API_TOKEN__; a legacy-path request during grace serves the derived credential, so devices self-migrate on their next update poll - web UI: Userscripts panel with session-gated install endpoints that render the script directly (credential never in markup, address bar or a redirect) and confirm-gated rotation; atomic epoch bump + hash rewrite in the store - both userscripts carry __API_TOKEN__ placeholders; the committed global-token literal is removed (rotating at deploy retires it for real — it survives in git history) - env: TOKEN_KEY required, API_TOKEN/API_TOKEN_GRACE_UNTIL retire the legacy credential; docs and compose updated
This commit is contained in:
@@ -74,6 +74,8 @@
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{{template "setup" .}}
|
||||
|
||||
{{template "keyrow" .}}
|
||||
|
||||
{{template "recent" .}}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{{/* The userscript install panel. Each link serves the script rendered
|
||||
with the acting Reader's credential inside it, so the credential never
|
||||
appears in this page's markup, the address bar, or a redirect. Rotation
|
||||
is confirm-gated because it invalidates every installed copy at once;
|
||||
the response swaps this same panel open with the reinstall warning. */}}
|
||||
{{define "setup"}}
|
||||
<details class="setup" id="setup"{{if .Rotated}} open{{end}}>
|
||||
<summary>Userscripts</summary>
|
||||
<p class="setup-copy">Install each script once per device. They keep your
|
||||
bookmarks in sync across every site and update themselves from here.</p>
|
||||
<p class="setup-links">
|
||||
<a class="ghost" href="/install/manga-bookmark.user.js">Install Manga script</a>
|
||||
<a class="ghost" href="/install/novel-bookmark.user.js">Install Novels script</a>
|
||||
</p>
|
||||
{{if .Rotated}}
|
||||
<p class="setup-warn" role="status">Credential rotated — the old one no
|
||||
longer works. Reinstall both scripts on every device now, or they will
|
||||
silently stop syncing.</p>
|
||||
{{else}}
|
||||
<form class="setup-rotate" hx-post="/rotate-token" hx-target="#setup"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="Rotation invalidates the current credential on every device immediately. You will have to reinstall both scripts everywhere. Rotate?">
|
||||
<button type="submit" class="ghost">Rotate credential</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</details>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user