@realtimex/email-automator 2.28.0 → 2.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/server.ts +1 -120
- package/api/src/middleware/auth.ts +15 -0
- package/api/src/routes/auth.ts +3 -48
- package/api/src/services/encryptionInit.ts +156 -0
- package/api/src/services/processor.ts +156 -145
- package/dist/api/server.js +2 -105
- package/dist/api/src/middleware/auth.js +12 -0
- package/dist/api/src/routes/auth.js +3 -45
- package/dist/api/src/services/encryptionInit.js +135 -0
- package/dist/api/src/services/processor.js +150 -139
- package/dist/assets/{index-DpVG-8N2.js → index-Xu8f_mT0.js} +3 -3
- package/dist/index.html +1 -1
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ import { config } from '../config/index.js';
|
|
|
5
5
|
import { getGmailService, GmailMessage } from './gmail.js';
|
|
6
6
|
import { getMicrosoftService, OutlookMessage } from './microsoft.js';
|
|
7
7
|
import { getImapService, EmailMessage as ImapMessage } from './imap-service.js';
|
|
8
|
+
import { initializePersistenceEncryption, isEncryptionReady } from './encryptionInit.js';
|
|
8
9
|
import { getIntelligenceService, EmailAnalysis, ContextAwareAnalysis, RuleContext } from './intelligence.js';
|
|
9
10
|
import { getStorageService } from './storage.js';
|
|
10
11
|
import { generateEmailFilename } from '../utils/filename.js';
|
|
@@ -144,179 +145,189 @@ export class EmailProcessorService {
|
|
|
144
145
|
async syncAccount(accountId: string, userId: string): Promise<ProcessingResult> {
|
|
145
146
|
const result: ProcessingResult = { processed: 0, deleted: 0, drafted: 0, errors: 0 };
|
|
146
147
|
|
|
147
|
-
// Reset stop request flag at the start of a manual sync
|
|
148
|
-
await this.resetStopRequest(userId);
|
|
149
|
-
|
|
150
|
-
// Zero-Config UX: Auto-seed default rules for new users (self-healing)
|
|
151
148
|
try {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
logger.info(`Seeded default rules for user ${userId}`);
|
|
149
|
+
// Ensure encryption is ready (especially for background syncs)
|
|
150
|
+
if (!isEncryptionReady()) {
|
|
151
|
+
await initializePersistenceEncryption(this.supabase);
|
|
156
152
|
}
|
|
157
|
-
} catch (error) {
|
|
158
|
-
// Don't fail sync if pack installation fails
|
|
159
|
-
logger.error('Failed to auto-install Universal Pack', error);
|
|
160
|
-
}
|
|
161
153
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
.from('processing_logs')
|
|
165
|
-
.insert({
|
|
166
|
-
user_id: userId,
|
|
167
|
-
account_id: accountId,
|
|
168
|
-
status: 'running',
|
|
169
|
-
})
|
|
170
|
-
.select()
|
|
171
|
-
.single();
|
|
154
|
+
// Reset stop request flag at the start of a manual sync
|
|
155
|
+
await this.resetStopRequest(userId);
|
|
172
156
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
throw new Error('Account not found or access denied');
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
logger.info('Retrieved account settings', {
|
|
187
|
-
accountId: account.id,
|
|
188
|
-
sync_start_date: account.sync_start_date,
|
|
189
|
-
last_sync_checkpoint: account.last_sync_checkpoint
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
// Refresh token if needed
|
|
193
|
-
let refreshedAccount = account;
|
|
194
|
-
if (account.provider === 'gmail') {
|
|
195
|
-
refreshedAccount = await this.gmailService.refreshTokenIfNeeded(this.supabase, account);
|
|
196
|
-
} else if (account.provider === 'outlook') {
|
|
197
|
-
refreshedAccount = await this.microsoftService.refreshTokenIfNeeded(this.supabase, account);
|
|
198
|
-
} else if (account.provider === 'imap') {
|
|
199
|
-
// IMAP doesn't need token refresh, but we could verify connection here if we wanted
|
|
200
|
-
refreshedAccount = account;
|
|
157
|
+
// Zero-Config UX: Auto-seed default rules for new users (self-healing)
|
|
158
|
+
try {
|
|
159
|
+
const defaultRuleService = new DefaultRuleService(this.supabase);
|
|
160
|
+
const { installed } = await defaultRuleService.ensureDefaultRules(userId);
|
|
161
|
+
if (installed) {
|
|
162
|
+
logger.info(`Seeded default rules for user ${userId}`);
|
|
163
|
+
}
|
|
164
|
+
} catch (error) {
|
|
165
|
+
// Don't fail sync if pack installation fails
|
|
166
|
+
logger.error('Failed to auto-install Universal Pack', error);
|
|
201
167
|
}
|
|
202
168
|
|
|
203
|
-
//
|
|
204
|
-
await this.supabase
|
|
205
|
-
.from('
|
|
206
|
-
.
|
|
207
|
-
|
|
208
|
-
|
|
169
|
+
// Create processing log
|
|
170
|
+
const { data: log } = await this.supabase
|
|
171
|
+
.from('processing_logs')
|
|
172
|
+
.insert({
|
|
173
|
+
user_id: userId,
|
|
174
|
+
account_id: accountId,
|
|
175
|
+
status: 'running',
|
|
209
176
|
})
|
|
210
|
-
.
|
|
211
|
-
|
|
212
|
-
// Fetch user's rules
|
|
213
|
-
const { data: rules } = await this.supabase
|
|
214
|
-
.from('rules')
|
|
215
|
-
.select('*')
|
|
216
|
-
.eq('user_id', userId)
|
|
217
|
-
.eq('is_enabled', true);
|
|
218
|
-
|
|
219
|
-
// Fetch user settings for AI preferences
|
|
220
|
-
const { data: settings } = await this.supabase
|
|
221
|
-
.from('user_settings')
|
|
222
|
-
.select('*')
|
|
223
|
-
.eq('user_id', userId)
|
|
177
|
+
.select()
|
|
224
178
|
.single();
|
|
225
179
|
|
|
226
|
-
const eventLogger = log ? new EventLogger(this.supabase, log.id) : null;
|
|
227
|
-
if (eventLogger) await eventLogger.info('Running', 'Starting sync process');
|
|
228
|
-
|
|
229
|
-
// --- STOP CHECK ---
|
|
230
|
-
if (await this.checkStopRequested(userId, eventLogger)) return result;
|
|
231
|
-
|
|
232
|
-
// Process based on provider
|
|
233
180
|
try {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
181
|
+
// Fetch account
|
|
182
|
+
const { data: account, error: accError } = await this.supabase
|
|
183
|
+
.from('email_accounts')
|
|
184
|
+
.select('*')
|
|
185
|
+
.eq('id', accountId)
|
|
186
|
+
.eq('user_id', userId)
|
|
187
|
+
.single();
|
|
188
|
+
|
|
189
|
+
if (accError || !account) {
|
|
190
|
+
throw new Error('Account not found or access denied');
|
|
240
191
|
}
|
|
241
|
-
} catch (providerError) {
|
|
242
|
-
const providerName = refreshedAccount.provider === 'gmail' ? 'Gmail' :
|
|
243
|
-
refreshedAccount.provider === 'outlook' ? 'Outlook' :
|
|
244
|
-
'IMAP';
|
|
245
|
-
throw new Error(`${providerName} Sync Error: ${providerError instanceof Error ? providerError.message : String(providerError)}`);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// After processing new emails, run retention rules for this account
|
|
249
|
-
if (await this.checkStopRequested(userId, eventLogger)) return result;
|
|
250
|
-
await this.runRetentionRules(refreshedAccount, rules || [], settings, result, eventLogger);
|
|
251
192
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
193
|
+
logger.info('Retrieved account settings', {
|
|
194
|
+
accountId: account.id,
|
|
195
|
+
sync_start_date: account.sync_start_date,
|
|
196
|
+
last_sync_checkpoint: account.last_sync_checkpoint
|
|
197
|
+
});
|
|
256
198
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
if (
|
|
260
|
-
await
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
199
|
+
// Refresh token if needed
|
|
200
|
+
let refreshedAccount = account;
|
|
201
|
+
if (account.provider === 'gmail') {
|
|
202
|
+
refreshedAccount = await this.gmailService.refreshTokenIfNeeded(this.supabase, account);
|
|
203
|
+
} else if (account.provider === 'outlook') {
|
|
204
|
+
refreshedAccount = await this.microsoftService.refreshTokenIfNeeded(this.supabase, account);
|
|
205
|
+
} else if (account.provider === 'imap') {
|
|
206
|
+
// IMAP doesn't need token refresh, but we could verify connection here if we wanted
|
|
207
|
+
refreshedAccount = account;
|
|
266
208
|
}
|
|
267
209
|
|
|
210
|
+
// Update status to syncing
|
|
268
211
|
await this.supabase
|
|
269
|
-
.from('
|
|
212
|
+
.from('email_accounts')
|
|
270
213
|
.update({
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
emails_processed: result.processed,
|
|
274
|
-
emails_deleted: result.deleted,
|
|
275
|
-
emails_drafted: result.drafted,
|
|
214
|
+
last_sync_status: 'syncing',
|
|
215
|
+
last_sync_at: new Date().toISOString()
|
|
276
216
|
})
|
|
277
|
-
.eq('id',
|
|
278
|
-
|
|
217
|
+
.eq('id', accountId);
|
|
218
|
+
|
|
219
|
+
// Fetch user's rules
|
|
220
|
+
const { data: rules } = await this.supabase
|
|
221
|
+
.from('rules')
|
|
222
|
+
.select('*')
|
|
223
|
+
.eq('user_id', userId)
|
|
224
|
+
.eq('is_enabled', true);
|
|
225
|
+
|
|
226
|
+
// Fetch user settings for AI preferences
|
|
227
|
+
const { data: settings } = await this.supabase
|
|
228
|
+
.from('user_settings')
|
|
229
|
+
.select('*')
|
|
230
|
+
.eq('user_id', userId)
|
|
231
|
+
.single();
|
|
232
|
+
|
|
233
|
+
const eventLogger = log ? new EventLogger(this.supabase, log.id) : null;
|
|
234
|
+
if (eventLogger) await eventLogger.info('Running', 'Starting sync process');
|
|
235
|
+
|
|
236
|
+
// --- STOP CHECK ---
|
|
237
|
+
if (await this.checkStopRequested(userId, eventLogger)) return result;
|
|
238
|
+
|
|
239
|
+
// Process based on provider
|
|
240
|
+
try {
|
|
241
|
+
if (refreshedAccount.provider === 'gmail') {
|
|
242
|
+
await this.processGmailAccount(refreshedAccount, rules || [], settings, result, eventLogger);
|
|
243
|
+
} else if (refreshedAccount.provider === 'outlook') {
|
|
244
|
+
await this.processOutlookAccount(refreshedAccount, rules || [], settings, result, eventLogger);
|
|
245
|
+
} else if (refreshedAccount.provider === 'imap') {
|
|
246
|
+
await this.processImapAccount(refreshedAccount, rules || [], settings, result, eventLogger);
|
|
247
|
+
}
|
|
248
|
+
} catch (providerError) {
|
|
249
|
+
const providerName = refreshedAccount.provider === 'gmail' ? 'Gmail' :
|
|
250
|
+
refreshedAccount.provider === 'outlook' ? 'Outlook' :
|
|
251
|
+
'IMAP';
|
|
252
|
+
throw new Error(`${providerName} Sync Error: ${providerError instanceof Error ? providerError.message : String(providerError)}`);
|
|
253
|
+
}
|
|
279
254
|
|
|
280
|
-
|
|
281
|
-
.
|
|
282
|
-
.
|
|
283
|
-
last_sync_status: 'success',
|
|
284
|
-
last_sync_error: null,
|
|
285
|
-
sync_start_date: null // Clear manual override once used successfully
|
|
286
|
-
})
|
|
287
|
-
.eq('id', accountId);
|
|
255
|
+
// After processing new emails, run retention rules for this account
|
|
256
|
+
if (await this.checkStopRequested(userId, eventLogger)) return result;
|
|
257
|
+
await this.runRetentionRules(refreshedAccount, rules || [], settings, result, eventLogger);
|
|
288
258
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
259
|
+
// Wait for background worker to process the queue (ensure sync is fully complete before event)
|
|
260
|
+
await this.processQueue(userId, settings, result).catch(err =>
|
|
261
|
+
logger.error('Background worker failed', err)
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// Update log and account on success
|
|
265
|
+
if (log) {
|
|
266
|
+
if (eventLogger) {
|
|
267
|
+
await eventLogger.success('Finished', 'Sync run completed', {
|
|
268
|
+
total_processed: result.processed,
|
|
269
|
+
deleted: result.deleted,
|
|
270
|
+
drafted: result.drafted,
|
|
271
|
+
errors: result.errors
|
|
272
|
+
});
|
|
273
|
+
}
|
|
292
274
|
|
|
293
|
-
|
|
275
|
+
await this.supabase
|
|
276
|
+
.from('processing_logs')
|
|
277
|
+
.update({
|
|
278
|
+
status: 'success',
|
|
279
|
+
completed_at: new Date().toISOString(),
|
|
280
|
+
emails_processed: result.processed,
|
|
281
|
+
emails_deleted: result.deleted,
|
|
282
|
+
emails_drafted: result.drafted,
|
|
283
|
+
})
|
|
284
|
+
.eq('id', log.id);
|
|
285
|
+
}
|
|
294
286
|
|
|
295
|
-
if (log) {
|
|
296
287
|
await this.supabase
|
|
297
|
-
.from('
|
|
288
|
+
.from('email_accounts')
|
|
298
289
|
.update({
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
290
|
+
last_sync_status: 'success',
|
|
291
|
+
last_sync_error: null,
|
|
292
|
+
sync_start_date: null // Clear manual override once used successfully
|
|
302
293
|
})
|
|
303
|
-
.eq('id',
|
|
304
|
-
}
|
|
294
|
+
.eq('id', accountId);
|
|
305
295
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
.
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
296
|
+
logger.info('Sync completed and override cleared', { accountId, ...result });
|
|
297
|
+
} catch (error) {
|
|
298
|
+
logger.error('Sync failed', error, { accountId });
|
|
299
|
+
|
|
300
|
+
const errMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
301
|
+
|
|
302
|
+
if (log) {
|
|
303
|
+
await this.supabase
|
|
304
|
+
.from('processing_logs')
|
|
305
|
+
.update({
|
|
306
|
+
status: 'failed',
|
|
307
|
+
completed_at: new Date().toISOString(),
|
|
308
|
+
error_message: errMsg,
|
|
309
|
+
})
|
|
310
|
+
.eq('id', log.id);
|
|
311
|
+
}
|
|
313
312
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
313
|
+
await this.supabase
|
|
314
|
+
.from('email_accounts')
|
|
315
|
+
.update({
|
|
316
|
+
last_sync_status: 'error',
|
|
317
|
+
last_sync_error: errMsg
|
|
318
|
+
})
|
|
319
|
+
.eq('id', accountId);
|
|
320
|
+
|
|
321
|
+
// If it's a fatal setup error (e.g. Account not found), throw it
|
|
322
|
+
if (errMsg.includes('Account not found') || errMsg.includes('access denied')) {
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
318
325
|
|
|
319
|
-
|
|
326
|
+
// Otherwise, increment error count and return partial results
|
|
327
|
+
result.errors++;
|
|
328
|
+
}
|
|
329
|
+
} catch (globalError) {
|
|
330
|
+
logger.error('Global sync execution failed', globalError);
|
|
320
331
|
result.errors++;
|
|
321
332
|
}
|
|
322
333
|
|
package/dist/api/server.js
CHANGED
|
@@ -11,112 +11,9 @@ import { errorHandler } from './src/middleware/errorHandler.js';
|
|
|
11
11
|
import { apiRateLimit } from './src/middleware/rateLimit.js';
|
|
12
12
|
import routes from './src/routes/index.js';
|
|
13
13
|
import { logger } from './src/utils/logger.js';
|
|
14
|
-
import { getServerSupabase
|
|
14
|
+
import { getServerSupabase } from './src/services/supabase.js';
|
|
15
15
|
import { startScheduler, stopScheduler } from './src/services/scheduler.js';
|
|
16
|
-
import {
|
|
17
|
-
// Initialize Persistence Encryption
|
|
18
|
-
// NOTE: In RealTimeX Desktop sandbox, all config is stored in Supabase (no .env files)
|
|
19
|
-
async function initializePersistenceEncryption() {
|
|
20
|
-
try {
|
|
21
|
-
const supabase = getServiceRoleSupabase();
|
|
22
|
-
if (!supabase) {
|
|
23
|
-
// BYOK mode: Supabase not configured at startup (credentials come via HTTP headers)
|
|
24
|
-
// Generate encryption key anyway and keep it in memory
|
|
25
|
-
logger.info('Supabase not configured yet (BYOK mode)');
|
|
26
|
-
logger.info('Generating encryption key in memory - will persist when Supabase becomes available');
|
|
27
|
-
const newKey = crypto.randomBytes(32).toString('hex');
|
|
28
|
-
setEncryptionKey(newKey);
|
|
29
|
-
logger.info('✓ Encryption key generated and loaded in memory');
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
// 1. Check if ANY user has an encryption key stored
|
|
33
|
-
// In sandbox mode, encryption key is always in database
|
|
34
|
-
const { data: users, error } = await supabase
|
|
35
|
-
.from('user_settings')
|
|
36
|
-
.select('user_id, encryption_key')
|
|
37
|
-
.not('encryption_key', 'is', null)
|
|
38
|
-
.limit(1);
|
|
39
|
-
if (error) {
|
|
40
|
-
logger.warn('Failed to query user_settings for encryption key', { error });
|
|
41
|
-
// Still generate a key for in-memory use
|
|
42
|
-
const newKey = crypto.randomBytes(32).toString('hex');
|
|
43
|
-
setEncryptionKey(newKey);
|
|
44
|
-
logger.info('✓ Generated fallback encryption key due to DB error');
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
if (users && users.length > 0 && users[0].encryption_key) {
|
|
48
|
-
// Found a persisted key in database
|
|
49
|
-
logger.info('✓ Loaded encryption key from database');
|
|
50
|
-
setEncryptionKey(users[0].encryption_key);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
// 2. No key in database - auto-generate and persist
|
|
54
|
-
// This happens on first run or after fresh database setup
|
|
55
|
-
logger.info('No encryption key found in database, generating new key...');
|
|
56
|
-
const newKey = crypto.randomBytes(32).toString('hex');
|
|
57
|
-
setEncryptionKey(newKey);
|
|
58
|
-
// 3. Persist to all existing users in database
|
|
59
|
-
const { data: allUsers } = await supabase
|
|
60
|
-
.from('user_settings')
|
|
61
|
-
.select('user_id')
|
|
62
|
-
.limit(100);
|
|
63
|
-
if (allUsers && allUsers.length > 0) {
|
|
64
|
-
logger.info(`Saving encryption key to database for ${allUsers.length} user(s)...`);
|
|
65
|
-
// Update all users with the new key
|
|
66
|
-
const updates = allUsers.map(user => supabase
|
|
67
|
-
.from('user_settings')
|
|
68
|
-
.update({ encryption_key: newKey })
|
|
69
|
-
.eq('user_id', user.user_id));
|
|
70
|
-
await Promise.all(updates);
|
|
71
|
-
logger.info('✓ Encryption key saved to database');
|
|
72
|
-
}
|
|
73
|
-
else {
|
|
74
|
-
logger.info('No users found yet, encryption key loaded in memory');
|
|
75
|
-
logger.info('Key will be persisted when users are created');
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
catch (err) {
|
|
79
|
-
logger.error('Error initializing encryption:', err);
|
|
80
|
-
// Always ensure we have a key, even if there was an error
|
|
81
|
-
if (!getEncryptionKeyHex()) {
|
|
82
|
-
logger.warn('Generating emergency fallback encryption key');
|
|
83
|
-
const fallbackKey = crypto.randomBytes(32).toString('hex');
|
|
84
|
-
setEncryptionKey(fallbackKey);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
// Periodic sync: Persist in-memory encryption key to users without one
|
|
89
|
-
// This handles the "first user" case and any users created before key was persisted
|
|
90
|
-
async function syncEncryptionKeyToUsers() {
|
|
91
|
-
try {
|
|
92
|
-
const supabase = getServiceRoleSupabase();
|
|
93
|
-
if (!supabase)
|
|
94
|
-
return;
|
|
95
|
-
const currentKey = getEncryptionKeyHex();
|
|
96
|
-
if (!currentKey)
|
|
97
|
-
return; // No key in memory yet
|
|
98
|
-
// Find users without encryption key
|
|
99
|
-
const { data: usersWithoutKey, error } = await supabase
|
|
100
|
-
.from('user_settings')
|
|
101
|
-
.select('user_id')
|
|
102
|
-
.is('encryption_key', null)
|
|
103
|
-
.limit(100);
|
|
104
|
-
if (error || !usersWithoutKey || usersWithoutKey.length === 0) {
|
|
105
|
-
return; // No users to update
|
|
106
|
-
}
|
|
107
|
-
logger.info(`Found ${usersWithoutKey.length} user(s) without encryption key, persisting...`);
|
|
108
|
-
// Update all users without a key
|
|
109
|
-
const updates = usersWithoutKey.map(user => supabase
|
|
110
|
-
.from('user_settings')
|
|
111
|
-
.update({ encryption_key: currentKey })
|
|
112
|
-
.eq('user_id', user.user_id));
|
|
113
|
-
await Promise.all(updates);
|
|
114
|
-
logger.info(`✓ Persisted encryption key to ${usersWithoutKey.length} user(s)`);
|
|
115
|
-
}
|
|
116
|
-
catch (err) {
|
|
117
|
-
logger.warn('Error syncing encryption key to users:', { error: err });
|
|
118
|
-
}
|
|
119
|
-
}
|
|
16
|
+
import { initializePersistenceEncryption, syncEncryptionKeyToUsers } from './src/services/encryptionInit.js';
|
|
120
17
|
const __filename = fileURLToPath(import.meta.url);
|
|
121
18
|
const __dirname = path.dirname(__filename);
|
|
122
19
|
// Validate configuration
|
|
@@ -4,6 +4,7 @@ import { AuthenticationError, AuthorizationError } from './errorHandler.js';
|
|
|
4
4
|
import { createLogger, Logger } from '../utils/logger.js';
|
|
5
5
|
const logger = createLogger('AuthMiddleware');
|
|
6
6
|
import { getServerSupabase, isValidUrl } from '../services/supabase.js';
|
|
7
|
+
import { initializePersistenceEncryption, isEncryptionReady } from '../services/encryptionInit.js';
|
|
7
8
|
// Check if anon key looks valid (JWT or publishable key format)
|
|
8
9
|
function isValidAnonKey(key) {
|
|
9
10
|
if (!key)
|
|
@@ -32,6 +33,17 @@ export async function authMiddleware(req, _res, next) {
|
|
|
32
33
|
const isEnvKeyValid = !!envKey && envKey.length > 0;
|
|
33
34
|
const supabaseUrl = isEnvUrlValid ? envUrl : (headerConfig?.url || '');
|
|
34
35
|
const supabaseAnonKey = isEnvKeyValid ? envKey : (headerConfig?.anonKey || '');
|
|
36
|
+
// If encryption is not ready, try to initialize it using available Supabase config
|
|
37
|
+
if (!isEncryptionReady() && supabaseUrl && supabaseAnonKey) {
|
|
38
|
+
// Note: Ideally we use service role to read encryption_key from user_settings,
|
|
39
|
+
// but even with anon key it might work if RLS allows or if we just use it
|
|
40
|
+
// to check for existence of keys.
|
|
41
|
+
const initClient = createClient(supabaseUrl, supabaseAnonKey, {
|
|
42
|
+
auth: { autoRefreshToken: false, persistSession: false },
|
|
43
|
+
});
|
|
44
|
+
// Run in background to not block auth
|
|
45
|
+
initializePersistenceEncryption(initClient).catch(err => logger.warn('Failed to initialize encryption in auth middleware', { error: err.message }));
|
|
46
|
+
}
|
|
35
47
|
// Development bypass: skip auth if DISABLE_AUTH=true in non-production
|
|
36
48
|
if (config.security.disableAuth && !config.isProduction) {
|
|
37
49
|
logger.warn('Auth disabled for development - creating mock user');
|
|
@@ -7,57 +7,15 @@ import { getGmailService } from '../services/gmail.js';
|
|
|
7
7
|
import { getMicrosoftService } from '../services/microsoft.js';
|
|
8
8
|
import { getImapService } from '../services/imap-service.js';
|
|
9
9
|
import { createLogger } from '../utils/logger.js';
|
|
10
|
-
import {
|
|
10
|
+
import { initializePersistenceEncryption } from '../services/encryptionInit.js';
|
|
11
11
|
const router = Router();
|
|
12
12
|
const logger = createLogger('AuthRoutes');
|
|
13
13
|
// IMAP/SMTP Connection
|
|
14
14
|
router.post('/imap/connect', connectionRateLimit, // More lenient for connection testing (30 attempts / 15 min)
|
|
15
15
|
authMiddleware, validateBody(schemas.imapConnect), asyncHandler(async (req, res) => {
|
|
16
16
|
const { email, password, imapHost, imapPort, imapSecure, smtpHost, smtpPort, smtpSecure } = req.body;
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
// We need to sync them: prefer database key (persistent) over server key (ephemeral)
|
|
20
|
-
const { data: userSettings, error: fetchError } = await req.supabase
|
|
21
|
-
.from('user_settings')
|
|
22
|
-
.select('encryption_key')
|
|
23
|
-
.eq('user_id', req.user.id)
|
|
24
|
-
.single();
|
|
25
|
-
if (fetchError) {
|
|
26
|
-
logger.error('Failed to fetch user settings', { error: fetchError });
|
|
27
|
-
throw new ValidationError('Failed to load user settings. Please try again.');
|
|
28
|
-
}
|
|
29
|
-
const currentServerKey = getEncryptionKeyHex();
|
|
30
|
-
const databaseKey = userSettings?.encryption_key;
|
|
31
|
-
logger.info('Encryption key sync check', {
|
|
32
|
-
hasServerKey: !!currentServerKey,
|
|
33
|
-
hasDatabaseKey: !!databaseKey,
|
|
34
|
-
userId: req.user.id
|
|
35
|
-
});
|
|
36
|
-
if (databaseKey) {
|
|
37
|
-
// Database has a key - use it (it's the source of truth)
|
|
38
|
-
if (currentServerKey !== databaseKey) {
|
|
39
|
-
logger.info('Loading encryption key from database (overriding server memory)');
|
|
40
|
-
setEncryptionKey(databaseKey);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
else if (currentServerKey) {
|
|
44
|
-
// Server has key but database doesn't - persist to database
|
|
45
|
-
logger.info('Database missing encryption key, persisting server key');
|
|
46
|
-
const { error: updateError } = await req.supabase
|
|
47
|
-
.from('user_settings')
|
|
48
|
-
.update({ encryption_key: currentServerKey })
|
|
49
|
-
.eq('user_id', req.user.id);
|
|
50
|
-
if (updateError) {
|
|
51
|
-
logger.error('Failed to persist encryption key', { error: updateError });
|
|
52
|
-
throw new ValidationError('Failed to initialize encryption. Please try again.');
|
|
53
|
-
}
|
|
54
|
-
logger.info('✓ Encryption key persisted to database');
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
// Neither server nor database has key - this should never happen with new trigger
|
|
58
|
-
logger.error('No encryption key found in server OR database!');
|
|
59
|
-
throw new ValidationError('Encryption not initialized. Please contact support.');
|
|
60
|
-
}
|
|
17
|
+
// Reconcile encryption key with database before saving credentials
|
|
18
|
+
await initializePersistenceEncryption(req.supabase);
|
|
61
19
|
const imapService = getImapService();
|
|
62
20
|
const imapConfig = {
|
|
63
21
|
host: imapHost,
|