Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | 7x 7x 187x 7x | /**
* What a signed-in member may change on a lead — the UI half of the server's
* staff write-scope allowlist.
*
* WHY THIS EXISTS
* `requireProAccess` (`apps/api/src/middleware/auth.middleware.ts`) now limits
* `staff` to a ten-row write allowlist. Exactly one of those rows is a lead
* route:
*
* { methods: ["POST"], path: /^\/crm\/leads\/[^/]+\/notes$/ }
*
* Every other lead write — `PATCH …/stage`, `PATCH …/quote-value`,
* `PATCH …/order-value`, `PATCH …/:leadId`, `POST …/leads`, archive,
* unarchive, reminders, documents — returns 403 for staff before the handler
* runs. A UI that offers a button the API refuses is worse than one that does
* not, so this module is how the screens agree with the middleware.
*
* **It is a mirror, not a second source of truth.** The allowlist is the
* access control; this only decides what to draw. If the allowlist gains or
* loses a lead row, change this file in the same commit.
*
* THE SHARP EDGE, NOW GUARDED ON BOTH SIDES
* `POST …/crm/leads/:leadId/notes` accepts an optional `newStageId` and moves
* the lead, so the one allowlisted route could reach the one capability staff
* are not meant to have. The server now refuses that field for staff
* (`routes/pro/crm/notes.routes.ts`), which is where the access control
* belongs. `canChangeStage` still gates the stage-change control inside the
* note forms, because a button the API will refuse is worse than no button —
* but it is the courtesy, not the gate.
*/
/** The roles `/api/pro/me` can report. `admin` is a platform admin. */
export type ProRole = "owner" | "manager" | "staff" | "admin";
export interface LeadWriteScope {
/** Move a lead between pipeline stages (incl. marking it Won or Lost). */
canChangeStage: boolean;
/** Edit the lead's own fields: name, phone, email, locality, scope. */
canEditLead: boolean;
/** Set the quote value or the order value. */
canEditValue: boolean;
/** Create a lead, archive or unarchive one, set reminders, add documents. */
canManageLeads: boolean;
/**
* Add, rename, reorder or delete a pipeline stage, and add or retire a lead
* source — `/settings/pipeline`. Separate from `canChangeStage`, which is
* about moving ONE lead: these are different capabilities and conflating
* them is how a settings page ends up editable by someone who can only
* drag a card.
*/
canConfigurePipeline: boolean;
/** Write a note, and mark the lead contacted. The staff capability. */
canWriteNotes: boolean;
}
/** Everything a lead can be written to — owner, manager, platform admin. */
const FULL: LeadWriteScope = {
canChangeStage: true,
canEditLead: true,
canEditValue: true,
canManageLeads: true,
canConfigurePipeline: true,
canWriteNotes: true,
};
/** Staff: notes only. One capability, matching the one allowlisted route. */
const NOTES_ONLY: LeadWriteScope = {
canChangeStage: false,
canEditLead: false,
canEditValue: false,
canManageLeads: false,
canConfigurePipeline: false,
canWriteNotes: true,
};
/**
* What `proRole` may write.
*
* An unknown or not-yet-loaded role gets the full scope on purpose: the server
* is the gate, and locking an owner out of their own pipeline for the one
* render before `/api/pro/me` resolves — or forever, if a future role is added
* to the API and not to this list — is the worse failure. Only the role the
* middleware actually restricts is restricted here.
*/
export function leadWriteScope(proRole: string | null | undefined): LeadWriteScope {
return proRole === "staff" ? NOTES_ONLY : FULL;
}
/**
* What staff are told on the leads screens when a control is missing.
*
* NOT the server's string, deliberately. `STAFF_WRITE_DENIED_MESSAGE` in
* `apps/api/src/middleware/auth.middleware.ts` answers a refused request from
* anywhere in the pro API, so it has to name both capabilities ("add and
* manage project photos and write lead notes") in the third person. This is a
* standing line on one screen, where project photos are not on offer and the
* reader is the member themselves — so it is second-person and scoped to
* leads. Two audiences, two sentences.
*
* What they must agree on is the SCOPE, not the words: notes and logging
* contact, yes; moving a lead or changing its value, no. If the allowlist's
* lead row changes, both strings change.
*/
export const STAFF_LEAD_SCOPE_NOTE =
"You can add notes and log contact on a lead. Moving it, or changing what it is worth, needs a manager or owner.";
|