瀏覽代碼

Fix: global credentials deleted on every container restart

The workspace migration scanned all platform_credentials docs with no
colon in their _id, copied them to 'default:TYPE', then deleted the
original. This matched global config docs (ai_config, meta_app,
openai_config, groq_config, gemini_config, pinterest_app, tiktok_app,
google_places) and permanently removed them from the key that
getCredentials() looks up, making all global settings invisible after
the first restart.

Fixes:
- Migration now skips GLOBAL_CREDENTIAL_TYPES entirely (they must keep
  their bare _id so getCredentials can find them)
- Added a recovery step: any global config erroneously migrated to
  'default:TYPE' is moved back to bare 'TYPE' on next startup
- getCredentials() now falls back to the workspace-prefixed key for
  global types so data silently corrupted by previous runs still loads
- Migration guard: only re-keys docs that haven't already been migrated
  (checks for absence of the 'type' field added by the migration)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Benjamin Harris 3 周之前
父節點
當前提交
8104a956f1
共有 1 個文件被更改,包括 38 次插入4 次删除
  1. 38 4
      services/gateway/server.js

+ 38 - 4
services/gateway/server.js

@@ -141,7 +141,13 @@ const GLOBAL_CREDENTIAL_TYPES = new Set([
 async function getCredentials(ws, type) {
 async function getCredentials(ws, type) {
   const db = await getDb();
   const db = await getDb();
   const id = GLOBAL_CREDENTIAL_TYPES.has(type) ? type : credId(ws, type);
   const id = GLOBAL_CREDENTIAL_TYPES.has(type) ? type : credId(ws, type);
-  return db.collection('platform_credentials').findOne({ _id: id });
+  const doc = await db.collection('platform_credentials').findOne({ _id: id });
+  // Fallback: if global credential not found under bare key, check the erroneously
+  // workspace-prefixed key that an earlier migration may have created.
+  if (!doc && GLOBAL_CREDENTIAL_TYPES.has(type)) {
+    return db.collection('platform_credentials').findOne({ _id: credId(ws, type) });
+  }
+  return doc;
 }
 }
 
 
 async function setCredentials(ws, type, data) {
 async function setCredentials(ws, type, data) {
@@ -232,22 +238,50 @@ async function runWorkspaceMigration() {
       { $setOnInsert: { _id: 'default', name: 'Default', color: '#3B82F6', createdAt: new Date(), updatedAt: new Date() } },
       { $setOnInsert: { _id: 'default', name: 'Default', color: '#3B82F6', createdAt: new Date(), updatedAt: new Date() } },
       { upsert: true }
       { upsert: true }
     );
     );
-    // Migrate platform_credentials: re-key any doc whose _id doesn't contain ':'
-    const oldCreds = await db.collection('platform_credentials').find({ _id: { $not: /\:/ } }).toArray();
+
+    // Recovery: any global credential that was previously mis-migrated to
+    // 'default:TYPE' must be moved back to the bare 'TYPE' key so that
+    // getCredentials() can find it. This repairs data corrupted by the earlier
+    // version of this migration that didn't skip global types.
+    for (const type of GLOBAL_CREDENTIAL_TYPES) {
+      const migratedId = credId('default', type);
+      const migrated = await db.collection('platform_credentials').findOne({ _id: migratedId });
+      if (migrated) {
+        const bareExists = await db.collection('platform_credentials').findOne({ _id: type });
+        if (!bareExists) {
+          const { _id, workspaceId, ...rest } = migrated;
+          await db.collection('platform_credentials').insertOne({ ...rest, _id: type });
+          log.info({ action: 'workspace_migration', step: 'recover_global', type, outcome: 'success' });
+        }
+        await db.collection('platform_credentials').deleteOne({ _id: migratedId });
+      }
+    }
+
+    // Migrate workspace-scoped platform_credentials: re-key docs whose _id
+    // has no colon AND is not a global type (e.g. bare 'facebook' → 'default:facebook').
+    // Global types (ai_config, meta_app, etc.) are intentionally never workspace-prefixed.
+    const oldCreds = await db.collection('platform_credentials').find({
+      _id: { $not: /\:/ },
+      type: { $exists: false },  // only un-migrated docs (migrated ones already have 'type' field)
+    }).toArray();
+    let migrated = 0;
     for (const cred of oldCreds) {
     for (const cred of oldCreds) {
+      if (GLOBAL_CREDENTIAL_TYPES.has(cred._id)) continue;  // never re-key global types
       const newId = credId('default', cred._id);
       const newId = credId('default', cred._id);
       const exists = await db.collection('platform_credentials').findOne({ _id: newId });
       const exists = await db.collection('platform_credentials').findOne({ _id: newId });
       if (!exists) {
       if (!exists) {
         await db.collection('platform_credentials').insertOne({ ...cred, _id: newId, workspaceId: 'default', type: cred._id });
         await db.collection('platform_credentials').insertOne({ ...cred, _id: newId, workspaceId: 'default', type: cred._id });
+        migrated++;
       }
       }
       await db.collection('platform_credentials').deleteOne({ _id: cred._id });
       await db.collection('platform_credentials').deleteOne({ _id: cred._id });
     }
     }
+
     // Stamp workspaceId on all other collections
     // Stamp workspaceId on all other collections
     const cols = ['competitors','hashtag_groups','hashtag_stats','account_profiles','content_calendars','bulk_draft_batches','drafts','posts','post_metrics','media_files','feeds','scheduled_jobs'];
     const cols = ['competitors','hashtag_groups','hashtag_stats','account_profiles','content_calendars','bulk_draft_batches','drafts','posts','post_metrics','media_files','feeds','scheduled_jobs'];
     for (const col of cols) {
     for (const col of cols) {
       await db.collection(col).updateMany({ workspaceId: { $exists: false } }, { $set: { workspaceId: 'default' } });
       await db.collection(col).updateMany({ workspaceId: { $exists: false } }, { $set: { workspaceId: 'default' } });
     }
     }
-    if (oldCreds.length > 0) log.info({ action: 'workspace_migration', migrated: oldCreds.length, outcome: 'success' });
+    if (migrated > 0) log.info({ action: 'workspace_migration', migrated, outcome: 'success' });
   } catch (err) {
   } catch (err) {
     log.error({ action: 'workspace_migration', outcome: 'failure', err: err.message });
     log.error({ action: 'workspace_migration', outcome: 'failure', err: err.message });
   }
   }