@realtimex/email-automator 2.2.0 → 2.3.0

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 (74) hide show
  1. package/api/server.ts +4 -8
  2. package/api/src/config/index.ts +6 -3
  3. package/bin/email-automator-setup.js +2 -3
  4. package/bin/email-automator.js +7 -11
  5. package/dist/api/server.js +109 -0
  6. package/dist/api/src/config/index.js +88 -0
  7. package/dist/api/src/middleware/auth.js +119 -0
  8. package/dist/api/src/middleware/errorHandler.js +78 -0
  9. package/dist/api/src/middleware/index.js +4 -0
  10. package/dist/api/src/middleware/rateLimit.js +57 -0
  11. package/dist/api/src/middleware/validation.js +111 -0
  12. package/dist/api/src/routes/actions.js +173 -0
  13. package/dist/api/src/routes/auth.js +106 -0
  14. package/dist/api/src/routes/emails.js +100 -0
  15. package/dist/api/src/routes/health.js +33 -0
  16. package/dist/api/src/routes/index.js +19 -0
  17. package/dist/api/src/routes/migrate.js +61 -0
  18. package/dist/api/src/routes/rules.js +104 -0
  19. package/dist/api/src/routes/settings.js +178 -0
  20. package/dist/api/src/routes/sync.js +118 -0
  21. package/dist/api/src/services/eventLogger.js +41 -0
  22. package/dist/api/src/services/gmail.js +350 -0
  23. package/dist/api/src/services/intelligence.js +243 -0
  24. package/dist/api/src/services/microsoft.js +256 -0
  25. package/dist/api/src/services/processor.js +503 -0
  26. package/dist/api/src/services/scheduler.js +210 -0
  27. package/dist/api/src/services/supabase.js +59 -0
  28. package/dist/api/src/utils/contentCleaner.js +94 -0
  29. package/dist/api/src/utils/crypto.js +68 -0
  30. package/dist/api/src/utils/logger.js +119 -0
  31. package/package.json +5 -5
  32. package/src/App.tsx +0 -622
  33. package/src/components/AccountSettings.tsx +0 -310
  34. package/src/components/AccountSettingsPage.tsx +0 -390
  35. package/src/components/Configuration.tsx +0 -1345
  36. package/src/components/Dashboard.tsx +0 -940
  37. package/src/components/ErrorBoundary.tsx +0 -71
  38. package/src/components/LiveTerminal.tsx +0 -308
  39. package/src/components/LoadingSpinner.tsx +0 -39
  40. package/src/components/Login.tsx +0 -371
  41. package/src/components/Logo.tsx +0 -57
  42. package/src/components/SetupWizard.tsx +0 -388
  43. package/src/components/Toast.tsx +0 -109
  44. package/src/components/migration/MigrationBanner.tsx +0 -97
  45. package/src/components/migration/MigrationModal.tsx +0 -458
  46. package/src/components/migration/MigrationPulseIndicator.tsx +0 -38
  47. package/src/components/mode-toggle.tsx +0 -24
  48. package/src/components/theme-provider.tsx +0 -72
  49. package/src/components/ui/alert.tsx +0 -66
  50. package/src/components/ui/button.tsx +0 -57
  51. package/src/components/ui/card.tsx +0 -75
  52. package/src/components/ui/dialog.tsx +0 -133
  53. package/src/components/ui/input.tsx +0 -22
  54. package/src/components/ui/label.tsx +0 -24
  55. package/src/components/ui/otp-input.tsx +0 -184
  56. package/src/context/AppContext.tsx +0 -422
  57. package/src/context/MigrationContext.tsx +0 -53
  58. package/src/context/TerminalContext.tsx +0 -31
  59. package/src/core/actions.ts +0 -76
  60. package/src/core/auth.ts +0 -108
  61. package/src/core/intelligence.ts +0 -76
  62. package/src/core/processor.ts +0 -112
  63. package/src/hooks/useRealtimeEmails.ts +0 -111
  64. package/src/index.css +0 -140
  65. package/src/lib/api-config.ts +0 -42
  66. package/src/lib/api-old.ts +0 -228
  67. package/src/lib/api.ts +0 -421
  68. package/src/lib/migration-check.ts +0 -264
  69. package/src/lib/sounds.ts +0 -120
  70. package/src/lib/supabase-config.ts +0 -117
  71. package/src/lib/supabase.ts +0 -28
  72. package/src/lib/types.ts +0 -166
  73. package/src/lib/utils.ts +0 -6
  74. package/src/main.tsx +0 -10
@@ -1,388 +0,0 @@
1
- import { useState } from 'react';
2
- import {
3
- Dialog,
4
- DialogContent,
5
- DialogDescription,
6
- DialogHeader,
7
- DialogTitle,
8
- DialogFooter
9
- } from './ui/dialog';
10
- import { Button } from './ui/button';
11
- import { Input } from './ui/input';
12
- import { Label } from './ui/label';
13
- import { Alert, AlertDescription } from './ui/alert';
14
- import {
15
- Loader2,
16
- Database,
17
- CheckCircle,
18
- AlertCircle,
19
- ExternalLink,
20
- Check,
21
- } from 'lucide-react';
22
- import {
23
- saveSupabaseConfig,
24
- validateSupabaseConnection,
25
- } from '../lib/supabase-config';
26
- import { Logo } from './Logo';
27
-
28
- type WizardStep = 'welcome' | 'credentials' | 'validating' | 'success';
29
-
30
- interface SetupWizardProps {
31
- onComplete: () => void;
32
- open?: boolean;
33
- canClose?: boolean;
34
- }
35
-
36
- /**
37
- * Normalizes Supabase URL input - accepts either full URL or just project ID
38
- */
39
- function normalizeSupabaseUrl(input: string): string {
40
- const trimmed = input.trim();
41
- if (!trimmed) return '';
42
-
43
- // If it starts with http:// or https://, treat as full URL
44
- if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
45
- return trimmed;
46
- }
47
-
48
- // Otherwise, treat as project ID and construct full URL
49
- return `https://${trimmed}.supabase.co`;
50
- }
51
-
52
- /**
53
- * Validates if input looks like a valid Supabase URL or project ID
54
- */
55
- function validateUrlFormat(input: string): {
56
- valid: boolean;
57
- message?: string;
58
- } {
59
- const trimmed = input.trim();
60
- if (!trimmed) return { valid: false };
61
-
62
- // Check if it's a full URL
63
- if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
64
- try {
65
- const url = new URL(trimmed);
66
- if (url.hostname.endsWith('.supabase.co')) {
67
- return {
68
- valid: true,
69
- message: "Valid Supabase URL",
70
- };
71
- }
72
- return {
73
- valid: false,
74
- message: "URL must be a supabase.co domain",
75
- };
76
- } catch {
77
- return {
78
- valid: false,
79
- message: "Invalid URL format",
80
- };
81
- }
82
- }
83
-
84
- // Check if it's a project ID (alphanumeric, typically 20 chars)
85
- if (/^[a-z0-9]+$/.test(trimmed)) {
86
- return {
87
- valid: true,
88
- message: "Project ID detected",
89
- };
90
- }
91
-
92
- return { valid: false, message: "Enter a URL or Project ID" };
93
- }
94
-
95
- /**
96
- * Validates if input looks like a valid Supabase API key
97
- */
98
- function validateKeyFormat(input: string): {
99
- valid: boolean;
100
- message?: string;
101
- } {
102
- const trimmed = input.trim();
103
- if (!trimmed) return { valid: false };
104
-
105
- // New publishable keys start with "sb_publishable_" followed by key content
106
- if (trimmed.startsWith("sb_publishable_")) {
107
- // Check that there's actual key content after the prefix
108
- if (trimmed.length > "sb_publishable_".length + 10) {
109
- return {
110
- valid: true,
111
- message: "Valid Publishable Key",
112
- };
113
- }
114
- return {
115
- valid: false,
116
- message: "Incomplete Publishable Key",
117
- };
118
- }
119
-
120
- // Legacy anon keys are JWT tokens starting with "eyJ"
121
- if (trimmed.startsWith("eyJ")) {
122
- if (trimmed.length > 50) {
123
- return {
124
- valid: true,
125
- message: "Valid Anon Key",
126
- };
127
- }
128
- return {
129
- valid: false,
130
- message: "Incomplete Anon Key",
131
- };
132
- }
133
-
134
- return {
135
- valid: false,
136
- message: "Invalid API Key format",
137
- };
138
- }
139
-
140
- export function SetupWizard({ onComplete, open = true, canClose = false }: SetupWizardProps) {
141
- const [step, setStep] = useState<WizardStep>('welcome');
142
- const [url, setUrl] = useState('');
143
- const [anonKey, setAnonKey] = useState('');
144
- const [error, setError] = useState<string | null>(null);
145
- const [urlTouched, setUrlTouched] = useState(false);
146
- const [keyTouched, setKeyTouched] = useState(false);
147
-
148
- const handleValidateAndSave = async () => {
149
- setError(null);
150
- setStep('validating');
151
-
152
- // Normalize the URL before validation
153
- const normalizedUrl = normalizeSupabaseUrl(url);
154
- const trimmedKey = anonKey.trim();
155
-
156
- const result = await validateSupabaseConnection(normalizedUrl, trimmedKey);
157
-
158
- if (result.valid) {
159
- saveSupabaseConfig({ url: normalizedUrl, anonKey: trimmedKey });
160
- setStep('success');
161
-
162
- // Completes the flow by reloading to pick up new config
163
- setTimeout(() => {
164
- window.location.reload();
165
- }, 1000);
166
- } else {
167
- setError(result.error || 'Connection failed. Please check your credentials.');
168
- setStep('credentials');
169
- }
170
- };
171
-
172
- // Get validation states
173
- const urlValidation = url ? validateUrlFormat(url) : { valid: false };
174
- const keyValidation = anonKey ? validateKeyFormat(anonKey) : { valid: false };
175
- const normalizedUrl = url ? normalizeSupabaseUrl(url) : '';
176
- const showUrlExpansion = url && !url.startsWith('http') && urlValidation.valid;
177
-
178
- return (
179
- <Dialog open={open} onOpenChange={(val) => {
180
- if (!val && canClose) onComplete();
181
- }}>
182
- <DialogContent
183
- className="sm:max-w-md"
184
- onPointerDownOutside={(e) => { if (!canClose) e.preventDefault() }}
185
- onEscapeKeyDown={(e) => { if (!canClose) e.preventDefault() }}
186
- >
187
- {step === 'welcome' && (
188
- <>
189
- <DialogHeader>
190
- <div className="flex items-center gap-2 mb-2">
191
- <Logo className="w-9 h-9" />
192
- <DialogTitle>Welcome to Email Automator</DialogTitle>
193
- </div>
194
- <DialogDescription>
195
- This application requires a Supabase database to store your emails and automation rules.
196
- </DialogDescription>
197
- </DialogHeader>
198
-
199
- <div className="space-y-4 py-4">
200
- <Alert>
201
- <AlertDescription>
202
- <strong>No Supabase Setup Detected</strong>
203
- <br />
204
- You can create a free project at{' '}
205
- <a
206
- href="https://supabase.com"
207
- target="_blank"
208
- rel="noopener noreferrer"
209
- className="underline text-primary inline-flex items-center gap-1"
210
- >
211
- supabase.com
212
- <ExternalLink className="h-3 w-3" />
213
- </a>
214
- </AlertDescription>
215
- </Alert>
216
-
217
- <div className="space-y-2">
218
- <h4 className="font-medium text-sm">You will need:</h4>
219
- <ul className="list-disc list-inside text-sm text-muted-foreground space-y-1">
220
- <li>Project URL</li>
221
- <li>Anon Public Key</li>
222
- </ul>
223
- </div>
224
-
225
- <div className="flex gap-2">
226
- {canClose && (
227
- <Button variant="outline" onClick={onComplete} className="flex-1">
228
- Close
229
- </Button>
230
- )}
231
- <Button onClick={() => setStep('credentials')} className="flex-1">
232
- Continue Setup
233
- </Button>
234
- </div>
235
- </div>
236
- </>
237
- )}
238
-
239
- {step === 'credentials' && (
240
- <>
241
- <DialogHeader>
242
- <DialogTitle>Connect to Supabase</DialogTitle>
243
- <DialogDescription>
244
- Enter your connection details below.
245
- </DialogDescription>
246
- </DialogHeader>
247
-
248
- <div className="space-y-4 py-4">
249
- {error && (
250
- <Alert variant="destructive">
251
- <AlertCircle className="h-4 w-4" />
252
- <AlertDescription>{error}</AlertDescription>
253
- </Alert>
254
- )}
255
-
256
- <div className="space-y-2">
257
- <Label htmlFor="supabase-url">Project URL or ID</Label>
258
- <div className="relative">
259
- <Input
260
- id="supabase-url"
261
- placeholder="https://xxx.supabase.co or project-id"
262
- value={url}
263
- onChange={(e) => {
264
- setUrl(e.target.value);
265
- setUrlTouched(true);
266
- }}
267
- onBlur={() => setUrlTouched(true)}
268
- className={
269
- urlTouched && url
270
- ? urlValidation.valid
271
- ? 'pr-8 border-green-500'
272
- : 'pr-8 border-destructive'
273
- : ''
274
- }
275
- />
276
- {urlTouched && url && urlValidation.valid && (
277
- <Check className="absolute right-2 top-1/2 -translate-y-1/2 h-4 w-4 text-green-500" />
278
- )}
279
- </div>
280
- {showUrlExpansion && (
281
- <div className="flex items-start gap-1.5 text-xs text-green-600 dark:text-green-400">
282
- <Check className="h-3 w-3 mt-0.5 flex-shrink-0" />
283
- <span>Will use: {normalizedUrl}</span>
284
- </div>
285
- )}
286
- {urlTouched && url && urlValidation.message && !urlValidation.valid && (
287
- <p className="text-xs text-destructive">{urlValidation.message}</p>
288
- )}
289
- </div>
290
-
291
- <div className="space-y-2">
292
- <Label htmlFor="anon-key">Anon Public Key</Label>
293
- <div className="relative">
294
- <Input
295
- id="anon-key"
296
- type="password"
297
- placeholder="eyJ..."
298
- value={anonKey}
299
- onChange={(e) => {
300
- setAnonKey(e.target.value);
301
- setKeyTouched(true);
302
- }}
303
- onBlur={() => setKeyTouched(true)}
304
- className={
305
- keyTouched && anonKey
306
- ? keyValidation.valid
307
- ? 'pr-8 border-green-500'
308
- : 'pr-8 border-destructive'
309
- : ''
310
- }
311
- />
312
- {keyTouched && anonKey && keyValidation.valid && (
313
- <Check className="absolute right-2 top-1/2 -translate-y-1/2 h-4 w-4 text-green-500" />
314
- )}
315
- </div>
316
- {keyTouched && anonKey && keyValidation.message && (
317
- <p
318
- className={`text-xs ${keyValidation.valid ? 'text-green-600 dark:text-green-400' : 'text-destructive'}`}
319
- >
320
- {keyValidation.message}
321
- </p>
322
- )}
323
- </div>
324
-
325
- <div className="flex gap-2">
326
- <Button
327
- variant="outline"
328
- onClick={() => setStep('welcome')}
329
- className="flex-1"
330
- >
331
- Back
332
- </Button>
333
- {canClose && (
334
- <Button
335
- variant="outline"
336
- onClick={onComplete}
337
- className="flex-1"
338
- >
339
- Close
340
- </Button>
341
- )}
342
- <Button
343
- onClick={handleValidateAndSave}
344
- disabled={!urlValidation.valid || !keyValidation.valid}
345
- className="flex-1"
346
- >
347
- Connect
348
- </Button>
349
- </div>
350
- </div>
351
- </>
352
- )}
353
-
354
- {step === 'validating' && (
355
- <>
356
- <DialogHeader>
357
- <DialogTitle>Validating Connection</DialogTitle>
358
- <DialogDescription>
359
- Verifying your credentials with Supabase...
360
- </DialogDescription>
361
- </DialogHeader>
362
-
363
- <div className="flex flex-col items-center justify-center py-8">
364
- <Loader2 className="h-12 w-12 animate-spin text-primary mb-4" />
365
- <p className="text-sm text-muted-foreground">Please wait...</p>
366
- </div>
367
- </>
368
- )}
369
-
370
- {step === 'success' && (
371
- <>
372
- <DialogHeader>
373
- <DialogTitle>Success!</DialogTitle>
374
- <DialogDescription>
375
- Your database is connected.
376
- </DialogDescription>
377
- </DialogHeader>
378
-
379
- <div className="flex flex-col items-center justify-center py-8">
380
- <CheckCircle className="h-12 w-12 text-green-500 mb-4" />
381
- <p className="text-sm text-muted-foreground">Restarting application...</p>
382
- </div>
383
- </>
384
- )}
385
- </DialogContent>
386
- </Dialog>
387
- );
388
- }
@@ -1,109 +0,0 @@
1
- import { useEffect, useState } from 'react';
2
- import { X, CheckCircle, AlertCircle, Info, AlertTriangle } from 'lucide-react';
3
- import { cn } from '../lib/utils';
4
-
5
- export type ToastType = 'success' | 'error' | 'info' | 'warning';
6
-
7
- interface Toast {
8
- id: string;
9
- type: ToastType;
10
- message: string;
11
- duration?: number;
12
- }
13
-
14
- interface ToastContextType {
15
- toasts: Toast[];
16
- addToast: (type: ToastType, message: string, duration?: number) => void;
17
- removeToast: (id: string) => void;
18
- }
19
-
20
- // Simple toast store
21
- let toasts: Toast[] = [];
22
- let listeners: Set<() => void> = new Set();
23
-
24
- function notify() {
25
- listeners.forEach(listener => listener());
26
- }
27
-
28
- export const toast = {
29
- success: (message: string, duration?: number) => addToast('success', message, duration),
30
- error: (message: string, duration?: number) => addToast('error', message, duration),
31
- info: (message: string, duration?: number) => addToast('info', message, duration),
32
- warning: (message: string, duration?: number) => addToast('warning', message, duration),
33
- };
34
-
35
- function addToast(type: ToastType, message: string, duration = 5000) {
36
- const id = Math.random().toString(36).substr(2, 9);
37
- toasts = [...toasts, { id, type, message, duration }];
38
- notify();
39
-
40
- if (duration > 0) {
41
- setTimeout(() => removeToast(id), duration);
42
- }
43
- }
44
-
45
- function removeToast(id: string) {
46
- toasts = toasts.filter(t => t.id !== id);
47
- notify();
48
- }
49
-
50
- export function useToasts() {
51
- const [, setTick] = useState(0);
52
-
53
- useEffect(() => {
54
- const listener = () => setTick(t => t + 1);
55
- listeners.add(listener);
56
- return () => { listeners.delete(listener); };
57
- }, []);
58
-
59
- return toasts;
60
- }
61
-
62
- const icons = {
63
- success: CheckCircle,
64
- error: AlertCircle,
65
- info: Info,
66
- warning: AlertTriangle,
67
- };
68
-
69
- const styles = {
70
- success: 'bg-emerald-500/10 border-emerald-500/20 text-emerald-600 dark:text-emerald-400',
71
- error: 'bg-destructive/10 border-destructive/20 text-destructive',
72
- info: 'bg-blue-500/10 border-blue-500/20 text-blue-600 dark:text-blue-400',
73
- warning: 'bg-yellow-500/10 border-yellow-500/20 text-yellow-600 dark:text-yellow-400',
74
- };
75
-
76
- function ToastItem({ toast: t }: { toast: Toast }) {
77
- const Icon = icons[t.type];
78
-
79
- return (
80
- <div className={cn(
81
- 'flex items-start gap-3 p-4 rounded-lg border shadow-lg backdrop-blur-sm',
82
- 'animate-in slide-in-from-right-full duration-300',
83
- styles[t.type]
84
- )}>
85
- <Icon className="w-5 h-5 flex-shrink-0 mt-0.5" />
86
- <p className="text-sm flex-1">{t.message}</p>
87
- <button
88
- onClick={() => removeToast(t.id)}
89
- className="p-1 hover:bg-black/10 rounded transition-colors"
90
- >
91
- <X className="w-4 h-4" />
92
- </button>
93
- </div>
94
- );
95
- }
96
-
97
- export function ToastContainer() {
98
- const toastList = useToasts();
99
-
100
- if (toastList.length === 0) return null;
101
-
102
- return (
103
- <div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 max-w-sm w-full">
104
- {toastList.map(t => (
105
- <ToastItem key={t.id} toast={t} />
106
- ))}
107
- </div>
108
- );
109
- }
@@ -1,97 +0,0 @@
1
- /**
2
- * MigrationBanner Component
3
- *
4
- * Displays a compact floating notification in top-right corner when migration is required.
5
- * Non-blocking: allows users to continue using the app while showing the reminder.
6
- */
7
-
8
- import { useState } from "react";
9
- import { AlertTriangle, X } from "lucide-react";
10
- import { Button } from "../ui/button";
11
- import { toast } from "../Toast";
12
- import {
13
- dismissMigrationReminder,
14
- type MigrationStatus,
15
- } from "../../lib/migration-check";
16
-
17
- interface MigrationBannerProps {
18
- /** Migration status from checkMigrationStatus */
19
- status: MigrationStatus;
20
- /** Callback when banner is dismissed */
21
- onDismiss?: () => void;
22
- /** Callback when user clicks to open modal */
23
- onLearnMore?: () => void;
24
- }
25
-
26
- export function MigrationBanner({
27
- status,
28
- onDismiss,
29
- onLearnMore,
30
- }: MigrationBannerProps) {
31
- const [isVisible, setIsVisible] = useState(true);
32
-
33
- if (!isVisible) return null;
34
-
35
- const handleDismiss = () => {
36
- dismissMigrationReminder();
37
- setIsVisible(false);
38
- onDismiss?.();
39
- };
40
-
41
- const handleClick = () => {
42
- if (onLearnMore) {
43
- onLearnMore();
44
- } else {
45
- // Fallback: copy command
46
- navigator.clipboard.writeText("npx email-automator migrate");
47
- toast.success("Command copied to clipboard");
48
- }
49
- };
50
-
51
- return (
52
- <div className="fixed right-4 top-16 z-50 max-w-sm animate-in slide-in-from-top-5">
53
- <div className="rounded-lg border border-yellow-500 bg-yellow-50 p-4 shadow-lg dark:bg-yellow-950/90 dark:border-yellow-600">
54
- <div className="flex items-start gap-3">
55
- <AlertTriangle className="h-5 w-5 flex-shrink-0 text-yellow-600 dark:text-yellow-500 mt-0.5" />
56
- <div className="flex-1 space-y-2">
57
- <div className="flex items-start justify-between gap-2">
58
- <p className="text-sm font-medium text-yellow-900 dark:text-yellow-100">
59
- Database Update Required
60
- </p>
61
- <Button
62
- size="icon"
63
- variant="ghost"
64
- className="h-5 w-5 -mr-1 -mt-1 text-yellow-900 hover:bg-yellow-100 dark:text-yellow-100 dark:hover:bg-yellow-900/30"
65
- onClick={handleDismiss}
66
- >
67
- <X className="h-3 w-3" />
68
- <span className="sr-only">Dismiss</span>
69
- </Button>
70
- </div>
71
- <p className="text-xs text-yellow-800 dark:text-yellow-200">
72
- Your app version ({status.appVersion}) requires a database schema update.
73
- </p>
74
- <div className="flex gap-2">
75
- <Button
76
- size="sm"
77
- variant="default"
78
- className="h-7 text-xs bg-yellow-600 hover:bg-yellow-700 dark:bg-yellow-500 dark:hover:bg-yellow-600"
79
- onClick={handleClick}
80
- >
81
- Update Now
82
- </Button>
83
- <Button
84
- size="sm"
85
- variant="ghost"
86
- className="h-7 text-xs text-yellow-900 hover:bg-yellow-100 dark:text-yellow-100 dark:hover:bg-yellow-900/30"
87
- onClick={handleDismiss}
88
- >
89
- Remind Later
90
- </Button>
91
- </div>
92
- </div>
93
- </div>
94
- </div>
95
- </div>
96
- );
97
- }