11 KiB
11 KiB
| id | title | status | assignee | created_date | labels | priority | dependencies | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| task-023 | Version History & Permissions Implementation Plan | To Do | 2025-12-04 13:10 |
|
high |
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)
- Has email permission → Access at assigned role (OWNER/ADMIN/EDITOR/VIEWER)
- Signed in + no PIN set on board → EDITOR
- Knows PIN (entered this session) → EDITOR
- 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)
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)
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)
interface SeenState {
[roomId: string]: {
lastSeenTimestamp: number;
seenShapeIds: string[]; // Shapes explicitly marked as seen
acknowledgedDeletions: string[]; // Deleted shape IDs acknowledged
};
}
3. Worker Endpoints
Metadata Endpoints
// 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
// 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 interfacesworker/boardMetadata.ts- CRUD functions for metadata in R2src/lib/board/types.ts- Client-side type definitions
Tasks:
- Define all TypeScript interfaces
- Create
getBoardMetadata(r2, roomId)function - Create
updateBoardMetadata(r2, roomId, updates)function - Add metadata initialization on board creation
Phase 2: Permission Logic
Files to create/modify:
worker/permissions.ts- Permission checking logicworker/AutomergeDurableObject.ts- Add permission checks to sync
Tasks:
- Create
getEffectiveRole(metadata, publicKey, hasValidPin)function - Create
canPerformAction(role, action)helper - Add permission check before accepting WebSocket edits
- Return role info in WebSocket handshake
Phase 3: Worker Endpoints
Files to modify:
worker/worker.ts- Add all new routes
Tasks:
- Implement
/room/:roomId/metadataGET - Implement
/room/:roomId/claimPOST - Implement
/room/:roomId/pinPOST/DELETE - Implement
/room/:roomId/pin/verifyPOST - Implement
/room/:roomId/permissionsPOST - Implement
/room/:roomId/versionsGET - Implement
/room/:roomId/versions/:dateGET - Implement
/room/:roomId/restorePOST
Phase 4: Client Permission Service
Files to create:
src/lib/board/permissionService.ts- Client-side permission managementsrc/lib/board/pinStorage.ts- Session-based PIN storage
Tasks:
- Create
BoardPermissionServiceclass - Implement
getMyRole(roomId)method - Implement
verifyPin(roomId, pin)method - Implement
claimBoard(roomId)method - Create React context for permission state
Phase 5: UI Components
Files to create:
src/components/BoardSettings/PermissionsPanel.tsxsrc/components/BoardSettings/VersionHistoryPanel.tsxsrc/components/BoardSettings/PinSetup.tsxsrc/components/PinEntryDialog.tsxsrc/components/ClaimBoardButton.tsx
Tasks:
- Create permissions management UI (OWNER/ADMIN view)
- Create version history browser with preview
- Create PIN entry dialog for guest access
- Create "Claim this board" button for unclaimed boards
- Add read-only indicator for VIEWERs
Phase 6: Change Tracking & Visualization
Files to create/modify:
src/lib/board/changeTracking.ts- Track changes by usersrc/hooks/useChangeVisualization.ts- Glow effect logicsrc/components/ChangeIndicator.tsx- Visual indicators
Tasks:
- Add
createdBy/modifiedBymetadata to shapes on edit - Track changes in local state (new shapes since last seen)
- Implement yellow glow CSS for new shapes
- Implement grey ghost effect for deleted shapes
- Add "Mark all as seen" button
- Add user attribution badges on hover
5. CSS for Visual Effects
/* 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