MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-353

6.4.4 Project members + access API — manage memberships, roles, access level (project-admin-gated)

Done
Description

Estimate: 24m · Depends on: 6.4.2

The write path for project membership + access, 4-layer (Route → Service → Repository → Prisma) with the explicit workspace gate (finding #26) + a project-admin check. projectMembersService: addMember(projectId, userId, role) (the user must be a workspace member), removeMember, setRole, and setAccessLevel(projectId, level). Setting a project private seeds its currently-eligible viewers as member rows (so the owner + current users aren’t locked out — the Jira "go private → keep current people" shape). All gated to project admin (or workspace owner/admin). Routes: …/projects/[key]/members (GET/POST/PATCH/DELETE) + PATCH …/projects/[key]/access; HTTP-only, typed-error→status.

Acceptance criteria

  • projectMembersService add/remove/setRole + setAccessLevel exist, own their transaction, validate (add target must be a workspace member; last-admin guard on remove/role-change), return DTOs; setting private seeds current viewers as members.
  • Routes are HTTP-only (one service call, typed-error→status: 400 invalid, 403 not project admin, 404 missing); project-admin (or workspace owner/admin) gated.
  • Vitest (real Postgres) covers add/remove/setRole, the go-private seeding, the last-admin guard, and a non-admin denial.

Context refs

  • lib/services/workspacesService.ts addMember/removeMember/listMembers (Story 1.2) — the membership service precedent to mirror at project scope; the last-member guard pattern
  • Story 6.4.2 (ProjectMembership + accessLevel); motir-core/CLAUDE.md (4-layer)