Diff is too large to be shown
135dika / action-plan.docs.md
Last active 2 months ago
135dika revised this gist 2 months ago · 30fdb25
1 file changed, 272 insertions, 186 deletions
135dika revised this gist 2 months ago · 9725535
1 file changed, 186 insertions, 272 deletions
Diff is too large to be shown
135dika revised this gist 2 months ago · 6ce3e5b
1 file changed, 28 insertions
| @@ -11,6 +11,7 @@ Action plan module: **Activity → Task → Subtask** hierarchy with progress en | |||
| 11 | 11 | - `INCOMPLETE_DATA` — not accessible. | |
| 12 | 12 | - `NOT_STARTED` / `IN_PROGRESS` — full access. | |
| 13 | 13 | - `NEED_APPROVAL` / `COMPLETED` / `REJECTED` — read-only for management writes; approval & BOD routes **stay writable**. | |
| 14 | + | - **VP/BOD read visibility** — a member assigned as VP (any item `approverId`) or as BOD (`ACTIVE` round approver) gets **full read** of tree/detail regardless of project tier or `ActionPlanMembers` membership. `MEMBER`-only users without any VP/BOD assignment see only their `ActionPlanMembers` scopes (others pruned). | |
| 14 | 15 | - **Progress input** (submit/respond/feedback) requires project `IN_PROGRESS` (`ProjectInProgressGuard`). | |
| 15 | 16 | ||
| 16 | 17 | ## Status enums | |
| @@ -329,3 +330,30 @@ List rows include `requestedData`, `oldData`, `reason`, `status`, requester/resp | |||
| 329 | 330 | - REVISE → VP: **Action Plan Rejected by BOD** — `{ScopeLabel} {scopeName} in {projectName} was rejected by {bodName}. You must make decision` (+ ` Note: {reason}`). | |
| 330 | 331 | - ROLLBACK → VP: **Action Plan Rolled Back by BOD** — `…was rolled back by {bodName}. Its prerequisite {prereqName} was re-opened for re-approval.` (+ note). | |
| 331 | 332 | - All-APPROVE → VP + admins + members: **Action Plan Approved by BOD** — `…was approved by BOD. Handover complete.` (+ note). | |
| 333 | + | - Downstream consumer released (prerequisite re-closed) → VP: **Action Plan Ready for Approval** — `{ScopeLabel} {scopeName} in {projectName} is now ready for your approval. Its prerequisite {prereqName} was rolled back and has been re-approved and closed.` (+ ` Note: {reason}`). Sent per item as the prerequisite chain re-closes (A → then P/Q → then I…). | |
| 334 | + | ||
| 335 | + | ## Downstream rollback consumers | |
| 336 | + | ||
| 337 | + | When a rollback resolves (B → target X), **all same-level items whose direct prerequisite is X** (and their full subtrees) are affected: | |
| 338 | + | ||
| 339 | + | | State | Effect | | |
| 340 | + | |---|---| | |
| 341 | + | | Item **not yet closed** | `rollbackPaused = true` marker only — data/progress/approvalStatus **unchanged**; display forced `NOT_STARTED`; **on hold** while X re-opens. All writes blocked 400: approval respond/re-request/BOD → `400 <Scope> is on hold: waiting for its prerequisite to be approved and closed`; also update, progress submit/respond, plans, members. | | |
| 342 | + | | Item **CLOSED** | node → `WAITING_APPROVAL` (no progress flip, `approvalReason` nulled); its children display `NOT_STARTED` (marker) until the chain resumes. | | |
| 343 | + | | Release | when X is re-approved & `CLOSED`, each held consumer releases in chain order (A after X closes; A's children after A closes — no progress flip, just `WAITING_APPROVAL`) + *Action Plan Ready for Approval* per item. | | |
| 344 | + | ||
| 345 | + | ## Activity log (`namespace: action_plan`) | |
| 346 | + | ||
| 347 | + | Every BOD/approval interaction writes an activity-log row (`GET /api/projects/:projectId/activity`, `recordId` = the affected item id): | |
| 348 | + | ||
| 349 | + | | Category | When | | |
| 350 | + | |---|---| | |
| 351 | + | | `ACTION_PLAN_APPROVAL_APPROVED` / `_REJECTED` | VP approve / reject | | |
| 352 | + | | `ACTION_PLAN_BOD_ESCALATED` | VP escalates to BOD (`approverCount`, `roundId`) | | |
| 353 | + | | `ACTION_PLAN_BOD_DECISION` | **each** BOD vote — `{decision, actor, reason, roundId}`, incl. votes that leave the round pending/conflicted | | |
| 354 | + | | `ACTION_PLAN_BOD_APPROVED` / `_REVISED` / `_ROLLBACK` | terminal round resolution (revise copy: *must be re-approved by VP*; rollback: `prerequisiteName`) | | |
| 355 | + | | `ACTION_PLAN_ROLLBACK_REOPENED` | **one row per node** restarted by rollback (B subtree + target X subtree): *re-opened for re-approval due to rollback by BOD* | | |
| 356 | + | | `ACTION_PLAN_ROLLBACK_PAUSED` | **one row per held downstream consumer**: *on hold — waiting for prerequisite … (rollback effect)* | | |
| 357 | + | | `ACTION_PLAN_ROLLBACK_RELEASED` | **one row per consumer** released after the prerequisite re-closes: *ready for re-approval — prerequisite … re-approved and closed (rollback effect)* | | |
| 358 | + | ||
| 359 | + | The per-node rows make the multi-item state changes explainable: why each item restarted, paused, or resumed is visible in its own history. | |
135dika revised this gist 2 months ago · a02a26c
1 file changed, 49 insertions, 2 deletions
| @@ -23,6 +23,8 @@ Action plan module: **Activity → Task → Subtask** hierarchy with progress en | |||
| 23 | 23 | ||
| 24 | 24 | `WAITING_APPROVAL` · `CLOSED` · `REJECTED` · `BOD_APPROVAL` (escalated) | |
| 25 | 25 | ||
| 26 | + | > Item without `approverId` (approval optional) never enters this enum — `approvalStatus` stays `null` (§ Approval optional). | |
| 27 | + | ||
| 26 | 28 | ### `bodResult` | |
| 27 | 29 | ||
| 28 | 30 | `REJECTED_REVISE` · `REJECTED_ROLLBACK` | |
| @@ -53,6 +55,34 @@ Action plan module: **Activity → Task → Subtask** hierarchy with progress en | |||
| 53 | 55 | 5. **VP REJECT**: walks descendants level by level; startable leaves reopen (`NEED_APPROVAL`, last `APPROVED`→`PENDING`), leaves blocked by unmet live prerequisite pause (`rollbackPaused`) and auto-resume when the prerequisite closes (`releaseRollbackPausedDependents`). | |
| 54 | 56 | 6. **Delete guard**: progress ≥ 100% (derived `COMPLETED`) or `approvalStatus = CLOSED` → delete blocked 400 (also blocks cascade delete of a complete/closed descendant). | |
| 55 | 57 | ||
| 58 | + | ## Approval optional (derived from `approverId`) | |
| 59 | + | ||
| 60 | + | VP approval is **optional per item**, derived from `approverId` itself (no separate flag): | |
| 61 | + | ||
| 62 | + | - Item **WITH** `approverId` → approval **mandatory**: the full VP/BOD cycle applies — auto `WAITING_APPROVAL` on completion, VP respond, BOD escalation, Closed chip. | |
| 63 | + | - Item **WITHOUT** `approverId` (`null`) → approval **optional**: effective progress ≥ 100 completes the item with `approvalStatus` staying `null` — no `WAITING_APPROVAL`, no *approval-requested* notification, no Closed chip, no BOD possible. | |
| 64 | + | ||
| 65 | + | **Assign-VP rule** (PATCH `approverId`) once the item's effective progress has reached ≥ 100 %: | |
| 66 | + | ||
| 67 | + | | Change | Result | | |
| 68 | + | |---|---| | |
| 69 | + | | assign (`null` → person) | **400** | | |
| 70 | + | | remove (person → `null`) | **400** | | |
| 71 | + | | replace (person → person) | **allowed — pure swap**: only `approverId` changes; `approvalStatus` / `approvalReason` / `approvedAt` / `bodResult` / `bodReason` / `bodRoundId` / `rollbackPaused` all untouched, no reopen, no late-assignment notification | | |
| 72 | + | ||
| 73 | + | Below 100 % the pre-existing assignment semantics are unchanged (assignment never mutates `approvalStatus` on an incomplete item). | |
| 74 | + | ||
| 75 | + | **Prerequisite rule**: | |
| 76 | + | ||
| 77 | + | - VP-less prerequisite (`approverId = null`) → satisfied by effective progress ≥ 100 **alone** (no `CLOSED` needed). | |
| 78 | + | - VP-held prerequisite → still requires `CLOSED` **and** effective progress ≥ 100. | |
| 79 | + | ||
| 80 | + | **Parent completeness** — a child counts toward the parent's `WAITING_APPROVAL` trigger ("done") when `child.approvalStatus = CLOSED` **or** (child has no VP **and** child's approved rollup ≥ 100). | |
| 81 | + | ||
| 82 | + | **All approval endpoints on a VP-less item return 400**: `…/approval/respond` (incl. `ESCALATE`), `…/approval/re-request`, `…/approval/bod`, `…/approval/bod/re-request`. | |
| 83 | + | ||
| 84 | + | **Existing-data migration** (applied mid-feature): prior rows with non-null `approvalStatus` were set to `CLOSED`, and ACTIVE BOD rounds were closed. | |
| 85 | + | ||
| 56 | 86 | --- | |
| 57 | 87 | ||
| 58 | 88 | # Endpoint catalog | |
| @@ -110,6 +140,8 @@ Detail — item payload (§ Item payload). | |||
| 110 | 140 | ### `PATCH /:activityId` | |
| 111 | 141 | Body (`UpdateActivityDto`) — all ✳️: `name`, `activityType`, `approverId`, `workWeight`, `workVolume`, `workVolumeUnit` (*requires workVolume pair rule*), `startDate`, `endDate`, `prerequisiteIds[]`. | |
| 112 | 142 | ||
| 143 | + | > `approverId`: assign / remove blocked 400 once complete (≥ 100 %); replace = pure swap (§ Approval optional). | |
| 144 | + | ||
| 113 | 145 | ### `DELETE /:activityId` | |
| 114 | 146 | No body. **400** if item or live descendant complete (`≥100%`) or `CLOSED`. | |
| 115 | 147 | ||
| @@ -151,6 +183,21 @@ Guards: item `BOD_APPROVAL`, caller is assigned BOD with no vote yet, round not | |||
| 151 | 183 | ### `POST …/approval/bod/re-request` — VP resumes BOD cycle after REVISE | |
| 152 | 184 | No body. | |
| 153 | 185 | ||
| 186 | + | ### `GET /approval-obligations` — pre-member-removal check (project level) | |
| 187 | + | Query: | |
| 188 | + | | Param | Req | Notes | | |
| 189 | + | |---|---|---| | |
| 190 | + | | `userId` | ✅ | uuid — the member whose action-plan responsibilities are checked | | |
| 191 | + | ||
| 192 | + | Purpose: list the user's outstanding approval responsibilities in the project before member removal (removal itself lives outside this module). → `{ "hasObligation": bool, "obligations": […] }`: | |
| 193 | + | ||
| 194 | + | - **VP obligations** — every item where the user is the VP, **all states including `CLOSED`**: | |
| 195 | + | `{ "type": "VP", "scope": "ACTIVITY" | "TASK" | "SUBTASK", "itemId", "itemName" }` | |
| 196 | + | - **BOD obligations** — every `ACTIVE` BOD round the user is an assigned approver, **including rounds they already voted on**: | |
| 197 | + | `{ "type": "BOD", "scope": "ACTIVITY" | "TASK" | "SUBTASK", "itemId", "itemName", "roundId", "responded" }` — `responded` = whether the user cast a decision. | |
| 198 | + | ||
| 199 | + | Empty result → `hasObligation: false`, empty `obligations`. | |
| 200 | + | ||
| 154 | 201 | ## 3. Item payload (detail/list/tree) | |
| 155 | 202 | ||
| 156 | 203 | BOD-related fields on every item: `approvalStatus`, `approvalReason`, `bodRoundId`, `bodResult`, `bodReason`, `bodDisplayStatus`, `rollbackPaused`, plus `approverId/approverName/approverEmail`, `prerequisites[]`, `prerequisiteIds[]`. | |
| @@ -169,7 +216,7 @@ Detail-only (`GET /A`, `GET A/tasks/T`, `GET A/tasks/T/subtasks/S`): | |||
| 169 | 216 | | `POST /` | `CreateTaskDto` — `name` ✅; `workWeight/workVolume/workVolumeUnit/startDate/endDate` ✳️ (volume pair rule) | | |
| 170 | 217 | | `GET /` | list under activity | | |
| 171 | 218 | | `GET /:taskId` | detail (§ Item payload) | | |
| 172 | - | | `PATCH /:taskId` | `UpdateTaskDto` — `name/approverId/workWeight/workVolume/workVolumeUnit/startDate/endDate/prerequisiteIds[]` all ✳️ | | |
| 219 | + | | `PATCH /:taskId` | `UpdateTaskDto` — `name/approverId/workWeight/workVolume/workVolumeUnit/startDate/endDate/prerequisiteIds[]` all ✳️ — `approverId`: assign/remove blocked 400 once complete, replace = pure swap (§ Approval optional) | | |
| 173 | 220 | | `DELETE /:taskId` | complete/CLOSED guard 400 | | |
| 174 | 221 | ||
| 175 | 222 | Plus per task: progress (`POST/GET /:taskId/progress`, `GET /:taskId/progress/:progressId`, `POST …/respond`), feedbacks, `GET/PUT /:taskId/plans`, members (`POST/DELETE/GET /:taskId/members`), edit-requests (`POST/GET /:taskId/edit-requests`), approval routes (§ 2). | |
| @@ -181,7 +228,7 @@ Plus per task: progress (`POST/GET /:taskId/progress`, `GET /:taskId/progress/:p | |||
| 181 | 228 | | `POST /` | `CreateSubtaskDto` — `name` ✅; `workVolume/workVolumeUnit/workWeight/startDate/endDate` ✳️ (volume pair rule) | | |
| 182 | 229 | | `GET /` | list under task | | |
| 183 | 230 | | `GET /:subtaskId` | detail | | |
| 184 | - | | `PATCH /:subtaskId` | `UpdateSubtaskDto` — all ✳️ incl `approverId`, `prerequisiteIds[]` | | |
| 231 | + | | `PATCH /:subtaskId` | `UpdateSubtaskDto` — all ✳️ incl `approverId`, `prerequisiteIds[]` — `approverId`: assign/remove blocked 400 once complete, replace = pure swap (§ Approval optional) | | |
| 185 | 232 | | `DELETE /:subtaskId` | complete/CLOSED guard 400 | | |
| 186 | 233 | ||
| 187 | 234 | Plus: members (`POST/DELETE/GET /:subtaskId/members`), plans (`GET/PUT /:subtaskId/plans`), edit-requests (`POST/GET /:subtaskId/edit-requests`), progress (`POST/GET /:subtaskId/progress`, `GET /:subtaskId/progress/:progressId`, `POST …/respond`), feedbacks (`POST/GET …/feedbacks`, `PATCH/DELETE …/feedbacks/:feedbackId`), approval routes (§ 2). | |
135dika revised this gist 2 months ago · a5ab435
1 file changed, 199 insertions, 192 deletions
Diff is too large to be shown
135dika revised this gist 2 months ago · 62e03eb
1 file changed, 5 insertions, 4 deletions
| @@ -60,10 +60,11 @@ Derived from progress rows + approval state. Display overrides applied (see BOD | |||
| 60 | 60 | 1. **VP** (designated approver) responds to an item's approval: `APPROVE` | `REJECT` | `ESCALATE`. `ESCALATE` requires `bodUserIds` (≥1, max 50; each must be a user in the project's program agenda) → approvalStatus `BOD_APPROVAL`, a new `ActionPlanBodRounds` round created, notification sent to each BOD. | |
| 61 | 61 | 2. **Each assigned BOD** votes on the round: `APPROVE` | `REVISE` | `ROLLBACK` (reason required, 1–500 chars). | |
| 62 | 62 | - One **rollback target lock** applies: once a BOD votes `ROLLBACK` to target T, all other BODs may only vote `ROLLBACK` to the same T (a different `rollbackId` → 400). `rollbackId` must be a **direct prerequisite** of the item. | |
| 63 | - | 3. **Resolution** when all assigned BODs have voted: | |
| 64 | - | - Any `REVISE` → REVISE wins (precedence: `REVISE > ROLLBACK > APPROVE`). Item → `WAITING_APPROVAL`, `bodResult = REJECTED_REVISE`, `bodReason` = earliest voter's reason; data & progress rows untouched; VP notified ("Action Plan Rejected by BOD"). | |
| 65 | - | - All `ROLLBACK` to same target T → item → `WAITING_APPROVAL`, `bodResult = REJECTED_ROLLBACK`; T's and item's subtrees restart (leaves: last `APPROVED` progress → `PENDING`; each node → `WAITING_APPROVAL`; nodes whose prerequisites aren't met → `rollbackPaused` until the prerequisite closes). VP notified ("Action Plan Rolled Back by BOD"). | |
| 66 | - | - All `APPROVE` → item `approvalStatus = CLOSED` (handover); VP + admins + action plan members notified ("Action Plan Approved by BOD"). | |
| 63 | + | 3. **Resolution** when all assigned BODs have voted — precedence `ROLLBACK > REVISE > APPROVE`: | |
| 64 | + | - Any/all `ROLLBACK` votes to the same target T → ROLLBACK wins. Item → `WAITING_APPROVAL`, `bodResult = REJECTED_ROLLBACK`, `bodReason` = earliest voter's reason; T's and item's subtrees restart (leaves: last `APPROVED` progress → `PENDING`; each node → `WAITING_APPROVAL`; nodes whose prerequisites aren't met → `rollbackPaused` until the prerequisite closes). VP notified ("Action Plan Rolled Back by BOD"). | |
| 65 | + | - Else any `REVISE` → REVISE wins. Item → `WAITING_APPROVAL`, `bodResult = REJECTED_REVISE`, `bodReason` = earliest voter's reason; data & progress rows untouched; VP notified ("Action Plan Rejected by BOD"). | |
| 66 | + | - Else (all `APPROVE`) → item `approvalStatus = CLOSED` (handover); VP + admins + action plan members notified ("Action Plan Approved by BOD"). | |
| 67 | + | - Different `ROLLBACK` targets across voters → round stays `ACTIVE`, item stays `BOD_APPROVAL`, no writes (defensive; the single-target lock normally prevents this). | |
| 67 | 68 | 4. **Re-escalation**: VP `ESCALATE` again after a BOD REVISE carries the previous BOD list over; may *add* members, may not remove/replace. | |
| 68 | 69 | 5. **VP `REJECT`** (non-escalated flow) does not reopen closed children wholesale: it walks descendants level by level (Activity → Tasks → Subtasks). Leaves that are startable (no unmet live prerequisite) reopen → `NEED_APPROVAL`/`PENDING`; leaves blocked by an unmet prerequisite pause with `rollbackPaused = true` and resume automatically when the prerequisite reaches `CLOSED` (`.releaseRollbackPausedDependents`). | |
| 69 | 70 | 6. **Delete guard**: an item whose effective progress ≥ 100% (derived `COMPLETED`) or `approvalStatus = CLOSED` (VP- or BOD-approved) cannot be deleted — including as a descendant of a parent delete (`400` naming the offending item). | |
135dika revised this gist 2 months ago · 1fd34e1
1 file changed, 204 insertions, 281 deletions
Diff is too large to be shown
135dika revised this gist 3 months ago · d0c38dd
1 file changed, 17 insertions, 15 deletions
| @@ -146,20 +146,24 @@ DELETE /.../feedbacks/:fId | |||
| 146 | 146 | ||
| 147 | 147 | ## Authorization | |
| 148 | 148 | ||
| 149 | - | Two-layer authorization at the controller level via guards. | |
| 149 | + | Two-layer authorization at the controller level. | |
| 150 | 150 | ||
| 151 | 151 | ### Guards | |
| 152 | 152 | ||
| 153 | 153 | | Guard | Applies to | Check | | |
| 154 | 154 | |---|---|---| | |
| 155 | + | | `AccessControlGuard` via `@Authorize({ membership: true })` | All routes | Token validation + workspace membership via `resolveAccess()` gRPC | | |
| 155 | 156 | | `ProjectInProgressGuard` | All routes | Project status must be `IN_PROGRESS` | | |
| 156 | - | | `WorkspaceAccessGuard` | All routes | User must have active workspace membership for this project | | |
| 157 | 157 | ||
| 158 | - | Guards run before any route handler. Authentication is handled upstream by `AccessControlGuard` (validates Bearer token). | |
| 158 | + | `@Authorize({ membership: true })` runs `AccessControlGuard` which: | |
| 159 | + | 1. Validates bearer token via AuthService gRPC | |
| 160 | + | 2. Resolves scope from `:projectId` route param | |
| 161 | + | 3. Calls `workspaceAccess.resolveAccess()` — checks user has any membership (MEMBER/OWNER/ADMIN) in the workspace/project hierarchy | |
| 162 | + | 4. Stashes validated session so `@CurrentUser()` reuses it without extra gRPC | |
| 159 | 163 | ||
| 160 | - | ### WorkspaceAccessGuard — How It Works | |
| 164 | + | ### Hierarchy Scope | |
| 161 | 165 | ||
| 162 | - | Every action-plan request requires the user to have an `active` row in `workspace_member_scopes` for the project's hierarchy. The guard resolves the hierarchy from `:projectId`: | |
| 166 | + | `resolveAccess()` checks membership across the full hierarchy: | |
| 163 | 167 | ||
| 164 | 168 | ``` | |
| 165 | 169 | AGENDA | |
| @@ -167,25 +171,22 @@ AGENDA | |||
| 167 | 171 | └─ PROJECT ← projectId from route param | |
| 168 | 172 | ``` | |
| 169 | 173 | ||
| 170 | - | User passes if they have at least one matching row with: | |
| 171 | - | - `status = 'active'` | |
| 172 | - | - `role IN (MEMBER, OWNER, ADMIN)` | |
| 173 | - | - One of: `scope_type=PROJECT, scope_id=projectId` OR `scope_type=WORKSPACE` OR `scope_type=PROGRAM` OR `scope_type=AGENDA` | |
| 174 | + | A user with membership at any ancestor level (WORKSPACE/PROGRAM/AGENDA) can access child projects. | |
| 174 | 175 | ||
| 175 | - | Rejection response — `403 Forbidden`: | |
| 176 | + | ### Rejection Response — `403 Forbidden`: | |
| 176 | 177 | ```json | |
| 177 | - | { "statusCode": 403, "message": "You do not have access to this project" } | |
| 178 | + | { "statusCode": 403, "message": "Access denied", "denial": { "type": "...", "details": {...} } } | |
| 178 | 179 | ``` | |
| 179 | 180 | ||
| 180 | 181 | ### Write Operation — Role Check | |
| 181 | 182 | ||
| 182 | - | Write operations have an additional inline role check via `validateWorkspaceRole` helper in `action-plans.helpers.ts`. | |
| 183 | + | Write operations have an additional inline role check via `validateWorkspaceRole` helper in `action-plans.helpers.ts`. This queries `workspace_member_scopes` directly to enforce exact role requirements (OWNER/ADMIN for admin ops, MEMBER+ for member ops). | |
| 183 | 184 | ||
| 184 | 185 | ### Operation → Role Mapping | |
| 185 | 186 | ||
| 186 | 187 | | Operation | Required Role | | |
| 187 | 188 | |---|---| | |
| 188 | - | | Read (GET any route) | `MEMBER` / `OWNER` / `ADMIN` (via guard) | | |
| 189 | + | | Read (GET any route) | Any workspace member (via guard) | | |
| 189 | 190 | | Create activity/task/subtask | `OWNER` / `ADMIN` | | |
| 190 | 191 | | Update activity/task/subtask | `OWNER` / `ADMIN` | | |
| 191 | 192 | | Delete activity/task/subtask | `OWNER` / `ADMIN` | | |
| @@ -200,7 +201,8 @@ Write operations have an additional inline role check via `validateWorkspaceRole | |||
| 200 | 201 | ||
| 201 | 202 | | Status | Message | Cause | | |
| 202 | 203 | |---|---|---| | |
| 203 | - | | `403` | `You do not have access to this project` | Not a workspace member (guard) | | |
| 204 | + | | `401` | `Authorization token required` | Missing/invalid bearer token | | |
| 205 | + | | `403` | `Access denied` (with `denial` payload) | Not a workspace member (AccessControlGuard) | | |
| 204 | 206 | | `403` | `Action plan is only available for projects with IN_PROGRESS status` | Project status not IN_PROGRESS | | |
| 205 | 207 | | `403` | `You do not have the required role to perform this action` | Insufficient role for write op | | |
| 206 | 208 | | `400` | `You are not assigned to this scope...` | Not in `actionPlanMembers` (member ops) | | |
| @@ -208,7 +210,7 @@ Write operations have an additional inline role check via `validateWorkspaceRole | |||
| 208 | 210 | ### Double-Check for Member Operations | |
| 209 | 211 | ||
| 210 | 212 | Member operations (`createProgress`, `createEditRequest`) have two-tier write validation: | |
| 211 | - | 1. **Tier 1** — `validateWorkspaceRole`: user must have active membership in workspace | |
| 213 | + | 1. **Tier 1** — `validateWorkspaceRole`: user must have active workspace membership with required role | |
| 212 | 214 | 2. **Tier 2** — `validateScopeMember`: user must be assigned to the specific scope via `actionPlanMembers` | |
| 213 | 215 | ||
| 214 | 216 | --- | |
135dika revised this gist 3 months ago · b10f2aa
1 file changed, 38 insertions, 10 deletions
| @@ -146,27 +146,46 @@ DELETE /.../feedbacks/:fId | |||
| 146 | 146 | ||
| 147 | 147 | ## Authorization | |
| 148 | 148 | ||
| 149 | - | All write operations require the actor to have an active role in `workspace_member_scopes`. Validation via shared helper `validateWorkspaceRole` in `action-plans.helpers.ts`. | |
| 149 | + | Two-layer authorization at the controller level via guards. | |
| 150 | 150 | ||
| 151 | - | ### Role Hierarchy Scope | |
| 151 | + | ### Guards | |
| 152 | 152 | ||
| 153 | - | Authorization checks across 4 scope levels — owner/admin at any level can manage child projects: | |
| 153 | + | | Guard | Applies to | Check | | |
| 154 | + | |---|---|---| | |
| 155 | + | | `ProjectInProgressGuard` | All routes | Project status must be `IN_PROGRESS` | | |
| 156 | + | | `WorkspaceAccessGuard` | All routes | User must have active workspace membership for this project | | |
| 157 | + | ||
| 158 | + | Guards run before any route handler. Authentication is handled upstream by `AccessControlGuard` (validates Bearer token). | |
| 159 | + | ||
| 160 | + | ### WorkspaceAccessGuard — How It Works | |
| 161 | + | ||
| 162 | + | Every action-plan request requires the user to have an `active` row in `workspace_member_scopes` for the project's hierarchy. The guard resolves the hierarchy from `:projectId`: | |
| 154 | 163 | ||
| 155 | 164 | ``` | |
| 156 | 165 | AGENDA | |
| 157 | 166 | └─ PROGRAM | |
| 158 | - | └─ PROJECT ← projectId from request | |
| 167 | + | └─ PROJECT ← projectId from route param | |
| 168 | + | ``` | |
| 169 | + | ||
| 170 | + | User passes if they have at least one matching row with: | |
| 171 | + | - `status = 'active'` | |
| 172 | + | - `role IN (MEMBER, OWNER, ADMIN)` | |
| 173 | + | - One of: `scope_type=PROJECT, scope_id=projectId` OR `scope_type=WORKSPACE` OR `scope_type=PROGRAM` OR `scope_type=AGENDA` | |
| 174 | + | ||
| 175 | + | Rejection response — `403 Forbidden`: | |
| 176 | + | ```json | |
| 177 | + | { "statusCode": 403, "message": "You do not have access to this project" } | |
| 159 | 178 | ``` | |
| 160 | 179 | ||
| 161 | - | A user must have an `active` row in `workspace_member_scopes` with: | |
| 162 | - | - `role` matching the required role set | |
| 163 | - | - `user_id` = actor | |
| 164 | - | - One of: `scope_type=PROJECT, scope_id=projectId` OR `scope_type=WORKSPACE, workspace_id=project.workspace_id` OR `scope_type=PROGRAM, scope_id=project.program_id` OR `scope_type=AGENDA, scope_id=project.program.agenda_id` | |
| 180 | + | ### Write Operation — Role Check | |
| 181 | + | ||
| 182 | + | Write operations have an additional inline role check via `validateWorkspaceRole` helper in `action-plans.helpers.ts`. | |
| 165 | 183 | ||
| 166 | 184 | ### Operation → Role Mapping | |
| 167 | 185 | ||
| 168 | 186 | | Operation | Required Role | | |
| 169 | 187 | |---|---| | |
| 188 | + | | Read (GET any route) | `MEMBER` / `OWNER` / `ADMIN` (via guard) | | |
| 170 | 189 | | Create activity/task/subtask | `OWNER` / `ADMIN` | | |
| 171 | 190 | | Update activity/task/subtask | `OWNER` / `ADMIN` | | |
| 172 | 191 | | Delete activity/task/subtask | `OWNER` / `ADMIN` | | |
| @@ -177,9 +196,18 @@ A user must have an `active` row in `workspace_member_scopes` with: | |||
| 177 | 196 | | Submit progress | `MEMBER` / `OWNER` / `ADMIN` + `actionPlanMembers` check | | |
| 178 | 197 | | Create edit request | `MEMBER` / `OWNER` / `ADMIN` + `actionPlanMembers` check | | |
| 179 | 198 | ||
| 199 | + | ### Rejection Responses | |
| 200 | + | ||
| 201 | + | | Status | Message | Cause | | |
| 202 | + | |---|---|---| | |
| 203 | + | | `403` | `You do not have access to this project` | Not a workspace member (guard) | | |
| 204 | + | | `403` | `Action plan is only available for projects with IN_PROGRESS status` | Project status not IN_PROGRESS | | |
| 205 | + | | `403` | `You do not have the required role to perform this action` | Insufficient role for write op | | |
| 206 | + | | `400` | `You are not assigned to this scope...` | Not in `actionPlanMembers` (member ops) | | |
| 207 | + | ||
| 180 | 208 | ### Double-Check for Member Operations | |
| 181 | 209 | ||
| 182 | - | Member operations (`createProgress`, `createEditRequest`) have two-tier validation: | |
| 210 | + | Member operations (`createProgress`, `createEditRequest`) have two-tier write validation: | |
| 183 | 211 | 1. **Tier 1** — `validateWorkspaceRole`: user must have active membership in workspace | |
| 184 | 212 | 2. **Tier 2** — `validateScopeMember`: user must be assigned to the specific scope via `actionPlanMembers` | |
| 185 | 213 | ||
| @@ -320,4 +348,4 @@ Response shape (all scopes): | |||
| 320 | 348 | - Subtask in progress → `workWeight`/`workVolume` locked | |
| 321 | 349 | ||
| 322 | 350 | ### Soft Delete | |
| 323 | - | All deletes are soft (`deletedAt`). Cascades: deleting activity → all tasks + subtasks. Deleting task → all subtasks. | |
| 351 | + | All deletes are soft (`deletedAt`). Cascades: deleting activity → all tasks + subtasks. Deleting task → all subtasks. | |
135dika revised this gist 3 months ago · 771c2a7
1 file changed, 72 insertions, 17 deletions
| @@ -144,26 +144,81 @@ DELETE /.../feedbacks/:fId | |||
| 144 | 144 | ||
| 145 | 145 | --- | |
| 146 | 146 | ||
| 147 | + | ## Authorization | |
| 148 | + | ||
| 149 | + | All write operations require the actor to have an active role in `workspace_member_scopes`. Validation via shared helper `validateWorkspaceRole` in `action-plans.helpers.ts`. | |
| 150 | + | ||
| 151 | + | ### Role Hierarchy Scope | |
| 152 | + | ||
| 153 | + | Authorization checks across 4 scope levels — owner/admin at any level can manage child projects: | |
| 154 | + | ||
| 155 | + | ``` | |
| 156 | + | AGENDA | |
| 157 | + | └─ PROGRAM | |
| 158 | + | └─ PROJECT ← projectId from request | |
| 159 | + | ``` | |
| 160 | + | ||
| 161 | + | A user must have an `active` row in `workspace_member_scopes` with: | |
| 162 | + | - `role` matching the required role set | |
| 163 | + | - `user_id` = actor | |
| 164 | + | - One of: `scope_type=PROJECT, scope_id=projectId` OR `scope_type=WORKSPACE, workspace_id=project.workspace_id` OR `scope_type=PROGRAM, scope_id=project.program_id` OR `scope_type=AGENDA, scope_id=project.program.agenda_id` | |
| 165 | + | ||
| 166 | + | ### Operation → Role Mapping | |
| 167 | + | ||
| 168 | + | | Operation | Required Role | | |
| 169 | + | |---|---| | |
| 170 | + | | Create activity/task/subtask | `OWNER` / `ADMIN` | | |
| 171 | + | | Update activity/task/subtask | `OWNER` / `ADMIN` | | |
| 172 | + | | Delete activity/task/subtask | `OWNER` / `ADMIN` | | |
| 173 | + | | Set plans | `OWNER` / `ADMIN` | | |
| 174 | + | | Add/remove member (any scope) | `OWNER` / `ADMIN` | | |
| 175 | + | | Respond progress (approve/reject) | `OWNER` / `ADMIN` | | |
| 176 | + | | Respond edit request | `OWNER` / `ADMIN` | | |
| 177 | + | | Submit progress | `MEMBER` / `OWNER` / `ADMIN` + `actionPlanMembers` check | | |
| 178 | + | | Create edit request | `MEMBER` / `OWNER` / `ADMIN` + `actionPlanMembers` check | | |
| 179 | + | ||
| 180 | + | ### Double-Check for Member Operations | |
| 181 | + | ||
| 182 | + | Member operations (`createProgress`, `createEditRequest`) have two-tier validation: | |
| 183 | + | 1. **Tier 1** — `validateWorkspaceRole`: user must have active membership in workspace | |
| 184 | + | 2. **Tier 2** — `validateScopeMember`: user must be assigned to the specific scope via `actionPlanMembers` | |
| 185 | + | ||
| 186 | + | --- | |
| 187 | + | ||
| 147 | 188 | ## Notifications | |
| 148 | 189 | ||
| 149 | - | Fire-and-forget. Does not block operation on failure. | |
| 190 | + | Fire-and-forget. Does not block operation on failure. All events are **bidirectional** — both actor (pelaku) and target (korban) receive the notification for audit trail. | |
| 150 | 191 | ||
| 151 | - | ### Member Notifications | |
| 152 | - | | Trigger | Type | Recipient | | |
| 153 | - | |---|---|---| | |
| 154 | - | | Add to subtask | `action_plan_member_added` | Added user | | |
| 155 | - | | Remove from subtask | `action_plan_member_removed` | Removed user | | |
| 156 | - | | Add to task | `action_plan_member_added` | Added user | | |
| 157 | - | | Remove from task | `action_plan_member_removed` | Removed user | | |
| 158 | - | | Add to activity | `action_plan_member_added` | Added user | | |
| 159 | - | | Remove from activity | `action_plan_member_removed` | Removed user | | |
| 160 | - | ||
| 161 | - | ### Progress Respond Notifications | |
| 162 | - | | Verb | Type | Recipient | | |
| 192 | + | ### Event Keys | |
| 193 | + | ||
| 194 | + | | Event Key | Category | Description | | |
| 163 | 195 | |---|---|---| | |
| 164 | - | | APPROVE | `action_plan_progress_approve` | Submitter | | |
| 165 | - | | APPROVE_WITH_NOTES | `action_plan_progress_approve_with_notes` | Submitter | | |
| 166 | - | | REJECT | `action_plan_progress_reject` | Submitter | | |
| 196 | + | | `action_plan.member.added` | Targeted (actor + target) | Member assigned to scope | | |
| 197 | + | | `action_plan.member.removed` | Targeted (actor + target) | Member removed from scope | | |
| 198 | + | | `action_plan.progress.submitted` | Broadcast (all approvers) | Member submits progress | | |
| 199 | + | | `action_plan.progress.approved` | Targeted (actor + target) | Admin approves progress | | |
| 200 | + | | `action_plan.progress.rejected` | Targeted (actor + target) | Admin rejects progress | | |
| 201 | + | | `action_plan.edit_request.requested` | Broadcast (all approvers) | Member requests edit | | |
| 202 | + | | `action_plan.edit_request.responded` | Targeted (actor + target) | Admin responds to edit request | | |
| 203 | + | ||
| 204 | + | ### Recipient Resolution | |
| 205 | + | ||
| 206 | + | **Broadcast events** (`progress.submitted`, `edit_request.requested`): | |
| 207 | + | Resolve all `OWNER`/`ADMIN` users from `workspace_member_scopes` across 4-level hierarchy (PROJECT → WORKSPACE → PROGRAM → AGENDA). Plus the submitter for audit trail. | |
| 208 | + | ||
| 209 | + | **Targeted events** (all others): | |
| 210 | + | Notify the specific actor + the specific target user only. No broadcast to hierarchy owners. | |
| 211 | + | ||
| 212 | + | ### Notification Recipients by Trigger | |
| 213 | + | ||
| 214 | + | | Trigger | Recipients | | |
| 215 | + | |---|---| | |
| 216 | + | | Submit progress | Submitter + all OWNER/ADMIN in hierarchy | | |
| 217 | + | | Approve/reject progress | Submitter + approving admin | | |
| 218 | + | | Submit edit request | All OWNER/ADMIN in hierarchy | | |
| 219 | + | | Respond edit request | Requester + responding admin | | |
| 220 | + | | Add member | Added user + admin who added | | |
| 221 | + | | Remove member | Removed user + admin who removed | | |
| 167 | 222 | ||
| 168 | 223 | --- | |
| 169 | 224 | ||
| @@ -265,4 +320,4 @@ Response shape (all scopes): | |||
| 265 | 320 | - Subtask in progress → `workWeight`/`workVolume` locked | |
| 266 | 321 | ||
| 267 | 322 | ### Soft Delete | |
| 268 | - | All deletes are soft (`deletedAt`). Cascades: deleting activity → all tasks + subtasks. Deleting task → all subtasks. | |
| 323 | + | All deletes are soft (`deletedAt`). Cascades: deleting activity → all tasks + subtasks. Deleting task → all subtasks. | |