Update api to support stripe
Remove secrets
This commit is contained in:
parent
20e07c11e6
commit
cb154cf5b6
9 changed files with 610 additions and 3 deletions
|
|
@ -4,6 +4,8 @@ SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhY
|
|||
STREAM_CHAT_API_KEY=t5vvvddteapa
|
||||
STREAM_CHAT_API_SECRET=zrr32sqenw3atpv9rnz2nhhyyncf7bunr7fmfqy9r7e69fcw978dhzevmhpxa2jj
|
||||
|
||||
STRIPE_SECRET_KEY=sk_test_51Qc159AmcXPHW4mTeEs86NXY2lAz6pPKiSteECBTsQ2BmaJxeFkbO4uopoMZM8USggRYJjuwJ4GCXVzy6ROT1hMJ00NJGOUM33
|
||||
STRIPE_WEBHOOK_SECRET=whsec_4c6f3742c4f3760eff1ef974202cb7f27acc93b8a0da6529db7b2ff2d5acec02
|
||||
|
||||
XTABLO_URL="https://app-staging.xtablo.com"
|
||||
|
||||
|
|
|
|||
191
api/package-lock.json
generated
191
api/package-lock.json
generated
|
|
@ -8,6 +8,7 @@
|
|||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.850.0",
|
||||
"@hono/node-server": "^1.14.4",
|
||||
"@supabase/stripe-sync-engine": "^0.45.0",
|
||||
"@supabase/supabase-js": "^2.49.4",
|
||||
"cors": "^2.8.5",
|
||||
"dd-trace": "^5.74.0",
|
||||
|
|
@ -20,6 +21,7 @@
|
|||
"multer": "^2.0.2",
|
||||
"nodemailer": "^7.0.4",
|
||||
"stream-chat": "^9.8.0",
|
||||
"stripe": "^19.2.0",
|
||||
"ts-node": "^10.9.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -33,6 +35,7 @@
|
|||
"@types/sinon": "^17.0.0",
|
||||
"chai": "^4.3.0",
|
||||
"mocha": "^10.0.0",
|
||||
"pino": "^10.1.0",
|
||||
"sinon": "^17.0.0",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.8.3"
|
||||
|
|
@ -2610,6 +2613,12 @@
|
|||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@pinojs/redact": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
|
||||
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
|
|
@ -3452,6 +3461,19 @@
|
|||
"@supabase/node-fetch": "^2.6.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/stripe-sync-engine": {
|
||||
"version": "0.45.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/stripe-sync-engine/-/stripe-sync-engine-0.45.0.tgz",
|
||||
"integrity": "sha512-3yP6Lyqg+jBZdI3MyGM0gBGmVmaaSBI5QO4hwvmsdJmIztU/3Wvu/YF0S8Ga1waWdeG1/9YChvVJV7gRON+t1A==",
|
||||
"dependencies": {
|
||||
"pg": "^8.16.3",
|
||||
"pg-node-migrations": "0.0.8",
|
||||
"yesql": "^7.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"stripe": "> 11"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/supabase-js": {
|
||||
"version": "2.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.50.0.tgz",
|
||||
|
|
@ -3844,6 +3866,15 @@
|
|||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/atomic-sleep": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
|
||||
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.12.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
|
||||
|
|
@ -5543,9 +5574,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/hono": {
|
||||
"version": "4.9.8",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.9.8.tgz",
|
||||
"integrity": "sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg==",
|
||||
"version": "4.10.4",
|
||||
"resolved": "https://registry.npmjs.org/hono/-/hono-4.10.4.tgz",
|
||||
"integrity": "sha512-YG/fo7zlU3KwrBL5vDpWKisLYiM+nVstBQqfr7gCPbSYURnNEP9BDxEMz8KfsDR9JX0lJWDRNc6nXX31v7ZEyg==",
|
||||
"engines": {
|
||||
"node": ">=16.9.0"
|
||||
}
|
||||
|
|
@ -6621,6 +6652,15 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-exit-leak-free": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
|
||||
"integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
|
|
@ -6897,6 +6937,22 @@
|
|||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-node-migrations": {
|
||||
"version": "0.0.8",
|
||||
"resolved": "https://registry.npmjs.org/pg-node-migrations/-/pg-node-migrations-0.0.8.tgz",
|
||||
"integrity": "sha512-44cMl9umOmCv0hzZyEcvjEq8Bm8u7mrzggZ06qXTJVSsMMB4j2OsjG+rSp+uzeKWyP2Vu0K9Ye2wKtjFUJwrdw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg": "^8.6.0",
|
||||
"sql-template-strings": "^2.2.2"
|
||||
},
|
||||
"bin": {
|
||||
"pg-validate-migrations": "dist/bin/validate.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">10.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
|
||||
|
|
@ -6950,6 +7006,43 @@
|
|||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pino": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz",
|
||||
"integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@pinojs/redact": "^0.4.0",
|
||||
"atomic-sleep": "^1.0.0",
|
||||
"on-exit-leak-free": "^2.1.0",
|
||||
"pino-abstract-transport": "^2.0.0",
|
||||
"pino-std-serializers": "^7.0.0",
|
||||
"process-warning": "^5.0.0",
|
||||
"quick-format-unescaped": "^4.0.3",
|
||||
"real-require": "^0.2.0",
|
||||
"safe-stable-stringify": "^2.3.1",
|
||||
"sonic-boom": "^4.0.1",
|
||||
"thread-stream": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"pino": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/pino-abstract-transport": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
|
||||
"integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"split2": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pino-std-serializers": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz",
|
||||
"integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
|
|
@ -7022,6 +7115,22 @@
|
|||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/process-warning": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
|
||||
"integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/proto3-json-serializer": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz",
|
||||
|
|
@ -7131,6 +7240,12 @@
|
|||
"resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
|
||||
"integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag=="
|
||||
},
|
||||
"node_modules/quick-format-unescaped": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
|
||||
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
|
|
@ -7195,6 +7310,15 @@
|
|||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/real-require": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
|
||||
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 12.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
|
|
@ -7299,6 +7423,15 @@
|
|||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-stable-stringify": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
|
||||
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
|
|
@ -7523,6 +7656,15 @@
|
|||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/sonic-boom": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
|
||||
"integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"atomic-sleep": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.7.6",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
|
||||
|
|
@ -7544,6 +7686,15 @@
|
|||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/sql-template-strings": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/sql-template-strings/-/sql-template-strings-2.2.2.tgz",
|
||||
"integrity": "sha512-UXhXR2869FQaD+GMly8jAMCRZ94nU5KcrFetZfWEMd+LVVG6y0ExgHAhatEcKZ/wk8YcKPdi+hiD2wm75lq3/Q==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/stream-chat": {
|
||||
"version": "9.8.0",
|
||||
"resolved": "https://registry.npmjs.org/stream-chat/-/stream-chat-9.8.0.tgz",
|
||||
|
|
@ -7659,6 +7810,25 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/stripe": {
|
||||
"version": "19.2.0",
|
||||
"resolved": "https://registry.npmjs.org/stripe/-/stripe-19.2.0.tgz",
|
||||
"integrity": "sha512-strzN8luMGMC1LEleGKg7pJGXFx0kSS4y/uSjK8yPQV9SUBMtJVAp/v8XMQLRnMbXaSaWLrIaHcMlKcsizdRDQ==",
|
||||
"dependencies": {
|
||||
"qs": "^6.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=16"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/strnum": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz",
|
||||
|
|
@ -7791,6 +7961,15 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/thread-stream": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
|
||||
"integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"real-require": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/through": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||
|
|
@ -8186,6 +8365,12 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yesql": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yesql/-/yesql-7.0.0.tgz",
|
||||
"integrity": "sha512-sosfr7agy4ibLM7BvXBkM6BpBmKMGuBO8DUYQEuey+QqaqrgW+2bsSg6D050ocBYIz0PuHxUyehyzEztZTU4pw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "^3.850.0",
|
||||
"@hono/node-server": "^1.14.4",
|
||||
"@supabase/stripe-sync-engine": "^0.45.0",
|
||||
"@supabase/supabase-js": "^2.49.4",
|
||||
"cors": "^2.8.5",
|
||||
"dd-trace": "^5.74.0",
|
||||
|
|
@ -26,6 +27,7 @@
|
|||
"multer": "^2.0.2",
|
||||
"nodemailer": "^7.0.4",
|
||||
"stream-chat": "^9.8.0",
|
||||
"stripe": "^19.2.0",
|
||||
"ts-node": "^10.9.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -39,6 +41,7 @@
|
|||
"@types/sinon": "^17.0.0",
|
||||
"chai": "^4.3.0",
|
||||
"mocha": "^10.0.0",
|
||||
"pino": "^10.1.0",
|
||||
"sinon": "^17.0.0",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.8.3"
|
||||
|
|
|
|||
|
|
@ -6,8 +6,11 @@ export interface AppConfig {
|
|||
SUPABASE_URL: string;
|
||||
SUPABASE_SERVICE_ROLE_KEY: string;
|
||||
SUPABASE_CONNECTION_STRING: string;
|
||||
SUPABASE_CA_CERT: string;
|
||||
STREAM_CHAT_API_KEY: string;
|
||||
STREAM_CHAT_API_SECRET: string;
|
||||
STRIPE_SECRET_KEY: string;
|
||||
STRIPE_WEBHOOK_SECRET: string;
|
||||
EMAIL_USER: string;
|
||||
EMAIL_CLIENT_ID: string;
|
||||
EMAIL_CLIENT_SECRET: string;
|
||||
|
|
@ -47,11 +50,17 @@ function createConfig(): AppConfig {
|
|||
process.env.SUPABASE_SERVICE_ROLE_KEY
|
||||
),
|
||||
SUPABASE_CONNECTION_STRING: process.env.SUPABASE_CONNECTION_STRING || "",
|
||||
SUPABASE_CA_CERT: process.env.SUPABASE_CA_CERT || "",
|
||||
STREAM_CHAT_API_KEY: validateEnvVar("STREAM_CHAT_API_KEY", process.env.STREAM_CHAT_API_KEY),
|
||||
STREAM_CHAT_API_SECRET: validateEnvVar(
|
||||
"STREAM_CHAT_API_SECRET",
|
||||
process.env.STREAM_CHAT_API_SECRET
|
||||
),
|
||||
STRIPE_SECRET_KEY: validateEnvVar("STRIPE_SECRET_KEY", process.env.STRIPE_SECRET_KEY),
|
||||
STRIPE_WEBHOOK_SECRET: validateEnvVar(
|
||||
"STRIPE_WEBHOOK_SECRET",
|
||||
process.env.STRIPE_WEBHOOK_SECRET
|
||||
),
|
||||
EMAIL_USER: validateEnvVar("EMAIL_USER", process.env.EMAIL_USER),
|
||||
EMAIL_CLIENT_ID: validateEnvVar("EMAIL_CLIENT_ID", process.env.EMAIL_CLIENT_ID),
|
||||
EMAIL_CLIENT_SECRET: validateEnvVar("EMAIL_CLIENT_SECRET", process.env.EMAIL_CLIENT_SECRET),
|
||||
|
|
|
|||
|
|
@ -355,6 +355,7 @@ export type Database = {
|
|||
last_name: string | null
|
||||
last_signed_in: string | null
|
||||
name: string | null
|
||||
plan: Database["public"]["Enums"]["subscription_plan"] | null
|
||||
short_user_id: string
|
||||
}
|
||||
Insert: {
|
||||
|
|
@ -366,6 +367,7 @@ export type Database = {
|
|||
last_name?: string | null
|
||||
last_signed_in?: string | null
|
||||
name?: string | null
|
||||
plan?: Database["public"]["Enums"]["subscription_plan"] | null
|
||||
short_user_id: string
|
||||
}
|
||||
Update: {
|
||||
|
|
@ -377,6 +379,7 @@ export type Database = {
|
|||
last_name?: string | null
|
||||
last_signed_in?: string | null
|
||||
name?: string | null
|
||||
plan?: Database["public"]["Enums"]["subscription_plan"] | null
|
||||
short_user_id?: string
|
||||
}
|
||||
Relationships: []
|
||||
|
|
@ -628,9 +631,100 @@ export type Database = {
|
|||
}
|
||||
Functions: {
|
||||
generate_random_string: { Args: { length?: number }; Returns: string }
|
||||
get_my_active_subscription: {
|
||||
Args: never
|
||||
Returns: {
|
||||
billing_interval: string
|
||||
cancel_at_period_end: boolean
|
||||
currency: string
|
||||
current_period_end: string
|
||||
current_period_start: string
|
||||
first_name: string
|
||||
last_name: string
|
||||
plan: Database["public"]["Enums"]["subscription_plan"]
|
||||
product_name: string
|
||||
status: string
|
||||
subscription_id: string
|
||||
unit_amount: number
|
||||
user_email: string
|
||||
user_id: string
|
||||
}[]
|
||||
}
|
||||
get_stripe_prices: {
|
||||
Args: never
|
||||
Returns: {
|
||||
active: boolean
|
||||
created: number
|
||||
currency: string
|
||||
id: string
|
||||
metadata: Json
|
||||
product: string
|
||||
recurring: Json
|
||||
unit_amount: number
|
||||
}[]
|
||||
}
|
||||
get_stripe_products: {
|
||||
Args: never
|
||||
Returns: {
|
||||
active: boolean
|
||||
created: number
|
||||
description: string
|
||||
id: string
|
||||
metadata: Json
|
||||
name: string
|
||||
}[]
|
||||
}
|
||||
get_user_stripe_customer: {
|
||||
Args: never
|
||||
Returns: {
|
||||
created: number
|
||||
email: string
|
||||
id: string
|
||||
metadata: Json
|
||||
user_id: string
|
||||
}[]
|
||||
}
|
||||
get_user_stripe_customer_id: {
|
||||
Args: { user_uuid: string }
|
||||
Returns: string
|
||||
}
|
||||
get_user_stripe_subscriptions: {
|
||||
Args: never
|
||||
Returns: {
|
||||
cancel_at_period_end: boolean
|
||||
canceled_at: number
|
||||
created: number
|
||||
current_period_end: number
|
||||
current_period_start: number
|
||||
customer: string
|
||||
id: string
|
||||
metadata: Json
|
||||
price_id: string
|
||||
quantity: number
|
||||
status: string
|
||||
trial_end: Json
|
||||
trial_start: Json
|
||||
user_id: string
|
||||
}[]
|
||||
}
|
||||
get_user_subscription_status: {
|
||||
Args: { user_uuid: string }
|
||||
Returns: {
|
||||
cancel_at_period_end: boolean
|
||||
current_period_end: number
|
||||
current_period_start: number
|
||||
plan: Database["public"]["Enums"]["subscription_plan"]
|
||||
price_id: string
|
||||
product_name: string
|
||||
status: string
|
||||
subscription_id: string
|
||||
}[]
|
||||
}
|
||||
is_paying_user: { Args: { user_uuid: string }; Returns: boolean }
|
||||
}
|
||||
Enums: {
|
||||
devis_status: "draft" | "sent" | "accepted" | "rejected" | "expired"
|
||||
subscription_plan: "none" | "trial" | "standard"
|
||||
}
|
||||
CompositeTypes: {
|
||||
time_range: {
|
||||
|
|
@ -762,6 +856,7 @@ export const Constants = {
|
|||
public: {
|
||||
Enums: {
|
||||
devis_status: ["draft", "sent", "accepted", "rejected", "expired"],
|
||||
subscription_plan: ["none", "trial", "standard"],
|
||||
},
|
||||
},
|
||||
} as const
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { tabloDataRouter } from "./tablo_data.js";
|
|||
import { taskRouter } from "./tasks.js";
|
||||
import { userRouter } from "./user.js";
|
||||
import { notesRouter } from "./notes.js";
|
||||
import { stripeRouter, stripeWebhookRouter } from "./stripe.js";
|
||||
|
||||
export const mainRouter = new Hono<{
|
||||
Bindings: {
|
||||
|
|
@ -36,3 +37,6 @@ mainRouter.route("/tablos", tabloRouter);
|
|||
mainRouter.route("/tasks", taskRouter);
|
||||
mainRouter.route("/tablo-data", tabloDataRouter);
|
||||
mainRouter.route("/notes", notesRouter);
|
||||
// stripe routes
|
||||
mainRouter.route("/stripe", stripeRouter);
|
||||
mainRouter.route("/stripe-webhook", stripeWebhookRouter);
|
||||
|
|
|
|||
279
api/src/stripe.ts
Normal file
279
api/src/stripe.ts
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
import { Hono } from "hono";
|
||||
import type { SupabaseClient, User } from "@supabase/supabase-js";
|
||||
import Stripe from "stripe";
|
||||
import { authMiddleware, regularUserCheckMiddleware } from "./middleware.js";
|
||||
import { config } from "./config.js";
|
||||
import { stripeSync } from "./stripeSync.js";
|
||||
|
||||
const stripe = new Stripe(config.STRIPE_SECRET_KEY || "", {
|
||||
apiVersion: "2025-10-29.clover",
|
||||
});
|
||||
|
||||
export const stripeRouter = new Hono<{
|
||||
Variables: {
|
||||
user: User;
|
||||
supabase: SupabaseClient;
|
||||
};
|
||||
}>();
|
||||
|
||||
stripeRouter.use(authMiddleware);
|
||||
|
||||
// ============================================================================
|
||||
// Webhook endpoint (no auth required - validated by signature)
|
||||
// ============================================================================
|
||||
|
||||
export const stripeWebhookRouter = new Hono();
|
||||
|
||||
/**
|
||||
* Stripe webhook handler using @supabase/stripe-sync-engine
|
||||
* This automatically syncs all Stripe events to Supabase tables
|
||||
* Repository: https://github.com/supabase/stripe-sync-engine
|
||||
*/
|
||||
stripeWebhookRouter.post("/", async (c) => {
|
||||
try {
|
||||
const signature = c.req.header("stripe-signature");
|
||||
|
||||
if (!signature) {
|
||||
return c.json({ error: "No signature provided" }, 400);
|
||||
}
|
||||
|
||||
// Get raw body for signature verification
|
||||
const rawBody = await c.req.text();
|
||||
|
||||
// Process webhook using Stripe Sync Engine
|
||||
// This handles signature verification and syncing automatically
|
||||
await stripeSync.processWebhook(rawBody, signature);
|
||||
|
||||
return c.json({ received: true });
|
||||
} catch (error) {
|
||||
console.error("Webhook error:", error);
|
||||
return c.json(
|
||||
{ error: error instanceof Error ? error.message : "Webhook processing failed" },
|
||||
400
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Authenticated endpoints
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create a Stripe Checkout Session
|
||||
* POST /api/v1/stripe/create-checkout-session
|
||||
*/
|
||||
stripeRouter.post("/create-checkout-session", regularUserCheckMiddleware, async (c) => {
|
||||
const user = c.get("user");
|
||||
const supabase = c.get("supabase");
|
||||
const body = await c.req.json();
|
||||
const { priceId, successUrl, cancelUrl } = body;
|
||||
|
||||
if (!priceId) {
|
||||
return c.json({ error: "priceId is required" }, 400);
|
||||
}
|
||||
|
||||
try {
|
||||
// Get or create Stripe customer
|
||||
let customerId: string;
|
||||
|
||||
// Check if customer already exists by querying stripe schema with metadata filter
|
||||
// Note: Using service role, so we filter manually by metadata
|
||||
const { data: customers } = await supabase
|
||||
.schema("stripe")
|
||||
.from("customers")
|
||||
.select("id, metadata")
|
||||
.limit(1000); // Get all customers to filter by metadata
|
||||
|
||||
const existingCustomer = customers?.find(
|
||||
(c: Stripe.Customer) => c.metadata?.user_id === user.id
|
||||
);
|
||||
|
||||
if (existingCustomer) {
|
||||
customerId = existingCustomer.id;
|
||||
} else {
|
||||
// Create new Stripe customer with user_id in metadata
|
||||
// stripe-sync-engine will automatically sync this to the database via webhook
|
||||
const customer = await stripe.customers.create({
|
||||
email: user.email!,
|
||||
metadata: {
|
||||
user_id: user.id, // Stored in metadata for tracking
|
||||
},
|
||||
});
|
||||
|
||||
customerId = customer.id;
|
||||
}
|
||||
|
||||
// Create Checkout Session
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
customer: customerId,
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
mode: "subscription",
|
||||
success_url: successUrl || `${process.env.FRONTEND_URL}/settings?success=true`,
|
||||
cancel_url: cancelUrl || `${process.env.FRONTEND_URL}/settings?canceled=true`,
|
||||
metadata: {
|
||||
user_id: user.id,
|
||||
},
|
||||
subscription_data: {
|
||||
metadata: {
|
||||
user_id: user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return c.json({ sessionId: session.id, url: session.url });
|
||||
} catch (error) {
|
||||
console.error("Error creating checkout session:", error);
|
||||
return c.json(
|
||||
{ error: error instanceof Error ? error.message : "Failed to create checkout session" },
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a Stripe Customer Portal Session
|
||||
* POST /api/v1/stripe/create-portal-session
|
||||
*/
|
||||
stripeRouter.post("/create-portal-session", regularUserCheckMiddleware, async (c) => {
|
||||
const user = c.get("user");
|
||||
const supabase = c.get("supabase");
|
||||
const body = await c.req.json();
|
||||
const { returnUrl } = body;
|
||||
|
||||
try {
|
||||
// Get Stripe customer ID by filtering metadata
|
||||
const { data: customers } = await supabase
|
||||
.schema("stripe")
|
||||
.from("customers")
|
||||
.select("id, metadata");
|
||||
|
||||
const customer = customers?.find((c: Stripe.Customer) => c.metadata?.user_id === user.id);
|
||||
|
||||
if (!customer) {
|
||||
return c.json({ error: "No Stripe customer found" }, 404);
|
||||
}
|
||||
|
||||
// Create portal session
|
||||
const session = await stripe.billingPortal.sessions.create({
|
||||
customer: customer.id,
|
||||
return_url: returnUrl || `${process.env.FRONTEND_URL}/settings`,
|
||||
});
|
||||
|
||||
return c.json({ url: session.url });
|
||||
} catch (error) {
|
||||
console.error("Error creating portal session:", error);
|
||||
return c.json(
|
||||
{ error: error instanceof Error ? error.message : "Failed to create portal session" },
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Note: Subscription status queries are handled directly from the frontend
|
||||
// using Supabase client with RLS policies. No API endpoints needed for reads.
|
||||
|
||||
/**
|
||||
* Cancel subscription at period end
|
||||
* POST /api/v1/stripe/cancel-subscription
|
||||
*/
|
||||
stripeRouter.post("/cancel-subscription", regularUserCheckMiddleware, async (c) => {
|
||||
const user = c.get("user");
|
||||
const supabase = c.get("supabase");
|
||||
|
||||
try {
|
||||
// Get user's Stripe customer first
|
||||
const { data: customers } = await supabase
|
||||
.schema("stripe")
|
||||
.from("customers")
|
||||
.select("id, metadata");
|
||||
|
||||
const customer = customers?.find((c: Stripe.Customer) => c.metadata?.user_id === user.id);
|
||||
|
||||
if (!customer) {
|
||||
return c.json({ error: "Customer not found" }, 404);
|
||||
}
|
||||
|
||||
// Get user's active subscription for this customer
|
||||
const { data: subscription } = await supabase
|
||||
.schema("stripe")
|
||||
.from("subscriptions")
|
||||
.select("id, status")
|
||||
.eq("customer", customer.id)
|
||||
.in("status", ["active", "trialing"])
|
||||
.maybeSingle();
|
||||
|
||||
if (!subscription) {
|
||||
return c.json({ error: "No active subscription found" }, 404);
|
||||
}
|
||||
|
||||
// Cancel subscription at period end in Stripe
|
||||
// The webhook will automatically sync the change to our database
|
||||
await stripe.subscriptions.cancel(subscription.id);
|
||||
|
||||
return c.json({
|
||||
success: true,
|
||||
message: "Subscription will cancel at period end",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error canceling subscription:", error);
|
||||
return c.json(
|
||||
{ error: error instanceof Error ? error.message : "Failed to cancel subscription" },
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Reactivate a canceled subscription
|
||||
* POST /api/v1/stripe/reactivate-subscription
|
||||
*/
|
||||
stripeRouter.post("/reactivate-subscription", regularUserCheckMiddleware, async (c) => {
|
||||
const user = c.get("user");
|
||||
const supabase = c.get("supabase");
|
||||
|
||||
try {
|
||||
// Get user's Stripe customer first
|
||||
const { data: customers } = await supabase
|
||||
.schema("stripe")
|
||||
.from("customers")
|
||||
.select("id, metadata");
|
||||
|
||||
const customer = customers?.find((c: Stripe.Customer) => c.metadata?.user_id === user.id);
|
||||
|
||||
if (!customer) {
|
||||
return c.json({ error: "No subscription found to reactivate" }, 404);
|
||||
}
|
||||
|
||||
// Get user's subscription that's set to cancel
|
||||
const { data: subscription } = await supabase
|
||||
.schema("stripe")
|
||||
.from("subscriptions")
|
||||
.select("id, cancel_at_period_end")
|
||||
.eq("customer", customer.id)
|
||||
.eq("cancel_at_period_end", true)
|
||||
.maybeSingle();
|
||||
|
||||
if (!subscription) {
|
||||
return c.json({ error: "No subscription found to reactivate" }, 404);
|
||||
}
|
||||
|
||||
// Reactivate subscription in Stripe
|
||||
// The webhook will automatically sync the change to our database
|
||||
await stripe.subscriptions.update(subscription.id, {
|
||||
cancel_at_period_end: false,
|
||||
});
|
||||
|
||||
return c.json({ success: true, message: "Subscription reactivated" });
|
||||
} catch (error) {
|
||||
console.error("Error reactivating subscription:", error);
|
||||
return c.json(
|
||||
{ error: error instanceof Error ? error.message : "Failed to reactivate subscription" },
|
||||
500
|
||||
);
|
||||
}
|
||||
});
|
||||
19
api/src/stripeSync.ts
Normal file
19
api/src/stripeSync.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { StripeSync } from "@supabase/stripe-sync-engine";
|
||||
import { config } from "./config.js";
|
||||
|
||||
const ssl = {
|
||||
ca: Buffer.from(config.SUPABASE_CA_CERT, "base64").toString("utf-8"),
|
||||
};
|
||||
|
||||
export const stripeSync = new StripeSync({
|
||||
stripeSecretKey: config.STRIPE_SECRET_KEY || "",
|
||||
stripeWebhookSecret: config.STRIPE_WEBHOOK_SECRET || "",
|
||||
schema: "stripe", // Use stripe schema (library default)
|
||||
poolConfig: {
|
||||
connectionString: config.SUPABASE_CONNECTION_STRING || "", // Direct Postgres connection string
|
||||
ssl,
|
||||
max: 10,
|
||||
},
|
||||
// Optional: force refetch from Stripe API to avoid stale data
|
||||
revalidateObjectsViaStripeApi: ["subscription", "customer"],
|
||||
});
|
||||
|
|
@ -5,6 +5,7 @@ import { config } from "./config.js";
|
|||
import { writeCalendarFileToR2 } from "./helpers.js";
|
||||
import { streamChatMiddleware } from "./middleware.js";
|
||||
import type { StreamChat } from "stream-chat";
|
||||
import { stripeSync } from "./stripeSync.js";
|
||||
|
||||
export const taskRouter = new Hono<{
|
||||
Variables: { supabase: SupabaseClient };
|
||||
|
|
@ -92,3 +93,13 @@ taskRouter.post(
|
|||
return c.json({ message: `Synced ${tablosData.length} tablo names` });
|
||||
}
|
||||
);
|
||||
|
||||
taskRouter.post("/sync-stripe-subscriptions", async (c) => {
|
||||
if (c.req.header("Authorization") !== `Basic ${config.TASKS_SECRET}`) {
|
||||
return c.json({ error: "Unauthorized" }, 401);
|
||||
}
|
||||
|
||||
const data = await stripeSync.syncBackfill({ object: "all" });
|
||||
|
||||
return c.json({ message: `Synced ${data.subscriptions?.synced} stripe subscriptions` });
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue