From f338db72f02daeaec88a3c8263f0586ba5dbce5d Mon Sep 17 00:00:00 2001 From: Jasdeep Singh Date: Wed, 31 Jul 2024 11:05:02 +0530 Subject: [PATCH] feat: Synchronising stripe and studio API state - [DEV-4157] (#561) * chore: Add dev mode to run api in local without building Signed-off-by: jay-dee7 * chore: Add logging info for app booting Signed-off-by: jay-dee7 * fix: Always perform update operations on active or trialing subscriptions Signed-off-by: jay-dee7 * feat: Create studio customer on Stripe `customer.subscription.created` event Signed-off-by: jay-dee7 * feat: Expose Stripe checkout session retrieval API Signed-off-by: jay-dee7 * fix: Make Stripe checkout session endpoint authenticated Signed-off-by: jay-dee7 * fix: Use `type` import and remove .node-version Signed-off-by: jay-dee7 --------- Signed-off-by: jay-dee7 --- .gitignore | 4 + package-lock.json | 479 +++++++++++++++++- package.json | 2 + src/app.ts | 17 +- src/controllers/admin/subscriptions.ts | 59 ++- src/controllers/api/account.ts | 42 +- src/database/entities/customer.entity.ts | 5 +- src/index.ts | 7 +- src/middleware/auth/logto-helper.ts | 4 + .../auth/routes/admin/admin-auth.ts | 1 + src/middleware/authentication.ts | 2 +- src/services/api/customer.ts | 4 +- src/services/track/admin/account-submitter.ts | 6 +- .../track/admin/subscription-submitter.ts | 56 +- src/static/swagger-admin.json | 60 ++- src/types/admin.ts | 5 + src/types/swagger-admin-types.ts | 13 +- 17 files changed, 715 insertions(+), 51 deletions(-) diff --git a/.gitignore b/.gitignore index 0097c3552..1857e62a0 100644 --- a/.gitignore +++ b/.gitignore @@ -440,6 +440,8 @@ venv/ ENV/ env.bak/ venv.bak/ +*.crt +*.priv # Spyder project settings .spyderproject @@ -471,3 +473,5 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +*.tar diff --git a/package-lock.json b/package-lock.json index 2a59f9e6d..e466628e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cheqd/studio", - "version": "3.0.0-develop.2", + "version": "3.0.1-develop.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cheqd/studio", - "version": "3.0.0-develop.2", + "version": "3.0.1-develop.2", "license": "Apache-2.0", "dependencies": { "@cheqd/did-provider-cheqd": "^4.1.1", @@ -100,6 +100,7 @@ "ts-jest": "^29.1.5", "ts-loader": "^9.5.1", "ts-node": "^10.9.2", + "tsx": "^4.16.2", "typescript": "^5.5.2", "uint8arrays": "^5.1.0" }, @@ -3291,6 +3292,397 @@ "node": ">=12" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -17498,6 +17890,45 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -19095,6 +19526,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.6.tgz", + "integrity": "sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/getenv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/getenv/-/getenv-1.0.0.tgz", @@ -30436,6 +30880,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve.exports": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", @@ -33139,6 +33593,7 @@ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "devOptional": true, + "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -33209,6 +33664,26 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/tsx": { + "version": "4.16.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.16.2.tgz", + "integrity": "sha512-C1uWweJDgdtX2x600HjaFaucXTilT7tgUZHbOE4+ypskZ1OP8CRCSDkCxG6Vya9EwaFIVagWwpaVAn5wzypaqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.21.5", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/package.json b/package.json index a89f53b0d..ce4263e0a 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ } }, "scripts": { + "dev": "tsx watch --clear-screen=false src/index.ts", "build": "npm run build:swagger && npm run build:app", "build:app": "tsc", "build:swagger-api": "swagger-jsdoc --definition src/static/swagger-api-options.json -o src/static/swagger-api.json ./src/controllers/api/*.ts ./src/types/swagger-api-types.ts", @@ -143,6 +144,7 @@ "ts-jest": "^29.1.5", "ts-loader": "^9.5.1", "ts-node": "^10.9.2", + "tsx": "^4.16.2", "typescript": "^5.5.2", "uint8arrays": "^5.1.0" }, diff --git a/src/app.ts b/src/app.ts index f8abb6dc7..361a19a63 100644 --- a/src/app.ts +++ b/src/app.ts @@ -6,7 +6,6 @@ import cookieParser from 'cookie-parser'; import path from 'path'; import swaggerUi from 'swagger-ui-express'; import { StatusCodes } from 'http-status-codes'; - import { CredentialController } from './controllers/api/credential.js'; import { AccountController } from './controllers/api/account.js'; import { Authentication } from './middleware/authentication.js'; @@ -15,10 +14,7 @@ import { CredentialStatusController } from './controllers/api/credential-status. import { CORS_ALLOWED_ORIGINS, CORS_ERROR_MSG } from './types/constants.js'; import { LogToWebHook } from './middleware/hook.js'; import { Middleware } from './middleware/middleware.js'; - import * as dotenv from 'dotenv'; -dotenv.config(); - // Define Swagger file import swaggerAPIDocument from './static/swagger-api.json' assert { type: 'json' }; import swaggerAdminDocument from './static/swagger-admin.json' assert { type: 'json' }; @@ -34,6 +30,8 @@ import { WebhookController } from './controllers/admin/webhook.js'; import { APIKeyController } from './controllers/admin/api-key.js'; import { OrganisationController } from './controllers/admin/organisation.js'; +dotenv.config(); + class App { public express: express.Application; @@ -41,7 +39,14 @@ class App { this.express = express(); this.middleware(); this.routes(); - Connection.instance.connect(); + Connection.instance + .connect() + .then(() => { + console.log('Database connection: successful'); + }) + .catch((err) => { + console.log('DBConnectorError: ', err); + }); } private middleware() { @@ -263,6 +268,8 @@ class App { new SubscriptionController().resume ); + app.get('/admin/checkout/session/:id', new SubscriptionController().getCheckoutSession); + // API key app.post('/admin/api-key/create', APIKeyController.apiKeyCreateValidator, new APIKeyController().create); app.post('/admin/api-key/update', APIKeyController.apiKeyUpdateValidator, new APIKeyController().update); diff --git a/src/controllers/admin/subscriptions.ts b/src/controllers/admin/subscriptions.ts index 883bcfa69..e91cd6eaa 100644 --- a/src/controllers/admin/subscriptions.ts +++ b/src/controllers/admin/subscriptions.ts @@ -18,6 +18,8 @@ import type { SubscriptionResumeResponseBody, SubscriptionResumeRequestBody, SubscriptionCancelRequestBody, + CheckoutSessionGetUnsuccessfulResponseBody, + CheckoutSessionGetResponseBody, } from '../../types/admin.js'; import { StatusCodes } from 'http-status-codes'; import { check } from '../validator/index.js'; @@ -260,7 +262,7 @@ export class SubscriptionController { const { returnURL, isManagePlan, priceId } = request.body satisfies SubscriptionUpdateRequestBody; try { // Get the subscription object from the DB - const subscription = await SubscriptionService.instance.findOne({ customer: response.locals.customer }); + const subscription = await SubscriptionService.instance.findCurrent(response.locals.customer); if (!subscription) { return response.status(StatusCodes.NOT_FOUND).json({ error: `Subscription was not found`, @@ -539,4 +541,59 @@ export class SubscriptionController { } satisfies SubscriptionResumeUnsuccessfulResponseBody); } } + + /** + * @openapi + * + * /admin/checkout/session/{id}: + * get: + * summary: Get a Stripe checkout session + * description: Retrieves a Stripe checkout session by id + * tags: [Subscription] + * parameters: + * - in: path + * name: id + * schema: + * type: string + * description: The session id which identifies a unique checkout session in Stripe + * required: true + * responses: + * 200: + * description: A Stripe checkout session record + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CheckoutSessionGetResponseBody' + * 400: + * $ref: '#/components/schemas/InvalidRequest' + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * $ref: '#/components/schemas/InternalError' + * 404: + * $ref: '#/components/schemas/NotFoundError' + */ + // @validate + async getCheckoutSession(request: Request, response: Response) { + const stripe = response.locals.stripe as Stripe; + const checkoutSessionId = request.params.id; + try { + // retrieve the checkout session + const checkoutSession = await stripe.checkout.sessions.retrieve(checkoutSessionId); + + // Check if the subscription was resumed + if (checkoutSession.lastResponse?.statusCode !== StatusCodes.OK) { + return response.status(StatusCodes.BAD_GATEWAY).json({ + error: `Checkout session not found`, + } satisfies CheckoutSessionGetUnsuccessfulResponseBody); + } + return response.status(StatusCodes.OK).json({ + session: checkoutSession, + } satisfies CheckoutSessionGetResponseBody); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies SubscriptionResumeUnsuccessfulResponseBody); + } + } } diff --git a/src/controllers/api/account.ts b/src/controllers/api/account.ts index 34ff3c7ee..c1200adcb 100644 --- a/src/controllers/api/account.ts +++ b/src/controllers/api/account.ts @@ -10,7 +10,6 @@ import { UserService } from '../../services/api/user.js'; import { RoleService } from '../../services/api/role.js'; import { PaymentAccountService } from '../../services/api/payment-account.js'; import type { CustomerEntity } from '../../database/entities/customer.entity.js'; -import type { UserEntity } from '../../database/entities/user.entity.js'; import type { PaymentAccountEntity } from '../../database/entities/payment.account.entity.js'; import { IdentityServiceStrategySetup } from '../../services/identity/index.js'; import type { @@ -163,8 +162,6 @@ export class AccountController { // 6.1 If custom_data is empty - create it // 7. Check the token balance for Testnet account - let customer: CustomerEntity | null; - let user: UserEntity | null; let paymentAccount: PaymentAccountEntity | null; // 1. Get logTo UserId from request body @@ -175,7 +172,8 @@ export class AccountController { } const logToUserId = request.body.user.id; const logToUserEmail = request.body.user.primaryEmail; - const logToName = request.body.user.name || logToUserEmail; // use email as name, because "name" is unique in the current db setup. + // use email as name, because "name" is unique in the current db setup. + const logToName = request.body.user.name || logToUserEmail; const defaultRole = await RoleService.instance.getDefaultRole(); if (!defaultRole) { @@ -184,7 +182,11 @@ export class AccountController { } satisfies UnsuccessfulResponseBody); } // 2. Check if such row exists in the DB - user = await UserService.instance.get(logToUserId); + let [user, [customer]] = await Promise.all([ + UserService.instance.get(logToUserId), + CustomerService.instance.find({ email: logToUserEmail }), + ]); + if (!user) { // 2.1. If no - create customer first // Cause for now we assume only 1-1 connection between user and customer @@ -194,19 +196,24 @@ export class AccountController { // 2.1.1. Create customer // I’m setting the "name" field to an empty string on the current CustomerEntity because it is non-nullable. // we will populate the customer's "name" field using the response from the Stripe account creation in account-submitter.ts. - customer = (await CustomerService.instance.create(logToName, logToUserEmail)) as CustomerEntity; if (!customer) { - return response.status(StatusCodes.BAD_REQUEST).json({ - error: 'User is not found in database: Customer was not created', - } satisfies UnsuccessfulResponseBody); + customer = (await CustomerService.instance.create(logToName, logToUserEmail)) as CustomerEntity; + if (!customer) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: 'User is not found in database: Customer was not created', + } satisfies UnsuccessfulResponseBody); + } + // Notify + await eventTracker.notify({ + message: EventTracker.compileBasicNotification( + 'User was not found in database: Customer with customerId: ' + + customer.customerId + + ' was created' + ), + severity: 'info', + }); } - // Notify - await eventTracker.notify({ - message: EventTracker.compileBasicNotification( - 'User was not found in database: Customer with customerId: ' + customer.customerId + ' was created' - ), - severity: 'info', - }); + // 2.2. Create user user = await UserService.instance.create(logToUserId, customer, defaultRole); if (!user) { @@ -222,6 +229,7 @@ export class AccountController { severity: 'info', }); } + // 3. If yes - check that there is customer associated with such user if (!user.customer) { // 3.1. If no: @@ -488,7 +496,7 @@ export class AccountController { } } // 5. Setup stripe account - if (process.env.STRIPE_ENABLED === 'true' && customer.paymentProviderId === null) { + if (process.env.STRIPE_ENABLED === 'true' && !customer.paymentProviderId) { eventTracker.submit({ operation: OperationNameEnum.STRIPE_ACCOUNT_CREATE, data: { diff --git a/src/database/entities/customer.entity.ts b/src/database/entities/customer.entity.ts index fd0a6ba7c..4c6d705d2 100644 --- a/src/database/entities/customer.entity.ts +++ b/src/database/entities/customer.entity.ts @@ -54,11 +54,14 @@ export class CustomerEntity { this.updatedAt = new Date(); } - constructor(customerId: string, name: string, email?: string, description?: string) { + constructor(customerId: string, name: string, email?: string, description?: string, paymentProviderId?: string) { this.customerId = customerId; this.name = name; this.email = email; this.description = description; + if (paymentProviderId) { + this.paymentProviderId = paymentProviderId; + } } public isEqual(customer: CustomerEntity): boolean { diff --git a/src/index.ts b/src/index.ts index 229382a90..ff87545ec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ import * as http from 'http'; import App from './app.js'; - import * as dotenv from 'dotenv'; + dotenv.config(); const port = process.env.PORT || 3000; @@ -9,6 +9,11 @@ App.set('port', port); const server = http.createServer(App); server.listen(port); + +server.on('listening', () => { + console.log('Listening on port:', port); +}); + server.on('error', onError); function onError(error: NodeJS.ErrnoException): void { diff --git a/src/middleware/auth/logto-helper.ts b/src/middleware/auth/logto-helper.ts index cf13b439d..1ce511384 100644 --- a/src/middleware/auth/logto-helper.ts +++ b/src/middleware/auth/logto-helper.ts @@ -185,6 +185,7 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { `Looks like resource with id ${process.env.LOGTO_DEFAULT_RESOURCE_URL} is not placed on LogTo` ); } + private async setAllScopes(): Promise { const allResources = await this.getAllResources(); if (allResources.status !== StatusCodes.OK) { @@ -204,6 +205,7 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { } return this.returnOk({}); } + private async getScopesForRole(roleId: string): Promise { const uri = new URL(`/api/roles/${roleId}/scopes`, process.env.LOGTO_ENDPOINT); const scopes = []; @@ -224,6 +226,7 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { return this.returnError(StatusCodes.BAD_GATEWAY, `askRoleForScopes ${err}`); } } + private async getScopesForResource(resourceId: string): Promise { const uri = new URL(`/api/resources/${resourceId}/scopes`, process.env.LOGTO_ENDPOINT); const scopes = []; @@ -244,6 +247,7 @@ export class LogToHelper extends OAuthProvider implements IOAuthProvider { return this.returnError(StatusCodes.BAD_GATEWAY, `askResourceForScopes ${err}`); } } + public async getScopesForRolesList(roles: string[]): Promise { const scopes = []; for (const role of roles) { diff --git a/src/middleware/auth/routes/admin/admin-auth.ts b/src/middleware/auth/routes/admin/admin-auth.ts index cebd79ff8..7b51b6411 100644 --- a/src/middleware/auth/routes/admin/admin-auth.ts +++ b/src/middleware/auth/routes/admin/admin-auth.ts @@ -26,6 +26,7 @@ export class AdminAuthRuleProvider extends AuthRuleProvider { skipNamespace: true, }); this.registerRule('/admin/subscription/get', 'GET', 'admin:subscription:get', { skipNamespace: true }); + this.registerRule('/admin/checkout/session/(.*)', 'GET', '', { skipNamespace: true }); // Prices this.registerRule('/admin/price/list', 'GET', 'admin:price:list', { skipNamespace: true }); diff --git a/src/middleware/authentication.ts b/src/middleware/authentication.ts index e21302092..c3fba2b49 100644 --- a/src/middleware/authentication.ts +++ b/src/middleware/authentication.ts @@ -90,7 +90,7 @@ export class Authentication { } public async wrapperHandleAuthRoutes(request: Request, response: Response, next: NextFunction) { - const resources = await this.logToHelper.getAllResourcesWithNames(); + const resources = this.logToHelper.getAllResourcesWithNames(); return handleAuthRoutes({ ...configLogToExpress, scopes: ['roles'], resources: resources as string[] })( request, response, diff --git a/src/services/api/customer.ts b/src/services/api/customer.ts index 08a413395..4531ceab5 100644 --- a/src/services/api/customer.ts +++ b/src/services/api/customer.ts @@ -22,7 +22,7 @@ export class CustomerService { this.customerRepository = Connection.instance.dbConnection.getRepository(CustomerEntity); } - public async create(name: string, email?: string, description?: string) { + public async create(name: string, email?: string, description?: string, paymentProviderId?: string) { // The sequence for creating a customer is supposed to be: // 1. Create a new customer entity in the database; // 2. Create new cosmos keypair @@ -32,7 +32,7 @@ export class CustomerService { if (await this.isExist({ name: name })) { throw new Error(`Cannot create a new customer since the customer with same name ${name} already exists`); } - const customerEntity = new CustomerEntity(uuidv4(), name, email, description); + const customerEntity = new CustomerEntity(uuidv4(), name, email, description, paymentProviderId); await this.customerRepository.insert(customerEntity); // Create a new Cosmos account for the customer and make a link with customer entity; diff --git a/src/services/track/admin/account-submitter.ts b/src/services/track/admin/account-submitter.ts index f6f1a3855..a75e8bcce 100644 --- a/src/services/track/admin/account-submitter.ts +++ b/src/services/track/admin/account-submitter.ts @@ -35,7 +35,7 @@ export class PortalAccountCreateSubmitter implements IObserver { email: data.email, }); if (account.lastResponse.statusCode !== StatusCodes.OK) { - await this.notify({ + this.notify({ message: EventTracker.compileBasicNotification( `Failed to create Stripe account with name: ${data.name}.`, operation.operation @@ -51,7 +51,7 @@ export class PortalAccountCreateSubmitter implements IObserver { name: data.name, paymentProviderId: account.id, }); - await this.notify({ + this.notify({ message: EventTracker.compileBasicNotification( `Stripe account created with name: ${data.name}.`, operation.operation @@ -59,7 +59,7 @@ export class PortalAccountCreateSubmitter implements IObserver { severity: 'info', } as INotifyMessage); } catch (error) { - await this.notify({ + this.notify({ message: EventTracker.compileBasicNotification( `Failed to create Stripe account with name: ${data.name as string}. Error: ${error}`, operation.operation diff --git a/src/services/track/admin/subscription-submitter.ts b/src/services/track/admin/subscription-submitter.ts index bccafb2be..7bd242714 100644 --- a/src/services/track/admin/subscription-submitter.ts +++ b/src/services/track/admin/subscription-submitter.ts @@ -1,3 +1,4 @@ +import Stripe from 'stripe'; import type { CustomerEntity } from '../../../database/entities/customer.entity.js'; import { OperationNameEnum } from '../../../types/constants.js'; import type { INotifyMessage } from '../../../types/track.js'; @@ -6,6 +7,7 @@ import { CustomerService } from '../../api/customer.js'; import type { ISubmitOperation, ISubmitSubscriptionData } from '../submitter.js'; import { EventTracker } from '../tracker.js'; import type { IObserver } from '../types.js'; +import type { FindOptionsWhere } from 'typeorm'; export class SubscriptionSubmitter implements IObserver { private emitter: EventEmitter; @@ -35,16 +37,46 @@ export class SubscriptionSubmitter implements IObserver { } async submitSubscriptionCreate(operation: ISubmitOperation): Promise { + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); const data = operation.data as ISubmitSubscriptionData; let customer: CustomerEntity | undefined = operation.options?.customer; try { if (!customer) { - const customers = await CustomerService.instance.find({ - paymentProviderId: data.paymentProviderId, + const stripeCustomer = await stripe.customers.retrieve(data.paymentProviderId); + + const whereClause: FindOptionsWhere[] = [{ paymentProviderId: data.paymentProviderId }]; + // we add an additional "OR" check in case that a customer was created locally with email and no paymentProviderId + if (!stripeCustomer.deleted && stripeCustomer.email) { + whereClause.push({ email: stripeCustomer.email }); + } + + const customers = await CustomerService.instance.customerRepository.find({ + where: whereClause, }); + if (customers.length === 0) { + this.notify({ + message: EventTracker.compileBasicNotification( + `Customer not found for Cheqd Studio, creating new customer record with paymentProviderId: ${data.paymentProviderId}`, + operation.operation + ), + severity: 'info', + }); + + if (!stripeCustomer.deleted && stripeCustomer.email) { + const customerName = stripeCustomer.name ?? stripeCustomer.email; + const customer = await CustomerService.instance.create( + customerName, + stripeCustomer.email, + undefined, + data.paymentProviderId + ); + customers.push(customer as CustomerEntity); + } + } + if (customers.length !== 1) { - await this.notify({ + this.notify({ message: EventTracker.compileBasicNotification( `Only one Stripe account should be associated with CaaS customer. Stripe accountId: ${data.paymentProviderId}.`, operation.operation @@ -65,7 +97,7 @@ export class SubscriptionSubmitter implements IObserver { data.trialEnd as Date ); if (!subscription) { - await this.notify({ + this.notify({ message: EventTracker.compileBasicNotification( `Failed to create a new subscription with id: ${data.subscriptionId}.`, operation.operation @@ -74,7 +106,7 @@ export class SubscriptionSubmitter implements IObserver { }); } - await this.notify({ + this.notify({ message: EventTracker.compileBasicNotification( `Subscription created with id: ${data.subscriptionId}.`, operation.operation @@ -82,7 +114,7 @@ export class SubscriptionSubmitter implements IObserver { severity: 'info', }); } catch (error) { - await this.notify({ + this.notify({ message: EventTracker.compileBasicNotification( `Failed to create a new subscription with id: ${data.subscriptionId} because of error: ${(error as Error)?.message || error}`, operation.operation @@ -104,7 +136,7 @@ export class SubscriptionSubmitter implements IObserver { data.trialEnd as Date ); if (!subscription) { - await this.notify({ + this.notify({ message: EventTracker.compileBasicNotification( `Failed to update subscription with id: ${data.subscriptionId}.`, operation.operation @@ -113,7 +145,7 @@ export class SubscriptionSubmitter implements IObserver { }); } - await this.notify({ + this.notify({ message: EventTracker.compileBasicNotification( `Subscription updated with id: ${data.subscriptionId}.`, operation.operation @@ -121,7 +153,7 @@ export class SubscriptionSubmitter implements IObserver { severity: 'info', }); } catch (error) { - await this.notify({ + this.notify({ message: EventTracker.compileBasicNotification( `Failed to update subscription with id: ${data.subscriptionId} because of error: ${(error as Error)?.message || error}`, operation.operation @@ -136,7 +168,7 @@ export class SubscriptionSubmitter implements IObserver { try { const subscription = await SubscriptionService.instance.update(data.subscriptionId, data.status); if (!subscription) { - await this.notify({ + this.notify({ message: EventTracker.compileBasicNotification( `Failed to cancel subscription with id: ${data.subscriptionId}.`, operation.operation @@ -145,7 +177,7 @@ export class SubscriptionSubmitter implements IObserver { }); } - await this.notify({ + this.notify({ message: EventTracker.compileBasicNotification( `Subscription canceled with id: ${data.subscriptionId}.`, operation.operation @@ -153,7 +185,7 @@ export class SubscriptionSubmitter implements IObserver { severity: 'info', }); } catch (error) { - await this.notify({ + this.notify({ message: EventTracker.compileBasicNotification( `Failed to cancel subscription with id: ${data.subscriptionId} because of error: ${(error as Error)?.message || error}`, operation.operation diff --git a/src/static/swagger-admin.json b/src/static/swagger-admin.json index 4cacbfa9a..8827ad87e 100644 --- a/src/static/swagger-admin.json +++ b/src/static/swagger-admin.json @@ -214,7 +214,7 @@ "type": "array", "items": { "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" } } } @@ -236,7 +236,7 @@ "properties": { "subscription": { "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" }, "idempotencyKey": { "type": "string", @@ -267,7 +267,17 @@ "properties": { "subscription": { "type": "object", - "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object]" + "description": "A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object)" + } + } + }, + "CheckoutSessionGetResponseBody": { + "description": "the response body for GET Stripe Checkout Session API", + "type": "object", + "properties": { + "session": { + "type": "object", + "description": "A Stripe checkout session object. For more information, see the [Stripe API documentation](https://docs.stripe.com/api/checkout/sessions/object)" } } }, @@ -1200,6 +1210,50 @@ } } } + }, + "/admin/checkout/session/{id}": { + "get": { + "summary": "Get a Stripe checkout session", + "description": "Retrieves a Stripe checkout session by id", + "tags": [ + "Subscription" + ], + "parameters": [ + { + "in": "path", + "name": "id", + "schema": { + "type": "string", + "description": "The session id which identifies a unique checkout session in Stripe" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "A Stripe checkout session record", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CheckoutSessionGetResponseBody" + } + } + } + }, + "400": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "$ref": "#/components/schemas/NotFoundError" + }, + "500": { + "$ref": "#/components/schemas/InternalError" + } + } + } } } } \ No newline at end of file diff --git a/src/types/admin.ts b/src/types/admin.ts index 6f96ce7fc..beb676169 100644 --- a/src/types/admin.ts +++ b/src/types/admin.ts @@ -88,12 +88,17 @@ export type SubscriptionResumeResponseBody = { subscription: Stripe.Response; }; +export type CheckoutSessionGetResponseBody = { + session: Stripe.Response; +}; + export type SubscriptionCreateUnsuccessfulResponseBody = UnsuccessfulResponseBody; export type SubscriptionListUnsuccessfulResponseBody = UnsuccessfulResponseBody; export type SubscriptionGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; export type SubscriptionUpdateUnsuccessfulResponseBody = UnsuccessfulResponseBody; export type SubscriptionCancelUnsuccessfulResponseBody = UnsuccessfulResponseBody; export type SubscriptionResumeUnsuccessfulResponseBody = UnsuccessfulResponseBody; +export type CheckoutSessionGetUnsuccessfulResponseBody = UnsuccessfulResponseBody; // Customer // Get diff --git a/src/types/swagger-admin-types.ts b/src/types/swagger-admin-types.ts index 7db0f8b0d..61deec71d 100644 --- a/src/types/swagger-admin-types.ts +++ b/src/types/swagger-admin-types.ts @@ -132,7 +132,7 @@ * type: array * items: * type: object - * description: A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object] + * description: A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object) * SubscriptionCancelRequestBody: * description: The request body for canceling a subscription * type: object @@ -147,7 +147,7 @@ * properties: * subscription: * type: object - * description: A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object] + * description: A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object) * idempotencyKey: * type: string * description: The idempotency key. It helps to prevent duplicate requests. In case if there was a request with the same idempotency key, the response will be the same as for the first request. @@ -170,7 +170,14 @@ * properties: * subscription: * type: object - * description: A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object] + * description: A subscription object from Stripe. For more information see the [Stripe API documentation](https://docs.stripe.com/api/subscriptions/object) + * CheckoutSessionGetResponseBody: + * description: the response body for GET Stripe Checkout Session API + * type: object + * properties: + * session: + * type: object + * description: A Stripe checkout session object. For more information, see the [Stripe API documentation](https://docs.stripe.com/api/checkout/sessions/object) * APIKeyResponse: * description: The general view for API key in response * type: object