Skip to content

Feat/multiple staff users per facility fixed roles no - #163

Open
dahmeezy wants to merge 3 commits into
DocForum:mainfrom
dahmeezy:feat/multiple-staff-users-per-facility-fixed-roles-no
Open

dahmeezy wants to merge 3 commits into
DocForum:mainfrom
dahmeezy:feat/multiple-staff-users-per-facility-fixed-roles-no

Conversation

@dahmeezy

Copy link
Copy Markdown

Closes #75

Status: Draft. Integration test suite is not yet green (see "Test status").

What changed and why

FacilityProfile was 1:1 with User, so a pharmacy with several pharmacists shared one login and lost attribution (PRD §7). This adds multiple staff accounts per facility with exactly two fixed roles (owner, staff) and no permissions matrix.

Schema and migration

  • New FacilityRole enum (owner | staff), enforced at DB level.
  • New FacilityMember model (userId unique, facilityId, role).
  • New FacilityInvite model (tokenHash unique, expiresAt, role, facilityId).
  • User now relates to FacilityMember instead of FacilityProfile.
  • Migration 20261019131059_add_facility_member_and_invite creates the tables and converts every existing FacilityProfile/User pair into a FacilityMember with role=owner.

Services

  • members.ts: getMembers, createMember, removeMember, isOwner, isMember. The last owner cannot be removed.
  • invites.ts: createInvite, acceptInvite, listInvites, revokeInvite. Tokens are random and stored as SHA-256 hashes (never plaintext). Expiry uses the configured TTL. Tokens are single-use, consumed with a conditional update.

Middleware (middleware/facility-guard.ts)

  • requireFacility resolves membership via FacilityMember and exposes facilityId and memberRole.
  • requireFacilityOwner guards management endpoints.
  • requireFacilityMember is the general guard.
  • Facility routes now use these guards.

Attribution

  • Acting staff userId is recorded alongside the facility on fulfillment events (append-only). [VERIFY: list each fulfillment action now attributed, and confirm production code writes the actor, not only tests.]

Docs

ARCHITECTURE.md §4, ARCHITECTURE_ESSENTIALS.md (data model map, role guardrail), ROADMAP.md (#75 under Phase 5).

Design decisions

  • A user belongs to at most one facility (unique constraint on FacilityMember.userId).
  • Exactly two roles; no permissions table.
  • Invite TTL is configurable via env var. [VERIFY: var name, default, validated at startup, in .env.example.]
  • Last-owner removal is blocked in the service layer.

Evidence for reviewer checks

  • Migration preserves existing logins: [TODO: paste output showing pre-migration facility users present as owners post-migration, and a login test against a migrated DB.]
  • Only two roles exist: [TODO: paste grep -rniE "role" backend/src backend/prisma | grep -iE "admin|manager|viewer" output showing no third role.]

Test status

  • npm run typecheck: [paste output]
  • npm test: 93 passing
  • npm run test:integration: 9 failures, 3 skips (NOT green). Cause: test DB schema is not built from the real migrations (LoginAttempt table mismatch). [Update once fixed.]

Open items

  • Integration suite must pass before this leaves draft.
  • Ownership transfer is out of scope.
  • Email delivery of invites: [state whether existing mailer is used or a dev-only path returns the token.]
  • Whether the old FacilityProfile.userId column was dropped, and that nothing still reads it. [VERIFY.]

…RBAC matrix)

- Add FacilityMember model: user↔facility with role enum (owner|staff), unique constraint on userId
- Add FacilityInvite model: hashed token, expiring TTL, single-use, conditional update
- Migrate existing 1:1 FacilityProfile/User links to FacilityMember with role=owner
- FacilityProfile.userId removes @unique (constraint now in FacilityMember)
- Member management: owners can list/remove staff; cannot remove last owner
- Owner-only invariant: staff cannot invite, remove, list invites, or revoke (403)
- Last owner cannot be removed from facility
- User cannot belong to two facilities (DB unique constraint on FacilityMember.userId)
- Invite acceptance: new user (create account + FacilityMember) and existing user (add to facility)
- Facilitiy guard middleware: resolves membership via FacilityMember, exposes facilityId + role
- Owner-only guard for management endpoints
- FulfillmentRecord.actorUserId: records acting staff user, nullable for legacy rows
- Updated ARCHITECTURE.md §4 with FacilityMember/FacilityInvite models and attribution field
- Updated ARCHITECTURE_ESSENTIALS.md data model map
- Updated ROADMAP.md: issue DocForum#75 status changed to Done
- Updated docs/api/README.md with new facility API endpoints
- Updated backend/src/modules/facilities/ with full implementation
- Updated backend/src/middleware/facility-guard.ts with membership resolution
…fixed roles, no RBAC matrix)

- Add FacilityMember and FacilityInvite models with FacilityRole enum
- Migrate existing FacilityProfile/User links to FacilityMember (role=owner)
- Implement invite flow (create, accept, list, revoke)
- Facility guard middleware (requireFacility, requireFacilityOwner)
- Owner-only route guards
- Update ARCHITECTURE.md, ARCHITECTURE_ESSENTIALS.md, ROADMAP.md
- Fix integration test setup (bcrypt, migration reset, type fields)
@drips-wave

drips-wave Bot commented Oct 19, 2026

Copy link
Copy Markdown

@dahmeezy Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@afeezorobsco-cyber

Copy link
Copy Markdown
Contributor

@dahmeezy Thanks for the PR. It can't be merged yet. To unblock it:

CI is failing. Here's what broke:

Backend (typecheck + test + build):

prisma generate: P1012 Error validating field `user` in model `FacilityProfile`: the relation is missing an opposite relation field on model `User` (prisma/schema.prisma:343)

Please fix these errors and run the same steps locally before pushing (npm run typecheck && npm test && npm run build, or cargo test for contracts).

Branch protection also requires the branch to be up to date with main, so please rebase onto the latest main when you push the fix.

Push to this same branch and CI will re-run. Once it's green and conflict-free, it'll be reviewed for merge.

@github-actions github-actions Bot added the needs-rebase Has merge conflicts with main; the author needs to rebase label Oct 20, 2026
@github-actions

Copy link
Copy Markdown

@dahmeezy This PR now has merge conflicts with main, so it can't be merged until it's rebased:

git fetch upstream            # upstream = https://github.com/DocForum/docforum-core.git
git rebase upstream/main
# resolve the conflicts, re-run the tests, then:
git push --force-with-lease

The usual culprits are shared files like ROADMAP.md and ARCHITECTURE*.md. CI re-runs when you push, and the needs-rebase label is removed automatically once the conflicts are resolved.

Automated message. It won't repeat unless the PR conflicts again after your next push.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-rebase Has merge conflicts with main; the author needs to rebase

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Phase 5] Multiple staff users per facility (fixed roles, no RBAC matrix)

2 participants