99 lines
4.3 KiB
Markdown
99 lines
4.3 KiB
Markdown
# Backend Contract Proposal — Club Rank Permission Matrix (WEB-W1)
|
|
|
|
Status: **Proposed / not implemented.** This documents the backend work required
|
|
before the "Operational Permission Matrix" on
|
|
`app/clubs/[clubId]/manage/ranks-roles/page.tsx` can be made editable. Until this
|
|
ships, the matrix is rendered read-only (`persistenceAvailable={false}`).
|
|
|
|
## Problem
|
|
|
|
`handleSavePermissions` previously waited ~400 ms and showed a "Saved" state
|
|
without writing anything (confirmed in source prior to this change). There is no
|
|
Firestore document, API route, or security rule that stores or enforces a
|
|
per-rank capability matrix. The enforced club authorization model is **role
|
|
based**, derived solely from the administrative role on the member document:
|
|
|
|
- `lib/club-data.ts` → `getPortalPermissions(role)` maps `owner`/`admin` →
|
|
`canManageSettings`, `canInviteMembers`, `canApproveApplications`,
|
|
`canPlanRides`; `member` → all false.
|
|
- Display rank (`President`, `Road Captain`, …) is cosmetic and explicitly does
|
|
**not** grant software authority.
|
|
|
|
Persisting the matrix without enforcement would mislead club owners into
|
|
believing a toggle restricts members when it does nothing. That is prohibited by
|
|
the project rules (no unenforced permission models, no false success).
|
|
|
|
## Why this was not implemented in the website task
|
|
|
|
- Enforcement requires **Firestore security rule** changes (out of scope / frozen).
|
|
- A new persistence model must not conflict with the existing Flutter club role
|
|
model (`owner`/`admin`/`member`). Rank → capability is a different axis and
|
|
needs a product decision on precedence.
|
|
|
|
## Proposed contract (for a future, authorized backend change)
|
|
|
|
### 1. Storage
|
|
|
|
Document: `clubs/{clubId}/settings/rankPermissions`
|
|
|
|
```jsonc
|
|
{
|
|
"version": 1,
|
|
"updatedAt": "<serverTimestamp>",
|
|
"updatedByUid": "<uid>",
|
|
"ranks": {
|
|
"road_captain": {
|
|
"planRides": true,
|
|
"manageApplications": false,
|
|
"postAnnouncements": true,
|
|
"moderateWall": true,
|
|
"manageRoster": false,
|
|
"editProfile": false
|
|
}
|
|
// ... one entry per rank id in DEFAULT_CLUB_RANKS except `president`
|
|
}
|
|
}
|
|
```
|
|
|
|
Invariants:
|
|
- `president` is immutable (always full authority); never written or editable.
|
|
- Unknown rank ids and unknown capability keys are rejected.
|
|
- Capability keys are constrained to `DEFAULT_CLUB_CAPABILITIES` ids.
|
|
|
|
### 2. Security rules (requires authorized change to the frozen rules)
|
|
|
|
- Read: any active club member.
|
|
- Write: only `clubs/{clubId}.ownerUid == request.auth.uid`, or an active member
|
|
whose `role == 'admin'`. Validate the written shape (version, allowed rank ids,
|
|
boolean capability values only). Reject writes that would alter `president`.
|
|
|
|
### 3. Enforcement precedence (product decision required)
|
|
|
|
Rank capabilities may only **narrow**, never widen, what the administrative role
|
|
already denies. Administrative `role` remains the hard security boundary enforced
|
|
in rules; rank capabilities are a soft, owner-configurable delegation layer that
|
|
the Flutter and web clients honor for UI affordances. The Flutter club model must
|
|
adopt the same precedence so web and mobile agree.
|
|
|
|
### 4. Website wiring once the above exists
|
|
|
|
1. Add `fetchRankPermissions(clubId)` and `saveRankPermissions(clubId, callerUid, matrix)`
|
|
to `lib/club-data.ts`, mirroring the authorization pattern of
|
|
`updateClubAccessSettings` (owner/admin check + `updateDoc`).
|
|
2. In `ranks-roles/page.tsx`: load persisted permissions in `loadData`, and
|
|
implement `handleSavePermissions` to call `saveRankPermissions`, then re-read
|
|
to confirm the write survived before showing success.
|
|
3. Flip `<ClubPermissionMatrix persistenceAvailable={true} />` and pass
|
|
`onSavePermissions` / `onResetDefaults` / `isSaving`.
|
|
4. Add tests: persistence round-trip (save → reload → values survive), owner/admin
|
|
authorization success, member/unauthenticated rejection, `president`
|
|
immutability, and rejection of unknown rank/capability keys.
|
|
|
|
## Current truthful behavior (shipped in this branch)
|
|
|
|
- Matrix renders as a read-only reference of the standard governance model.
|
|
- No Save/Reset controls; a "View-only reference" indicator and an availability
|
|
notice explain that access is enforced by Owner/Admin roles and that per-rank
|
|
customization is planned.
|
|
- No save is attempted and no success is reported.
|