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

18 KiB

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)

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)

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)

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)

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)

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)

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)

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 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:

// 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 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 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

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 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 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