76 lines
3.7 KiB
JavaScript
76 lines
3.7 KiB
JavaScript
|
|
/// <reference path="../pb_data/types.d.ts" />
|
||
|
|
|
||
|
|
// More than one shared-credentials template, each behind its own code.
|
||
|
|
//
|
||
|
|
// The family shares one set of paid accounts. A few friends are now getting the
|
||
|
|
// same treatment, but must NOT land on the family's subscriptions, so there has to
|
||
|
|
// be a second template with its own credentials and its own code.
|
||
|
|
//
|
||
|
|
// **The bug this migration exists to make safe.** Before it, the route checked the
|
||
|
|
// code and then chose a record with `findRecordsByFilter(..., "-updated", 1, 0)` —
|
||
|
|
// newest wins, with no reference to the code that was given. One record made that
|
||
|
|
// invisible. A second record would have made the family code hand out whichever row
|
||
|
|
// was edited last, and the friends code do the same, silently and with no error.
|
||
|
|
// The hook is rewritten in the same commit; adding a row before that would have been
|
||
|
|
// the whole failure.
|
||
|
|
//
|
||
|
|
// **Why the code lives on the record and not in a new env var.** A second
|
||
|
|
// `AMBER_FRIENDS_CODE` would mean a Coolify env change and a redeploy for every
|
||
|
|
// group, and the codes would sit somewhere other than the credentials they unlock.
|
||
|
|
// Here a new group is one row in the admin UI: no deploy, each code rotatable on its
|
||
|
|
// own, and the code is visible next to what it hands out.
|
||
|
|
//
|
||
|
|
// **The unique index is partial.** PocketBase text fields default to `''`, not NULL,
|
||
|
|
// and SQLite treats two empty strings as equal — so a plain unique index would stop
|
||
|
|
// a second row from existing until both had codes. `WHERE code != ''` is the same
|
||
|
|
// shape PocketBase uses for its own optional-unique columns (see the `email` index on
|
||
|
|
// `users`).
|
||
|
|
//
|
||
|
|
// **No secret is written here.** `AMBER_INVITE_CODE` is deliberately not copied into
|
||
|
|
// the family row: a migration that bakes in a credential is one that leaks it into
|
||
|
|
// every future database dump for no gain. The row is flagged `isDefault` instead, and
|
||
|
|
// the hook keeps honouring the env var for it until the owner pastes a real code in.
|
||
|
|
// That also means anyone halfway through setup right now is unaffected.
|
||
|
|
|
||
|
|
migrate(
|
||
|
|
(app) => {
|
||
|
|
const tpl = app.findCollectionByNameOrId("onboarding_template")
|
||
|
|
// Which group this is, for the admin list. Never served to clients.
|
||
|
|
tpl.fields.add(new Field({ type: "text", name: "name", max: 80 }))
|
||
|
|
// The shared secret that unlocks this row. Still a bearer secret, still
|
||
|
|
// rate-limited per IP in the hook; being per row is what makes it revocable
|
||
|
|
// without touching the others.
|
||
|
|
tpl.fields.add(new Field({ type: "text", name: "code", max: 200 }))
|
||
|
|
// Which row the legacy AMBER_INVITE_CODE still opens.
|
||
|
|
tpl.fields.add(new Field({ type: "bool", name: "isDefault" }))
|
||
|
|
tpl.indexes = (tpl.indexes || []).concat([
|
||
|
|
"CREATE UNIQUE INDEX idx_onboarding_template_code " +
|
||
|
|
"ON onboarding_template (code) WHERE code != ''",
|
||
|
|
])
|
||
|
|
app.save(tpl)
|
||
|
|
|
||
|
|
// Preserve today's behaviour exactly: the row the route is currently handing
|
||
|
|
// out is the most recently updated one, so that is the one the old code must
|
||
|
|
// keep opening. Marking every row would make the fallback ambiguous again.
|
||
|
|
try {
|
||
|
|
const rows = app.findRecordsByFilter(
|
||
|
|
"onboarding_template", "id != ''", "-updated", 1, 0)
|
||
|
|
if (rows && rows.length) {
|
||
|
|
const r = rows[0]
|
||
|
|
r.set("isDefault", true)
|
||
|
|
if (!r.getString("name")) r.set("name", "family")
|
||
|
|
app.save(r)
|
||
|
|
}
|
||
|
|
} catch (_) { /* no template configured yet: nothing to preserve */ }
|
||
|
|
},
|
||
|
|
(app) => {
|
||
|
|
const tpl = app.findCollectionByNameOrId("onboarding_template")
|
||
|
|
tpl.indexes = (tpl.indexes || []).filter(
|
||
|
|
(i) => !i.includes("idx_onboarding_template_code"))
|
||
|
|
tpl.fields.removeByName("name")
|
||
|
|
tpl.fields.removeByName("code")
|
||
|
|
tpl.fields.removeByName("isDefault")
|
||
|
|
app.save(tpl)
|
||
|
|
},
|
||
|
|
)
|