agentmail-clone-v1 0.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 +20 -0
- package/.github/workflows/docs-deploy.yml +37 -0
- package/.github/workflows/landing-preview.yml +43 -0
- package/.github/workflows/openapi-lint.yml +31 -0
- package/.github/workflows/sdk-generate-check.yml +66 -0
- package/.github/workflows/sdk-release.yml +62 -0
- package/CHANGELOG.md +10 -0
- package/README.md +208 -0
- package/RELEASING.md +43 -0
- package/docs/api-reference/api-keys.mdx +11 -0
- package/docs/api-reference/domains.mdx +13 -0
- package/docs/api-reference/emails.mdx +26 -0
- package/docs/api-reference/inboxes.mdx +13 -0
- package/docs/api-reference/metrics.mdx +10 -0
- package/docs/authentication.mdx +25 -0
- package/docs/docs.json +35 -0
- package/docs/errors.mdx +34 -0
- package/docs/favicon.svg +5 -0
- package/docs/idempotency.mdx +18 -0
- package/docs/index.mdx +24 -0
- package/docs/quickstart.mdx +134 -0
- package/landing/DEPLOYING.md +33 -0
- package/landing/favicon.svg +5 -0
- package/landing/index.html +129 -0
- package/landing/main.js +45 -0
- package/landing/privacy.html +29 -0
- package/landing/styles.css +356 -0
- package/landing/terms.html +29 -0
- package/netlify.toml +15 -0
- package/openapi/openapi.yaml +1016 -0
- package/package.json +34 -0
- package/render.yaml +48 -0
- package/scripts/generate-sdk-py.sh +16 -0
- package/scripts/generate-sdk-ts.sh +16 -0
- package/scripts/migrate.js +66 -0
- package/scripts/validate-docs.js +56 -0
- package/scripts/validate-landing.js +39 -0
- package/sdks/python/README.md +40 -0
- package/sdks/python/emailagent_sdk/__init__.py +157 -0
- package/sdks/python/generated/.openapi-generator/FILES +101 -0
- package/sdks/python/generated/.openapi-generator/VERSION +1 -0
- package/sdks/python/generated/.openapi-generator-ignore +23 -0
- package/sdks/python/generated/emailagent_sdk_generated/__init__.py +105 -0
- package/sdks/python/generated/emailagent_sdk_generated/api/__init__.py +9 -0
- package/sdks/python/generated/emailagent_sdk_generated/api/api_keys_api.py +1162 -0
- package/sdks/python/generated/emailagent_sdk_generated/api/domains_api.py +1168 -0
- package/sdks/python/generated/emailagent_sdk_generated/api/emails_api.py +1232 -0
- package/sdks/python/generated/emailagent_sdk_generated/api/inboxes_api.py +1191 -0
- package/sdks/python/generated/emailagent_sdk_generated/api/metrics_api.py +285 -0
- package/sdks/python/generated/emailagent_sdk_generated/api_client.py +801 -0
- package/sdks/python/generated/emailagent_sdk_generated/api_response.py +21 -0
- package/sdks/python/generated/emailagent_sdk_generated/configuration.py +586 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/APIKeysApi.md +334 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/ApiKeyCreated.md +35 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/ApiKeyCreatedResponse.md +29 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/ApiKeyListItem.md +35 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/ApiKeyListResponse.md +29 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/CreateApiKeyRequest.md +30 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/CreateApiKeyRequestScopes.md +29 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/CreateDomainRequest.md +29 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/CreateInboxRequest.md +31 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/Domain.md +37 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/DomainListResponse.md +29 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/DomainResponse.md +29 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/DomainsApi.md +336 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/Email.md +43 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/EmailListResponse.md +29 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/EmailResponse.md +29 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/EmailsApi.md +353 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/ErrorResponse.md +29 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/Inbox.md +38 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/InboxListResponse.md +29 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/InboxResponse.md +29 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/InboxesApi.md +337 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/MetricsApi.md +83 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/MetricsData.md +35 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/MetricsResponse.md +29 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/OkResponse.md +29 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/PlanLimits.md +32 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/SendEmailRequest.md +32 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/UpdateEmailReadRequest.md +29 -0
- package/sdks/python/generated/emailagent_sdk_generated/docs/UpdateInboxRequest.md +29 -0
- package/sdks/python/generated/emailagent_sdk_generated/exceptions.py +216 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/__init__.py +41 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/api_key_created.py +113 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/api_key_created_response.py +91 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/api_key_list_item.py +123 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/api_key_list_response.py +95 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/create_api_key_request.py +93 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/create_api_key_request_scopes.py +143 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/create_domain_request.py +87 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/create_inbox_request.py +91 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/domain.py +134 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/domain_list_response.py +95 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/domain_response.py +91 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/email.py +175 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/email_list_response.py +95 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/email_response.py +91 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/error_response.py +87 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/inbox.py +136 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/inbox_list_response.py +95 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/inbox_response.py +91 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/metrics_data.py +110 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/metrics_response.py +91 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/ok_response.py +87 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/plan_limits.py +93 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/send_email_request.py +93 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/update_email_read_request.py +87 -0
- package/sdks/python/generated/emailagent_sdk_generated/models/update_inbox_request.py +92 -0
- package/sdks/python/generated/emailagent_sdk_generated/rest.py +258 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/__init__.py +0 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_api_key_created.py +68 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_api_key_created_response.py +52 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_api_key_list_item.py +66 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_api_key_list_response.py +56 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_api_keys_api.py +59 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_create_api_key_request.py +53 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_create_api_key_request_scopes.py +50 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_create_domain_request.py +52 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_create_inbox_request.py +54 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_domain.py +70 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_domain_list_response.py +56 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_domain_response.py +52 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_domains_api.py +59 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_email.py +79 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_email_list_response.py +56 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_email_response.py +52 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_emails_api.py +59 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_error_response.py +52 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_inbox.py +68 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_inbox_list_response.py +56 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_inbox_response.py +52 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_inboxes_api.py +59 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_metrics_api.py +38 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_metrics_data.py +72 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_metrics_response.py +74 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_ok_response.py +52 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_plan_limits.py +58 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_send_email_request.py +56 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_update_email_read_request.py +52 -0
- package/sdks/python/generated/emailagent_sdk_generated/test/test_update_inbox_request.py +52 -0
- package/sdks/python/generated/emailagent_sdk_generated_README.md +140 -0
- package/sdks/python/openapitools.json +7 -0
- package/sdks/python/pyproject.toml +19 -0
- package/sdks/typescript/README.md +41 -0
- package/sdks/typescript/generated/.openapi-generator/FILES +41 -0
- package/sdks/typescript/generated/.openapi-generator/VERSION +1 -0
- package/sdks/typescript/generated/.openapi-generator-ignore +23 -0
- package/sdks/typescript/generated/package.json +21 -0
- package/sdks/typescript/generated/src/apis/APIKeysApi.ts +314 -0
- package/sdks/typescript/generated/src/apis/DomainsApi.ts +314 -0
- package/sdks/typescript/generated/src/apis/EmailsApi.ts +350 -0
- package/sdks/typescript/generated/src/apis/InboxesApi.ts +329 -0
- package/sdks/typescript/generated/src/apis/MetricsApi.ts +93 -0
- package/sdks/typescript/generated/src/apis/index.ts +7 -0
- package/sdks/typescript/generated/src/index.ts +5 -0
- package/sdks/typescript/generated/src/models/ApiKeyCreated.ts +123 -0
- package/sdks/typescript/generated/src/models/ApiKeyCreatedResponse.ts +74 -0
- package/sdks/typescript/generated/src/models/ApiKeyListItem.ts +121 -0
- package/sdks/typescript/generated/src/models/ApiKeyListResponse.ts +74 -0
- package/sdks/typescript/generated/src/models/CreateApiKeyRequest.ts +82 -0
- package/sdks/typescript/generated/src/models/CreateApiKeyRequestScopes.ts +45 -0
- package/sdks/typescript/generated/src/models/CreateDomainRequest.ts +66 -0
- package/sdks/typescript/generated/src/models/CreateInboxRequest.ts +82 -0
- package/sdks/typescript/generated/src/models/Domain.ts +152 -0
- package/sdks/typescript/generated/src/models/DomainListResponse.ts +74 -0
- package/sdks/typescript/generated/src/models/DomainResponse.ts +74 -0
- package/sdks/typescript/generated/src/models/Email.ts +222 -0
- package/sdks/typescript/generated/src/models/EmailListResponse.ts +74 -0
- package/sdks/typescript/generated/src/models/EmailResponse.ts +74 -0
- package/sdks/typescript/generated/src/models/ErrorResponse.ts +66 -0
- package/sdks/typescript/generated/src/models/Inbox.ts +159 -0
- package/sdks/typescript/generated/src/models/InboxListResponse.ts +74 -0
- package/sdks/typescript/generated/src/models/InboxResponse.ts +74 -0
- package/sdks/typescript/generated/src/models/MetricsData.ts +139 -0
- package/sdks/typescript/generated/src/models/MetricsResponse.ts +74 -0
- package/sdks/typescript/generated/src/models/OkResponse.ts +66 -0
- package/sdks/typescript/generated/src/models/PlanLimits.ts +93 -0
- package/sdks/typescript/generated/src/models/SendEmailRequest.ts +91 -0
- package/sdks/typescript/generated/src/models/UpdateEmailReadRequest.ts +66 -0
- package/sdks/typescript/generated/src/models/UpdateInboxRequest.ts +66 -0
- package/sdks/typescript/generated/src/models/index.ts +27 -0
- package/sdks/typescript/generated/src/runtime.ts +432 -0
- package/sdks/typescript/generated/tsconfig.esm.json +7 -0
- package/sdks/typescript/generated/tsconfig.json +16 -0
- package/sdks/typescript/openapitools.json +8 -0
- package/sdks/typescript/package.json +27 -0
- package/sdks/typescript/src/index.ts +138 -0
- package/sdks/typescript/tsconfig.json +14 -0
- package/sql/001_init.sql +143 -0
- package/sql/002_local_auth.sql +38 -0
- package/sql/003_domain_routes.sql +2 -0
- package/sql/004_reliability_primitives.sql +75 -0
- package/sql/005_auth_email_flows.sql +22 -0
- package/src/config.js +30 -0
- package/src/db.js +25 -0
- package/src/lib/api-auth.js +55 -0
- package/src/lib/auth.js +71 -0
- package/src/lib/csrf.js +46 -0
- package/src/lib/dodo.js +67 -0
- package/src/lib/email-templates.js +67 -0
- package/src/lib/idempotency.js +85 -0
- package/src/lib/mailgun.js +188 -0
- package/src/lib/plan.js +24 -0
- package/src/lib/rate-limit.js +43 -0
- package/src/lib/security.js +62 -0
- package/src/lib/session.js +21 -0
- package/src/lib/store.js +638 -0
- package/src/lib/transactional-mailer.js +54 -0
- package/src/lib/validation.js +30 -0
- package/src/routes/api.js +485 -0
- package/src/routes/app.js +699 -0
- package/src/routes/auth.js +404 -0
- package/src/routes/webhooks.js +257 -0
- package/src/server.js +79 -0
- package/src/views/pages/admin.ejs +58 -0
- package/src/views/pages/api-keys.ejs +56 -0
- package/src/views/pages/billing.ejs +71 -0
- package/src/views/pages/domains.ejs +106 -0
- package/src/views/pages/inboxes.ejs +127 -0
- package/src/views/pages/login.ejs +57 -0
- package/src/views/pages/metrics.ejs +34 -0
- package/src/views/pages/reset-password.ejs +19 -0
- package/src/views/partials/bottom.ejs +3 -0
- package/src/views/partials/csrf-field.ejs +3 -0
- package/src/views/partials/flash.ejs +3 -0
- package/src/views/partials/top.ejs +130 -0
package/sql/001_init.sql
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
|
2
|
+
|
|
3
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
4
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
5
|
+
email TEXT UNIQUE NOT NULL,
|
|
6
|
+
full_name TEXT NOT NULL,
|
|
7
|
+
password_hash TEXT NOT NULL,
|
|
8
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
CREATE TABLE IF NOT EXISTS organizations (
|
|
12
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
13
|
+
name TEXT NOT NULL,
|
|
14
|
+
plan TEXT NOT NULL DEFAULT 'free' CHECK (plan IN ('free', 'paid')),
|
|
15
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
CREATE TABLE IF NOT EXISTS memberships (
|
|
19
|
+
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
20
|
+
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
21
|
+
role TEXT NOT NULL DEFAULT 'owner' CHECK (role IN ('owner', 'member')),
|
|
22
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
23
|
+
PRIMARY KEY (user_id, org_id)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE TABLE IF NOT EXISTS domains (
|
|
27
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
28
|
+
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
29
|
+
name TEXT NOT NULL UNIQUE,
|
|
30
|
+
provider_domain_id TEXT,
|
|
31
|
+
provider_route_id TEXT,
|
|
32
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'active', 'failed', 'deleted')),
|
|
33
|
+
dns_records_json JSONB NOT NULL DEFAULT '[]'::jsonb,
|
|
34
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
35
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
CREATE TABLE IF NOT EXISTS inboxes (
|
|
39
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
40
|
+
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
41
|
+
local_part TEXT NOT NULL,
|
|
42
|
+
domain_name TEXT NOT NULL,
|
|
43
|
+
email_address TEXT NOT NULL UNIQUE,
|
|
44
|
+
display_name TEXT,
|
|
45
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'deleted')),
|
|
46
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
47
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
48
|
+
deleted_at TIMESTAMPTZ,
|
|
49
|
+
UNIQUE (org_id, local_part, domain_name)
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
CREATE TABLE IF NOT EXISTS emails (
|
|
53
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
54
|
+
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
55
|
+
inbox_id UUID REFERENCES inboxes(id) ON DELETE SET NULL,
|
|
56
|
+
direction TEXT NOT NULL CHECK (direction IN ('inbound', 'outbound')),
|
|
57
|
+
delivery_status TEXT NOT NULL DEFAULT 'unknown' CHECK (delivery_status IN ('unknown', 'queued', 'sent', 'failed', 'received')),
|
|
58
|
+
message_id TEXT,
|
|
59
|
+
subject TEXT NOT NULL DEFAULT '',
|
|
60
|
+
from_address TEXT NOT NULL,
|
|
61
|
+
to_addresses TEXT[] NOT NULL DEFAULT '{}',
|
|
62
|
+
text_body TEXT,
|
|
63
|
+
html_body TEXT,
|
|
64
|
+
is_read BOOLEAN NOT NULL DEFAULT FALSE,
|
|
65
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'deleted')),
|
|
66
|
+
provider_payload JSONB,
|
|
67
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
71
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
72
|
+
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
73
|
+
name TEXT NOT NULL,
|
|
74
|
+
key_prefix TEXT NOT NULL,
|
|
75
|
+
key_hash TEXT NOT NULL UNIQUE,
|
|
76
|
+
scopes TEXT[] NOT NULL DEFAULT '{}',
|
|
77
|
+
last_used_at TIMESTAMPTZ,
|
|
78
|
+
revoked_at TIMESTAMPTZ,
|
|
79
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
CREATE TABLE IF NOT EXISTS usage_monthly (
|
|
83
|
+
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
84
|
+
month_start DATE NOT NULL,
|
|
85
|
+
sent_count INTEGER NOT NULL DEFAULT 0,
|
|
86
|
+
inbound_count INTEGER NOT NULL DEFAULT 0,
|
|
87
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
88
|
+
PRIMARY KEY (org_id, month_start)
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
CREATE TABLE IF NOT EXISTS plan_limits (
|
|
92
|
+
plan TEXT PRIMARY KEY CHECK (plan IN ('free', 'paid')),
|
|
93
|
+
max_inboxes INTEGER NOT NULL,
|
|
94
|
+
max_custom_domains INTEGER NOT NULL,
|
|
95
|
+
max_api_keys INTEGER NOT NULL,
|
|
96
|
+
monthly_emails INTEGER NOT NULL,
|
|
97
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
INSERT INTO plan_limits (plan, max_inboxes, max_custom_domains, max_api_keys, monthly_emails)
|
|
101
|
+
VALUES
|
|
102
|
+
('free', 1, 0, 5, 1000),
|
|
103
|
+
('paid', 10, 1, 100, 10000)
|
|
104
|
+
ON CONFLICT (plan) DO NOTHING;
|
|
105
|
+
|
|
106
|
+
CREATE TABLE IF NOT EXISTS idempotency_records (
|
|
107
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
108
|
+
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
109
|
+
idempotency_key TEXT NOT NULL,
|
|
110
|
+
method TEXT NOT NULL,
|
|
111
|
+
request_path TEXT NOT NULL,
|
|
112
|
+
request_hash TEXT NOT NULL,
|
|
113
|
+
status TEXT NOT NULL CHECK (status IN ('in_progress', 'completed')),
|
|
114
|
+
response_status INTEGER,
|
|
115
|
+
response_body JSONB,
|
|
116
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
117
|
+
expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '24 hours',
|
|
118
|
+
UNIQUE (org_id, idempotency_key, method, request_path)
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
CREATE TABLE IF NOT EXISTS webhook_events (
|
|
122
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
123
|
+
provider TEXT NOT NULL,
|
|
124
|
+
event_id TEXT NOT NULL,
|
|
125
|
+
event_type TEXT,
|
|
126
|
+
payload JSONB NOT NULL,
|
|
127
|
+
processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
128
|
+
UNIQUE (provider, event_id)
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
132
|
+
id BIGSERIAL PRIMARY KEY,
|
|
133
|
+
filename TEXT UNIQUE NOT NULL,
|
|
134
|
+
executed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
CREATE INDEX IF NOT EXISTS idx_inboxes_org_status ON inboxes (org_id, status);
|
|
138
|
+
CREATE INDEX IF NOT EXISTS idx_domains_org_status ON domains (org_id, status);
|
|
139
|
+
CREATE INDEX IF NOT EXISTS idx_emails_org_inbox_created ON emails (org_id, inbox_id, created_at DESC);
|
|
140
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_emails_org_direction_message_id_unique
|
|
141
|
+
ON emails (org_id, direction, message_id) WHERE message_id IS NOT NULL;
|
|
142
|
+
CREATE INDEX IF NOT EXISTS idx_api_keys_org_revoked ON api_keys (org_id, revoked_at);
|
|
143
|
+
CREATE INDEX IF NOT EXISTS idx_idempotency_expires ON idempotency_records (expires_at);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
ALTER TABLE users
|
|
2
|
+
ADD COLUMN IF NOT EXISTS password_hash TEXT;
|
|
3
|
+
|
|
4
|
+
DO $$
|
|
5
|
+
BEGIN
|
|
6
|
+
IF EXISTS (
|
|
7
|
+
SELECT 1
|
|
8
|
+
FROM information_schema.columns
|
|
9
|
+
WHERE table_name = 'users' AND column_name = 'google_sub'
|
|
10
|
+
) THEN
|
|
11
|
+
ALTER TABLE users ALTER COLUMN google_sub DROP NOT NULL;
|
|
12
|
+
END IF;
|
|
13
|
+
END $$;
|
|
14
|
+
|
|
15
|
+
DO $$
|
|
16
|
+
BEGIN
|
|
17
|
+
IF EXISTS (
|
|
18
|
+
SELECT 1
|
|
19
|
+
FROM pg_constraint
|
|
20
|
+
WHERE conname = 'users_google_sub_key'
|
|
21
|
+
) THEN
|
|
22
|
+
ALTER TABLE users DROP CONSTRAINT users_google_sub_key;
|
|
23
|
+
END IF;
|
|
24
|
+
END $$;
|
|
25
|
+
|
|
26
|
+
UPDATE users
|
|
27
|
+
SET full_name = COALESCE(full_name, '')
|
|
28
|
+
WHERE full_name IS NULL;
|
|
29
|
+
|
|
30
|
+
UPDATE users
|
|
31
|
+
SET password_hash = COALESCE(password_hash, '')
|
|
32
|
+
WHERE password_hash IS NULL;
|
|
33
|
+
|
|
34
|
+
ALTER TABLE users
|
|
35
|
+
ALTER COLUMN full_name SET NOT NULL;
|
|
36
|
+
|
|
37
|
+
ALTER TABLE users
|
|
38
|
+
ALTER COLUMN password_hash SET NOT NULL;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
ALTER TABLE emails
|
|
2
|
+
ADD COLUMN IF NOT EXISTS delivery_status TEXT;
|
|
3
|
+
|
|
4
|
+
UPDATE emails
|
|
5
|
+
SET delivery_status = CASE
|
|
6
|
+
WHEN direction = 'inbound' THEN 'received'
|
|
7
|
+
WHEN direction = 'outbound' THEN 'queued'
|
|
8
|
+
ELSE 'unknown'
|
|
9
|
+
END
|
|
10
|
+
WHERE delivery_status IS NULL;
|
|
11
|
+
|
|
12
|
+
ALTER TABLE emails
|
|
13
|
+
ALTER COLUMN delivery_status SET DEFAULT 'unknown';
|
|
14
|
+
|
|
15
|
+
DO $$
|
|
16
|
+
BEGIN
|
|
17
|
+
IF NOT EXISTS (
|
|
18
|
+
SELECT 1
|
|
19
|
+
FROM pg_constraint
|
|
20
|
+
WHERE conname = 'emails_delivery_status_check'
|
|
21
|
+
) THEN
|
|
22
|
+
ALTER TABLE emails
|
|
23
|
+
ADD CONSTRAINT emails_delivery_status_check
|
|
24
|
+
CHECK (delivery_status IN ('unknown', 'queued', 'sent', 'failed', 'received'));
|
|
25
|
+
END IF;
|
|
26
|
+
END $$;
|
|
27
|
+
|
|
28
|
+
ALTER TABLE emails
|
|
29
|
+
ALTER COLUMN delivery_status SET NOT NULL;
|
|
30
|
+
|
|
31
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_emails_org_direction_message_id_unique
|
|
32
|
+
ON emails (org_id, direction, message_id)
|
|
33
|
+
WHERE message_id IS NOT NULL;
|
|
34
|
+
|
|
35
|
+
CREATE TABLE IF NOT EXISTS plan_limits (
|
|
36
|
+
plan TEXT PRIMARY KEY CHECK (plan IN ('free', 'paid')),
|
|
37
|
+
max_inboxes INTEGER NOT NULL,
|
|
38
|
+
max_custom_domains INTEGER NOT NULL,
|
|
39
|
+
max_api_keys INTEGER NOT NULL,
|
|
40
|
+
monthly_emails INTEGER NOT NULL,
|
|
41
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
INSERT INTO plan_limits (plan, max_inboxes, max_custom_domains, max_api_keys, monthly_emails)
|
|
45
|
+
VALUES
|
|
46
|
+
('free', 1, 0, 5, 1000),
|
|
47
|
+
('paid', 10, 1, 100, 10000)
|
|
48
|
+
ON CONFLICT (plan) DO NOTHING;
|
|
49
|
+
|
|
50
|
+
CREATE TABLE IF NOT EXISTS idempotency_records (
|
|
51
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
52
|
+
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
|
|
53
|
+
idempotency_key TEXT NOT NULL,
|
|
54
|
+
method TEXT NOT NULL,
|
|
55
|
+
request_path TEXT NOT NULL,
|
|
56
|
+
request_hash TEXT NOT NULL,
|
|
57
|
+
status TEXT NOT NULL CHECK (status IN ('in_progress', 'completed')),
|
|
58
|
+
response_status INTEGER,
|
|
59
|
+
response_body JSONB,
|
|
60
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
61
|
+
expires_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '24 hours',
|
|
62
|
+
UNIQUE (org_id, idempotency_key, method, request_path)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_idempotency_expires ON idempotency_records (expires_at);
|
|
66
|
+
|
|
67
|
+
CREATE TABLE IF NOT EXISTS webhook_events (
|
|
68
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
69
|
+
provider TEXT NOT NULL,
|
|
70
|
+
event_id TEXT NOT NULL,
|
|
71
|
+
event_type TEXT,
|
|
72
|
+
payload JSONB NOT NULL,
|
|
73
|
+
processed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
74
|
+
UNIQUE (provider, event_id)
|
|
75
|
+
);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
ALTER TABLE users
|
|
2
|
+
ADD COLUMN IF NOT EXISTS email_verified_at TIMESTAMPTZ;
|
|
3
|
+
|
|
4
|
+
UPDATE users
|
|
5
|
+
SET email_verified_at = COALESCE(email_verified_at, created_at)
|
|
6
|
+
WHERE email_verified_at IS NULL;
|
|
7
|
+
|
|
8
|
+
CREATE TABLE IF NOT EXISTS auth_tokens (
|
|
9
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
10
|
+
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
11
|
+
purpose TEXT NOT NULL CHECK (purpose IN ('email_verification', 'password_reset')),
|
|
12
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
13
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
14
|
+
used_at TIMESTAMPTZ,
|
|
15
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_auth_tokens_user_purpose
|
|
19
|
+
ON auth_tokens (user_id, purpose, created_at DESC);
|
|
20
|
+
|
|
21
|
+
CREATE INDEX IF NOT EXISTS idx_auth_tokens_expires
|
|
22
|
+
ON auth_tokens (expires_at);
|
package/src/config.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import dotenv from 'dotenv';
|
|
2
|
+
|
|
3
|
+
dotenv.config();
|
|
4
|
+
|
|
5
|
+
const required = ['DATABASE_URL', 'APP_BASE_URL', 'APP_SHARED_DOMAIN', 'SESSION_SECRET', 'API_KEY_HASH_SECRET'];
|
|
6
|
+
for (const key of required) {
|
|
7
|
+
if (!process.env[key]) {
|
|
8
|
+
throw new Error(`Missing required environment variable: ${key}`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const config = {
|
|
13
|
+
nodeEnv: process.env.NODE_ENV ?? 'development',
|
|
14
|
+
port: Number(process.env.PORT ?? 3000),
|
|
15
|
+
appBaseUrl: process.env.APP_BASE_URL,
|
|
16
|
+
appSharedDomain: process.env.APP_SHARED_DOMAIN.toLowerCase(),
|
|
17
|
+
adminEmail: (process.env.ADMIN_EMAIL ?? 'jagan.ganti@gmail.com').toLowerCase(),
|
|
18
|
+
sessionSecret: process.env.SESSION_SECRET,
|
|
19
|
+
apiKeyHashSecret: process.env.API_KEY_HASH_SECRET,
|
|
20
|
+
databaseUrl: process.env.DATABASE_URL,
|
|
21
|
+
mailgunApiKey: process.env.MAILGUN_API_KEY,
|
|
22
|
+
mailgunRegion: process.env.MAILGUN_REGION ?? 'us',
|
|
23
|
+
mailgunWebhookSigningKey: process.env.MAILGUN_WEBHOOK_SIGNING_KEY,
|
|
24
|
+
mailgunAccountDomain: process.env.MAILGUN_ACCOUNT_DOMAIN,
|
|
25
|
+
mailFromEmail: process.env.MAIL_FROM_EMAIL,
|
|
26
|
+
dodoPaymentsApiKey: process.env.DODO_PAYMENTS_API_KEY,
|
|
27
|
+
dodoPaymentsEnvironment: process.env.DODO_PAYMENTS_ENVIRONMENT ?? 'test_mode',
|
|
28
|
+
dodoPaymentsWebhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY,
|
|
29
|
+
dodoPaidProductId: process.env.DODO_PAYMENTS_PRODUCT_ID_PAID
|
|
30
|
+
};
|
package/src/db.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import pg from 'pg';
|
|
2
|
+
import { config } from './config.js';
|
|
3
|
+
|
|
4
|
+
const { Pool } = pg;
|
|
5
|
+
|
|
6
|
+
export const pool = new Pool({ connectionString: config.databaseUrl });
|
|
7
|
+
|
|
8
|
+
export async function query(text, values = []) {
|
|
9
|
+
return pool.query(text, values);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function withTransaction(fn) {
|
|
13
|
+
const client = await pool.connect();
|
|
14
|
+
try {
|
|
15
|
+
await client.query('BEGIN');
|
|
16
|
+
const result = await fn(client);
|
|
17
|
+
await client.query('COMMIT');
|
|
18
|
+
return result;
|
|
19
|
+
} catch (err) {
|
|
20
|
+
await client.query('ROLLBACK');
|
|
21
|
+
throw err;
|
|
22
|
+
} finally {
|
|
23
|
+
client.release();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fp from 'fastify-plugin';
|
|
2
|
+
import { hashApiKey } from './security.js';
|
|
3
|
+
import { getApiKeyByHash, getOrganizationById, touchApiKey } from './store.js';
|
|
4
|
+
|
|
5
|
+
function parseBearer(authHeader) {
|
|
6
|
+
if (!authHeader) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
const [scheme, token] = authHeader.split(' ');
|
|
10
|
+
if (!scheme || !token || scheme.toLowerCase() !== 'bearer') {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
return token.trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const apiAuthPlugin = fp(async function apiAuthPlugin(fastify) {
|
|
17
|
+
fastify.decorateRequest('apiAuth', null);
|
|
18
|
+
|
|
19
|
+
fastify.decorate('requireApiKey', async (req, reply) => {
|
|
20
|
+
const token = parseBearer(req.headers.authorization);
|
|
21
|
+
if (!token) {
|
|
22
|
+
return reply.code(401).send({ error: 'Missing bearer token' });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const keyHash = hashApiKey(token);
|
|
26
|
+
const apiKey = await getApiKeyByHash(keyHash);
|
|
27
|
+
if (!apiKey) {
|
|
28
|
+
return reply.code(401).send({ error: 'Invalid API key' });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const org = await getOrganizationById(apiKey.org_id);
|
|
32
|
+
if (!org) {
|
|
33
|
+
return reply.code(401).send({ error: 'Invalid API key context' });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
await touchApiKey(apiKey.id);
|
|
37
|
+
req.apiAuth = {
|
|
38
|
+
apiKey,
|
|
39
|
+
org
|
|
40
|
+
};
|
|
41
|
+
return undefined;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
fastify.decorate('requireScope', (scope) => {
|
|
45
|
+
return async (req, reply) => {
|
|
46
|
+
if (!req.apiAuth) {
|
|
47
|
+
return reply.code(401).send({ error: 'Unauthorized' });
|
|
48
|
+
}
|
|
49
|
+
if (!req.apiAuth.apiKey.scopes.includes(scope) && !req.apiAuth.apiKey.scopes.includes('*')) {
|
|
50
|
+
return reply.code(403).send({ error: `Missing scope: ${scope}` });
|
|
51
|
+
}
|
|
52
|
+
return undefined;
|
|
53
|
+
};
|
|
54
|
+
});
|
|
55
|
+
});
|
package/src/lib/auth.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fp from 'fastify-plugin';
|
|
2
|
+
import fastifyCookie from '@fastify/cookie';
|
|
3
|
+
import fastifySession from '@fastify/session';
|
|
4
|
+
import { config } from '../config.js';
|
|
5
|
+
import { getAuthSession } from './session.js';
|
|
6
|
+
import { ensureCsrfToken, hasValidCsrfToken } from './csrf.js';
|
|
7
|
+
import { getOrganizationForUser, getUserById } from './store.js';
|
|
8
|
+
|
|
9
|
+
export const authPlugin = fp(async function authPlugin(fastify) {
|
|
10
|
+
await fastify.register(fastifyCookie);
|
|
11
|
+
await fastify.register(fastifySession, {
|
|
12
|
+
secret: config.sessionSecret,
|
|
13
|
+
cookieName: 'agentinbox.sid',
|
|
14
|
+
saveUninitialized: false,
|
|
15
|
+
rolling: true,
|
|
16
|
+
cookie: {
|
|
17
|
+
secure: config.nodeEnv === 'production' ? 'auto' : false,
|
|
18
|
+
httpOnly: true,
|
|
19
|
+
sameSite: 'lax',
|
|
20
|
+
maxAge: 1000 * 60 * 60 * 12
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
fastify.decorateRequest('authUser', null);
|
|
25
|
+
fastify.decorateRequest('authOrg', null);
|
|
26
|
+
|
|
27
|
+
fastify.decorate('ensureCsrfToken', (req) => ensureCsrfToken(req));
|
|
28
|
+
|
|
29
|
+
fastify.decorate('attachCsrfToView', (req, reply) => {
|
|
30
|
+
reply.locals = {
|
|
31
|
+
...(reply.locals ?? {}),
|
|
32
|
+
csrfToken: fastify.ensureCsrfToken(req)
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
fastify.decorate('requireCsrf', async (req, reply) => {
|
|
37
|
+
if (hasValidCsrfToken(req)) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
return reply.code(403).send('Invalid CSRF token');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
fastify.decorate('loadAuthContext', async (req) => {
|
|
44
|
+
const auth = getAuthSession(req);
|
|
45
|
+
if (!auth?.userId || !auth?.orgId) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const user = await getUserById(auth.userId);
|
|
50
|
+
if (!user) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const org = await getOrganizationForUser(auth.userId, auth.orgId);
|
|
55
|
+
if (!org) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
req.authUser = user;
|
|
60
|
+
req.authOrg = org;
|
|
61
|
+
return { user, org };
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
fastify.decorate('requireUser', async (req, reply) => {
|
|
65
|
+
const context = await fastify.loadAuthContext(req);
|
|
66
|
+
if (!context) {
|
|
67
|
+
return reply.redirect('/');
|
|
68
|
+
}
|
|
69
|
+
return undefined;
|
|
70
|
+
});
|
|
71
|
+
});
|
package/src/lib/csrf.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
const CSRF_TOKEN_KEY = 'csrfToken';
|
|
4
|
+
const TOKEN_BYTES = 32;
|
|
5
|
+
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
|
|
6
|
+
|
|
7
|
+
function asSingleValue(value) {
|
|
8
|
+
if (Array.isArray(value)) {
|
|
9
|
+
return String(value[0] ?? '');
|
|
10
|
+
}
|
|
11
|
+
return String(value ?? '');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function ensureCsrfToken(req) {
|
|
15
|
+
const existing = asSingleValue(req.session?.[CSRF_TOKEN_KEY]).trim();
|
|
16
|
+
if (existing) {
|
|
17
|
+
return existing;
|
|
18
|
+
}
|
|
19
|
+
const created = crypto.randomBytes(TOKEN_BYTES).toString('hex');
|
|
20
|
+
req.session[CSRF_TOKEN_KEY] = created;
|
|
21
|
+
return created;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function hasValidCsrfToken(req) {
|
|
25
|
+
if (SAFE_METHODS.has(req.method)) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const sessionToken = asSingleValue(req.session?.[CSRF_TOKEN_KEY]).trim();
|
|
30
|
+
if (!sessionToken) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const bodyToken = asSingleValue(req.body?._csrf).trim();
|
|
35
|
+
const headerToken = asSingleValue(req.headers['x-csrf-token']).trim();
|
|
36
|
+
const candidate = bodyToken || headerToken;
|
|
37
|
+
if (!candidate || candidate.length !== sessionToken.length) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
return crypto.timingSafeEqual(Buffer.from(candidate), Buffer.from(sessionToken));
|
|
43
|
+
} catch (_) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/lib/dodo.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import DodoPayments from 'dodopayments';
|
|
2
|
+
import { config } from '../config.js';
|
|
3
|
+
|
|
4
|
+
let client = null;
|
|
5
|
+
|
|
6
|
+
function getClient() {
|
|
7
|
+
if (!client) {
|
|
8
|
+
client = new DodoPayments({
|
|
9
|
+
bearerToken: config.dodoPaymentsApiKey,
|
|
10
|
+
webhookKey: config.dodoPaymentsWebhookKey || null,
|
|
11
|
+
environment: config.dodoPaymentsEnvironment
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
return client;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const dodo = {
|
|
18
|
+
isConfigured() {
|
|
19
|
+
return Boolean(config.dodoPaymentsApiKey && config.dodoPaidProductId);
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
getClient,
|
|
23
|
+
|
|
24
|
+
async createPaidUpgradeCheckout({ customerName, customerEmail, orgId, returnUrl }) {
|
|
25
|
+
if (!this.isConfigured()) {
|
|
26
|
+
throw new Error('Dodo Payments is not configured');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const response = await getClient().checkoutSessions.create({
|
|
30
|
+
customer: {
|
|
31
|
+
email: customerEmail,
|
|
32
|
+
name: customerName
|
|
33
|
+
},
|
|
34
|
+
product_cart: [
|
|
35
|
+
{
|
|
36
|
+
product_id: config.dodoPaidProductId,
|
|
37
|
+
quantity: 1
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
metadata: {
|
|
41
|
+
org_id: orgId,
|
|
42
|
+
target_plan: 'paid'
|
|
43
|
+
},
|
|
44
|
+
return_url: returnUrl
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return response;
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
async getCheckoutStatus(sessionId) {
|
|
51
|
+
if (!sessionId) {
|
|
52
|
+
throw new Error('sessionId is required');
|
|
53
|
+
}
|
|
54
|
+
return getClient().checkoutSessions.retrieve(sessionId);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
unwrapWebhook({ body, headers }) {
|
|
58
|
+
return getClient().webhooks.unwrap(body, {
|
|
59
|
+
headers,
|
|
60
|
+
key: config.dodoPaymentsWebhookKey
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
unsafeUnwrapWebhook(body) {
|
|
65
|
+
return getClient().webhooks.unsafeUnwrap(body);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
function greeting(fullName) {
|
|
2
|
+
const safeName = String(fullName ?? '').trim();
|
|
3
|
+
return safeName ? `Hi ${safeName},` : 'Hi,';
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function buildVerificationEmailTemplate({ fullName, verifyUrl }) {
|
|
7
|
+
const intro = greeting(fullName);
|
|
8
|
+
const subject = 'Verify your EmailAgent account';
|
|
9
|
+
const text = `${intro}
|
|
10
|
+
|
|
11
|
+
Please verify your email to activate your EmailAgent account.
|
|
12
|
+
|
|
13
|
+
Verify now: ${verifyUrl}
|
|
14
|
+
|
|
15
|
+
This link expires in 24 hours.
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
const html = `
|
|
19
|
+
<p>${intro}</p>
|
|
20
|
+
<p>Please verify your email to activate your EmailAgent account.</p>
|
|
21
|
+
<p><a href="${verifyUrl}">Verify your account</a></p>
|
|
22
|
+
<p>This link expires in 24 hours.</p>
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
return { subject, text, html };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildWelcomeEmailTemplate({ fullName, appUrl }) {
|
|
29
|
+
const intro = greeting(fullName);
|
|
30
|
+
const subject = 'Welcome to EmailAgent';
|
|
31
|
+
const text = `${intro}
|
|
32
|
+
|
|
33
|
+
Welcome to EmailAgent. Your account is now active.
|
|
34
|
+
|
|
35
|
+
Open your dashboard: ${appUrl}/app/inboxes
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
const html = `
|
|
39
|
+
<p>${intro}</p>
|
|
40
|
+
<p>Welcome to EmailAgent. Your account is now active.</p>
|
|
41
|
+
<p><a href="${appUrl}/app/inboxes">Open your dashboard</a></p>
|
|
42
|
+
`;
|
|
43
|
+
|
|
44
|
+
return { subject, text, html };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function buildPasswordResetEmailTemplate({ fullName, resetUrl }) {
|
|
48
|
+
const intro = greeting(fullName);
|
|
49
|
+
const subject = 'Reset your EmailAgent password';
|
|
50
|
+
const text = `${intro}
|
|
51
|
+
|
|
52
|
+
We received a request to reset your password.
|
|
53
|
+
|
|
54
|
+
Reset password: ${resetUrl}
|
|
55
|
+
|
|
56
|
+
This link expires in 1 hour. If you did not request this, you can ignore this email.
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
const html = `
|
|
60
|
+
<p>${intro}</p>
|
|
61
|
+
<p>We received a request to reset your password.</p>
|
|
62
|
+
<p><a href="${resetUrl}">Reset password</a></p>
|
|
63
|
+
<p>This link expires in 1 hour. If you did not request this, you can ignore this email.</p>
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
return { subject, text, html };
|
|
67
|
+
}
|