- 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
121 lines
No EOL
3.3 KiB
JavaScript
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;
|
|
}
|
|
); |