Benjamin Harris 3 недель назад
Родитель
Сommit
e4ceed5b57

+ 11 - 10
services/facebook/index.js

@@ -3,6 +3,7 @@ const axios = require('axios');
 const BasePlatformService = require('./utils/BasePlatformService');
 const { getDb } = require('./utils/MongoDBConnector');
 const { decryptToken, warnIfNoKey } = require('./utils/crypto');
+const { getWorkspaceCredential } = require('./utils/credentials');
 
 const GRAPH_API = 'https://graph.facebook.com/v22.0';
 
@@ -13,10 +14,10 @@ class FacebookService extends BasePlatformService {
 
   // Read selected Facebook Pages from MongoDB.
   // Falls back to env vars for backwards compatibility.
-  async _getPages() {
+  async _getPages(workspaceId = 'default') {
     try {
       const db = await getDb();
-      const cred = await db.collection('platform_credentials').findOne({ _id: 'facebook' });
+      const cred = await getWorkspaceCredential(db, 'facebook', workspaceId);
       const dbPages = (cred?.pages || []).filter((p) => p.selected);
       if (dbPages.length > 0) {
         return dbPages.map((p) => ({ ...p, accessToken: decryptToken(p.accessToken) })).filter((p) => p.accessToken);
@@ -32,8 +33,8 @@ class FacebookService extends BasePlatformService {
     return [];
   }
 
-  async getStatus() {
-    const pages = await this._getPages();
+  async getStatus(workspaceId = 'default') {
+    const pages = await this._getPages(workspaceId);
     if (pages.length === 0) {
       return { connected: false, platform: 'facebook', error: 'No Facebook Pages connected — use Settings to connect via Facebook OAuth' };
     }
@@ -58,8 +59,8 @@ class FacebookService extends BasePlatformService {
     }
   }
 
-  async fetchFeed({ limit = 20 } = {}) {
-    const pages = await this._getPages();
+  async fetchFeed({ limit = 20, workspaceId = 'default' } = {}) {
+    const pages = await this._getPages(workspaceId);
     if (pages.length === 0) throw new Error('No Facebook Pages connected');
 
     const allItems = [];
@@ -100,8 +101,8 @@ class FacebookService extends BasePlatformService {
       const col = db.collection('feeds');
       for (const item of allItems) {
         await col.updateOne(
-          { platform: 'facebook', originalId: item.originalId },
-          { $set: item },
+          { platform: 'facebook', originalId: item.originalId, workspaceId },
+          { $set: { ...item, workspaceId } },
           { upsert: true }
         );
       }
@@ -112,8 +113,8 @@ class FacebookService extends BasePlatformService {
     return allItems;
   }
 
-  async publishPost({ content, link, imageUrl, accountId, firstComment } = {}) {
-    const allPages = await this._getPages();
+  async publishPost({ content, link, imageUrl, accountId, firstComment, workspaceId = 'default' } = {}) {
+    const allPages = await this._getPages(workspaceId);
     if (allPages.length === 0) throw new Error('No Facebook Pages connected');
     if (!content) throw new Error('content is required');
 

+ 16 - 6
services/feed-aggregator/index.js

@@ -25,9 +25,12 @@ let producer;
 
 // ─── Feed Çekme ──────────────────────────────────────────────────────────────
 
-async function fetchPlatformFeed(platform, serviceUrl) {
+async function fetchPlatformFeed(platform, serviceUrl, workspaceId = 'default') {
   try {
-    const response = await axios.get(`${serviceUrl}/feed`, { timeout: 15000 });
+    const response = await axios.get(`${serviceUrl}/feed`, {
+      timeout: 15000,
+      headers: { 'X-Workspace-Id': workspaceId },
+    });
     const items = response.data.items || [];
     log.info({ action: 'feed_fetch', platform, count: items.length, outcome: 'success' });
 
@@ -67,8 +70,9 @@ app.get('/health', async () => ({ status: 'ok', service: 'feed-aggregator' }));
 
 app.post('/fetch', async (request) => {
   const { platform } = request.body || {};
+  const workspaceId = request.headers['x-workspace-id'] || 'default';
   if (platform && PLATFORM_SERVICES[platform]) {
-    const items = await fetchPlatformFeed(platform, PLATFORM_SERVICES[platform]);
+    const items = await fetchPlatformFeed(platform, PLATFORM_SERVICES[platform], workspaceId);
     return { success: true, platform, count: items.length };
   }
   const summary = await fetchAllFeeds();
@@ -77,10 +81,12 @@ app.post('/fetch', async (request) => {
 
 app.get('/feeds', async (request) => {
   const { platform, tag, limit = 50, skip = 0 } = request.query;
+  const workspaceId = request.headers['x-workspace-id'] || 'default';
   const db = await getDb();
   const col = db.collection('feeds');
 
-  const filter = {};
+  // Include legacy items without workspaceId (backwards compat)
+  const filter = { $or: [{ workspaceId }, { workspaceId: { $exists: false } }] };
   if (platform) filter.platform = platform;
   if (tag) filter.tags = tag;
 
@@ -94,10 +100,14 @@ app.get('/feeds', async (request) => {
   return { success: true, count: items.length, items };
 });
 
-app.get('/platform-status', async () => {
+app.get('/platform-status', async (request) => {
+  const workspaceId = request.headers['x-workspace-id'] || 'default';
   const statuses = await Promise.allSettled(
     Object.entries(PLATFORM_SERVICES).map(async ([platform, url]) => {
-      const response = await axios.get(`${url}/status`, { timeout: 5000 });
+      const response = await axios.get(`${url}/status`, {
+        timeout: 5000,
+        headers: { 'X-Workspace-Id': workspaceId },
+      });
       return { platform, ...response.data };
     })
   );

+ 17 - 12
services/gateway/server.js

@@ -243,7 +243,7 @@ async function runWorkspaceMigration() {
       await db.collection('platform_credentials').deleteOne({ _id: cred._id });
     }
     // 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'];
+    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) {
       await db.collection(col).updateMany({ workspaceId: { $exists: false } }, { $set: { workspaceId: 'default' } });
     }
@@ -1677,6 +1677,7 @@ app.post('/post', async (request, reply) => {
   if (!content?.trim()) return reply.code(400).send({ error: 'content is required' });
   if (!destinations.length) return reply.code(400).send({ error: 'destinations must not be empty' });
 
+  const workspaceId = request.workspaceId;
   const results = await Promise.allSettled(
     destinations.map(async ({ platform, accountId, imageUrl, videoUrl, link }) => {
       const serviceUrl = PLATFORM_SERVICES[platform];
@@ -1684,7 +1685,7 @@ app.post('/post', async (request, reply) => {
       const res = await axios.post(
         `${serviceUrl}/post`,
         { content, accountId, imageUrl, videoUrl, link, firstComment: firstComment?.trim() || undefined },
-        { timeout: 30000 }
+        { timeout: 30000, headers: { 'X-Workspace-Id': workspaceId } }
       );
       return { platform, accountId, ...res.data };
     })
@@ -2559,9 +2560,13 @@ function parseAccountFilter(account) {
 }
 
 // Build a MongoDB match fragment for scheduled_jobs given an account filter.
-function sjFilter(filter) {
-  if (!filter) return {};
+function sjFilter(filter, workspaceId) {
+  const wsClause = workspaceId
+    ? { $or: [{ workspaceId }, { workspaceId: { $exists: false } }] }
+    : {};
+  if (!filter) return wsClause;
   return {
+    ...wsClause,
     'destinations.platform': filter.platform,
     ...(filter.accountId && { 'destinations.accountId': filter.accountId }),
   };
@@ -2586,7 +2591,7 @@ app.get('/analytics/summary', async (request) => {
   // Post-unwind filter for scheduled_jobs platform breakdown — re-applies the
   // account filter after $unwind so a job targeting multiple platforms only
   // counts the platform(s) that match the filter.
-  const unwindFilter = filter ? [{ $match: sjFilter(filter) }] : [];
+  const unwindFilter = filter ? [{ $match: sjFilter(filter, ws) }] : [];
 
   const wsIp = { workspaceId: ws };
   const [
@@ -2596,15 +2601,15 @@ app.get('/analytics/summary', async (request) => {
     schedPlatformRaw, immPlatformRaw,
     schedDayRaw, immDayRaw,
   ] = await Promise.all([
-    db.collection('scheduled_jobs').countDocuments({ status: 'completed', ...sjFilter(filter) }),
-    db.collection('scheduled_jobs').countDocuments({ status: 'failed', ...sjFilter(filter) }),
+    db.collection('scheduled_jobs').countDocuments({ status: 'completed', ...sjFilter(filter, ws) }),
+    db.collection('scheduled_jobs').countDocuments({ status: 'failed', ...sjFilter(filter, ws) }),
     db.collection('posts').countDocuments({ type: 'immediate', status: { $in: ['published', 'partial'] }, ...wsIp, ...ipFilter(filter) }),
     db.collection('posts').countDocuments({ type: 'immediate', status: 'failed', ...wsIp, ...ipFilter(filter) }),
-    db.collection('scheduled_jobs').countDocuments({ status: 'completed', completedAt: { $gte: sevenDaysAgo }, ...sjFilter(filter) }),
+    db.collection('scheduled_jobs').countDocuments({ status: 'completed', completedAt: { $gte: sevenDaysAgo }, ...sjFilter(filter, ws) }),
     db.collection('posts').countDocuments({ type: 'immediate', publishedAt: { $gte: sevenDaysAgo }, ...wsIp, ...ipFilter(filter) }),
     // Platform breakdown from scheduled_jobs destinations
     db.collection('scheduled_jobs').aggregate([
-      { $match: { status: 'completed', ...sjFilter(filter) } },
+      { $match: { status: 'completed', ...sjFilter(filter, ws) } },
       { $unwind: '$destinations' },
       ...unwindFilter,
       { $group: { _id: '$destinations.platform', count: { $sum: 1 } } },
@@ -2621,7 +2626,7 @@ app.get('/analytics/summary', async (request) => {
     ]).toArray(),
     // Activity by day from scheduled_jobs (using completedAt)
     db.collection('scheduled_jobs').aggregate([
-      { $match: { status: 'completed', completedAt: { $gte: thirtyDaysAgo }, ...sjFilter(filter) } },
+      { $match: { status: 'completed', completedAt: { $gte: thirtyDaysAgo }, ...sjFilter(filter, ws) } },
       { $group: { _id: { $dateToString: { format: '%Y-%m-%d', date: '$completedAt' } }, count: { $sum: 1 } } },
       { $sort: { _id: 1 } },
     ]).toArray(),
@@ -2660,7 +2665,7 @@ app.get('/analytics/posts', async (request) => {
   const filter = parseAccountFilter(request.query.account);
   const db = await getDb();
 
-  const sjMatch = { status: { $in: ['completed', 'failed'] }, ...sjFilter(filter) };
+  const sjMatch = { status: { $in: ['completed', 'failed'] }, ...sjFilter(filter, ws) };
   const ipMatch = { type: 'immediate', workspaceId: ws, ...ipFilter(filter) };
 
   const [scheduledJobs, immediatePosts, schedTotal, immTotal] = await Promise.all([
@@ -2725,7 +2730,7 @@ app.get('/analytics/export', async (request, reply) => {
 
   const sjMatch = {
     status: { $in: ['completed', 'failed'] },
-    ...sjFilter(filter),
+    ...sjFilter(filter, ws),
     ...(month ? { completedAt: dateFilter } : {}),
   };
   const ipMatch = {

+ 11 - 10
services/instagram/index.js

@@ -3,6 +3,7 @@ const axios = require('axios');
 const BasePlatformService = require('./utils/BasePlatformService');
 const { getDb } = require('./utils/MongoDBConnector');
 const { decryptToken, warnIfNoKey } = require('./utils/crypto');
+const { getWorkspaceCredential } = require('./utils/credentials');
 
 const GRAPH_API = 'https://graph.facebook.com/v22.0';
 
@@ -13,10 +14,10 @@ class InstagramService extends BasePlatformService {
 
   // Read selected Instagram Business Accounts from MongoDB.
   // Falls back to env vars for backwards compatibility.
-  async _getAccounts() {
+  async _getAccounts(workspaceId = 'default') {
     try {
       const db = await getDb();
-      const cred = await db.collection('platform_credentials').findOne({ _id: 'instagram' });
+      const cred = await getWorkspaceCredential(db, 'instagram', workspaceId);
       const dbAccounts = (cred?.accounts || []).filter((a) => a.selected);
       if (dbAccounts.length > 0) {
         return dbAccounts.map((a) => ({ ...a, accessToken: decryptToken(a.accessToken) })).filter((a) => a.accessToken);
@@ -32,8 +33,8 @@ class InstagramService extends BasePlatformService {
     return [];
   }
 
-  async getStatus() {
-    const accounts = await this._getAccounts();
+  async getStatus(workspaceId = 'default') {
+    const accounts = await this._getAccounts(workspaceId);
     if (accounts.length === 0) {
       return { connected: false, platform: 'instagram', error: 'No Instagram accounts connected — use Settings to connect via Facebook OAuth' };
     }
@@ -58,8 +59,8 @@ class InstagramService extends BasePlatformService {
     }
   }
 
-  async fetchFeed({ limit = 20 } = {}) {
-    const accounts = await this._getAccounts();
+  async fetchFeed({ limit = 20, workspaceId = 'default' } = {}) {
+    const accounts = await this._getAccounts(workspaceId);
     if (accounts.length === 0) throw new Error('No Instagram accounts connected');
 
     const allItems = [];
@@ -106,8 +107,8 @@ class InstagramService extends BasePlatformService {
       const col = db.collection('feeds');
       for (const item of allItems) {
         await col.updateOne(
-          { platform: 'instagram', originalId: item.originalId },
-          { $set: item },
+          { platform: 'instagram', originalId: item.originalId, workspaceId },
+          { $set: { ...item, workspaceId } },
           { upsert: true }
         );
       }
@@ -119,8 +120,8 @@ class InstagramService extends BasePlatformService {
   }
 
   // Instagram requires media (image_url or video_url) — text-only posts are not supported.
-  async publishPost({ content, imageUrl, videoUrl, accountId, firstComment } = {}) {
-    const allAccounts = await this._getAccounts();
+  async publishPost({ content, imageUrl, videoUrl, accountId, firstComment, workspaceId = 'default' } = {}) {
+    const allAccounts = await this._getAccounts(workspaceId);
     if (allAccounts.length === 0) throw new Error('No Instagram accounts connected');
 
     if (!imageUrl && !videoUrl) {

+ 13 - 11
services/pinterest/index.js

@@ -3,6 +3,7 @@ const axios = require('axios');
 const BasePlatformService = require('./utils/BasePlatformService');
 const { getDb } = require('./utils/MongoDBConnector');
 const { decryptToken, warnIfNoKey } = require('./utils/crypto');
+const { getWorkspaceCredential } = require('./utils/credentials');
 
 const PINTEREST_API = 'https://api.pinterest.com/v5';
 
@@ -11,10 +12,10 @@ class PinterestService extends BasePlatformService {
     super('pinterest');
   }
 
-  async _getAccount() {
+  async _getAccount(workspaceId = 'default') {
     try {
       const db = await getDb();
-      const cred = await db.collection('platform_credentials').findOne({ _id: 'pinterest' });
+      const cred = await getWorkspaceCredential(db, 'pinterest', workspaceId);
       if (cred?.accessToken) {
         return { ...cred, accessToken: decryptToken(cred.accessToken) };
       }
@@ -22,8 +23,8 @@ class PinterestService extends BasePlatformService {
     return null;
   }
 
-  async getStatus() {
-    const account = await this._getAccount();
+  async getStatus(workspaceId = 'default') {
+    const account = await this._getAccount(workspaceId);
     if (!account?.accessToken) {
       return { connected: false, platform: 'pinterest', error: 'Not connected — use Settings to connect via Pinterest OAuth' };
     }
@@ -46,8 +47,8 @@ class PinterestService extends BasePlatformService {
     }
   }
 
-  async fetchFeed({ limit = 25 } = {}) {
-    const account = await this._getAccount();
+  async fetchFeed({ limit = 25, workspaceId = 'default' } = {}) {
+    const account = await this._getAccount(workspaceId);
     if (!account?.accessToken) throw new Error('Pinterest not connected');
 
     const allItems = [];
@@ -90,8 +91,8 @@ class PinterestService extends BasePlatformService {
       const col = db.collection('feeds');
       for (const item of allItems) {
         await col.updateOne(
-          { platform: 'pinterest', originalId: item.originalId },
-          { $set: item },
+          { platform: 'pinterest', originalId: item.originalId, workspaceId },
+          { $set: { ...item, workspaceId } },
           { upsert: true }
         );
       }
@@ -102,8 +103,8 @@ class PinterestService extends BasePlatformService {
     return allItems;
   }
 
-  async publishPost({ content, imageUrl, accountId: boardId } = {}) {
-    const account = await this._getAccount();
+  async publishPost({ content, imageUrl, accountId: boardId, workspaceId = 'default' } = {}) {
+    const account = await this._getAccount(workspaceId);
     if (!account?.accessToken) throw new Error('Pinterest not connected');
     if (!boardId) throw new Error('boardId is required for Pinterest — select a board as destination');
     if (!imageUrl) throw new Error('Pinterest requires an image URL');
@@ -144,7 +145,8 @@ const service = new PinterestService();
 // Returns selected boards from DB (used by gateway/compose to list destinations)
 service.app.get('/boards', async (request, reply) => {
   try {
-    const account = await service._getAccount();
+    const workspaceId = request.headers['x-workspace-id'] || 'default';
+    const account = await service._getAccount(workspaceId);
     if (!account) return { boards: [] };
     const selected = (account.boards || []).filter((b) => b.selected);
     return { boards: selected };

+ 24 - 8
services/scheduler/index.js

@@ -31,7 +31,7 @@ let redis;
 async function processPostJob(job) {
   // destinations: [{ platform, accountId?, imageUrl?, videoUrl?, link? }]
   // Falls back to legacy { platforms: string[] } format
-  const { postId, content, destinations, platforms, media = [], firstComment } = job.data;
+  const { postId, content, destinations, platforms, media = [], firstComment, workspaceId = 'default' } = job.data;
   // Ensure every post has a stable ID for analytics tracking
   const effectivePostId = postId || randomUUID();
 
@@ -60,7 +60,11 @@ async function processPostJob(job) {
       continue;
     }
     try {
-      const response = await axios.post(`${serviceUrl}/post`, { content, accountId, imageUrl, videoUrl, link, media, firstComment: firstComment?.trim() || undefined }, { timeout: 30000 });
+      const response = await axios.post(
+        `${serviceUrl}/post`,
+        { content, accountId, imageUrl, videoUrl, link, media, firstComment: firstComment?.trim() || undefined },
+        { timeout: 30000, headers: { 'X-Workspace-Id': workspaceId } }
+      );
       results[resultKey] = { success: true, ...response.data.result };
     } catch (err) {
       results[resultKey] = { success: false, error: err.message };
@@ -81,6 +85,7 @@ async function processPostJob(job) {
         status: postStatus,
         publishedAt: new Date(),
         platformResults: results,
+        workspaceId,
       },
       $setOnInsert: { createdAt: new Date() },
     },
@@ -132,6 +137,7 @@ app.get('/health', async () => ({ status: 'ok', service: 'scheduler' }));
 // Legacy { platforms: string[] } still accepted for backwards compatibility.
 app.post('/schedule', async (request, reply) => {
   const { postId, content, destinations, platforms, scheduledAt, media = [], firstComment } = request.body;
+  const workspaceId = request.headers['x-workspace-id'] || 'default';
 
   const destList = destinations || (platforms || []).map((p) => ({ platform: p }));
 
@@ -146,7 +152,7 @@ app.post('/schedule', async (request, reply) => {
 
   const job = await postQueue.add(
     'scheduled-post',
-    { postId, content, destinations: destList, media, firstComment: firstComment?.trim() || undefined },
+    { postId, content, destinations: destList, media, firstComment: firstComment?.trim() || undefined, workspaceId },
     { delay, attempts: 3, backoff: { type: 'exponential', delay: 60000 } }
   );
 
@@ -161,6 +167,7 @@ app.post('/schedule', async (request, reply) => {
     attempts: 0,
     maxAttempts: 3,
     bullJobId: String(job.id),
+    workspaceId,
     createdAt: new Date(),
   });
 
@@ -170,10 +177,13 @@ app.post('/schedule', async (request, reply) => {
 // Zamanlanmış görevleri listele
 app.get('/jobs', async (request) => {
   const { status = 'pending' } = request.query;
+  const workspaceId = request.headers['x-workspace-id'] || 'default';
   const db = await getDb();
+  // Include legacy jobs without workspaceId (backwards compat)
+  const filter = { status, $or: [{ workspaceId }, { workspaceId: { $exists: false } }] };
   const jobs = await db
     .collection('scheduled_jobs')
-    .find({ status })
+    .find(filter)
     .sort({ scheduledAt: 1 })
     .toArray();
   return { success: true, count: jobs.length, jobs };
@@ -182,12 +192,18 @@ app.get('/jobs', async (request) => {
 // Görevi iptal et
 app.delete('/jobs/:jobId', async (request, reply) => {
   const { jobId } = request.params;
-  const job = await postQueue.getJob(jobId);
-  if (!job) return reply.code(404).send({ error: 'Job bulunamadı' });
-
-  await job.remove();
+  const workspaceId = request.headers['x-workspace-id'] || 'default';
 
   const db = await getDb();
+  const jobDoc = await db.collection('scheduled_jobs').findOne({
+    bullJobId: jobId,
+    $or: [{ workspaceId }, { workspaceId: { $exists: false } }],
+  });
+  if (!jobDoc) return reply.code(404).send({ error: 'Job bulunamadı' });
+
+  const job = await postQueue.getJob(jobId);
+  if (job) await job.remove();
+
   await db.collection('scheduled_jobs').updateOne(
     { bullJobId: jobId },
     { $set: { status: 'cancelled' } }

+ 12 - 11
services/tiktok/index.js

@@ -3,6 +3,7 @@ const axios = require('axios');
 const BasePlatformService = require('./utils/BasePlatformService');
 const { getDb } = require('./utils/MongoDBConnector');
 const { encryptToken, decryptToken, warnIfNoKey } = require('./utils/crypto');
+const { getWorkspaceCredential } = require('./utils/credentials');
 
 const TIKTOK_API = 'https://open.tiktokapis.com/v2';
 const TOKEN_URL = `${TIKTOK_API}/oauth/token/`;
@@ -12,11 +13,11 @@ class TikTokService extends BasePlatformService {
     super('tiktok');
   }
 
-  async _getAccount() {
+  async _getAccount(workspaceId = 'default') {
     try {
       const db = await getDb();
       const [cred, appCred] = await Promise.all([
-        db.collection('platform_credentials').findOne({ _id: 'tiktok' }),
+        getWorkspaceCredential(db, 'tiktok', workspaceId),
         db.collection('platform_credentials').findOne({ _id: 'tiktok_app' }),
       ]);
 
@@ -44,7 +45,7 @@ class TikTokService extends BasePlatformService {
               : cred.refreshExpiry;
 
             await db.collection('platform_credentials').updateOne(
-              { _id: 'tiktok' },
+              { _id: cred._id },
               {
                 $set: {
                   accessToken: encryptToken(access_token),
@@ -69,8 +70,8 @@ class TikTokService extends BasePlatformService {
     return null;
   }
 
-  async getStatus() {
-    const account = await this._getAccount();
+  async getStatus(workspaceId = 'default') {
+    const account = await this._getAccount(workspaceId);
     if (!account?.accessToken) {
       return { connected: false, platform: 'tiktok', error: 'Not connected — use Settings to connect via TikTok OAuth' };
     }
@@ -93,8 +94,8 @@ class TikTokService extends BasePlatformService {
     }
   }
 
-  async fetchFeed({ limit = 20 } = {}) {
-    const account = await this._getAccount();
+  async fetchFeed({ limit = 20, workspaceId = 'default' } = {}) {
+    const account = await this._getAccount(workspaceId);
     if (!account?.accessToken) throw new Error('TikTok not connected');
 
     const res = await axios.post(
@@ -137,8 +138,8 @@ class TikTokService extends BasePlatformService {
       const col = db.collection('feeds');
       for (const item of items) {
         await col.updateOne(
-          { platform: 'tiktok', originalId: item.originalId },
-          { $set: item },
+          { platform: 'tiktok', originalId: item.originalId, workspaceId },
+          { $set: { ...item, workspaceId } },
           { upsert: true }
         );
       }
@@ -149,8 +150,8 @@ class TikTokService extends BasePlatformService {
     return items;
   }
 
-  async publishPost({ content, videoUrl } = {}) {
-    const account = await this._getAccount();
+  async publishPost({ content, videoUrl, workspaceId = 'default' } = {}) {
+    const account = await this._getAccount(workspaceId);
     if (!account?.accessToken) throw new Error('TikTok not connected');
     if (!videoUrl) throw new Error('TikTok requires a video URL — text-only posts are not supported');
 

+ 7 - 4
services/utils/BasePlatformService.js

@@ -22,13 +22,15 @@ class BasePlatformService extends RabbitMQConnector {
 
   /** HTTP route'ları kaydet — platform servisleri override edebilir */
   _setupRoutes() {
-    this.app.get('/status', async () => {
-      return this.getStatus();
+    this.app.get('/status', async (request) => {
+      const workspaceId = request.headers['x-workspace-id'] || 'default';
+      return this.getStatus(workspaceId);
     });
 
     this.app.get('/feed', async (request, reply) => {
+      const workspaceId = request.headers['x-workspace-id'] || 'default';
       try {
-        const items = await this.fetchFeed(request.query);
+        const items = await this.fetchFeed({ ...request.query, workspaceId });
         return { success: true, platform: this.platformName, count: items.length, items };
       } catch (err) {
         reply.code(500).send({ success: false, error: err.message });
@@ -36,8 +38,9 @@ class BasePlatformService extends RabbitMQConnector {
     });
 
     this.app.post('/post', async (request, reply) => {
+      const workspaceId = request.headers['x-workspace-id'] || 'default';
       try {
-        const result = await this.publishPost(request.body);
+        const result = await this.publishPost({ ...request.body, workspaceId });
         return { success: true, platform: this.platformName, result };
       } catch (err) {
         reply.code(500).send({ success: false, error: err.message });

+ 12 - 0
services/utils/credentials.js

@@ -0,0 +1,12 @@
+/**
+ * Workspace-aware credential lookup.
+ * Tries `${workspaceId}:${type}` first, falls back to bare `${type}` for
+ * backwards compatibility with pre-migration documents.
+ */
+async function getWorkspaceCredential(db, type, workspaceId = 'default') {
+  const scoped = await db.collection('platform_credentials').findOne({ _id: `${workspaceId}:${type}` });
+  if (scoped) return scoped;
+  return db.collection('platform_credentials').findOne({ _id: type });
+}
+
+module.exports = { getWorkspaceCredential };