@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.
@@ -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;