diff --git a/client/components/sidebar/sidebarMigrations.jade b/client/components/sidebar/sidebarMigrations.jade index 78da56983..f5f7f08f8 100644 --- a/client/components/sidebar/sidebarMigrations.jade +++ b/client/components/sidebar/sidebarMigrations.jade @@ -58,8 +58,6 @@ template(name='migrationsSidebar') else span.badge.badge-success {{_ 'migration-complete'}} - hr - h4 {{_ 'global-migrations'}} .migration-item a.js-run-migration(data-migration="fixAvatarUrls") .migration-name diff --git a/client/components/sidebar/sidebarMigrations.js b/client/components/sidebar/sidebarMigrations.js index cc5461af2..89d3343ec 100644 --- a/client/components/sidebar/sidebarMigrations.js +++ b/client/components/sidebar/sidebarMigrations.js @@ -57,17 +57,17 @@ BlazeComponent.extendComponent({ } }); - // Check fix avatar URLs migration (global) - Meteor.call('fixAvatarUrls.needsMigration', (err, res) => { + // Check fix avatar URLs migration (board-specific) + Meteor.call('fixAvatarUrls.needsMigration', boardId, (err, res) => { if (!err) { const statuses = this.migrationStatuses.get(); - statuses.fixAvatarUrls = res; + statuses.fixAvatarUrls = res; this.migrationStatuses.set(statuses); } }); - // Check fix all file URLs migration (global) - Meteor.call('fixAllFileUrls.needsMigration', (err, res) => { + // Check fix all file URLs migration (board-specific) + Meteor.call('fixAllFileUrls.needsMigration', boardId, (err, res) => { if (!err) { const statuses = this.migrationStatuses.get(); statuses.fixAllFileUrls = res; @@ -190,10 +190,12 @@ BlazeComponent.extendComponent({ case 'fixAvatarUrls': methodName = 'fixAvatarUrls.execute'; + methodArgs = [boardId]; break; case 'fixAllFileUrls': methodName = 'fixAllFileUrls.execute'; + methodArgs = [boardId]; break; } @@ -231,12 +233,12 @@ BlazeComponent.extendComponent({ { step: 'fix_missing_ids', name: 'Fix Missing IDs', duration: 600 }, ], fixAvatarUrls: [ - { step: 'scan_users', name: 'Scan Users', duration: 500 }, - { step: 'fix_urls', name: 'Fix Avatar URLs', duration: 900 }, + { step: 'scan_users', name: 'Checking board member avatars', duration: 500 }, + { step: 'fix_urls', name: 'Fixing avatar URLs', duration: 900 }, ], fixAllFileUrls: [ - { step: 'scan_files', name: 'Scan Files', duration: 600 }, - { step: 'fix_urls', name: 'Fix File URLs', duration: 1000 }, + { step: 'scan_files', name: 'Checking board file attachments', duration: 600 }, + { step: 'fix_urls', name: 'Fixing file URLs', duration: 1000 }, ], }; diff --git a/imports/i18n/data/en.i18n.json b/imports/i18n/data/en.i18n.json index 182bafd21..958d8313a 100644 --- a/imports/i18n/data/en.i18n.json +++ b/imports/i18n/data/en.i18n.json @@ -1419,10 +1419,9 @@ "fix-missing-lists-migration": "Fix Missing Lists", "fix-missing-lists-migration-description": "Detects and repairs missing or corrupted lists in the board structure.", "fix-avatar-urls-migration": "Fix Avatar URLs", - "fix-avatar-urls-migration-description": "Updates avatar URLs to use the correct storage backend and fixes broken avatar references.", + "fix-avatar-urls-migration-description": "Updates avatar URLs for board members to use the correct storage backend and fixes broken avatar references.", "fix-all-file-urls-migration": "Fix All File URLs", - "fix-all-file-urls-migration-description": "Updates all file attachment URLs to use the correct storage backend and fixes broken file references.", - "global-migrations": "Global Migrations", + "fix-all-file-urls-migration-description": "Updates all file attachment URLs on this board to use the correct storage backend and fixes broken file references.", "migration-needed": "Migration Needed", "migration-complete": "Complete", "migration-running": "Running...", @@ -1438,8 +1437,8 @@ "run-restore-lost-cards-migration-confirm": "This will create a 'Lost Cards' swimlane and restore all cards and lists with missing swimlaneId or listId. This only affects non-archived items. Continue?", "run-restore-all-archived-migration-confirm": "This will restore ALL archived swimlanes, lists, and cards, making them visible again. Any items with missing IDs will be automatically fixed. This cannot be easily undone. Continue?", "run-fix-missing-lists-migration-confirm": "This will detect and repair missing or corrupted lists in the board structure. Continue?", - "run-fix-avatar-urls-migration-confirm": "This will update avatar URLs across all boards to use the correct storage backend. This is a global operation. Continue?", - "run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs across all boards to use the correct storage backend. This is a global operation. Continue?", + "run-fix-avatar-urls-migration-confirm": "This will update avatar URLs for board members to use the correct storage backend. Continue?", + "run-fix-all-file-urls-migration-confirm": "This will update all file attachment URLs on this board to use the correct storage backend. Continue?", "restore-lost-cards-nothing-to-restore": "No lost swimlanes, lists, or cards to restore", "migration-progress-title": "Board Migration in Progress", @@ -1466,9 +1465,9 @@ "step-restore-cards": "Restore Cards", "step-restore-swimlanes": "Restore Swimlanes", "step-fix-missing-ids": "Fix Missing IDs", - "step-scan-users": "Scan Users", - "step-scan-files": "Scan Files", - "step-fix-file-urls": "Fix File URLs", + "step-scan-users": "Checking board member avatars", + "step-scan-files": "Checking board file attachments", + "step-fix-file-urls": "Fixing file URLs", "cleanup": "Cleanup", "cleanup-old-jobs": "Cleanup Old Jobs", "completed": "Completed", diff --git a/server/migrations/fixAllFileUrls.js b/server/migrations/fixAllFileUrls.js index 6b3be9ccf..f713ac8ae 100644 --- a/server/migrations/fixAllFileUrls.js +++ b/server/migrations/fixAllFileUrls.js @@ -3,10 +3,14 @@ * Ensures all attachment and avatar URLs are universal and work regardless of ROOT_URL and PORT settings */ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; import { ReactiveCache } from '/imports/reactiveCache'; +import Boards from '/models/boards'; import Users from '/models/users'; import Attachments from '/models/attachments'; import Avatars from '/models/avatars'; +import Cards from '/models/cards'; import { generateUniversalAttachmentUrl, generateUniversalAvatarUrl, cleanFileUrl, extractFileIdFromUrl, isUniversalFileUrl } from '/models/lib/universalUrlGenerator'; class FixAllFileUrlsMigration { @@ -16,11 +20,19 @@ class FixAllFileUrlsMigration { } /** - * Check if migration is needed + * Check if migration is needed for a board */ - needsMigration() { - // Check for problematic avatar URLs - const users = ReactiveCache.getUsers({}); + needsMigration(boardId) { + // Get all users who are members of this board + const board = ReactiveCache.getBoard(boardId); + 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) { if (user.profile && user.profile.avatarUrl) { const avatarUrl = user.profile.avatarUrl; @@ -30,8 +42,11 @@ class FixAllFileUrlsMigration { } } - // Check for problematic attachment URLs - const attachments = ReactiveCache.getAttachments({}); + // Check for problematic attachment URLs on this board + 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; @@ -78,46 +93,53 @@ class FixAllFileUrlsMigration { } /** - * Execute the migration + * Execute the migration for a board */ - async execute() { + async execute(boardId) { let filesFixed = 0; let errors = []; - console.log(`Starting universal file URL migration...`); + console.log(`Starting universal file URL migration for board ${boardId}...`); try { - // Fix avatar URLs - const avatarFixed = await this.fixAvatarUrls(); + // Fix avatar URLs for board members + const avatarFixed = await this.fixAvatarUrls(boardId); filesFixed += avatarFixed; - // Fix attachment URLs - const attachmentFixed = await this.fixAttachmentUrls(); + // Fix attachment URLs for board cards + const attachmentFixed = await this.fixAttachmentUrls(boardId); filesFixed += attachmentFixed; // Fix card attachment references - const cardFixed = await this.fixCardAttachmentUrls(); + const cardFixed = await this.fixCardAttachmentUrls(boardId); filesFixed += cardFixed; } catch (error) { - console.error('Error during file URL migration:', error); + console.error('Error during file URL migration for board', boardId, ':', error); errors.push(error.message); } - console.log(`Universal file URL migration completed. Fixed ${filesFixed} file URLs.`); + console.log(`Universal file URL migration completed for board ${boardId}. Fixed ${filesFixed} file URLs.`); return { success: errors.length === 0, filesFixed, - errors + errors, + changes: [`Fixed ${filesFixed} file URLs for this board`] }; } /** - * Fix avatar URLs in user profiles + * Fix avatar URLs in user profiles for board members */ - async fixAvatarUrls() { - const users = ReactiveCache.getUsers({}); + async fixAvatarUrls(boardId) { + const board = ReactiveCache.getBoard(boardId); + if (!board || !board.members) { + return 0; + } + + const memberIds = board.members.map(m => m.userId); + const users = ReactiveCache.getUsers({ _id: { $in: memberIds } }); let avatarsFixed = 0; for (const user of users) { @@ -164,10 +186,12 @@ class FixAllFileUrlsMigration { } /** - * Fix attachment URLs in attachment records + * Fix attachment URLs in attachment records for this board */ - async fixAttachmentUrls() { - const attachments = ReactiveCache.getAttachments({}); + async fixAttachmentUrls(boardId) { + const cards = ReactiveCache.getCards({ boardId }); + const cardIds = cards.map(c => c._id); + const attachments = ReactiveCache.getAttachments({ cardId: { $in: cardIds } }); let attachmentsFixed = 0; for (const attachment of attachments) { @@ -202,10 +226,12 @@ class FixAllFileUrlsMigration { } /** - * Fix attachment URLs in the Attachments collection + * Fix attachment URLs in the Attachments collection for this board */ - async fixCardAttachmentUrls() { - const attachments = ReactiveCache.getAttachments({}); + async fixCardAttachmentUrls(boardId) { + const cards = ReactiveCache.getCards({ boardId }); + const cardIds = cards.map(c => c._id); + const attachments = ReactiveCache.getAttachments({ cardId: { $in: cardIds } }); let attachmentsFixed = 0; for (const attachment of attachments) { @@ -244,19 +270,43 @@ export const fixAllFileUrlsMigration = new FixAllFileUrlsMigration(); // Meteor methods Meteor.methods({ - 'fixAllFileUrls.execute'() { + 'fixAllFileUrls.execute'(boardId) { + check(boardId, String); + if (!this.userId) { - throw new Meteor.Error('not-authorized'); + throw new Meteor.Error('not-authorized', 'You must be logged in'); + } + + // Check if user is board admin + const board = ReactiveCache.getBoard(boardId); + if (!board) { + throw new Meteor.Error('board-not-found', 'Board not found'); + } + + const user = ReactiveCache.getUser(this.userId); + if (!user) { + throw new Meteor.Error('user-not-found', 'User not found'); + } + + // Only board admins can run migrations + const isBoardAdmin = board.members && board.members.some( + member => member.userId === this.userId && member.isAdmin + ); + + if (!isBoardAdmin && !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Only board administrators can run migrations'); } - return fixAllFileUrlsMigration.execute(); + return fixAllFileUrlsMigration.execute(boardId); }, - 'fixAllFileUrls.needsMigration'() { + 'fixAllFileUrls.needsMigration'(boardId) { + check(boardId, String); + if (!this.userId) { - throw new Meteor.Error('not-authorized'); + throw new Meteor.Error('not-authorized', 'You must be logged in'); } - return fixAllFileUrlsMigration.needsMigration(); + return fixAllFileUrlsMigration.needsMigration(boardId); } }); diff --git a/server/migrations/fixAvatarUrls.js b/server/migrations/fixAvatarUrls.js index f542903ed..82677eb48 100644 --- a/server/migrations/fixAvatarUrls.js +++ b/server/migrations/fixAvatarUrls.js @@ -3,7 +3,10 @@ * Removes problematic auth parameters from existing avatar URLs */ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; import { ReactiveCache } from '/imports/reactiveCache'; +import Boards from '/models/boards'; import Users from '/models/users'; import { generateUniversalAvatarUrl, cleanFileUrl, extractFileIdFromUrl, isUniversalFileUrl } from '/models/lib/universalUrlGenerator'; @@ -14,10 +17,17 @@ class FixAvatarUrlsMigration { } /** - * Check if migration is needed + * Check if migration is needed for a board */ - needsMigration() { - const users = ReactiveCache.getUsers({}); + needsMigration(boardId) { + // Get all users who are members of this board + const board = ReactiveCache.getBoard(boardId); + 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) { @@ -32,13 +42,23 @@ class FixAvatarUrlsMigration { } /** - * Execute the migration + * Execute the migration for a board */ - async execute() { - const users = ReactiveCache.getUsers({}); + async execute(boardId) { + // Get all users who are members of this board + const board = ReactiveCache.getBoard(boardId); + if (!board || !board.members) { + return { + success: false, + 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; - console.log(`Starting avatar URL fix migration...`); + console.log(`Starting avatar URL fix migration for board ${boardId}...`); for (const user of users) { if (user.profile && user.profile.avatarUrl) { @@ -96,11 +116,12 @@ class FixAvatarUrlsMigration { } } - console.log(`Avatar URL fix migration completed. Fixed ${avatarsFixed} avatar URLs.`); + console.log(`Avatar URL fix migration completed for board ${boardId}. Fixed ${avatarsFixed} avatar URLs.`); return { success: true, - avatarsFixed + avatarsFixed, + changes: [`Fixed ${avatarsFixed} avatar URLs for board members`] }; } } @@ -110,19 +131,43 @@ export const fixAvatarUrlsMigration = new FixAvatarUrlsMigration(); // Meteor method Meteor.methods({ - 'fixAvatarUrls.execute'() { + 'fixAvatarUrls.execute'(boardId) { + check(boardId, String); + if (!this.userId) { - throw new Meteor.Error('not-authorized'); + throw new Meteor.Error('not-authorized', 'You must be logged in'); + } + + // Check if user is board admin + const board = ReactiveCache.getBoard(boardId); + if (!board) { + throw new Meteor.Error('board-not-found', 'Board not found'); + } + + const user = ReactiveCache.getUser(this.userId); + if (!user) { + throw new Meteor.Error('user-not-found', 'User not found'); + } + + // Only board admins can run migrations + const isBoardAdmin = board.members && board.members.some( + member => member.userId === this.userId && member.isAdmin + ); + + if (!isBoardAdmin && !user.isAdmin) { + throw new Meteor.Error('not-authorized', 'Only board administrators can run migrations'); } - return fixAvatarUrlsMigration.execute(); + return fixAvatarUrlsMigration.execute(boardId); }, - 'fixAvatarUrls.needsMigration'() { + 'fixAvatarUrls.needsMigration'(boardId) { + check(boardId, String); + if (!this.userId) { - throw new Meteor.Error('not-authorized'); + throw new Meteor.Error('not-authorized', 'You must be logged in'); } - return fixAvatarUrlsMigration.needsMigration(); + return fixAvatarUrlsMigration.needsMigration(boardId); } });