@realtimex/email-automator 2.1.1 → 2.2.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 +0 -6
- package/api/src/config/index.ts +3 -0
- package/bin/email-automator-setup.js +2 -3
- package/bin/email-automator.js +23 -7
- package/package.json +1 -2
- package/src/App.tsx +0 -622
- package/src/components/AccountSettings.tsx +0 -310
- package/src/components/AccountSettingsPage.tsx +0 -390
- package/src/components/Configuration.tsx +0 -1345
- package/src/components/Dashboard.tsx +0 -940
- package/src/components/ErrorBoundary.tsx +0 -71
- package/src/components/LiveTerminal.tsx +0 -308
- package/src/components/LoadingSpinner.tsx +0 -39
- package/src/components/Login.tsx +0 -371
- package/src/components/Logo.tsx +0 -57
- package/src/components/SetupWizard.tsx +0 -388
- package/src/components/Toast.tsx +0 -109
- package/src/components/migration/MigrationBanner.tsx +0 -97
- package/src/components/migration/MigrationModal.tsx +0 -458
- package/src/components/migration/MigrationPulseIndicator.tsx +0 -38
- package/src/components/mode-toggle.tsx +0 -24
- package/src/components/theme-provider.tsx +0 -72
- package/src/components/ui/alert.tsx +0 -66
- package/src/components/ui/button.tsx +0 -57
- package/src/components/ui/card.tsx +0 -75
- package/src/components/ui/dialog.tsx +0 -133
- package/src/components/ui/input.tsx +0 -22
- package/src/components/ui/label.tsx +0 -24
- package/src/components/ui/otp-input.tsx +0 -184
- package/src/context/AppContext.tsx +0 -422
- package/src/context/MigrationContext.tsx +0 -53
- package/src/context/TerminalContext.tsx +0 -31
- package/src/core/actions.ts +0 -76
- package/src/core/auth.ts +0 -108
- package/src/core/intelligence.ts +0 -76
- package/src/core/processor.ts +0 -112
- package/src/hooks/useRealtimeEmails.ts +0 -111
- package/src/index.css +0 -140
- package/src/lib/api-config.ts +0 -42
- package/src/lib/api-old.ts +0 -228
- package/src/lib/api.ts +0 -421
- package/src/lib/migration-check.ts +0 -264
- package/src/lib/sounds.ts +0 -120
- package/src/lib/supabase-config.ts +0 -117
- package/src/lib/supabase.ts +0 -28
- package/src/lib/types.ts +0 -166
- package/src/lib/utils.ts +0 -6
- package/src/main.tsx +0 -10
|
@@ -1,422 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext, useReducer, useEffect, ReactNode } from 'react';
|
|
2
|
-
import { supabase } from '../lib/supabase';
|
|
3
|
-
import { api, initializeApi } from '../lib/api';
|
|
4
|
-
import { Email, EmailAccount, Rule, UserSettings, Stats, Profile } from '../lib/types';
|
|
5
|
-
import { getSupabaseConfig } from '../lib/supabase-config';
|
|
6
|
-
|
|
7
|
-
// Helper to extract error message from API response error
|
|
8
|
-
function getErrorMessage(error: { message?: string; code?: string } | string | undefined, fallback: string): string {
|
|
9
|
-
if (!error) return fallback;
|
|
10
|
-
if (typeof error === 'string') return error;
|
|
11
|
-
return error.message || fallback;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// State
|
|
15
|
-
interface AppState {
|
|
16
|
-
// Auth
|
|
17
|
-
user: any | null;
|
|
18
|
-
isAuthenticated: boolean;
|
|
19
|
-
|
|
20
|
-
// Data
|
|
21
|
-
profile: Profile | null;
|
|
22
|
-
emails: Email[];
|
|
23
|
-
accounts: EmailAccount[];
|
|
24
|
-
rules: Rule[];
|
|
25
|
-
settings: UserSettings | null;
|
|
26
|
-
stats: Stats | null;
|
|
27
|
-
|
|
28
|
-
// UI
|
|
29
|
-
isLoading: boolean;
|
|
30
|
-
isInitialized: boolean;
|
|
31
|
-
error: string | null;
|
|
32
|
-
selectedEmailId: string | null;
|
|
33
|
-
|
|
34
|
-
// Pagination
|
|
35
|
-
emailsTotal: number;
|
|
36
|
-
emailsOffset: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const initialState: AppState = {
|
|
40
|
-
user: null,
|
|
41
|
-
isAuthenticated: false,
|
|
42
|
-
profile: null,
|
|
43
|
-
emails: [],
|
|
44
|
-
accounts: [],
|
|
45
|
-
rules: [],
|
|
46
|
-
settings: null,
|
|
47
|
-
stats: null,
|
|
48
|
-
isLoading: true,
|
|
49
|
-
isInitialized: false,
|
|
50
|
-
error: null,
|
|
51
|
-
selectedEmailId: null,
|
|
52
|
-
emailsTotal: 0,
|
|
53
|
-
emailsOffset: 0,
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
// Actions
|
|
57
|
-
type Action =
|
|
58
|
-
| { type: 'SET_USER'; payload: any }
|
|
59
|
-
| { type: 'SET_LOADING'; payload: boolean }
|
|
60
|
-
| { type: 'SET_INITIALIZED'; payload: boolean }
|
|
61
|
-
| { type: 'SET_ERROR'; payload: string | null }
|
|
62
|
-
| { type: 'SET_EMAILS'; payload: { emails: Email[]; total: number; offset: number } }
|
|
63
|
-
| { type: 'ADD_EMAIL'; payload: Email }
|
|
64
|
-
| { type: 'UPDATE_EMAIL'; payload: Email }
|
|
65
|
-
| { type: 'SET_PROFILE'; payload: Profile }
|
|
66
|
-
| { type: 'UPDATE_PROFILE'; payload: Profile }
|
|
67
|
-
| { type: 'SET_ACCOUNTS'; payload: EmailAccount[] }
|
|
68
|
-
| { type: 'ADD_ACCOUNT'; payload: EmailAccount }
|
|
69
|
-
| { type: 'REMOVE_ACCOUNT'; payload: string }
|
|
70
|
-
| { type: 'SET_RULES'; payload: Rule[] }
|
|
71
|
-
| { type: 'ADD_RULE'; payload: Rule }
|
|
72
|
-
| { type: 'UPDATE_RULE'; payload: Rule }
|
|
73
|
-
| { type: 'REMOVE_RULE'; payload: string }
|
|
74
|
-
| { type: 'SET_SETTINGS'; payload: UserSettings }
|
|
75
|
-
| { type: 'SET_STATS'; payload: Stats }
|
|
76
|
-
| { type: 'SET_SELECTED_EMAIL'; payload: string | null }
|
|
77
|
-
| { type: 'CLEAR_DATA' };
|
|
78
|
-
|
|
79
|
-
function reducer(state: AppState, action: Action): AppState {
|
|
80
|
-
switch (action.type) {
|
|
81
|
-
case 'SET_USER':
|
|
82
|
-
return {
|
|
83
|
-
...state,
|
|
84
|
-
user: action.payload,
|
|
85
|
-
isAuthenticated: !!action.payload,
|
|
86
|
-
isLoading: false,
|
|
87
|
-
};
|
|
88
|
-
case 'SET_LOADING':
|
|
89
|
-
return { ...state, isLoading: action.payload };
|
|
90
|
-
case 'SET_INITIALIZED':
|
|
91
|
-
return { ...state, isInitialized: action.payload };
|
|
92
|
-
case 'SET_ERROR':
|
|
93
|
-
return { ...state, error: action.payload, isLoading: false };
|
|
94
|
-
case 'SET_EMAILS':
|
|
95
|
-
return {
|
|
96
|
-
...state,
|
|
97
|
-
emails: action.payload.emails,
|
|
98
|
-
emailsTotal: action.payload.total,
|
|
99
|
-
emailsOffset: action.payload.offset,
|
|
100
|
-
};
|
|
101
|
-
case 'ADD_EMAIL':
|
|
102
|
-
return {
|
|
103
|
-
...state,
|
|
104
|
-
emails: [action.payload, ...state.emails],
|
|
105
|
-
emailsTotal: state.emailsTotal + 1,
|
|
106
|
-
};
|
|
107
|
-
case 'UPDATE_EMAIL':
|
|
108
|
-
return {
|
|
109
|
-
...state,
|
|
110
|
-
emails: state.emails.map(e =>
|
|
111
|
-
e.id === action.payload.id ? action.payload : e
|
|
112
|
-
),
|
|
113
|
-
};
|
|
114
|
-
case 'SET_PROFILE':
|
|
115
|
-
return { ...state, profile: action.payload };
|
|
116
|
-
case 'UPDATE_PROFILE':
|
|
117
|
-
return { ...state, profile: action.payload };
|
|
118
|
-
case 'SET_ACCOUNTS':
|
|
119
|
-
return { ...state, accounts: action.payload };
|
|
120
|
-
case 'ADD_ACCOUNT':
|
|
121
|
-
return { ...state, accounts: [action.payload, ...state.accounts] };
|
|
122
|
-
case 'REMOVE_ACCOUNT':
|
|
123
|
-
return {
|
|
124
|
-
...state,
|
|
125
|
-
accounts: state.accounts.filter(a => a.id !== action.payload),
|
|
126
|
-
};
|
|
127
|
-
case 'SET_RULES':
|
|
128
|
-
return { ...state, rules: action.payload };
|
|
129
|
-
case 'ADD_RULE':
|
|
130
|
-
return { ...state, rules: [action.payload, ...state.rules] };
|
|
131
|
-
case 'UPDATE_RULE':
|
|
132
|
-
return {
|
|
133
|
-
...state,
|
|
134
|
-
rules: state.rules.map(r =>
|
|
135
|
-
r.id === action.payload.id ? action.payload : r
|
|
136
|
-
),
|
|
137
|
-
};
|
|
138
|
-
case 'REMOVE_RULE':
|
|
139
|
-
return {
|
|
140
|
-
...state,
|
|
141
|
-
rules: state.rules.filter(r => r.id !== action.payload),
|
|
142
|
-
};
|
|
143
|
-
case 'SET_SETTINGS':
|
|
144
|
-
return { ...state, settings: action.payload };
|
|
145
|
-
case 'SET_STATS':
|
|
146
|
-
return { ...state, stats: action.payload };
|
|
147
|
-
case 'SET_SELECTED_EMAIL':
|
|
148
|
-
return { ...state, selectedEmailId: action.payload };
|
|
149
|
-
case 'CLEAR_DATA':
|
|
150
|
-
return { ...initialState, isLoading: false, isInitialized: true };
|
|
151
|
-
default:
|
|
152
|
-
return state;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Context
|
|
157
|
-
interface AppContextType {
|
|
158
|
-
state: AppState;
|
|
159
|
-
dispatch: React.Dispatch<Action>;
|
|
160
|
-
actions: {
|
|
161
|
-
fetchEmails: (params?: { category?: string; search?: string; offset?: number }) => Promise<void>;
|
|
162
|
-
fetchAccounts: () => Promise<void>;
|
|
163
|
-
fetchRules: () => Promise<void>;
|
|
164
|
-
fetchSettings: () => Promise<void>;
|
|
165
|
-
fetchProfile: () => Promise<void>;
|
|
166
|
-
fetchStats: () => Promise<void>;
|
|
167
|
-
executeAction: (emailId: string, action: string, draftContent?: string) => Promise<boolean>;
|
|
168
|
-
triggerSync: (accountId?: string) => Promise<boolean>;
|
|
169
|
-
disconnectAccount: (accountId: string) => Promise<boolean>;
|
|
170
|
-
updateSettings: (settings: Partial<UserSettings>) => Promise<boolean>;
|
|
171
|
-
updateProfile: (updates: { first_name?: string; last_name?: string; avatar_url?: string }) => Promise<boolean>;
|
|
172
|
-
updateAccount: (accountId: string, updates: Partial<EmailAccount>) => Promise<boolean>;
|
|
173
|
-
createRule: (rule: Omit<Rule, 'id' | 'user_id' | 'created_at'>) => Promise<boolean>;
|
|
174
|
-
deleteRule: (ruleId: string) => Promise<boolean>;
|
|
175
|
-
toggleRule: (ruleId: string) => Promise<boolean>;
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const AppContext = createContext<AppContextType | null>(null);
|
|
180
|
-
|
|
181
|
-
export function AppProvider({ children }: { children: ReactNode }) {
|
|
182
|
-
const [state, dispatch] = useReducer(reducer, initialState);
|
|
183
|
-
|
|
184
|
-
// Initialize auth
|
|
185
|
-
useEffect(() => {
|
|
186
|
-
async function init() {
|
|
187
|
-
// Check if we have valid Supabase config
|
|
188
|
-
const config = getSupabaseConfig();
|
|
189
|
-
if (!config) {
|
|
190
|
-
// No config - mark as initialized but not authenticated
|
|
191
|
-
// App.tsx will show SetupWizard
|
|
192
|
-
dispatch({ type: 'SET_INITIALIZED', payload: true });
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
try {
|
|
197
|
-
// Validate connection before proceeding
|
|
198
|
-
// This catches cases where env vars are present but invalid
|
|
199
|
-
const { error: pingError } = await supabase.from('init_state').select('count', { count: 'exact', head: true });
|
|
200
|
-
|
|
201
|
-
// If we get an auth error (401/403) or connection error, the config is likely bad.
|
|
202
|
-
// However, RLS might deny access, which counts as a success connectivity-wise but a 403.
|
|
203
|
-
// WE MUST distinguishing between "Bad Key" (401) and "RLS Denied" (401/403?? usually 401 is bad key).
|
|
204
|
-
// Actually, init_state view is public or has specific RLS.
|
|
205
|
-
// A bad key comes as a PostgrestError with code 'PGRST301' or similar, or just a 401 response.
|
|
206
|
-
|
|
207
|
-
if (pingError && pingError.message === 'Invalid API key') {
|
|
208
|
-
console.error('[AppContext] Invalid API Key detected during init');
|
|
209
|
-
// Clear invalid config if it was from storage?
|
|
210
|
-
// If it's env vars, we can't clear them, but we can ignore them?
|
|
211
|
-
// App.tsx needs to know to show SetupWizard.
|
|
212
|
-
// We can signal this by setting error or specific state.
|
|
213
|
-
dispatch({ type: 'SET_INITIALIZED', payload: true }); // Let App render, but it might fail.
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
await initializeApi(supabase);
|
|
218
|
-
|
|
219
|
-
const { data: { session } } = await supabase.auth.getSession();
|
|
220
|
-
dispatch({ type: 'SET_USER', payload: session?.user || null });
|
|
221
|
-
dispatch({ type: 'SET_INITIALIZED', payload: true });
|
|
222
|
-
|
|
223
|
-
// Listen for auth changes
|
|
224
|
-
const { data: { subscription } } = supabase.auth.onAuthStateChange(
|
|
225
|
-
(_event, session) => {
|
|
226
|
-
dispatch({ type: 'SET_USER', payload: session?.user || null });
|
|
227
|
-
if (!session) {
|
|
228
|
-
dispatch({ type: 'CLEAR_DATA' });
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
return () => subscription.unsubscribe();
|
|
234
|
-
} catch (error) {
|
|
235
|
-
console.error('[AppContext] Init error:', error);
|
|
236
|
-
|
|
237
|
-
// If valid config exists but basic connection fails, we should probably let the user know
|
|
238
|
-
// or fall back to setup.
|
|
239
|
-
// For now, just mark initialized so UI shows error state if needed.
|
|
240
|
-
dispatch({ type: 'SET_ERROR', payload: 'Failed to initialize' });
|
|
241
|
-
dispatch({ type: 'SET_INITIALIZED', payload: true });
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
init();
|
|
245
|
-
}, []);
|
|
246
|
-
|
|
247
|
-
// Actions
|
|
248
|
-
const actions = {
|
|
249
|
-
fetchEmails: async (params: { category?: string; search?: string; offset?: number } = {}) => {
|
|
250
|
-
const response = await api.getEmails({
|
|
251
|
-
limit: 20,
|
|
252
|
-
offset: params.offset || 0,
|
|
253
|
-
category: params.category,
|
|
254
|
-
search: params.search,
|
|
255
|
-
});
|
|
256
|
-
if (response.data) {
|
|
257
|
-
dispatch({
|
|
258
|
-
type: 'SET_EMAILS',
|
|
259
|
-
payload: {
|
|
260
|
-
emails: response.data.emails,
|
|
261
|
-
total: response.data.total,
|
|
262
|
-
offset: params.offset || 0,
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
},
|
|
267
|
-
|
|
268
|
-
fetchAccounts: async () => {
|
|
269
|
-
const response = await api.getAccounts();
|
|
270
|
-
if (response.data) {
|
|
271
|
-
dispatch({ type: 'SET_ACCOUNTS', payload: response.data.accounts });
|
|
272
|
-
}
|
|
273
|
-
},
|
|
274
|
-
|
|
275
|
-
fetchRules: async () => {
|
|
276
|
-
const response = await api.getRules();
|
|
277
|
-
if (response.data) {
|
|
278
|
-
dispatch({ type: 'SET_RULES', payload: response.data.rules });
|
|
279
|
-
}
|
|
280
|
-
},
|
|
281
|
-
|
|
282
|
-
fetchSettings: async () => {
|
|
283
|
-
const response = await api.getSettings();
|
|
284
|
-
if (response.data) {
|
|
285
|
-
dispatch({ type: 'SET_SETTINGS', payload: response.data.settings });
|
|
286
|
-
}
|
|
287
|
-
},
|
|
288
|
-
|
|
289
|
-
fetchProfile: async () => {
|
|
290
|
-
const response = await api.getProfile();
|
|
291
|
-
if (response.data) {
|
|
292
|
-
dispatch({ type: 'SET_PROFILE', payload: response.data });
|
|
293
|
-
}
|
|
294
|
-
},
|
|
295
|
-
|
|
296
|
-
fetchStats: async () => {
|
|
297
|
-
const response = await api.getStats();
|
|
298
|
-
if (response.data) {
|
|
299
|
-
dispatch({ type: 'SET_STATS', payload: response.data.stats });
|
|
300
|
-
}
|
|
301
|
-
},
|
|
302
|
-
|
|
303
|
-
executeAction: async (emailId: string, action: string, draftContent?: string) => {
|
|
304
|
-
const response = await api.executeAction(emailId, action, draftContent);
|
|
305
|
-
if (response.data?.success) {
|
|
306
|
-
// Update local state
|
|
307
|
-
const email = state.emails.find(e => e.id === emailId);
|
|
308
|
-
if (email) {
|
|
309
|
-
dispatch({
|
|
310
|
-
type: 'UPDATE_EMAIL',
|
|
311
|
-
payload: { ...email, action_taken: action as any }
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
return true;
|
|
315
|
-
}
|
|
316
|
-
dispatch({ type: 'SET_ERROR', payload: getErrorMessage(response.error, 'Action failed') });
|
|
317
|
-
return false;
|
|
318
|
-
},
|
|
319
|
-
|
|
320
|
-
triggerSync: async (accountId?: string) => {
|
|
321
|
-
const response = accountId
|
|
322
|
-
? await api.triggerSync(accountId)
|
|
323
|
-
: await api.syncAll();
|
|
324
|
-
|
|
325
|
-
// Always refresh accounts to show updated sync status
|
|
326
|
-
await actions.fetchAccounts();
|
|
327
|
-
|
|
328
|
-
if (response.error) {
|
|
329
|
-
dispatch({ type: 'SET_ERROR', payload: getErrorMessage(response.error, 'Sync failed') });
|
|
330
|
-
return false;
|
|
331
|
-
}
|
|
332
|
-
return true;
|
|
333
|
-
},
|
|
334
|
-
|
|
335
|
-
disconnectAccount: async (accountId: string) => {
|
|
336
|
-
const response = await api.disconnectAccount(accountId);
|
|
337
|
-
if (response.data?.success) {
|
|
338
|
-
dispatch({ type: 'REMOVE_ACCOUNT', payload: accountId });
|
|
339
|
-
return true;
|
|
340
|
-
}
|
|
341
|
-
dispatch({ type: 'SET_ERROR', payload: getErrorMessage(response.error, 'Failed to disconnect') });
|
|
342
|
-
return false;
|
|
343
|
-
},
|
|
344
|
-
|
|
345
|
-
updateSettings: async (settings: Partial<UserSettings>) => {
|
|
346
|
-
const response = await api.updateSettings(settings);
|
|
347
|
-
if (response.data) {
|
|
348
|
-
dispatch({ type: 'SET_SETTINGS', payload: response.data.settings });
|
|
349
|
-
return true;
|
|
350
|
-
}
|
|
351
|
-
dispatch({ type: 'SET_ERROR', payload: getErrorMessage(response.error, 'Failed to update settings') });
|
|
352
|
-
return false;
|
|
353
|
-
},
|
|
354
|
-
|
|
355
|
-
updateProfile: async (updates: { first_name?: string; last_name?: string; avatar_url?: string }) => {
|
|
356
|
-
const response = await api.updateProfile(updates);
|
|
357
|
-
if (response.data) {
|
|
358
|
-
dispatch({ type: 'UPDATE_PROFILE', payload: response.data });
|
|
359
|
-
return true;
|
|
360
|
-
}
|
|
361
|
-
dispatch({ type: 'SET_ERROR', payload: getErrorMessage(response.error, 'Failed to update profile') });
|
|
362
|
-
return false;
|
|
363
|
-
},
|
|
364
|
-
|
|
365
|
-
updateAccount: async (accountId: string, updates: Partial<EmailAccount>) => {
|
|
366
|
-
const response = await api.updateAccount(accountId, updates);
|
|
367
|
-
if (response.data) {
|
|
368
|
-
dispatch({
|
|
369
|
-
type: 'SET_ACCOUNTS',
|
|
370
|
-
payload: state.accounts.map(a => a.id === accountId ? { ...a, ...updates } : a)
|
|
371
|
-
});
|
|
372
|
-
return true;
|
|
373
|
-
}
|
|
374
|
-
dispatch({ type: 'SET_ERROR', payload: getErrorMessage(response.error, 'Failed to update account') });
|
|
375
|
-
return false;
|
|
376
|
-
},
|
|
377
|
-
|
|
378
|
-
createRule: async (rule: Omit<Rule, 'id' | 'user_id' | 'created_at'>) => {
|
|
379
|
-
const response = await api.createRule(rule);
|
|
380
|
-
if (response.data) {
|
|
381
|
-
dispatch({ type: 'ADD_RULE', payload: response.data.rule });
|
|
382
|
-
return true;
|
|
383
|
-
}
|
|
384
|
-
dispatch({ type: 'SET_ERROR', payload: getErrorMessage(response.error, 'Failed to create rule') });
|
|
385
|
-
return false;
|
|
386
|
-
},
|
|
387
|
-
|
|
388
|
-
deleteRule: async (ruleId: string) => {
|
|
389
|
-
const response = await api.deleteRule(ruleId);
|
|
390
|
-
if (response.data?.success) {
|
|
391
|
-
dispatch({ type: 'REMOVE_RULE', payload: ruleId });
|
|
392
|
-
return true;
|
|
393
|
-
}
|
|
394
|
-
dispatch({ type: 'SET_ERROR', payload: getErrorMessage(response.error, 'Failed to delete rule') });
|
|
395
|
-
return false;
|
|
396
|
-
},
|
|
397
|
-
|
|
398
|
-
toggleRule: async (ruleId: string) => {
|
|
399
|
-
const response = await api.toggleRule(ruleId);
|
|
400
|
-
if (response.data) {
|
|
401
|
-
dispatch({ type: 'UPDATE_RULE', payload: response.data.rule });
|
|
402
|
-
return true;
|
|
403
|
-
}
|
|
404
|
-
dispatch({ type: 'SET_ERROR', payload: getErrorMessage(response.error, 'Failed to toggle rule') });
|
|
405
|
-
return false;
|
|
406
|
-
},
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
return (
|
|
410
|
-
<AppContext.Provider value={{ state, dispatch, actions }}>
|
|
411
|
-
{children}
|
|
412
|
-
</AppContext.Provider>
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
export function useApp() {
|
|
417
|
-
const context = useContext(AppContext);
|
|
418
|
-
if (!context) {
|
|
419
|
-
throw new Error('useApp must be used within AppProvider');
|
|
420
|
-
}
|
|
421
|
-
return context;
|
|
422
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext, type ReactNode } from "react";
|
|
2
|
-
import type { MigrationStatus } from "../lib/migration-check";
|
|
3
|
-
|
|
4
|
-
interface MigrationContextValue {
|
|
5
|
-
/** Current migration status */
|
|
6
|
-
migrationStatus: MigrationStatus | null;
|
|
7
|
-
/** Whether the migration banner is currently showing */
|
|
8
|
-
showMigrationBanner: boolean;
|
|
9
|
-
/** Whether the migration modal is currently showing */
|
|
10
|
-
showMigrationModal: boolean;
|
|
11
|
-
/** Open the migration modal */
|
|
12
|
-
openMigrationModal: () => void;
|
|
13
|
-
/** Whether the migration banner should be suppressed (e.g., when showing Setup Guide) */
|
|
14
|
-
suppressMigrationBanner: boolean;
|
|
15
|
-
/** Set whether to suppress the migration banner */
|
|
16
|
-
setSuppressMigrationBanner: (suppress: boolean) => void;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const MigrationContext = createContext<MigrationContextValue | undefined>(
|
|
20
|
-
undefined,
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
export function MigrationProvider({
|
|
24
|
-
children,
|
|
25
|
-
value,
|
|
26
|
-
}: {
|
|
27
|
-
children: ReactNode;
|
|
28
|
-
value: MigrationContextValue;
|
|
29
|
-
}) {
|
|
30
|
-
return (
|
|
31
|
-
<MigrationContext.Provider value={value}>
|
|
32
|
-
{children}
|
|
33
|
-
</MigrationContext.Provider>
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function useMigrationContext() {
|
|
38
|
-
const context = useContext(MigrationContext);
|
|
39
|
-
if (context === undefined) {
|
|
40
|
-
throw new Error(
|
|
41
|
-
"useMigrationContext must be used within a MigrationProvider",
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
return context;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Safe version that returns null if used outside provider
|
|
49
|
-
* Useful for components that may render outside the provider
|
|
50
|
-
*/
|
|
51
|
-
export function useMigrationContextSafe() {
|
|
52
|
-
return useContext(MigrationContext);
|
|
53
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { createContext, useContext, useState, ReactNode } from 'react';
|
|
2
|
-
|
|
3
|
-
interface TerminalContextType {
|
|
4
|
-
isExpanded: boolean;
|
|
5
|
-
setIsExpanded: (expanded: boolean) => void;
|
|
6
|
-
openTerminal: () => void;
|
|
7
|
-
closeTerminal: () => void;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const TerminalContext = createContext<TerminalContextType | null>(null);
|
|
11
|
-
|
|
12
|
-
export function TerminalProvider({ children }: { children: ReactNode }) {
|
|
13
|
-
const [isExpanded, setIsExpanded] = useState(false);
|
|
14
|
-
|
|
15
|
-
const openTerminal = () => setIsExpanded(true);
|
|
16
|
-
const closeTerminal = () => setIsExpanded(false);
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<TerminalContext.Provider value={{ isExpanded, setIsExpanded, openTerminal, closeTerminal }}>
|
|
20
|
-
{children}
|
|
21
|
-
</TerminalContext.Provider>
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function useTerminal() {
|
|
26
|
-
const context = useContext(TerminalContext);
|
|
27
|
-
if (!context) {
|
|
28
|
-
throw new Error('useTerminal must be used within a TerminalProvider');
|
|
29
|
-
}
|
|
30
|
-
return context;
|
|
31
|
-
}
|
package/src/core/actions.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import { google } from 'googleapis';
|
|
2
|
-
|
|
3
|
-
export class EmailActions {
|
|
4
|
-
private supabase: any;
|
|
5
|
-
|
|
6
|
-
constructor(supabaseClient?: any) {
|
|
7
|
-
this.supabase = supabaseClient;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
async executeAction(emailId: string, action: 'delete' | 'archive' | 'draft', draftContent?: string) {
|
|
11
|
-
if (!this.supabase) throw new Error('Supabase client not configured');
|
|
12
|
-
|
|
13
|
-
// 1. Fetch email and account details
|
|
14
|
-
const { data: email, error: emailError } = await this.supabase
|
|
15
|
-
.from('emails')
|
|
16
|
-
.select('*, email_accounts(*)')
|
|
17
|
-
.eq('id', emailId)
|
|
18
|
-
.single();
|
|
19
|
-
|
|
20
|
-
if (emailError || !email) throw new Error('Email not found');
|
|
21
|
-
const account = email.email_accounts;
|
|
22
|
-
|
|
23
|
-
if (account.provider === 'gmail') {
|
|
24
|
-
await this.executeGmailAction(account, email.external_id, action, draftContent);
|
|
25
|
-
} else {
|
|
26
|
-
console.log('Outlook actions not implemented yet');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// 2. Update status in Supabase
|
|
30
|
-
await this.supabase.from('emails').update({ action_taken: action }).eq('id', emailId);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
private async executeGmailAction(account: any, messageId: string, action: string, draftContent?: string) {
|
|
34
|
-
const auth = new google.auth.OAuth2(
|
|
35
|
-
process.env.GMAIL_CLIENT_ID,
|
|
36
|
-
process.env.GMAIL_CLIENT_SECRET
|
|
37
|
-
);
|
|
38
|
-
auth.setCredentials({
|
|
39
|
-
access_token: account.access_token,
|
|
40
|
-
refresh_token: account.refresh_token,
|
|
41
|
-
expiry_date: account.token_expires_at ? new Date(account.token_expires_at).getTime() : undefined
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const gmail = google.gmail({ version: 'v1', auth });
|
|
45
|
-
|
|
46
|
-
if (action === 'delete') {
|
|
47
|
-
await gmail.users.messages.trash({ userId: 'me', id: messageId });
|
|
48
|
-
} else if (action === 'archive') {
|
|
49
|
-
await gmail.users.messages.modify({
|
|
50
|
-
userId: 'me',
|
|
51
|
-
id: messageId,
|
|
52
|
-
requestBody: { removeLabelIds: ['INBOX'] }
|
|
53
|
-
});
|
|
54
|
-
} else if (action === 'draft' && draftContent) {
|
|
55
|
-
// Get original message for thread continuity (optional but better)
|
|
56
|
-
const original = await gmail.users.messages.get({ userId: 'me', id: messageId });
|
|
57
|
-
const threadId = original.data.threadId;
|
|
58
|
-
|
|
59
|
-
await gmail.users.drafts.create({
|
|
60
|
-
userId: 'me',
|
|
61
|
-
requestBody: {
|
|
62
|
-
message: {
|
|
63
|
-
threadId: threadId,
|
|
64
|
-
raw: Buffer.from(
|
|
65
|
-
`To: ${original.data.payload?.headers?.find(h => h.name === 'From')?.value || ''} \r\n` +
|
|
66
|
-
`Subject: Re: ${original.data.payload?.headers?.find(h => h.name === 'Subject')?.value || ''} \r\n` +
|
|
67
|
-
`In - Reply - To: ${messageId} \r\n` +
|
|
68
|
-
`References: ${messageId} \r\n\r\n` +
|
|
69
|
-
`${draftContent} `
|
|
70
|
-
).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
package/src/core/auth.ts
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { google } from 'googleapis';
|
|
2
|
-
import * as msal from '@azure/msal-node';
|
|
3
|
-
|
|
4
|
-
export class GmailHandler {
|
|
5
|
-
private oauth2Client;
|
|
6
|
-
|
|
7
|
-
constructor() {
|
|
8
|
-
this.oauth2Client = new google.auth.OAuth2(
|
|
9
|
-
process.env.GMAIL_CLIENT_ID,
|
|
10
|
-
process.env.GMAIL_CLIENT_SECRET,
|
|
11
|
-
'urn:ietf:wg:oauth:2.0:oob'
|
|
12
|
-
);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async getAuthUrl() {
|
|
16
|
-
const scopes = ['https://www.googleapis.com/auth/gmail.readonly'];
|
|
17
|
-
return this.oauth2Client.generateAuthUrl({
|
|
18
|
-
access_type: 'offline',
|
|
19
|
-
scope: scopes,
|
|
20
|
-
prompt: 'consent'
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async handleCallback(code: string) {
|
|
25
|
-
const { tokens } = await this.oauth2Client.getToken(code);
|
|
26
|
-
this.oauth2Client.setCredentials(tokens);
|
|
27
|
-
|
|
28
|
-
// Return tokens for the caller to save
|
|
29
|
-
return tokens;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async authenticate(code: string, userId: string, emailAddress: string, supabaseClient: any) {
|
|
33
|
-
const { tokens } = await this.oauth2Client.getToken(code);
|
|
34
|
-
this.oauth2Client.setCredentials(tokens);
|
|
35
|
-
|
|
36
|
-
// Save tokens to Supabase
|
|
37
|
-
const { error } = await supabaseClient
|
|
38
|
-
.from('email_accounts')
|
|
39
|
-
.upsert({
|
|
40
|
-
user_id: userId,
|
|
41
|
-
email_address: emailAddress,
|
|
42
|
-
provider: 'gmail',
|
|
43
|
-
access_token: tokens.access_token,
|
|
44
|
-
refresh_token: tokens.refresh_token,
|
|
45
|
-
token_expires_at: tokens.expiry_date ? new Date(tokens.expiry_date).toISOString() : null,
|
|
46
|
-
scopes: tokens.scope ? tokens.scope.split(' ') : [],
|
|
47
|
-
is_active: true
|
|
48
|
-
}, { onConflict: 'user_id, email_address' });
|
|
49
|
-
|
|
50
|
-
if (error) throw error;
|
|
51
|
-
return tokens;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export class MicrosoftGraphHandler {
|
|
56
|
-
private cca;
|
|
57
|
-
|
|
58
|
-
constructor() {
|
|
59
|
-
const config = {
|
|
60
|
-
auth: {
|
|
61
|
-
clientId: process.env.MS_GRAPH_CLIENT_ID || '',
|
|
62
|
-
authority: `https://login.microsoftonline.com/${process.env.MS_GRAPH_TENANT_ID || 'common'}`,
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
this.cca = new msal.PublicClientApplication(config);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async initiateDeviceFlow() {
|
|
69
|
-
const scopes = ['https://graph.microsoft.com/Mail.Read'];
|
|
70
|
-
const deviceCodeRequest = {
|
|
71
|
-
deviceCodeCallback: (response: any) => {
|
|
72
|
-
return response;
|
|
73
|
-
},
|
|
74
|
-
scopes: scopes,
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const response = await this.cca.acquireTokenByDeviceCode(deviceCodeRequest);
|
|
78
|
-
return {
|
|
79
|
-
device_code: (response as any).deviceCode,
|
|
80
|
-
user_code: (response as any).userCode,
|
|
81
|
-
message: (response as any).message
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async completeDeviceFlow(deviceCode: string) {
|
|
86
|
-
// The device flow is already handled by acquireTokenByDeviceCode
|
|
87
|
-
// This is a placeholder for completing the flow
|
|
88
|
-
// In practice, you'd poll or wait for the user to complete auth
|
|
89
|
-
return { success: true };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async saveToken(userId: string, emailAddress: string, response: msal.AuthenticationResult, supabaseClient: any) {
|
|
93
|
-
const { error } = await supabaseClient
|
|
94
|
-
.from('email_accounts')
|
|
95
|
-
.upsert({
|
|
96
|
-
user_id: userId,
|
|
97
|
-
email_address: emailAddress,
|
|
98
|
-
provider: 'outlook',
|
|
99
|
-
access_token: response.accessToken,
|
|
100
|
-
refresh_token: (response as any).refreshToken || null, // msal-node handles refresh tokens in cache, but we might want to store it
|
|
101
|
-
token_expires_at: response.expiresOn ? response.expiresOn.toISOString() : null,
|
|
102
|
-
scopes: response.scopes,
|
|
103
|
-
is_active: true
|
|
104
|
-
}, { onConflict: 'user_id, email_address' });
|
|
105
|
-
|
|
106
|
-
if (error) throw error;
|
|
107
|
-
}
|
|
108
|
-
}
|