mirror of
https://github.com/wekan/wekan.git
synced 2026-02-09 09:44:22 +01:00
Reverted New UI Design of WeKan v8.29 and added more fixes and performance improvements.
Thanks to xet7 !
This commit is contained in:
parent
d152d8fc1b
commit
1b8b8d2eef
196 changed files with 17659 additions and 10028 deletions
|
|
@ -150,7 +150,7 @@ if (Meteor.isServer) {
|
|||
readStream.on('end', () => {
|
||||
const fileBuffer = Buffer.concat(chunks);
|
||||
const base64Data = fileBuffer.toString('base64');
|
||||
|
||||
|
||||
resolve({
|
||||
success: true,
|
||||
attachmentId: attachmentId,
|
||||
|
|
@ -200,7 +200,7 @@ if (Meteor.isServer) {
|
|||
}
|
||||
|
||||
const attachments = ReactiveCache.getAttachments(query);
|
||||
|
||||
|
||||
const attachmentList = attachments.map(attachment => {
|
||||
const strategy = fileStoreStrategyFactory.getFileStrategy(attachment, 'original');
|
||||
return {
|
||||
|
|
@ -438,7 +438,7 @@ if (Meteor.isServer) {
|
|||
|
||||
try {
|
||||
const strategy = fileStoreStrategyFactory.getFileStrategy(attachment, 'original');
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
attachmentId: attachment._id,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { ReactiveVar } from 'meteor/reactive-var';
|
|||
import { check } from 'meteor/check';
|
||||
import { ReactiveCache } from '/imports/reactiveCache';
|
||||
import Attachments from '/models/attachments';
|
||||
import { AttachmentMigrationStatus } from './attachmentMigrationStatus';
|
||||
|
||||
// Reactive variables for tracking migration progress
|
||||
const migrationProgress = new ReactiveVar(0);
|
||||
|
|
@ -28,7 +29,21 @@ class AttachmentMigrationService {
|
|||
* @returns {boolean} - True if board has been migrated
|
||||
*/
|
||||
isBoardMigrated(boardId) {
|
||||
return migratedBoards.has(boardId);
|
||||
const isMigrated = migratedBoards.has(boardId);
|
||||
|
||||
// Update status collection for pub/sub
|
||||
AttachmentMigrationStatus.upsert(
|
||||
{ boardId },
|
||||
{
|
||||
$set: {
|
||||
boardId,
|
||||
isMigrated,
|
||||
updatedAt: new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return isMigrated;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -44,7 +59,7 @@ class AttachmentMigrationService {
|
|||
}
|
||||
|
||||
console.log(`Starting attachment migration for board: ${boardId}`);
|
||||
|
||||
|
||||
// Get all attachments for the board
|
||||
const attachments = Attachments.find({
|
||||
'meta.boardId': boardId
|
||||
|
|
@ -63,12 +78,12 @@ class AttachmentMigrationService {
|
|||
await this.migrateAttachment(attachment);
|
||||
this.migrationCache.set(attachment._id, true);
|
||||
}
|
||||
|
||||
|
||||
migratedCount++;
|
||||
const progress = Math.round((migratedCount / totalAttachments) * 100);
|
||||
migrationProgress.set(progress);
|
||||
migrationStatus.set(`Migrated ${migratedCount}/${totalAttachments} attachments...`);
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error migrating attachment ${attachment._id}:`, error);
|
||||
}
|
||||
|
|
@ -86,6 +101,23 @@ class AttachmentMigrationService {
|
|||
console.log(`Attachment migration completed for board: ${boardId}`);
|
||||
console.log(`Marked board ${boardId} as migrated`);
|
||||
|
||||
// Update status collection
|
||||
AttachmentMigrationStatus.upsert(
|
||||
{ boardId },
|
||||
{
|
||||
$set: {
|
||||
boardId,
|
||||
isMigrated: true,
|
||||
totalAttachments,
|
||||
migratedAttachments: totalAttachments,
|
||||
unconvertedAttachments: 0,
|
||||
progress: 100,
|
||||
status: 'completed',
|
||||
updatedAt: new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return { success: true, message: 'Migration completed' };
|
||||
|
||||
} catch (error) {
|
||||
|
|
@ -106,8 +138,8 @@ class AttachmentMigrationService {
|
|||
}
|
||||
|
||||
// Check if attachment has old structure
|
||||
return !attachment.meta ||
|
||||
!attachment.meta.cardId ||
|
||||
return !attachment.meta ||
|
||||
!attachment.meta.cardId ||
|
||||
!attachment.meta.boardId ||
|
||||
!attachment.meta.listId;
|
||||
}
|
||||
|
|
@ -188,6 +220,25 @@ class AttachmentMigrationService {
|
|||
const progress = migrationProgress.get();
|
||||
const status = migrationStatus.get();
|
||||
const unconverted = this.getUnconvertedAttachments(boardId);
|
||||
const total = Attachments.find({ 'meta.boardId': boardId }).count();
|
||||
const migratedCount = total - unconverted.length;
|
||||
|
||||
// Update status collection for pub/sub
|
||||
AttachmentMigrationStatus.upsert(
|
||||
{ boardId },
|
||||
{
|
||||
$set: {
|
||||
boardId,
|
||||
totalAttachments: total,
|
||||
migratedAttachments: migratedCount,
|
||||
unconvertedAttachments: unconverted.length,
|
||||
progress: total > 0 ? Math.round((migratedCount / total) * 100) : 0,
|
||||
status: status || 'idle',
|
||||
isMigrated: unconverted.length === 0,
|
||||
updatedAt: new Date()
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
progress,
|
||||
|
|
@ -203,20 +254,20 @@ const attachmentMigrationService = new AttachmentMigrationService();
|
|||
Meteor.methods({
|
||||
async 'attachmentMigration.migrateBoardAttachments'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) {
|
||||
throw new Meteor.Error('board-not-found');
|
||||
}
|
||||
|
||||
|
||||
const user = ReactiveCache.getUser(this.userId);
|
||||
const isBoardAdmin = board.hasAdmin(this.userId);
|
||||
const isInstanceAdmin = user && user.isAdmin;
|
||||
|
||||
|
||||
if (!isBoardAdmin && !isInstanceAdmin) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be a board admin or instance admin to perform this action.');
|
||||
}
|
||||
|
|
@ -226,11 +277,11 @@ Meteor.methods({
|
|||
|
||||
'attachmentMigration.getProgress'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board || !board.isVisibleBy({ _id: this.userId })) {
|
||||
throw new Meteor.Error('not-authorized', 'You do not have access to this board.');
|
||||
|
|
@ -241,11 +292,11 @@ Meteor.methods({
|
|||
|
||||
'attachmentMigration.getUnconvertedAttachments'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board || !board.isVisibleBy({ _id: this.userId })) {
|
||||
throw new Meteor.Error('not-authorized', 'You do not have access to this board.');
|
||||
|
|
@ -256,11 +307,11 @@ Meteor.methods({
|
|||
|
||||
'attachmentMigration.isBoardMigrated'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board || !board.isVisibleBy({ _id: this.userId })) {
|
||||
throw new Meteor.Error('not-authorized', 'You do not have access to this board.');
|
||||
|
|
|
|||
22
server/attachmentMigrationStatus.js
Normal file
22
server/attachmentMigrationStatus.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { Mongo } from 'meteor/mongo';
|
||||
|
||||
// Server-side collection for attachment migration status
|
||||
export const AttachmentMigrationStatus = new Mongo.Collection('attachmentMigrationStatus');
|
||||
|
||||
// Allow/Deny rules
|
||||
// This collection is server-only and should not be modified by clients
|
||||
// Allow server-side operations (when userId is undefined) but deny all client operations
|
||||
if (Meteor.isServer) {
|
||||
AttachmentMigrationStatus.allow({
|
||||
insert: (userId) => !userId,
|
||||
update: (userId) => !userId,
|
||||
remove: (userId) => !userId,
|
||||
});
|
||||
}
|
||||
|
||||
// Create indexes for better query performance
|
||||
Meteor.startup(() => {
|
||||
AttachmentMigrationStatus._collection.createIndexAsync({ boardId: 1 });
|
||||
AttachmentMigrationStatus._collection.createIndexAsync({ userId: 1, boardId: 1 });
|
||||
AttachmentMigrationStatus._collection.createIndexAsync({ updatedAt: -1 });
|
||||
});
|
||||
|
|
@ -63,7 +63,7 @@ class BoardMigrationDetector {
|
|||
isSystemIdle() {
|
||||
const resources = cronJobStorage.getSystemResources();
|
||||
const queueStats = cronJobStorage.getQueueStats();
|
||||
|
||||
|
||||
// Check if no jobs are running
|
||||
if (queueStats.running > 0) {
|
||||
return false;
|
||||
|
|
@ -120,7 +120,7 @@ class BoardMigrationDetector {
|
|||
|
||||
try {
|
||||
// Scanning for unmigrated boards
|
||||
|
||||
|
||||
// Get all boards from the database
|
||||
const boards = this.getAllBoards();
|
||||
const unmigrated = [];
|
||||
|
|
@ -155,7 +155,7 @@ class BoardMigrationDetector {
|
|||
if (typeof Boards !== 'undefined') {
|
||||
return Boards.find({}, { fields: { _id: 1, title: 1, createdAt: 1, modifiedAt: 1 } }).fetch();
|
||||
}
|
||||
|
||||
|
||||
// Fallback: return empty array if Boards collection not available
|
||||
return [];
|
||||
} catch (error) {
|
||||
|
|
@ -171,14 +171,14 @@ class BoardMigrationDetector {
|
|||
try {
|
||||
// Check if board has been migrated by looking for migration markers
|
||||
const migrationMarkers = this.getMigrationMarkers(board._id);
|
||||
|
||||
|
||||
// Check for specific migration indicators
|
||||
const needsListMigration = !migrationMarkers.listsMigrated;
|
||||
const needsAttachmentMigration = !migrationMarkers.attachmentsMigrated;
|
||||
const needsSwimlaneMigration = !migrationMarkers.swimlanesMigrated;
|
||||
|
||||
|
||||
return needsListMigration || needsAttachmentMigration || needsSwimlaneMigration;
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error checking migration status for board ${board._id}:`, error);
|
||||
return false;
|
||||
|
|
@ -192,7 +192,7 @@ class BoardMigrationDetector {
|
|||
try {
|
||||
// Check if board has migration metadata
|
||||
const board = Boards.findOne(boardId, { fields: { migrationMarkers: 1 } });
|
||||
|
||||
|
||||
if (!board || !board.migrationMarkers) {
|
||||
return {
|
||||
listsMigrated: false,
|
||||
|
|
@ -230,7 +230,7 @@ class BoardMigrationDetector {
|
|||
|
||||
// Create migration job for this board
|
||||
const jobId = `board_migration_${board._id}_${Date.now()}`;
|
||||
|
||||
|
||||
// Add to job queue with high priority
|
||||
cronJobStorage.addToQueue(jobId, 'board_migration', 1, {
|
||||
boardId: board._id,
|
||||
|
|
@ -292,14 +292,14 @@ class BoardMigrationDetector {
|
|||
getBoardMigrationStatus(boardId) {
|
||||
const unmigrated = unmigratedBoards.get();
|
||||
const isUnmigrated = unmigrated.some(b => b._id === boardId);
|
||||
|
||||
|
||||
if (!isUnmigrated) {
|
||||
return { needsMigration: false, reason: 'Board is already migrated' };
|
||||
}
|
||||
|
||||
const migrationMarkers = this.getMigrationMarkers(boardId);
|
||||
const needsMigration = !migrationMarkers.listsMigrated ||
|
||||
!migrationMarkers.attachmentsMigrated ||
|
||||
const needsMigration = !migrationMarkers.listsMigrated ||
|
||||
!migrationMarkers.attachmentsMigrated ||
|
||||
!migrationMarkers.swimlanesMigrated;
|
||||
|
||||
return {
|
||||
|
|
@ -352,7 +352,7 @@ Meteor.methods({
|
|||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
return boardMigrationDetector.getMigrationStats();
|
||||
},
|
||||
|
||||
|
|
@ -360,38 +360,38 @@ Meteor.methods({
|
|||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
return boardMigrationDetector.forceScan();
|
||||
},
|
||||
|
||||
'boardMigration.getBoardStatus'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
return boardMigrationDetector.getBoardMigrationStatus(boardId);
|
||||
},
|
||||
|
||||
'boardMigration.markAsMigrated'(boardId, migrationType) {
|
||||
check(boardId, String);
|
||||
check(migrationType, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
return boardMigrationDetector.markBoardAsMigrated(boardId, migrationType);
|
||||
},
|
||||
|
||||
'boardMigration.startBoardMigration'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
return boardMigrationDetector.startBoardMigration(boardId);
|
||||
},
|
||||
|
||||
|
|
@ -399,7 +399,7 @@ Meteor.methods({
|
|||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
// Find boards that have migration markers but no migrationVersion
|
||||
const stuckBoards = Boards.find({
|
||||
'migrationMarkers.fullMigrationCompleted': true,
|
||||
|
|
@ -408,15 +408,15 @@ Meteor.methods({
|
|||
{ migrationVersion: { $lt: 1 } }
|
||||
]
|
||||
}).fetch();
|
||||
|
||||
|
||||
let fixedCount = 0;
|
||||
stuckBoards.forEach(board => {
|
||||
try {
|
||||
Boards.update(board._id, {
|
||||
$set: {
|
||||
Boards.update(board._id, {
|
||||
$set: {
|
||||
migrationVersion: 1,
|
||||
'migrationMarkers.lastMigration': new Date()
|
||||
}
|
||||
}
|
||||
});
|
||||
fixedCount++;
|
||||
console.log(`Fixed stuck board: ${board._id} (${board.title})`);
|
||||
|
|
@ -424,7 +424,7 @@ Meteor.methods({
|
|||
console.error(`Error fixing board ${board._id}:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
message: `Fixed ${fixedCount} stuck boards`,
|
||||
fixedCount,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,38 @@ export const CronJobSteps = new Mongo.Collection('cronJobSteps');
|
|||
export const CronJobQueue = new Mongo.Collection('cronJobQueue');
|
||||
export const CronJobErrors = new Mongo.Collection('cronJobErrors');
|
||||
|
||||
// Allow/Deny rules
|
||||
// These collections are server-only and should not be modified by clients
|
||||
// Allow server-side operations (when userId is undefined) but deny all client operations
|
||||
if (Meteor.isServer) {
|
||||
// Helper function to check if operation is server-only
|
||||
const isServerOperation = (userId) => !userId;
|
||||
|
||||
CronJobStatus.allow({
|
||||
insert: isServerOperation,
|
||||
update: isServerOperation,
|
||||
remove: isServerOperation,
|
||||
});
|
||||
|
||||
CronJobSteps.allow({
|
||||
insert: isServerOperation,
|
||||
update: isServerOperation,
|
||||
remove: isServerOperation,
|
||||
});
|
||||
|
||||
CronJobQueue.allow({
|
||||
insert: isServerOperation,
|
||||
update: isServerOperation,
|
||||
remove: isServerOperation,
|
||||
});
|
||||
|
||||
CronJobErrors.allow({
|
||||
insert: isServerOperation,
|
||||
update: isServerOperation,
|
||||
remove: isServerOperation,
|
||||
});
|
||||
}
|
||||
|
||||
// Indexes for performance
|
||||
if (Meteor.isServer) {
|
||||
Meteor.startup(async () => {
|
||||
|
|
@ -55,7 +87,7 @@ class CronJobStorage {
|
|||
if (envLimit) {
|
||||
return parseInt(envLimit, 10);
|
||||
}
|
||||
|
||||
|
||||
// Auto-detect based on CPU cores
|
||||
const os = require('os');
|
||||
const cpuCores = os.cpus().length;
|
||||
|
|
@ -68,7 +100,7 @@ class CronJobStorage {
|
|||
saveJobStatus(jobId, jobData) {
|
||||
const now = new Date();
|
||||
const existingJob = CronJobStatus.findOne({ jobId });
|
||||
|
||||
|
||||
if (existingJob) {
|
||||
CronJobStatus.update(
|
||||
{ jobId },
|
||||
|
|
@ -111,7 +143,7 @@ class CronJobStorage {
|
|||
saveJobStep(jobId, stepIndex, stepData) {
|
||||
const now = new Date();
|
||||
const existingStep = CronJobSteps.findOne({ jobId, stepIndex });
|
||||
|
||||
|
||||
if (existingStep) {
|
||||
CronJobSteps.update(
|
||||
{ jobId, stepIndex },
|
||||
|
|
@ -159,7 +191,7 @@ class CronJobStorage {
|
|||
saveJobError(jobId, errorData) {
|
||||
const now = new Date();
|
||||
const { stepId, stepIndex, error, severity = 'error', context = {} } = errorData;
|
||||
|
||||
|
||||
CronJobErrors.insert({
|
||||
jobId,
|
||||
stepId,
|
||||
|
|
@ -177,15 +209,15 @@ class CronJobStorage {
|
|||
*/
|
||||
getJobErrors(jobId, options = {}) {
|
||||
const { limit = 100, severity = null } = options;
|
||||
|
||||
|
||||
const query = { jobId };
|
||||
if (severity) {
|
||||
query.severity = severity;
|
||||
}
|
||||
|
||||
return CronJobErrors.find(query, {
|
||||
|
||||
return CronJobErrors.find(query, {
|
||||
sort: { createdAt: -1 },
|
||||
limit
|
||||
limit
|
||||
}).fetch();
|
||||
}
|
||||
|
||||
|
|
@ -193,9 +225,9 @@ class CronJobStorage {
|
|||
* Get all recent errors across all jobs
|
||||
*/
|
||||
getAllRecentErrors(limit = 50) {
|
||||
return CronJobErrors.find({}, {
|
||||
return CronJobErrors.find({}, {
|
||||
sort: { createdAt: -1 },
|
||||
limit
|
||||
limit
|
||||
}).fetch();
|
||||
}
|
||||
|
||||
|
|
@ -211,13 +243,13 @@ class CronJobStorage {
|
|||
*/
|
||||
addToQueue(jobId, jobType, priority = 5, jobData = {}) {
|
||||
const now = new Date();
|
||||
|
||||
|
||||
// Check if job already exists in queue
|
||||
const existingJob = CronJobQueue.findOne({ jobId });
|
||||
if (existingJob) {
|
||||
return existingJob._id;
|
||||
}
|
||||
|
||||
|
||||
return CronJobQueue.insert({
|
||||
jobId,
|
||||
jobType,
|
||||
|
|
@ -269,26 +301,26 @@ class CronJobStorage {
|
|||
*/
|
||||
getSystemResources() {
|
||||
const os = require('os');
|
||||
|
||||
|
||||
// Get CPU usage (simplified)
|
||||
const cpus = os.cpus();
|
||||
let totalIdle = 0;
|
||||
let totalTick = 0;
|
||||
|
||||
|
||||
cpus.forEach(cpu => {
|
||||
for (const type in cpu.times) {
|
||||
totalTick += cpu.times[type];
|
||||
}
|
||||
totalIdle += cpu.times.idle;
|
||||
});
|
||||
|
||||
|
||||
const cpuUsage = 100 - Math.round(100 * totalIdle / totalTick);
|
||||
|
||||
|
||||
// Get memory usage
|
||||
const totalMem = os.totalmem();
|
||||
const freeMem = os.freemem();
|
||||
const memoryUsage = Math.round(100 * (totalMem - freeMem) / totalMem);
|
||||
|
||||
|
||||
return {
|
||||
cpuUsage,
|
||||
memoryUsage,
|
||||
|
|
@ -304,21 +336,21 @@ class CronJobStorage {
|
|||
canStartNewJob() {
|
||||
const resources = this.getSystemResources();
|
||||
const runningJobs = CronJobQueue.find({ status: 'running' }).count();
|
||||
|
||||
|
||||
// Check CPU and memory thresholds
|
||||
if (resources.cpuUsage > this.cpuThreshold) {
|
||||
return { canStart: false, reason: 'CPU usage too high' };
|
||||
}
|
||||
|
||||
|
||||
if (resources.memoryUsage > this.memoryThreshold) {
|
||||
return { canStart: false, reason: 'Memory usage too high' };
|
||||
}
|
||||
|
||||
|
||||
// Check concurrent job limit
|
||||
if (runningJobs >= this.maxConcurrentJobs) {
|
||||
return { canStart: false, reason: 'Maximum concurrent jobs reached' };
|
||||
}
|
||||
|
||||
|
||||
return { canStart: true, reason: 'System can handle new job' };
|
||||
}
|
||||
|
||||
|
|
@ -331,7 +363,7 @@ class CronJobStorage {
|
|||
const running = CronJobQueue.find({ status: 'running' }).count();
|
||||
const completed = CronJobQueue.find({ status: 'completed' }).count();
|
||||
const failed = CronJobQueue.find({ status: 'failed' }).count();
|
||||
|
||||
|
||||
return {
|
||||
total,
|
||||
pending,
|
||||
|
|
@ -348,25 +380,25 @@ class CronJobStorage {
|
|||
cleanupOldJobs(daysOld = 7) {
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
|
||||
|
||||
|
||||
// Remove old completed jobs from queue
|
||||
const removedQueue = CronJobQueue.remove({
|
||||
status: 'completed',
|
||||
updatedAt: { $lt: cutoffDate }
|
||||
});
|
||||
|
||||
|
||||
// Remove old job statuses
|
||||
const removedStatus = CronJobStatus.remove({
|
||||
status: 'completed',
|
||||
updatedAt: { $lt: cutoffDate }
|
||||
});
|
||||
|
||||
|
||||
// Remove old job steps
|
||||
const removedSteps = CronJobSteps.remove({
|
||||
status: 'completed',
|
||||
updatedAt: { $lt: cutoffDate }
|
||||
});
|
||||
|
||||
|
||||
return {
|
||||
removedQueue,
|
||||
removedStatus,
|
||||
|
|
@ -380,7 +412,7 @@ class CronJobStorage {
|
|||
resumeIncompleteJobs() {
|
||||
const incompleteJobs = this.getIncompleteJobs();
|
||||
const resumedJobs = [];
|
||||
|
||||
|
||||
incompleteJobs.forEach(job => {
|
||||
// Reset running jobs to pending
|
||||
if (job.status === 'running') {
|
||||
|
|
@ -391,14 +423,14 @@ class CronJobStorage {
|
|||
});
|
||||
resumedJobs.push(job.jobId);
|
||||
}
|
||||
|
||||
|
||||
// Add to queue if not already there
|
||||
const queueJob = CronJobQueue.findOne({ jobId: job.jobId });
|
||||
if (!queueJob) {
|
||||
this.addToQueue(job.jobId, job.jobType || 'unknown', job.priority || 5, job);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
return resumedJobs;
|
||||
}
|
||||
|
||||
|
|
@ -408,7 +440,7 @@ class CronJobStorage {
|
|||
getJobProgress(jobId) {
|
||||
const steps = this.getJobSteps(jobId);
|
||||
if (steps.length === 0) return 0;
|
||||
|
||||
|
||||
const completedSteps = steps.filter(step => step.status === 'completed').length;
|
||||
return Math.round((completedSteps / steps.length) * 100);
|
||||
}
|
||||
|
|
@ -420,7 +452,7 @@ class CronJobStorage {
|
|||
const jobStatus = this.getJobStatus(jobId);
|
||||
const jobSteps = this.getJobSteps(jobId);
|
||||
const progress = this.getJobProgress(jobId);
|
||||
|
||||
|
||||
return {
|
||||
...jobStatus,
|
||||
steps: jobSteps,
|
||||
|
|
@ -440,7 +472,7 @@ class CronJobStorage {
|
|||
CronJobSteps.remove({});
|
||||
CronJobQueue.remove({});
|
||||
CronJobErrors.remove({});
|
||||
|
||||
|
||||
console.log('All cron job data cleared from storage');
|
||||
return { success: true, message: 'All cron job data cleared' };
|
||||
} catch (error) {
|
||||
|
|
@ -460,7 +492,7 @@ Meteor.startup(() => {
|
|||
if (resumedJobs.length > 0) {
|
||||
// Resumed incomplete cron jobs
|
||||
}
|
||||
|
||||
|
||||
// Cleanup old jobs
|
||||
const cleanup = cronJobStorage.cleanupOldJobs();
|
||||
if (cleanup.removedQueue > 0 || cleanup.removedStatus > 0 || cleanup.removedSteps > 0) {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -161,7 +161,7 @@ describe('attachmentApi authentication', function() {
|
|||
describe('request handler DoS prevention', function() {
|
||||
it('enforces timeout on hanging requests', function(done) {
|
||||
this.timeout(5000);
|
||||
|
||||
|
||||
const req = createMockReq({ 'x-user-id': 'user1', 'x-auth-token': 'token1' });
|
||||
const res = createMockRes();
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ Meteor.methods({
|
|||
if (process.env.DEBUG === 'true') {
|
||||
console.log('Starting duplicate lists fix for all boards...');
|
||||
}
|
||||
|
||||
|
||||
const allBoards = Boards.find({}).fetch();
|
||||
let totalFixed = 0;
|
||||
let totalBoardsProcessed = 0;
|
||||
|
|
@ -33,7 +33,7 @@ Meteor.methods({
|
|||
const result = fixDuplicateListsForBoard(board._id);
|
||||
totalFixed += result.fixed;
|
||||
totalBoardsProcessed++;
|
||||
|
||||
|
||||
if (result.fixed > 0 && process.env.DEBUG === 'true') {
|
||||
console.log(`Fixed ${result.fixed} duplicate lists in board "${board.title}" (${board._id})`);
|
||||
}
|
||||
|
|
@ -45,7 +45,7 @@ Meteor.methods({
|
|||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Duplicate lists fix completed. Processed ${totalBoardsProcessed} boards, fixed ${totalFixed} duplicate lists.`);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
message: `Fixed ${totalFixed} duplicate lists across ${totalBoardsProcessed} boards`,
|
||||
totalFixed,
|
||||
|
|
@ -55,7 +55,7 @@ Meteor.methods({
|
|||
|
||||
'fixDuplicateLists.fixBoard'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
|
@ -74,13 +74,13 @@ function fixDuplicateListsForBoard(boardId) {
|
|||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Fixing duplicate lists for board ${boardId}...`);
|
||||
}
|
||||
|
||||
|
||||
// First, fix duplicate swimlanes
|
||||
const swimlaneResult = fixDuplicateSwimlanes(boardId);
|
||||
|
||||
|
||||
// Then, fix duplicate lists
|
||||
const listResult = fixDuplicateLists(boardId);
|
||||
|
||||
|
||||
return {
|
||||
boardId,
|
||||
fixedSwimlanes: swimlaneResult.fixed,
|
||||
|
|
@ -193,7 +193,7 @@ function fixDuplicateLists(boardId) {
|
|||
{ $set: { listId: keepList._id } },
|
||||
{ multi: true }
|
||||
);
|
||||
|
||||
|
||||
// Remove duplicate list
|
||||
Lists.remove(list._id);
|
||||
fixed++;
|
||||
|
|
@ -223,7 +223,7 @@ Meteor.methods({
|
|||
for (const board of allBoards) {
|
||||
const swimlanes = Swimlanes.find({ boardId: board._id }).fetch();
|
||||
const lists = Lists.find({ boardId: board._id }).fetch();
|
||||
|
||||
|
||||
// Check for duplicate swimlanes
|
||||
const swimlaneGroups = {};
|
||||
swimlanes.forEach(swimlane => {
|
||||
|
|
|
|||
|
|
@ -15,21 +15,21 @@ Meteor.methods({
|
|||
*/
|
||||
'positionHistory.trackSwimlane'(swimlaneId) {
|
||||
check(swimlaneId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in.');
|
||||
}
|
||||
|
||||
|
||||
const swimlane = Swimlanes.findOne(swimlaneId);
|
||||
if (!swimlane) {
|
||||
throw new Meteor.Error('swimlane-not-found', 'Swimlane not found');
|
||||
}
|
||||
|
||||
|
||||
const board = ReactiveCache.getBoard(swimlane.boardId);
|
||||
if (!board || !board.isVisibleBy({ _id: this.userId })) {
|
||||
throw new Meteor.Error('not-authorized', 'You do not have access to this board.');
|
||||
}
|
||||
|
||||
|
||||
return swimlane.trackOriginalPosition();
|
||||
},
|
||||
|
||||
|
|
@ -38,21 +38,21 @@ Meteor.methods({
|
|||
*/
|
||||
'positionHistory.trackList'(listId) {
|
||||
check(listId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in.');
|
||||
}
|
||||
|
||||
|
||||
const list = Lists.findOne(listId);
|
||||
if (!list) {
|
||||
throw new Meteor.Error('list-not-found', 'List not found');
|
||||
}
|
||||
|
||||
|
||||
const board = ReactiveCache.getBoard(list.boardId);
|
||||
if (!board || !board.isVisibleBy({ _id: this.userId })) {
|
||||
throw new Meteor.Error('not-authorized', 'You do not have access to this board.');
|
||||
}
|
||||
|
||||
|
||||
return list.trackOriginalPosition();
|
||||
},
|
||||
|
||||
|
|
@ -61,21 +61,21 @@ Meteor.methods({
|
|||
*/
|
||||
'positionHistory.trackCard'(cardId) {
|
||||
check(cardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in.');
|
||||
}
|
||||
|
||||
|
||||
const card = Cards.findOne(cardId);
|
||||
if (!card) {
|
||||
throw new Meteor.Error('card-not-found', 'Card not found');
|
||||
}
|
||||
|
||||
|
||||
const board = ReactiveCache.getBoard(card.boardId);
|
||||
if (!board || !board.isVisibleBy({ _id: this.userId })) {
|
||||
throw new Meteor.Error('not-authorized', 'You do not have access to this board.');
|
||||
}
|
||||
|
||||
|
||||
return card.trackOriginalPosition();
|
||||
},
|
||||
|
||||
|
|
@ -84,21 +84,21 @@ Meteor.methods({
|
|||
*/
|
||||
'positionHistory.getSwimlaneOriginalPosition'(swimlaneId) {
|
||||
check(swimlaneId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in.');
|
||||
}
|
||||
|
||||
|
||||
const swimlane = Swimlanes.findOne(swimlaneId);
|
||||
if (!swimlane) {
|
||||
throw new Meteor.Error('swimlane-not-found', 'Swimlane not found');
|
||||
}
|
||||
|
||||
|
||||
const board = ReactiveCache.getBoard(swimlane.boardId);
|
||||
if (!board || !board.isVisibleBy({ _id: this.userId })) {
|
||||
throw new Meteor.Error('not-authorized', 'You do not have access to this board.');
|
||||
}
|
||||
|
||||
|
||||
return swimlane.getOriginalPosition();
|
||||
},
|
||||
|
||||
|
|
@ -107,21 +107,21 @@ Meteor.methods({
|
|||
*/
|
||||
'positionHistory.getListOriginalPosition'(listId) {
|
||||
check(listId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in.');
|
||||
}
|
||||
|
||||
|
||||
const list = Lists.findOne(listId);
|
||||
if (!list) {
|
||||
throw new Meteor.Error('list-not-found', 'List not found');
|
||||
}
|
||||
|
||||
|
||||
const board = ReactiveCache.getBoard(list.boardId);
|
||||
if (!board || !board.isVisibleBy({ _id: this.userId })) {
|
||||
throw new Meteor.Error('not-authorized', 'You do not have access to this board.');
|
||||
}
|
||||
|
||||
|
||||
return list.getOriginalPosition();
|
||||
},
|
||||
|
||||
|
|
@ -130,21 +130,21 @@ Meteor.methods({
|
|||
*/
|
||||
'positionHistory.getCardOriginalPosition'(cardId) {
|
||||
check(cardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in.');
|
||||
}
|
||||
|
||||
|
||||
const card = Cards.findOne(cardId);
|
||||
if (!card) {
|
||||
throw new Meteor.Error('card-not-found', 'Card not found');
|
||||
}
|
||||
|
||||
|
||||
const board = ReactiveCache.getBoard(card.boardId);
|
||||
if (!board || !board.isVisibleBy({ _id: this.userId })) {
|
||||
throw new Meteor.Error('not-authorized', 'You do not have access to this board.');
|
||||
}
|
||||
|
||||
|
||||
return card.getOriginalPosition();
|
||||
},
|
||||
|
||||
|
|
@ -153,21 +153,21 @@ Meteor.methods({
|
|||
*/
|
||||
'positionHistory.hasSwimlaneMoved'(swimlaneId) {
|
||||
check(swimlaneId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in.');
|
||||
}
|
||||
|
||||
|
||||
const swimlane = Swimlanes.findOne(swimlaneId);
|
||||
if (!swimlane) {
|
||||
throw new Meteor.Error('swimlane-not-found', 'Swimlane not found');
|
||||
}
|
||||
|
||||
|
||||
const board = ReactiveCache.getBoard(swimlane.boardId);
|
||||
if (!board || !board.isVisibleBy({ _id: this.userId })) {
|
||||
throw new Meteor.Error('not-authorized', 'You do not have access to this board.');
|
||||
}
|
||||
|
||||
|
||||
return swimlane.hasMovedFromOriginalPosition();
|
||||
},
|
||||
|
||||
|
|
@ -176,21 +176,21 @@ Meteor.methods({
|
|||
*/
|
||||
'positionHistory.hasListMoved'(listId) {
|
||||
check(listId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in.');
|
||||
}
|
||||
|
||||
|
||||
const list = Lists.findOne(listId);
|
||||
if (!list) {
|
||||
throw new Meteor.Error('list-not-found', 'List not found');
|
||||
}
|
||||
|
||||
|
||||
const board = ReactiveCache.getBoard(list.boardId);
|
||||
if (!board || !board.isVisibleBy({ _id: this.userId })) {
|
||||
throw new Meteor.Error('not-authorized', 'You do not have access to this board.');
|
||||
}
|
||||
|
||||
|
||||
return list.hasMovedFromOriginalPosition();
|
||||
},
|
||||
|
||||
|
|
@ -199,21 +199,21 @@ Meteor.methods({
|
|||
*/
|
||||
'positionHistory.hasCardMoved'(cardId) {
|
||||
check(cardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in.');
|
||||
}
|
||||
|
||||
|
||||
const card = Cards.findOne(cardId);
|
||||
if (!card) {
|
||||
throw new Meteor.Error('card-not-found', 'Card not found');
|
||||
}
|
||||
|
||||
|
||||
const board = ReactiveCache.getBoard(card.boardId);
|
||||
if (!board || !board.isVisibleBy({ _id: this.userId })) {
|
||||
throw new Meteor.Error('not-authorized', 'You do not have access to this board.');
|
||||
}
|
||||
|
||||
|
||||
return card.hasMovedFromOriginalPosition();
|
||||
},
|
||||
|
||||
|
|
@ -222,21 +222,21 @@ Meteor.methods({
|
|||
*/
|
||||
'positionHistory.getSwimlaneDescription'(swimlaneId) {
|
||||
check(swimlaneId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in.');
|
||||
}
|
||||
|
||||
|
||||
const swimlane = Swimlanes.findOne(swimlaneId);
|
||||
if (!swimlane) {
|
||||
throw new Meteor.Error('swimlane-not-found', 'Swimlane not found');
|
||||
}
|
||||
|
||||
|
||||
const board = ReactiveCache.getBoard(swimlane.boardId);
|
||||
if (!board || !board.isVisibleBy({ _id: this.userId })) {
|
||||
throw new Meteor.Error('not-authorized', 'You do not have access to this board.');
|
||||
}
|
||||
|
||||
|
||||
return swimlane.getOriginalPositionDescription();
|
||||
},
|
||||
|
||||
|
|
@ -245,21 +245,21 @@ Meteor.methods({
|
|||
*/
|
||||
'positionHistory.getListDescription'(listId) {
|
||||
check(listId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in.');
|
||||
}
|
||||
|
||||
|
||||
const list = Lists.findOne(listId);
|
||||
if (!list) {
|
||||
throw new Meteor.Error('list-not-found', 'List not found');
|
||||
}
|
||||
|
||||
|
||||
const board = ReactiveCache.getBoard(list.boardId);
|
||||
if (!board || !board.isVisibleBy({ _id: this.userId })) {
|
||||
throw new Meteor.Error('not-authorized', 'You do not have access to this board.');
|
||||
}
|
||||
|
||||
|
||||
return list.getOriginalPositionDescription();
|
||||
},
|
||||
|
||||
|
|
@ -268,21 +268,21 @@ Meteor.methods({
|
|||
*/
|
||||
'positionHistory.getCardDescription'(cardId) {
|
||||
check(cardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in.');
|
||||
}
|
||||
|
||||
|
||||
const card = Cards.findOne(cardId);
|
||||
if (!card) {
|
||||
throw new Meteor.Error('card-not-found', 'Card not found');
|
||||
}
|
||||
|
||||
|
||||
const board = ReactiveCache.getBoard(card.boardId);
|
||||
if (!board || !board.isVisibleBy({ _id: this.userId })) {
|
||||
throw new Meteor.Error('not-authorized', 'You do not have access to this board.');
|
||||
}
|
||||
|
||||
|
||||
return card.getOriginalPositionDescription();
|
||||
},
|
||||
|
||||
|
|
@ -291,16 +291,16 @@ Meteor.methods({
|
|||
*/
|
||||
'positionHistory.getBoardHistory'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in.');
|
||||
}
|
||||
|
||||
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board || !board.isVisibleBy({ _id: this.userId })) {
|
||||
throw new Meteor.Error('not-authorized', 'You do not have access to this board.');
|
||||
}
|
||||
|
||||
|
||||
return PositionHistory.find({
|
||||
boardId: boardId,
|
||||
}, {
|
||||
|
|
@ -314,20 +314,20 @@ Meteor.methods({
|
|||
'positionHistory.getBoardHistoryByType'(boardId, entityType) {
|
||||
check(boardId, String);
|
||||
check(entityType, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in.');
|
||||
}
|
||||
|
||||
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board || !board.isVisibleBy({ _id: this.userId })) {
|
||||
throw new Meteor.Error('not-authorized', 'You do not have access to this board.');
|
||||
}
|
||||
|
||||
|
||||
if (!['swimlane', 'list', 'card'].includes(entityType)) {
|
||||
throw new Meteor.Error('invalid-entity-type', 'Entity type must be swimlane, list, or card');
|
||||
}
|
||||
|
||||
|
||||
return PositionHistory.find({
|
||||
boardId: boardId,
|
||||
entityType: entityType,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
/**
|
||||
* Comprehensive Board Migration System
|
||||
*
|
||||
*
|
||||
* This migration handles all database structure changes from previous Wekan versions
|
||||
* to the current per-swimlane lists structure. It ensures:
|
||||
*
|
||||
*
|
||||
* 1. All cards are visible with proper swimlaneId and listId
|
||||
* 2. Lists are per-swimlane (no shared lists across swimlanes)
|
||||
* 3. No empty lists are created
|
||||
* 4. Handles various database structure versions from git history
|
||||
*
|
||||
*
|
||||
* Supported versions and their database structures:
|
||||
* - v7.94 and earlier: Shared lists across all swimlanes
|
||||
* - v8.00-v8.02: Transition period with mixed structures
|
||||
|
|
@ -66,7 +66,7 @@ class ComprehensiveBoardMigration {
|
|||
*/
|
||||
detectMigrationIssues(boardId) {
|
||||
const issues = [];
|
||||
|
||||
|
||||
try {
|
||||
const cards = ReactiveCache.getCards({ boardId });
|
||||
const lists = ReactiveCache.getLists({ boardId });
|
||||
|
|
@ -178,7 +178,7 @@ class ComprehensiveBoardMigration {
|
|||
const updateProgress = (stepName, stepProgress, stepStatus, stepDetails = null) => {
|
||||
currentStep++;
|
||||
const overallProgress = Math.round((currentStep / totalSteps) * 100);
|
||||
|
||||
|
||||
const progressData = {
|
||||
overallProgress,
|
||||
currentStep: currentStep,
|
||||
|
|
@ -206,7 +206,7 @@ class ComprehensiveBoardMigration {
|
|||
issuesFound: results.steps.analyze.issueCount,
|
||||
needsMigration: results.steps.analyze.needsMigration
|
||||
});
|
||||
|
||||
|
||||
// Step 2: Fix orphaned cards
|
||||
updateProgress('fix_orphaned_cards', 0, 'Fixing orphaned cards...');
|
||||
results.steps.fixOrphanedCards = await this.fixOrphanedCards(boardId, (progress, status) => {
|
||||
|
|
@ -323,7 +323,7 @@ class ComprehensiveBoardMigration {
|
|||
if (!card.listId) {
|
||||
// Find or create a default list for this swimlane
|
||||
const swimlaneId = updates.swimlaneId || card.swimlaneId;
|
||||
let defaultList = lists.find(list =>
|
||||
let defaultList = lists.find(list =>
|
||||
list.swimlaneId === swimlaneId && list.title === 'Default'
|
||||
);
|
||||
|
||||
|
|
@ -426,7 +426,7 @@ class ComprehensiveBoardMigration {
|
|||
|
||||
// Check if we already have a list with the same title in this swimlane
|
||||
let targetList = existingLists.find(list => list.title === originalList.title);
|
||||
|
||||
|
||||
if (!targetList) {
|
||||
// Create a new list for this swimlane
|
||||
const newListData = {
|
||||
|
|
@ -508,12 +508,12 @@ class ComprehensiveBoardMigration {
|
|||
|
||||
for (const list of lists) {
|
||||
const listCards = cards.filter(card => card.listId === list._id);
|
||||
|
||||
|
||||
if (listCards.length === 0) {
|
||||
// Remove empty list
|
||||
Lists.remove(list._id);
|
||||
listsRemoved++;
|
||||
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Removed empty list: ${list.title} (${list._id})`);
|
||||
}
|
||||
|
|
@ -563,7 +563,7 @@ class ComprehensiveBoardMigration {
|
|||
const avatarUrl = user.profile.avatarUrl;
|
||||
let needsUpdate = false;
|
||||
let cleanUrl = avatarUrl;
|
||||
|
||||
|
||||
// Check if URL has problematic parameters
|
||||
if (avatarUrl.includes('auth=false') || avatarUrl.includes('brokenIsFine=true')) {
|
||||
// Remove problematic parameters
|
||||
|
|
@ -573,13 +573,13 @@ class ComprehensiveBoardMigration {
|
|||
cleanUrl = cleanUrl.replace(/\?$/g, '');
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
|
||||
// Check if URL is using old CollectionFS format
|
||||
if (avatarUrl.includes('/cfs/files/avatars/')) {
|
||||
cleanUrl = cleanUrl.replace('/cfs/files/avatars/', '/cdn/storage/avatars/');
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
|
||||
// Check if URL is missing the /cdn/storage/avatars/ prefix
|
||||
if (avatarUrl.includes('avatars/') && !avatarUrl.includes('/cdn/storage/avatars/') && !avatarUrl.includes('/cfs/files/avatars/')) {
|
||||
// This might be a relative URL, make it absolute
|
||||
|
|
@ -588,7 +588,7 @@ class ComprehensiveBoardMigration {
|
|||
needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (needsUpdate) {
|
||||
// Update user's avatar URL
|
||||
Users.update(user._id, {
|
||||
|
|
@ -597,7 +597,7 @@ class ComprehensiveBoardMigration {
|
|||
modifiedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
avatarsFixed++;
|
||||
}
|
||||
}
|
||||
|
|
@ -619,7 +619,7 @@ class ComprehensiveBoardMigration {
|
|||
const attachmentUrl = attachment.url;
|
||||
let needsUpdate = false;
|
||||
let cleanUrl = attachmentUrl;
|
||||
|
||||
|
||||
// Check if URL has problematic parameters
|
||||
if (attachmentUrl.includes('auth=false') || attachmentUrl.includes('brokenIsFine=true')) {
|
||||
// Remove problematic parameters
|
||||
|
|
@ -629,26 +629,26 @@ class ComprehensiveBoardMigration {
|
|||
cleanUrl = cleanUrl.replace(/\?$/g, '');
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
|
||||
// Check if URL is using old CollectionFS format
|
||||
if (attachmentUrl.includes('/cfs/files/attachments/')) {
|
||||
cleanUrl = cleanUrl.replace('/cfs/files/attachments/', '/cdn/storage/attachments/');
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
|
||||
// Check if URL has /original/ path that should be removed
|
||||
if (attachmentUrl.includes('/original/')) {
|
||||
cleanUrl = cleanUrl.replace(/\/original\/[^\/\?#]+/, '');
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
|
||||
// If we have a file ID, generate a universal URL
|
||||
const fileId = attachment._id;
|
||||
if (fileId && !isUniversalFileUrl(cleanUrl, 'attachment')) {
|
||||
cleanUrl = generateUniversalAttachmentUrl(fileId);
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
|
||||
if (needsUpdate) {
|
||||
// Update attachment URL
|
||||
Attachments.update(attachment._id, {
|
||||
|
|
@ -657,7 +657,7 @@ class ComprehensiveBoardMigration {
|
|||
modifiedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
attachmentsFixed++;
|
||||
}
|
||||
}
|
||||
|
|
@ -677,7 +677,7 @@ class ComprehensiveBoardMigration {
|
|||
}
|
||||
|
||||
if (board.comprehensiveMigrationCompleted) {
|
||||
return {
|
||||
return {
|
||||
status: 'completed',
|
||||
completedAt: board.comprehensiveMigrationCompletedAt,
|
||||
results: board.comprehensiveMigrationResults
|
||||
|
|
@ -686,7 +686,7 @@ class ComprehensiveBoardMigration {
|
|||
|
||||
const needsMigration = this.needsMigration(boardId);
|
||||
const issues = this.detectMigrationIssues(boardId);
|
||||
|
||||
|
||||
return {
|
||||
status: needsMigration ? 'needed' : 'not_needed',
|
||||
issues,
|
||||
|
|
@ -707,54 +707,54 @@ export const comprehensiveBoardMigration = new ComprehensiveBoardMigration();
|
|||
Meteor.methods({
|
||||
'comprehensiveBoardMigration.check'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
return comprehensiveBoardMigration.getMigrationStatus(boardId);
|
||||
},
|
||||
|
||||
'comprehensiveBoardMigration.execute'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
const user = ReactiveCache.getUser(this.userId);
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) {
|
||||
throw new Meteor.Error('board-not-found');
|
||||
}
|
||||
|
||||
|
||||
const isBoardAdmin = board.hasAdmin(this.userId);
|
||||
const isInstanceAdmin = user && user.isAdmin;
|
||||
|
||||
|
||||
if (!isBoardAdmin && !isInstanceAdmin) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be a board admin or instance admin to perform this action.');
|
||||
}
|
||||
|
||||
|
||||
return comprehensiveBoardMigration.executeMigration(boardId);
|
||||
},
|
||||
|
||||
'comprehensiveBoardMigration.needsMigration'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
return comprehensiveBoardMigration.needsMigration(boardId);
|
||||
},
|
||||
|
||||
'comprehensiveBoardMigration.detectIssues'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
return comprehensiveBoardMigration.detectMigrationIssues(boardId);
|
||||
},
|
||||
|
||||
|
|
@ -762,12 +762,12 @@ Meteor.methods({
|
|||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
const user = ReactiveCache.getUser(this.userId);
|
||||
if (!user || !user.isAdmin) {
|
||||
throw new Meteor.Error('not-authorized', 'Only instance admins can perform this action.');
|
||||
}
|
||||
|
||||
|
||||
return comprehensiveBoardMigration.fixAvatarUrls();
|
||||
},
|
||||
|
||||
|
|
@ -775,12 +775,12 @@ Meteor.methods({
|
|||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
const user = ReactiveCache.getUser(this.userId);
|
||||
if (!user || !user.isAdmin) {
|
||||
throw new Meteor.Error('not-authorized', 'Only instance admins can perform this action.');
|
||||
}
|
||||
|
||||
|
||||
return comprehensiveBoardMigration.fixAttachmentUrls();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* Delete Duplicate Empty Lists Migration
|
||||
*
|
||||
*
|
||||
* Safely deletes empty duplicate lists from a board:
|
||||
* 1. First converts any shared lists to per-swimlane lists
|
||||
* 2. Only deletes per-swimlane lists that:
|
||||
|
|
@ -42,9 +42,9 @@ class DeleteDuplicateEmptyListsMigration {
|
|||
const listCards = cards.filter(card => card.listId === list._id);
|
||||
if (listCards.length === 0) {
|
||||
// Check if there's a duplicate list with the same title that has cards
|
||||
const duplicateListsWithSameTitle = lists.filter(l =>
|
||||
l._id !== list._id &&
|
||||
l.title === list.title &&
|
||||
const duplicateListsWithSameTitle = lists.filter(l =>
|
||||
l._id !== list._id &&
|
||||
l.title === list.title &&
|
||||
l.boardId === boardId
|
||||
);
|
||||
|
||||
|
|
@ -107,7 +107,7 @@ class DeleteDuplicateEmptyListsMigration {
|
|||
const lists = ReactiveCache.getLists({ boardId });
|
||||
const swimlanes = ReactiveCache.getSwimlanes({ boardId, archived: false });
|
||||
const cards = ReactiveCache.getCards({ boardId });
|
||||
|
||||
|
||||
let listsConverted = 0;
|
||||
|
||||
// Find shared lists (lists without swimlaneId)
|
||||
|
|
@ -137,8 +137,8 @@ class DeleteDuplicateEmptyListsMigration {
|
|||
|
||||
if (swimlaneCards.length > 0) {
|
||||
// Check if per-swimlane list already exists
|
||||
const existingList = lists.find(l =>
|
||||
l.title === sharedList.title &&
|
||||
const existingList = lists.find(l =>
|
||||
l.title === sharedList.title &&
|
||||
l.swimlaneId === swimlane._id &&
|
||||
l._id !== sharedList._id
|
||||
);
|
||||
|
|
@ -208,7 +208,7 @@ class DeleteDuplicateEmptyListsMigration {
|
|||
async deleteEmptyPerSwimlaneLists(boardId) {
|
||||
const lists = ReactiveCache.getLists({ boardId });
|
||||
const cards = ReactiveCache.getCards({ boardId });
|
||||
|
||||
|
||||
let listsDeleted = 0;
|
||||
|
||||
for (const list of lists) {
|
||||
|
|
@ -230,9 +230,9 @@ class DeleteDuplicateEmptyListsMigration {
|
|||
}
|
||||
|
||||
// Safety check 3: There must be another list with the same title on the same board that has cards
|
||||
const duplicateListsWithSameTitle = lists.filter(l =>
|
||||
l._id !== list._id &&
|
||||
l.title === list.title &&
|
||||
const duplicateListsWithSameTitle = lists.filter(l =>
|
||||
l._id !== list._id &&
|
||||
l.title === list.title &&
|
||||
l.boardId === boardId
|
||||
);
|
||||
|
||||
|
|
@ -321,7 +321,7 @@ const deleteDuplicateEmptyListsMigration = new DeleteDuplicateEmptyListsMigratio
|
|||
Meteor.methods({
|
||||
'deleteDuplicateEmptyLists.needsMigration'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
||||
}
|
||||
|
|
@ -331,7 +331,7 @@ Meteor.methods({
|
|||
|
||||
'deleteDuplicateEmptyLists.execute'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
||||
}
|
||||
|
|
@ -361,7 +361,7 @@ Meteor.methods({
|
|||
|
||||
'deleteDuplicateEmptyLists.getStatus'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
/**
|
||||
* Migration: Ensure all entities have valid swimlaneId
|
||||
*
|
||||
*
|
||||
* This migration ensures that:
|
||||
* 1. All cards have a valid swimlaneId
|
||||
* 2. All lists have a valid swimlaneId (if applicable)
|
||||
* 3. Orphaned entities (without valid swimlaneId) are moved to a "Rescued Data" swimlane
|
||||
*
|
||||
*
|
||||
* This is similar to the existing rescue migration but specifically for swimlaneId validation
|
||||
*/
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ function getOrCreateRescuedSwimlane(boardId) {
|
|||
});
|
||||
|
||||
rescuedSwimlane = Swimlanes.findOne(swimlaneId);
|
||||
|
||||
|
||||
Activities.insert({
|
||||
userId: 'migration',
|
||||
type: 'swimlane',
|
||||
|
|
@ -164,7 +164,7 @@ function getOrCreateRescuedSwimlane(boardId) {
|
|||
let rescuedCount = 0;
|
||||
|
||||
const allCards = Cards.find({}).fetch();
|
||||
|
||||
|
||||
allCards.forEach(card => {
|
||||
if (!card.swimlaneId) return; // Handled by fixCardsWithoutSwimlaneId
|
||||
|
||||
|
|
@ -173,7 +173,7 @@ function getOrCreateRescuedSwimlane(boardId) {
|
|||
if (!swimlane) {
|
||||
// Orphaned card - swimlane doesn't exist
|
||||
const rescuedSwimlane = getOrCreateRescuedSwimlane(card.boardId);
|
||||
|
||||
|
||||
if (rescuedSwimlane) {
|
||||
Cards.update(card._id, {
|
||||
$set: { swimlaneId: rescuedSwimlane._id },
|
||||
|
|
@ -290,7 +290,7 @@ function getOrCreateRescuedSwimlane(boardId) {
|
|||
);
|
||||
|
||||
console.log(`Migration ${MIGRATION_NAME} completed successfully`);
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
cardsFixed: cardResults.fixedCount,
|
||||
|
|
@ -306,7 +306,7 @@ function getOrCreateRescuedSwimlane(boardId) {
|
|||
// Install validation hooks on startup (always run these for data integrity)
|
||||
Meteor.startup(() => {
|
||||
if (!Meteor.isServer) return;
|
||||
|
||||
|
||||
try {
|
||||
addSwimlaneIdValidationHooks();
|
||||
console.log('SwimlaneId validation hooks installed');
|
||||
|
|
|
|||
|
|
@ -28,9 +28,9 @@ class FixAllFileUrlsMigration {
|
|||
if (!board || !board.members) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
const memberIds = board.members.map(m => m.userId);
|
||||
|
||||
|
||||
// Check for problematic avatar URLs for board members
|
||||
const users = ReactiveCache.getUsers({ _id: { $in: memberIds } });
|
||||
for (const user of users) {
|
||||
|
|
@ -46,7 +46,7 @@ class FixAllFileUrlsMigration {
|
|||
const cards = ReactiveCache.getCards({ boardId });
|
||||
const cardIds = cards.map(c => c._id);
|
||||
const attachments = ReactiveCache.getAttachments({ cardId: { $in: cardIds } });
|
||||
|
||||
|
||||
for (const attachment of attachments) {
|
||||
if (attachment.url && this.hasProblematicUrl(attachment.url)) {
|
||||
return true;
|
||||
|
|
@ -61,17 +61,17 @@ class FixAllFileUrlsMigration {
|
|||
*/
|
||||
hasProblematicUrl(url) {
|
||||
if (!url) return false;
|
||||
|
||||
|
||||
// Check for auth parameters
|
||||
if (url.includes('auth=false') || url.includes('brokenIsFine=true')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Check for absolute URLs with domains
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// Check for ROOT_URL dependencies
|
||||
if (Meteor.isServer && process.env.ROOT_URL) {
|
||||
try {
|
||||
|
|
@ -83,12 +83,12 @@ class FixAllFileUrlsMigration {
|
|||
// Ignore URL parsing errors
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check for non-universal file URLs
|
||||
if (url.includes('/cfs/files/') && !isUniversalFileUrl(url, 'attachment') && !isUniversalFileUrl(url, 'avatar')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -120,7 +120,7 @@ class FixAllFileUrlsMigration {
|
|||
}
|
||||
|
||||
console.log(`Universal file URL migration completed for board ${boardId}. Fixed ${filesFixed} file URLs.`);
|
||||
|
||||
|
||||
return {
|
||||
success: errors.length === 0,
|
||||
filesFixed,
|
||||
|
|
@ -137,7 +137,7 @@ class FixAllFileUrlsMigration {
|
|||
if (!board || !board.members) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
const memberIds = board.members.map(m => m.userId);
|
||||
const users = ReactiveCache.getUsers({ _id: { $in: memberIds } });
|
||||
let avatarsFixed = 0;
|
||||
|
|
@ -145,12 +145,12 @@ class FixAllFileUrlsMigration {
|
|||
for (const user of users) {
|
||||
if (user.profile && user.profile.avatarUrl) {
|
||||
const avatarUrl = user.profile.avatarUrl;
|
||||
|
||||
|
||||
if (this.hasProblematicUrl(avatarUrl)) {
|
||||
try {
|
||||
// Extract file ID from URL
|
||||
const fileId = extractFileIdFromUrl(avatarUrl, 'avatar');
|
||||
|
||||
|
||||
let cleanUrl;
|
||||
if (fileId) {
|
||||
// Generate universal URL
|
||||
|
|
@ -159,7 +159,7 @@ class FixAllFileUrlsMigration {
|
|||
// Clean existing URL
|
||||
cleanUrl = cleanFileUrl(avatarUrl, 'avatar');
|
||||
}
|
||||
|
||||
|
||||
if (cleanUrl && cleanUrl !== avatarUrl) {
|
||||
// Update user's avatar URL
|
||||
Users.update(user._id, {
|
||||
|
|
@ -168,9 +168,9 @@ class FixAllFileUrlsMigration {
|
|||
modifiedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
avatarsFixed++;
|
||||
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Fixed avatar URL for user ${user.username}: ${avatarUrl} -> ${cleanUrl}`);
|
||||
}
|
||||
|
|
@ -200,7 +200,7 @@ class FixAllFileUrlsMigration {
|
|||
try {
|
||||
const fileId = attachment._id;
|
||||
const cleanUrl = generateUniversalAttachmentUrl(fileId);
|
||||
|
||||
|
||||
if (cleanUrl && cleanUrl !== attachment.url) {
|
||||
// Update attachment URL
|
||||
Attachments.update(attachment._id, {
|
||||
|
|
@ -209,9 +209,9 @@ class FixAllFileUrlsMigration {
|
|||
modifiedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
attachmentsFixed++;
|
||||
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Fixed attachment URL: ${attachment.url} -> ${cleanUrl}`);
|
||||
}
|
||||
|
|
@ -239,7 +239,7 @@ class FixAllFileUrlsMigration {
|
|||
try {
|
||||
const fileId = attachment._id || extractFileIdFromUrl(attachment.url, 'attachment');
|
||||
const cleanUrl = fileId ? generateUniversalAttachmentUrl(fileId) : cleanFileUrl(attachment.url, 'attachment');
|
||||
|
||||
|
||||
if (cleanUrl && cleanUrl !== attachment.url) {
|
||||
// Update attachment with fixed URL
|
||||
Attachments.update(attachment._id, {
|
||||
|
|
@ -248,9 +248,9 @@ class FixAllFileUrlsMigration {
|
|||
modifiedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
attachmentsFixed++;
|
||||
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Fixed attachment URL ${attachment._id}`);
|
||||
}
|
||||
|
|
@ -272,7 +272,7 @@ export const fixAllFileUrlsMigration = new FixAllFileUrlsMigration();
|
|||
Meteor.methods({
|
||||
'fixAllFileUrls.execute'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
||||
}
|
||||
|
|
@ -296,17 +296,17 @@ Meteor.methods({
|
|||
if (!isBoardAdmin && !user.isAdmin) {
|
||||
throw new Meteor.Error('not-authorized', 'Only board administrators can run migrations');
|
||||
}
|
||||
|
||||
|
||||
return fixAllFileUrlsMigration.execute(boardId);
|
||||
},
|
||||
|
||||
'fixAllFileUrls.needsMigration'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
||||
}
|
||||
|
||||
|
||||
return fixAllFileUrlsMigration.needsMigration(boardId);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@ class FixAvatarUrlsMigration {
|
|||
if (!board || !board.members) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
const memberIds = board.members.map(m => m.userId);
|
||||
const users = ReactiveCache.getUsers({ _id: { $in: memberIds } });
|
||||
|
||||
|
||||
for (const user of users) {
|
||||
if (user.profile && user.profile.avatarUrl) {
|
||||
const avatarUrl = user.profile.avatarUrl;
|
||||
|
|
@ -37,7 +37,7 @@ class FixAvatarUrlsMigration {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ class FixAvatarUrlsMigration {
|
|||
error: 'Board not found or has no members'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const memberIds = board.members.map(m => m.userId);
|
||||
const users = ReactiveCache.getUsers({ _id: { $in: memberIds } });
|
||||
let avatarsFixed = 0;
|
||||
|
|
@ -65,7 +65,7 @@ class FixAvatarUrlsMigration {
|
|||
const avatarUrl = user.profile.avatarUrl;
|
||||
let needsUpdate = false;
|
||||
let cleanUrl = avatarUrl;
|
||||
|
||||
|
||||
// Check if URL has problematic parameters
|
||||
if (avatarUrl.includes('auth=false') || avatarUrl.includes('brokenIsFine=true')) {
|
||||
// Remove problematic parameters
|
||||
|
|
@ -75,13 +75,13 @@ class FixAvatarUrlsMigration {
|
|||
cleanUrl = cleanUrl.replace(/\?$/g, '');
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
|
||||
// Check if URL is using old CollectionFS format
|
||||
if (avatarUrl.includes('/cfs/files/avatars/')) {
|
||||
cleanUrl = cleanUrl.replace('/cfs/files/avatars/', '/cdn/storage/avatars/');
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
|
||||
// Check if URL is missing the /cdn/storage/avatars/ prefix
|
||||
if (avatarUrl.includes('avatars/') && !avatarUrl.includes('/cdn/storage/avatars/') && !avatarUrl.includes('/cfs/files/avatars/')) {
|
||||
// This might be a relative URL, make it absolute
|
||||
|
|
@ -90,14 +90,14 @@ class FixAvatarUrlsMigration {
|
|||
needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If we have a file ID, generate a universal URL
|
||||
const fileId = extractFileIdFromUrl(avatarUrl, 'avatar');
|
||||
if (fileId && !isUniversalFileUrl(cleanUrl, 'avatar')) {
|
||||
cleanUrl = generateUniversalAvatarUrl(fileId);
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
|
||||
if (needsUpdate) {
|
||||
// Update user's avatar URL
|
||||
Users.update(user._id, {
|
||||
|
|
@ -106,9 +106,9 @@ class FixAvatarUrlsMigration {
|
|||
modifiedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
avatarsFixed++;
|
||||
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Fixed avatar URL for user ${user.username}: ${avatarUrl} -> ${cleanUrl}`);
|
||||
}
|
||||
|
|
@ -117,7 +117,7 @@ class FixAvatarUrlsMigration {
|
|||
}
|
||||
|
||||
console.log(`Avatar URL fix migration completed for board ${boardId}. Fixed ${avatarsFixed} avatar URLs.`);
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
avatarsFixed,
|
||||
|
|
@ -133,7 +133,7 @@ export const fixAvatarUrlsMigration = new FixAvatarUrlsMigration();
|
|||
Meteor.methods({
|
||||
'fixAvatarUrls.execute'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
||||
}
|
||||
|
|
@ -157,17 +157,17 @@ Meteor.methods({
|
|||
if (!isBoardAdmin && !user.isAdmin) {
|
||||
throw new Meteor.Error('not-authorized', 'Only board administrators can run migrations');
|
||||
}
|
||||
|
||||
|
||||
return fixAvatarUrlsMigration.execute(boardId);
|
||||
},
|
||||
|
||||
'fixAvatarUrls.needsMigration'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
||||
}
|
||||
|
||||
|
||||
return fixAvatarUrlsMigration.needsMigration(boardId);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
/**
|
||||
* Fix Missing Lists Migration
|
||||
*
|
||||
*
|
||||
* This migration fixes the issue where cards have incorrect listId references
|
||||
* due to the per-swimlane lists change. It detects cards with mismatched
|
||||
* listId/swimlaneId and creates the missing lists.
|
||||
*
|
||||
*
|
||||
* Issue: When upgrading from v7.94 to v8.02, cards that were in different
|
||||
* swimlanes but shared the same list now have wrong listId references.
|
||||
*
|
||||
*
|
||||
* Example:
|
||||
* - Card1: listId: 'HB93dWNnY5bgYdtxc', swimlaneId: 'sK69SseWkh3tMbJvg'
|
||||
* - Card2: listId: 'HB93dWNnY5bgYdtxc', swimlaneId: 'XeecF9nZxGph4zcT4'
|
||||
*
|
||||
*
|
||||
* Card2 should have a different listId that corresponds to its swimlane.
|
||||
*/
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ class FixMissingListsMigration {
|
|||
// Check if there are cards with mismatched listId/swimlaneId
|
||||
const cards = ReactiveCache.getCards({ boardId });
|
||||
const lists = ReactiveCache.getLists({ boardId });
|
||||
|
||||
|
||||
// Create a map of listId -> swimlaneId for existing lists
|
||||
const listSwimlaneMap = new Map();
|
||||
lists.forEach(list => {
|
||||
|
|
@ -77,7 +77,7 @@ class FixMissingListsMigration {
|
|||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Starting fix missing lists migration for board ${boardId}`);
|
||||
}
|
||||
|
||||
|
||||
const board = ReactiveCache.getBoard(boardId);
|
||||
if (!board) {
|
||||
throw new Error(`Board ${boardId} not found`);
|
||||
|
|
@ -90,7 +90,7 @@ class FixMissingListsMigration {
|
|||
// Create maps for efficient lookup
|
||||
const listSwimlaneMap = new Map();
|
||||
const swimlaneListsMap = new Map();
|
||||
|
||||
|
||||
lists.forEach(list => {
|
||||
listSwimlaneMap.set(list._id, list.swimlaneId || '');
|
||||
if (!swimlaneListsMap.has(list.swimlaneId || '')) {
|
||||
|
|
@ -142,7 +142,7 @@ class FixMissingListsMigration {
|
|||
|
||||
// Check if we already have a list with the same title in this swimlane
|
||||
let targetList = existingLists.find(list => list.title === originalList.title);
|
||||
|
||||
|
||||
if (!targetList) {
|
||||
// Create a new list for this swimlane
|
||||
const newListData = {
|
||||
|
|
@ -168,7 +168,7 @@ class FixMissingListsMigration {
|
|||
const newListId = Lists.insert(newListData);
|
||||
targetList = { _id: newListId, ...newListData };
|
||||
createdLists++;
|
||||
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Created new list "${originalList.title}" for swimlane ${swimlaneId}`);
|
||||
}
|
||||
|
|
@ -198,7 +198,7 @@ class FixMissingListsMigration {
|
|||
if (process.env.DEBUG === 'true') {
|
||||
console.log(`Fix missing lists migration completed for board ${boardId}: created ${createdLists} lists, updated ${updatedCards} cards`);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
success: true,
|
||||
createdLists,
|
||||
|
|
@ -222,7 +222,7 @@ class FixMissingListsMigration {
|
|||
}
|
||||
|
||||
if (board.fixMissingListsCompleted) {
|
||||
return {
|
||||
return {
|
||||
status: 'completed',
|
||||
completedAt: board.fixMissingListsCompletedAt
|
||||
};
|
||||
|
|
@ -247,31 +247,31 @@ export const fixMissingListsMigration = new FixMissingListsMigration();
|
|||
Meteor.methods({
|
||||
'fixMissingListsMigration.check'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
return fixMissingListsMigration.getMigrationStatus(boardId);
|
||||
},
|
||||
|
||||
'fixMissingListsMigration.execute'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
return fixMissingListsMigration.executeMigration(boardId);
|
||||
},
|
||||
|
||||
'fixMissingListsMigration.needsMigration'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized');
|
||||
}
|
||||
|
||||
|
||||
return fixMissingListsMigration.needsMigration(boardId);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
/**
|
||||
* Restore All Archived Migration
|
||||
*
|
||||
*
|
||||
* Restores all archived swimlanes, lists, and cards.
|
||||
* If any restored items are missing swimlaneId, listId, or cardId,
|
||||
* If any restored items are missing swimlaneId, listId, or cardId,
|
||||
* creates/assigns proper IDs to make them visible.
|
||||
*/
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ class RestoreAllArchivedMigration {
|
|||
if (!list.swimlaneId) {
|
||||
// Try to find a suitable swimlane or use default
|
||||
let targetSwimlane = activeSwimlanes.find(s => !s.archived);
|
||||
|
||||
|
||||
if (!targetSwimlane) {
|
||||
// No active swimlane found, create default
|
||||
const swimlaneId = Swimlanes.insert({
|
||||
|
|
@ -139,11 +139,11 @@ class RestoreAllArchivedMigration {
|
|||
if (!card.listId) {
|
||||
// Find or create a default list
|
||||
let targetList = allLists.find(l => !l.archived);
|
||||
|
||||
|
||||
if (!targetList) {
|
||||
// No active list found, create one
|
||||
const defaultSwimlane = allSwimlanes.find(s => !s.archived) || allSwimlanes[0];
|
||||
|
||||
|
||||
const listId = Lists.insert({
|
||||
title: TAPi18n.__('default'),
|
||||
boardId: boardId,
|
||||
|
|
@ -224,7 +224,7 @@ const restoreAllArchivedMigration = new RestoreAllArchivedMigration();
|
|||
Meteor.methods({
|
||||
'restoreAllArchived.needsMigration'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
||||
}
|
||||
|
|
@ -234,7 +234,7 @@ Meteor.methods({
|
|||
|
||||
'restoreAllArchived.execute'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* Restore Lost Cards Migration
|
||||
*
|
||||
*
|
||||
* Finds and restores cards and lists that have missing swimlaneId, listId, or are orphaned.
|
||||
* Creates a "Lost Cards" swimlane and restores visibility of lost items.
|
||||
* Only processes non-archived items.
|
||||
|
|
@ -217,7 +217,7 @@ const restoreLostCardsMigration = new RestoreLostCardsMigration();
|
|||
Meteor.methods({
|
||||
'restoreLostCards.needsMigration'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
||||
}
|
||||
|
|
@ -227,7 +227,7 @@ Meteor.methods({
|
|||
|
||||
'restoreLostCards.execute'(boardId) {
|
||||
check(boardId, String);
|
||||
|
||||
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'You must be logged in');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { meteorMongoIntegration } from '/models/lib/meteorMongoIntegration';
|
|||
|
||||
/**
|
||||
* MongoDB Driver Startup
|
||||
*
|
||||
*
|
||||
* This module initializes the MongoDB driver system on server startup,
|
||||
* providing automatic version detection and driver selection for
|
||||
* MongoDB versions 3.0 through 8.0.
|
||||
|
|
@ -14,7 +14,7 @@ import { meteorMongoIntegration } from '/models/lib/meteorMongoIntegration';
|
|||
// Initialize MongoDB driver system on server startup
|
||||
Meteor.startup(async function() {
|
||||
// MongoDB Driver System Startup (status available in Admin Panel)
|
||||
|
||||
|
||||
try {
|
||||
// Check if MONGO_URL is available
|
||||
const mongoUrl = process.env.MONGO_URL;
|
||||
|
|
@ -31,7 +31,7 @@ Meteor.startup(async function() {
|
|||
|
||||
// Test the connection
|
||||
const testResult = await meteorMongoIntegration.testConnection();
|
||||
|
||||
|
||||
if (testResult.success) {
|
||||
// MongoDB connection test successful
|
||||
// Driver and version information available in Admin Panel
|
||||
|
|
@ -51,7 +51,7 @@ Meteor.startup(async function() {
|
|||
} catch (error) {
|
||||
console.error('Error during MongoDB driver system startup:', error.message);
|
||||
console.error('Stack trace:', error.stack);
|
||||
|
||||
|
||||
// Don't fail the entire startup, just log the error
|
||||
console.log('Continuing with default MongoDB connection...');
|
||||
}
|
||||
|
|
@ -65,7 +65,7 @@ if (Meteor.isServer) {
|
|||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'Must be logged in');
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
connectionStats: mongodbConnectionManager.getConnectionStats(),
|
||||
driverStats: mongodbDriverManager.getConnectionStats(),
|
||||
|
|
@ -77,7 +77,7 @@ if (Meteor.isServer) {
|
|||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'Must be logged in');
|
||||
}
|
||||
|
||||
|
||||
return await meteorMongoIntegration.testConnection();
|
||||
},
|
||||
|
||||
|
|
@ -85,7 +85,7 @@ if (Meteor.isServer) {
|
|||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'Must be logged in');
|
||||
}
|
||||
|
||||
|
||||
meteorMongoIntegration.reset();
|
||||
return { success: true, message: 'MongoDB driver system reset' };
|
||||
},
|
||||
|
|
@ -94,7 +94,7 @@ if (Meteor.isServer) {
|
|||
if (!this.userId) {
|
||||
throw new Meteor.Error('not-authorized', 'Must be logged in');
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
supportedVersions: mongodbDriverManager.getSupportedVersions(),
|
||||
compatibility: mongodbDriverManager.getSupportedVersions().map(version => {
|
||||
|
|
@ -120,11 +120,11 @@ if (Meteor.isServer) {
|
|||
}
|
||||
|
||||
const self = this;
|
||||
|
||||
|
||||
// Send initial data
|
||||
const stats = meteorMongoIntegration.getStats();
|
||||
self.added('mongodbDriverMonitor', 'stats', stats);
|
||||
|
||||
|
||||
// Update every 30 seconds
|
||||
const interval = setInterval(() => {
|
||||
const updatedStats = meteorMongoIntegration.getStats();
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ Notifications = {
|
|||
notify: (user, title, description, params) => {
|
||||
// Skip if user is invalid
|
||||
if (!user || !user._id) return;
|
||||
|
||||
|
||||
for (const k in notifyServices) {
|
||||
const notifyImpl = notifyServices[k];
|
||||
if (notifyImpl && typeof notifyImpl === 'function')
|
||||
|
|
|
|||
43
server/publications/attachmentMigrationStatus.js
Normal file
43
server/publications/attachmentMigrationStatus.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { AttachmentMigrationStatus } from '../attachmentMigrationStatus';
|
||||
|
||||
// Publish attachment migration status for boards user has access to
|
||||
Meteor.publish('attachmentMigrationStatus', function(boardId) {
|
||||
if (!this.userId) {
|
||||
return this.ready();
|
||||
}
|
||||
|
||||
check(boardId, String);
|
||||
|
||||
const board = Boards.findOne(boardId);
|
||||
if (!board || !board.isVisibleBy({ _id: this.userId })) {
|
||||
return this.ready();
|
||||
}
|
||||
|
||||
// Publish migration status for this board
|
||||
return AttachmentMigrationStatus.find({ boardId });
|
||||
});
|
||||
|
||||
// Publish all attachment migration statuses for user's boards
|
||||
Meteor.publish('attachmentMigrationStatuses', function() {
|
||||
if (!this.userId) {
|
||||
return this.ready();
|
||||
}
|
||||
|
||||
const user = Users.findOne(this.userId);
|
||||
if (!user) {
|
||||
return this.ready();
|
||||
}
|
||||
|
||||
// Get all boards user has access to
|
||||
const boards = Boards.find({
|
||||
$or: [
|
||||
{ 'members.userId': this.userId },
|
||||
{ isPublic: true }
|
||||
]
|
||||
}, { fields: { _id: 1 } }).fetch();
|
||||
|
||||
const boardIds = boards.map(b => b._id);
|
||||
|
||||
// Publish migration status for all user's boards
|
||||
return AttachmentMigrationStatus.find({ boardId: { $in: boardIds } });
|
||||
});
|
||||
|
|
@ -2,25 +2,25 @@ import { ReactiveCache } from '/imports/reactiveCache';
|
|||
import { publishComposite } from 'meteor/reywood:publish-composite';
|
||||
import escapeForRegex from 'escape-string-regexp';
|
||||
import Users from '../../models/users';
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
import {
|
||||
formatDateTime,
|
||||
formatDate,
|
||||
formatTime,
|
||||
getISOWeek,
|
||||
isValidDate,
|
||||
isBefore,
|
||||
isAfter,
|
||||
isSame,
|
||||
add,
|
||||
subtract,
|
||||
startOf,
|
||||
endOf,
|
||||
format,
|
||||
parseDate,
|
||||
now,
|
||||
createDate,
|
||||
fromNow,
|
||||
calendar
|
||||
} from '/imports/lib/dateUtils';
|
||||
import Boards from '../../models/boards';
|
||||
import Lists from '../../models/lists';
|
||||
|
|
@ -76,19 +76,19 @@ import Team from "../../models/team";
|
|||
|
||||
Meteor.publish('card', cardId => {
|
||||
check(cardId, String);
|
||||
|
||||
|
||||
const userId = Meteor.userId();
|
||||
const card = ReactiveCache.getCard({ _id: cardId });
|
||||
|
||||
|
||||
if (!card || !card.boardId) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
const board = ReactiveCache.getBoard({ _id: card.boardId });
|
||||
if (!board || !board.isVisibleBy(userId)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
// If user has assigned-only permissions, check if they're assigned to this card
|
||||
if (userId && board.members) {
|
||||
const member = _.findWhere(board.members, { userId: userId, isActive: true });
|
||||
|
|
@ -99,7 +99,7 @@ Meteor.publish('card', cardId => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const ret = ReactiveCache.getCards(
|
||||
{ _id: cardId },
|
||||
{},
|
||||
|
|
@ -177,7 +177,7 @@ Meteor.publish('myCards', function(sessionId) {
|
|||
// Optimized due cards publication for better performance
|
||||
Meteor.publish('dueCards', function(allUsers = false) {
|
||||
check(allUsers, Boolean);
|
||||
|
||||
|
||||
const userId = this.userId;
|
||||
if (!userId) {
|
||||
return this.ready();
|
||||
|
|
@ -198,7 +198,7 @@ Meteor.publish('dueCards', function(allUsers = false) {
|
|||
if (process.env.DEBUG === 'true') {
|
||||
console.log('dueCards userBoards:', userBoards);
|
||||
console.log('dueCards userBoards count:', userBoards.length);
|
||||
|
||||
|
||||
// Also check if there are any cards with due dates in the system at all
|
||||
const allCardsWithDueDates = Cards.find({
|
||||
type: 'cardType-card',
|
||||
|
|
@ -255,7 +255,7 @@ Meteor.publish('dueCards', function(allUsers = false) {
|
|||
}
|
||||
|
||||
const result = Cards.find(selector, options);
|
||||
|
||||
|
||||
if (process.env.DEBUG === 'true') {
|
||||
const count = result.count();
|
||||
console.log('dueCards publication: returning', count, 'cards');
|
||||
|
|
@ -295,7 +295,7 @@ Meteor.publish('sessionData', function(sessionId) {
|
|||
if (process.env.DEBUG === 'true') {
|
||||
console.log('sessionData publication called with:', { sessionId, userId });
|
||||
}
|
||||
|
||||
|
||||
const cursor = SessionData.find({ userId, sessionId });
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('sessionData publication returning cursor with count:', cursor.count());
|
||||
|
|
@ -903,7 +903,7 @@ function findCards(sessionId, query) {
|
|||
if (process.env.DEBUG === 'true') {
|
||||
console.log('findCards - upsertResult:', upsertResult);
|
||||
}
|
||||
|
||||
|
||||
// Check if the session data was actually stored
|
||||
const storedSessionData = SessionData.findOne({ userId, sessionId });
|
||||
if (process.env.DEBUG === 'true') {
|
||||
|
|
@ -968,7 +968,7 @@ function findCards(sessionId, query) {
|
|||
console.log('findCards - session data count (after delay):', sessionDataCursor.count());
|
||||
}
|
||||
}, 100);
|
||||
|
||||
|
||||
const sessionDataCursor = SessionData.find({ userId, sessionId });
|
||||
if (process.env.DEBUG === 'true') {
|
||||
console.log('findCards - publishing session data cursor:', sessionDataCursor);
|
||||
|
|
|
|||
16
server/publications/cronJobs.js
Normal file
16
server/publications/cronJobs.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { CronJobStatus } from '/server/cronJobStorage';
|
||||
|
||||
// Publish cron jobs status for admin users only
|
||||
Meteor.publish('cronJobs', function() {
|
||||
if (!this.userId) {
|
||||
return this.ready();
|
||||
}
|
||||
|
||||
const user = Users.findOne(this.userId);
|
||||
if (!user || !user.isAdmin) {
|
||||
return this.ready();
|
||||
}
|
||||
|
||||
// Publish all cron job status documents
|
||||
return CronJobStatus.find({});
|
||||
});
|
||||
16
server/publications/cronMigrationStatus.js
Normal file
16
server/publications/cronMigrationStatus.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { CronJobStatus } from '../cronJobStorage';
|
||||
|
||||
// Publish migration status for admin users only
|
||||
Meteor.publish('cronMigrationStatus', function() {
|
||||
if (!this.userId) {
|
||||
return this.ready();
|
||||
}
|
||||
|
||||
const user = Users.findOne(this.userId);
|
||||
if (!user || !user.isAdmin) {
|
||||
return this.ready();
|
||||
}
|
||||
|
||||
// Publish all cron job status documents
|
||||
return CronJobStatus.find({});
|
||||
});
|
||||
29
server/publications/customUI.js
Normal file
29
server/publications/customUI.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// Publish custom UI configuration
|
||||
Meteor.publish('customUI', function() {
|
||||
// Published to all users (public configuration)
|
||||
return Settings.find({}, {
|
||||
fields: {
|
||||
customLoginLogoImageUrl: 1,
|
||||
customLoginLogoLinkUrl: 1,
|
||||
customHelpLinkUrl: 1,
|
||||
textBelowCustomLoginLogo: 1,
|
||||
customTopLeftCornerLogoImageUrl: 1,
|
||||
customTopLeftCornerLogoLinkUrl: 1,
|
||||
customTopLeftCornerLogoHeight: 1,
|
||||
customHTMLafterBodyStart: 1,
|
||||
customHTMLbeforeBodyEnd: 1,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Publish Matomo configuration
|
||||
Meteor.publish('matomoConfig', function() {
|
||||
// Published to all users (public configuration)
|
||||
return Settings.find({}, {
|
||||
fields: {
|
||||
matomoEnabled: 1,
|
||||
matomoURL: 1,
|
||||
matomoSiteId: 1,
|
||||
}
|
||||
});
|
||||
});
|
||||
22
server/publications/migrationProgress.js
Normal file
22
server/publications/migrationProgress.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { CronJobStatus } from '/server/cronJobStorage';
|
||||
|
||||
// Publish detailed migration progress data for admin users
|
||||
Meteor.publish('migrationProgress', function() {
|
||||
if (!this.userId) {
|
||||
return this.ready();
|
||||
}
|
||||
|
||||
const user = Users.findOne(this.userId);
|
||||
if (!user || !user.isAdmin) {
|
||||
return this.ready();
|
||||
}
|
||||
|
||||
// Publish detailed migration progress documents
|
||||
// This includes current running job details, estimated time, etc.
|
||||
return CronJobStatus.find({
|
||||
$or: [
|
||||
{ jobType: 'migration' },
|
||||
{ jobId: 'migration' }
|
||||
]
|
||||
});
|
||||
});
|
||||
|
|
@ -62,10 +62,10 @@ if (Meteor.isServer) {
|
|||
|
||||
try {
|
||||
const userId = authenticateApiRequest(req);
|
||||
|
||||
|
||||
let body = '';
|
||||
let bodyComplete = false;
|
||||
|
||||
|
||||
req.on('data', chunk => {
|
||||
body += chunk.toString();
|
||||
// Prevent excessive payload
|
||||
|
|
@ -79,7 +79,7 @@ if (Meteor.isServer) {
|
|||
if (bodyComplete) return; // Already processed
|
||||
bodyComplete = true;
|
||||
clearTimeout(timeout);
|
||||
|
||||
|
||||
try {
|
||||
const data = JSON.parse(body);
|
||||
const { boardId, swimlaneId, listId, cardId, fileData, fileName, fileType, storageBackend } = data;
|
||||
|
|
@ -192,7 +192,7 @@ if (Meteor.isServer) {
|
|||
sendErrorResponse(res, 500, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
req.on('error', (error) => {
|
||||
clearTimeout(timeout);
|
||||
if (!res.headersSent) {
|
||||
|
|
@ -245,7 +245,7 @@ if (Meteor.isServer) {
|
|||
readStream.on('end', () => {
|
||||
const fileBuffer = Buffer.concat(chunks);
|
||||
const base64Data = fileBuffer.toString('base64');
|
||||
|
||||
|
||||
sendJsonResponse(res, 200, {
|
||||
success: true,
|
||||
attachmentId: attachmentId,
|
||||
|
|
@ -308,7 +308,7 @@ if (Meteor.isServer) {
|
|||
}
|
||||
|
||||
const attachments = ReactiveCache.getAttachments(query);
|
||||
|
||||
|
||||
const attachmentList = attachments.map(attachment => {
|
||||
const strategy = fileStoreStrategyFactory.getFileStrategy(attachment, 'original');
|
||||
return {
|
||||
|
|
@ -350,10 +350,10 @@ if (Meteor.isServer) {
|
|||
|
||||
try {
|
||||
const userId = authenticateApiRequest(req);
|
||||
|
||||
|
||||
let body = '';
|
||||
let bodyComplete = false;
|
||||
|
||||
|
||||
req.on('data', chunk => {
|
||||
body += chunk.toString();
|
||||
if (body.length > 10 * 1024 * 1024) { // 10MB limit for metadata
|
||||
|
|
@ -366,7 +366,7 @@ if (Meteor.isServer) {
|
|||
if (bodyComplete) return;
|
||||
bodyComplete = true;
|
||||
clearTimeout(timeout);
|
||||
|
||||
|
||||
try {
|
||||
const data = JSON.parse(body);
|
||||
const { attachmentId, targetBoardId, targetSwimlaneId, targetListId, targetCardId } = data;
|
||||
|
|
@ -478,7 +478,7 @@ if (Meteor.isServer) {
|
|||
sendErrorResponse(res, 500, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
req.on('error', (error) => {
|
||||
clearTimeout(timeout);
|
||||
if (!res.headersSent) {
|
||||
|
|
@ -506,10 +506,10 @@ if (Meteor.isServer) {
|
|||
|
||||
try {
|
||||
const userId = authenticateApiRequest(req);
|
||||
|
||||
|
||||
let body = '';
|
||||
let bodyComplete = false;
|
||||
|
||||
|
||||
req.on('data', chunk => {
|
||||
body += chunk.toString();
|
||||
if (body.length > 10 * 1024 * 1024) {
|
||||
|
|
@ -522,7 +522,7 @@ if (Meteor.isServer) {
|
|||
if (bodyComplete) return;
|
||||
bodyComplete = true;
|
||||
clearTimeout(timeout);
|
||||
|
||||
|
||||
try {
|
||||
const data = JSON.parse(body);
|
||||
const { attachmentId, targetBoardId, targetSwimlaneId, targetListId, targetCardId } = data;
|
||||
|
|
@ -595,7 +595,7 @@ if (Meteor.isServer) {
|
|||
sendErrorResponse(res, 500, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
req.on('error', (error) => {
|
||||
clearTimeout(timeout);
|
||||
if (!res.headersSent) {
|
||||
|
|
@ -668,7 +668,7 @@ if (Meteor.isServer) {
|
|||
}
|
||||
|
||||
const strategy = fileStoreStrategyFactory.getFileStrategy(attachment, 'original');
|
||||
|
||||
|
||||
sendJsonResponse(res, 200, {
|
||||
success: true,
|
||||
attachmentId: attachment._id,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ if (Meteor.isServer) {
|
|||
|
||||
try {
|
||||
const fileName = req.params[0];
|
||||
|
||||
|
||||
if (!fileName) {
|
||||
res.writeHead(400);
|
||||
res.end('Invalid avatar file name');
|
||||
|
|
@ -29,7 +29,7 @@ if (Meteor.isServer) {
|
|||
|
||||
// Extract file ID from filename (format: fileId-original-filename)
|
||||
const fileId = fileName.split('-original-')[0];
|
||||
|
||||
|
||||
if (!fileId) {
|
||||
res.writeHead(400);
|
||||
res.end('Invalid avatar file format');
|
||||
|
|
@ -68,7 +68,7 @@ if (Meteor.isServer) {
|
|||
res.setHeader('Content-Length', avatar.size || 0);
|
||||
res.setHeader('Cache-Control', 'public, max-age=31536000'); // Cache for 1 year
|
||||
res.setHeader('ETag', `"${avatar._id}"`);
|
||||
|
||||
|
||||
// Handle conditional requests
|
||||
const ifNoneMatch = req.headers['if-none-match'];
|
||||
if (ifNoneMatch && ifNoneMatch === `"${avatar._id}"`) {
|
||||
|
|
@ -106,12 +106,12 @@ if (Meteor.isServer) {
|
|||
|
||||
try {
|
||||
const fileName = req.params[0];
|
||||
|
||||
|
||||
// Redirect to new avatar URL format
|
||||
const newUrl = `/cdn/storage/avatars/${fileName}`;
|
||||
res.writeHead(301, { 'Location': newUrl });
|
||||
res.end();
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('Legacy avatar redirect error:', error);
|
||||
res.writeHead(500);
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ function sanitizeFilenameForHeader(filename) {
|
|||
// For non-ASCII filenames, provide a fallback and RFC 5987 encoded version
|
||||
const fallback = sanitized.replace(/[^\x20-\x7E]/g, '_').slice(0, 100) || 'download';
|
||||
const encoded = encodeURIComponent(sanitized);
|
||||
|
||||
|
||||
// Return special marker format that will be handled by buildContentDispositionHeader
|
||||
// Format: "fallback|RFC5987:encoded"
|
||||
return `${fallback}|RFC5987:${encoded}`;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ if (Meteor.isServer) {
|
|||
const nameLower = (fileObj.name || '').toLowerCase();
|
||||
const typeLower = (fileObj.type || '').toLowerCase();
|
||||
const isPdfByExt = nameLower.endsWith('.pdf');
|
||||
|
||||
|
||||
// Define dangerous types that must never be served inline
|
||||
const dangerousTypes = new Set([
|
||||
'text/html',
|
||||
|
|
@ -37,7 +37,7 @@ if (Meteor.isServer) {
|
|||
'application/javascript',
|
||||
'text/javascript'
|
||||
]);
|
||||
|
||||
|
||||
// Define safe types that can be served inline for viewing
|
||||
const safeInlineTypes = new Set([
|
||||
'application/pdf',
|
||||
|
|
@ -59,7 +59,7 @@ if (Meteor.isServer) {
|
|||
'text/plain',
|
||||
'application/json'
|
||||
]);
|
||||
|
||||
|
||||
const isSvg = nameLower.endsWith('.svg') || typeLower === 'image/svg+xml';
|
||||
const isDangerous = dangerousTypes.has(typeLower) || isSvg;
|
||||
// Consider PDF safe inline by extension if type is missing/mis-set
|
||||
|
|
@ -342,7 +342,7 @@ if (Meteor.isServer) {
|
|||
// For non-ASCII filenames, provide a fallback and RFC 5987 encoded version
|
||||
const fallback = sanitized.replace(/[^\x20-\x7E]/g, '_').slice(0, 100) || 'download';
|
||||
const encoded = encodeURIComponent(sanitized);
|
||||
|
||||
|
||||
// Return special marker format that will be handled by buildContentDispositionHeader
|
||||
// Format: "fallback|RFC5987:encoded"
|
||||
return `${fallback}|RFC5987:${encoded}`;
|
||||
|
|
@ -396,7 +396,7 @@ if (Meteor.isServer) {
|
|||
|
||||
try {
|
||||
const fileId = extractFirstIdFromUrl(req, '/cdn/storage/attachments');
|
||||
|
||||
|
||||
if (!fileId) {
|
||||
res.writeHead(400);
|
||||
res.end('Invalid attachment file ID');
|
||||
|
|
@ -483,7 +483,7 @@ if (Meteor.isServer) {
|
|||
|
||||
try {
|
||||
const fileId = extractFirstIdFromUrl(req, '/cdn/storage/avatars');
|
||||
|
||||
|
||||
if (!fileId) {
|
||||
res.writeHead(400);
|
||||
res.end('Invalid avatar file ID');
|
||||
|
|
@ -548,7 +548,7 @@ if (Meteor.isServer) {
|
|||
|
||||
try {
|
||||
const attachmentId = extractFirstIdFromUrl(req, '/cfs/files/attachments');
|
||||
|
||||
|
||||
if (!attachmentId) {
|
||||
res.writeHead(400);
|
||||
res.end('Invalid attachment ID');
|
||||
|
|
@ -624,7 +624,7 @@ if (Meteor.isServer) {
|
|||
|
||||
try {
|
||||
const avatarId = extractFirstIdFromUrl(req, '/cfs/files/avatars');
|
||||
|
||||
|
||||
if (!avatarId) {
|
||||
res.writeHead(400);
|
||||
res.end('Invalid avatar ID');
|
||||
|
|
@ -633,7 +633,7 @@ if (Meteor.isServer) {
|
|||
|
||||
// Try to get avatar from database (new structure first)
|
||||
let avatar = ReactiveCache.getAvatar(avatarId);
|
||||
|
||||
|
||||
// If not found in new structure, try to handle legacy format
|
||||
if (!avatar) {
|
||||
// For legacy avatars, we might need to handle different ID formats
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue