Commit graph

4 commits

Author SHA1 Message Date
fb9abaae17 Drop the 18+ account flag, its rule clause and the web page's 18+ field
Nothing read `users.nsfwEnabled` any more. `/api/amber/whoami` stopped returning
it, `GET /api/update/manifest` always answers `clean`, and the `releases` read
rule stopped mentioning it in migration 1793000000, when the nine 18+ rows (437
MB) were deleted. What was left was a per-account flag nobody writes and nobody
reads, sitting on the family's accounts and showing up in every schema dump and
access-rule review as if it still gated something. A flag that looks like a
protection but protects nothing costs the next reader the work of proving it dead.

One live reader did remain: pb_public/index.html showed a "Zdroj 18+" source
field and wrote `adultAddonUrl` into the encrypted addon config for a flagged
account, and picked a different child-profile label for one. Both are gone, so
the page offers the same three sources to everyone.

Migration 1797000000 drops the field, rewrites users.updateRule to
`id = @request.auth.id` (the clause it loses guarded a field that no longer
exists), and removes `adult` from releases.variant's allowed values. Checked
before writing it: 15 release rows, every one `clean`; three accounts carried the
flag (the owner's, one family member's, the retired test account); the separate
18+ app has no accounts and no PocketBase, so nothing outside Amber reads it.

`variant` itself stays and stays required: every installed Amber compares it
against its own build and refuses a mismatch SILENTLY, so dropping the field
would strand those installs with no error to see. publish-release.sh now accepts
only `clean`.

The status dashboard stops listing `adult-stremio-addon` and `stripchat-proxy`.
Both still run on the VPS; they are the owner's, not Amber's, and Amber never
reached them.

scripts/verify.py asserts the flag stays gone (field absent, no rule mentions it,
updateRule own-record-only). Run against a local PocketBase built from origin/main
it failed on exactly those three checks; with this migration applied, ALL CHECKS
PASSED.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 20:32:02 +02:00
8f2cb994ec The code has to choose the template, not the clock
Sharing a second set of paid accounts with friends means a second template,
and the route was not ready for one. It checked the code and then took the
most recently updated row:

  findRecordsByFilter("onboarding_template", "id != ''", "-updated", 1, 0)

With a single row that is invisible. With two it hands the family's
credentials to friends, or the reverse, decided purely by which row was
edited last, silently and with no error. status.html carried a comment
warning about exactly this: "never create a second".

So the code now selects the row, bound as a filter parameter rather than
concatenated. AMBER_INVITE_CODE becomes a fallback that opens the row
flagged isDefault, which means nobody halfway through setup broke and
Coolify needed no edit; once the family row has its own code the env var
stops mattering. A wrong code and a code with no row behind it give the
same 403, since telling them apart would confirm which codes exist.

Codes live on the rows rather than in more env vars, so a new group is one
row in the admin UI instead of a redeploy, and each code is revocable on its
own. The unique index is partial because PocketBase text fields default to
'' and SQLite calls two empty strings equal.

status.html read the template in three places, all by recency. They now go
through familyTemplate(), which selects on isDefault, so adding a friends
row cannot make the editor wander onto it or make account creation preload
the wrong group. Its one-click preload stays family-only; a group picker
there is left undone rather than half-built.

The website says "rodinný kód" in four places and friends are not family, so
that copy widens. The setup flow also offers "Mám kód" up front now: someone
Richard shares accounts with has nothing to buy, and walking them through
three price lists first would be actively misleading.

Verified against PocketBase 0.39.6 with two rows and friends as the most
recently updated, the state that used to break: each code resolved to its own
credentials, the legacy env code resolved to family via isDefault, wrong and
empty codes gave 403, a duplicate code was refused by the index, and
familyTemplate() returned family while -updated returned friends.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 21:35:46 +02:00
00b20550c2 Accounts are owner-created, and one page manages the whole account
Registration is closed. The instance was gated by a single shared invite code in
AMBER_INVITE_CODE -- a bearer secret, one string for everyone, forever, no record
of who used it, and known to every family member who ever onboarded, so it leaked
by design. An email whitelist was the obvious replacement and does not survive
contact with the facts: it binds to a person only if the address is verified, and
all five accounts on this instance are `verified = false`. For a household this
size the owner creating each account in the admin UI is stronger than either and
has nothing to leak. `users.createRule` is superuser-only now, and the hook
refuses with a sentence a family member can read rather than a bare 403.

The family code survives with a narrower job. It is no longer a key to the front
door; it is what unlocks the *shared family credentials* during setup. Close
friends get accounts too and bring their own (BYOC), so "has an account" must not
mean "may have the family's sources" -- the template route now wants auth AND the
code, keeping its rate limiter because the code is still a shared secret.

pb_public/account.html replaces index/settings/onboarding with one page behind a
persisted session: profiles (name, child flag, age ceiling, PIN), the 13 synced
playback preferences per profile, sources, devices, downloads, account. The token
persists in localStorage; the password never does, and anything needing it asks in
the moment -- that is the honest split between "stay signed in" and "hold the key
to your credentials in a browser tab".

Sources can now be set up from your own service credentials. The page posts them
straight to the addon's own /encode and builds TorBox's config token locally --
both addons send permissive CORS, so the credentials go from the family member's
browser to the addon and never touch our server. prehraj.to is required because
the addon's /encode returns 400 without it and decodeConfig rejects such a token,
so "webshare only" is not currently possible; the copy says so rather than
pretending. TorBox is optional and the copy is emphatic about what it is for.

Two hazards handled rather than hoped over. Changing the password re-keys the
addon vault, and the re-encrypt happens BEFORE the password PATCH: if it were
after, a failure would leave a blob no device can read. And prefs writes carry
`updatedAt`, the clock the app's LWW actually compares -- the server's `updated`
is only the pull cursor -- and merge into the existing data rather than replacing
it, because the app writes fields this page does not show yet.

Also: `users.ratingDefault`, so the account-wide age ceiling stops being the one
half of a parental control that does not sync. The app side ships with the next
release. The other three device-local settings stay local on purpose, and
`adult_enabled` most deliberately of all.

Plan and reasoning: docs/plan-accounts-and-web.md in amber-app.
2026-08-07 01:54:25 +02:00
0b65fd7f49 Family onboarding site: invite-gated signup, template config, /get/tv
Distribution site served by PocketBase itself (pb_public/, same origin):
- Landing (/): create-account CTA, TV sign-in steps, login-gated PC
  downloads via the existing update manifest + file tokens
- Wizard (/onboarding.html): invite code -> account -> named default
  profile -> family template fetched and encrypted IN THE BROWSER under
  the new user's password (PBKDF2-210k + AES-GCM, byte-compatible with
  the app's AddonConfigCrypto - a wizard blob decrypts in Dart, verified)
  -> pushed as their addon_config ciphertext. Adult fields never included.
- /get/tv: public direct download of the latest clean Android APK
  (Downloader-friendly; adult builds stay account-gated)

Backend:
- onboarding_template collection (admin-only, maintained via admin UI)
- AMBER_INVITE_CODE env gates BOTH users creation (X-Amber-Invite header
  or ?invite=) and the template route (per-IP rate limit 10/5min);
  fail-closed when unset. Note: this closes the previously-open in-app
  registration too.
- Dockerfile ships pb_public + --publicDir; compose passes the env var

Contract + Coolify steps (second domain amber.petruzalekr.cz) in
docs/onboarding-contract.md. Verified locally end-to-end against
pocketbase 0.39.6 (real-browser wizard run + Dart decrypt interop).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 18:23:05 +02:00