@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.
Files changed (48) hide show
  1. package/api/server.ts +0 -6
  2. package/api/src/config/index.ts +3 -0
  3. package/bin/email-automator-setup.js +2 -3
  4. package/bin/email-automator.js +23 -7
  5. package/package.json +1 -2
  6. package/src/App.tsx +0 -622
  7. package/src/components/AccountSettings.tsx +0 -310
  8. package/src/components/AccountSettingsPage.tsx +0 -390
  9. package/src/components/Configuration.tsx +0 -1345
  10. package/src/components/Dashboard.tsx +0 -940
  11. package/src/components/ErrorBoundary.tsx +0 -71
  12. package/src/components/LiveTerminal.tsx +0 -308
  13. package/src/components/LoadingSpinner.tsx +0 -39
  14. package/src/components/Login.tsx +0 -371
  15. package/src/components/Logo.tsx +0 -57
  16. package/src/components/SetupWizard.tsx +0 -388
  17. package/src/components/Toast.tsx +0 -109
  18. package/src/components/migration/MigrationBanner.tsx +0 -97
  19. package/src/components/migration/MigrationModal.tsx +0 -458
  20. package/src/components/migration/MigrationPulseIndicator.tsx +0 -38
  21. package/src/components/mode-toggle.tsx +0 -24
  22. package/src/components/theme-provider.tsx +0 -72
  23. package/src/components/ui/alert.tsx +0 -66
  24. package/src/components/ui/button.tsx +0 -57
  25. package/src/components/ui/card.tsx +0 -75
  26. package/src/components/ui/dialog.tsx +0 -133
  27. package/src/components/ui/input.tsx +0 -22
  28. package/src/components/ui/label.tsx +0 -24
  29. package/src/components/ui/otp-input.tsx +0 -184
  30. package/src/context/AppContext.tsx +0 -422
  31. package/src/context/MigrationContext.tsx +0 -53
  32. package/src/context/TerminalContext.tsx +0 -31
  33. package/src/core/actions.ts +0 -76
  34. package/src/core/auth.ts +0 -108
  35. package/src/core/intelligence.ts +0 -76
  36. package/src/core/processor.ts +0 -112
  37. package/src/hooks/useRealtimeEmails.ts +0 -111
  38. package/src/index.css +0 -140
  39. package/src/lib/api-config.ts +0 -42
  40. package/src/lib/api-old.ts +0 -228
  41. package/src/lib/api.ts +0 -421
  42. package/src/lib/migration-check.ts +0 -264
  43. package/src/lib/sounds.ts +0 -120
  44. package/src/lib/supabase-config.ts +0 -117
  45. package/src/lib/supabase.ts +0 -28
  46. package/src/lib/types.ts +0 -166
  47. package/src/lib/utils.ts +0 -6
  48. 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
- }
@@ -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
- }