mirror of
https://github.com/wekan/wekan.git
synced 2026-01-27 03:36:09 +01:00
Per-User and Board-level data save fixes. Part 3.
Some checks are pending
Some checks are pending
Thanks to xet7 !
This commit is contained in:
parent
90a7a61904
commit
a039bb1066
12 changed files with 2996 additions and 82 deletions
451
docs/Security/PerUserDataAudit2025-12-23/IMPLEMENTATION_GUIDE.md
Normal file
451
docs/Security/PerUserDataAudit2025-12-23/IMPLEMENTATION_GUIDE.md
Normal file
|
|
@ -0,0 +1,451 @@
|
|||
# Implementation Guide - Per-Board vs Per-User Data Storage
|
||||
|
||||
**Status**: ✅ Complete
|
||||
**Updated**: 2025-12-23
|
||||
**Scope**: Changes to implement per-board height/width storage and per-user-only collapse/label visibility
|
||||
|
||||
---
|
||||
|
||||
## Overview of Changes
|
||||
|
||||
This document details all changes required to properly separate per-board data from per-user data.
|
||||
|
||||
---
|
||||
|
||||
## 1. Schema Changes ✅ COMPLETED
|
||||
|
||||
### Swimlanes (swimlanes.js) ✅
|
||||
**Change**: Add `height` field to schema
|
||||
|
||||
```javascript
|
||||
// ADDED:
|
||||
height: {
|
||||
/**
|
||||
* The height of the swimlane in pixels.
|
||||
* -1 = auto-height (default)
|
||||
* 50-2000 = fixed height in pixels
|
||||
*/
|
||||
type: Number,
|
||||
optional: true,
|
||||
defaultValue: -1,
|
||||
custom() {
|
||||
const h = this.value;
|
||||
if (h !== -1 && (h < 50 || h > 2000)) {
|
||||
return 'heightOutOfRange';
|
||||
}
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
**Status**: ✅ Implemented
|
||||
|
||||
### Lists (lists.js) ✅
|
||||
**Change**: Add `width` field to schema
|
||||
|
||||
```javascript
|
||||
// ADDED:
|
||||
width: {
|
||||
/**
|
||||
* The width of the list in pixels (100-1000).
|
||||
* Default width is 272 pixels.
|
||||
*/
|
||||
type: Number,
|
||||
optional: true,
|
||||
defaultValue: 272,
|
||||
custom() {
|
||||
const w = this.value;
|
||||
if (w < 100 || w > 1000) {
|
||||
return 'widthOutOfRange';
|
||||
}
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
**Status**: ✅ Implemented
|
||||
|
||||
### Cards (cards.js) ✅
|
||||
**Current**: Already has per-board `sort` field
|
||||
**No Change Needed**: Positions stored in card.sort (per-board)
|
||||
|
||||
**Status**: ✅ Already Correct
|
||||
|
||||
### Checklists (checklists.js) ✅
|
||||
**Current**: Already has per-board `sort` field
|
||||
**No Change Needed**: Positions stored in checklist.sort (per-board)
|
||||
|
||||
**Status**: ✅ Already Correct
|
||||
|
||||
### ChecklistItems (checklistItems.js) ✅
|
||||
**Current**: Already has per-board `sort` field
|
||||
**No Change Needed**: Positions stored in checklistItem.sort (per-board)
|
||||
|
||||
**Status**: ✅ Already Correct
|
||||
|
||||
---
|
||||
|
||||
## 2. User Model Changes
|
||||
|
||||
### Users (users.js) - Remove Per-User Width/Height Storage
|
||||
|
||||
**Current Code Problem**:
|
||||
- User profile stores `listWidths` (per-user) → should be per-board
|
||||
- User profile stores `swimlaneHeights` (per-user) → should be per-board
|
||||
- These methods access user.profile.listWidths and user.profile.swimlaneHeights
|
||||
|
||||
**Solution**: Refactor these methods to read from list/swimlane documents instead
|
||||
|
||||
#### Option A: Create Migration Helper (Recommended)
|
||||
|
||||
Create a new file: `models/lib/persistenceHelpers.js`
|
||||
|
||||
```javascript
|
||||
// Get swimlane height from swimlane document (per-board storage)
|
||||
export const getSwimlaneHeight = (swimlaneId) => {
|
||||
const swimlane = Swimlanes.findOne(swimlaneId);
|
||||
return swimlane && swimlane.height !== undefined ? swimlane.height : -1;
|
||||
};
|
||||
|
||||
// Get list width from list document (per-board storage)
|
||||
export const getListWidth = (listId) => {
|
||||
const list = Lists.findOne(listId);
|
||||
return list && list.width !== undefined ? list.width : 272;
|
||||
};
|
||||
|
||||
// Set swimlane height in swimlane document (per-board storage)
|
||||
export const setSwimlaneHeight = (swimlaneId, height) => {
|
||||
if (height !== -1 && (height < 50 || height > 2000)) {
|
||||
throw new Error('Height out of range: -1 or 50-2000');
|
||||
}
|
||||
Swimlanes.update(swimlaneId, { $set: { height } });
|
||||
};
|
||||
|
||||
// Set list width in list document (per-board storage)
|
||||
export const setListWidth = (listId, width) => {
|
||||
if (width < 100 || width > 1000) {
|
||||
throw new Error('Width out of range: 100-1000');
|
||||
}
|
||||
Lists.update(listId, { $set: { width } });
|
||||
};
|
||||
```
|
||||
|
||||
#### Option B: Modify User Methods
|
||||
|
||||
**Change these methods in users.js**:
|
||||
|
||||
1. **getListWidth(boardId, listId)** - Remove per-user lookup
|
||||
```javascript
|
||||
// OLD (removes this):
|
||||
// const listWidths = this.getListWidths();
|
||||
// if (listWidths[boardId] && listWidths[boardId][listId]) {
|
||||
// return listWidths[boardId][listId];
|
||||
// }
|
||||
|
||||
// NEW:
|
||||
getListWidth(listId) {
|
||||
const list = ReactiveCache.getList({ _id: listId });
|
||||
return list && list.width ? list.width : 272;
|
||||
},
|
||||
```
|
||||
|
||||
2. **getSwimlaneHeight(boardId, swimlaneId)** - Remove per-user lookup
|
||||
```javascript
|
||||
// OLD (removes this):
|
||||
// const swimlaneHeights = this.getSwimlaneHeights();
|
||||
// if (swimlaneHeights[boardId] && swimlaneHeights[boardId][swimlaneId]) {
|
||||
// return swimlaneHeights[boardId][swimlaneId];
|
||||
// }
|
||||
|
||||
// NEW:
|
||||
getSwimlaneHeight(swimlaneId) {
|
||||
const swimlane = ReactiveCache.getSwimlane(swimlaneId);
|
||||
return swimlane && swimlane.height ? swimlane.height : -1;
|
||||
},
|
||||
```
|
||||
|
||||
3. **setListWidth(boardId, listId, width)** - Update list document
|
||||
```javascript
|
||||
// OLD (removes this):
|
||||
// let currentWidths = this.getListWidths();
|
||||
// if (!currentWidths[boardId]) {
|
||||
// currentWidths[boardId] = {};
|
||||
// }
|
||||
// currentWidths[boardId][listId] = width;
|
||||
|
||||
// NEW:
|
||||
setListWidth(listId, width) {
|
||||
Lists.update(listId, { $set: { width } });
|
||||
},
|
||||
```
|
||||
|
||||
4. **setSwimlaneHeight(boardId, swimlaneId, height)** - Update swimlane document
|
||||
```javascript
|
||||
// OLD (removes this):
|
||||
// let currentHeights = this.getSwimlaneHeights();
|
||||
// if (!currentHeights[boardId]) {
|
||||
// currentHeights[boardId] = {};
|
||||
// }
|
||||
// currentHeights[boardId][swimlaneId] = height;
|
||||
|
||||
// NEW:
|
||||
setSwimlaneHeight(swimlaneId, height) {
|
||||
Swimlanes.update(swimlaneId, { $set: { height } });
|
||||
},
|
||||
```
|
||||
|
||||
### Keep These Per-User Storage Methods
|
||||
|
||||
These should remain in user profile (per-user only):
|
||||
|
||||
1. **Collapse Swimlanes** (per-user)
|
||||
```javascript
|
||||
getCollapsedSwimlanes() {
|
||||
const { collapsedSwimlanes = {} } = this.profile || {};
|
||||
return collapsedSwimlanes;
|
||||
},
|
||||
setCollapsedSwimlane(boardId, swimlaneId, collapsed) {
|
||||
// ... update user.profile.collapsedSwimlanes[boardId][swimlaneId]
|
||||
},
|
||||
isCollapsedSwimlane(boardId, swimlaneId) {
|
||||
// ... check user.profile.collapsedSwimlanes
|
||||
},
|
||||
```
|
||||
|
||||
2. **Collapse Lists** (per-user)
|
||||
```javascript
|
||||
getCollapsedLists() {
|
||||
const { collapsedLists = {} } = this.profile || {};
|
||||
return collapsedLists;
|
||||
},
|
||||
setCollapsedList(boardId, listId, collapsed) {
|
||||
// ... update user.profile.collapsedLists[boardId][listId]
|
||||
},
|
||||
isCollapsedList(boardId, listId) {
|
||||
// ... check user.profile.collapsedLists
|
||||
},
|
||||
```
|
||||
|
||||
3. **Hide Minicard Label Text** (per-user)
|
||||
```javascript
|
||||
getHideMiniCardLabelText(boardId) {
|
||||
const { hideMiniCardLabelText = {} } = this.profile || {};
|
||||
return hideMiniCardLabelText[boardId] || false;
|
||||
},
|
||||
setHideMiniCardLabelText(boardId, hidden) {
|
||||
// ... update user.profile.hideMiniCardLabelText[boardId]
|
||||
},
|
||||
```
|
||||
|
||||
### Remove From User Schema
|
||||
|
||||
These fields should be removed from user.profile schema in users.js:
|
||||
|
||||
```javascript
|
||||
// REMOVE from schema:
|
||||
'profile.listWidths': { ... }, // Now stored in list.width
|
||||
'profile.swimlaneHeights': { ... }, // Now stored in swimlane.height
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Client-Side Changes
|
||||
|
||||
### Storage Access Layer
|
||||
|
||||
When UI needs to get/set widths and heights:
|
||||
|
||||
**OLD APPROACH** (removes this):
|
||||
```javascript
|
||||
// Getting from user profile
|
||||
const width = Meteor.user().getListWidth(boardId, listId);
|
||||
|
||||
// Setting to user profile
|
||||
Meteor.call('setListWidth', boardId, listId, 300);
|
||||
```
|
||||
|
||||
**NEW APPROACH**:
|
||||
```javascript
|
||||
// Getting from list document
|
||||
const width = Lists.findOne(listId)?.width || 272;
|
||||
|
||||
// Setting to list document
|
||||
Lists.update(listId, { $set: { width: 300 } });
|
||||
```
|
||||
|
||||
### Meteor Methods to Remove
|
||||
|
||||
Remove these Meteor methods that updated user profile:
|
||||
|
||||
```javascript
|
||||
// Remove:
|
||||
Meteor.methods({
|
||||
'setListWidth': function(boardId, listId, width) { ... },
|
||||
'setSwimlaneHeight': function(boardId, swimlaneId, height) { ... },
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Migration Script
|
||||
|
||||
Create file: `server/migrations/migrateToPerBoardStorage.js`
|
||||
|
||||
```javascript
|
||||
const MIGRATION_NAME = 'migrate-to-per-board-height-width-storage';
|
||||
|
||||
Migrations = new Mongo.Collection('migrations');
|
||||
|
||||
Meteor.startup(() => {
|
||||
const existingMigration = Migrations.findOne({ name: MIGRATION_NAME });
|
||||
|
||||
if (!existingMigration) {
|
||||
try {
|
||||
// Migrate swimlane heights from user.profile to swimlane.height
|
||||
Meteor.users.find().forEach(user => {
|
||||
const swimlaneHeights = user.profile?.swimlaneHeights || {};
|
||||
|
||||
Object.keys(swimlaneHeights).forEach(boardId => {
|
||||
Object.keys(swimlaneHeights[boardId]).forEach(swimlaneId => {
|
||||
const height = swimlaneHeights[boardId][swimlaneId];
|
||||
|
||||
// Validate height
|
||||
if (height === -1 || (height >= 50 && height <= 2000)) {
|
||||
Swimlanes.update(
|
||||
{ _id: swimlaneId, boardId },
|
||||
{ $set: { height } },
|
||||
{ multi: false }
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Migrate list widths from user.profile to list.width
|
||||
Meteor.users.find().forEach(user => {
|
||||
const listWidths = user.profile?.listWidths || {};
|
||||
|
||||
Object.keys(listWidths).forEach(boardId => {
|
||||
Object.keys(listWidths[boardId]).forEach(listId => {
|
||||
const width = listWidths[boardId][listId];
|
||||
|
||||
// Validate width
|
||||
if (width >= 100 && width <= 1000) {
|
||||
Lists.update(
|
||||
{ _id: listId, boardId },
|
||||
{ $set: { width } },
|
||||
{ multi: false }
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Record successful migration
|
||||
Migrations.insert({
|
||||
name: MIGRATION_NAME,
|
||||
status: 'completed',
|
||||
createdAt: new Date(),
|
||||
migratedSwimlanes: Swimlanes.find({ height: { $exists: true, $ne: -1 } }).count(),
|
||||
migratedLists: Lists.find({ width: { $exists: true, $ne: 272 } }).count(),
|
||||
});
|
||||
|
||||
console.log('✅ Migration to per-board height/width storage completed');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Migration failed:', error);
|
||||
Migrations.insert({
|
||||
name: MIGRATION_NAME,
|
||||
status: 'failed',
|
||||
error: error.message,
|
||||
createdAt: new Date(),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Testing Checklist
|
||||
|
||||
### Schema Testing
|
||||
- [ ] Swimlane with height = -1 accepts insert
|
||||
- [ ] Swimlane with height = 100 accepts insert
|
||||
- [ ] Swimlane with height = 25 rejects (< 50)
|
||||
- [ ] Swimlane with height = 3000 rejects (> 2000)
|
||||
- [ ] List with width = 272 accepts insert
|
||||
- [ ] List with width = 50 rejects (< 100)
|
||||
- [ ] List with width = 2000 rejects (> 1000)
|
||||
|
||||
### Data Persistence Testing
|
||||
- [ ] Resize swimlane → height saved in swimlane document
|
||||
- [ ] Reload page → swimlane height persists
|
||||
- [ ] Different user loads page → sees same height
|
||||
- [ ] Resize list → width saved in list document
|
||||
- [ ] Reload page → list width persists
|
||||
- [ ] Different user loads page → sees same width
|
||||
|
||||
### Per-User Testing
|
||||
- [ ] User A collapses swimlane → User B sees it expanded
|
||||
- [ ] User A hides labels → User B sees labels
|
||||
- [ ] Reload page → per-user preferences persist for same user
|
||||
- [ ] Different user logs in → doesn't see previous user's preferences
|
||||
|
||||
### Migration Testing
|
||||
- [ ] Run migration on database with old per-user data
|
||||
- [ ] All swimlane heights migrated to swimlane documents
|
||||
- [ ] All list widths migrated to list documents
|
||||
- [ ] User.profile.swimlaneHeights can be safely removed
|
||||
- [ ] User.profile.listWidths can be safely removed
|
||||
|
||||
---
|
||||
|
||||
## 6. Rollback Plan
|
||||
|
||||
If issues occur:
|
||||
|
||||
1. **Before Migration**: Backup MongoDB
|
||||
```bash
|
||||
mongodump -d wekan -o backup-wekan-before-migration
|
||||
```
|
||||
|
||||
2. **If Needed**: Restore from backup
|
||||
```bash
|
||||
mongorestore -d wekan backup-wekan-before-migration/wekan
|
||||
```
|
||||
|
||||
3. **Revert Code**: Restore previous swimlanes.js, lists.js, users.js
|
||||
|
||||
---
|
||||
|
||||
## 7. Files Modified
|
||||
|
||||
| File | Change | Status |
|
||||
|------|--------|--------|
|
||||
| [models/swimlanes.js](../../../models/swimlanes.js) | Add height field | ✅ Done |
|
||||
| [models/lists.js](../../../models/lists.js) | Add width field | ✅ Done |
|
||||
| [models/users.js](../../../models/users.js) | Refactor height/width methods | ⏳ TODO |
|
||||
| server/migrations/migrateToPerBoardStorage.js | Migration script | ⏳ TODO |
|
||||
| [docs/Security/PerUserDataAudit2025-12-23/DATA_PERSISTENCE_ARCHITECTURE.md](DATA_PERSISTENCE_ARCHITECTURE.md) | Architecture docs | ✅ Done |
|
||||
|
||||
---
|
||||
|
||||
## 8. Summary of Per-User vs Per-Board Data
|
||||
|
||||
### ✅ Per-Board Data (All Users See Same Value)
|
||||
- Swimlane height
|
||||
- List width
|
||||
- Card position (sort)
|
||||
- Checklist position (sort)
|
||||
- ChecklistItem position (sort)
|
||||
- All titles, colors, descriptions
|
||||
|
||||
### 🔒 Per-User Data (Only That User Sees Their Value)
|
||||
- Collapse state (swimlane, list, card)
|
||||
- Hide minicard label text visibility
|
||||
- Stored in user.profile or cookie
|
||||
|
||||
---
|
||||
|
||||
**Status**: ✅ Architecture and schema changes complete
|
||||
**Next**: Refactor user methods and run migration
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue