430 lines
21 KiB
Markdown
430 lines
21 KiB
Markdown
# Project Memberships
|
|
|
|
Complete documentation for the Tour Builder Platform's Project Memberships system including team collaboration, member roles, and access control per project.
|
|
|
|
## Overview
|
|
|
|
The Project Memberships system enables team collaboration by managing user access to specific projects. It operates as a separate access layer from the application-level RBAC system, allowing granular per-project permissions.
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ Project Memberships Architecture │
|
|
│ │
|
|
│ ┌────────────────────────────────────────────────────────────────────────┐ │
|
|
│ │ Application-Level RBAC (roles table) │ │
|
|
│ │ Controls which users can manage project memberships │ │
|
|
│ │ │ │
|
|
│ │ • PlatformOwner, Administrator: Full CRUD on memberships │ │
|
|
│ │ • AccountManager: Create, Read, Update (no delete) │ │
|
|
│ │ • TourDesigner, ContentReviewer, AnalyticsViewer: Read only │ │
|
|
│ └─────────────────────────────┬──────────────────────────────────────────┘ │
|
|
│ │ │
|
|
│ (permission to manage) │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌────────────────────────────────────────────────────────────────────────┐ │
|
|
│ │ Project-Level Access (project_memberships) │ │
|
|
│ │ Controls what users can do within a specific project │ │
|
|
│ │ │ │
|
|
│ │ Project A Project B │ │
|
|
│ │ ┌─────────────────────┐ ┌─────────────────────┐ │ │
|
|
│ │ │ User1: owner │ │ User2: owner │ │ │
|
|
│ │ │ User2: editor │ │ User3: viewer │ │ │
|
|
│ │ │ User3: reviewer │ │ User4: editor │ │ │
|
|
│ │ └─────────────────────┘ └─────────────────────┘ │ │
|
|
│ └────────────────────────────────────────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
## Data Model
|
|
|
|
### Database Schema
|
|
|
|
**Table:** `project_memberships`
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `id` | UUID | Primary key (auto-generated) |
|
|
| `access_level` | ENUM | Project role: `owner`, `editor`, `reviewer`, `viewer` |
|
|
| `is_active` | BOOLEAN | Whether membership is active (default: false) |
|
|
| `invited_at` | DATE | When invitation was sent |
|
|
| `accepted_at` | DATE | When user accepted invitation |
|
|
| `projectId` | UUID | Foreign key to `projects` table |
|
|
| `userId` | UUID | Foreign key to `users` table |
|
|
| `createdAt` | TIMESTAMP | Record creation time |
|
|
| `updatedAt` | TIMESTAMP | Record update time |
|
|
| `deletedAt` | TIMESTAMP | Soft delete timestamp (paranoid mode) |
|
|
| `importHash` | STRING(255) | Unique hash for CSV imports |
|
|
|
|
**Source:** `backend/src/db/models/project_memberships.js`
|
|
|
|
### Database Indexes
|
|
|
|
```javascript
|
|
indexes: [
|
|
{ fields: ['projectId'] }, // Fast project lookups
|
|
{ fields: ['userId'] }, // Fast user lookups
|
|
{ fields: ['projectId', 'userId'], unique: true }, // One membership per user-project
|
|
{ fields: ['is_active'] }, // Filter by active status
|
|
{ fields: ['deletedAt'] }, // Soft delete queries
|
|
]
|
|
```
|
|
|
|
### Relationships
|
|
|
|
```javascript
|
|
// Each membership belongs to one project
|
|
db.project_memberships.belongsTo(db.projects, {
|
|
as: 'project',
|
|
foreignKey: 'projectId',
|
|
onDelete: 'CASCADE', // Delete membership when project is deleted
|
|
onUpdate: 'CASCADE',
|
|
});
|
|
|
|
// Each membership belongs to one user
|
|
db.project_memberships.belongsTo(db.users, {
|
|
as: 'user',
|
|
foreignKey: 'userId',
|
|
onDelete: 'CASCADE', // Delete membership when user is deleted
|
|
onUpdate: 'CASCADE',
|
|
});
|
|
|
|
// Audit trail associations
|
|
db.project_memberships.belongsTo(db.users, { as: 'createdBy' });
|
|
db.project_memberships.belongsTo(db.users, { as: 'updatedBy' });
|
|
```
|
|
|
|
## Project Access Levels
|
|
|
|
### ENUM Values
|
|
|
|
| Level | Description | Typical Permissions |
|
|
|-------|-------------|---------------------|
|
|
| `owner` | Full project control | All operations, can delete project, manage team |
|
|
| `editor` | Content editing | Edit pages, elements, assets, transitions |
|
|
| `reviewer` | Review only | View all content, add comments/feedback |
|
|
| `viewer` | Read-only | View published content only |
|
|
|
|
**Default:** `viewer` (when not specified)
|
|
|
|
### Access Level Hierarchy
|
|
|
|
```
|
|
owner → editor → reviewer → viewer
|
|
│ │ │ │
|
|
│ │ │ └── View published content
|
|
│ │ └── View all content + provide feedback
|
|
│ └── Edit content + reviewer permissions
|
|
└── Full control + editor permissions + manage team
|
|
```
|
|
|
|
## Invitation Workflow
|
|
|
|
### Membership States
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────┐
|
|
│ Invitation Lifecycle │
|
|
│ │
|
|
│ 1. Membership Created │
|
|
│ ├── invited_at: timestamp │
|
|
│ ├── accepted_at: null │
|
|
│ └── is_active: false │
|
|
│ │
|
|
│ ↓ │
|
|
│ │
|
|
│ 2. User Accepts Invitation │
|
|
│ ├── invited_at: original timestamp │
|
|
│ ├── accepted_at: timestamp │
|
|
│ └── is_active: true │
|
|
│ │
|
|
│ ↓ │
|
|
│ │
|
|
│ 3. Membership Active │
|
|
│ └── User can access project with assigned access_level │
|
|
│ │
|
|
│ ↓ (optional) │
|
|
│ │
|
|
│ 4. Membership Revoked (soft delete) │
|
|
│ └── deletedAt: timestamp (paranoid mode) │
|
|
└─────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Timestamp Fields
|
|
|
|
| Field | Set When | Purpose |
|
|
|-------|----------|---------|
|
|
| `invited_at` | Membership created | Track when invitation was sent |
|
|
| `accepted_at` | User accepts | Track when user joined project |
|
|
| `is_active` | User accepts | Quick filter for active members |
|
|
|
|
## API Reference
|
|
|
|
### Endpoint
|
|
|
|
**Base URL:** `/api/project_memberships`
|
|
|
|
**Source:** `backend/src/routes/project_memberships.ts`
|
|
|
|
### Standard Operations
|
|
|
|
| Method | Endpoint | Description |
|
|
|--------|----------|-------------|
|
|
| `GET` | `/api/project_memberships` | List all memberships |
|
|
| `GET` | `/api/project_memberships/:id` | Get single membership |
|
|
| `POST` | `/api/project_memberships` | Create membership (invite user) |
|
|
| `PUT` | `/api/project_memberships/:id` | Update membership |
|
|
| `DELETE` | `/api/project_memberships/:id` | Delete membership (soft delete) |
|
|
|
|
### Request/Response Format
|
|
|
|
**Create Membership:**
|
|
```json
|
|
POST /api/project_memberships
|
|
{
|
|
"data": {
|
|
"project": "project-uuid",
|
|
"user": "user-uuid",
|
|
"access_level": "editor",
|
|
"invited_at": "2024-01-15T10:00:00Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
**Query with Filters:**
|
|
```
|
|
GET /api/project_memberships?project=<uuid>&access_level=editor&is_active=true
|
|
```
|
|
|
|
### Filtering Options
|
|
|
|
| Parameter | Type | Description |
|
|
|-----------|------|-------------|
|
|
| `project` | UUID/name | Filter by project ID or name (iLike search) |
|
|
| `user` | UUID/firstName | Filter by user ID or firstName (iLike search) |
|
|
| `access_level` | ENUM | Filter by access level |
|
|
| `is_active` | boolean | Filter by active status |
|
|
| `invited_atRange` | [start, end] | Filter by invitation date range |
|
|
| `accepted_atRange` | [start, end] | Filter by acceptance date range |
|
|
| `createdAtRange` | [start, end] | Filter by record creation date range |
|
|
|
|
**Source:** `backend/src/db/api/project_memberships.ts`
|
|
|
|
## Application-Level Permissions
|
|
|
|
### Permission Names
|
|
|
|
The RBAC system uses these permissions to control who can manage memberships:
|
|
|
|
| Permission | Description |
|
|
|------------|-------------|
|
|
| `CREATE_PROJECT_MEMBERSHIPS` | Invite users to projects |
|
|
| `READ_PROJECT_MEMBERSHIPS` | View project team members |
|
|
| `UPDATE_PROJECT_MEMBERSHIPS` | Change user access levels |
|
|
| `DELETE_PROJECT_MEMBERSHIPS` | Remove users from projects |
|
|
|
|
### Role-Permission Matrix
|
|
|
|
| Role | CREATE | READ | UPDATE | DELETE |
|
|
|------|--------|------|--------|--------|
|
|
| PlatformOwner | ✓ | ✓ | ✓ | ✓ |
|
|
| Administrator | ✓ | ✓ | ✓ | ✓ |
|
|
| AccountManager | ✓ | ✓ | ✓ | ✗ |
|
|
| TourDesigner | ✗ | ✓ | ✗ | ✗ |
|
|
| ContentReviewer | ✗ | ✓ | ✗ | ✗ |
|
|
| AnalyticsViewer | ✗ | ✓ | ✗ | ✗ |
|
|
|
|
**Source:** `backend/src/db/seeders/20200430130760-user-roles.js`
|
|
|
|
## Frontend Integration
|
|
|
|
### Redux State Management
|
|
|
|
**Slice:** `frontend/src/stores/project_memberships/project_membershipsSlice.ts`
|
|
|
|
```typescript
|
|
import { createEntitySlice } from '../createEntitySlice';
|
|
import type { ProjectMembership } from '../../types/entities';
|
|
|
|
const { slice, actions, reducer } = createEntitySlice<ProjectMembership>({
|
|
name: 'project_memberships',
|
|
endpoint: 'project_memberships',
|
|
singularName: 'Project Membership',
|
|
});
|
|
|
|
export const {
|
|
fetch,
|
|
create,
|
|
update,
|
|
deleteItem,
|
|
deleteItemsByIds,
|
|
uploadCsv,
|
|
setRefetch,
|
|
} = actions;
|
|
```
|
|
|
|
### TypeScript Interface
|
|
|
|
**Source:** `frontend/src/types/entities.ts`
|
|
|
|
```typescript
|
|
export interface ProjectMembership extends BaseEntity {
|
|
user?: User | string | null;
|
|
project?: Project | string | null;
|
|
access_level?: 'owner' | 'editor' | 'reviewer' | 'viewer';
|
|
is_active?: boolean;
|
|
invited_at?: string | Date | null;
|
|
accepted_at?: string | Date | null;
|
|
// Legacy field
|
|
role?: 'owner' | 'editor' | 'viewer';
|
|
}
|
|
```
|
|
|
|
### Admin Pages
|
|
|
|
**Location:** `frontend/src/pages/project_memberships/`
|
|
|
|
| Page | Path | Description |
|
|
|------|------|-------------|
|
|
| List | `/project_memberships/project_memberships-list` | Paginated table with filters (factory-generated) |
|
|
| Table | `/project_memberships/project_memberships-table` | Alternative table view (manual implementation) |
|
|
| New | `/project_memberships/project_memberships-new` | Create membership form |
|
|
| Edit (query) | `/project_memberships/project_memberships-edit?id=<uuid>` | Edit membership form (query param) |
|
|
| Edit (path) | `/project_memberships/<uuid>` | Edit membership form (path param) |
|
|
| View | `/project_memberships/project_memberships-view?id=<uuid>` | Read-only details |
|
|
|
|
**Note:** Two edit routes exist - query-based (`?id=`) and dynamic path-based (`[project_membershipsId].tsx`).
|
|
|
|
**Table Columns:** project, user, access_level, is_active, invited_at, accepted_at, actions
|
|
|
|
**List Filters:** access_level (enum dropdown with owner/editor/reviewer/viewer options), project (text search), user (text search), invited_at (date range), accepted_at (date range)
|
|
|
|
> **Note:** `is_active` filter is supported by the API but not exposed in the admin list UI.
|
|
|
|
**Components:** `frontend/src/components/Project_memberships/`
|
|
- `TableProject_memberships.tsx` - Data grid component (23 LOC)
|
|
- `CardProject_memberships.tsx` - Card view component (163 LOC)
|
|
- `ListProject_memberships.tsx` - List view component (125 LOC)
|
|
- `configureProject_membershipsCols.tsx` - Column definitions with permission-based editability (51 LOC)
|
|
|
|
### Usage Example
|
|
|
|
```typescript
|
|
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
|
import { fetch, create, update, deleteItem } from '../stores/project_memberships/project_membershipsSlice';
|
|
|
|
// Fetch project members
|
|
dispatch(fetch({ query: `?project=${projectId}&is_active=true` }));
|
|
|
|
// Invite user to project
|
|
dispatch(create({
|
|
project: projectId,
|
|
user: userId,
|
|
access_level: 'editor',
|
|
invited_at: new Date().toISOString(),
|
|
}));
|
|
|
|
// Update access level
|
|
dispatch(update({
|
|
id: membershipId,
|
|
access_level: 'reviewer',
|
|
}));
|
|
|
|
// Remove from project
|
|
dispatch(deleteItem(membershipId));
|
|
```
|
|
|
|
## Unique Constraint
|
|
|
|
The composite unique index on `(projectId, userId)` ensures:
|
|
- Each user can only have one membership per project
|
|
- Attempting to invite the same user twice will fail
|
|
- To change a user's role, update the existing membership
|
|
|
|
```sql
|
|
-- This constraint prevents duplicate memberships
|
|
CREATE UNIQUE INDEX ON project_memberships (projectId, userId);
|
|
```
|
|
|
|
## Soft Delete Behavior
|
|
|
|
The model uses Sequelize's paranoid mode:
|
|
- `DELETE` operations set `deletedAt` timestamp instead of removing record
|
|
- Default queries exclude soft-deleted records
|
|
- Membership history is preserved for audit purposes
|
|
|
|
```javascript
|
|
{
|
|
timestamps: true,
|
|
paranoid: true, // Enables soft delete
|
|
}
|
|
```
|
|
|
|
## Known Considerations
|
|
|
|
1. **Two-Tier Access System**: Application-level roles control who can manage memberships; project-level access controls what users can do within projects.
|
|
|
|
2. **Invitation Flow**: Memberships start with `is_active: false`. The system tracks `invited_at` and `accepted_at` separately to support invitation workflows.
|
|
|
|
3. **Legacy Field**: The TypeScript interface includes a `role` field for backward compatibility, but the database uses `access_level` ENUM.
|
|
|
|
4. **Cascade Deletes**: Memberships are automatically deleted when their associated project or user is deleted.
|
|
|
|
5. **Dedicated Admin Pages**: Standard CRUD pages exist at `/project_memberships/*`:
|
|
- `project_memberships-list.tsx` - List with filters using factory pattern (35 LOC)
|
|
- `project_memberships-table.tsx` - Alternative table view (185 LOC, manual implementation)
|
|
- `project_memberships-new.tsx` - Create new membership (149 LOC)
|
|
- `project_memberships-edit.tsx` - Edit via query param (200 LOC)
|
|
- `[project_membershipsId].tsx` - Edit via path param (221 LOC)
|
|
- `project_memberships-view.tsx` - View membership details (163 LOC)
|
|
- Components: `TableProject_memberships`, `CardProject_memberships`, `ListProject_memberships`
|
|
|
|
6. **Default Access Level**: New memberships default to `viewer` access, the most restrictive level.
|
|
|
|
7. **Autocomplete Field**: The DB API uses `access_level` for autocomplete searches, enabling dropdown-style user interfaces.
|
|
|
|
## Data Flow Diagram
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ Team Collaboration Flow │
|
|
│ │
|
|
│ Project Owner Backend API Database │
|
|
│ │ │ │ │
|
|
│ │ POST /project_memberships │ │ │
|
|
│ │ {user, project, editor} │ │ │
|
|
│ │──────────────────────────────>│ │ │
|
|
│ │ │ │ │
|
|
│ │ │ Check CREATE_PROJECT_ │ │
|
|
│ │ │ MEMBERSHIPS permission │ │
|
|
│ │ │ │ │
|
|
│ │ │ Validate unique constraint │ │
|
|
│ │ │──────────────────────────────>│ │
|
|
│ │ │ │ │
|
|
│ │ │ Create membership record │ │
|
|
│ │ │ invited_at: now │ │
|
|
│ │ │ is_active: false │ │
|
|
│ │ │<──────────────────────────────│ │
|
|
│ │ │ │ │
|
|
│ │<──────────────────────────────│ │ │
|
|
│ │ 200 OK {membership} │ │ │
|
|
│ │
|
|
│ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │
|
|
│ │
|
|
│ Invited User │
|
|
│ │ PUT /project_memberships/:id │
|
|
│ │ {is_active: true} │ │ │
|
|
│ │──────────────────────────────>│ │ │
|
|
│ │ │ │ │
|
|
│ │ │ Update membership │ │
|
|
│ │ │ accepted_at: now │ │
|
|
│ │ │ is_active: true │ │
|
|
│ │ │──────────────────────────────>│ │
|
|
│ │ │<──────────────────────────────│ │
|
|
│ │<──────────────────────────────│ │ │
|
|
│ │ │ │ │
|
|
│ │ User can now access project │ │ │
|
|
│ │ with 'editor' permissions │ │ │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
```
|