First pass at implementing a stripe-powered billing service

This commit is contained in:
Martin Kleinschrodt 2019-05-09 08:02:07 +02:00
parent 72e5ac8926
commit adbaafe26a
18 changed files with 1723 additions and 604 deletions

1
.gitignore vendored
View File

@ -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

249
package-lock.json generated
View File

@ -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": {

View File

@ -15,7 +15,7 @@
"packages/*"
],
"devDependencies": {
"lerna": "^3.13.2",
"lerna": "^3.13.4",
"typescript": "^3.4.3"
},
"dependencies": {},

View File

@ -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"
};

View File

@ -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",

View File

@ -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());

1009
packages/billing/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

143
packages/billing/src/api.ts Normal file
View File

@ -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>;
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"rootDir": "src",
"outDir": "lib"
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules/**/*.ts"]
}

View File

@ -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;

View File

@ -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));

View File

@ -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);
}
}
}

View File

@ -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",

View File

@ -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;
}
}

View File

@ -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();