diff --git a/package.json b/package.json index a5d6fa1..353d4cb 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "lib": "lib" }, "dependencies": { + "@types/bcrypt": "^5.0.0", "@types/busboy": "^0.2.3", "@types/mkdirp": "^1.0.1", "@types/negotiator": "^0.6.1", @@ -18,6 +19,7 @@ "@types/rimraf": "^3.0.0", "@types/ssh2": "^0.5.46", "@types/uuid": "^8.3.0", + "bcrypt": "^5.0.1", "busboy": "^0.3.1", "colors": "^1.4.0", "dotenv": "^8.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bfcce2a..371af46 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,5 @@ dependencies: + '@types/bcrypt': 5.0.0 '@types/busboy': 0.2.3 '@types/mkdirp': 1.0.1 '@types/negotiator': 0.6.1 @@ -9,6 +10,7 @@ dependencies: '@types/rimraf': 3.0.0 '@types/ssh2': 0.5.46 '@types/uuid': 8.3.0 + bcrypt: 5.0.1 busboy: 0.3.1 colors: 1.4.0 dotenv: 8.2.0 @@ -85,6 +87,21 @@ packages: node: ^10.12.0 || >=12.0.0 resolution: integrity: sha512-5v7TDE9plVhvxQeWLXDTvFvJBdH6pEsdnl2g/dAptmuFEPedQ4Erq5rsDsX+mvAM610IhNaO2W5V1dOOnDKxkQ== + /@mapbox/node-pre-gyp/1.0.5: + dependencies: + detect-libc: 1.0.3 + https-proxy-agent: 5.0.0 + make-dir: 3.1.0 + node-fetch: 2.6.1 + nopt: 5.0.0 + npmlog: 4.1.2 + rimraf: 3.0.2 + semver: 7.3.5 + tar: 6.1.0 + dev: false + hasBin: true + resolution: + integrity: sha512-4srsKPXWlIxp5Vbqz5uLfBN+du2fJChBoYn/f2h991WLdk7jUvcSk/McVLSv/X+xQIPI8eGD5GjrnygdyHnhPA== /@nodelib/fs.scandir/2.1.4: dependencies: '@nodelib/fs.stat': 2.0.4 @@ -109,6 +126,12 @@ packages: node: '>= 8' resolution: integrity: sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow== + /@types/bcrypt/5.0.0: + dependencies: + '@types/node': 14.17.2 + dev: false + resolution: + integrity: sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw== /@types/busboy/0.2.3: dependencies: '@types/node': 14.14.37 @@ -148,6 +171,10 @@ packages: dev: false resolution: integrity: sha512-/tpUyFD7meeooTRwl3sYlihx2BrJE7q9XF71EguPFIySj9B7qgnRtHsHTho+0AUm4m1SvWGm6uSncrR94q6Vtw== + /@types/node/14.17.2: + dev: false + resolution: + integrity: sha512-sld7b/xmFum66AAKuz/rp/CUO8+98fMpyQ3SBfzzBNGMd/1iHBTAg9oyAvcYlAj46bpc74r91jSw2iFdnx29nw== /@types/pg/8.6.0: dependencies: '@types/node': 14.17.1 @@ -293,6 +320,10 @@ packages: node: ^8.10.0 || ^10.13.0 || >=11.10.1 resolution: integrity: sha512-cw4j8lH38V1ycGBbF+aFiLUls9Z0Bw8QschP3mkth50BbWzgFS33ISIgBzUMuQ2IdahoEv/rXstr8Zhlz4B1Zg== + /abbrev/1.1.1: + dev: false + resolution: + integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== /acorn-jsx/5.3.1_acorn@7.4.1: dependencies: acorn: 7.4.1 @@ -307,6 +338,14 @@ packages: hasBin: true resolution: integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + /agent-base/6.0.2: + dependencies: + debug: 4.3.1 + dev: false + engines: + node: '>= 6.0.0' + resolution: + integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== /ajv/6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -331,6 +370,12 @@ packages: node: '>=6' resolution: integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + /ansi-regex/2.1.1: + dev: false + engines: + node: '>=0.10.0' + resolution: + integrity: sha1-w7M6te42DYbg5ijwRorn7yfWVN8= /ansi-regex/5.0.0: dev: true engines: @@ -353,6 +398,17 @@ packages: node: '>=8' resolution: integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + /aproba/1.2.0: + dev: false + resolution: + integrity: sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + /are-we-there-yet/1.1.5: + dependencies: + delegates: 1.0.0 + readable-stream: 2.3.7 + dev: false + resolution: + integrity: sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== /arg/4.1.3: dev: false resolution: @@ -412,6 +468,16 @@ packages: dev: false resolution: integrity: sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + /bcrypt/5.0.1: + dependencies: + '@mapbox/node-pre-gyp': 1.0.5 + node-addon-api: 3.2.1 + dev: false + engines: + node: '>= 10.0.0' + requiresBuild: true + resolution: + integrity: sha512-9BTgmrhZM2t1bNuDtrtIMVSmmxZBrJ71n8Wg+YgdjHuIWYF7SjjmCPZFB+/5i/o/PIeRpwVJR3P+NrpIItUjqw== /brace-expansion/1.1.11: dependencies: balanced-match: 1.0.2 @@ -482,6 +548,18 @@ packages: dev: false resolution: integrity: sha1-x84o821LzZdE5f/CxfzeHHMmH8A= + /chownr/2.0.0: + dev: false + engines: + node: '>=10' + resolution: + integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + /code-point-at/1.1.0: + dev: false + engines: + node: '>=0.10.0' + resolution: + integrity: sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= /color-convert/1.9.3: dependencies: color-name: 1.1.3 @@ -517,6 +595,10 @@ packages: /concat-map/0.0.1: resolution: integrity: sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + /console-control-strings/1.1.0: + dev: false + resolution: + integrity: sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= /constantinople/4.0.1: dependencies: '@babel/parser': 7.13.13 @@ -524,6 +606,10 @@ packages: dev: false resolution: integrity: sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw== + /core-util-is/1.0.2: + dev: false + resolution: + integrity: sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= /cpu-features/0.0.2: dependencies: nan: 2.14.2 @@ -551,7 +637,6 @@ packages: /debug/4.3.1: dependencies: ms: 2.1.2 - dev: true engines: node: '>=6.0' peerDependencies: @@ -565,6 +650,17 @@ packages: dev: true resolution: integrity: sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + /delegates/1.0.0: + dev: false + resolution: + integrity: sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + /detect-libc/1.0.3: + dev: false + engines: + node: '>=0.10' + hasBin: true + resolution: + integrity: sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= /dicer/0.3.0: dependencies: streamsearch: 0.1.2 @@ -838,6 +934,14 @@ packages: node: '>=10' resolution: integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + /fs-minipass/2.1.0: + dependencies: + minipass: 3.1.3 + dev: false + engines: + node: '>= 8' + resolution: + integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== /fs.realpath/1.0.0: resolution: integrity: sha1-FQStJSMVjKpA20onh8sBQRmU6k8= @@ -849,6 +953,19 @@ packages: dev: true resolution: integrity: sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + /gauge/2.7.4: + dependencies: + aproba: 1.2.0 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.3 + string-width: 1.0.2 + strip-ansi: 3.0.1 + wide-align: 1.1.3 + dev: false + resolution: + integrity: sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= /get-intrinsic/1.1.1: dependencies: function-bind: 1.1.1 @@ -940,6 +1057,10 @@ packages: node: '>= 0.4' resolution: integrity: sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== + /has-unicode/2.0.1: + dev: false + resolution: + integrity: sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= /has/1.0.3: dependencies: function-bind: 1.1.1 @@ -948,6 +1069,15 @@ packages: node: '>= 0.4.0' resolution: integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + /https-proxy-agent/5.0.0: + dependencies: + agent-base: 6.0.2 + debug: 4.3.1 + dev: false + engines: + node: '>= 6' + resolution: + integrity: sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== /ignore/4.0.6: dev: true engines: @@ -1009,6 +1139,14 @@ packages: node: '>=0.10.0' resolution: integrity: sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + /is-fullwidth-code-point/1.0.0: + dependencies: + number-is-nan: 1.0.1 + dev: false + engines: + node: '>=0.10.0' + resolution: + integrity: sha1-754xOG8DGn8NZDr4L95QxFfvAMs= /is-fullwidth-code-point/3.0.0: dev: true engines: @@ -1042,6 +1180,10 @@ packages: node: '>= 0.4' resolution: integrity: sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg== + /isarray/1.0.0: + dev: false + resolution: + integrity: sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= /isexe/2.0.0: dev: true resolution: @@ -1122,7 +1264,6 @@ packages: /lru-cache/6.0.0: dependencies: yallist: 4.0.0 - dev: true engines: node: '>=10' resolution: @@ -1131,6 +1272,14 @@ packages: dev: false resolution: integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow== + /make-dir/3.1.0: + dependencies: + semver: 6.3.0 + dev: false + engines: + node: '>=8' + resolution: + integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== /make-error/1.3.6: dev: false resolution: @@ -1166,6 +1315,23 @@ packages: dev: false resolution: integrity: sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + /minipass/3.1.3: + dependencies: + yallist: 4.0.0 + dev: false + engines: + node: '>=8' + resolution: + integrity: sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== + /minizlib/2.1.2: + dependencies: + minipass: 3.1.3 + yallist: 4.0.0 + dev: false + engines: + node: '>= 8' + resolution: + integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== /mkdirp/1.0.4: dev: false engines: @@ -1174,7 +1340,6 @@ packages: resolution: integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== /ms/2.1.2: - dev: true resolution: integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== /nan/2.14.2: @@ -1196,6 +1361,40 @@ packages: dev: false resolution: integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + /node-addon-api/3.2.1: + dev: false + resolution: + integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== + /node-fetch/2.6.1: + dev: false + engines: + node: 4.x || >=6.0.0 + resolution: + integrity: sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== + /nopt/5.0.0: + dependencies: + abbrev: 1.1.1 + dev: false + engines: + node: '>=6' + hasBin: true + resolution: + integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + /npmlog/4.1.2: + dependencies: + are-we-there-yet: 1.1.5 + console-control-strings: 1.1.0 + gauge: 2.7.4 + set-blocking: 2.0.0 + dev: false + resolution: + integrity: sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + /number-is-nan/1.0.1: + dev: false + engines: + node: '>=0.10.0' + resolution: + integrity: sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= /object-assign/4.1.1: dev: false engines: @@ -1362,6 +1561,10 @@ packages: node: '>= 0.8.0' resolution: integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + /process-nextick-args/2.0.1: + dev: false + resolution: + integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== /progress/2.0.3: engines: node: '>=0.4.0' @@ -1474,6 +1677,18 @@ packages: dev: true resolution: integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + /readable-stream/2.3.7: + dependencies: + core-util-is: 1.0.2 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + dev: false + resolution: + integrity: sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== /readable-stream/3.6.0: dependencies: inherits: 2.0.4 @@ -1540,6 +1755,10 @@ packages: dev: true resolution: integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + /safe-buffer/5.1.2: + dev: false + resolution: + integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== /safe-buffer/5.2.1: dev: false resolution: @@ -1548,15 +1767,23 @@ packages: dev: false resolution: integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + /semver/6.3.0: + dev: false + hasBin: true + resolution: + integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== /semver/7.3.5: dependencies: lru-cache: 6.0.0 - dev: true engines: node: '>=10' hasBin: true resolution: integrity: sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + /set-blocking/2.0.0: + dev: false + resolution: + integrity: sha1-BF+XgtARrppoA93TgrJDkrPYkPc= /shebang-command/2.0.0: dependencies: shebang-regex: 3.0.0 @@ -1589,6 +1816,10 @@ packages: dev: false resolution: integrity: sha512-NEjg1mVbAUrzRv2eIcUt3TG7X9svX7l3n3F5/3OdFq+/BxUdmBOeKGiH4icZJBLHy354Shnj6sfBTemea2e7XA== + /signal-exit/3.0.3: + dev: false + resolution: + integrity: sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== /slash/3.0.0: dev: true engines: @@ -1647,6 +1878,16 @@ packages: node: '>=0.8.0' resolution: integrity: sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= + /string-width/1.0.2: + dependencies: + code-point-at: 1.1.0 + is-fullwidth-code-point: 1.0.0 + strip-ansi: 3.0.1 + dev: false + engines: + node: '>=0.10.0' + resolution: + integrity: sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= /string-width/4.2.2: dependencies: emoji-regex: 8.0.0 @@ -1657,12 +1898,26 @@ packages: node: '>=8' resolution: integrity: sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== + /string_decoder/1.1.1: + dependencies: + safe-buffer: 5.1.2 + dev: false + resolution: + integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== /string_decoder/1.3.0: dependencies: safe-buffer: 5.2.1 dev: false resolution: integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + /strip-ansi/3.0.1: + dependencies: + ansi-regex: 2.1.1 + dev: false + engines: + node: '>=0.10.0' + resolution: + integrity: sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= /strip-ansi/6.0.0: dependencies: ansi-regex: 5.0.0 @@ -1706,6 +1961,19 @@ packages: node: '>=10.0.0' resolution: integrity: sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg== + /tar/6.1.0: + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 3.1.3 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + dev: false + engines: + node: '>= 10' + resolution: + integrity: sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA== /text-table/0.2.0: dev: true resolution: @@ -1894,6 +2162,12 @@ packages: hasBin: true resolution: integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + /wide-align/1.1.3: + dependencies: + string-width: 1.0.2 + dev: false + resolution: + integrity: sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== /with/7.0.2: dependencies: '@babel/parser': 7.13.13 @@ -1929,7 +2203,6 @@ packages: resolution: integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== /yallist/4.0.0: - dev: true resolution: integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== /yn/3.1.1: @@ -1939,6 +2212,7 @@ packages: resolution: integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== specifiers: + '@types/bcrypt': ^5.0.0 '@types/busboy': ^0.2.3 '@types/mkdirp': ^1.0.1 '@types/negotiator': ^0.6.1 @@ -1951,6 +2225,7 @@ specifiers: '@types/uuid': ^8.3.0 '@typescript-eslint/eslint-plugin': ^4.26.0 '@typescript-eslint/parser': ^4.26.0 + bcrypt: ^5.0.1 busboy: ^0.3.1 colors: ^1.4.0 dotenv: ^8.2.0 diff --git a/src/auth/Authentication.ts b/src/auth/Authentication.ts new file mode 100644 index 0000000..2b5cc49 --- /dev/null +++ b/src/auth/Authentication.ts @@ -0,0 +1,16 @@ +import {Inject, Injectable} from '../di' +import {Unit} from '../lifecycle/Unit' +import {Logging} from '../service/Logging' + +/** + * Unit class that bootstraps the authentication framework. + */ +@Injectable() +export class Authentication extends Unit { + @Inject() + protected readonly logging!: Logging + + async up(): Promise { + this.container() + } +} diff --git a/src/auth/SecurityContext.ts b/src/auth/SecurityContext.ts new file mode 100644 index 0000000..e694050 --- /dev/null +++ b/src/auth/SecurityContext.ts @@ -0,0 +1,122 @@ +import {Inject, Injectable} from '../di' +import {EventBus} from '../event/EventBus' +import {Awaitable, Maybe} from '../util' +import {Authenticatable, AuthenticatableRepository} from './types' +import {UserAuthenticatedEvent} from './event/UserAuthenticatedEvent' +import {UserFlushedEvent} from './event/UserFlushedEvent' + +/** + * Base-class for a context that authenticates users and manages security. + */ +@Injectable() +export abstract class SecurityContext { + @Inject() + protected readonly bus!: EventBus + + /** The currently authenticated user, if one exists. */ + private authenticatedUser?: Authenticatable + + constructor( + /** The repository from which to draw users. */ + protected readonly repository: AuthenticatableRepository, + + /** The name of this context. */ + public readonly name: string, + ) { } + + /** + * Called when the context is created. Can be used by child-classes to do setup work. + */ + initialize(): Awaitable {} // eslint-disable-line @typescript-eslint/no-empty-function + + /** + * Authenticate the given user, without persisting the authentication. + * That is, when the lifecycle ends, the user will be unauthenticated implicitly. + * @param user + */ + async authenticateOnce(user: Authenticatable): Promise { + this.authenticatedUser = user + await this.bus.dispatch(new UserAuthenticatedEvent(user, this)) + } + + /** + * Authenticate the given user and persist the authentication. + * @param user + */ + async authenticate(user: Authenticatable): Promise { + this.authenticatedUser = user + await this.persist() + await this.bus.dispatch(new UserAuthenticatedEvent(user, this)) + } + + /** + * Attempt to authenticate a user based on their credentials. + * If the credentials are valid, the user will be authenticated, but the authentication + * will not be persisted. That is, when the lifecycle ends, the user will be + * unauthenticated implicitly. + * @param credentials + */ + async attemptOnce(credentials: Record): Promise> { + const user = await this.repository.getByCredentials(credentials) + if ( user ) { + await this.authenticateOnce(user) + return user + } + } + + /** + * Attempt to authenticate a user based on their credentials. + * If the credentials are valid, the user will be authenticated and the + * authentication will be persisted. + * @param credentials + */ + async attempt(credentials: Record): Promise> { + const user = await this.repository.getByCredentials(credentials) + if ( user ) { + await this.authenticate(user) + return user + } + } + + /** + * Unauthenticate the current user, if one exists, but do not persist the change. + */ + async flushOnce(): Promise { + const user = this.authenticatedUser + if ( user ) { + this.authenticatedUser = undefined + await this.bus.dispatch(new UserFlushedEvent(user, this)) + } + } + + /** + * Unauthenticate the current user, if one exists, and persist the change. + */ + async flush(): Promise { + const user = this.authenticatedUser + if ( user ) { + this.authenticatedUser = undefined + await this.persist() + await this.bus.dispatch(new UserFlushedEvent(user, this)) + } + } + + /** + * Write the current state of the security context to whatever storage + * medium the context's host provides. + */ + abstract persist(): Awaitable + + /** + * Get the credentials for the current user from whatever storage medium + * the context's host provides. + */ + abstract getCredentials(): Awaitable> + + /** + * Get the currently authenticated user, if one exists. + */ + getUser(): Maybe { + return this.authenticatedUser + } +} diff --git a/src/auth/contexts/SessionSecurityContext.ts b/src/auth/contexts/SessionSecurityContext.ts new file mode 100644 index 0000000..a58382b --- /dev/null +++ b/src/auth/contexts/SessionSecurityContext.ts @@ -0,0 +1,23 @@ +import {SecurityContext} from '../SecurityContext' +import {Inject, Injectable} from '../../di' +import {Session} from '../../http/session/Session' +import {Awaitable} from '../../util' + +/** + * Security context implementation that uses the session as storage. + */ +@Injectable() +export class SessionSecurityContext extends SecurityContext { + @Inject() + protected readonly session!: Session + + getCredentials(): Awaitable> { + return { + securityIdentifier: this.session.get('extollo.auth.securityIdentifier'), + } + } + + persist(): Awaitable { + this.session.set('extollo.auth.securityIdentifier', this.getUser()?.getIdentifier()) + } +} diff --git a/src/auth/event/UserAuthenticatedEvent.ts b/src/auth/event/UserAuthenticatedEvent.ts new file mode 100644 index 0000000..95a1571 --- /dev/null +++ b/src/auth/event/UserAuthenticatedEvent.ts @@ -0,0 +1,27 @@ +import {Event} from '../../event/Event' +import {SecurityContext} from '../SecurityContext' +import {Awaitable, JSONState} from '../../util' +import {Authenticatable} from '../types' + +/** + * Event fired when a user is authenticated. + */ +export class UserAuthenticatedEvent extends Event { + constructor( + public readonly user: Authenticatable, + public readonly context: SecurityContext, + ) { + super() + } + + async dehydrate(): Promise { + return { + user: await this.user.dehydrate(), + contextName: this.context.name, + } + } + + rehydrate(state: JSONState): Awaitable { // eslint-disable-line @typescript-eslint/no-unused-vars + // TODO fill this in + } +} diff --git a/src/auth/event/UserFlushedEvent.ts b/src/auth/event/UserFlushedEvent.ts new file mode 100644 index 0000000..d1c97a2 --- /dev/null +++ b/src/auth/event/UserFlushedEvent.ts @@ -0,0 +1,27 @@ +import {Event} from '../../event/Event' +import {SecurityContext} from '../SecurityContext' +import {Awaitable, JSONState} from '../../util' +import {Authenticatable} from '../types' + +/** + * Event fired when a user is unauthenticated. + */ +export class UserFlushedEvent extends Event { + constructor( + public readonly user: Authenticatable, + public readonly context: SecurityContext, + ) { + super() + } + + async dehydrate(): Promise { + return { + user: await this.user.dehydrate(), + contextName: this.context.name, + } + } + + rehydrate(state: JSONState): Awaitable { // eslint-disable-line @typescript-eslint/no-unused-vars + // TODO fill this in + } +} diff --git a/src/auth/index.ts b/src/auth/index.ts new file mode 100644 index 0000000..f98447b --- /dev/null +++ b/src/auth/index.ts @@ -0,0 +1,13 @@ +export * from './types' + +export * from './SecurityContext' + +export * from './event/UserAuthenticatedEvent' +export * from './event/UserFlushedEvent' + +export * from './contexts/SessionSecurityContext' + +export * from './orm/ORMUser' +export * from './orm/ORMUserRepository' + +export * from './Authentication' diff --git a/src/auth/orm/ORMUser.ts b/src/auth/orm/ORMUser.ts new file mode 100644 index 0000000..95ebf78 --- /dev/null +++ b/src/auth/orm/ORMUser.ts @@ -0,0 +1,64 @@ +import {Field, FieldType, Model} from '../../orm' +import {Authenticatable, AuthenticatableIdentifier} from '../types' +import {Injectable} from '../../di' +import * as bcrypt from 'bcrypt' +import {Awaitable, JSONState} from '../../util' + +/** + * A basic ORM-driven user class. + */ +@Injectable() +export class ORMUser extends Model implements Authenticatable { + + protected static table = 'users' + + protected static key = 'user_id' + + /** The primary key of the user in the table. */ + @Field(FieldType.serial, 'user_id') + public userId!: number + + /** The unique string-identifier of the user. */ + @Field(FieldType.varchar) + public username!: string + + /** The user's first name. */ + @Field(FieldType.varchar, 'first_name') + public firstName!: string + + /** The user's last name. */ + @Field(FieldType.varchar, 'last_name') + public lastName!: string + + /** The hashed and salted password of the user. */ + @Field(FieldType.varchar, 'password_hash') + public passwordHash!: string + + /** Human-readable display name of the user. */ + getDisplayIdentifier(): string { + return `${this.firstName} ${this.lastName}` + } + + /** Unique identifier of the user. */ + getIdentifier(): AuthenticatableIdentifier { + return this.username + } + + /** Check if the provided password is valid for the user. */ + verifyPassword(password: string): Awaitable { + return bcrypt.compare(password, this.passwordHash) + } + + /** Change the user's password, hashing it. */ + async setPassword(password: string): Promise { + this.passwordHash = await bcrypt.hash(password, 10) + } + + async dehydrate(): Promise { + return this.toQueryRow() + } + + async rehydrate(state: JSONState): Promise { + await this.assumeFromSource(state) + } +} diff --git a/src/auth/orm/ORMUserRepository.ts b/src/auth/orm/ORMUserRepository.ts new file mode 100644 index 0000000..5655105 --- /dev/null +++ b/src/auth/orm/ORMUserRepository.ts @@ -0,0 +1,41 @@ +import {Authenticatable, AuthenticatableIdentifier, AuthenticatableRepository} from '../types' +import {Awaitable, Maybe} from '../../util' +import {ORMUser} from './ORMUser' +import {Singleton} from '../../di' + +/** + * A user repository implementation that looks up users stored in the database. + */ +@Singleton() +export class ORMUserRepository extends AuthenticatableRepository { + /** Look up the user by their username. */ + getByIdentifier(id: AuthenticatableIdentifier): Awaitable> { + return ORMUser.query() + .where('username', '=', id) + .first() + } + + /** + * Try to look up a user by the credentials provided. + * If a securityIdentifier is specified, look up the user by username. + * If username/password are specified, look up the user and verify the password. + * @param credentials + */ + async getByCredentials(credentials: Record): Promise> { + if ( credentials.securityIdentifier ) { + return ORMUser.query() + .where('username', '=', credentials.securityIdentifier) + .first() + } + + if ( credentials.username && credentials.password ) { + const user = await ORMUser.query() + .where('username', '=', credentials.username) + .first() + + if ( user && await user.verifyPassword(credentials.password) ) { + return user + } + } + } +} diff --git a/src/auth/types.ts b/src/auth/types.ts new file mode 100644 index 0000000..e575c9f --- /dev/null +++ b/src/auth/types.ts @@ -0,0 +1,36 @@ +import {Awaitable, JSONState, Maybe, Rehydratable} from '../util' + +/** Value that can be used to uniquely identify a user. */ +export type AuthenticatableIdentifier = string | number + +/** + * Base class for entities that can be authenticated. + */ +export abstract class Authenticatable implements Rehydratable { + + /** Get the unique identifier of the user. */ + abstract getIdentifier(): AuthenticatableIdentifier + + /** Get the human-readable identifier of the user. */ + abstract getDisplayIdentifier(): string + + abstract dehydrate(): Promise + + abstract rehydrate(state: JSONState): Awaitable +} + +/** + * Base class for a repository that stores and recalls users. + */ +export abstract class AuthenticatableRepository { + + /** Look up the user by their unique identifier. */ + abstract getByIdentifier(id: AuthenticatableIdentifier): Awaitable> + + /** + * Attempt to look up and verify a user by their credentials. + * Returns the user if the credentials are valid. + * @param credentials + */ + abstract getByCredentials(credentials: Record): Awaitable> +} diff --git a/src/index.ts b/src/index.ts index 957a53b..c51b45d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -79,3 +79,4 @@ export * from './cli' export * from './i18n' export * from './forms' export * from './orm' +export * from './auth' diff --git a/src/util/support/types.ts b/src/util/support/types.ts index f800e72..e387a9b 100644 --- a/src/util/support/types.ts +++ b/src/util/support/types.ts @@ -1 +1,5 @@ +/** Type alias for something that may or may not be wrapped in a promise. */ export type Awaitable = T | Promise + +/** Type alias for something that may be undefined. */ +export type Maybe = T | undefined