fixAvatarUrls.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. /**
  2. * Fix Avatar URLs Migration
  3. * Removes problematic auth parameters from existing avatar URLs
  4. */
  5. import { Meteor } from 'meteor/meteor';
  6. import { check } from 'meteor/check';
  7. import { ReactiveCache } from '/imports/reactiveCache';
  8. import Boards from '/models/boards';
  9. import Users from '/models/users';
  10. import { generateUniversalAvatarUrl, cleanFileUrl, extractFileIdFromUrl, isUniversalFileUrl } from '/models/lib/universalUrlGenerator';
  11. class FixAvatarUrlsMigration {
  12. constructor() {
  13. this.name = 'fixAvatarUrls';
  14. this.version = 1;
  15. }
  16. /**
  17. * Check if migration is needed for a board
  18. */
  19. needsMigration(boardId) {
  20. // Get all users who are members of this board
  21. const board = ReactiveCache.getBoard(boardId);
  22. if (!board || !board.members) {
  23. return false;
  24. }
  25. const memberIds = board.members.map(m => m.userId);
  26. const users = ReactiveCache.getUsers({ _id: { $in: memberIds } });
  27. for (const user of users) {
  28. if (user.profile && user.profile.avatarUrl) {
  29. const avatarUrl = user.profile.avatarUrl;
  30. if (avatarUrl.includes('auth=false') || avatarUrl.includes('brokenIsFine=true')) {
  31. return true;
  32. }
  33. }
  34. }
  35. return false;
  36. }
  37. /**
  38. * Execute the migration for a board
  39. */
  40. async execute(boardId) {
  41. // Get all users who are members of this board
  42. const board = ReactiveCache.getBoard(boardId);
  43. if (!board || !board.members) {
  44. return {
  45. success: false,
  46. error: 'Board not found or has no members'
  47. };
  48. }
  49. const memberIds = board.members.map(m => m.userId);
  50. const users = ReactiveCache.getUsers({ _id: { $in: memberIds } });
  51. let avatarsFixed = 0;
  52. console.log(`Starting avatar URL fix migration for board ${boardId}...`);
  53. for (const user of users) {
  54. if (user.profile && user.profile.avatarUrl) {
  55. const avatarUrl = user.profile.avatarUrl;
  56. let needsUpdate = false;
  57. let cleanUrl = avatarUrl;
  58. // Check if URL has problematic parameters
  59. if (avatarUrl.includes('auth=false') || avatarUrl.includes('brokenIsFine=true')) {
  60. // Remove problematic parameters
  61. cleanUrl = cleanUrl.replace(/[?&]auth=false/g, '');
  62. cleanUrl = cleanUrl.replace(/[?&]brokenIsFine=true/g, '');
  63. cleanUrl = cleanUrl.replace(/\?&/g, '?');
  64. cleanUrl = cleanUrl.replace(/\?$/g, '');
  65. needsUpdate = true;
  66. }
  67. // Check if URL is using old CollectionFS format
  68. if (avatarUrl.includes('/cfs/files/avatars/')) {
  69. cleanUrl = cleanUrl.replace('/cfs/files/avatars/', '/cdn/storage/avatars/');
  70. needsUpdate = true;
  71. }
  72. // Check if URL is missing the /cdn/storage/avatars/ prefix
  73. if (avatarUrl.includes('avatars/') && !avatarUrl.includes('/cdn/storage/avatars/') && !avatarUrl.includes('/cfs/files/avatars/')) {
  74. // This might be a relative URL, make it absolute
  75. if (!avatarUrl.startsWith('http') && !avatarUrl.startsWith('/')) {
  76. cleanUrl = `/cdn/storage/avatars/${avatarUrl}`;
  77. needsUpdate = true;
  78. }
  79. }
  80. // If we have a file ID, generate a universal URL
  81. const fileId = extractFileIdFromUrl(avatarUrl, 'avatar');
  82. if (fileId && !isUniversalFileUrl(cleanUrl, 'avatar')) {
  83. cleanUrl = generateUniversalAvatarUrl(fileId);
  84. needsUpdate = true;
  85. }
  86. if (needsUpdate) {
  87. // Update user's avatar URL
  88. Users.update(user._id, {
  89. $set: {
  90. 'profile.avatarUrl': cleanUrl,
  91. modifiedAt: new Date()
  92. }
  93. });
  94. avatarsFixed++;
  95. if (process.env.DEBUG === 'true') {
  96. console.log(`Fixed avatar URL for user ${user.username}: ${avatarUrl} -> ${cleanUrl}`);
  97. }
  98. }
  99. }
  100. }
  101. console.log(`Avatar URL fix migration completed for board ${boardId}. Fixed ${avatarsFixed} avatar URLs.`);
  102. return {
  103. success: true,
  104. avatarsFixed,
  105. changes: [`Fixed ${avatarsFixed} avatar URLs for board members`]
  106. };
  107. }
  108. }
  109. // Export singleton instance
  110. export const fixAvatarUrlsMigration = new FixAvatarUrlsMigration();
  111. // Meteor method
  112. Meteor.methods({
  113. 'fixAvatarUrls.execute'(boardId) {
  114. check(boardId, String);
  115. if (!this.userId) {
  116. throw new Meteor.Error('not-authorized', 'You must be logged in');
  117. }
  118. // Check if user is board admin
  119. const board = ReactiveCache.getBoard(boardId);
  120. if (!board) {
  121. throw new Meteor.Error('board-not-found', 'Board not found');
  122. }
  123. const user = ReactiveCache.getUser(this.userId);
  124. if (!user) {
  125. throw new Meteor.Error('user-not-found', 'User not found');
  126. }
  127. // Only board admins can run migrations
  128. const isBoardAdmin = board.members && board.members.some(
  129. member => member.userId === this.userId && member.isAdmin
  130. );
  131. if (!isBoardAdmin && !user.isAdmin) {
  132. throw new Meteor.Error('not-authorized', 'Only board administrators can run migrations');
  133. }
  134. return fixAvatarUrlsMigration.execute(boardId);
  135. },
  136. 'fixAvatarUrls.needsMigration'(boardId) {
  137. check(boardId, String);
  138. if (!this.userId) {
  139. throw new Meteor.Error('not-authorized', 'You must be logged in');
  140. }
  141. return fixAvatarUrlsMigration.needsMigration(boardId);
  142. }
  143. });