@realtimex/email-automator 2.1.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 (139) hide show
  1. package/.env.example +35 -0
  2. package/LICENSE +21 -0
  3. package/README.md +247 -0
  4. package/api/server.ts +130 -0
  5. package/api/src/config/index.ts +102 -0
  6. package/api/src/middleware/auth.ts +166 -0
  7. package/api/src/middleware/errorHandler.ts +97 -0
  8. package/api/src/middleware/index.ts +4 -0
  9. package/api/src/middleware/rateLimit.ts +87 -0
  10. package/api/src/middleware/validation.ts +118 -0
  11. package/api/src/routes/actions.ts +214 -0
  12. package/api/src/routes/auth.ts +157 -0
  13. package/api/src/routes/emails.ts +144 -0
  14. package/api/src/routes/health.ts +36 -0
  15. package/api/src/routes/index.ts +22 -0
  16. package/api/src/routes/migrate.ts +76 -0
  17. package/api/src/routes/rules.ts +149 -0
  18. package/api/src/routes/settings.ts +229 -0
  19. package/api/src/routes/sync.ts +152 -0
  20. package/api/src/services/eventLogger.ts +52 -0
  21. package/api/src/services/gmail.ts +456 -0
  22. package/api/src/services/intelligence.ts +288 -0
  23. package/api/src/services/microsoft.ts +368 -0
  24. package/api/src/services/processor.ts +596 -0
  25. package/api/src/services/scheduler.ts +255 -0
  26. package/api/src/services/supabase.ts +144 -0
  27. package/api/src/utils/contentCleaner.ts +114 -0
  28. package/api/src/utils/crypto.ts +80 -0
  29. package/api/src/utils/logger.ts +142 -0
  30. package/bin/email-automator-deploy.js +79 -0
  31. package/bin/email-automator-setup.js +144 -0
  32. package/bin/email-automator.js +61 -0
  33. package/dist/assets/index-BQ1uMdFh.js +97 -0
  34. package/dist/assets/index-Dzi17fx5.css +1 -0
  35. package/dist/email-automator-logo.svg +51 -0
  36. package/dist/favicon.svg +45 -0
  37. package/dist/index.html +14 -0
  38. package/index.html +13 -0
  39. package/package.json +112 -0
  40. package/public/email-automator-logo.svg +51 -0
  41. package/public/favicon.svg +45 -0
  42. package/scripts/deploy-functions.sh +55 -0
  43. package/scripts/migrate.sh +177 -0
  44. package/src/App.tsx +622 -0
  45. package/src/components/AccountSettings.tsx +310 -0
  46. package/src/components/AccountSettingsPage.tsx +390 -0
  47. package/src/components/Configuration.tsx +1345 -0
  48. package/src/components/Dashboard.tsx +940 -0
  49. package/src/components/ErrorBoundary.tsx +71 -0
  50. package/src/components/LiveTerminal.tsx +308 -0
  51. package/src/components/LoadingSpinner.tsx +39 -0
  52. package/src/components/Login.tsx +371 -0
  53. package/src/components/Logo.tsx +57 -0
  54. package/src/components/SetupWizard.tsx +388 -0
  55. package/src/components/Toast.tsx +109 -0
  56. package/src/components/migration/MigrationBanner.tsx +97 -0
  57. package/src/components/migration/MigrationModal.tsx +458 -0
  58. package/src/components/migration/MigrationPulseIndicator.tsx +38 -0
  59. package/src/components/mode-toggle.tsx +24 -0
  60. package/src/components/theme-provider.tsx +72 -0
  61. package/src/components/ui/alert.tsx +66 -0
  62. package/src/components/ui/button.tsx +57 -0
  63. package/src/components/ui/card.tsx +75 -0
  64. package/src/components/ui/dialog.tsx +133 -0
  65. package/src/components/ui/input.tsx +22 -0
  66. package/src/components/ui/label.tsx +24 -0
  67. package/src/components/ui/otp-input.tsx +184 -0
  68. package/src/context/AppContext.tsx +422 -0
  69. package/src/context/MigrationContext.tsx +53 -0
  70. package/src/context/TerminalContext.tsx +31 -0
  71. package/src/core/actions.ts +76 -0
  72. package/src/core/auth.ts +108 -0
  73. package/src/core/intelligence.ts +76 -0
  74. package/src/core/processor.ts +112 -0
  75. package/src/hooks/useRealtimeEmails.ts +111 -0
  76. package/src/index.css +140 -0
  77. package/src/lib/api-config.ts +42 -0
  78. package/src/lib/api-old.ts +228 -0
  79. package/src/lib/api.ts +421 -0
  80. package/src/lib/migration-check.ts +264 -0
  81. package/src/lib/sounds.ts +120 -0
  82. package/src/lib/supabase-config.ts +117 -0
  83. package/src/lib/supabase.ts +28 -0
  84. package/src/lib/types.ts +166 -0
  85. package/src/lib/utils.ts +6 -0
  86. package/src/main.tsx +10 -0
  87. package/supabase/.env.example +15 -0
  88. package/supabase/.temp/cli-latest +1 -0
  89. package/supabase/.temp/gotrue-version +1 -0
  90. package/supabase/.temp/pooler-url +1 -0
  91. package/supabase/.temp/postgres-version +1 -0
  92. package/supabase/.temp/project-ref +1 -0
  93. package/supabase/.temp/rest-version +1 -0
  94. package/supabase/.temp/storage-migration +1 -0
  95. package/supabase/.temp/storage-version +1 -0
  96. package/supabase/config.toml +95 -0
  97. package/supabase/functions/_shared/auth-helper.ts +76 -0
  98. package/supabase/functions/_shared/auth.ts +33 -0
  99. package/supabase/functions/_shared/cors.ts +45 -0
  100. package/supabase/functions/_shared/encryption.ts +70 -0
  101. package/supabase/functions/_shared/supabaseAdmin.ts +14 -0
  102. package/supabase/functions/api-v1-accounts/index.ts +133 -0
  103. package/supabase/functions/api-v1-emails/index.ts +177 -0
  104. package/supabase/functions/api-v1-rules/index.ts +177 -0
  105. package/supabase/functions/api-v1-settings/index.ts +247 -0
  106. package/supabase/functions/auth-gmail/index.ts +197 -0
  107. package/supabase/functions/auth-microsoft/index.ts +215 -0
  108. package/supabase/functions/setup/index.ts +92 -0
  109. package/supabase/migrations/20260114000000_initial_schema.sql +81 -0
  110. package/supabase/migrations/20260115000000_add_user_settings.sql +49 -0
  111. package/supabase/migrations/20260115000001_add_auth_flow.sql +80 -0
  112. package/supabase/migrations/20260115000002_fix_permissions.sql +5 -0
  113. package/supabase/migrations/20260115000003_fix_init_state_permissions.sql +9 -0
  114. package/supabase/migrations/20260115000004_add_migration_rpc.sql +13 -0
  115. package/supabase/migrations/20260115000005_add_provider_creds.sql +7 -0
  116. package/supabase/migrations/20260115000006_backfill_profiles.sql +22 -0
  117. package/supabase/migrations/20260116000000_add_sync_scope.sql +15 -0
  118. package/supabase/migrations/20260116000001_per_account_sync_scope.sql +19 -0
  119. package/supabase/migrations/20260116000002_add_llm_api_key.sql +5 -0
  120. package/supabase/migrations/20260117000000_refactor_integrations.sql +36 -0
  121. package/supabase/migrations/20260117000001_add_processing_events.sql +30 -0
  122. package/supabase/migrations/20260117000002_multi_actions.sql +15 -0
  123. package/supabase/migrations/20260117000003_seed_default_rules.sql +77 -0
  124. package/supabase/migrations/20260117000004_rule_instructions.sql +5 -0
  125. package/supabase/migrations/20260117000005_rule_attachments.sql +7 -0
  126. package/supabase/migrations/20260117000006_setup_storage.sql +32 -0
  127. package/supabase/migrations/20260117000007_add_system_logs.sql +26 -0
  128. package/supabase/migrations/20260117000008_link_logs_to_accounts.sql +8 -0
  129. package/supabase/migrations/20260117000009_convert_toggles_to_rules.sql +28 -0
  130. package/supabase/migrations/20260117000010_add_atomic_action_append.sql +13 -0
  131. package/supabase/migrations/20260117000011_add_profile_avatar.sql +4 -0
  132. package/supabase/migrations/20260117000012_setup_avatars_storage.sql +26 -0
  133. package/supabase/templates/confirmation.html +76 -0
  134. package/supabase/templates/email-change.html +76 -0
  135. package/supabase/templates/invite.html +72 -0
  136. package/supabase/templates/magic-link.html +68 -0
  137. package/supabase/templates/recovery.html +82 -0
  138. package/tsconfig.json +36 -0
  139. package/vite.config.ts +162 -0
package/src/main.tsx ADDED
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+ import './index.css';
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')!).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>
10
+ );
@@ -0,0 +1,15 @@
1
+ # Edge Functions Environment Variables
2
+ # Copy this to Supabase Dashboard > Project Settings > Edge Functions
3
+
4
+ # Token Encryption (Required)
5
+ TOKEN_ENCRYPTION_KEY=your-32-character-encryption-key-here
6
+
7
+ # Gmail OAuth (Required for Gmail integration)
8
+ GMAIL_CLIENT_ID=your-gmail-client-id
9
+ GMAIL_CLIENT_SECRET=your-gmail-client-secret
10
+ GMAIL_REDIRECT_URI=urn:ietf:wg:oauth:2.0:oob
11
+
12
+ # Microsoft Graph OAuth (Required for Outlook integration)
13
+ MS_GRAPH_CLIENT_ID=your-ms-graph-client-id
14
+ MS_GRAPH_CLIENT_SECRET=your-ms-graph-client-secret
15
+ MS_GRAPH_TENANT_ID=common
@@ -0,0 +1 @@
1
+ v2.72.7
@@ -0,0 +1 @@
1
+ v2.185.0
@@ -0,0 +1 @@
1
+ postgresql://postgres.dphtysocoxwtohdsdbom@aws-1-us-east-2.pooler.supabase.com:5432/postgres
@@ -0,0 +1 @@
1
+ 17.6.1.063
@@ -0,0 +1 @@
1
+ dphtysocoxwtohdsdbom
@@ -0,0 +1 @@
1
+ v14.1
@@ -0,0 +1 @@
1
+ buckets-objects-grants-postgres
@@ -0,0 +1 @@
1
+ v1.33.0
@@ -0,0 +1,95 @@
1
+ # Supabase Configuration
2
+
3
+ [api]
4
+ enabled = true
5
+ port = 54321
6
+ schemas = ["public", "storage", "graphql_public"]
7
+ extra_search_path = ["public", "extensions"]
8
+ max_rows = 1000
9
+
10
+ [db]
11
+ port = 54322
12
+ major_version = 15
13
+
14
+ [studio]
15
+ enabled = true
16
+ port = 54323
17
+
18
+ [storage]
19
+ enabled = true
20
+ file_size_limit = "50MiB"
21
+
22
+ [auth]
23
+ enabled = true
24
+ # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
25
+ # in emails.
26
+ site_url = "http://localhost:5173"
27
+ # A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
28
+ additional_redirect_urls = ["http://localhost:3000"]
29
+ # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
30
+ jwt_expiry = 3600
31
+ # If disabled, the refresh token will never expire.
32
+ enable_refresh_token_rotation = true
33
+ # Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
34
+ # Requires enable_refresh_token_rotation = true.
35
+ refresh_token_reuse_interval = 10
36
+ # Allow/disallow new user signups to your project.
37
+ enable_signup = true
38
+ # Allow/disallow anonymous sign-ins to your project.
39
+ enable_anonymous_sign_ins = false
40
+ # Allow/disallow testing manual linking of accounts
41
+ enable_manual_linking = false
42
+ # Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more.
43
+ minimum_password_length = 6
44
+ # Passwords that do not meet the following requirements will be rejected as weak. Supported values
45
+ # are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols`
46
+ password_requirements = ""
47
+
48
+ [auth.email]
49
+ # Allow/disallow new user signups via email to your project.
50
+ enable_signup = true
51
+ # If enabled, a user will be required to confirm any email change on both the old, and new email
52
+ # addresses. If disabled, only the new email is required to confirm.
53
+ double_confirm_changes = true
54
+ # If enabled, users need to confirm their email address before signing in.
55
+ enable_confirmations = false
56
+ # If enabled, users will need to reauthenticate or have logged in recently to change their password.
57
+ secure_password_change = false
58
+ # Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email.
59
+ max_frequency = "1s"
60
+ # Number of characters used in the email OTP.
61
+ otp_length = 6
62
+ # Number of seconds before the email OTP expires (defaults to 1 hour).
63
+ otp_expiry = 3600
64
+
65
+ [auth.email.template.invite]
66
+ subject = "You have been invited to Email Automator"
67
+ content_path = "supabase/templates/invite.html"
68
+
69
+ [auth.email.template.confirmation]
70
+ subject = "Confirm your Email Automator account"
71
+ content_path = "supabase/templates/confirmation.html"
72
+
73
+ [auth.email.template.recovery]
74
+ subject = "Reset your Email Automator password"
75
+ content_path = "supabase/templates/recovery.html"
76
+
77
+ [auth.email.template.magic_link]
78
+ subject = "Login to Email Automator"
79
+ content_path = "supabase/templates/magic-link.html"
80
+
81
+ [auth.email.template.email_change]
82
+ subject = "Confirm your new email for Email Automator"
83
+ content_path = "supabase/templates/email-change.html"
84
+
85
+ [functions]
86
+ # Required environment variables for Edge Functions
87
+ # These should be set in Supabase Dashboard > Project Settings > Edge Functions
88
+ #
89
+ # TOKEN_ENCRYPTION_KEY - 32-character encryption key for OAuth tokens
90
+ # GMAIL_CLIENT_ID - Gmail OAuth Client ID
91
+ # GMAIL_CLIENT_SECRET - Gmail OAuth Client Secret
92
+ # GMAIL_REDIRECT_URI - Gmail OAuth Redirect URI (default: urn:ietf:wg:oauth:2.0:oob)
93
+ # MS_GRAPH_CLIENT_ID - Microsoft Graph Client ID
94
+ # MS_GRAPH_CLIENT_SECRET - Microsoft Graph Client Secret (optional)
95
+ # MS_GRAPH_TENANT_ID - Microsoft Tenant ID (default: common)
@@ -0,0 +1,76 @@
1
+ import { supabaseAdmin } from './supabaseAdmin.ts';
2
+
3
+ export interface OAuthCredentials {
4
+ clientId: string;
5
+ clientSecret: string;
6
+ // Optional extras
7
+ tenantId?: string;
8
+ redirectUri?: string;
9
+ }
10
+
11
+ /**
12
+ * Fetch credentials for a specific provider.
13
+ * Priority:
14
+ * 1. integrations table (for the given user)
15
+ * 2. Deno.env (server-side secrets)
16
+ */
17
+ export async function getProviderCredentials(
18
+ userId: string,
19
+ provider: 'google' | 'microsoft'
20
+ ): Promise<OAuthCredentials> {
21
+ // 1. Try to fetch from integrations
22
+ const { data: integration } = await supabaseAdmin
23
+ .from('integrations')
24
+ .select('credentials')
25
+ .eq('user_id', userId)
26
+ .eq('provider', provider)
27
+ .maybeSingle();
28
+
29
+ if (integration?.credentials) {
30
+ const creds = integration.credentials as any;
31
+
32
+ if (provider === 'google' && creds.client_id && creds.client_secret) {
33
+ return {
34
+ clientId: creds.client_id,
35
+ clientSecret: creds.client_secret,
36
+ redirectUri: Deno.env.get('GMAIL_REDIRECT_URI')
37
+ };
38
+ }
39
+
40
+ if (provider === 'microsoft' && creds.client_id) {
41
+ return {
42
+ clientId: creds.client_id,
43
+ clientSecret: creds.client_secret || Deno.env.get('MS_GRAPH_CLIENT_SECRET') || '',
44
+ tenantId: creds.tenant_id || Deno.env.get('MS_GRAPH_TENANT_ID') || 'common'
45
+ };
46
+ }
47
+ }
48
+
49
+ // 2. Fallback to Env Vars
50
+ if (provider === 'google') {
51
+ const clientId = Deno.env.get('GMAIL_CLIENT_ID');
52
+ const clientSecret = Deno.env.get('GMAIL_CLIENT_SECRET');
53
+ if (!clientId || !clientSecret) {
54
+ throw new Error('Gmail OAuth credentials not configured (Database or Env)');
55
+ }
56
+ return {
57
+ clientId,
58
+ clientSecret,
59
+ redirectUri: Deno.env.get('GMAIL_REDIRECT_URI')
60
+ };
61
+ }
62
+
63
+ if (provider === 'microsoft') {
64
+ const clientId = Deno.env.get('MS_GRAPH_CLIENT_ID');
65
+ if (!clientId) {
66
+ throw new Error('Microsoft OAuth credentials not configured (Database or Env)');
67
+ }
68
+ return {
69
+ clientId,
70
+ clientSecret: Deno.env.get('MS_GRAPH_CLIENT_SECRET') || '',
71
+ tenantId: Deno.env.get('MS_GRAPH_TENANT_ID') || 'common'
72
+ };
73
+ }
74
+
75
+ throw new Error(`Unknown provider: ${provider}`);
76
+ }
@@ -0,0 +1,33 @@
1
+ import { createClient } from 'jsr:@supabase/supabase-js@2';
2
+
3
+ // Verify user from authorization header
4
+ export async function verifyUser(req: Request) {
5
+ const authHeader = req.headers.get('Authorization');
6
+
7
+ if (!authHeader?.startsWith('Bearer ')) {
8
+ return { user: null, error: 'Missing or invalid authorization header' };
9
+ }
10
+
11
+ const token = authHeader.substring(7);
12
+
13
+ // Create client to verify token
14
+ const supabase = createClient(
15
+ Deno.env.get('SUPABASE_URL') ?? '',
16
+ Deno.env.get('SUPABASE_ANON_KEY') ?? '',
17
+ {
18
+ global: {
19
+ headers: {
20
+ Authorization: `Bearer ${token}`,
21
+ },
22
+ },
23
+ }
24
+ );
25
+
26
+ const { data: { user }, error } = await supabase.auth.getUser();
27
+
28
+ if (error || !user) {
29
+ return { user: null, error: 'Invalid or expired token' };
30
+ }
31
+
32
+ return { user, error: null };
33
+ }
@@ -0,0 +1,45 @@
1
+ // CORS headers for Edge Functions
2
+ export const corsHeaders = {
3
+ 'Access-Control-Allow-Origin': '*',
4
+ 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
5
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
6
+ };
7
+
8
+ // Handle CORS preflight
9
+ export function handleCors(req: Request): Response | null {
10
+ if (req.method === 'OPTIONS') {
11
+ return new Response(null, {
12
+ status: 204,
13
+ headers: corsHeaders,
14
+ });
15
+ }
16
+ return null;
17
+ }
18
+
19
+ // Create error response with CORS
20
+ export function createErrorResponse(status: number, message: string): Response {
21
+ return new Response(
22
+ JSON.stringify({ error: message }),
23
+ {
24
+ status,
25
+ headers: {
26
+ ...corsHeaders,
27
+ 'Content-Type': 'application/json',
28
+ },
29
+ }
30
+ );
31
+ }
32
+
33
+ // Create success response with CORS
34
+ export function createSuccessResponse(data: any, status = 200): Response {
35
+ return new Response(
36
+ JSON.stringify(data),
37
+ {
38
+ status,
39
+ headers: {
40
+ ...corsHeaders,
41
+ 'Content-Type': 'application/json',
42
+ },
43
+ }
44
+ );
45
+ }
@@ -0,0 +1,70 @@
1
+ // Token encryption utilities for securing OAuth credentials
2
+ // Uses Web Crypto API available in Deno
3
+
4
+ const ALGORITHM = 'AES-GCM';
5
+ const KEY_LENGTH = 256;
6
+ const IV_LENGTH = 12;
7
+
8
+ // Get encryption key from environment
9
+ function getEncryptionKey(): string {
10
+ const key = Deno.env.get('TOKEN_ENCRYPTION_KEY');
11
+ if (!key) {
12
+ throw new Error('TOKEN_ENCRYPTION_KEY environment variable not set');
13
+ }
14
+ return key;
15
+ }
16
+
17
+ // Convert string key to CryptoKey
18
+ async function getKey(): Promise<CryptoKey> {
19
+ const keyString = getEncryptionKey();
20
+ const keyData = new TextEncoder().encode(keyString.padEnd(32, '0').slice(0, 32));
21
+
22
+ return await crypto.subtle.importKey(
23
+ 'raw',
24
+ keyData,
25
+ { name: ALGORITHM },
26
+ false,
27
+ ['encrypt', 'decrypt']
28
+ );
29
+ }
30
+
31
+ // Encrypt data
32
+ export async function encrypt(plaintext: string): Promise<string> {
33
+ const key = await getKey();
34
+ const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
35
+ const encoded = new TextEncoder().encode(plaintext);
36
+
37
+ const ciphertext = await crypto.subtle.encrypt(
38
+ { name: ALGORITHM, iv },
39
+ key,
40
+ encoded
41
+ );
42
+
43
+ // Combine IV and ciphertext
44
+ const combined = new Uint8Array(iv.length + ciphertext.byteLength);
45
+ combined.set(iv);
46
+ combined.set(new Uint8Array(ciphertext), iv.length);
47
+
48
+ // Convert to base64
49
+ return btoa(String.fromCharCode(...combined));
50
+ }
51
+
52
+ // Decrypt data
53
+ export async function decrypt(ciphertext: string): Promise<string> {
54
+ const key = await getKey();
55
+
56
+ // Convert from base64
57
+ const combined = Uint8Array.from(atob(ciphertext), c => c.charCodeAt(0));
58
+
59
+ // Extract IV and ciphertext
60
+ const iv = combined.slice(0, IV_LENGTH);
61
+ const encrypted = combined.slice(IV_LENGTH);
62
+
63
+ const decrypted = await crypto.subtle.decrypt(
64
+ { name: ALGORITHM, iv },
65
+ key,
66
+ encrypted
67
+ );
68
+
69
+ return new TextDecoder().decode(decrypted);
70
+ }
@@ -0,0 +1,14 @@
1
+ import { createClient } from 'jsr:@supabase/supabase-js@2';
2
+
3
+ // Create a Supabase client with the service role key
4
+ // This client bypasses Row Level Security (RLS)
5
+ export const supabaseAdmin = createClient(
6
+ Deno.env.get('SUPABASE_URL') ?? '',
7
+ Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '',
8
+ {
9
+ auth: {
10
+ autoRefreshToken: false,
11
+ persistSession: false,
12
+ },
13
+ }
14
+ );
@@ -0,0 +1,133 @@
1
+ import 'jsr:@supabase/functions-js/edge-runtime.d.ts';
2
+ import { supabaseAdmin } from '../_shared/supabaseAdmin.ts';
3
+ import { handleCors, createErrorResponse, createSuccessResponse } from '../_shared/cors.ts';
4
+ import { verifyUser } from '../_shared/auth.ts';
5
+
6
+ /**
7
+ * Email Accounts API
8
+ *
9
+ * GET /api-v1-accounts - List all accounts for the authenticated user
10
+ * DELETE /api-v1-accounts/:id - Disconnect an account
11
+ */
12
+
13
+ Deno.serve(async (req) => {
14
+ // Handle CORS preflight
15
+ const corsResponse = handleCors(req);
16
+ if (corsResponse) return corsResponse;
17
+
18
+ // Verify user authentication
19
+ const { user, error: authError } = await verifyUser(req);
20
+ if (authError || !user) {
21
+ return createErrorResponse(401, authError || 'Unauthorized');
22
+ }
23
+
24
+ try {
25
+ const url = new URL(req.url);
26
+ const pathParts = url.pathname.split('/').filter(Boolean);
27
+
28
+ // GET /api-v1-accounts - List accounts
29
+ if (req.method === 'GET' && pathParts.length === 1) {
30
+ const { data, error } = await supabaseAdmin
31
+ .from('email_accounts')
32
+ .select('id, provider, email_address, is_active, last_sync_checkpoint, sync_start_date, sync_max_emails_per_run, last_sync_at, last_sync_status, last_sync_error, created_at, updated_at')
33
+ .eq('user_id', user.id)
34
+ .order('created_at', { ascending: false });
35
+
36
+ if (error) {
37
+ console.error('Database error:', error);
38
+ return createErrorResponse(500, 'Failed to fetch accounts');
39
+ }
40
+
41
+ return createSuccessResponse({ accounts: data || [] });
42
+ }
43
+
44
+ // PATCH /api-v1-accounts/:id - Update account settings
45
+ if (req.method === 'PATCH' && pathParts.length === 2) {
46
+ const accountId = pathParts[1];
47
+ const updates = await req.json();
48
+
49
+ // Fetch current state to check if we are moving the start date backwards
50
+ const { data: currentAccount } = await supabaseAdmin
51
+ .from('email_accounts')
52
+ .select('sync_start_date, last_sync_checkpoint')
53
+ .eq('id', accountId)
54
+ .eq('user_id', user.id)
55
+ .single();
56
+
57
+ // Only allow updating specific fields
58
+ const allowedUpdates: Record<string, any> = {};
59
+ if (updates.sync_start_date !== undefined) {
60
+ allowedUpdates.sync_start_date = updates.sync_start_date;
61
+
62
+ // If moving start date backwards, reset checkpoint to force backfill
63
+ if (currentAccount && updates.sync_start_date) {
64
+ const newDate = new Date(updates.sync_start_date).getTime();
65
+ const oldDate = currentAccount.sync_start_date ? new Date(currentAccount.sync_start_date).getTime() : Infinity;
66
+
67
+ if (newDate < oldDate) {
68
+ console.log('Sync start date moved backwards, resetting checkpoint for account:', accountId);
69
+ allowedUpdates.last_sync_checkpoint = null;
70
+ }
71
+ }
72
+ }
73
+ if (updates.sync_max_emails_per_run !== undefined) allowedUpdates.sync_max_emails_per_run = updates.sync_max_emails_per_run;
74
+ if (updates.is_active !== undefined) allowedUpdates.is_active = updates.is_active;
75
+ if (updates.last_sync_checkpoint !== undefined) allowedUpdates.last_sync_checkpoint = updates.last_sync_checkpoint;
76
+
77
+ const { data, error } = await supabaseAdmin
78
+ .from('email_accounts')
79
+ .update(allowedUpdates)
80
+ .eq('id', accountId)
81
+ .eq('user_id', user.id)
82
+ .select()
83
+ .single();
84
+
85
+ if (error) {
86
+ console.error('Database error:', error);
87
+ return createErrorResponse(500, 'Failed to update account');
88
+ }
89
+
90
+ if (!data) {
91
+ return createErrorResponse(404, 'Account not found');
92
+ }
93
+
94
+ return createSuccessResponse({ account: data });
95
+ }
96
+
97
+ // DELETE /api-v1-accounts/:id - Disconnect account
98
+ if (req.method === 'DELETE' && pathParts.length === 2) {
99
+ const accountId = pathParts[1];
100
+
101
+ // Verify account ownership
102
+ const { data: account } = await supabaseAdmin
103
+ .from('email_accounts')
104
+ .select('id')
105
+ .eq('id', accountId)
106
+ .eq('user_id', user.id)
107
+ .single();
108
+
109
+ if (!account) {
110
+ return createErrorResponse(404, 'Account not found');
111
+ }
112
+
113
+ // Delete account
114
+ const { error } = await supabaseAdmin
115
+ .from('email_accounts')
116
+ .delete()
117
+ .eq('id', accountId)
118
+ .eq('user_id', user.id);
119
+
120
+ if (error) {
121
+ console.error('Database error:', error);
122
+ return createErrorResponse(500, 'Failed to delete account');
123
+ }
124
+
125
+ return createSuccessResponse({ success: true });
126
+ }
127
+
128
+ return createErrorResponse(405, 'Method not allowed');
129
+ } catch (error) {
130
+ console.error('Request error:', error);
131
+ return createErrorResponse(500, 'Internal server error');
132
+ }
133
+ });