First pass at implementing a stripe-powered billing service
This commit is contained in:
parent
72e5ac8926
commit
adbaafe26a
|
@ -2,6 +2,7 @@ node_modules
|
|||
packages/*/node_modules
|
||||
packages/*/docs
|
||||
packages/core/lib
|
||||
packages/billing/lib
|
||||
packages/app/dist
|
||||
packages/app/build
|
||||
packages/app/test/dist
|
||||
|
|
|
@ -66,16 +66,16 @@
|
|||
}
|
||||
},
|
||||
"@lerna/changed": {
|
||||
"version": "3.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@lerna/changed/-/changed-3.13.3.tgz",
|
||||
"integrity": "sha512-REMZ/1UvYrizUhN7ktlbfMUa0vhMf1ogAe97WQC4I8r3s973Orfhs3aselo1GwudUwM4tMHBH8A9vnll9or3iA==",
|
||||
"version": "3.13.4",
|
||||
"resolved": "https://registry.npmjs.org/@lerna/changed/-/changed-3.13.4.tgz",
|
||||
"integrity": "sha512-9lfOyRVObasw6L/z7yCSfsEl1QKy0Eamb8t2Krg1deIoAt+cE3JXOdGGC1MhOSli+7f/U9LyLXjJzIOs/pc9fw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@lerna/collect-updates": "3.13.3",
|
||||
"@lerna/command": "3.13.3",
|
||||
"@lerna/listable": "3.13.0",
|
||||
"@lerna/output": "3.13.0",
|
||||
"@lerna/version": "3.13.3"
|
||||
"@lerna/version": "3.13.4"
|
||||
}
|
||||
},
|
||||
"@lerna/check-working-tree": {
|
||||
|
@ -346,9 +346,9 @@
|
|||
}
|
||||
},
|
||||
"@lerna/import": {
|
||||
"version": "3.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@lerna/import/-/import-3.13.3.tgz",
|
||||
"integrity": "sha512-gDjLAFVavG/CMvj9leBfiwd7vrXqtdFXPIz1oXmghBMnje7nCTbodbNWFe4VDDWx7reDaZIN+6PxTSvrPcF//A==",
|
||||
"version": "3.13.4",
|
||||
"resolved": "https://registry.npmjs.org/@lerna/import/-/import-3.13.4.tgz",
|
||||
"integrity": "sha512-dn6eNuPEljWsifBEzJ9B6NoaLwl/Zvof7PBUPA4hRyRlqG5sXRn6F9DnusMTovvSarbicmTURbOokYuotVWQQA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@lerna/child-process": "3.13.3",
|
||||
|
@ -581,9 +581,9 @@
|
|||
}
|
||||
},
|
||||
"@lerna/publish": {
|
||||
"version": "3.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@lerna/publish/-/publish-3.13.3.tgz",
|
||||
"integrity": "sha512-Ni3pZKueIfgJJoL0OXfbAuWhGlJrDNwGx3CYWp2dbNqJmKD6uBZmsDtmeARKDp92oUK60W0drXCMydkIFFHMDQ==",
|
||||
"version": "3.13.4",
|
||||
"resolved": "https://registry.npmjs.org/@lerna/publish/-/publish-3.13.4.tgz",
|
||||
"integrity": "sha512-v03pabiPlqCDwX6cVNis1PDdT6/jBgkVb5Nl4e8wcJXevIhZw3ClvtI94gSZu/wdoVFX0RMfc8QBVmaimSO0qg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@lerna/batch-packages": "3.13.0",
|
||||
|
@ -603,7 +603,7 @@
|
|||
"@lerna/run-lifecycle": "3.13.0",
|
||||
"@lerna/run-parallel-batches": "3.13.0",
|
||||
"@lerna/validation-error": "3.13.0",
|
||||
"@lerna/version": "3.13.3",
|
||||
"@lerna/version": "3.13.4",
|
||||
"figgy-pudding": "^3.5.1",
|
||||
"fs-extra": "^7.0.0",
|
||||
"libnpmaccess": "^3.0.1",
|
||||
|
@ -732,9 +732,9 @@
|
|||
}
|
||||
},
|
||||
"@lerna/version": {
|
||||
"version": "3.13.3",
|
||||
"resolved": "https://registry.npmjs.org/@lerna/version/-/version-3.13.3.tgz",
|
||||
"integrity": "sha512-o/yQGAwDHmyu17wTj4Kat1/uDhjYFMeG+H0Y0HC4zJ4a/T6rEiXx7jJrnucPTmTQTDcUBoH/It5LrPYGOPsExA==",
|
||||
"version": "3.13.4",
|
||||
"resolved": "https://registry.npmjs.org/@lerna/version/-/version-3.13.4.tgz",
|
||||
"integrity": "sha512-pptWUEgN/lUTQZu34+gfH1g4Uhs7TDKRcdZY9A4T9k6RTOwpKC2ceLGiXdeR+ZgQJAey2C4qiE8fo5Z6Rbc6QA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@lerna/batch-packages": "3.13.0",
|
||||
|
@ -788,15 +788,32 @@
|
|||
"dev": true
|
||||
},
|
||||
"@octokit/endpoint": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-4.0.0.tgz",
|
||||
"integrity": "sha512-b8sptNUekjREtCTJFpOfSIL4SKh65WaakcyxWzRcSPOk5RxkZJ/S8884NGZFxZ+jCB2rDURU66pSHn14cVgWVg==",
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-4.2.2.tgz",
|
||||
"integrity": "sha512-5IZjkUNhx5q0IRN7Juwf5A+Lu2qAso7ULST7C1P2mbGHePuCOk936Stcl/5GdJpB3ovD8M6/Lv3xra6Mn0IKNQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"deepmerge": "3.2.0",
|
||||
"is-plain-object": "^2.0.4",
|
||||
"is-plain-object": "^3.0.0",
|
||||
"universal-user-agent": "^2.0.1",
|
||||
"url-template": "^2.0.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"is-plain-object": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz",
|
||||
"integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"isobject": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"isobject": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz",
|
||||
"integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@octokit/plugin-enterprise-rest": {
|
||||
|
@ -806,26 +823,43 @@
|
|||
"dev": true
|
||||
},
|
||||
"@octokit/request": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-3.0.0.tgz",
|
||||
"integrity": "sha512-DZqmbm66tq+a9FtcKrn0sjrUpi0UaZ9QPUCxxyk/4CJ2rseTMpAWRf6gCwOSUCzZcx/4XVIsDk+kz5BVdaeenA==",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-3.0.1.tgz",
|
||||
"integrity": "sha512-aH61OVkMKMofGW/go2x4mJ44X4U/JF8xsiFFictwkZYtz0psE8OPKpsP2TZBZaJoCg2wmeTyEgqGfY+veg0hGQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@octokit/endpoint": "^4.0.0",
|
||||
"deprecation": "^1.0.1",
|
||||
"is-plain-object": "^2.0.4",
|
||||
"is-plain-object": "^3.0.0",
|
||||
"node-fetch": "^2.3.0",
|
||||
"once": "^1.4.0",
|
||||
"universal-user-agent": "^2.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"is-plain-object": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz",
|
||||
"integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"isobject": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"isobject": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz",
|
||||
"integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@octokit/rest": {
|
||||
"version": "16.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.25.0.tgz",
|
||||
"integrity": "sha512-QKIzP0gNYjyIGmY3Gpm3beof0WFwxFR+HhRZ+Wi0fYYhkEUvkJiXqKF56Pf5glzzfhEwOrggfluEld5F/ZxsKw==",
|
||||
"version": "16.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-16.25.1.tgz",
|
||||
"integrity": "sha512-a1Byzjj07OMQNUQDP5Ng/rChaI7aq6TNMY1ZFf8+zCVEEtYzCgcmrFG9BDerFbLPPKGQ5TAeRRFyLujUUN1HIg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@octokit/request": "3.0.0",
|
||||
"@octokit/request": "3.0.1",
|
||||
"atob-lite": "^2.0.0",
|
||||
"before-after-hook": "^1.4.0",
|
||||
"btoa-lite": "^1.0.0",
|
||||
|
@ -1559,13 +1593,13 @@
|
|||
}
|
||||
},
|
||||
"conventional-changelog-core": {
|
||||
"version": "3.1.6",
|
||||
"resolved": "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-3.1.6.tgz",
|
||||
"integrity": "sha512-5teTAZOtJ4HLR6384h50nPAaKdDr+IaU0rnD2Gg2C3MS7hKsEPH8pZxrDNqam9eOSPQg9tET6uZY79zzgSz+ig==",
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-3.2.2.tgz",
|
||||
"integrity": "sha512-cssjAKajxaOX5LNAJLB+UOcoWjAIBvXtDMedv/58G+YEmAXMNfC16mmPl0JDOuVJVfIqM0nqQiZ8UCm8IXbE0g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"conventional-changelog-writer": "^4.0.3",
|
||||
"conventional-commits-parser": "^3.0.1",
|
||||
"conventional-changelog-writer": "^4.0.5",
|
||||
"conventional-commits-parser": "^3.0.2",
|
||||
"dateformat": "^3.0.0",
|
||||
"get-pkg-repo": "^1.0.0",
|
||||
"git-raw-commits": "2.0.0",
|
||||
|
@ -1576,7 +1610,18 @@
|
|||
"q": "^1.5.1",
|
||||
"read-pkg": "^3.0.0",
|
||||
"read-pkg-up": "^3.0.0",
|
||||
"through2": "^2.0.0"
|
||||
"through2": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"through2": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz",
|
||||
"integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"readable-stream": "2 || 3"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"conventional-changelog-preset-loader": {
|
||||
|
@ -1586,13 +1631,13 @@
|
|||
"dev": true
|
||||
},
|
||||
"conventional-changelog-writer": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.0.3.tgz",
|
||||
"integrity": "sha512-bIlpSiQtQZ1+nDVHEEh798Erj2jhN/wEjyw9sfxY9es6h7pREE5BNJjfv0hXGH/FTrAsEpHUq4xzK99eePpwuA==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.0.5.tgz",
|
||||
"integrity": "sha512-g/Myp4MaJ1A+f7Ai+SnVhkcWtaHk6flw0SYN7A+vQ+MTu0+gSovQWs4Pg4NtcNUcIztYQ9YHsoxHP+GGQplI7Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"compare-func": "^1.3.1",
|
||||
"conventional-commits-filter": "^2.0.1",
|
||||
"conventional-commits-filter": "^2.0.2",
|
||||
"dateformat": "^3.0.0",
|
||||
"handlebars": "^4.1.0",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
|
@ -1600,23 +1645,34 @@
|
|||
"meow": "^4.0.0",
|
||||
"semver": "^5.5.0",
|
||||
"split": "^1.0.0",
|
||||
"through2": "^2.0.0"
|
||||
"through2": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"through2": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz",
|
||||
"integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"readable-stream": "2 || 3"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"conventional-commits-filter": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.1.tgz",
|
||||
"integrity": "sha512-92OU8pz/977udhBjgPEbg3sbYzIxMDFTlQT97w7KdhR9igNqdJvy8smmedAAgn4tPiqseFloKkrVfbXCVd+E7A==",
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.2.tgz",
|
||||
"integrity": "sha512-WpGKsMeXfs21m1zIw4s9H5sys2+9JccTzpN6toXtxhpw2VNF2JUXwIakthKBy+LN4DvJm+TzWhxOMWOs1OFCFQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-subset": "^0.1.1",
|
||||
"lodash.ismatch": "^4.4.0",
|
||||
"modify-values": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"conventional-commits-parser": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.0.1.tgz",
|
||||
"integrity": "sha512-P6U5UOvDeidUJ8ebHVDIoXzI7gMlQ1OF/id6oUvp8cnZvOXMt1n8nYl74Ey9YMn0uVQtxmCtjPQawpsssBWtGg==",
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.0.2.tgz",
|
||||
"integrity": "sha512-y5eqgaKR0F6xsBNVSQ/5cI5qIF3MojddSUi1vKIggRkqUTbkqFKH9P5YX/AT1BVZp9DtSzBTIkvjyVLotLsVog==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"JSONStream": "^1.0.4",
|
||||
|
@ -1624,8 +1680,19 @@
|
|||
"lodash": "^4.2.1",
|
||||
"meow": "^4.0.0",
|
||||
"split2": "^2.0.0",
|
||||
"through2": "^2.0.0",
|
||||
"through2": "^3.0.0",
|
||||
"trim-off-newlines": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"through2": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz",
|
||||
"integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"readable-stream": "2 || 3"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"conventional-recommended-bump": {
|
||||
|
@ -1656,31 +1723,6 @@
|
|||
"typedarray": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"conventional-commits-filter": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.2.tgz",
|
||||
"integrity": "sha512-WpGKsMeXfs21m1zIw4s9H5sys2+9JccTzpN6toXtxhpw2VNF2JUXwIakthKBy+LN4DvJm+TzWhxOMWOs1OFCFQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lodash.ismatch": "^4.4.0",
|
||||
"modify-values": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"conventional-commits-parser": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.0.2.tgz",
|
||||
"integrity": "sha512-y5eqgaKR0F6xsBNVSQ/5cI5qIF3MojddSUi1vKIggRkqUTbkqFKH9P5YX/AT1BVZp9DtSzBTIkvjyVLotLsVog==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"JSONStream": "^1.0.4",
|
||||
"is-text-path": "^1.0.0",
|
||||
"lodash": "^4.2.1",
|
||||
"meow": "^4.0.0",
|
||||
"split2": "^2.0.0",
|
||||
"through2": "^3.0.0",
|
||||
"trim-off-newlines": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.3.0.tgz",
|
||||
|
@ -1691,15 +1733,6 @@
|
|||
"string_decoder": "^1.1.1",
|
||||
"util-deprecate": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"through2": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz",
|
||||
"integrity": "sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"readable-stream": "2 || 3"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -3287,12 +3320,6 @@
|
|||
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
|
||||
"dev": true
|
||||
},
|
||||
"is-subset": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz",
|
||||
"integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=",
|
||||
"dev": true
|
||||
},
|
||||
"is-text-path": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz",
|
||||
|
@ -3427,26 +3454,26 @@
|
|||
}
|
||||
},
|
||||
"lerna": {
|
||||
"version": "3.13.3",
|
||||
"resolved": "https://registry.npmjs.org/lerna/-/lerna-3.13.3.tgz",
|
||||
"integrity": "sha512-0TkG40F02A4wjKraJBztPtj87BjUezFmaZKAha8eLdtngZkSpAdrSANa5K7jnnA8mywmpQwrKJuBmjdNpm9cBw==",
|
||||
"version": "3.13.4",
|
||||
"resolved": "https://registry.npmjs.org/lerna/-/lerna-3.13.4.tgz",
|
||||
"integrity": "sha512-qTp22nlpcgVrJGZuD7oHnFbTk72j2USFimc2Pj4kC0/rXmcU2xPtCiyuxLl8y6/6Lj5g9kwEuvKDZtSXujjX/A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@lerna/add": "3.13.3",
|
||||
"@lerna/bootstrap": "3.13.3",
|
||||
"@lerna/changed": "3.13.3",
|
||||
"@lerna/changed": "3.13.4",
|
||||
"@lerna/clean": "3.13.3",
|
||||
"@lerna/cli": "3.13.0",
|
||||
"@lerna/create": "3.13.3",
|
||||
"@lerna/diff": "3.13.3",
|
||||
"@lerna/exec": "3.13.3",
|
||||
"@lerna/import": "3.13.3",
|
||||
"@lerna/import": "3.13.4",
|
||||
"@lerna/init": "3.13.3",
|
||||
"@lerna/link": "3.13.3",
|
||||
"@lerna/list": "3.13.3",
|
||||
"@lerna/publish": "3.13.3",
|
||||
"@lerna/publish": "3.13.4",
|
||||
"@lerna/run": "3.13.3",
|
||||
"@lerna/version": "3.13.3",
|
||||
"@lerna/version": "3.13.4",
|
||||
"import-local": "^1.0.0",
|
||||
"npmlog": "^4.1.2"
|
||||
}
|
||||
|
@ -3750,18 +3777,18 @@
|
|||
}
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.39.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.39.0.tgz",
|
||||
"integrity": "sha512-DTsrw/iWVvwHH+9Otxccdyy0Tgiil6TWK/xhfARJZF/QFhwOgZgOIvA2/VIGpM8U7Q8z5nDmdDWC6tuVMJNibw==",
|
||||
"version": "1.40.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
|
||||
"integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==",
|
||||
"dev": true
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.23",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.23.tgz",
|
||||
"integrity": "sha512-ROk/m+gMVSrRxTkMlaQOvFmFmYDc7sZgrjjM76abqmd2Cc5fCV7jAMA5XUccEtJ3cYiYdgixUVI+fApc2LkXlw==",
|
||||
"version": "2.1.24",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
|
||||
"integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mime-db": "~1.39.0"
|
||||
"mime-db": "1.40.0"
|
||||
}
|
||||
},
|
||||
"mimic-fn": {
|
||||
|
@ -3946,9 +3973,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz",
|
||||
"integrity": "sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA==",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.5.0.tgz",
|
||||
"integrity": "sha512-YuZKluhWGJwCcUu4RlZstdAxr8bFfOVHakc1mplwHkk8J+tqM1Y5yraYvIUpeX8aY7+crCwiELJq7Vl0o0LWXw==",
|
||||
"dev": true
|
||||
},
|
||||
"node-fetch-npm": {
|
||||
|
@ -4838,9 +4865,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz",
|
||||
"integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==",
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.1.tgz",
|
||||
"integrity": "sha512-KuIe4mf++td/eFb6wkaPbMDnP6kObCaEtIDuHOUED6MNUo4K670KZUHuuvYPZDxNF0WVLw49n06M2m2dXphEzA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"path-parse": "^1.0.6"
|
||||
|
@ -4925,9 +4952,9 @@
|
|||
}
|
||||
},
|
||||
"rxjs": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz",
|
||||
"integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==",
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.1.tgz",
|
||||
"integrity": "sha512-y0j31WJc83wPu31vS1VlAFW5JGrnGC+j+TtGAa1fRQphy48+fDYiDmX8tjGloToEsMkxnouOg/1IzXGKkJnZMg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^1.9.0"
|
||||
|
@ -5561,9 +5588,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.5.tgz",
|
||||
"integrity": "sha512-e58FqZzPwaLODQetDQKlvErZaGkh1UmzP8YwU0aG65NLourKNtwVyDG8tkIyUU0vqWzxaikSvTaxrCSscmvqvQ==",
|
||||
"version": "3.5.9",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.5.9.tgz",
|
||||
"integrity": "sha512-WpT0RqsDtAWPNJK955DEnb6xjymR8Fn0OlK4TT4pS0ASYsVPqr5ELhgwOwLCP5J5vHeJ4xmMmz3DEgdqC10JeQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
"packages/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"lerna": "^3.13.2",
|
||||
"lerna": "^3.13.4",
|
||||
"typescript": "^3.4.3"
|
||||
},
|
||||
"dependencies": {},
|
||||
|
|
|
@ -1 +1,5 @@
|
|||
window.env = {};
|
||||
window.env = {
|
||||
clientUrl: "http://192.168.1.187:8081",
|
||||
serverUrl: "http://192.168.1.187:3000",
|
||||
billingUrl: "http://192.168.1.187:3001"
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@padloc/core": "^3.0.0",
|
||||
"@padloc/billing": "^3.0.0",
|
||||
"@webcomponents/webcomponentsjs": "^2.0.0",
|
||||
"autosize": "^4.0.2",
|
||||
"lit-element": "^2.1.0",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { App } from "@padloc/core/lib/app.js";
|
||||
import { setProvider } from "@padloc/core/lib/crypto.js";
|
||||
import { setPlatform } from "@padloc/core/lib/platform.js";
|
||||
import { BillingClient } from "@padloc/billing/lib/client.js";
|
||||
import { WebCryptoProvider } from "./crypto.js";
|
||||
import { Router } from "./route.js";
|
||||
import { AjaxSender } from "./ajax.js";
|
||||
|
@ -9,6 +10,7 @@ import { LocalStorage } from "./storage.js";
|
|||
|
||||
const sender = new AjaxSender((window.env && window.env.serverUrl) || "http://localhost:3000");
|
||||
export const app = (window.app = new App(new LocalStorage(), sender));
|
||||
window.billing = new BillingClient(app.state, new AjaxSender(window.env.billingUrl));
|
||||
export const router = (window.router = new Router());
|
||||
|
||||
setPlatform(new WebPlatform());
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "@padloc/billing",
|
||||
"version": "3.0.0",
|
||||
"description": "Padloc billing api",
|
||||
"main": "index.js",
|
||||
"author": "Martin Kleinschrodt <martin@maklesoft.com>",
|
||||
"license": "GPLv3",
|
||||
"private": false,
|
||||
"devDependencies": {
|
||||
"@types/stripe": "^6.25.14",
|
||||
"typescript": "^3.3.3333"
|
||||
},
|
||||
"dependencies": {
|
||||
"@padloc/core": "^3.0.0",
|
||||
"stripe": "^6.31.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
import { Account, AccountID } from "@padloc/core/src/account";
|
||||
import { OrgType, OrgID } from "@padloc/core/lib/org";
|
||||
import { Serializable } from "@padloc/core/lib/encoding";
|
||||
|
||||
export enum Plan {
|
||||
Free,
|
||||
Pro,
|
||||
Family,
|
||||
Team,
|
||||
Business
|
||||
}
|
||||
|
||||
export class PlanInfo extends Serializable {
|
||||
id = "";
|
||||
plan: Plan = Plan.Free;
|
||||
storage: number = 0;
|
||||
groups: number = 0;
|
||||
vaults: number = 0;
|
||||
min: number = 0;
|
||||
max: number = 0;
|
||||
available = false;
|
||||
|
||||
validate() {
|
||||
return (
|
||||
typeof this.id === "string" &&
|
||||
this.plan in Plan &&
|
||||
typeof this.min === "number" &&
|
||||
typeof this.max === "number" &&
|
||||
typeof this.storage === "number" &&
|
||||
typeof this.groups === "number" &&
|
||||
typeof this.vaults === "number" &&
|
||||
typeof this.available === "boolean"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export enum SubscriptionStatus {
|
||||
Incomplete = "incomplete",
|
||||
IncompleteExpired = "incomplete_expired",
|
||||
Trialing = "trialing",
|
||||
Active = "active",
|
||||
PastDue = "past_due",
|
||||
Canceled = "canceled",
|
||||
Unpaied = "unpaid"
|
||||
}
|
||||
|
||||
export class Subscription extends Serializable {
|
||||
id = "";
|
||||
account: AccountID = "";
|
||||
org: OrgID = "";
|
||||
plan: PlanInfo = new PlanInfo();
|
||||
status: SubscriptionStatus = SubscriptionStatus.Incomplete;
|
||||
storage: number = 0;
|
||||
groups: number = 0;
|
||||
vaults: number = 0;
|
||||
members: number = 0;
|
||||
|
||||
get orgType() {
|
||||
switch (this.plan.plan) {
|
||||
case Plan.Family:
|
||||
return OrgType.Basic;
|
||||
case Plan.Team:
|
||||
return OrgType.Team;
|
||||
case Plan.Business:
|
||||
return OrgType.Business;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
fromRaw({ id, status, account, plan, members, storage, groups, vaults, org }: any) {
|
||||
this.plan.fromRaw(plan);
|
||||
return super.fromRaw({ id, status, account, members, storage, groups, vaults, org });
|
||||
}
|
||||
|
||||
validate() {
|
||||
return (
|
||||
typeof this.id === "string" &&
|
||||
typeof this.status === "string" &&
|
||||
typeof this.account === "string" &&
|
||||
typeof this.org === "string" &&
|
||||
typeof this.members === "number" &&
|
||||
typeof this.storage === "number" &&
|
||||
typeof this.groups === "number" &&
|
||||
typeof this.vaults === "number"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class BillingInfo extends Serializable {
|
||||
customerId: string = "";
|
||||
subscription: Subscription | null = null;
|
||||
|
||||
fromRaw({ customerId, subscription }: any) {
|
||||
return super.fromRaw({
|
||||
subscription: (subscription && new Subscription().fromRaw(subscription)) || null,
|
||||
customerId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class GetBillingInfoParams extends Serializable {
|
||||
email!: string;
|
||||
|
||||
constructor(params?: Partial<GetBillingInfoParams>) {
|
||||
super();
|
||||
if (params) {
|
||||
Object.assign(this, params);
|
||||
}
|
||||
}
|
||||
|
||||
validate() {
|
||||
return typeof this.email === "string";
|
||||
}
|
||||
}
|
||||
|
||||
export class UpdateBillingInfoParams extends Serializable {
|
||||
email!: string;
|
||||
plan?: Plan;
|
||||
members?: number;
|
||||
source?: string;
|
||||
|
||||
constructor(params?: Partial<UpdateBillingInfoParams>) {
|
||||
super();
|
||||
if (params) {
|
||||
Object.assign(this, params);
|
||||
}
|
||||
}
|
||||
|
||||
validate() {
|
||||
return (
|
||||
typeof this.email === "string" &&
|
||||
(!this.members || typeof this.members === "number") &&
|
||||
(!this.plan || this.plan in Plan) &&
|
||||
(!this.source || typeof this.source === "string")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface BillingAPI {
|
||||
getBillingInfo(account: Account): Promise<BillingInfo>;
|
||||
updateBillingInfo(account: Account, params: UpdateBillingInfoParams): Promise<BillingInfo>;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { Account } from "@padloc/core/src/account";
|
||||
import { BaseClient } from "@padloc/core/lib/client";
|
||||
import { BillingAPI, BillingInfo, UpdateBillingInfoParams } from "./api";
|
||||
|
||||
export class BillingClient extends BaseClient implements BillingAPI {
|
||||
async getBillingInfo(_: Account) {
|
||||
const res = await this.call("getBillingInfo");
|
||||
return new BillingInfo().fromRaw(res.result);
|
||||
}
|
||||
|
||||
async updateBillingInfo(_: Account, params: UpdateBillingInfoParams) {
|
||||
const res = await this.call("updateBillingInfo", [params.toRaw()]);
|
||||
return new BillingInfo().fromRaw(res.result);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
import * as Stripe from "stripe";
|
||||
import { QuotaProvider, AccountQuota, OrgQuota } from "@padloc/core/src/quota";
|
||||
import { Account } from "@padloc/core/src/account";
|
||||
import { Org } from "@padloc/core/src/org";
|
||||
import { Serializable } from "@padloc/core/src/encoding";
|
||||
import { Err, ErrorCode } from "@padloc/core/src/error";
|
||||
import { BaseServer, ServerConfig, Context } from "@padloc/core/src/server";
|
||||
import { Storage } from "@padloc/core/src/storage";
|
||||
import { Messenger } from "@padloc/core/src/messenger";
|
||||
import { Request, Response } from "@padloc/core/src/transport";
|
||||
import { BillingAPI, Plan, PlanInfo, Subscription, UpdateBillingInfoParams } from "./api";
|
||||
|
||||
export interface BillingConfig {
|
||||
stripeSecret: string;
|
||||
}
|
||||
|
||||
function parsePlan({ id, metadata: { plan, storage, groups, vaults, min, max, available } }: Stripe.plans.IPlan) {
|
||||
return new PlanInfo().fromRaw({
|
||||
id,
|
||||
plan: plan ? (parseInt(plan) as Plan) : Plan.Free,
|
||||
storage: storage ? parseInt(storage) : 0,
|
||||
min: min ? parseInt(min) : 0,
|
||||
max: max ? parseInt(max) : 0,
|
||||
groups: groups ? parseInt(groups) : 0,
|
||||
vaults: vaults ? parseInt(vaults) : 0,
|
||||
available: available === "true"
|
||||
});
|
||||
}
|
||||
|
||||
function parseSubscription({
|
||||
id,
|
||||
status,
|
||||
plan,
|
||||
quantity,
|
||||
metadata: { storage, groups, vaults, account, org }
|
||||
}: Stripe.subscriptions.ISubscription) {
|
||||
const planInfo = parsePlan(plan!);
|
||||
return new Subscription().fromRaw({
|
||||
id,
|
||||
status,
|
||||
plan: planInfo.toRaw(),
|
||||
account: account || "",
|
||||
org: org || "",
|
||||
storage: storage ? parseInt(storage) : planInfo.storage,
|
||||
groups: groups ? parseInt(groups) : planInfo.groups,
|
||||
vaults: vaults ? parseInt(vaults) : planInfo.vaults,
|
||||
members: quantity
|
||||
});
|
||||
}
|
||||
|
||||
export class BillingInfo extends Serializable {
|
||||
customerId: string = "";
|
||||
subscription: Subscription | null = null;
|
||||
|
||||
fromRaw({ customerId, subscription }: any) {
|
||||
return super.fromRaw({
|
||||
subscription: (subscription && new Subscription().fromRaw(subscription)) || null,
|
||||
customerId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class BillingServer extends BaseServer implements QuotaProvider, BillingAPI {
|
||||
private _stripe: Stripe;
|
||||
private _availablePlans = new Map<Plan, PlanInfo>();
|
||||
|
||||
constructor(config: ServerConfig, storage: Storage, messenger: Messenger, public billingConfig: BillingConfig) {
|
||||
super(config, storage, messenger);
|
||||
this._stripe = new Stripe(billingConfig.stripeSecret);
|
||||
}
|
||||
|
||||
async init() {
|
||||
const plans = await this._stripe.plans.list();
|
||||
|
||||
for (const p of plans.data) {
|
||||
const plan = parsePlan(p);
|
||||
if (plan.available && plan.plan in Plan) {
|
||||
this._availablePlans.set(plan.plan, plan);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getAccountQuota(account: Account) {
|
||||
const { subscription } = await this.getBillingInfo(account);
|
||||
return new AccountQuota((subscription && { storage: subscription.storage }) || undefined);
|
||||
}
|
||||
|
||||
async getOrgQuota(account: Account, org: Org) {
|
||||
const info = await this.getBillingInfo(account);
|
||||
const sub = info.subscription;
|
||||
return sub && sub.org === org.id && sub.orgType == org.type ? new OrgQuota(sub) : null;
|
||||
}
|
||||
|
||||
async getBillingInfo(account: Account) {
|
||||
const customer = await this._getOrCreateCustomer(account);
|
||||
const subscription = customer.subscriptions.data[0] ? parseSubscription(customer.subscriptions.data[0]) : null;
|
||||
const info = new BillingInfo();
|
||||
info.subscription = subscription;
|
||||
info.customerId = customer.id;
|
||||
return info;
|
||||
}
|
||||
|
||||
async updateBillingInfo(account: Account, { plan, members, source }: UpdateBillingInfoParams) {
|
||||
const info = await this.getBillingInfo(account);
|
||||
|
||||
if (source) {
|
||||
await this._stripe.customers.update(info.customerId, { source });
|
||||
}
|
||||
|
||||
if (typeof plan !== "undefined" || typeof members !== "undefined") {
|
||||
const params: any = info.subscription ? {} : { customer: info.customerId };
|
||||
|
||||
if (typeof plan !== "undefined") {
|
||||
const planInfo = this._availablePlans.get(plan);
|
||||
if (!planInfo) {
|
||||
throw new Err(ErrorCode.BAD_REQUEST, "Invalid plan!");
|
||||
}
|
||||
params.plan = planInfo.id;
|
||||
}
|
||||
|
||||
if (typeof members !== "undefined") {
|
||||
params.quantity = members;
|
||||
}
|
||||
|
||||
if (info.subscription) {
|
||||
await this._stripe.subscriptions.update(
|
||||
info.subscription.id,
|
||||
params as Stripe.subscriptions.ISubscriptionUpdateOptions
|
||||
);
|
||||
} else {
|
||||
await this._stripe.subscriptions.create(params as Stripe.subscriptions.ISubscriptionCreationOptions);
|
||||
}
|
||||
}
|
||||
|
||||
return this.getBillingInfo(account);
|
||||
}
|
||||
|
||||
private async _getOrCreateCustomer({ email }: { email: string }): Promise<Stripe.customers.ICustomer> {
|
||||
let {
|
||||
data: [customer]
|
||||
} = await this._stripe.customers.list({ email });
|
||||
|
||||
// console.log("customer: ", customer);
|
||||
|
||||
if (!customer) {
|
||||
customer = await this._stripe.customers.create({
|
||||
email,
|
||||
plan: this._availablePlans.get(Plan.Free)!.id
|
||||
});
|
||||
}
|
||||
|
||||
return customer;
|
||||
}
|
||||
|
||||
async _process(req: Request, res: Response, ctx: Context): Promise<void> {
|
||||
console.log("process request", req, res, ctx);
|
||||
if (!ctx.account) {
|
||||
throw new Err(ErrorCode.INSUFFICIENT_PERMISSIONS);
|
||||
}
|
||||
|
||||
const method = req.method;
|
||||
const params = req.params || [];
|
||||
|
||||
switch (method) {
|
||||
case "getBillingInfo":
|
||||
res.result = (await this.getBillingInfo(ctx.account)).toRaw();
|
||||
break;
|
||||
|
||||
case "updateBillingInfo":
|
||||
res.result = (await this.updateBillingInfo(
|
||||
ctx.account,
|
||||
new UpdateBillingInfoParams(params[0])
|
||||
)).toRaw();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Err(ErrorCode.INVALID_REQUEST);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules/**/*.ts"]
|
||||
}
|
|
@ -38,7 +38,7 @@ export interface ClientState {
|
|||
* Client-side interface for Client-Server communication. Manages serialization,
|
||||
* authentication and some state like current session and account.
|
||||
*/
|
||||
export class Client implements API {
|
||||
export class BaseClient {
|
||||
constructor(
|
||||
/** Object for storing state */
|
||||
public state: ClientState,
|
||||
|
@ -93,7 +93,9 @@ export class Client implements API {
|
|||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export class Client extends BaseClient implements API {
|
||||
async requestEmailVerification(params: RequestEmailVerificationParams) {
|
||||
const res = await this.call("requestEmailVerification", [params.toRaw()]);
|
||||
return res.result;
|
||||
|
|
|
@ -82,7 +82,7 @@ export class Serializable {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (val instanceof Serializable) {
|
||||
if (typeof val === "object" && typeof val.toRaw === "function") {
|
||||
raw[prop] = val.toRaw();
|
||||
} else if (Array.isArray(val)) {
|
||||
raw[prop] = val.map((each: any) => (each instanceof Serializable ? each.toRaw() : each));
|
||||
|
|
|
@ -42,9 +42,9 @@ export interface ServerConfig {
|
|||
}
|
||||
|
||||
/**
|
||||
* Request context constructed for each request by [[Server]] to execute API requests
|
||||
* Request context
|
||||
*/
|
||||
export class Context implements API {
|
||||
export interface Context {
|
||||
/** Current [[Session]] */
|
||||
session?: Session;
|
||||
|
||||
|
@ -53,8 +53,14 @@ export class Context implements API {
|
|||
|
||||
/** Information about the device the request is coming from */
|
||||
device?: DeviceInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller class for processing api requests
|
||||
*/
|
||||
class Controller implements API {
|
||||
constructor(
|
||||
public context: Context,
|
||||
/** Server config */
|
||||
public config: ServerConfig,
|
||||
/** Storage for persisting data */
|
||||
|
@ -88,7 +94,8 @@ export class Context implements API {
|
|||
}
|
||||
}
|
||||
|
||||
const deviceTrusted = auth && this.device && auth.trustedDevices.some(({ id }) => id === this.device!.id);
|
||||
const deviceTrusted =
|
||||
auth && this.context.device && auth.trustedDevices.some(({ id }) => id === this.context.device!.id);
|
||||
|
||||
if (!deviceTrusted) {
|
||||
if (!verify) {
|
||||
|
@ -157,7 +164,7 @@ export class Context implements API {
|
|||
const session = new Session();
|
||||
session.id = await uuid();
|
||||
session.account = account;
|
||||
session.device = this.device;
|
||||
session.device = this.context.device;
|
||||
session.key = srp.K!;
|
||||
|
||||
// Add the session to the list of active sessions
|
||||
|
@ -171,8 +178,8 @@ export class Context implements API {
|
|||
|
||||
// Add device to trusted devices
|
||||
const auth = await this.storage.get(Auth, acc.email);
|
||||
if (this.device && !auth.trustedDevices.some(({ id }) => id === this.device!.id)) {
|
||||
auth.trustedDevices.push(this.device);
|
||||
if (this.context.device && !auth.trustedDevices.some(({ id }) => id === this.context.device!.id)) {
|
||||
auth.trustedDevices.push(this.context.device);
|
||||
}
|
||||
await this.storage.save(auth);
|
||||
|
||||
|
@ -221,8 +228,8 @@ export class Context implements API {
|
|||
auth.account = account.id;
|
||||
|
||||
// Add device to trusted devices
|
||||
if (this.device && !auth.trustedDevices.some(({ id }) => id === this.device!.id)) {
|
||||
auth.trustedDevices.push(this.device);
|
||||
if (this.context.device && !auth.trustedDevices.some(({ id }) => id === this.context.device!.id)) {
|
||||
auth.trustedDevices.push(this.context.device);
|
||||
}
|
||||
|
||||
// Provision the private vault for this account
|
||||
|
@ -779,7 +786,7 @@ export class Context implements API {
|
|||
}
|
||||
|
||||
private _requireAuth(): { account: Account; session: Session } {
|
||||
const { account, session } = this;
|
||||
const { account, session } = this.context;
|
||||
|
||||
if (!session || !account) {
|
||||
throw new Err(ErrorCode.INVALID_SESSION);
|
||||
|
@ -834,41 +841,14 @@ export class Context implements API {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Padloc server acts as a central repository for [[Account]]s, [[Org]]s
|
||||
* and [[Vault]]s. [[Server]] handles authentication, enforces user privileges
|
||||
* and acts as a mediator for key exchange between clients.
|
||||
*
|
||||
* The server component acts on a strict zero-trust, zero-knowledge principle
|
||||
* when it comes to sensitive data, meaning no sensitive data is ever exposed
|
||||
* to the server at any point, nor should the server (or the person controlling
|
||||
* it) ever be able to temper with critical data or trick users into granting
|
||||
* them access to encrypted information.
|
||||
*/
|
||||
export class Server {
|
||||
constructor(
|
||||
/** Server config */
|
||||
public config: ServerConfig,
|
||||
/** Storage for persisting data */
|
||||
public storage: Storage,
|
||||
/** [[Messenger]] implemenation for sending messages to users */
|
||||
public messenger: Messenger,
|
||||
/** Attachment storage */
|
||||
public attachmentStorage: AttachmentStorage,
|
||||
public quotaProvider: QuotaProvider
|
||||
) {}
|
||||
export abstract class BaseServer {
|
||||
constructor(public config: ServerConfig, public storage: Storage, public messenger: Messenger) {}
|
||||
|
||||
/** Handles an incoming [[Request]], processing it and constructing a [[Reponse]] */
|
||||
async handle(req: Request) {
|
||||
const res = new Response();
|
||||
try {
|
||||
const context = new Context(
|
||||
this.config,
|
||||
this.storage,
|
||||
this.messenger,
|
||||
this.attachmentStorage,
|
||||
this.quotaProvider
|
||||
);
|
||||
const context: Context = {};
|
||||
context.device = req.device && new DeviceInfo().fromRaw(req.device);
|
||||
await this._authenticate(req, context);
|
||||
await this._process(req, res, context);
|
||||
|
@ -881,117 +861,7 @@ export class Server {
|
|||
return res;
|
||||
}
|
||||
|
||||
private async _process(req: Request, res: Response, ctx: Context): Promise<void> {
|
||||
const method = req.method;
|
||||
const params = req.params || [];
|
||||
|
||||
switch (method) {
|
||||
case "requestEmailVerification":
|
||||
await ctx.requestEmailVerification(new RequestEmailVerificationParams().fromRaw(params[0]));
|
||||
break;
|
||||
|
||||
case "completeEmailVerification":
|
||||
res.result = await ctx.completeEmailVerification(
|
||||
new CompleteEmailVerificationParams().fromRaw(params[0])
|
||||
);
|
||||
break;
|
||||
|
||||
case "initAuth":
|
||||
res.result = (await ctx.initAuth(new InitAuthParams().fromRaw(params[0]))).toRaw();
|
||||
break;
|
||||
|
||||
case "updateAuth":
|
||||
await ctx.updateAuth(new Auth().fromRaw(params[0]));
|
||||
break;
|
||||
|
||||
case "createSession":
|
||||
res.result = (await ctx.createSession(new CreateSessionParams().fromRaw(params[0]))).toRaw();
|
||||
break;
|
||||
|
||||
case "revokeSession":
|
||||
if (typeof params[0] !== "string") {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
await ctx.revokeSession(params[0]);
|
||||
break;
|
||||
|
||||
case "getAccount":
|
||||
res.result = (await ctx.getAccount()).toRaw();
|
||||
break;
|
||||
|
||||
case "createAccount":
|
||||
res.result = (await ctx.createAccount(new CreateAccountParams().fromRaw(params[0]))).toRaw();
|
||||
break;
|
||||
|
||||
case "updateAccount":
|
||||
res.result = (await ctx.updateAccount(new Account().fromRaw(params[0]))).toRaw();
|
||||
break;
|
||||
|
||||
case "recoverAccount":
|
||||
res.result = (await ctx.recoverAccount(new RecoverAccountParams().fromRaw(params[0]))).toRaw();
|
||||
break;
|
||||
|
||||
case "createOrg":
|
||||
res.result = (await ctx.createOrg(new Org().fromRaw(params[0]))).toRaw();
|
||||
break;
|
||||
|
||||
case "getOrg":
|
||||
if (typeof params[0] !== "string") {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
res.result = (await ctx.getOrg(params[0])).toRaw();
|
||||
break;
|
||||
|
||||
case "updateOrg":
|
||||
res.result = (await ctx.updateOrg(new Org().fromRaw(params[0]))).toRaw();
|
||||
break;
|
||||
|
||||
case "getVault":
|
||||
if (typeof params[0] !== "string") {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
res.result = (await ctx.getVault(params[0])).toRaw();
|
||||
break;
|
||||
|
||||
case "updateVault":
|
||||
res.result = (await ctx.updateVault(new Vault().fromRaw(params[0]))).toRaw();
|
||||
break;
|
||||
|
||||
case "createVault":
|
||||
res.result = (await ctx.createVault(new Vault().fromRaw(params[0]))).toRaw();
|
||||
break;
|
||||
|
||||
case "deleteVault":
|
||||
if (typeof params[0] !== "string") {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
await ctx.deleteVault(params[0]);
|
||||
break;
|
||||
|
||||
case "getInvite":
|
||||
res.result = (await ctx.getInvite(new GetInviteParams().fromRaw(params[0]))).toRaw();
|
||||
break;
|
||||
|
||||
case "acceptInvite":
|
||||
await ctx.acceptInvite(new Invite().fromRaw(params[0]));
|
||||
break;
|
||||
|
||||
case "createAttachment":
|
||||
res.result = (await ctx.createAttachment(new Attachment().fromRaw(params[0]))).id;
|
||||
break;
|
||||
|
||||
case "getAttachment":
|
||||
res.result = (await ctx.getAttachment(new GetAttachmentParams().fromRaw(params[0]))).toRaw();
|
||||
break;
|
||||
|
||||
case "deleteAttachment":
|
||||
await ctx.deleteAttachment(new DeleteAttachmentParams().fromRaw(params[0]));
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Err(ErrorCode.INVALID_REQUEST);
|
||||
}
|
||||
}
|
||||
abstract _process(req: Request, res: Response, ctx: Context): Promise<void>;
|
||||
|
||||
private async _authenticate(req: Request, ctx: Context) {
|
||||
if (!req.auth) {
|
||||
|
@ -1002,7 +872,7 @@ export class Server {
|
|||
|
||||
// Find the session with the id specified in the [[Request.auth]] property
|
||||
try {
|
||||
session = await ctx.storage.get(Session, req.auth.session);
|
||||
session = await this.storage.get(Session, req.auth.session);
|
||||
} catch (e) {
|
||||
if (e.code === ErrorCode.NOT_FOUND) {
|
||||
throw new Err(ErrorCode.INVALID_SESSION);
|
||||
|
@ -1022,7 +892,7 @@ export class Server {
|
|||
}
|
||||
|
||||
// Get account associated with this session
|
||||
const account = await ctx.storage.get(Account, session.account);
|
||||
const account = await this.storage.get(Account, session.account);
|
||||
|
||||
// Store account and session on context
|
||||
ctx.session = session;
|
||||
|
@ -1040,10 +910,10 @@ export class Server {
|
|||
account.sessions.push(session.info);
|
||||
}
|
||||
|
||||
await Promise.all([ctx.storage.save(session), ctx.storage.save(account)]);
|
||||
await Promise.all([this.storage.save(session), this.storage.save(account)]);
|
||||
}
|
||||
|
||||
_handleError(e: Error, res: Response) {
|
||||
private _handleError(e: Error, res: Response) {
|
||||
if (e instanceof Err) {
|
||||
res.error = {
|
||||
code: e.code,
|
||||
|
@ -1067,3 +937,150 @@ export class Server {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Padloc server acts as a central repository for [[Account]]s, [[Org]]s
|
||||
* and [[Vault]]s. [[Server]] handles authentication, enforces user privileges
|
||||
* and acts as a mediator for key exchange between clients.
|
||||
*
|
||||
* The server component acts on a strict zero-trust, zero-knowledge principle
|
||||
* when it comes to sensitive data, meaning no sensitive data is ever exposed
|
||||
* to the server at any point, nor should the server (or the person controlling
|
||||
* it) ever be able to temper with critical data or trick users into granting
|
||||
* them access to encrypted information.
|
||||
*/
|
||||
export class Server extends BaseServer {
|
||||
constructor(
|
||||
/** Server config */
|
||||
config: ServerConfig,
|
||||
/** Storage for persisting data */
|
||||
storage: Storage,
|
||||
/** [[Messenger]] implemenation for sending messages to users */
|
||||
messenger: Messenger,
|
||||
/** Attachment storage */
|
||||
public attachmentStorage: AttachmentStorage,
|
||||
public quotaProvider: QuotaProvider
|
||||
) {
|
||||
super(config, storage, messenger);
|
||||
}
|
||||
|
||||
async _process(req: Request, res: Response, ctx: Context): Promise<void> {
|
||||
const ctlr = new Controller(
|
||||
ctx,
|
||||
this.config,
|
||||
this.storage,
|
||||
this.messenger,
|
||||
this.attachmentStorage,
|
||||
this.quotaProvider
|
||||
);
|
||||
const method = req.method;
|
||||
const params = req.params || [];
|
||||
|
||||
switch (method) {
|
||||
case "requestEmailVerification":
|
||||
await ctlr.requestEmailVerification(new RequestEmailVerificationParams().fromRaw(params[0]));
|
||||
break;
|
||||
|
||||
case "completeEmailVerification":
|
||||
res.result = await ctlr.completeEmailVerification(
|
||||
new CompleteEmailVerificationParams().fromRaw(params[0])
|
||||
);
|
||||
break;
|
||||
|
||||
case "initAuth":
|
||||
res.result = (await ctlr.initAuth(new InitAuthParams().fromRaw(params[0]))).toRaw();
|
||||
break;
|
||||
|
||||
case "updateAuth":
|
||||
await ctlr.updateAuth(new Auth().fromRaw(params[0]));
|
||||
break;
|
||||
|
||||
case "createSession":
|
||||
res.result = (await ctlr.createSession(new CreateSessionParams().fromRaw(params[0]))).toRaw();
|
||||
break;
|
||||
|
||||
case "revokeSession":
|
||||
if (typeof params[0] !== "string") {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
await ctlr.revokeSession(params[0]);
|
||||
break;
|
||||
|
||||
case "getAccount":
|
||||
res.result = (await ctlr.getAccount()).toRaw();
|
||||
break;
|
||||
|
||||
case "createAccount":
|
||||
res.result = (await ctlr.createAccount(new CreateAccountParams().fromRaw(params[0]))).toRaw();
|
||||
break;
|
||||
|
||||
case "updateAccount":
|
||||
res.result = (await ctlr.updateAccount(new Account().fromRaw(params[0]))).toRaw();
|
||||
break;
|
||||
|
||||
case "recoverAccount":
|
||||
res.result = (await ctlr.recoverAccount(new RecoverAccountParams().fromRaw(params[0]))).toRaw();
|
||||
break;
|
||||
|
||||
case "createOrg":
|
||||
res.result = (await ctlr.createOrg(new Org().fromRaw(params[0]))).toRaw();
|
||||
break;
|
||||
|
||||
case "getOrg":
|
||||
if (typeof params[0] !== "string") {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
res.result = (await ctlr.getOrg(params[0])).toRaw();
|
||||
break;
|
||||
|
||||
case "updateOrg":
|
||||
res.result = (await ctlr.updateOrg(new Org().fromRaw(params[0]))).toRaw();
|
||||
break;
|
||||
|
||||
case "getVault":
|
||||
if (typeof params[0] !== "string") {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
res.result = (await ctlr.getVault(params[0])).toRaw();
|
||||
break;
|
||||
|
||||
case "updateVault":
|
||||
res.result = (await ctlr.updateVault(new Vault().fromRaw(params[0]))).toRaw();
|
||||
break;
|
||||
|
||||
case "createVault":
|
||||
res.result = (await ctlr.createVault(new Vault().fromRaw(params[0]))).toRaw();
|
||||
break;
|
||||
|
||||
case "deleteVault":
|
||||
if (typeof params[0] !== "string") {
|
||||
throw new Err(ErrorCode.BAD_REQUEST);
|
||||
}
|
||||
await ctlr.deleteVault(params[0]);
|
||||
break;
|
||||
|
||||
case "getInvite":
|
||||
res.result = (await ctlr.getInvite(new GetInviteParams().fromRaw(params[0]))).toRaw();
|
||||
break;
|
||||
|
||||
case "acceptInvite":
|
||||
await ctlr.acceptInvite(new Invite().fromRaw(params[0]));
|
||||
break;
|
||||
|
||||
case "createAttachment":
|
||||
res.result = (await ctlr.createAttachment(new Attachment().fromRaw(params[0]))).id;
|
||||
break;
|
||||
|
||||
case "getAttachment":
|
||||
res.result = (await ctlr.getAttachment(new GetAttachmentParams().fromRaw(params[0]))).toRaw();
|
||||
break;
|
||||
|
||||
case "deleteAttachment":
|
||||
await ctlr.deleteAttachment(new DeleteAttachmentParams().fromRaw(params[0]));
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Err(ErrorCode.INVALID_REQUEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,11 +15,10 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@padloc/core": "^3.0.0",
|
||||
"@types/stripe": "^6.25.14",
|
||||
"@padloc/billing": "^3.0.0",
|
||||
"fs-extra": "^7.0.1",
|
||||
"level": "^5.0.1",
|
||||
"nodemailer": "^4.6.7",
|
||||
"stripe": "^6.31.2"
|
||||
"nodemailer": "^4.6.7"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "ts-node src/init.ts",
|
||||
|
|
|
@ -1,311 +0,0 @@
|
|||
import * as Stripe from "stripe";
|
||||
import { QuotaProvider, AccountQuota, OrgQuota } from "@padloc/core/src/quota";
|
||||
import { Account, AccountID } from "@padloc/core/src/account";
|
||||
import { Org, OrgType, OrgID } from "@padloc/core/src/org";
|
||||
import { Serializable } from "@padloc/core/src/encoding";
|
||||
import { Err, ErrorCode } from "@padloc/core/src/error";
|
||||
|
||||
export interface BillingConfig {
|
||||
stripeSecret: string;
|
||||
}
|
||||
|
||||
export enum Plan {
|
||||
Free,
|
||||
Pro,
|
||||
Family,
|
||||
Team,
|
||||
Business
|
||||
}
|
||||
|
||||
export class PlanInfo extends Serializable {
|
||||
id = "";
|
||||
plan: Plan = Plan.Free;
|
||||
storage: number = 0;
|
||||
groups: number = 0;
|
||||
vaults: number = 0;
|
||||
min: number = 0;
|
||||
max: number = 0;
|
||||
available = false;
|
||||
|
||||
validate() {
|
||||
return (
|
||||
typeof this.id === "string" &&
|
||||
this.plan in Plan &&
|
||||
typeof this.min === "number" &&
|
||||
typeof this.max === "number" &&
|
||||
typeof this.storage === "number" &&
|
||||
typeof this.groups === "number" &&
|
||||
typeof this.vaults === "number" &&
|
||||
typeof this.available === "boolean"
|
||||
);
|
||||
}
|
||||
|
||||
fromStripe({ id, metadata: { plan, storage, groups, vaults, min, max, available } }: Stripe.plans.IPlan) {
|
||||
return this.fromRaw({
|
||||
id,
|
||||
plan: plan ? (parseInt(plan) as Plan) : Plan.Free,
|
||||
storage: storage ? parseInt(storage) : 0,
|
||||
min: min ? parseInt(min) : 0,
|
||||
max: max ? parseInt(max) : 0,
|
||||
groups: groups ? parseInt(groups) : 0,
|
||||
vaults: vaults ? parseInt(vaults) : 0,
|
||||
available: available === "true"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export enum SubscriptionStatus {
|
||||
Incomplete = "incomplete",
|
||||
IncompleteExpired = "incomplete_expired",
|
||||
Trialing = "trialing",
|
||||
Active = "active",
|
||||
PastDue = "past_due",
|
||||
Canceled = "canceled",
|
||||
Unpaied = "unpaid"
|
||||
}
|
||||
|
||||
export class Subscription extends Serializable {
|
||||
id = "";
|
||||
account: AccountID = "";
|
||||
org: OrgID = "";
|
||||
plan: PlanInfo = new PlanInfo();
|
||||
status: SubscriptionStatus = SubscriptionStatus.Incomplete;
|
||||
storage: number = 0;
|
||||
groups: number = 0;
|
||||
vaults: number = 0;
|
||||
members: number = 0;
|
||||
|
||||
get orgType() {
|
||||
switch (this.plan.plan) {
|
||||
case Plan.Family:
|
||||
return OrgType.Basic;
|
||||
case Plan.Team:
|
||||
return OrgType.Team;
|
||||
case Plan.Business:
|
||||
return OrgType.Business;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
fromRaw({ id, status, account, plan, members, storage, groups, vaults, org }: any) {
|
||||
this.plan.fromRaw(plan);
|
||||
return super.fromRaw({ id, status, account, members, storage, groups, vaults, org });
|
||||
}
|
||||
|
||||
fromStripe({
|
||||
id,
|
||||
status,
|
||||
plan,
|
||||
quantity,
|
||||
metadata: { storage, groups, vaults, account, org }
|
||||
}: Stripe.subscriptions.ISubscription) {
|
||||
this.plan.fromStripe(plan!);
|
||||
return this.fromRaw({
|
||||
id,
|
||||
status,
|
||||
account: account || "",
|
||||
org: org || "",
|
||||
storage: storage ? parseInt(storage) : this.plan.storage,
|
||||
groups: groups ? parseInt(groups) : this.plan.groups,
|
||||
vaults: vaults ? parseInt(vaults) : this.plan.vaults,
|
||||
members: quantity
|
||||
});
|
||||
}
|
||||
|
||||
validate() {
|
||||
return (
|
||||
typeof this.id === "string" &&
|
||||
typeof this.status === "string" &&
|
||||
typeof this.account === "string" &&
|
||||
typeof this.org === "string" &&
|
||||
typeof this.members === "number" &&
|
||||
typeof this.storage === "number" &&
|
||||
typeof this.groups === "number" &&
|
||||
typeof this.vaults === "number"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// function parseSubscription(sub?: Stripe.subscriptions.ISubscription) {
|
||||
// const subscription = new Subscription();
|
||||
//
|
||||
// if (!sub) {
|
||||
// return subscription;
|
||||
// }
|
||||
//
|
||||
// Object.assign(subscription, parseMetaData(sub.plan!.metadata), parseMetaData(sub.metadata));
|
||||
//
|
||||
// subscription.status = sub.status as SubscriptionStatus;
|
||||
// subscription.members = sub.quantity;
|
||||
// subscription.id = sub.id;
|
||||
//
|
||||
// return subscription;
|
||||
// }
|
||||
|
||||
// function parseMetaData(raw: any = {}) {
|
||||
// const meta: {
|
||||
// members?: number;
|
||||
// groups?: number;
|
||||
// vaults?: number;
|
||||
// storage?: number;
|
||||
// plan?: Plan;
|
||||
// org?: string;
|
||||
// account?: string;
|
||||
// available?: boolean;
|
||||
// } = {
|
||||
// account: raw.account,
|
||||
// org: raw.org
|
||||
// };
|
||||
//
|
||||
// raw.members && (meta.members = parseInt(raw.members));
|
||||
// raw.groups && (meta.groups = parseInt(raw.groups));
|
||||
// raw.vaults && (meta.vaults = parseInt(raw.vaults));
|
||||
// raw.storage && (meta.storage = parseInt(raw.storage));
|
||||
// raw.plan && (meta.plan = parseInt(raw.plan) as Plan);
|
||||
// raw.available && (meta.available = raw.available === "true");
|
||||
//
|
||||
// return meta;
|
||||
// }
|
||||
|
||||
export class BillingInfo extends Serializable {
|
||||
customerId: string = "";
|
||||
subscription: Subscription | null = null;
|
||||
|
||||
fromRaw({ customerId, subscription }: any) {
|
||||
return super.fromRaw({
|
||||
subscription: (subscription && new Subscription().fromRaw(subscription)) || null,
|
||||
customerId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class GetBillingInfoParams extends Serializable {
|
||||
email!: string;
|
||||
|
||||
constructor(params?: Partial<GetBillingInfoParams>) {
|
||||
super();
|
||||
if (params) {
|
||||
Object.assign(this, params);
|
||||
}
|
||||
}
|
||||
|
||||
validate() {
|
||||
return typeof this.email === "string";
|
||||
}
|
||||
}
|
||||
|
||||
export class UpdateBillingInfoParams extends Serializable {
|
||||
email!: string;
|
||||
plan?: Plan;
|
||||
members?: number;
|
||||
source?: string;
|
||||
|
||||
constructor(params?: Partial<UpdateBillingInfoParams>) {
|
||||
super();
|
||||
if (params) {
|
||||
Object.assign(this, params);
|
||||
}
|
||||
}
|
||||
|
||||
validate() {
|
||||
return (
|
||||
typeof this.email === "string" &&
|
||||
(!this.members || typeof this.members === "number") &&
|
||||
(!this.plan || this.plan in Plan) &&
|
||||
(!this.source || typeof this.source === "string")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class BillingProvider implements QuotaProvider {
|
||||
private _stripe: Stripe;
|
||||
private _availablePlans = new Map<Plan, PlanInfo>();
|
||||
|
||||
constructor(public config: BillingConfig) {
|
||||
this._stripe = new Stripe(config.stripeSecret);
|
||||
}
|
||||
|
||||
async init() {
|
||||
const plans = await this._stripe.plans.list();
|
||||
|
||||
for (const p of plans.data) {
|
||||
const plan = new PlanInfo().fromStripe(p);
|
||||
if (plan.available && plan.plan in Plan) {
|
||||
this._availablePlans.set(plan.plan, plan);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getAccountQuota(account: Account) {
|
||||
const { subscription } = await this.getBillingInfo(account);
|
||||
return new AccountQuota((subscription && { storage: subscription.storage }) || undefined);
|
||||
}
|
||||
|
||||
async getOrgQuota(account: Account, org: Org) {
|
||||
const info = await this.getBillingInfo(account);
|
||||
const sub = info.subscription;
|
||||
return sub && sub.org === org.id && sub.orgType == org.type ? new OrgQuota(sub) : null;
|
||||
}
|
||||
|
||||
async getBillingInfo(params: GetBillingInfoParams) {
|
||||
const customer = await this._getOrCreateCustomer(params);
|
||||
const subscription = customer.subscriptions.data[0]
|
||||
? new Subscription().fromStripe(customer.subscriptions.data[0])
|
||||
: null;
|
||||
const info = new BillingInfo();
|
||||
info.subscription = subscription;
|
||||
info.customerId = customer.id;
|
||||
return info;
|
||||
}
|
||||
|
||||
async updateBillingInfo({ email, plan, members, source }: UpdateBillingInfoParams) {
|
||||
const info = await this.getBillingInfo(new GetBillingInfoParams({ email }));
|
||||
|
||||
if (source) {
|
||||
await this._stripe.customers.update(info.customerId, { source });
|
||||
}
|
||||
|
||||
if (typeof plan !== "undefined" || typeof members !== "undefined") {
|
||||
const params: any = info.subscription ? {} : { customer: info.customerId };
|
||||
|
||||
if (typeof plan !== "undefined") {
|
||||
const planInfo = this._availablePlans.get(plan);
|
||||
if (!planInfo) {
|
||||
throw new Err(ErrorCode.BAD_REQUEST, "Invalid plan!");
|
||||
}
|
||||
params.plan = planInfo.id;
|
||||
}
|
||||
|
||||
if (typeof members !== "undefined") {
|
||||
params.quantity = members;
|
||||
}
|
||||
|
||||
if (info.subscription) {
|
||||
await this._stripe.subscriptions.update(
|
||||
info.subscription.id,
|
||||
params as Stripe.subscriptions.ISubscriptionUpdateOptions
|
||||
);
|
||||
} else {
|
||||
await this._stripe.subscriptions.create(params as Stripe.subscriptions.ISubscriptionCreationOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _getOrCreateCustomer({ email }: { email: string }): Promise<Stripe.customers.ICustomer> {
|
||||
let {
|
||||
data: [customer]
|
||||
} = await this._stripe.customers.list({ email });
|
||||
|
||||
// console.log("customer: ", customer);
|
||||
|
||||
if (!customer) {
|
||||
customer = await this._stripe.customers.create({
|
||||
email,
|
||||
plan: this._availablePlans.get(Plan.Free)!.id
|
||||
});
|
||||
}
|
||||
|
||||
return customer;
|
||||
}
|
||||
}
|
|
@ -1,15 +1,19 @@
|
|||
import { Server } from "@padloc/core/src/server";
|
||||
import { setProvider } from "@padloc/core/src/crypto";
|
||||
import { BillingServer } from "@padloc/billing/src/server";
|
||||
import { NodeCryptoProvider } from "./crypto";
|
||||
import { HTTPReceiver } from "./http";
|
||||
import { LevelDBStorage } from "./storage";
|
||||
import { EmailMessenger } from "./messenger";
|
||||
import { FileSystemStorage } from "./attachment";
|
||||
import { BillingProvider, GetBillingInfoParams, UpdateBillingInfoParams, Plan } from "./billing";
|
||||
|
||||
async function init() {
|
||||
setProvider(new NodeCryptoProvider());
|
||||
|
||||
const config = {
|
||||
clientUrl: process.env.PL_CLIENT_URL || "https://localhost:8081",
|
||||
reportErrors: process.env.PL_REPORT_ERRORS || ""
|
||||
};
|
||||
const messenger = new EmailMessenger({
|
||||
host: process.env.PL_EMAIL_SERVER || "",
|
||||
port: process.env.PL_EMAIL_PORT || "",
|
||||
|
@ -19,33 +23,28 @@ async function init() {
|
|||
});
|
||||
const storage = new LevelDBStorage(process.env.PL_DB_PATH || "db");
|
||||
const attachmentStorage = new FileSystemStorage({ path: process.env.PL_ATTACHMENTS_PATH || "attachments" });
|
||||
const billingProvider = new BillingProvider({ stripeSecret: process.env.PL_STRIPE_SECRET || "" });
|
||||
const billingProvider = new BillingServer(config, storage, messenger, {
|
||||
stripeSecret: process.env.PL_STRIPE_SECRET || ""
|
||||
});
|
||||
|
||||
await billingProvider.init();
|
||||
await billingProvider.updateBillingInfo(
|
||||
new UpdateBillingInfoParams({ email: "martin@maklesoft.com", plan: Plan.Team })
|
||||
);
|
||||
console.log(await billingProvider.getBillingInfo(new GetBillingInfoParams({ email: "martin@maklesoft.com" })));
|
||||
|
||||
const server = new Server(
|
||||
{
|
||||
clientUrl: process.env.PL_CLIENT_URL || "https://localhost:8081",
|
||||
reportErrors: process.env.PL_REPORT_ERRORS || ""
|
||||
},
|
||||
storage,
|
||||
messenger,
|
||||
attachmentStorage,
|
||||
billingProvider
|
||||
);
|
||||
const server = new Server(config, storage, messenger, attachmentStorage, billingProvider);
|
||||
|
||||
let port = 3000;
|
||||
|
||||
try {
|
||||
port = parseInt(process.env.PL_SERVER_PORT!);
|
||||
} catch (e) {}
|
||||
let port = parseInt(process.env.PL_SERVER_PORT!);
|
||||
if (isNaN(port)) {
|
||||
port = 3000;
|
||||
}
|
||||
let billingPort = parseInt(process.env.PL_BILLING_PORT!);
|
||||
if (isNaN(billingPort)) {
|
||||
billingPort = 3001;
|
||||
}
|
||||
|
||||
console.log(`Starting server on port ${port}`);
|
||||
new HTTPReceiver(port).listen(req => server.handle(req));
|
||||
|
||||
console.log(`Starting billing server on port ${billingPort}`);
|
||||
new HTTPReceiver(billingPort).listen(req => billingProvider.handle(req));
|
||||
}
|
||||
|
||||
init();
|
||||
|
|
Loading…
Reference in New Issue