wekan/docs/Security/PerUserDataAudit2025-12-23/PERSISTENCE_AUDIT.md

472 lines
18 KiB
Markdown

# Wekan Persistence Audit Report
## Overview
This document audits the persistence mechanisms for Wekan board data, including swimlanes, lists, cards, checklists, and their properties (order, color, background, titles, etc.), as well as per-user settings.
---
## 1. BOARD-LEVEL PERSISTENCE (Persisted Across All Users)
### 1.1 Swimlanes
**Collection**: `swimlanes` ([models/swimlanes.js](models/swimlanes.js))
**Persisted Fields**:
-`title` - Swimlane title (via `rename()` mutation)
-`sort` - Swimlane ordering/position (decimal number)
-`color` - Swimlane color (via `setColor()` mutation)
-`collapsed` - Swimlane collapsed state (via `collapse()` mutation) **⚠️ See note below**
-`archived` - Swimlane archived status
**Persistence Mechanism**:
- Direct MongoDB updates via `Swimlanes.update()` and `Swimlanes.direct.update()`
- Automatic timestamps: `updatedAt`, `modifiedAt` fields
- Activity tracking for title changes and archive/restore operations
**Issues Found**:
- ⚠️ **ISSUE**: `collapsed` field in swimlanes.js line 127 is set to `defaultValue: false` but the `isCollapsed()` helper (line 251-263) checks for per-user stored values. This creates a mismatch between board-level and per-user storage.
---
### 1.2 Lists
**Collection**: `lists` ([models/lists.js](models/lists.js))
**Persisted Fields**:
-`title` - List title
-`sort` - List ordering/position (decimal number)
-`color` - List color
-`collapsed` - List collapsed state (board-wide via REST API line 768-775) **⚠️ See note below**
-`starred` - List starred status
-`wipLimit` - WIP limit configuration
-`archived` - List archived status
**Persistence Mechanism**:
- Direct MongoDB updates via `Lists.update()` and `Lists.direct.update()`
- Automatic timestamps: `updatedAt`, `modifiedAt`
- Activity tracking for title changes, archive/restore
**Issues Found**:
- ⚠️ **ISSUE**: Similar to swimlanes, `collapsed` field (line 147) defaults to `false` but the `isCollapsed()` helper (line 303-311) also checks for per-user stored values. The REST API allows board-level collapsed state updates (line 768-775), but client also stores per-user via `getCollapsedListFromStorage()`.
- ⚠️ **ISSUE**: The `swimlaneId` field is part of the list (line 48), but `draggableLists()` method (line 275) filters by board only, suggesting lists are shared across swimlanes rather than per-swimlane.
---
### 1.3 Cards
**Collection**: `cards` ([models/cards.js](models/cards.js))
**Persisted Fields**:
-`title` - Card title
-`sort` - Card ordering/position within list
-`color` - Card color (via `setColor()` mutation, line 2268)
-`boardId`, `swimlaneId`, `listId` - Card location
-`archived` - Card archived status
-`description` - Card description
- ✅ Custom fields, labels, members, assignees, etc.
**Persistence Mechanism**:
- `move()` method (line 2063+) handles reordering and moving cards across swimlanes/lists/boards
- Automatic timestamp updates via `modifiedAt`, `dateLastActivity`
- Activity tracking for moves, title changes, etc.
- Attachment metadata updated alongside card moves (line 2101-2115)
**Issues Found**:
-**OK**: Order/sort persistence working correctly via card.move() and card.moveOptionalArgs()
-**OK**: Color persistence working correctly
-**OK**: Title changes persisted automatically
---
### 1.4 Checklists
**Collection**: `checklists` ([models/checklists.js](models/checklists.js))
**Persisted Fields**:
-`title` - Checklist title (via `setTitle()` mutation)
-`sort` - Checklist ordering (decimal number)
-`hideCheckedChecklistItems` - Toggle for hiding checked items
-`hideAllChecklistItems` - Toggle for hiding all items
**Persistence Mechanism**:
- Direct MongoDB updates via `Checklists.update()`
- Automatic timestamps: `createdAt`, `modifiedAt`
- Activity tracking for creation and removal
---
### 1.5 Checklist Items
**Collection**: `checklistItems` ([models/checklistItems.js](models/checklistItems.js))
**Persisted Fields**:
-`title` - Item text (via `setTitle()` mutation)
-`sort` - Item ordering within checklist (decimal number)
-`isFinished` - Item completion status (via `check()`, `uncheck()`, `toggleItem()` mutations)
**Persistence Mechanism**:
- `move()` mutation (line 159-168) handles reordering within checklists
- Direct MongoDB updates via `ChecklistItems.update()`
- Automatic timestamps: `createdAt`, `modifiedAt`
- Activity tracking for item creation/removal and completion state changes
**Issue Found**:
-**OK**: Item order and completion status persist correctly
---
### 1.6 Position History Tracking
**Collection**: `positionHistory` ([models/positionHistory.js](models/positionHistory.js))
**Purpose**: Tracks original positions of swimlanes, lists, and cards before changes
**Features**:
- ✅ Stores original `sort` position
- ✅ Stores original titles
- ✅ Supports swimlanes, lists, and cards
- ✅ Provides helpers to check if entity moved from original position
**Implementation Notes**:
- Swimlanes track position automatically on insert (swimlanes.js line 387-393)
- Lists track position automatically on insert (lists.js line 487+)
- Can detect moves via `hasMoved()` and `hasMovedFromOriginalPosition()` helpers
---
## 2. PER-USER SETTINGS (NOT Persisted Across Boards)
### 2.1 Per-Board, Per-User Settings
**Storage**: User `profile` subdocuments ([models/users.js](models/users.js))
#### A. List Widths
- **Field**: `profile.listWidths` (line 527)
- **Structure**: `listWidths[boardId][listId] = width`
- **Persistence**: Via `setListWidth()` mutation (line 1834)
- **Retrieval**: `getListWidth()`, `getListWidthFromStorage()` (line 1288-1313)
- **Constraints**: Also stored in `profile.listConstraints`
-**Status**: Working correctly
#### B. Swimlane Heights
- **Field**: `profile.swimlaneHeights` (searchable in line 1047+)
- **Structure**: `swimlaneHeights[boardId][swimlaneId] = height`
- **Persistence**: Via `setSwimlaneHeight()` mutation (line 1878)
- **Retrieval**: `getSwimlaneHeight()`, `getSwimlaneHeightFromStorage()` (line 1050-1080)
-**Status**: Working correctly
#### C. Collapsed Swimlanes (Per-User)
- **Field**: `profile.collapsedSwimlanes` (line 1900)
- **Structure**: `collapsedSwimlanes[boardId][swimlaneId] = boolean`
- **Persistence**: Via `setCollapsedSwimlane()` mutation (line 1900-1906)
- **Retrieval**: `getCollapsedSwimlaneFromStorage()` (swimlanes.js line 251-263)
- **Client-Side Fallback**: `Users.getPublicCollapsedSwimlane()` for public/non-logged-in users (users.js line 60-73)
-**Status**: Working correctly for logged-in users
#### D. Collapsed Lists (Per-User)
- **Field**: `profile.collapsedLists` (line 1893)
- **Structure**: `collapsedLists[boardId][listId] = boolean`
- **Persistence**: Via `setCollapsedList()` mutation (line 1893-1899)
- **Retrieval**: `getCollapsedListFromStorage()` (lists.js line 303-311)
- **Client-Side Fallback**: `Users.getPublicCollapsedList()` for public users (users.js line 44-52)
-**Status**: Working correctly for logged-in users
#### E. Card Collapsed State (Global Per-User)
- **Field**: `profile.cardCollapsed` (line 267)
- **Persistence**: Via `setCardCollapsed()` method (line 2088-2091)
- **Retrieval**: `cardCollapsed()` helper in cardDetails.js (line 100-107)
- **Client-Side Fallback**: `Users.getPublicCardCollapsed()` for public users (users.js line 80-85)
-**Status**: Working correctly (applies to all boards for a user)
#### F. Card Maximized State (Global Per-User)
- **Field**: `profile.cardMaximized` (line 260)
- **Persistence**: Via `toggleCardMaximized()` mutation (line 1720-1726)
- **Retrieval**: `hasCardMaximized()` helper (line 1194-1196)
-**Status**: Working correctly
#### G. Board Workspace Trees (Global Per-User)
- **Field**: `profile.boardWorkspacesTree` (line 1981-2026)
- **Purpose**: Stores nested workspace structure for organizing boards
- **Persistence**: Via `setWorkspacesTree()` method (line 1995-2000)
-**Status**: Working correctly
#### H. Board Workspace Assignments (Global Per-User)
- **Field**: `profile.boardWorkspaceAssignments` (line 2002-2011)
- **Purpose**: Maps each board to a workspace ID
- **Persistence**: Via `assignBoardToWorkspace()` and `unassignBoardFromWorkspace()` methods
-**Status**: Working correctly
#### I. All Boards Workspaces Setting
- **Field**: `profile.boardView` (line 1807)
- **Persistence**: Via `setBoardView()` method (line 1805-1809)
- **Description**: Per-user preference for "All Boards" view style
-**Status**: Working correctly
---
### 2.2 Client-Side Storage (Non-Logged-In Users)
**Storage Methods**:
1. **Cookies** (via `readCookieMap()`/`writeCookieMap()`):
- `wekan-collapsed-lists` - Collapsed list states (users.js line 44-58)
- `wekan-collapsed-swimlanes` - Collapsed swimlane states
2. **localStorage**:
- `wekan-list-widths` - List widths (getListWidthFromStorage, line 1316-1327)
- `wekan-swimlane-heights` - Swimlane heights (setSwimlaneHeightToStorage, line 1100-1123)
**Coverage**:
- ✅ Collapse status for lists and swimlanes
- ✅ Width constraints for lists
- ✅ Height constraints for swimlanes
- ❌ Card collapsed state (only via cookies, fallback available)
---
## 3. CRITICAL FINDINGS & ISSUES
### 3.1 HIGH PRIORITY ISSUES
#### Issue #1: Collapsed State Inconsistency (Swimlanes)
**Severity**: HIGH
**Location**: [models/swimlanes.js](models/swimlanes.js) lines 127, 251-263
**Problem**:
- The swimlane schema defines `collapsed` as a board-level field (defaults to false)
- But the `isCollapsed()` helper prioritizes per-user stored values from the user profile
- This creates confusion: is collapsed state board-wide or per-user?
**Expected Behavior**: Per-user settings should be stored in `profile.collapsedSwimlanes`, not in the swimlane document itself.
**Recommendation**:
```javascript
// CURRENT (WRONG):
collapsed: {
type: Boolean,
defaultValue: false, // Board-wide field
},
// SUGGESTED (CORRECT):
// Remove 'collapsed' from swimlane schema
// Only store per-user state in profile.collapsedSwimlanes
```
---
#### Issue #2: Collapsed State Inconsistency (Lists)
**Severity**: HIGH
**Location**: [models/lists.js](models/lists.js) lines 147, 303-311
**Problem**:
- Similar to swimlanes, lists have a board-level `collapsed` field
- REST API allows updating this field (line 768-775)
- But `isCollapsed()` helper checks per-user values first
- Migrations copy `collapsed` status between lists (fixMissingListsMigration.js line 165)
**Recommendation**: Clarify whether collapsed state should be:
1. **Option A**: Board-level only (remove per-user override)
2. **Option B**: Per-user only (remove board-level field)
3. **Option C**: Hybrid with clear precedence rules
---
#### Issue #3: Swimlane/List Organization Model Unclear
**Severity**: MEDIUM
**Location**: [models/lists.js](models/lists.js) lines 48, 201-230, 275
**Problem**:
- Lists have a `swimlaneId` field but `draggableLists()` filters by `boardId` only
- Some methods reference `myLists()` which filters by both `boardId` and `swimlaneId`
- Migrations suggest lists were transitioning from per-swimlane to shared-across-swimlane model
**Questions**:
- Are lists shared across all swimlanes or isolated to each swimlane?
- What happens when dragging a list to a different swimlane?
**Recommendation**: Document the intended architecture clearly.
---
### 3.2 MEDIUM PRIORITY ISSUES
#### Issue #4: Position History Only Tracks Original Position
**Severity**: MEDIUM
**Location**: [models/positionHistory.js](models/positionHistory.js)
**Problem**:
- Position history tracks the *original* position when an entity is created
- It does NOT track subsequent moves/reorders
- Historical audit trail of all position changes is lost
**Impact**: Cannot determine full history of where a card/list was located over time
**Recommendation**: Consider extending to track all position changes with timestamps.
---
#### Issue #5: Card Collapsed State is Global Per-User, Not Per-Card
**Severity**: LOW
**Location**: [models/users.js](models/users.js) line 267, users.js line 2088-2091
**Problem**:
- `profile.cardCollapsed` is a single boolean affecting all cards for a user
- It's not per-card or per-board, just a global toggle
- Name is misleading
**Recommendation**: Consider renaming to `cardDetailsCollapsedByDefault` or similar.
---
#### Issue #6: Public User Settings Storage Incomplete
**Severity**: MEDIUM
**Location**: [models/users.js](models/users.js) lines 44-85
**Problem**:
- Cookie-based storage for public users only covers:
- Collapsed lists
- Collapsed swimlanes
- Missing storage for:
- List widths
- Swimlane heights
- Card collapsed state
**Impact**: Public/non-logged-in users lose UI preferences on page reload
**Recommendation**: Implement localStorage storage for all per-user preferences.
---
### 3.3 VERIFICATION CHECKLIST
| Item | Status | Notes |
|------|--------|-------|
| Swimlane order persistence | ✅ | Via `sort` field, board-level |
| List order persistence | ✅ | Via `sort` field, board-level |
| Card order persistence | ✅ | Via `sort` field, card.move() |
| Checklist order persistence | ✅ | Via `sort` field |
| Checklist item order persistence | ✅ | Via `sort` field, ChecklistItems.move() |
| Swimlane color changes | ✅ | Via `setColor()` mutation |
| List color changes | ✅ | Via REST API or direct update |
| Card color changes | ✅ | Via `setColor()` mutation |
| Swimlane title changes | ✅ | Via `rename()` mutation, activity tracked |
| List title changes | ✅ | Via REST API or `rename()` mutation, activity tracked |
| Card title changes | ✅ | Via direct update, activity tracked |
| Checklist title changes | ✅ | Via `setTitle()` mutation |
| Checklist item title changes | ✅ | Via `setTitle()` mutation |
| Per-user list widths | ✅ | Via `profile.listWidths` |
| Per-user swimlane heights | ✅ | Via `profile.swimlaneHeights` |
| Per-user swimlane collapse state | ✅ | Via `profile.collapsedSwimlanes` |
| Per-user list collapse state | ✅ | Via `profile.collapsedLists` |
| Per-user card collapse state | ✅ | Via `profile.cardCollapsed` |
| Per-user board workspace organization | ✅ | Via `profile.boardWorkspacesTree` |
| Activity logging for changes | ✅ | Via Activities collection |
---
## 4. RECOMMENDATIONS
### 4.1 Immediate Actions
1. **Clarify Collapsed State Architecture**
- Decide if collapsed state should be per-user or board-wide
- Update swimlanes.js and lists.js schema accordingly
- Update documentation
2. **Complete Public User Storage**
- Implement localStorage for list widths/swimlane heights for non-logged-in users
- Test persistence across page reloads
3. **Review Position History Usage**
- Confirm if current position history implementation meets requirements
- Consider extending to track all changes (not just original position)
### 4.2 Long-Term Improvements
1. **Audit Trail Feature**
- Extend position history to track all moves with timestamps
- Enable board managers to see complete history of card/list movements
2. **Data Integrity Tests**
- Add integration tests to verify:
- Order is persisted correctly after drag-drop
- Color changes persist across sessions
- Per-user settings apply only to correct user
- Per-user settings don't leak across boards
3. **Database Indexes**
- Verify indexes exist for common queries:
- `sort` fields for swimlanes, lists, cards, checklists
- `boardId` fields for filtering
### 4.3 Code Quality Improvements
1. **Document Persistence Model**
- Add clear comments explaining which fields are board-level vs. per-user
- Document swimlane/list relationship model
2. **Consistent Naming**
- Rename misleading field names (e.g., `cardCollapsed`)
- Align method names with actual functionality
---
## 5. SUMMARY TABLE
### Board-Level Persistence (Shared Across Users)
| Entity | Field | Type | Persisted | Notes |
|--------|-------|------|-----------|-------|
| Swimlane | title | Text | ✅ | Via rename() |
| Swimlane | sort | Number | ✅ | For ordering |
| Swimlane | color | String | ✅ | Via setColor() |
| Swimlane | collapsed | Boolean | ⚠️ | **Issue #1**: Conflicts with per-user storage |
| Swimlane | archived | Boolean | ✅ | Via archive()/restore() |
| | | | | |
| List | title | Text | ✅ | Via rename() or REST |
| List | sort | Number | ✅ | For ordering |
| List | color | String | ✅ | Via REST or update |
| List | collapsed | Boolean | ⚠️ | **Issue #2**: Conflicts with per-user storage |
| List | starred | Boolean | ✅ | Via REST or update |
| List | wipLimit | Object | ✅ | Via REST or setWipLimit() |
| List | archived | Boolean | ✅ | Via archive() |
| | | | | |
| Card | title | Text | ✅ | Direct update |
| Card | sort | Number | ✅ | Via move() |
| Card | color | String | ✅ | Via setColor() |
| Card | boardId/swimlaneId/listId | String | ✅ | Via move() |
| Card | archived | Boolean | ✅ | Via archive() |
| Card | description | Text | ✅ | Direct update |
| Card | customFields | Array | ✅ | Direct update |
| | | | | |
| Checklist | title | Text | ✅ | Via setTitle() |
| Checklist | sort | Number | ✅ | Direct update |
| Checklist | hideCheckedChecklistItems | Boolean | ✅ | Via toggle mutation |
| Checklist | hideAllChecklistItems | Boolean | ✅ | Via toggle mutation |
| | | | | |
| ChecklistItem | title | Text | ✅ | Via setTitle() |
| ChecklistItem | sort | Number | ✅ | Via move() |
| ChecklistItem | isFinished | Boolean | ✅ | Via check/uncheck/toggle |
### Per-User Settings (NOT Persisted Across Boards)
| Setting | Storage | Scope | Notes |
|---------|---------|-------|-------|
| List Widths | profile.listWidths | Per-board, per-user | ✅ Working |
| Swimlane Heights | profile.swimlaneHeights | Per-board, per-user | ✅ Working |
| Collapsed Swimlanes | profile.collapsedSwimlanes | Per-board, per-user | ✅ Working |
| Collapsed Lists | profile.collapsedLists | Per-board, per-user | ✅ Working |
| Card Collapsed State | profile.cardCollapsed | Global per-user | ⚠️ Name misleading |
| Card Maximized State | profile.cardMaximized | Global per-user | ✅ Working |
| Board Workspaces | profile.boardWorkspacesTree | Global per-user | ✅ Working |
| Board Workspace Assignments | profile.boardWorkspaceAssignments | Global per-user | ✅ Working |
| Board View Style | profile.boardView | Global per-user | ✅ Working |
---
## Document History
- **Created**: 2025-12-23
- **Status**: Initial Audit Complete
- **Reviewed**: Swimlanes, Lists, Cards, Checklists, ChecklistItems, PositionHistory, Users
- **Next Review**: After addressing high-priority issues