kanban-dev-webapp/pocketbase/pb_migrations/1789600900_applied_api_rules.js
Dave ba990f0109 feat(schema): all collections + role-scoped API rules on remote PB
- migrations/ documented in pocketbase/pb_migrations/ for local PB
- scripts/setup-remote.ts applies them via admin API to remote PB
- collections: clients, projects, columns, cards, subtasks, comments, activity, settings
- user extension: role (owner|client_manager), client (relation)
- writes: owner only for app collections, superuser-only for activity and settings
2026-09-16 09:41:20 +02:00

121 lines
No EOL
3.3 KiB
JavaScript

/// <reference path="../pb_data/types.d.ts" />
/**
* The permission boundary.
*
* Reads: scoped by role. An owner reads everything; a client_manager reads
* only records belonging to their own client, via relation traversal.
*
* Writes: allowed for the owner, denied for everyone else. A client_manager's
* token is refused by PocketBase outright. Their writes arrive instead
* through /api/client/* route handlers that authenticate as a superuser and
* copy across only whitelisted fields, because PocketBase rules are
* record-level and cannot say "priority yes, column no".
*
* Do not relax a write rule here to make a client-side form work. That form
* is meant to POST to a route handler.
*
* Rule value meanings in PocketBase, which are easy to confuse:
* null -> superuser only
* "" -> anyone, including unauthenticated
*/
migrate(
(app) => {
const OWNER = '@request.auth.role = "owner"';
/** Read rule for a collection reachable from a project relation. */
const scopedRead = (pathToClient) =>
`@request.auth.id != "" && (${OWNER} || ${pathToClient} = @request.auth.client)`;
const apply = (name, { list, view, create, update, del }) => {
const c = app.findCollectionByNameOrId(name);
c.listRule = list;
c.viewRule = view;
c.createRule = create;
c.updateRule = update;
c.deleteRule = del;
app.save(c);
};
apply("clients", {
list: `@request.auth.id != "" && (${OWNER} || id = @request.auth.client)`,
view: `@request.auth.id != "" && (${OWNER} || id = @request.auth.client)`,
create: OWNER,
update: OWNER,
del: OWNER,
});
apply("projects", {
list: scopedRead("client"),
view: scopedRead("client"),
create: OWNER,
update: OWNER,
del: OWNER,
});
apply("columns", {
list: scopedRead("project.client"),
view: scopedRead("project.client"),
create: OWNER,
update: OWNER,
del: OWNER,
});
apply("cards", {
list: scopedRead("project.client"),
view: scopedRead("project.client"),
create: OWNER,
update: OWNER,
del: OWNER,
});
apply("subtasks", {
list: scopedRead("card.project.client"),
view: scopedRead("card.project.client"),
create: OWNER,
update: OWNER,
del: OWNER,
});
apply("comments", {
list: scopedRead("card.project.client"),
view: scopedRead("card.project.client"),
create: OWNER,
update: OWNER,
del: OWNER,
});
// Activity is append-only and never written from a browser: even the
// owner's creates go through server code, so create stays superuser-only.
apply("activity", {
list: scopedRead("project.client"),
view: scopedRead("project.client"),
create: null,
update: null,
del: null,
});
// settings keeps every rule null — superuser only. See its migration.
return true;
},
(app) => {
const open = '@request.auth.id != ""';
for (const name of [
"clients",
"projects",
"columns",
"cards",
"subtasks",
"comments",
"activity",
]) {
const c = app.findCollectionByNameOrId(name);
c.listRule = open;
c.viewRule = open;
c.createRule = null;
c.updateRule = null;
c.deleteRule = null;
app.save(c);
}
return true;
}
);