@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.
- package/.env.example +35 -0
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/api/server.ts +130 -0
- package/api/src/config/index.ts +102 -0
- package/api/src/middleware/auth.ts +166 -0
- package/api/src/middleware/errorHandler.ts +97 -0
- package/api/src/middleware/index.ts +4 -0
- package/api/src/middleware/rateLimit.ts +87 -0
- package/api/src/middleware/validation.ts +118 -0
- package/api/src/routes/actions.ts +214 -0
- package/api/src/routes/auth.ts +157 -0
- package/api/src/routes/emails.ts +144 -0
- package/api/src/routes/health.ts +36 -0
- package/api/src/routes/index.ts +22 -0
- package/api/src/routes/migrate.ts +76 -0
- package/api/src/routes/rules.ts +149 -0
- package/api/src/routes/settings.ts +229 -0
- package/api/src/routes/sync.ts +152 -0
- package/api/src/services/eventLogger.ts +52 -0
- package/api/src/services/gmail.ts +456 -0
- package/api/src/services/intelligence.ts +288 -0
- package/api/src/services/microsoft.ts +368 -0
- package/api/src/services/processor.ts +596 -0
- package/api/src/services/scheduler.ts +255 -0
- package/api/src/services/supabase.ts +144 -0
- package/api/src/utils/contentCleaner.ts +114 -0
- package/api/src/utils/crypto.ts +80 -0
- package/api/src/utils/logger.ts +142 -0
- package/bin/email-automator-deploy.js +79 -0
- package/bin/email-automator-setup.js +144 -0
- package/bin/email-automator.js +61 -0
- package/dist/assets/index-BQ1uMdFh.js +97 -0
- package/dist/assets/index-Dzi17fx5.css +1 -0
- package/dist/email-automator-logo.svg +51 -0
- package/dist/favicon.svg +45 -0
- package/dist/index.html +14 -0
- package/index.html +13 -0
- package/package.json +112 -0
- package/public/email-automator-logo.svg +51 -0
- package/public/favicon.svg +45 -0
- package/scripts/deploy-functions.sh +55 -0
- package/scripts/migrate.sh +177 -0
- package/src/App.tsx +622 -0
- package/src/components/AccountSettings.tsx +310 -0
- package/src/components/AccountSettingsPage.tsx +390 -0
- package/src/components/Configuration.tsx +1345 -0
- package/src/components/Dashboard.tsx +940 -0
- package/src/components/ErrorBoundary.tsx +71 -0
- package/src/components/LiveTerminal.tsx +308 -0
- package/src/components/LoadingSpinner.tsx +39 -0
- package/src/components/Login.tsx +371 -0
- package/src/components/Logo.tsx +57 -0
- package/src/components/SetupWizard.tsx +388 -0
- package/src/components/Toast.tsx +109 -0
- package/src/components/migration/MigrationBanner.tsx +97 -0
- package/src/components/migration/MigrationModal.tsx +458 -0
- package/src/components/migration/MigrationPulseIndicator.tsx +38 -0
- package/src/components/mode-toggle.tsx +24 -0
- package/src/components/theme-provider.tsx +72 -0
- package/src/components/ui/alert.tsx +66 -0
- package/src/components/ui/button.tsx +57 -0
- package/src/components/ui/card.tsx +75 -0
- package/src/components/ui/dialog.tsx +133 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/otp-input.tsx +184 -0
- package/src/context/AppContext.tsx +422 -0
- package/src/context/MigrationContext.tsx +53 -0
- package/src/context/TerminalContext.tsx +31 -0
- package/src/core/actions.ts +76 -0
- package/src/core/auth.ts +108 -0
- package/src/core/intelligence.ts +76 -0
- package/src/core/processor.ts +112 -0
- package/src/hooks/useRealtimeEmails.ts +111 -0
- package/src/index.css +140 -0
- package/src/lib/api-config.ts +42 -0
- package/src/lib/api-old.ts +228 -0
- package/src/lib/api.ts +421 -0
- package/src/lib/migration-check.ts +264 -0
- package/src/lib/sounds.ts +120 -0
- package/src/lib/supabase-config.ts +117 -0
- package/src/lib/supabase.ts +28 -0
- package/src/lib/types.ts +166 -0
- package/src/lib/utils.ts +6 -0
- package/src/main.tsx +10 -0
- package/supabase/.env.example +15 -0
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/.temp/gotrue-version +1 -0
- package/supabase/.temp/pooler-url +1 -0
- package/supabase/.temp/postgres-version +1 -0
- package/supabase/.temp/project-ref +1 -0
- package/supabase/.temp/rest-version +1 -0
- package/supabase/.temp/storage-migration +1 -0
- package/supabase/.temp/storage-version +1 -0
- package/supabase/config.toml +95 -0
- package/supabase/functions/_shared/auth-helper.ts +76 -0
- package/supabase/functions/_shared/auth.ts +33 -0
- package/supabase/functions/_shared/cors.ts +45 -0
- package/supabase/functions/_shared/encryption.ts +70 -0
- package/supabase/functions/_shared/supabaseAdmin.ts +14 -0
- package/supabase/functions/api-v1-accounts/index.ts +133 -0
- package/supabase/functions/api-v1-emails/index.ts +177 -0
- package/supabase/functions/api-v1-rules/index.ts +177 -0
- package/supabase/functions/api-v1-settings/index.ts +247 -0
- package/supabase/functions/auth-gmail/index.ts +197 -0
- package/supabase/functions/auth-microsoft/index.ts +215 -0
- package/supabase/functions/setup/index.ts +92 -0
- package/supabase/migrations/20260114000000_initial_schema.sql +81 -0
- package/supabase/migrations/20260115000000_add_user_settings.sql +49 -0
- package/supabase/migrations/20260115000001_add_auth_flow.sql +80 -0
- package/supabase/migrations/20260115000002_fix_permissions.sql +5 -0
- package/supabase/migrations/20260115000003_fix_init_state_permissions.sql +9 -0
- package/supabase/migrations/20260115000004_add_migration_rpc.sql +13 -0
- package/supabase/migrations/20260115000005_add_provider_creds.sql +7 -0
- package/supabase/migrations/20260115000006_backfill_profiles.sql +22 -0
- package/supabase/migrations/20260116000000_add_sync_scope.sql +15 -0
- package/supabase/migrations/20260116000001_per_account_sync_scope.sql +19 -0
- package/supabase/migrations/20260116000002_add_llm_api_key.sql +5 -0
- package/supabase/migrations/20260117000000_refactor_integrations.sql +36 -0
- package/supabase/migrations/20260117000001_add_processing_events.sql +30 -0
- package/supabase/migrations/20260117000002_multi_actions.sql +15 -0
- package/supabase/migrations/20260117000003_seed_default_rules.sql +77 -0
- package/supabase/migrations/20260117000004_rule_instructions.sql +5 -0
- package/supabase/migrations/20260117000005_rule_attachments.sql +7 -0
- package/supabase/migrations/20260117000006_setup_storage.sql +32 -0
- package/supabase/migrations/20260117000007_add_system_logs.sql +26 -0
- package/supabase/migrations/20260117000008_link_logs_to_accounts.sql +8 -0
- package/supabase/migrations/20260117000009_convert_toggles_to_rules.sql +28 -0
- package/supabase/migrations/20260117000010_add_atomic_action_append.sql +13 -0
- package/supabase/migrations/20260117000011_add_profile_avatar.sql +4 -0
- package/supabase/migrations/20260117000012_setup_avatars_storage.sql +26 -0
- package/supabase/templates/confirmation.html +76 -0
- package/supabase/templates/email-change.html +76 -0
- package/supabase/templates/invite.html +72 -0
- package/supabase/templates/magic-link.html +68 -0
- package/supabase/templates/recovery.html +82 -0
- package/tsconfig.json +36 -0
- package/vite.config.ts +162 -0
package/src/main.tsx
ADDED
|
@@ -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
|
+
});
|