@realtimex/email-automator 2.2.1 → 2.3.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 +4 -2
- package/api/src/config/index.ts +11 -9
- package/bin/email-automator.js +4 -24
- package/dist/api/server.js +109 -0
- package/dist/api/src/config/index.js +89 -0
- package/dist/api/src/middleware/auth.js +119 -0
- package/dist/api/src/middleware/errorHandler.js +78 -0
- package/dist/api/src/middleware/index.js +4 -0
- package/dist/api/src/middleware/rateLimit.js +57 -0
- package/dist/api/src/middleware/validation.js +111 -0
- package/dist/api/src/routes/actions.js +173 -0
- package/dist/api/src/routes/auth.js +106 -0
- package/dist/api/src/routes/emails.js +100 -0
- package/dist/api/src/routes/health.js +33 -0
- package/dist/api/src/routes/index.js +19 -0
- package/dist/api/src/routes/migrate.js +61 -0
- package/dist/api/src/routes/rules.js +104 -0
- package/dist/api/src/routes/settings.js +178 -0
- package/dist/api/src/routes/sync.js +118 -0
- package/dist/api/src/services/eventLogger.js +41 -0
- package/dist/api/src/services/gmail.js +350 -0
- package/dist/api/src/services/intelligence.js +243 -0
- package/dist/api/src/services/microsoft.js +256 -0
- package/dist/api/src/services/processor.js +503 -0
- package/dist/api/src/services/scheduler.js +210 -0
- package/dist/api/src/services/supabase.js +59 -0
- package/dist/api/src/utils/contentCleaner.js +94 -0
- package/dist/api/src/utils/crypto.js +68 -0
- package/dist/api/src/utils/logger.js +119 -0
- package/package.json +5 -4
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { asyncHandler, NotFoundError } from '../middleware/errorHandler.js';
|
|
3
|
+
import { authMiddleware } from '../middleware/auth.js';
|
|
4
|
+
import { apiRateLimit } from '../middleware/rateLimit.js';
|
|
5
|
+
import { validateBody, schemas } from '../middleware/validation.js';
|
|
6
|
+
import { getGmailService } from '../services/gmail.js';
|
|
7
|
+
import { getMicrosoftService } from '../services/microsoft.js';
|
|
8
|
+
import { getIntelligenceService } from '../services/intelligence.js';
|
|
9
|
+
import { createLogger } from '../utils/logger.js';
|
|
10
|
+
const router = Router();
|
|
11
|
+
const logger = createLogger('ActionRoutes');
|
|
12
|
+
// Execute action on email
|
|
13
|
+
router.post('/execute', apiRateLimit, authMiddleware, validateBody(schemas.executeAction), asyncHandler(async (req, res) => {
|
|
14
|
+
const { emailId, action, draftContent } = req.body;
|
|
15
|
+
const userId = req.user.id;
|
|
16
|
+
// Fetch email with account info
|
|
17
|
+
const { data: email, error } = await req.supabase
|
|
18
|
+
.from('emails')
|
|
19
|
+
.select('*, email_accounts(*)')
|
|
20
|
+
.eq('id', emailId)
|
|
21
|
+
.single();
|
|
22
|
+
if (error || !email) {
|
|
23
|
+
throw new NotFoundError('Email');
|
|
24
|
+
}
|
|
25
|
+
// Verify ownership
|
|
26
|
+
if (email.email_accounts.user_id !== userId) {
|
|
27
|
+
throw new NotFoundError('Email');
|
|
28
|
+
}
|
|
29
|
+
const account = email.email_accounts;
|
|
30
|
+
let actionResult = { success: true, details: '' };
|
|
31
|
+
if (action === 'none') {
|
|
32
|
+
// Just mark as reviewed
|
|
33
|
+
await req.supabase
|
|
34
|
+
.from('emails')
|
|
35
|
+
.update({ action_taken: 'none' })
|
|
36
|
+
.eq('id', emailId);
|
|
37
|
+
}
|
|
38
|
+
else if (account.provider === 'gmail') {
|
|
39
|
+
const gmailService = getGmailService();
|
|
40
|
+
if (action === 'delete') {
|
|
41
|
+
await gmailService.trashMessage(account, email.external_id);
|
|
42
|
+
}
|
|
43
|
+
else if (action === 'archive') {
|
|
44
|
+
await gmailService.archiveMessage(account, email.external_id);
|
|
45
|
+
}
|
|
46
|
+
else if (action === 'draft') {
|
|
47
|
+
const content = draftContent || email.ai_analysis?.draft_response || '';
|
|
48
|
+
if (content) {
|
|
49
|
+
const draftId = await gmailService.createDraft(account, email.external_id, content);
|
|
50
|
+
actionResult.details = `Draft created: ${draftId}`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
else if (action === 'flag') {
|
|
54
|
+
await gmailService.addLabel(account, email.external_id, ['STARRED']);
|
|
55
|
+
}
|
|
56
|
+
await req.supabase
|
|
57
|
+
.from('emails')
|
|
58
|
+
.update({ action_taken: action })
|
|
59
|
+
.eq('id', emailId);
|
|
60
|
+
}
|
|
61
|
+
else if (account.provider === 'outlook') {
|
|
62
|
+
const microsoftService = getMicrosoftService();
|
|
63
|
+
if (action === 'delete') {
|
|
64
|
+
await microsoftService.trashMessage(account, email.external_id);
|
|
65
|
+
}
|
|
66
|
+
else if (action === 'archive') {
|
|
67
|
+
await microsoftService.archiveMessage(account, email.external_id);
|
|
68
|
+
}
|
|
69
|
+
else if (action === 'draft') {
|
|
70
|
+
const content = draftContent || email.ai_analysis?.draft_response || '';
|
|
71
|
+
if (content) {
|
|
72
|
+
const draftId = await microsoftService.createDraft(account, email.external_id, content);
|
|
73
|
+
actionResult.details = `Draft created: ${draftId}`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
await req.supabase
|
|
77
|
+
.from('emails')
|
|
78
|
+
.update({ action_taken: action })
|
|
79
|
+
.eq('id', emailId);
|
|
80
|
+
}
|
|
81
|
+
logger.info('Action executed', { emailId, action, userId });
|
|
82
|
+
res.json(actionResult);
|
|
83
|
+
}));
|
|
84
|
+
// Generate AI draft for an email
|
|
85
|
+
router.post('/draft/:emailId', apiRateLimit, authMiddleware, asyncHandler(async (req, res) => {
|
|
86
|
+
const { emailId } = req.params;
|
|
87
|
+
const { instructions } = req.body;
|
|
88
|
+
const userId = req.user.id;
|
|
89
|
+
// Fetch email
|
|
90
|
+
const { data: email, error } = await req.supabase
|
|
91
|
+
.from('emails')
|
|
92
|
+
.select('*, email_accounts(user_id)')
|
|
93
|
+
.eq('id', emailId)
|
|
94
|
+
.single();
|
|
95
|
+
if (error || !email) {
|
|
96
|
+
throw new NotFoundError('Email');
|
|
97
|
+
}
|
|
98
|
+
if (email.email_accounts.user_id !== userId) {
|
|
99
|
+
throw new NotFoundError('Email');
|
|
100
|
+
}
|
|
101
|
+
// Get user settings for LLM config
|
|
102
|
+
const { data: settings } = await req.supabase
|
|
103
|
+
.from('user_settings')
|
|
104
|
+
.select('llm_model, llm_base_url')
|
|
105
|
+
.eq('user_id', userId)
|
|
106
|
+
.single();
|
|
107
|
+
const intelligenceService = getIntelligenceService(settings ? { model: settings.llm_model, baseUrl: settings.llm_base_url } : undefined);
|
|
108
|
+
const draft = await intelligenceService.generateDraftReply({
|
|
109
|
+
subject: email.subject,
|
|
110
|
+
sender: email.sender,
|
|
111
|
+
body: email.body_snippet,
|
|
112
|
+
}, instructions);
|
|
113
|
+
if (!draft) {
|
|
114
|
+
return res.status(500).json({ error: 'Failed to generate draft' });
|
|
115
|
+
}
|
|
116
|
+
res.json({ draft });
|
|
117
|
+
}));
|
|
118
|
+
// Bulk actions
|
|
119
|
+
router.post('/bulk', apiRateLimit, authMiddleware, asyncHandler(async (req, res) => {
|
|
120
|
+
const { emailIds, action } = req.body;
|
|
121
|
+
const userId = req.user.id;
|
|
122
|
+
if (!Array.isArray(emailIds) || emailIds.length === 0) {
|
|
123
|
+
return res.status(400).json({ error: 'emailIds must be a non-empty array' });
|
|
124
|
+
}
|
|
125
|
+
if (!['delete', 'archive', 'none'].includes(action)) {
|
|
126
|
+
return res.status(400).json({ error: 'Invalid action for bulk operation' });
|
|
127
|
+
}
|
|
128
|
+
const results = { success: 0, failed: 0 };
|
|
129
|
+
for (const emailId of emailIds) {
|
|
130
|
+
try {
|
|
131
|
+
// Fetch email
|
|
132
|
+
const { data: email } = await req.supabase
|
|
133
|
+
.from('emails')
|
|
134
|
+
.select('*, email_accounts(*)')
|
|
135
|
+
.eq('id', emailId)
|
|
136
|
+
.single();
|
|
137
|
+
if (!email || email.email_accounts.user_id !== userId) {
|
|
138
|
+
results.failed++;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const account = email.email_accounts;
|
|
142
|
+
if (account.provider === 'gmail') {
|
|
143
|
+
const gmailService = getGmailService();
|
|
144
|
+
if (action === 'delete') {
|
|
145
|
+
await gmailService.trashMessage(account, email.external_id);
|
|
146
|
+
}
|
|
147
|
+
else if (action === 'archive') {
|
|
148
|
+
await gmailService.archiveMessage(account, email.external_id);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
else if (account.provider === 'outlook') {
|
|
152
|
+
const microsoftService = getMicrosoftService();
|
|
153
|
+
if (action === 'delete') {
|
|
154
|
+
await microsoftService.trashMessage(account, email.external_id);
|
|
155
|
+
}
|
|
156
|
+
else if (action === 'archive') {
|
|
157
|
+
await microsoftService.archiveMessage(account, email.external_id);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
await req.supabase
|
|
161
|
+
.from('emails')
|
|
162
|
+
.update({ action_taken: action })
|
|
163
|
+
.eq('id', emailId);
|
|
164
|
+
results.success++;
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
logger.error('Bulk action failed for email', err, { emailId });
|
|
168
|
+
results.failed++;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
res.json(results);
|
|
172
|
+
}));
|
|
173
|
+
export default router;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { asyncHandler, ValidationError } from '../middleware/errorHandler.js';
|
|
3
|
+
import { authMiddleware } from '../middleware/auth.js';
|
|
4
|
+
import { authRateLimit } from '../middleware/rateLimit.js';
|
|
5
|
+
import { validateBody, schemas } from '../middleware/validation.js';
|
|
6
|
+
import { getGmailService } from '../services/gmail.js';
|
|
7
|
+
import { getMicrosoftService } from '../services/microsoft.js';
|
|
8
|
+
import { createLogger } from '../utils/logger.js';
|
|
9
|
+
const router = Router();
|
|
10
|
+
const logger = createLogger('AuthRoutes');
|
|
11
|
+
// Gmail OAuth
|
|
12
|
+
router.get('/gmail/url', authRateLimit, asyncHandler(async (req, res) => {
|
|
13
|
+
const gmailService = getGmailService();
|
|
14
|
+
const url = gmailService.getAuthUrl();
|
|
15
|
+
logger.info('Gmail auth URL generated');
|
|
16
|
+
res.json({ url });
|
|
17
|
+
}));
|
|
18
|
+
router.post('/gmail/callback', authRateLimit, authMiddleware, validateBody(schemas.gmailCallback), asyncHandler(async (req, res) => {
|
|
19
|
+
const { code } = req.body;
|
|
20
|
+
const gmailService = getGmailService();
|
|
21
|
+
// Exchange code for tokens
|
|
22
|
+
const tokens = await gmailService.exchangeCode(code);
|
|
23
|
+
if (!tokens.access_token) {
|
|
24
|
+
throw new ValidationError('Failed to obtain access token');
|
|
25
|
+
}
|
|
26
|
+
// Get email address from Gmail profile
|
|
27
|
+
const tempAccount = {
|
|
28
|
+
id: '',
|
|
29
|
+
user_id: req.user.id,
|
|
30
|
+
provider: 'gmail',
|
|
31
|
+
email_address: '',
|
|
32
|
+
access_token: tokens.access_token,
|
|
33
|
+
refresh_token: tokens.refresh_token || null,
|
|
34
|
+
token_expires_at: tokens.expiry_date ? new Date(tokens.expiry_date).toISOString() : null,
|
|
35
|
+
scopes: tokens.scope?.split(' ') || [],
|
|
36
|
+
is_active: true,
|
|
37
|
+
created_at: '',
|
|
38
|
+
updated_at: '',
|
|
39
|
+
};
|
|
40
|
+
const profile = await gmailService.getProfile(tempAccount);
|
|
41
|
+
// Save account
|
|
42
|
+
const account = await gmailService.saveAccount(req.supabase, req.user.id, profile.emailAddress, tokens);
|
|
43
|
+
logger.info('Gmail account connected', {
|
|
44
|
+
userId: req.user.id,
|
|
45
|
+
email: profile.emailAddress
|
|
46
|
+
});
|
|
47
|
+
res.json({
|
|
48
|
+
success: true,
|
|
49
|
+
account: {
|
|
50
|
+
id: account.id,
|
|
51
|
+
email_address: account.email_address,
|
|
52
|
+
provider: account.provider,
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}));
|
|
56
|
+
// Microsoft OAuth (Device Code Flow)
|
|
57
|
+
router.post('/microsoft/device-flow', authRateLimit, authMiddleware, asyncHandler(async (req, res) => {
|
|
58
|
+
const microsoftService = getMicrosoftService();
|
|
59
|
+
// Start device code flow
|
|
60
|
+
const result = await microsoftService.acquireTokenByDeviceCode((response) => {
|
|
61
|
+
// This callback is called when the device code is ready
|
|
62
|
+
res.json({
|
|
63
|
+
userCode: response.userCode,
|
|
64
|
+
verificationUri: response.verificationUri,
|
|
65
|
+
message: response.message,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
// If we get here, the user completed the flow
|
|
69
|
+
if (result) {
|
|
70
|
+
const account = await microsoftService.saveAccount(req.supabase, req.user.id, result.account?.username || '', result);
|
|
71
|
+
logger.info('Microsoft account connected', {
|
|
72
|
+
userId: req.user.id,
|
|
73
|
+
email: account.email_address,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
77
|
+
router.post('/microsoft/complete', authRateLimit, authMiddleware, validateBody(schemas.deviceFlow), asyncHandler(async (_req, res) => {
|
|
78
|
+
// Device flow completion is handled in the device-flow endpoint
|
|
79
|
+
// This is kept for backwards compatibility
|
|
80
|
+
res.json({ success: true });
|
|
81
|
+
}));
|
|
82
|
+
// Disconnect account
|
|
83
|
+
router.delete('/accounts/:accountId', authMiddleware, asyncHandler(async (req, res) => {
|
|
84
|
+
const { accountId } = req.params;
|
|
85
|
+
const { error } = await req.supabase
|
|
86
|
+
.from('email_accounts')
|
|
87
|
+
.delete()
|
|
88
|
+
.eq('id', accountId)
|
|
89
|
+
.eq('user_id', req.user.id);
|
|
90
|
+
if (error)
|
|
91
|
+
throw error;
|
|
92
|
+
logger.info('Account disconnected', { accountId, userId: req.user.id });
|
|
93
|
+
res.json({ success: true });
|
|
94
|
+
}));
|
|
95
|
+
// List connected accounts
|
|
96
|
+
router.get('/accounts', authMiddleware, asyncHandler(async (req, res) => {
|
|
97
|
+
const { data, error } = await req.supabase
|
|
98
|
+
.from('email_accounts')
|
|
99
|
+
.select('id, provider, email_address, is_active, created_at, updated_at')
|
|
100
|
+
.eq('user_id', req.user.id)
|
|
101
|
+
.order('created_at', { ascending: false });
|
|
102
|
+
if (error)
|
|
103
|
+
throw error;
|
|
104
|
+
res.json({ accounts: data || [] });
|
|
105
|
+
}));
|
|
106
|
+
export default router;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { asyncHandler, NotFoundError } from '../middleware/errorHandler.js';
|
|
3
|
+
import { authMiddleware } from '../middleware/auth.js';
|
|
4
|
+
import { apiRateLimit } from '../middleware/rateLimit.js';
|
|
5
|
+
import { createLogger } from '../utils/logger.js';
|
|
6
|
+
const router = Router();
|
|
7
|
+
const logger = createLogger('EmailsRoutes');
|
|
8
|
+
// List emails with pagination and filters
|
|
9
|
+
router.get('/', authMiddleware, asyncHandler(async (req, res) => {
|
|
10
|
+
const { limit = '20', offset = '0', category, is_useless, account_id, action_taken, search, } = req.query;
|
|
11
|
+
let query = req.supabase
|
|
12
|
+
.from('emails')
|
|
13
|
+
.select(`
|
|
14
|
+
*,
|
|
15
|
+
email_accounts!inner(id, user_id, email_address, provider)
|
|
16
|
+
`, { count: 'exact' })
|
|
17
|
+
.eq('email_accounts.user_id', req.user.id)
|
|
18
|
+
.order('date', { ascending: false })
|
|
19
|
+
.range(parseInt(offset, 10), parseInt(offset, 10) + parseInt(limit, 10) - 1);
|
|
20
|
+
// Apply filters
|
|
21
|
+
if (category) {
|
|
22
|
+
query = query.eq('category', category);
|
|
23
|
+
}
|
|
24
|
+
if (is_useless !== undefined) {
|
|
25
|
+
query = query.eq('is_useless', is_useless === 'true');
|
|
26
|
+
}
|
|
27
|
+
if (account_id) {
|
|
28
|
+
query = query.eq('account_id', account_id);
|
|
29
|
+
}
|
|
30
|
+
if (action_taken) {
|
|
31
|
+
query = query.eq('action_taken', action_taken);
|
|
32
|
+
}
|
|
33
|
+
if (search) {
|
|
34
|
+
query = query.or(`subject.ilike.%${search}%,sender.ilike.%${search}%`);
|
|
35
|
+
}
|
|
36
|
+
const { data, error, count } = await query;
|
|
37
|
+
if (error)
|
|
38
|
+
throw error;
|
|
39
|
+
res.json({
|
|
40
|
+
emails: data || [],
|
|
41
|
+
total: count || 0,
|
|
42
|
+
limit: parseInt(limit, 10),
|
|
43
|
+
offset: parseInt(offset, 10),
|
|
44
|
+
});
|
|
45
|
+
}));
|
|
46
|
+
// Get single email
|
|
47
|
+
router.get('/:emailId', authMiddleware, asyncHandler(async (req, res) => {
|
|
48
|
+
const { emailId } = req.params;
|
|
49
|
+
const { data, error } = await req.supabase
|
|
50
|
+
.from('emails')
|
|
51
|
+
.select(`
|
|
52
|
+
*,
|
|
53
|
+
email_accounts!inner(id, user_id, email_address, provider)
|
|
54
|
+
`)
|
|
55
|
+
.eq('id', emailId)
|
|
56
|
+
.eq('email_accounts.user_id', req.user.id)
|
|
57
|
+
.single();
|
|
58
|
+
if (error || !data) {
|
|
59
|
+
throw new NotFoundError('Email');
|
|
60
|
+
}
|
|
61
|
+
res.json({ email: data });
|
|
62
|
+
}));
|
|
63
|
+
// Delete email record (not the actual email from provider)
|
|
64
|
+
router.delete('/:emailId', apiRateLimit, authMiddleware, asyncHandler(async (req, res) => {
|
|
65
|
+
const { emailId } = req.params;
|
|
66
|
+
// Verify ownership first
|
|
67
|
+
const { data: email } = await req.supabase
|
|
68
|
+
.from('emails')
|
|
69
|
+
.select('id, email_accounts!inner(user_id)')
|
|
70
|
+
.eq('id', emailId)
|
|
71
|
+
.eq('email_accounts.user_id', req.user.id)
|
|
72
|
+
.single();
|
|
73
|
+
if (!email) {
|
|
74
|
+
throw new NotFoundError('Email');
|
|
75
|
+
}
|
|
76
|
+
const { error } = await req.supabase
|
|
77
|
+
.from('emails')
|
|
78
|
+
.delete()
|
|
79
|
+
.eq('id', emailId);
|
|
80
|
+
if (error)
|
|
81
|
+
throw error;
|
|
82
|
+
logger.info('Email record deleted', { emailId, userId: req.user.id });
|
|
83
|
+
res.json({ success: true });
|
|
84
|
+
}));
|
|
85
|
+
// Get email categories summary
|
|
86
|
+
router.get('/summary/categories', authMiddleware, asyncHandler(async (req, res) => {
|
|
87
|
+
const { data, error } = await req.supabase
|
|
88
|
+
.from('emails')
|
|
89
|
+
.select('category, email_accounts!inner(user_id)')
|
|
90
|
+
.eq('email_accounts.user_id', req.user.id);
|
|
91
|
+
if (error)
|
|
92
|
+
throw error;
|
|
93
|
+
const summary = {};
|
|
94
|
+
for (const email of data || []) {
|
|
95
|
+
const cat = email.category || 'uncategorized';
|
|
96
|
+
summary[cat] = (summary[cat] || 0) + 1;
|
|
97
|
+
}
|
|
98
|
+
res.json({ categories: summary });
|
|
99
|
+
}));
|
|
100
|
+
export default router;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { getServerSupabase } from '../services/supabase.js';
|
|
3
|
+
import { config } from '../config/index.js';
|
|
4
|
+
const router = Router();
|
|
5
|
+
router.get('/', async (_req, res) => {
|
|
6
|
+
const supabase = getServerSupabase();
|
|
7
|
+
let dbStatus = 'unknown';
|
|
8
|
+
if (supabase) {
|
|
9
|
+
try {
|
|
10
|
+
const { error } = await supabase.from('email_accounts').select('id').limit(1);
|
|
11
|
+
dbStatus = error ? 'error' : 'connected';
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
dbStatus = 'error';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
dbStatus = 'not_configured';
|
|
19
|
+
}
|
|
20
|
+
res.json({
|
|
21
|
+
status: 'healthy',
|
|
22
|
+
timestamp: new Date().toISOString(),
|
|
23
|
+
version: '1.0.0',
|
|
24
|
+
environment: config.nodeEnv,
|
|
25
|
+
services: {
|
|
26
|
+
database: dbStatus,
|
|
27
|
+
llm: config.llm.apiKey ? 'configured' : 'not_configured',
|
|
28
|
+
gmail: config.gmail.clientId ? 'configured' : 'not_configured',
|
|
29
|
+
microsoft: config.microsoft.clientId ? 'configured' : 'not_configured',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
export default router;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import healthRoutes from './health.js';
|
|
3
|
+
import authRoutes from './auth.js';
|
|
4
|
+
import syncRoutes from './sync.js';
|
|
5
|
+
import actionsRoutes from './actions.js';
|
|
6
|
+
import rulesRoutes from './rules.js';
|
|
7
|
+
import settingsRoutes from './settings.js';
|
|
8
|
+
import emailsRoutes from './emails.js';
|
|
9
|
+
import migrateRoutes from './migrate.js';
|
|
10
|
+
const router = Router();
|
|
11
|
+
router.use('/health', healthRoutes);
|
|
12
|
+
router.use('/auth', authRoutes);
|
|
13
|
+
router.use('/sync', syncRoutes);
|
|
14
|
+
router.use('/actions', actionsRoutes);
|
|
15
|
+
router.use('/rules', rulesRoutes);
|
|
16
|
+
router.use('/settings', settingsRoutes);
|
|
17
|
+
router.use('/emails', emailsRoutes);
|
|
18
|
+
router.use('/migrate', migrateRoutes);
|
|
19
|
+
export default router;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { asyncHandler } from '../middleware/errorHandler.js';
|
|
4
|
+
import { validateBody, schemas } from '../middleware/validation.js';
|
|
5
|
+
import { config } from '../config/index.js';
|
|
6
|
+
import { createLogger } from '../utils/logger.js';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
const router = Router();
|
|
9
|
+
const logger = createLogger('MigrateRoutes');
|
|
10
|
+
// Run database migration
|
|
11
|
+
router.post('/', validateBody(schemas.migrate), asyncHandler(async (req, res) => {
|
|
12
|
+
const { projectRef, dbPassword, accessToken } = req.body;
|
|
13
|
+
logger.info('Starting migration', { projectRef });
|
|
14
|
+
res.setHeader('Content-Type', 'text/plain');
|
|
15
|
+
res.setHeader('Transfer-Encoding', 'chunked');
|
|
16
|
+
const sendLog = (message) => {
|
|
17
|
+
res.write(message + '\n');
|
|
18
|
+
};
|
|
19
|
+
try {
|
|
20
|
+
sendLog('🔧 Starting migration...');
|
|
21
|
+
const scriptPath = join(config.scriptsDir, 'migrate.sh');
|
|
22
|
+
const env = {
|
|
23
|
+
...process.env,
|
|
24
|
+
SUPABASE_PROJECT_ID: projectRef,
|
|
25
|
+
SUPABASE_DB_PASSWORD: dbPassword || '',
|
|
26
|
+
SUPABASE_ACCESS_TOKEN: accessToken || '',
|
|
27
|
+
};
|
|
28
|
+
const child = spawn('bash', [scriptPath], {
|
|
29
|
+
env,
|
|
30
|
+
cwd: config.rootDir,
|
|
31
|
+
});
|
|
32
|
+
child.stdout.on('data', (data) => {
|
|
33
|
+
sendLog(data.toString().trim());
|
|
34
|
+
});
|
|
35
|
+
child.stderr.on('data', (data) => {
|
|
36
|
+
sendLog(`⚠️ ${data.toString().trim()}`);
|
|
37
|
+
});
|
|
38
|
+
child.on('close', (code) => {
|
|
39
|
+
if (code === 0) {
|
|
40
|
+
sendLog('✅ Migration successful!');
|
|
41
|
+
logger.info('Migration completed successfully', { projectRef });
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
sendLog(`❌ Migration failed with code ${code}`);
|
|
45
|
+
logger.error('Migration failed', new Error(`Exit code: ${code}`), { projectRef });
|
|
46
|
+
}
|
|
47
|
+
res.end();
|
|
48
|
+
});
|
|
49
|
+
child.on('error', (error) => {
|
|
50
|
+
sendLog(`❌ Failed to run migration: ${error.message}`);
|
|
51
|
+
logger.error('Migration spawn error', error);
|
|
52
|
+
res.end();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
sendLog(`❌ Server error: ${error.message}`);
|
|
57
|
+
logger.error('Migration error', error);
|
|
58
|
+
res.end();
|
|
59
|
+
}
|
|
60
|
+
}));
|
|
61
|
+
export default router;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { asyncHandler, NotFoundError } from '../middleware/errorHandler.js';
|
|
3
|
+
import { authMiddleware } from '../middleware/auth.js';
|
|
4
|
+
import { apiRateLimit } from '../middleware/rateLimit.js';
|
|
5
|
+
import { validateBody, validateParams, schemas } from '../middleware/validation.js';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { createLogger } from '../utils/logger.js';
|
|
8
|
+
const router = Router();
|
|
9
|
+
const logger = createLogger('RulesRoutes');
|
|
10
|
+
// List all rules
|
|
11
|
+
router.get('/', authMiddleware, asyncHandler(async (req, res) => {
|
|
12
|
+
const { data, error } = await req.supabase
|
|
13
|
+
.from('rules')
|
|
14
|
+
.select('*')
|
|
15
|
+
.eq('user_id', req.user.id)
|
|
16
|
+
.order('created_at', { ascending: false });
|
|
17
|
+
if (error)
|
|
18
|
+
throw error;
|
|
19
|
+
res.json({ rules: data || [] });
|
|
20
|
+
}));
|
|
21
|
+
// Create rule
|
|
22
|
+
router.post('/', apiRateLimit, authMiddleware, validateBody(schemas.createRule), asyncHandler(async (req, res) => {
|
|
23
|
+
const { name, condition, action, is_enabled, instructions, attachments } = req.body;
|
|
24
|
+
const { data, error } = await req.supabase
|
|
25
|
+
.from('rules')
|
|
26
|
+
.insert({
|
|
27
|
+
user_id: req.user.id,
|
|
28
|
+
name,
|
|
29
|
+
condition,
|
|
30
|
+
action,
|
|
31
|
+
is_enabled,
|
|
32
|
+
instructions,
|
|
33
|
+
attachments,
|
|
34
|
+
})
|
|
35
|
+
.select()
|
|
36
|
+
.single();
|
|
37
|
+
if (error)
|
|
38
|
+
throw error;
|
|
39
|
+
logger.info('Rule created', { ruleId: data.id, userId: req.user.id });
|
|
40
|
+
res.status(201).json({ rule: data });
|
|
41
|
+
}));
|
|
42
|
+
// Update rule
|
|
43
|
+
router.patch('/:ruleId', apiRateLimit, authMiddleware, validateParams(z.object({ ruleId: schemas.uuid })), validateBody(schemas.updateRule), asyncHandler(async (req, res) => {
|
|
44
|
+
const { ruleId } = req.params;
|
|
45
|
+
const updates = req.body;
|
|
46
|
+
const { data, error } = await req.supabase
|
|
47
|
+
.from('rules')
|
|
48
|
+
.update(updates)
|
|
49
|
+
.eq('id', ruleId)
|
|
50
|
+
.eq('user_id', req.user.id)
|
|
51
|
+
.select()
|
|
52
|
+
.single();
|
|
53
|
+
if (error)
|
|
54
|
+
throw error;
|
|
55
|
+
if (!data)
|
|
56
|
+
throw new NotFoundError('Rule');
|
|
57
|
+
logger.info('Rule updated', { ruleId, userId: req.user.id });
|
|
58
|
+
res.json({ rule: data });
|
|
59
|
+
}));
|
|
60
|
+
// Delete rule
|
|
61
|
+
router.delete('/:ruleId', authMiddleware, validateParams(z.object({ ruleId: schemas.uuid })), asyncHandler(async (req, res) => {
|
|
62
|
+
const { ruleId } = req.params;
|
|
63
|
+
const { error } = await req.supabase
|
|
64
|
+
.from('rules')
|
|
65
|
+
.delete()
|
|
66
|
+
.eq('id', ruleId)
|
|
67
|
+
.eq('user_id', req.user.id);
|
|
68
|
+
if (error)
|
|
69
|
+
throw error;
|
|
70
|
+
logger.info('Rule deleted', { ruleId, userId: req.user.id });
|
|
71
|
+
res.json({ success: true });
|
|
72
|
+
}));
|
|
73
|
+
// Toggle rule enabled/disabled
|
|
74
|
+
router.post('/:ruleId/toggle', authMiddleware, validateParams(z.object({ ruleId: schemas.uuid })), asyncHandler(async (req, res) => {
|
|
75
|
+
const { ruleId } = req.params;
|
|
76
|
+
const userId = req.user.id;
|
|
77
|
+
// Get current state
|
|
78
|
+
const { data: rule, error: fetchError } = await req.supabase
|
|
79
|
+
.from('rules')
|
|
80
|
+
.select('name, is_enabled')
|
|
81
|
+
.eq('id', ruleId)
|
|
82
|
+
.eq('user_id', userId)
|
|
83
|
+
.single();
|
|
84
|
+
if (fetchError || !rule) {
|
|
85
|
+
logger.error('Toggle failed: Rule not found', { ruleId, userId });
|
|
86
|
+
throw new NotFoundError('Rule');
|
|
87
|
+
}
|
|
88
|
+
const nextState = !rule.is_enabled;
|
|
89
|
+
logger.info('Toggling rule', { ruleName: rule.name, from: rule.is_enabled, to: nextState });
|
|
90
|
+
// Toggle
|
|
91
|
+
const { data, error } = await req.supabase
|
|
92
|
+
.from('rules')
|
|
93
|
+
.update({ is_enabled: nextState })
|
|
94
|
+
.eq('id', ruleId)
|
|
95
|
+
.select()
|
|
96
|
+
.single();
|
|
97
|
+
if (error) {
|
|
98
|
+
logger.error('Database error during toggle', error);
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
logger.info('Toggle successful', { ruleId, is_enabled: data.is_enabled });
|
|
102
|
+
res.json({ rule: data });
|
|
103
|
+
}));
|
|
104
|
+
export default router;
|