390 lines
11 KiB
Markdown
390 lines
11 KiB
Markdown
---
|
|
id: task-023
|
|
title: Version History & Permissions Implementation Plan
|
|
status: To Do
|
|
assignee: []
|
|
created_date: '2025-12-04 13:10'
|
|
labels: [feature, permissions, version-history, collaboration, plan]
|
|
priority: high
|
|
dependencies: []
|
|
---
|
|
|
|
## Description
|
|
|
|
Comprehensive implementation plan for board permissions, R2 backup version browsing/restoration, and visual change highlighting. This task contains the full technical specification for implementation.
|
|
|
|
---
|
|
|
|
## 1. Access Model Summary
|
|
|
|
### Permission Levels
|
|
| Role | Capabilities |
|
|
|------|--------------|
|
|
| **OWNER** | Full control, delete board, transfer ownership, manage all permissions |
|
|
| **ADMIN** | Restore versions, manage EDITOR/VIEWER permissions, cannot delete board |
|
|
| **EDITOR** | Create/edit/delete shapes, changes are tracked |
|
|
| **VIEWER** | Read-only access, can see board but not modify |
|
|
|
|
### Access Logic (in order of precedence)
|
|
1. Has email permission → Access at assigned role (OWNER/ADMIN/EDITOR/VIEWER)
|
|
2. Signed in + no PIN set on board → EDITOR
|
|
3. Knows PIN (entered this session) → EDITOR
|
|
4. Otherwise → VIEWER (read-only)
|
|
|
|
### Ownership Rules
|
|
- New board created by signed-in user → auto OWNER
|
|
- Existing unclaimed board → "Claim admin" button to become OWNER
|
|
- Anonymous users cannot claim boards
|
|
|
|
### PIN System
|
|
- Optional 4-digit code set by OWNER
|
|
- Grants EDITOR access to anyone who enters it correctly
|
|
- Session-based (stored in sessionStorage, cleared on browser close)
|
|
- Stored hashed with salt in R2 metadata
|
|
|
|
---
|
|
|
|
## 2. Data Structures
|
|
|
|
### Board Metadata (R2: `rooms/${roomId}/metadata.json`)
|
|
|
|
```typescript
|
|
interface BoardMetadata {
|
|
// Ownership
|
|
owner: {
|
|
cryptidUsername: string;
|
|
publicKey: string;
|
|
claimedAt: number; // timestamp
|
|
} | null;
|
|
|
|
// PIN for guest editor access
|
|
pin: {
|
|
hash: string; // SHA-256(salt + pin)
|
|
salt: string; // Random 16-byte hex string
|
|
attempts: number; // Failed attempts counter
|
|
lockedUntil: number | null; // Lockout timestamp
|
|
} | null;
|
|
|
|
// Explicit user permissions (by publicKey)
|
|
permissions: {
|
|
[publicKey: string]: {
|
|
role: 'ADMIN' | 'EDITOR' | 'VIEWER';
|
|
grantedBy: string; // publicKey of granter
|
|
grantedAt: number; // timestamp
|
|
email?: string; // Optional email for display
|
|
cryptidUsername?: string;
|
|
};
|
|
};
|
|
|
|
// Default access for signed-in users without explicit permission
|
|
defaultSignedInAccess: 'EDITOR' | 'VIEWER'; // Default: 'EDITOR'
|
|
|
|
// Audit log (last 100 entries)
|
|
auditLog: AuditEntry[];
|
|
|
|
// Metadata
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
}
|
|
|
|
interface AuditEntry {
|
|
action: 'claim' | 'restore' | 'permission_change' | 'pin_set' | 'pin_removed' | 'ownership_transfer';
|
|
actor: {
|
|
cryptidUsername?: string;
|
|
publicKey?: string;
|
|
};
|
|
timestamp: number;
|
|
details: Record<string, any>;
|
|
}
|
|
```
|
|
|
|
### Shape Change Metadata (added to shape.meta)
|
|
|
|
```typescript
|
|
interface ShapeChangeMeta {
|
|
createdBy?: {
|
|
cryptidUsername: string;
|
|
publicKey: string;
|
|
timestamp: number;
|
|
};
|
|
modifiedBy?: {
|
|
cryptidUsername: string;
|
|
publicKey: string;
|
|
timestamp: number;
|
|
};
|
|
deletedBy?: { // For ghost shapes
|
|
cryptidUsername: string;
|
|
publicKey: string;
|
|
timestamp: number;
|
|
};
|
|
}
|
|
```
|
|
|
|
### Client-Side Seen State (localStorage)
|
|
|
|
```typescript
|
|
interface SeenState {
|
|
[roomId: string]: {
|
|
lastSeenTimestamp: number;
|
|
seenShapeIds: string[]; // Shapes explicitly marked as seen
|
|
acknowledgedDeletions: string[]; // Deleted shape IDs acknowledged
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Worker Endpoints
|
|
|
|
### Metadata Endpoints
|
|
|
|
```typescript
|
|
// GET /room/:roomId/metadata
|
|
// Returns board metadata (filtered based on requester's role)
|
|
// Response: { owner, myRole, hasPin, permissions (if ADMIN+), ... }
|
|
|
|
// POST /room/:roomId/claim
|
|
// Claim ownership of unclaimed board
|
|
// Body: { publicKey, cryptidUsername }
|
|
// Response: { success, metadata }
|
|
|
|
// POST /room/:roomId/pin
|
|
// Set or update PIN (OWNER only)
|
|
// Body: { pin: string (4 digits), publicKey }
|
|
// Response: { success }
|
|
|
|
// DELETE /room/:roomId/pin
|
|
// Remove PIN (OWNER only)
|
|
// Body: { publicKey }
|
|
// Response: { success }
|
|
|
|
// POST /room/:roomId/pin/verify
|
|
// Verify PIN for guest access
|
|
// Body: { pin: string }
|
|
// Response: { success, granted: 'EDITOR' }
|
|
|
|
// POST /room/:roomId/permissions
|
|
// Update user permissions (OWNER/ADMIN)
|
|
// Body: {
|
|
// publicKey: string, // requester
|
|
// targetPublicKey: string,
|
|
// role: 'ADMIN' | 'EDITOR' | 'VIEWER' | null, // null = remove
|
|
// targetEmail?: string
|
|
// }
|
|
// Response: { success }
|
|
|
|
// POST /room/:roomId/transfer
|
|
// Transfer ownership (OWNER only)
|
|
// Body: { publicKey, newOwnerPublicKey }
|
|
// Response: { success }
|
|
```
|
|
|
|
### Version History Endpoints
|
|
|
|
```typescript
|
|
// GET /room/:roomId/versions
|
|
// List available backup versions (ADMIN+ only)
|
|
// Query: ?limit=30&before=2025-12-01
|
|
// Response: {
|
|
// versions: [
|
|
// { date: '2025-12-04', key: '2025-12-04/rooms/abc123', size: 12345 },
|
|
// ...
|
|
// ]
|
|
// }
|
|
|
|
// GET /room/:roomId/versions/:date
|
|
// Preview a specific backup (ADMIN+ only)
|
|
// Response: {
|
|
// date,
|
|
// shapeCount,
|
|
// recordCount,
|
|
// preview: { shapes: [...first 50 shapes...] }
|
|
// }
|
|
|
|
// POST /room/:roomId/restore
|
|
// Restore from backup (ADMIN+ only)
|
|
// Body: {
|
|
// date: string,
|
|
// publicKey: string,
|
|
// cryptidUsername: string
|
|
// }
|
|
// Response: { success, restoredShapeCount }
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Implementation Phases
|
|
|
|
### Phase 1: Types & Metadata Storage
|
|
**Files to create/modify:**
|
|
- `worker/types.ts` - Add BoardMetadata, AuditEntry interfaces
|
|
- `worker/boardMetadata.ts` - CRUD functions for metadata in R2
|
|
- `src/lib/board/types.ts` - Client-side type definitions
|
|
|
|
**Tasks:**
|
|
1. Define all TypeScript interfaces
|
|
2. Create `getBoardMetadata(r2, roomId)` function
|
|
3. Create `updateBoardMetadata(r2, roomId, updates)` function
|
|
4. Add metadata initialization on board creation
|
|
|
|
### Phase 2: Permission Logic
|
|
**Files to create/modify:**
|
|
- `worker/permissions.ts` - Permission checking logic
|
|
- `worker/AutomergeDurableObject.ts` - Add permission checks to sync
|
|
|
|
**Tasks:**
|
|
1. Create `getEffectiveRole(metadata, publicKey, hasValidPin)` function
|
|
2. Create `canPerformAction(role, action)` helper
|
|
3. Add permission check before accepting WebSocket edits
|
|
4. Return role info in WebSocket handshake
|
|
|
|
### Phase 3: Worker Endpoints
|
|
**Files to modify:**
|
|
- `worker/worker.ts` - Add all new routes
|
|
|
|
**Tasks:**
|
|
1. Implement `/room/:roomId/metadata` GET
|
|
2. Implement `/room/:roomId/claim` POST
|
|
3. Implement `/room/:roomId/pin` POST/DELETE
|
|
4. Implement `/room/:roomId/pin/verify` POST
|
|
5. Implement `/room/:roomId/permissions` POST
|
|
6. Implement `/room/:roomId/versions` GET
|
|
7. Implement `/room/:roomId/versions/:date` GET
|
|
8. Implement `/room/:roomId/restore` POST
|
|
|
|
### Phase 4: Client Permission Service
|
|
**Files to create:**
|
|
- `src/lib/board/permissionService.ts` - Client-side permission management
|
|
- `src/lib/board/pinStorage.ts` - Session-based PIN storage
|
|
|
|
**Tasks:**
|
|
1. Create `BoardPermissionService` class
|
|
2. Implement `getMyRole(roomId)` method
|
|
3. Implement `verifyPin(roomId, pin)` method
|
|
4. Implement `claimBoard(roomId)` method
|
|
5. Create React context for permission state
|
|
|
|
### Phase 5: UI Components
|
|
**Files to create:**
|
|
- `src/components/BoardSettings/PermissionsPanel.tsx`
|
|
- `src/components/BoardSettings/VersionHistoryPanel.tsx`
|
|
- `src/components/BoardSettings/PinSetup.tsx`
|
|
- `src/components/PinEntryDialog.tsx`
|
|
- `src/components/ClaimBoardButton.tsx`
|
|
|
|
**Tasks:**
|
|
1. Create permissions management UI (OWNER/ADMIN view)
|
|
2. Create version history browser with preview
|
|
3. Create PIN entry dialog for guest access
|
|
4. Create "Claim this board" button for unclaimed boards
|
|
5. Add read-only indicator for VIEWERs
|
|
|
|
### Phase 6: Change Tracking & Visualization
|
|
**Files to create/modify:**
|
|
- `src/lib/board/changeTracking.ts` - Track changes by user
|
|
- `src/hooks/useChangeVisualization.ts` - Glow effect logic
|
|
- `src/components/ChangeIndicator.tsx` - Visual indicators
|
|
|
|
**Tasks:**
|
|
1. Add `createdBy`/`modifiedBy` metadata to shapes on edit
|
|
2. Track changes in local state (new shapes since last seen)
|
|
3. Implement yellow glow CSS for new shapes
|
|
4. Implement grey ghost effect for deleted shapes
|
|
5. Add "Mark all as seen" button
|
|
6. Add user attribution badges on hover
|
|
|
|
---
|
|
|
|
## 5. CSS for Visual Effects
|
|
|
|
```css
|
|
/* Yellow glow for new shapes from other users */
|
|
.shape-new-unseen {
|
|
filter: drop-shadow(0 0 8px rgba(255, 200, 0, 0.8));
|
|
animation: pulse-yellow 2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes pulse-yellow {
|
|
0%, 100% { filter: drop-shadow(0 0 8px rgba(255, 200, 0, 0.8)); }
|
|
50% { filter: drop-shadow(0 0 16px rgba(255, 200, 0, 0.5)); }
|
|
}
|
|
|
|
/* Grey ghost for recently deleted shapes */
|
|
.shape-deleted-ghost {
|
|
opacity: 0.3;
|
|
filter: grayscale(100%) drop-shadow(0 0 4px rgba(128, 128, 128, 0.5));
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* User attribution badge */
|
|
.shape-attribution {
|
|
position: absolute;
|
|
top: -20px;
|
|
left: 0;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
color: white;
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
font-size: 10px;
|
|
white-space: nowrap;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Security Considerations
|
|
|
|
### PIN Security
|
|
- Store as SHA-256(salt + pin)
|
|
- Generate new 16-byte random salt each time PIN is set
|
|
- Rate limit: Lock after 5 failed attempts for 15 minutes
|
|
- PIN verification happens server-side only
|
|
|
|
### Permission Verification
|
|
- Always verify permissions server-side in Durable Object
|
|
- Client-side checks are for UX only (hide/disable buttons)
|
|
- WebSocket messages include sender's publicKey for verification
|
|
|
|
### Audit Logging
|
|
- Log all permission changes, restores, ownership transfers
|
|
- Keep last 100 entries per board
|
|
- Include actor identity and timestamp
|
|
|
|
---
|
|
|
|
## 7. Testing Checklist
|
|
|
|
- [ ] Anonymous user can view but not edit
|
|
- [ ] Signed-in user can edit unclaimed board
|
|
- [ ] Board creator is auto-assigned as OWNER
|
|
- [ ] "Claim admin" button works for unclaimed boards
|
|
- [ ] OWNER can set/remove PIN
|
|
- [ ] PIN grants EDITOR access when verified
|
|
- [ ] PIN lockout after 5 failed attempts
|
|
- [ ] OWNER can assign ADMIN/EDITOR/VIEWER roles
|
|
- [ ] ADMIN can manage EDITOR/VIEWER but not other ADMINs
|
|
- [ ] Version history shows available backups
|
|
- [ ] Version preview shows shape count and sample
|
|
- [ ] Restore replaces current board with backup
|
|
- [ ] New shapes from others show yellow glow
|
|
- [ ] Deleted shapes show grey ghost
|
|
- [ ] "Mark as seen" clears visual indicators
|
|
- [ ] Read-only mode works for VIEWERs
|
|
|
|
---
|
|
|
|
## Acceptance Criteria
|
|
|
|
- [ ] Board creator becomes OWNER automatically
|
|
- [ ] OWNER can set optional 4-digit PIN
|
|
- [ ] OWNER can assign ADMIN/EDITOR/VIEWER roles to users
|
|
- [ ] ADMINs can restore board versions
|
|
- [ ] EDITORs can modify board content
|
|
- [ ] VIEWERs have read-only access
|
|
- [ ] Version history panel shows available backup dates
|
|
- [ ] Can preview a backup before restoring
|
|
- [ ] New objects from other users show yellow glow
|
|
- [ ] Deleted objects show grey ghost glow until acknowledged
|
|
- [ ] Changes show user attribution
|
|
- [ ] Changes can be marked as seen
|