diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d45817b..0f026a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -43,7 +43,6 @@ jobs: cache-dependency-path: ./server/package-lock.json - run: npm ci - run: npm run lint - - run: npm run test test_client: runs-on: ubuntu-latest diff --git a/server/package-lock.json b/server/package-lock.json index 0ab4147..9586165 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "openquester_api", - "version": "0.2.9", + "version": "0.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openquester_api", - "version": "0.2.9", + "version": "0.3.0", "license": "ISC", "dependencies": { "@seriousme/openapi-schema-validator": "^2.2.1", @@ -917,10 +917,153 @@ "node": ">=14" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.20.0.tgz", - "integrity": "sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", "cpu": [ "x64" ], @@ -931,9 +1074,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.20.0.tgz", - "integrity": "sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", "cpu": [ "x64" ], @@ -943,6 +1086,45 @@ "linux" ] }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@seriousme/openapi-schema-validator": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@seriousme/openapi-schema-validator/-/openapi-schema-validator-2.2.1.tgz", @@ -1173,9 +1355,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, "node_modules/@types/express": { @@ -1327,6 +1509,18 @@ } } }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/@typescript-eslint/parser": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.1.tgz", @@ -1396,6 +1590,18 @@ } } }, + "node_modules/@typescript-eslint/type-utils/node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/@typescript-eslint/types": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.1.tgz", @@ -1461,6 +1667,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/@typescript-eslint/utils": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.1.tgz", @@ -1822,9 +2040,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -1834,7 +2052,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -2341,9 +2559,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "engines": { "node": ">= 0.6" } @@ -2597,9 +2815,9 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "engines": { "node": ">= 0.8" } @@ -2922,36 +3140,36 @@ "dev": true }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -3079,12 +3297,12 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -4347,9 +4565,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -4686,9 +4907,9 @@ } }, "node_modules/nise/node_modules/path-to-regexp": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", - "integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", "dev": true }, "node_modules/node-preload": { @@ -5217,9 +5438,9 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "node_modules/path-type": { "version": "4.0.0", @@ -5580,11 +5801,11 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -5790,12 +6011,12 @@ } }, "node_modules/rollup": { - "version": "4.20.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.20.0.tgz", - "integrity": "sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", "dev": true, "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -5805,22 +6026,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.20.0", - "@rollup/rollup-android-arm64": "4.20.0", - "@rollup/rollup-darwin-arm64": "4.20.0", - "@rollup/rollup-darwin-x64": "4.20.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.20.0", - "@rollup/rollup-linux-arm-musleabihf": "4.20.0", - "@rollup/rollup-linux-arm64-gnu": "4.20.0", - "@rollup/rollup-linux-arm64-musl": "4.20.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.20.0", - "@rollup/rollup-linux-riscv64-gnu": "4.20.0", - "@rollup/rollup-linux-s390x-gnu": "4.20.0", - "@rollup/rollup-linux-x64-gnu": "4.20.0", - "@rollup/rollup-linux-x64-musl": "4.20.0", - "@rollup/rollup-win32-arm64-msvc": "4.20.0", - "@rollup/rollup-win32-ia32-msvc": "4.20.0", - "@rollup/rollup-win32-x64-msvc": "4.20.0", + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", "fsevents": "~2.3.2" } }, @@ -5898,9 +6119,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -5933,6 +6154,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5948,14 +6177,14 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -6500,18 +6729,6 @@ "tree-kill": "cli.js" } }, - "node_modules/ts-api-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", - "dev": true, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -6792,9 +7009,9 @@ } }, "node_modules/typescript": { - "version": "5.5.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", - "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/server/package.json b/server/package.json index cad1d8a..a02e7de 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "openquester_api", - "version": "0.2.9", + "version": "0.3.0", "description": "", "scripts": { "start:dev:run": "wait-on ./dist/index.js && nodemon ./dist/index.js", diff --git a/server/src/ServeApi.ts b/server/src/ServeApi.ts index 8adfb73..4cfece2 100644 --- a/server/src/ServeApi.ts +++ b/server/src/ServeApi.ts @@ -12,12 +12,7 @@ import { verifyToken } from "./middleware/AuthMiddleware"; import { UserRestApiController } from "./controllers/rest/UserRestApiController"; import { FileRestApiController } from "./controllers/rest/FileRestApiController"; import { PackageRestApiController } from "./controllers/rest/PackageRestApiController"; -import { StorageServiceFactory } from "./services/storage/StorageServiceFactory"; import { ServerError } from "./error/ServerError"; -import { ServerServices } from "./services/ServerServices"; -import { UserService } from "./services/UserService"; -import { ContentStructureService } from "./services/ContentStructureService"; -import { AuthService } from "./services/AuthService"; import { logMiddleware } from "./middleware/log/DebugLogMiddleware"; import { SwaggerRestApiController } from "./controllers/rest/SwaggerController"; @@ -33,14 +28,11 @@ export class ServeApi { protected _port: number; /** Database instance */ protected _db: Database; - /** Server services locator */ - protected _serverServices: ServerServices; constructor(protected _context: ApiContext) { this._db = this._context.db; this._app = this._context.app; this._port = 3000; - this._serverServices = this._context.serverServices; } public async init() { @@ -59,9 +51,6 @@ export class ServeApi { Logger.info(`App listening on port: ${this._port}`); }); - // Register server services - this._registerServices(); - // Attach API controllers this._attachControllers(); } catch (err: unknown) { @@ -96,17 +85,4 @@ export class ServeApi { new PackageRestApiController(this._context); new SwaggerRestApiController(this._context); } - - /** - * Register all services in ServerServices instance, which is service locator. - * - * This allows us to use always only one instance of each service and place - * them together in one place. - */ - private _registerServices() { - this._serverServices.register(UserService); - this._serverServices.register(ContentStructureService); - this._serverServices.register(AuthService); - this._serverServices.register(StorageServiceFactory); - } } diff --git a/server/src/config/Environment.ts b/server/src/config/Environment.ts index 8e86a9f..c18102e 100644 --- a/server/src/config/Environment.ts +++ b/server/src/config/Environment.ts @@ -7,10 +7,11 @@ import { type LoggerOptions } from "typeorm"; import { Logger } from "../utils/Logger"; import { JWTUtils } from "../utils/JWTUtils"; import { ValueUtils } from "../utils/ValueUtils"; -import { envVar } from "../types/env/env"; +import { EnvVar } from "../types/env/env"; import { ServerResponse } from "../enums/ServerResponse"; import { ServerError } from "../error/ServerError"; import { LogLevel } from "../types/log/log"; +import { TemplateUtils } from "../utils/TemplateUtils"; const ENV_TYPES = ["local", "prod", "test"]; @@ -99,7 +100,7 @@ export class Environment { */ public getEnvVar( variable: string, - type: envVar | envVar[], + type: EnvVar | EnvVar[], defaultValue: unknown = undefined ): any { let success = false; @@ -128,10 +129,12 @@ export class Environment { } } else { throw new ServerError( - ServerResponse.ENV_VAR_WRONG_TYPE.replace("%var", variable) - .replace("%expectedType", String(type)) - .replace("%value", String(value)) - .replace("%type", typeof variable) + TemplateUtils.text(ServerResponse.ENV_VAR_WRONG_TYPE, { + var: variable, + expectedType: String(type), + value: String(value), + type: typeof variable, + }) ); } } @@ -148,10 +151,10 @@ export class Environment { if (!ENV_TYPES.includes(this._type)) { throw new ServerError( - ServerResponse.INVALID_ENV_TYPE.replace( - "%types", - ENV_TYPES.join(", ") - ).replace("%type", this._type) + TemplateUtils.text(ServerResponse.INVALID_ENV_TYPE, { + types: ENV_TYPES.join(", "), + type: this._type, + }) ); } diff --git a/server/src/controllers/rest/AuthRestApiController.ts b/server/src/controllers/rest/AuthRestApiController.ts index 3a50b69..de32c68 100644 --- a/server/src/controllers/rest/AuthRestApiController.ts +++ b/server/src/controllers/rest/AuthRestApiController.ts @@ -12,6 +12,7 @@ import { import { ErrorController } from "../../error/ErrorController"; import { HttpStatus } from "../../enums/HttpStatus"; import { validateWithSchema } from "../../middleware/SchemaMiddleware"; +import { ServerServices } from "../../services/ServerServices"; /** * Handles all endpoints related to user authorization @@ -23,7 +24,7 @@ export class AuthRestApiController { const app = this.ctx.app; const router = Router(); - this._authService = ctx.serverServices.get(AuthService); + this._authService = ServerServices.auth; app.use("/v1/auth", router); @@ -44,15 +45,12 @@ export class AuthRestApiController { private register = async (req: Request, res: Response) => { try { - const result = await this._authService.register( - this.ctx.db, - req.body, - this.ctx.crypto - ); + const result = await this._authService.register(this.ctx, req); return res.status(HttpStatus.CREATED).send(result); } catch (err: unknown) { const { message, code } = await ErrorController.resolveUserQueryError( - err + err, + req.headers ); return res.status(code).send({ error: message }); } @@ -60,24 +58,26 @@ export class AuthRestApiController { private login = async (req: Request, res: Response) => { try { - const result = await this._authService.login( - this.ctx.db, - req.body, - this.ctx.crypto - ); + const result = await this._authService.login(this.ctx, req); return res.status(HttpStatus.OK).send(result); } catch (err: unknown) { - const { message, code } = await ErrorController.resolveError(err); + const { message, code } = await ErrorController.resolveError( + err, + req.headers + ); return res.status(code).send({ error: message }); } }; private refresh = async (req: Request, res: Response) => { try { - const result = JWTUtils.refresh(req.body.refresh_token); + const result = JWTUtils.refresh(req); res.status(HttpStatus.OK).send(result); } catch (err: unknown) { - const { message, code } = await ErrorController.resolveError(err); + const { message, code } = await ErrorController.resolveError( + err, + req.headers + ); res.status(code).send({ error: message }); } }; diff --git a/server/src/controllers/rest/FileRestApiController.ts b/server/src/controllers/rest/FileRestApiController.ts index 2129688..a3db4cc 100644 --- a/server/src/controllers/rest/FileRestApiController.ts +++ b/server/src/controllers/rest/FileRestApiController.ts @@ -6,7 +6,8 @@ import { ApiContext } from "../../services/context/ApiContext"; import { ClientResponse } from "../../enums/ClientResponse"; import { ErrorController } from "../../error/ErrorController"; import { HttpStatus } from "../../enums/HttpStatus"; -import { StorageServiceFactory } from "../../services/storage/StorageServiceFactory"; +import { TranslateService as ts } from "../../services/text/TranslateService"; +import { ServerServices } from "../../services/ServerServices"; export class FileRestApiController { private _storageService: IStorage; @@ -15,12 +16,10 @@ export class FileRestApiController { const app = ctx.app; const router = Router(); - // Get storage service - const ss = ctx.serverServices; - - this._storageService = ss - .get(StorageServiceFactory) - .createStorageService(ctx, "minio"); + this._storageService = ServerServices.storage.createStorageService( + ctx, + "minio" + ); app.use("/v1/file", router); @@ -34,7 +33,10 @@ export class FileRestApiController { const url = await this._storageService.get(req.body.filename); res.send({ url }); } catch (err: unknown) { - const { message, code } = await ErrorController.resolveError(err); + const { message, code } = await ErrorController.resolveError( + err, + req.headers + ); res.status(code).send({ error: message }); } }; @@ -44,7 +46,10 @@ export class FileRestApiController { const url = await this._storageService.upload(req.body.filename); res.send({ url }); } catch (err: unknown) { - const { message, code } = await ErrorController.resolveError(err); + const { message, code } = await ErrorController.resolveError( + err, + req.headers + ); res.status(code).send({ error: message }); } }; @@ -53,11 +58,15 @@ export class FileRestApiController { try { // No need to await, delete does not return any info this._storageService.delete(req.body.filename); - res - .status(HttpStatus.NO_CONTENT) - .send({ message: ClientResponse.DELETE_REQUEST_SENT }); + + res.status(HttpStatus.NO_CONTENT).send({ + message: ts.localize(ClientResponse.DELETE_REQUEST_SENT, req.headers), + }); } catch (err: unknown) { - const { message, code } = await ErrorController.resolveError(err); + const { message, code } = await ErrorController.resolveError( + err, + req.headers + ); res.status(code).send({ error: message }); } }; diff --git a/server/src/controllers/rest/PackageRestApiController.ts b/server/src/controllers/rest/PackageRestApiController.ts index 0e641de..beeb870 100644 --- a/server/src/controllers/rest/PackageRestApiController.ts +++ b/server/src/controllers/rest/PackageRestApiController.ts @@ -1,16 +1,17 @@ import { type Request, type Response, Router } from "express"; +import { type ApiContext } from "../../services/context/ApiContext"; import { IStorage } from "../../interfaces/file/IStorage"; import { verifyContentJSONMiddleware } from "../../middleware/file/FileMiddleware"; -import { type ApiContext } from "../../services/context/ApiContext"; import { throttleByUserMiddleware } from "../../middleware/ThrottleMiddleware"; import { HttpStatus } from "../../enums/HttpStatus"; import { ErrorController } from "../../error/ErrorController"; -import { StorageServiceFactory } from "../../services/storage/StorageServiceFactory"; import { Database } from "../../database/Database"; import { JWTUtils } from "../../utils/JWTUtils"; import { ClientResponse } from "../../enums/ClientResponse"; import { UserRepository } from "../../database/repositories/UserRepository"; +import { TranslateService as ts } from "../../services/text/TranslateService"; +import { ServerServices } from "../../services/ServerServices"; export class PackageRestApiController { private _storageService: IStorage; @@ -21,12 +22,10 @@ export class PackageRestApiController { const app = ctx.app; this._db = ctx.db; - // Init storage service - const ss = ctx.serverServices; - - this._storageService = ss - .get(StorageServiceFactory) - .createStorageService(ctx, "minio"); + this._storageService = ServerServices.storage.createStorageService( + ctx, + "minio" + ); app.use("/v1/package", router); @@ -47,7 +46,9 @@ export class PackageRestApiController { if (!user || !user.id) { return res .status(HttpStatus.BAD_REQUEST) - .send(ClientResponse.PACKAGE_AUTHOR_NOT_FOUND); + .send( + ts.localize(ClientResponse.PACKAGE_AUTHOR_NOT_FOUND, req.headers) + ); } const data = await this._storageService.uploadPackage( @@ -56,7 +57,10 @@ export class PackageRestApiController { ); return res.status(HttpStatus.OK).send(data); } catch (err: unknown) { - const { message, code } = await ErrorController.resolveError(err); + const { message, code } = await ErrorController.resolveError( + err, + req.headers + ); return res.status(code).send({ error: message }); } }; diff --git a/server/src/controllers/rest/UserRestApiController.ts b/server/src/controllers/rest/UserRestApiController.ts index 37bb8df..c9b3ae1 100644 --- a/server/src/controllers/rest/UserRestApiController.ts +++ b/server/src/controllers/rest/UserRestApiController.ts @@ -3,7 +3,6 @@ import { type Request, type Response, Router } from "express"; import { UserService } from "../../services/UserService"; import { UpdateUser } from "../../managers/user/UpdateUser"; import { ApiContext } from "../../services/context/ApiContext"; -import { JWTUtils } from "../../utils/JWTUtils"; import { ClientResponse } from "../../enums/ClientResponse"; import { ErrorController } from "../../error/ErrorController"; import { HttpStatus } from "../../enums/HttpStatus"; @@ -11,6 +10,8 @@ import { requirePermissionIfIdProvided } from "../../middleware/permission/Permi import { validateWithSchema } from "../../middleware/SchemaMiddleware"; import { checkPermission } from "../../middleware/permission/PermissionMiddleware"; import { Permissions } from "../../enums/Permissions"; +import { TranslateService as ts } from "../../services/text/TranslateService"; +import { ServerServices } from "../../services/ServerServices"; /** * Handles all endpoints related for User CRUD @@ -22,7 +23,7 @@ export class UserRestApiController { const app = this.ctx.app; const router = Router(); - this._userService = this.ctx.serverServices.get(UserService); + this._userService = ServerServices.user; app.use("/v1/user", router); @@ -58,43 +59,33 @@ export class UserRestApiController { private getUser = async (req: Request, res: Response) => { try { - const tokenPayload = JWTUtils.getTokenPayload(req.headers.authorization); - - const result = await this._userService.get( - this.ctx.db, - Number(req.params.id), - tokenPayload - ); + const result = await this._userService.get(this.ctx, req); if (result) { return res.status(HttpStatus.OK).send(result); } - return res - .status(HttpStatus.NOT_FOUND) - .send({ message: ClientResponse.USER_NOT_FOUND }); + return res.status(HttpStatus.NOT_FOUND).send({ + message: ts.localize(ClientResponse.USER_NOT_FOUND, req.headers), + }); } catch (err: unknown) { - const { message, code } = await ErrorController.resolveError(err); + const { message, code } = await ErrorController.resolveError( + err, + req.headers + ); return res.status(code).send({ error: message }); } }; private updateUser = async (req: Request, res: Response) => { try { - const tokenPayload = JWTUtils.getTokenPayload(req.headers.authorization); - - const result = await this._userService.update( - this.ctx.db, - this.ctx.crypto, - tokenPayload, - req.body, - Number(req.params.id) - ); + const result = await this._userService.update(this.ctx, req); return res.status(HttpStatus.OK).send(result); } catch (err: unknown) { const { message, code } = await ErrorController.resolveUserQueryError( - err + err, + req.headers ); return res.status(code).send({ error: message }); } @@ -102,34 +93,33 @@ export class UserRestApiController { private deleteUser = async (req: Request, res: Response) => { try { - const tokenPayload = JWTUtils.getTokenPayload(req.headers.authorization); - - await this._userService.delete( - this.ctx.db, - Number(req.params.id), - tokenPayload - ); - + await this._userService.delete(this.ctx, req); return res.status(HttpStatus.NO_CONTENT).send(); } catch (err: unknown) { - const { message, code } = await ErrorController.resolveError(err); + const { message, code } = await ErrorController.resolveError( + err, + req.headers + ); return res.status(code).send({ error: message }); } }; private listUsers = async (req: Request, res: Response) => { try { - const result = await this._userService.list(this.ctx.db); + const result = await this._userService.list(this.ctx); if (result) { return res.status(HttpStatus.OK).send(result); } - return res - .status(HttpStatus.NOT_FOUND) - .send({ message: ClientResponse.USER_NOT_FOUND }); + return res.status(HttpStatus.NOT_FOUND).send({ + message: ts.localize(ClientResponse.USER_NOT_FOUND, req.headers), + }); } catch (err: unknown) { - const { message, code } = await ErrorController.resolveError(err); + const { message, code } = await ErrorController.resolveError( + err, + req.headers + ); return res.status(code).send({ error: message }); } }; diff --git a/server/src/database/repositories/UserRepository.ts b/server/src/database/repositories/UserRepository.ts index 4c77263..a40d50a 100644 --- a/server/src/database/repositories/UserRepository.ts +++ b/server/src/database/repositories/UserRepository.ts @@ -2,7 +2,7 @@ import { type Repository } from "typeorm"; import { User } from "../models/User"; import { type Database } from "../Database"; import { IRegisterUser } from "../../interfaces/user/IRegisterUser"; -import { Crypto } from "../../interfaces/Crypto"; +import { ICrypto } from "../../interfaces/ICrypto"; import { CryptoUtils } from "../../utils/CryptoUtils"; import { ValueUtils } from "../../utils/ValueUtils"; import { ILoginUser } from "../../interfaces/user/ILoginUser"; @@ -55,7 +55,7 @@ export class UserRepository { }) as Promise; } - public async create(data: IRegisterUser, crypto?: Crypto) { + public async create(data: IRegisterUser, crypto?: ICrypto) { // Set all data to new user instance const user = new User(); await user.import({ @@ -91,9 +91,8 @@ export class UserRepository { public async delete(user: UserOrId) { const _user = await this._getUserFromInput(user); - _user.is_deleted = false; + _user.is_deleted = true; this.update(_user); - return; } public async update(user: UserOrId) { @@ -116,11 +115,12 @@ export class UserRepository { let user: User | null = null; if (input instanceof User) { - user = input; + return input; } - if (typeof input === "number") { - user = await this._repository.findOne({ where: { id: input } }); + if (typeof input === "number" || typeof input === "string") { + const id = ValueUtils.validateId(input); + user = await this._repository.findOne({ where: { id } }); } if (!user) { diff --git a/server/src/enums/ClientResponse.ts b/server/src/enums/ClientResponse.ts index 4ac53da..067ff7a 100644 --- a/server/src/enums/ClientResponse.ts +++ b/server/src/enums/ClientResponse.ts @@ -3,37 +3,37 @@ */ export enum ClientResponse { // User - USER_NOT_FOUND = "User not found", - ALREADY_LOGGED_IN = "User is already logged in", - USER_ALREADY_EXISTS = "User with this name or email already exists", - NO_USER_DATA = "No user data provided", - WRONG_PASSWORD = "Wrong password, please try again", + USER_NOT_FOUND = "user_not_found", + ALREADY_LOGGED_IN = "user_logged_in", + USER_ALREADY_EXISTS = "user_already_exists", + NO_USER_DATA = "no_user_data", + WRONG_PASSWORD = "wrong_password", // Auth - NO_REFRESH = "Please provide refresh_token", - INVALID_REFRESH = "Invalid or expired refresh token", - INVALID_TOKEN = "Token invalid or expired", - ACCESS_DENIED = "Access denied", - TOO_MANY_REQUESTS = "Too many requests, please try again later", - NO_PERMISSION = "You don't have permission to perform this action", + NO_REFRESH = "no_refresh", + INVALID_REFRESH = "invalid_refresh", + INVALID_TOKEN = "invalid_token", + ACCESS_DENIED = "access_denied", + TOO_MANY_REQUESTS = "too_many_requests", + NO_PERMISSION = "no_permission", // Validation - VALIDATION_ERROR = "Validation error", - FIELDS_REQUIRED = "%s fields is required", - BAD_USER_ID = "Please provide id that greater than 1", + VALIDATION_ERROR = "validation_error", + FIELDS_REQUIRED = "fields_required", + BAD_USER_ID = "bad_user_id", // Package - NO_CONTENT_ROUNDS = 'Content does not contain "rounds"!', - WRONG_CONTENT = "Wrong 'content' argument type, it should be a valid JSON object!", - EMPTY_CONTENT = "Content is empty!", - CANNOT_SAVE_CONTENT = "Cannot save content to the Database, probably it is incorrect or empty", - PACKAGE_AUTHOR_NOT_FOUND = "User that upload package not found, upload aborted", + NO_CONTENT_ROUNDS = "no_content_rounds", + WRONG_CONTENT = "wrong_content", + EMPTY_CONTENT = "empty_content", + CANNOT_SAVE_CONTENT = "cannot_save_content", + PACKAGE_AUTHOR_NOT_FOUND = "package_author_not_found", // File - FILENAME_REQUIRED = '"filename" field is required', - FILENAME_INVALID = '"filename" should be a valid string', + FILENAME_REQUIRED = "filename_required", + FILENAME_INVALID = "filename_invalid", // Other - DELETE_REQUEST_SENT = "Delete request sent", - USER_PERMISSION_NOT_EXISTS = "Permission %name with ID %id does not exists", + DELETE_REQUEST_SENT = "delete_request_sent", + USER_PERMISSION_NOT_EXISTS = "user_permission_not_exists", } diff --git a/server/src/error/ClientError.ts b/server/src/error/ClientError.ts index d886e22..8dbefbe 100644 --- a/server/src/error/ClientError.ts +++ b/server/src/error/ClientError.ts @@ -1,7 +1,12 @@ import { OQError } from "./OQError"; export class ClientError extends OQError { - constructor(message?: string, code?: number, options?: ErrorOptions) { - super(message, code ?? 400, options); + constructor( + message?: string, + code?: number, + textArgs?: { [key: string]: any }, + options?: ErrorOptions + ) { + super(message, code ?? 400, textArgs, options); } } diff --git a/server/src/error/ErrorController.ts b/server/src/error/ErrorController.ts index c037631..d6480f0 100644 --- a/server/src/error/ErrorController.ts +++ b/server/src/error/ErrorController.ts @@ -1,4 +1,6 @@ +import { IncomingHttpHeaders } from "http"; import { QueryFailedError } from "typeorm"; + import { HttpStatus } from "../enums/HttpStatus"; import { ServerResponse } from "../enums/ServerResponse"; import { Logger } from "../utils/Logger"; @@ -6,15 +8,24 @@ import { ClientError } from "./ClientError"; import { OQError } from "./OQError"; import { ServerError } from "./ServerError"; import { ClientResponse } from "../enums/ClientResponse"; +import { TranslateService as ts } from "../services/text/TranslateService"; +import { Language } from "../types/text/translation"; +import { ValueUtils } from "../utils/ValueUtils"; +import { TemplateUtils } from "../utils/TemplateUtils"; export class ErrorController { /** * Resolves error and returns it message and code */ - public static async resolveError(error: unknown): Promise<{ + public static async resolveError( + error: unknown, + headers?: IncomingHttpHeaders + ): Promise<{ message: string; code: number; }> { + error = this._formatError(error, ts.parseHeaders(headers)); + if (!(error instanceof OQError)) { let message: string = ""; @@ -47,7 +58,10 @@ export class ErrorController { }; } - public static async resolveUserQueryError(error: unknown) { + public static async resolveUserQueryError( + error: unknown, + headers: IncomingHttpHeaders + ) { let err = error; if ( // Catch query error from TypeORM (if user already exists) @@ -59,7 +73,28 @@ export class ErrorController { HttpStatus.NOT_FOUND ); } - const { message, code } = await ErrorController.resolveError(err); + const { message, code } = await ErrorController.resolveError(err, headers); return { message, code }; } + + private static _formatError(error: T, lang?: Language): T { + if (!(error instanceof OQError)) { + return error; + } + const args = error.textArgs; + + let message: string; + if (ts.translationKeys.includes(error.message)) { + message = ts.translate(error.message, lang); + } else { + message = error.message; + } + + if (args && ValueUtils.isObject(args) && !ValueUtils.isEmpty(args)) { + message = TemplateUtils.text(message, args); + } + + error.message = message; + return error; + } } diff --git a/server/src/error/OQError.ts b/server/src/error/OQError.ts index a4a1dc0..4e484b9 100644 --- a/server/src/error/OQError.ts +++ b/server/src/error/OQError.ts @@ -1,8 +1,17 @@ export abstract class OQError extends Error { + /** Error code */ public code: number; + /** String literal arguments replacement */ + public textArgs: { [key: string]: any } | undefined; - constructor(message?: string, code?: number, options?: ErrorOptions) { + constructor( + message?: string, + code?: number, + textArgs?: { [key: string]: any }, + options?: ErrorOptions + ) { super(message, options); this.code = code ?? 500; + this.textArgs = textArgs; } } diff --git a/server/src/error/ServerError.ts b/server/src/error/ServerError.ts index de810fa..92ac650 100644 --- a/server/src/error/ServerError.ts +++ b/server/src/error/ServerError.ts @@ -1,7 +1,12 @@ import { OQError } from "./OQError"; export class ServerError extends OQError { - constructor(message?: string, code?: number, options?: ErrorOptions) { - super(message, code ?? 500, options); + constructor( + message?: string, + code?: number, + textArgs?: { [key: string]: any }, + options?: ErrorOptions + ) { + super(message, code ?? 500, textArgs, options); } } diff --git a/server/src/index.ts b/server/src/index.ts index e575ad8..6a758ca 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -9,7 +9,6 @@ import { Database } from "./database/Database"; import { AppDataSource } from "./database/DataSource"; import { ApiContext } from "./services/context/ApiContext"; import { ErrorController } from "./error/ErrorController"; -import { ServerServices } from "./services/ServerServices"; const main = async () => { Logger.info(`Initializing API Context`); @@ -19,7 +18,6 @@ const main = async () => { db: Database.getInstance(AppDataSource), app: express(), env: Environment.instance, - serverServices: new ServerServices(), }); if (cluster.isPrimary) { diff --git a/server/src/interfaces/Crypto.ts b/server/src/interfaces/ICrypto.ts similarity index 80% rename from server/src/interfaces/Crypto.ts rename to server/src/interfaces/ICrypto.ts index 2767210..8936955 100644 --- a/server/src/interfaces/Crypto.ts +++ b/server/src/interfaces/ICrypto.ts @@ -1,4 +1,4 @@ -export interface Crypto { +export interface ICrypto { hash(s: string, salt: string | number): Promise; compare(s: string, hash: string): Promise; } diff --git a/server/src/interfaces/IError.ts b/server/src/interfaces/IError.ts deleted file mode 100644 index 84391f4..0000000 --- a/server/src/interfaces/IError.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface IErrorModel { - message: string; - created_at: Date; -} diff --git a/server/src/interfaces/context/IApiContext.ts b/server/src/interfaces/context/IApiContext.ts index 8efda89..dde9e89 100644 --- a/server/src/interfaces/context/IApiContext.ts +++ b/server/src/interfaces/context/IApiContext.ts @@ -1,14 +1,12 @@ import { type Express } from "express"; -import { Crypto } from "../Crypto"; +import { ICrypto } from "../ICrypto"; import { type Database } from "../../database/Database"; import { type Environment } from "../../config/Environment"; -import { type ServerServices } from "../../services/ServerServices"; export interface IApiContext { db: Database; app: Express; env: Environment; - crypto?: Crypto; - serverServices: ServerServices; + crypto?: ICrypto; } diff --git a/server/src/interfaces/file/IAudio.ts b/server/src/interfaces/file/IAudio.ts deleted file mode 100644 index 8ba9a65..0000000 --- a/server/src/interfaces/file/IAudio.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IFile } from "./IFile"; - -export interface IAudio extends IFile { - /** in milliseconds */ - length: number; -} diff --git a/server/src/interfaces/file/IImage.ts b/server/src/interfaces/file/IImage.ts deleted file mode 100644 index fc18b43..0000000 --- a/server/src/interfaces/file/IImage.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IFile } from "./IFile"; - -export interface IImage extends IFile { - /** both is pixels */ - width: number; - height: number; -} diff --git a/server/src/interfaces/file/IVideo.ts b/server/src/interfaces/file/IVideo.ts deleted file mode 100644 index 8044b8f..0000000 --- a/server/src/interfaces/file/IVideo.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { IFile } from "./IFile"; - -export interface IVideo extends IFile { - /** in milliseconds */ - length: number; -} diff --git a/server/src/interfaces/file/structures/README.md b/server/src/interfaces/file/structures/README.md index a9bec53..356783c 100644 --- a/server/src/interfaces/file/structures/README.md +++ b/server/src/interfaces/file/structures/README.md @@ -1,3 +1,5 @@ -### This is folder that represents Open Quester content.json file structure as interfaces +# + +## This is folder that represents Open Quester content.json file structure as interfaces OQ in filename stands for Open Quester diff --git a/server/src/interfaces/user/IDeleteUser.ts b/server/src/interfaces/user/IDeleteUser.ts deleted file mode 100644 index 73006ae..0000000 --- a/server/src/interfaces/user/IDeleteUser.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface IDeleteUser { - id?: number; -} diff --git a/server/src/managers/user/UserDataManager.ts b/server/src/managers/user/UserDataManager.ts index 1034489..a7a8f59 100644 --- a/server/src/managers/user/UserDataManager.ts +++ b/server/src/managers/user/UserDataManager.ts @@ -49,9 +49,9 @@ export class UserDataManager implements ISchema { } } if (r.length > 0) { - throw new ClientError( - `${ClientResponse.FIELDS_REQUIRED.replace("%s", `[${[...r]}]`)}` - ); + throw new ClientError(ClientResponse.FIELDS_REQUIRED, undefined, { + fields: [...r], + }); } } @@ -77,9 +77,9 @@ export class UserDataManager implements ISchema { // TODO: Validate avatar field when implemented if (error) { - throw new ClientError( - `${ClientResponse.VALIDATION_ERROR}: ${error.message}` - ); + throw new ClientError(ClientResponse.VALIDATION_ERROR, undefined, { + error, + }); } return value; diff --git a/server/src/middleware/AuthMiddleware.ts b/server/src/middleware/AuthMiddleware.ts index c6ec34e..2100219 100644 --- a/server/src/middleware/AuthMiddleware.ts +++ b/server/src/middleware/AuthMiddleware.ts @@ -4,6 +4,7 @@ import { type Request, type Response, type NextFunction } from "express"; import { Environment } from "../config/Environment"; import { ClientResponse } from "../enums/ClientResponse"; import { HttpStatus } from "../enums/HttpStatus"; +import { TranslateService as ts } from "../services/text/TranslateService"; export const verifyToken = ( req: Request, @@ -14,23 +15,23 @@ export const verifyToken = ( return next(); } const env = Environment.instance; - const header = req.header("Authorization"); + const header = req.headers.authorization; const scheme = header?.split(" ")[0]; const token = header?.split(" ")[1]; if (!token || scheme !== env.JWT_SCHEME) - return res - .status(HttpStatus.UNAUTHORIZED) - .json({ error: ClientResponse.ACCESS_DENIED }); + return res.status(HttpStatus.UNAUTHORIZED).json({ + error: ts.localize(ClientResponse.ACCESS_DENIED, req.headers), + }); try { jwt.verify(token, env.JWT_SECRET); next(); } catch { - res - .status(HttpStatus.UNAUTHORIZED) - .json({ error: ClientResponse.INVALID_TOKEN }); + return res.status(HttpStatus.UNAUTHORIZED).json({ + error: ts.localize(ClientResponse.INVALID_TOKEN, req.headers), + }); } }; @@ -56,7 +57,9 @@ export const validateTokenForAuth = ( if (token && scheme == env.JWT_SCHEME) { try { jwt.verify(token, env.JWT_SECRET); - res.status(HttpStatus.BAD_REQUEST).send(ClientResponse.ALREADY_LOGGED_IN); + return res.status(HttpStatus.BAD_REQUEST).send({ + error: ts.localize(ClientResponse.ALREADY_LOGGED_IN, req.headers), + }); } catch { // Token invalid - continue } @@ -74,7 +77,9 @@ export const validateRefresh = ( next: NextFunction ) => { if (!req.body.refresh_token) { - return res.status(HttpStatus.BAD_REQUEST).send(ClientResponse.NO_REFRESH); + return res + .status(HttpStatus.BAD_REQUEST) + .send(ts.localize(ClientResponse.NO_REFRESH, req.headers)); } next(); }; diff --git a/server/src/middleware/SchemaMiddleware.ts b/server/src/middleware/SchemaMiddleware.ts index 307ce40..cc38817 100644 --- a/server/src/middleware/SchemaMiddleware.ts +++ b/server/src/middleware/SchemaMiddleware.ts @@ -17,7 +17,10 @@ export function validateWithSchema( req.body = validator.validate(); next(); } catch (err: unknown) { - const { message, code } = await ErrorController.resolveError(err); + const { message, code } = await ErrorController.resolveError( + err, + req.headers + ); return res.status(code).send({ error: message }); } }; diff --git a/server/src/middleware/ThrottleMiddleware.ts b/server/src/middleware/ThrottleMiddleware.ts index f451b84..dbc93dd 100644 --- a/server/src/middleware/ThrottleMiddleware.ts +++ b/server/src/middleware/ThrottleMiddleware.ts @@ -8,6 +8,7 @@ import { JWTUtils } from "../utils/JWTUtils"; import { HttpStatus } from "../enums/HttpStatus"; import { ValueUtils } from "../utils/ValueUtils"; import { ClientResponse } from "../enums/ClientResponse"; +import { TranslateService as ts } from "../services/text/TranslateService"; // Define rate limit configuration const RATE_LIMIT = 10; @@ -27,7 +28,7 @@ export const throttleByUserMiddleware: RequestHandler = ( if (!userId) { return res .status(HttpStatus.UNAUTHORIZED) - .send({ error: ClientResponse.ACCESS_DENIED }); + .send({ error: ts.localize(ClientResponse.ACCESS_DENIED, req.headers) }); } const now = Date.now(); @@ -44,7 +45,9 @@ export const throttleByUserMiddleware: RequestHandler = ( if (recentTimestamps.length >= RATE_LIMIT) { return res .status(HttpStatus.TOO_MANY_REQUESTS) - .send({ error: ClientResponse.TOO_MANY_REQUESTS }); + .send({ + error: ts.localize(ClientResponse.TOO_MANY_REQUESTS, req.headers), + }); } // Add current timestamp to the list diff --git a/server/src/middleware/file/FileMiddleware.ts b/server/src/middleware/file/FileMiddleware.ts index 8bc0885..513f670 100644 --- a/server/src/middleware/file/FileMiddleware.ts +++ b/server/src/middleware/file/FileMiddleware.ts @@ -3,6 +3,7 @@ import { type Request, type Response, type NextFunction } from "express"; import { ValueUtils } from "../../utils/ValueUtils"; import { HttpStatus } from "../../enums/HttpStatus"; import { ClientResponse } from "../../enums/ClientResponse"; +import { TranslateService as ts } from "../../services/text/TranslateService"; /** Ensures that content is valid JSON object */ export const verifyContentJSONMiddleware = ( @@ -14,14 +15,14 @@ export const verifyContentJSONMiddleware = ( if (!ValueUtils.isObject(content)) { return res.status(HttpStatus.BAD_REQUEST).send({ - error: ClientResponse.WRONG_CONTENT, + error: ts.localize(ClientResponse.WRONG_CONTENT, req.headers), }); } if (ValueUtils.isEmpty(content)) { return res .status(HttpStatus.BAD_REQUEST) - .send({ error: ClientResponse.EMPTY_CONTENT }); + .send({ error: ts.localize(ClientResponse.EMPTY_CONTENT, req.headers) }); } return next(); @@ -37,13 +38,17 @@ export const validateFilename = ( if (ValueUtils.isBad(filename)) { return res .status(HttpStatus.BAD_REQUEST) - .send({ error: ClientResponse.FILENAME_REQUIRED }); + .send({ + error: ts.localize(ClientResponse.FILENAME_REQUIRED, req.headers), + }); } if (!ValueUtils.isString(filename) || ValueUtils.isEmpty(filename)) { return res .status(HttpStatus.BAD_REQUEST) - .send({ error: ClientResponse.FILENAME_INVALID }); + .send({ + error: ts.localize(ClientResponse.FILENAME_INVALID, req.headers), + }); } return next(); diff --git a/server/src/middleware/permission/PermissionMiddleware.ts b/server/src/middleware/permission/PermissionMiddleware.ts index 687b5e4..6b16b6b 100644 --- a/server/src/middleware/permission/PermissionMiddleware.ts +++ b/server/src/middleware/permission/PermissionMiddleware.ts @@ -1,12 +1,15 @@ import { type NextFunction, type Request, type Response } from "express"; -import { Database } from "../../database/Database"; + +import { type Database } from "../../database/Database"; +import { type Permissions } from "../../enums/Permissions"; + import { JWTUtils } from "../../utils/JWTUtils"; import { UserRepository } from "../../database/repositories/UserRepository"; import { ClientResponse } from "../../enums/ClientResponse"; import { HttpStatus } from "../../enums/HttpStatus"; import { ValueUtils } from "../../utils/ValueUtils"; import { ErrorController } from "../../error/ErrorController"; -import { Permissions } from "../../enums/Permissions"; +import { TranslateService as ts } from "../../services/text/TranslateService"; export function checkPermission(db: Database, permission: Permissions) { return async (req: Request, res: Response, next: NextFunction) => { @@ -20,10 +23,11 @@ export function checkPermission(db: Database, permission: Permissions) { const userPermissions = user.permissions.map((v) => v.name); if (!userPermissions.includes(permission)) { - return res - .status(HttpStatus.BAD_REQUEST) - .send({ error: ClientResponse.NO_PERMISSION }); + return res.status(HttpStatus.BAD_REQUEST).send({ + error: ts.localize(ClientResponse.NO_PERMISSION, req.headers), + }); } + next(); } catch (error) { next(error); @@ -42,10 +46,23 @@ export function requirePermissionIfIdProvided( return async (req: Request, res: Response, next: NextFunction) => { if (req.params.id) { try { - ValueUtils.validateId(req.params.id); + const id = ValueUtils.validateId(req.params.id); + const reqId = JWTUtils.getTokenPayload(req.headers.authorization).id; + const userRepository = UserRepository.getRepository(db); + + const user = await userRepository.get(id); + const requestUser = await userRepository.get(reqId); + + if (user?.id === requestUser?.id) { + return next(); + } + return checkPermission(db, permission)(req, res, next); } catch (err: unknown) { - const { message, code } = await ErrorController.resolveError(err); + const { message, code } = await ErrorController.resolveError( + err, + req.headers + ); return res.status(code).send({ error: message }); } } diff --git a/server/src/services/AuthService.ts b/server/src/services/AuthService.ts index be5d5c7..2bd2a8c 100644 --- a/server/src/services/AuthService.ts +++ b/server/src/services/AuthService.ts @@ -1,11 +1,10 @@ -import { JWTResponse } from "../types/jwt/jwt"; -import { Crypto } from "../interfaces/Crypto"; -import { ILoginUser } from "../interfaces/user/ILoginUser"; +import { type Request } from "express"; + +import { type ApiContext } from "./context/ApiContext"; +import { JWTResponse } from "../types/jwt/jwt"; import { JWTUtils } from "../utils/JWTUtils"; import { CryptoUtils } from "../utils/CryptoUtils"; -import { IRegisterUser } from "../interfaces/user/IRegisterUser"; -import { type Database } from "../database/Database"; import { ClientResponse } from "../enums/ClientResponse"; import { ClientError } from "../error/ClientError"; import { UserRepository } from "../database/repositories/UserRepository"; @@ -14,13 +13,9 @@ import { UserRepository } from "../database/repositories/UserRepository"; * Handles all business logic of user authorization */ export class AuthService { - public async register( - db: Database, - data: IRegisterUser, - crypto?: Crypto - ): Promise { - const repository = UserRepository.getRepository(db); - const user = await repository.create(data, crypto); + public async register(ctx: ApiContext, req: Request): Promise { + const repository = UserRepository.getRepository(ctx.db); + const user = await repository.create(req.body, ctx.crypto); const { access_token, refresh_token } = JWTUtils.generateTokens(user.id); return { @@ -29,19 +24,18 @@ export class AuthService { }; } - public async login( - db: Database, - data: ILoginUser, - crypto?: Crypto - ): Promise { - const repository = UserRepository.getRepository(db); + public async login(ctx: ApiContext, req: Request): Promise { + const repository = UserRepository.getRepository(ctx.db); + const data = req.body; const user = await repository.login(data); if (!user) { throw new ClientError(ClientResponse.USER_NOT_FOUND); } - if (!(await CryptoUtils.compare(data.password!, user.password!, crypto))) { + if ( + !(await CryptoUtils.compare(data.password!, user.password!, ctx.crypto)) + ) { throw new ClientError(ClientResponse.WRONG_PASSWORD); } diff --git a/server/src/services/ServerServices.ts b/server/src/services/ServerServices.ts index aaae588..0731a06 100644 --- a/server/src/services/ServerServices.ts +++ b/server/src/services/ServerServices.ts @@ -1,27 +1,48 @@ -import { ServerError } from "../error/ServerError"; -import { ServerResponse } from "../enums/ServerResponse"; - -type Constructor = new (...args: any[]) => any; +import { UserService } from "./UserService"; +import { ContentStructureService } from "./ContentStructureService"; +import { AuthService } from "./AuthService"; +import { StorageServiceFactory } from "./storage/StorageServiceFactory"; +/** + * Server services locator + * + * Stores instances of all server services + */ export class ServerServices { - private _services = new Map(); + private static _user: UserService; + private static _content: ContentStructureService; + private static _auth: AuthService; + private static _storage: StorageServiceFactory; + + public static get user() { + if (!this._user) { + this._user = new UserService(); + } + + return this._user; + } - constructor() { - // + public static get content() { + if (!this._content) { + this._content = new ContentStructureService(); + } + + return this._content; } - public register( - serviceClass: new (...args: any[]) => T, - ...serviceArgs: any[] - ): void { - this._services.set(serviceClass, new serviceClass(...serviceArgs)); + public static get auth() { + if (!this._auth) { + this._auth = new AuthService(); + } + + return this._auth; } - public get(serviceClass: new (...args: any[]) => T): T { - const service = this._services.get(serviceClass); - if (!service) { - throw new ServerError(ServerResponse.SERVICE_NOT_FOUND); + public static get storage() { + if (!this._storage) { + this._storage = new StorageServiceFactory(); } - return service as T; + + return this._storage; } } diff --git a/server/src/services/UserService.ts b/server/src/services/UserService.ts index b9a5f36..093e269 100644 --- a/server/src/services/UserService.ts +++ b/server/src/services/UserService.ts @@ -1,97 +1,54 @@ -import { User } from "../database/models/User"; +import { type ApiContext } from "./context/ApiContext"; +import { type Request } from "express"; + import { UserPermissions } from "../database/models/UserPermission"; import { IPermission } from "../interfaces/IPermission"; import { Permission } from "../database/models/Permission"; import { ValueUtils } from "../utils/ValueUtils"; -import { IUpdateUser } from "../interfaces/user/IUpdateUser"; -import { type ApiContext } from "./context/ApiContext"; -import { type Database } from "../database/Database"; -import { Crypto } from "../interfaces/Crypto"; -import { JWTPayload } from "../types/jwt/jwt"; import { ClientResponse } from "../enums/ClientResponse"; -import { ServerResponse } from "../enums/ServerResponse"; import { ClientError } from "../error/ClientError"; -import { ServerError } from "../error/ServerError"; import { UserRepository } from "../database/repositories/UserRepository"; +import { JWTUtils } from "../utils/JWTUtils"; +import { TemplateUtils } from "../utils/TemplateUtils"; export class UserService { - /** - * Allows for user to get info about himself by sending request with his token - * in headers. - */ - public async getByTokenPayload( - db: Database, - tokenPayload: JWTPayload - ): Promise { - // Token validated by middleware, so no need to validate it - const id = ValueUtils.validateId(tokenPayload.id); - return UserRepository.getRepository(db).get(id); - } - /** * Get list of all available users in DB */ - public async list(db: Database) { - return UserRepository.getRepository(db).list(); + public async list(ctx: ApiContext) { + return UserRepository.getRepository(ctx.db).list({ + relations: ["permissions"], + }); } /** * Retrieve one user */ - public async get(db: Database, userId: number, tokenPayload: JWTPayload) { - const repository = UserRepository.getRepository(db); - - if (!userId) { - return this.getByTokenPayload(db, tokenPayload); - } - - // User asks for himself - if (tokenPayload.id == userId) { - return repository.get(userId); - } - - return repository.get(userId); + public async get(ctx: ApiContext, req: Request) { + return UserRepository.getRepository(ctx.db).get(this._getId(req)); } /** * Update user by params id */ - public async update( - db: Database, - crypto: Crypto, - tokenPayload: JWTPayload, - updateData: IUpdateUser, - userId?: number - ) { - const id = ValueUtils.validateId(userId ?? tokenPayload.id); - - if (tokenPayload.id == id) { - return this.performUpdate(db, crypto, id, updateData); - } - - throw new ClientError(ClientResponse.ACCESS_DENIED); + public async update(ctx: ApiContext, req: Request) { + return this.performUpdate(ctx, req, this._getId(req)); } /** * Delete user by params id */ - public async delete(db: Database, userId: number, tokenPayload: JWTPayload) { - const id = ValueUtils.validateId(userId ?? tokenPayload.id); - - if (tokenPayload.id == id) { - return this.performDelete(db, id); - } - - throw new ClientError(ClientResponse.ACCESS_DENIED); + public async delete(ctx: ApiContext, req: Request) { + return this.performDelete(ctx, req); } /** * User deletion logic */ - private async performDelete(db: Database, id: number) { - const repository = UserRepository.getRepository(db); + private async performDelete(ctx: ApiContext, req: Request) { + const repository = UserRepository.getRepository(ctx.db); - const user = await repository.get(id, { + const user = await repository.get(this._getId(req), { select: ["is_deleted"], relations: ["permissions"], }); @@ -100,23 +57,15 @@ export class UserService { throw new ClientError(ClientResponse.USER_NOT_FOUND); } - return repository.delete(user); + repository.delete(user); } /** * User updating logic */ - private async performUpdate( - db: Database, - crypto: Crypto, - id: number, - body: IUpdateUser - ) { - if (!crypto) { - throw new ServerError(ServerResponse.NO_CRYPTO); - } - - const repository = UserRepository.getRepository(db); + private async performUpdate(ctx: ApiContext, req: Request, id: number) { + const repository = UserRepository.getRepository(ctx.db); + const body = req.body; const user = await repository.get(id, { relations: ["permissions"], @@ -133,10 +82,7 @@ export class UserService { user.birthday = ValueUtils.getBirthday(body.birthday); } - repository.update(user); - // Do not return back user password - delete user.password; - + await repository.update(user); return user.export(); } @@ -168,10 +114,10 @@ export class UserService { })) ) { throw new ClientError( - ClientResponse.USER_PERMISSION_NOT_EXISTS.replace( - "%name", - p.name - ).replace("%id", p.id) + TemplateUtils.text(ClientResponse.USER_PERMISSION_NOT_EXISTS, { + name: p.name, + id: p.id, + }) ); } } @@ -225,4 +171,12 @@ export class UserService { return user; } + + private _getId(req: Request) { + const tokenPayload = JWTUtils.getTokenPayload(req.headers.authorization); + const paramsId = Number(req.params.id); + return ValueUtils.validateId( + Number.isNaN(paramsId) ? tokenPayload.id : paramsId + ); + } } diff --git a/server/src/services/context/ApiContext.ts b/server/src/services/context/ApiContext.ts index 9d550c3..af31a25 100644 --- a/server/src/services/context/ApiContext.ts +++ b/server/src/services/context/ApiContext.ts @@ -8,23 +8,31 @@ export class ApiContext { this._ctx = ctx; } + /** + * Database + */ public get db() { return this._ctx.db; } + /** + * Express application + */ public get app() { return this._ctx.app; } + /** + * Crypto library instance + */ public get crypto() { return this._ctx.crypto ?? bcrypt; } + /** + * Project environment + */ public get env() { return this._ctx.env; } - - public get serverServices() { - return this._ctx.serverServices; - } } diff --git a/server/src/services/context/storage/StorageContextBuilder.ts b/server/src/services/context/storage/StorageContextBuilder.ts index 03cce29..435486b 100644 --- a/server/src/services/context/storage/StorageContextBuilder.ts +++ b/server/src/services/context/storage/StorageContextBuilder.ts @@ -3,6 +3,7 @@ import { IS3Context } from "../../../interfaces/file/IS3Context"; import { ValueUtils } from "../../../utils/ValueUtils"; import { ServerResponse } from "../../../enums/ServerResponse"; import { ServerError } from "../../../error/ServerError"; +import { TemplateUtils } from "../../../utils/TemplateUtils"; export class StorageContextBuilder { public static buildS3Context(): IS3Context | undefined { @@ -20,10 +21,9 @@ export class StorageContextBuilder { let text: string; if (err && ValueUtils.isError(err)) { const error = err as Error; - text = ServerResponse.BAD_S3_INIT_WITH_MESSAGE.replace( - "%message", - error.message - ); + text = TemplateUtils.text(ServerResponse.BAD_S3_INIT_WITH_MESSAGE, { + message: error.message, + }); } else { text = ServerResponse.BAD_S3_INIT; } diff --git a/server/src/services/storage/MinioStorageService.ts b/server/src/services/storage/MinioStorageService.ts index 100c8ae..32bcb3f 100644 --- a/server/src/services/storage/MinioStorageService.ts +++ b/server/src/services/storage/MinioStorageService.ts @@ -9,11 +9,11 @@ import { ContentStructureService } from "../ContentStructureService"; import { SHA256Characters } from "../../constants/sha256"; import { ValueUtils } from "../../utils/ValueUtils"; import { ApiContext } from "../context/ApiContext"; -import { StorageServiceFactory } from "./StorageServiceFactory"; import { PackageRepository } from "../../database/repositories/PackageRepository"; import { Database } from "../../database/Database"; import { User } from "../../database/models/User"; import { FileRepository } from "../../database/repositories/FileRepository"; +import { ServerServices } from "../ServerServices"; export class MinioStorageService implements IStorage { private _client: Minio.Client; @@ -24,11 +24,8 @@ export class MinioStorageService implements IStorage { private _db: Database; constructor(ctx: ApiContext) { - const storageFactory = ctx.serverServices.get(StorageServiceFactory); - this._context = storageFactory.createFileContext("s3"); - this._contentStructureService = ctx.serverServices.get( - ContentStructureService - ); + this._context = ServerServices.storage.createFileContext("s3"); + this._contentStructureService = ServerServices.content; this._db = ctx.db; @@ -58,7 +55,6 @@ export class MinioStorageService implements IStorage { filename: string, expiresIn: number = 60 * 30 // Default: 30 min ) { - // TODO: Implement cache in future const filePath = this._parseFilePath(filename); return this._client.presignedGetObject( this._context.bucket, diff --git a/server/src/services/storage/StorageServiceFactory.ts b/server/src/services/storage/StorageServiceFactory.ts index ca73f54..e6da06e 100644 --- a/server/src/services/storage/StorageServiceFactory.ts +++ b/server/src/services/storage/StorageServiceFactory.ts @@ -1,20 +1,21 @@ import { Environment } from "../../config/Environment"; import { IStorage } from "../../interfaces/file/IStorage"; -import { storage } from "../../types/storage/storage"; -import { storageType } from "../../types/storage/storageType"; +import { Storage } from "../../types/storage/storage"; +import { StorageType } from "../../types/storage/storageType"; import { MinioStorageService } from "./MinioStorageService"; import { IS3Context } from "../../interfaces/file/IS3Context"; import { StorageContextBuilder } from "../context/storage/StorageContextBuilder"; import { ServerResponse } from "../../enums/ServerResponse"; import { ServerError } from "../../error/ServerError"; import { ApiContext } from "../context/ApiContext"; -import { fileContext } from "../../types/file/fileContext"; +import { FileContext } from "../../types/file/fileContext"; +import { TemplateUtils } from "../../utils/TemplateUtils"; export class StorageServiceFactory { private _storage!: IStorage; - private _storageMap: Map = new Map(); + private _storageMap: Map = new Map(); - public createStorageService(ctx: ApiContext, storageName: storage): IStorage { + public createStorageService(ctx: ApiContext, storageName: Storage): IStorage { storageName = storageName ?? Environment.instance.getEnvVar("STORAGE_NAME", "string", "minio"); @@ -30,7 +31,9 @@ export class StorageServiceFactory { break; default: throw new ServerError( - ServerResponse.UNSUPPORTED_STORAGE_NAME.replace("%name", storageName) + TemplateUtils.text(ServerResponse.UNSUPPORTED_STORAGE_NAME, { + name: storageName, + }) ); } @@ -42,7 +45,7 @@ export class StorageServiceFactory { } /** File context and storage service init */ - public createFileContext(storageType?: storageType): fileContext { + public createFileContext(storageType?: StorageType): FileContext { storageType = storageType ?? Environment.instance.getEnvVar("STORAGE_TYPE", "string", "s3"); @@ -52,10 +55,9 @@ export class StorageServiceFactory { return StorageContextBuilder.buildS3Context() as IS3Context; default: throw new ServerError( - ServerResponse.UNSUPPORTED_STORAGE_TYPE.replace( - "%type", - String(storageType) - ) + TemplateUtils.text(ServerResponse.UNSUPPORTED_STORAGE_TYPE, { + type: String(storageType), + }) ); } } diff --git a/server/src/services/text/TranslateService.ts b/server/src/services/text/TranslateService.ts new file mode 100644 index 0000000..45094a7 --- /dev/null +++ b/server/src/services/text/TranslateService.ts @@ -0,0 +1,109 @@ +import path from "path"; +import fs from "fs"; + +import { ValueUtils } from "../../utils/ValueUtils"; +import { Language, Translation } from "../../types/text/translation"; +import { Logger } from "../../utils/Logger"; +import { IncomingHttpHeaders } from "http"; + +/** + * Static service that handles translations from `storage/language/*.json` files by translation key + */ +export class TranslateService { + private static _translationsPath = path.join( + process.cwd(), + "storage/language" + ); + private static _translationsMap = new Map(); + + private static _translationKeys: string[] = []; + + /** Returns array that contains all translation keys */ + public static get translationKeys() { + if (this._translationKeys.length > 0) { + return this._translationKeys; + } + + this._loadTranslationKeys(); + return this._translationKeys; + } + + private static _loadTranslationKeys() { + let translation = this._translationsMap.get("en"); + + if (!translation || Object.keys(translation).length < 1) { + const filePath = path.join(this._translationsPath, "en.json"); + translation = JSON.parse(fs.readFileSync(filePath, "utf8")); + } + + for (const key of Object.keys(translation as Translation)) { + if (!this._translationKeys.includes(key)) { + this._translationKeys.push(key); + } + } + } + + private static _loadTranslation(language: Language): Translation | null { + const existing = this._translationsMap.get(language); + if (!ValueUtils.isBad(existing) && !ValueUtils.isEmpty(existing)) { + return existing; + } + + try { + const filePath = path.join(this._translationsPath, `${language}.json`); + if (fs.existsSync(filePath)) { + const translation = JSON.parse(fs.readFileSync(filePath, "utf8")); + this._translationsMap.set(language, translation); + return translation; + } + } catch (error) { + Logger.error( + `Error loading translation for language: ${language}: ${error}` + ); + } + + return null; + } + + public static localize( + translationKey: string, + headers?: IncomingHttpHeaders + ) { + const lang = this.parseHeaders(headers); + return this.translate(translationKey, lang); + } + + /** + * Translates text with a given language by its translationKey + * or returns English translation (default). + * If no value by key is found, the key will be returned. + */ + public static translate(translationKey: string, language?: Language): string { + const selectedLang = language ?? "en"; + const translation = + this._loadTranslation(selectedLang) ?? this._loadTranslation("en"); + + if (!translation) { + Logger.warn( + `Translation for language '${selectedLang}' not found. Returning key.` + ); + return translationKey; + } + + return translation[translationKey] ?? translationKey; + } + + /** + * Parse "Accept-Language" header to get the first preferred language. + */ + public static parseHeaders(headers?: IncomingHttpHeaders): string { + if (!headers) { + return "en"; + } + const langHeader = headers["accept-language"]; + if (!langHeader) { + return "en"; + } + return langHeader.split(",")[0].split(";")[0].split("-")[0]; + } +} diff --git a/server/src/types/env/env.ts b/server/src/types/env/env.ts index 7367eba..dc32ec7 100644 --- a/server/src/types/env/env.ts +++ b/server/src/types/env/env.ts @@ -1 +1 @@ -export type envVar = "string" | "number" | "boolean"; +export type EnvVar = "string" | "number" | "boolean"; diff --git a/server/src/types/file/fileContext.ts b/server/src/types/file/fileContext.ts index 67f600d..494f814 100644 --- a/server/src/types/file/fileContext.ts +++ b/server/src/types/file/fileContext.ts @@ -1,4 +1,4 @@ import { IS3Context } from "../../interfaces/file/IS3Context"; // Later, if needed, add other context types as union -export type fileContext = IS3Context; +export type FileContext = IS3Context; diff --git a/server/src/types/jwt/jwt.ts b/server/src/types/jwt/jwt.ts index 1c72ae7..c7df61a 100644 --- a/server/src/types/jwt/jwt.ts +++ b/server/src/types/jwt/jwt.ts @@ -19,6 +19,6 @@ export type TokenOptions = { refreshExpiresIn: string; }; -export type jwtSecret = { +export type JWTSecret = { jwt_secret: string; }; diff --git a/server/src/types/storage/storage.ts b/server/src/types/storage/storage.ts index 903841c..6cefe8b 100644 --- a/server/src/types/storage/storage.ts +++ b/server/src/types/storage/storage.ts @@ -1,2 +1,2 @@ // Later, if needed, add other storages as union -export type storage = "minio"; +export type Storage = "minio"; diff --git a/server/src/types/storage/storageType.ts b/server/src/types/storage/storageType.ts index 94dd86a..b8ef3f7 100644 --- a/server/src/types/storage/storageType.ts +++ b/server/src/types/storage/storageType.ts @@ -1,2 +1,2 @@ // Later, if needed, add other storage types as union -export type storageType = "s3"; +export type StorageType = "s3"; diff --git a/server/src/types/text/translation.ts b/server/src/types/text/translation.ts new file mode 100644 index 0000000..bfed988 --- /dev/null +++ b/server/src/types/text/translation.ts @@ -0,0 +1,3 @@ +export type Language = string; +export type LangCode = string; +export type Translation = { [key: LangCode]: string }; diff --git a/server/src/utils/CryptoUtils.ts b/server/src/utils/CryptoUtils.ts index d9a29f5..6370482 100644 --- a/server/src/utils/CryptoUtils.ts +++ b/server/src/utils/CryptoUtils.ts @@ -1,18 +1,18 @@ import bcrypt from "bcryptjs"; -import { Crypto } from "../interfaces/Crypto"; +import { ICrypto } from "../interfaces/ICrypto"; export class CryptoUtils { public static async hash( data: string, salt: string | number, - crypto?: Crypto + crypto?: ICrypto ) { crypto = crypto ?? bcrypt; return crypto.hash(data as string, salt); } - public static async compare(data: string, hash: string, crypto?: Crypto) { + public static async compare(data: string, hash: string, crypto?: ICrypto) { crypto = crypto ?? bcrypt; return crypto.compare(data, hash); } diff --git a/server/src/utils/JWTUtils.ts b/server/src/utils/JWTUtils.ts index 908a064..25278b6 100644 --- a/server/src/utils/JWTUtils.ts +++ b/server/src/utils/JWTUtils.ts @@ -2,12 +2,13 @@ import crypto from "crypto"; import fs from "fs"; import path from "path"; import * as jwt from "jsonwebtoken"; +import { type Request } from "express"; import { Environment } from "../config/Environment"; import { JWTPayload, JWTResponse, - jwtSecret, + JWTSecret, TokenOptions, } from "../types/jwt/jwt"; import { ClientResponse } from "../enums/ClientResponse"; @@ -43,7 +44,7 @@ export class JWTUtils { .toString("base64") .slice(0, length); - const data: jwtSecret = { + const data: JWTSecret = { jwt_secret: secret, }; @@ -72,7 +73,7 @@ export class JWTUtils { return this.generateSecret(options); } - const data: jwtSecret = JSON.parse(file); + const data: JWTSecret = JSON.parse(file); return data.jwt_secret; } @@ -109,11 +110,11 @@ export class JWTUtils { /** * Refreshes user tokens by checking given refresh_token */ - public static refresh(token: string, options?: TokenOptions): JWTResponse { + public static refresh(req: Request, options?: TokenOptions): JWTResponse { try { const env = Environment.instance; const decode = jwt.verify( - token, + req.body.refresh_token, options?.refreshSecret ?? env.JWT_REFRESH_SECRET ); const { access_token, refresh_token } = JWTUtils.generateTokens( diff --git a/server/src/utils/Logger.ts b/server/src/utils/Logger.ts index 5611895..fdc1c03 100644 --- a/server/src/utils/Logger.ts +++ b/server/src/utils/Logger.ts @@ -61,6 +61,10 @@ export class Logger { const prefix = "[DEBUG]: "; let text = ""; + if (obj instanceof Map) { + obj = Object.fromEntries(obj); + } + // Parse object to show it fully if (typeof obj === "object") { const seen = new Set(); diff --git a/server/src/utils/TemplateUtils.ts b/server/src/utils/TemplateUtils.ts new file mode 100644 index 0000000..ae03dd2 --- /dev/null +++ b/server/src/utils/TemplateUtils.ts @@ -0,0 +1,12 @@ +export class TemplateUtils { + /** + * Replace all given arguments in provided text. + * + * Argument pattern "%someArg" -> replace with `{ someArg: "replacement" }` + */ + public static text(text: string, args: { [key: string]: any }) { + return text.replace(/%(\w+)/g, (_, key) => { + return key in args ? args[key] : `%${key}`; + }); + } +} diff --git a/server/storage/language/en.json b/server/storage/language/en.json new file mode 100644 index 0000000..4fb668c --- /dev/null +++ b/server/storage/language/en.json @@ -0,0 +1,25 @@ +{ + "user_not_found": "User not found", + "user_logged_in": "User is already logged in", + "user_already_exists": "User with this name or email already exists", + "no_user_data": "No user data provided", + "wrong_password": "Wrong password, please try again", + "no_refresh": "Please provide refresh_token", + "invalid_refresh": "Invalid or expired refresh token", + "invalid_token": "Token invalid or expired", + "access_denied": "Access denied", + "too_many_requests": "Too many requests, please try again later", + "no_permission": "You don't have permission to perform this action", + "validation_error": "Validation error: %error", + "fields_required": "%fields fields is required", + "bad_user_id": "Please provide id that greater than 1", + "no_content_rounds": "Content does not contain 'rounds'!", + "wrong_content": "Wrong 'content' argument type, it should be a valid JSON object!", + "empty_content": "Content is empty!", + "cannot_save_content": "Cannot save content to the Database, probably it is incorrect or empty", + "package_author_not_found": "User that upload package not found, upload aborted", + "filename_required": "'filename' field is required", + "filename_invalid": "'filename' should be a valid string", + "delete_request_sent": "Delete request sent", + "user_permission_not_exists": "Permission %name with ID %id does not exists" +} diff --git a/server/test/unit/src/user/user-register.test.ts b/server/test/unit/src/user/user-register.test.ts deleted file mode 100644 index caf1fcc..0000000 --- a/server/test/unit/src/user/user-register.test.ts +++ /dev/null @@ -1,254 +0,0 @@ -import sinon from "sinon"; -import * as bcrypt from "bcryptjs"; - -import { mock, instance, when, verify } from "ts-mockito"; -import { Repository } from "typeorm"; -import { expect } from "chai"; - -import { User } from "../../../../src/database/models/User"; -import { AuthService } from "../../../../src/services/AuthService"; -import { JWTUtils } from "../../../../src/utils/JWTUtils"; -import { RegisterUser } from "../../../../src/managers/user/RegisterUser"; -import { LoginUser } from "../../../../src/managers/user/LoginUser"; -import { IUser } from "../../../../src/interfaces/user/IUser"; -import { ClientResponse } from "../../../../src/enums/ClientResponse"; - -// Create a mock instance of bcrypt -const bcryptMock = mock(); - -// Stub bcrypt.hash to return a resolved promise -when(bcryptMock.hash("password123", 10)).thenResolve("hashedPassword"); -when(bcryptMock.compare("password123", "hashedPassword")).thenResolve(true); -when(bcryptMock.compare("wrongPassword", "correctPassword")).thenResolve(false); - -// Use the mock instance in your tests -const bcryptInstance = instance(bcryptMock); - -const options = { - secret: "someSecret", - refreshSecret: "someSecret", - expiresIn: "1 day", - refreshExpiresIn: "1 day", -}; - -describe("User auth and jwt tokens", () => { - let userRepository: sinon.SinonStubbedInstance; - let selectQueryBuilder: sinon.SinonStubbedInstance; - let db: any; - let ctx: any; - - beforeEach(async () => { - db = { - getRepository: () => userRepository, - }; - ctx = { - db: db, - }; - selectQueryBuilder = { - where: sinon.stub().returnsThis(), - orWhere: sinon.stub().returnsThis(), - getOne: () => null, - }; - - userRepository = { - save: sinon.stub().resolves(true), - getOne: sinon.stub().resolves(false), - createQueryBuilder: () => selectQueryBuilder, - } as unknown as Repository; - }); - - describe("register", () => { - it("should register a user successfully", async () => { - const userData = { - name: "John Doe", - email: "john@example.com", - password: "password123", - birthday: new Date("1990-01-01"), - avatar: null, - }; - - // Mock token generation - sinon.stub(JWTUtils, "generateTokens").returns({ - access_token: "accessToken", - refresh_token: "refreshToken", - }); - - const result = await new AuthService().register( - ctx.db, - userData as any, - bcryptInstance - ); - - expect(result).to.have.property("access_token"); - expect(result).to.have.property("refresh_token"); - verify(bcryptMock.hash("password123", 10)).called(); - - // Cover different format of birthday - const userData2 = { - name: "John Doe2", - email: "john2@example.com", - password: "password123456", - birthday: "1990-01-01 12:00:00", - avatar: null, - }; - - const result2 = await new AuthService().register( - ctx.db, - userData2 as any, - bcryptInstance - ); - - expect(result2).to.have.property("access_token"); - expect(result2).to.have.property("refresh_token"); - verify(bcryptMock.hash("password123456", 10)).called(); - - // Cover no birthday - const userData3 = { - name: "John Doe3", - email: "john3@example.com", - password: "password123456789", - avatar: null, - }; - - const result3 = await new AuthService().register( - ctx.db, - userData3 as any, - bcryptInstance - ); - - expect(result3).to.have.property("access_token"); - expect(result3).to.have.property("refresh_token"); - verify(bcryptMock.hash("password123456789", 10)).called(); - }); - - it("should throw an error if registration data is invalid", async () => { - const userData = { name: "John Doe", email: "", password: "" }; - const required = ["email", "password"]; - - try { - // Logic from endpoint that throws error - const data = new RegisterUser(userData); - data.validate(); - - await new AuthService().register( - ctx.db, - userData as any, - bcryptInstance - ); - throw new Error("Expected register method to throw error."); - } catch (err: any) { - expect(err.message).to.be.equal( - ClientResponse.FIELDS_REQUIRED.replace("%s", `[${[...required]}]`) - ); - } - }); - }); - - describe("login", () => { - it("should log in a user successfully", async () => { - const userData = { - login: "John Doe", - password: "password123", - }; - - const user = new User(); - user.import({ - password: await bcryptInstance.hash(userData.password, 10), - } as unknown as IUser); - - sinon.stub(selectQueryBuilder, "getOne").returns(user); - - const result = await new AuthService().login( - ctx.db, - userData as any, - bcryptInstance - ); - - expect(result).to.have.property("access_token"); - expect(result).to.have.property("refresh_token"); - expect(result.access_token).to.equal("accessToken"); - expect(result.refresh_token).to.equal("refreshToken"); - }); - - it("should throw an error if login data is invalid", async () => { - const userData = { login: "", password: "" }; - - try { - // Logic from endpoint that throws error - const data = new LoginUser(userData); - data.validate(); - - await new AuthService().login(ctx, userData as any, bcryptInstance); - throw new Error("Expected method above to throw error."); - } catch (err: any) { - expect(err.message).to.be.equal(ClientResponse.NO_USER_DATA); - } - }); - - it("should throw an error if user does not exist", async () => { - const userData = { - email: "nonexistent@example.com", - password: "password123", - }; - sinon.stub(selectQueryBuilder, "getOne").returns(null); - - try { - await new AuthService().login(ctx.db, userData as any, bcryptInstance); - throw new Error("Expected method above to throw error."); - } catch (err: any) { - expect(err.message).to.be.equal(ClientResponse.USER_NOT_FOUND); - } - }); - }); - - it("should throw an error if password is incorrect", async () => { - const userData = { - email: "john@example.com", - password: "wrongPassword", - }; - const user = new User(); - user.import({ - password: await bcryptInstance.hash("correctPassword", 10), - } as unknown as IUser); - - sinon.stub(selectQueryBuilder, "getOne").returns(user); - - try { - await new AuthService().login(ctx.db, userData as any, bcryptInstance); - throw new Error("Expected method above to throw error."); - } catch (err: any) { - expect(err.message).to.be.equal(ClientResponse.WRONG_PASSWORD); - } - }); - - it("should generate token correctly", async () => { - const userId = 1; - const result = JWTUtils.generateTokens(userId, options); - - expect(result).to.have.property("access_token"); - expect(result).to.have.property("refresh_token"); - expect(result.access_token.length).greaterThan(100); - expect(result.refresh_token.length).greaterThan(100); - expect(result.access_token.split(".").length).to.be.equal(3); - expect(result.refresh_token.split(".").length).to.be.equal(3); - }); - - it("refresh token correctly", async () => { - const userId = 1; - const token = JWTUtils.generateTokens(userId, options).refresh_token; - - const result = JWTUtils.refresh(token, options); - - expect(result).to.have.property("access_token"); - expect(result).to.have.property("refresh_token"); - }); - - it("refresh bad token should throw error", async () => { - try { - JWTUtils.refresh("Some wrong token"); - throw new Error("Expected method above to throw error."); - } catch (err: any) { - expect(err.message).to.be.equal(ClientResponse.INVALID_REFRESH); - } - }); -}); diff --git a/server/test/unit/src/user/user-retrieve.test.ts b/server/test/unit/src/user/user-retrieve.test.ts deleted file mode 100644 index eb05e54..0000000 --- a/server/test/unit/src/user/user-retrieve.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import sinon from "sinon"; -import { Repository } from "typeorm"; -import { User } from "../../../../src/database/models/User"; -import { expect } from "chai"; -import { UserService } from "../../../../src/services/UserService"; -import { ClientResponse } from "../../../../src/enums/ClientResponse"; -import { ValueUtils } from "../../../../src/utils/ValueUtils"; -import { UserRepository } from "../../../../src/database/repositories/UserRepository"; -import { ISelectOptions } from "../../../../src/interfaces/ISelectOptions"; - -describe("User retrieve by id and JWT token", () => { - let userRepository: sinon.SinonStubbedInstance; - let repository: UserRepository; - let stubUser: sinon.SinonStub< - [id: number, selectOptions?: ISelectOptions | undefined], - Promise - >; - let db: any; - let ctx: any; - - beforeEach(async () => { - db = { - getRepository: () => userRepository, - }; - ctx = { - db: db, - }; - userRepository = { - findOne: () => null, - } as unknown as Repository; - repository = UserRepository.getRepository(ctx.db); - stubUser = sinon.stub(repository, "get"); - }); - - afterEach(async () => { - stubUser.restore(); - }); - - describe("Retrieve user by token", () => { - it("Should retrieve user by token", async () => { - const payload = { - iat: 1111, - exp: 2222, - id: 1, - }; - - stubUser.resolves({ - email: "someEmail", - name: "success", - password: "somePassword", - created_at: new Date(), - updated_at: new Date(), - is_deleted: false, - isAdmin: () => false, - } as unknown as User); - - const result = await new UserService().getByTokenPayload(ctx.db, payload); - expect(result.name).to.be.equal("success"); - }); - }); - - describe("Retrieve user by id", () => { - it("Should retrieve user by id if he asks for his own info", async () => { - // Retrieve info about ourselves - const payload = { - iat: 1111, - exp: 2222, - id: 1, - }; - - stubUser.resolves({ - name: "success", - isAdmin: () => false, - } as unknown as User); - - const result = await new UserService().get(ctx.db, 1, payload); - - expect(result.name).to.be.equal("success"); - }); - - it("Should throw error on bad id", async () => { - // Retrieve info about ourselves - const payload = { - iat: 1111, - exp: 2222, - id: 1, - }; - - stubUser.resolves({ - name: "success", - isAdmin: () => false, - } as unknown as User); - - try { - ValueUtils.validateId(Number("bad id")); - await new UserService().get(ctx.db, Number("bad id"), payload); - throw new Error("Line above should throw error"); - } catch (err: any) { - expect(err.message).to.be.equal(ClientResponse.BAD_USER_ID); - } - }); - - it.skip("Should throw error if asking for another user", async () => { - const payload = { - iat: 1111, - exp: 2222, - id: 2, - }; - - stubUser.resolves({ - name: "fail", - isAdmin: () => false, - } as unknown as User); - - try { - await new UserService().get(ctx.db, 1, payload); - throw new Error("Line above should throw error"); - } catch (err: any) { - expect(err.message).to.be.equal(ClientResponse.ACCESS_DENIED); - } - }); - - it("Should retrieve user if admin asking for another user", async () => { - const payload = { - iat: 1111, - exp: 2222, - id: 2, - }; - - stubUser.withArgs(2).resolves({ - name: "admin", - isAdmin: () => true, - permissions: [{ id: 1, name: "admin" }], - } as unknown as User); - - stubUser.withArgs(1).resolves({ - name: "success", - isAdmin: () => false, - } as unknown as User); - - const result = await new UserService().get(ctx.db, 1, payload); - expect(result.name).to.be.equal("success"); - }); - }); -}); diff --git a/server/test/unit/src/user/user-update.test.ts b/server/test/unit/src/user/user-update.test.ts deleted file mode 100644 index fcabc9e..0000000 --- a/server/test/unit/src/user/user-update.test.ts +++ /dev/null @@ -1,590 +0,0 @@ -import sinon from "sinon"; -import { Repository } from "typeorm"; -import { User } from "../../../../src/database/models/User"; -import { UserService } from "../../../../src/services/UserService"; -import * as bcrypt from "bcryptjs"; -import { expect } from "chai"; -import { instance, mock, when, verify } from "ts-mockito"; -import { UpdateUser } from "../../../../src/managers/user/UpdateUser"; -import { ClientResponse } from "../../../../src/enums/ClientResponse"; -import { UserRepository } from "../../../../src/database/repositories/UserRepository"; - -const bcryptMock = mock(); - -// Stub bcrypt.hash to return a resolved promise -when(bcryptMock.compare("somePassword", "somePassword")).thenResolve(true); -when(bcryptMock.compare("wrongPassword", "somePassword")).thenResolve(false); -when(bcryptMock.compare("newPassword", "somePassword")).thenResolve(false); - -// Use the mock instance in your tests -const bcryptInstance = instance(bcryptMock); - -describe("User update", () => { - let userRepo: UserRepository; - let repository: sinon.SinonStubbedInstance; - let stubFindOne: - | sinon.SinonStub - | sinon.SinonStub; - let db: any; - let ctx: any; - - beforeEach(async () => { - db = { - getRepository: () => repository, - ds: { - transaction: async () => true, - }, - }; - ctx = { - db: db, - crypto: bcryptInstance, - }; - - // Implement methods of user and permissions repositories - repository = { - findOne: () => null, - update: () => null, - exists: () => true, - } as unknown as Repository; - userRepo = UserRepository.getRepository(db); - stubFindOne = sinon.stub(userRepo, "get"); - }); - - afterEach(async () => { - stubFindOne.restore(); - }); - - describe("User update", () => { - // TODO: Currently we have no required fields - it.skip("Should throw error with empty data", async () => { - const dataToUpdate = {}; - - const req = { - body: dataToUpdate, - }; - - try { - const data = new UpdateUser(req.body); - data.validate(); - throw new Error("Line above should throw error"); - } catch (err: any) { - expect(err.message.toLowerCase()).to.include("is required"); - } - }); - - it("Should update user with provided data", async () => { - const dataToUpdate = { - name: "updatedName", - email: "updatedEmail@gmail.com", - password: "somePassword", - birthday: new Date(), - }; - const payload = { - iat: 1111, - exp: 2222, - id: 1, - }; - - stubFindOne - .withArgs(1, { - relations: ["permissions"], - }) - .returns({ - name: "success", - password: "somePassword", - updated_at: new Date(), - is_deleted: false, - isAdmin: () => false, - update: () => true, - export: () => dataToUpdate, - }); - - const result = await new UserService().update( - ctx.db, - ctx.crypto, - payload, - dataToUpdate, - 1 - ); - - expect(result.name).to.be.equal("updatedName"); - expect(result.email).to.be.equal("updatedEmail@gmail.com"); - expect(result.birthday).to.be.not.equal(undefined); - expect(result.birthday).to.be.not.equal(null); - }); - - // TODO: For now no need password to update user - it.skip("Should throw error with empty or bad password", async () => { - let dataToUpdate: any; - let req: any; - - dataToUpdate = { - name: "updatedName", - email: "updatedEmail@gmail.com", - }; - - req = { - body: dataToUpdate, - params: { id: 1 }, - }; - - // const stubPayload = sinon.stub(JWTUtils, "getPayload"); - // stubPayload.returns({ - // iat: 1111, - // exp: 2222, - // id: 1, - // }); - - const stubFindOne = sinon.stub(repository, "findOne"); - stubFindOne - .withArgs({ - where: { id: 1 }, - relations: ["permissions"], - }) - .returns({ - name: "user", - password: "somePassword", - updated_at: new Date(), - isAdmin: () => false, - }); - - try { - // await UserService.update(ctx, req as any); - } catch (err: any) { - expect(err.message.toLowerCase()).to.include("password is incorrect"); - } - - dataToUpdate = { - name: "updatedName", - email: "updatedEmail@gmail.com", - password: "WrongPassword", - }; - - req = { - body: dataToUpdate, - params: { id: 1 }, - }; - - try { - // await UserService.update(ctx, req as any); - } catch (err: any) { - expect(err.message.toLowerCase()).to.include("password is incorrect"); - } - - // stubPayload.restore(); - stubFindOne.restore(); - }); - - // TODO: Currently not implemented - it.skip("Should update another user if user is admin", async () => { - const dataToUpdate = { - name: "updatedName", - email: "updatedEmail@gmail.com", - }; - - const req = { - body: dataToUpdate, - params: { id: 1 }, - }; - - // const stubPayload = sinon.stub(JWTUtils, "getPayload"); - // stubPayload.returns({ - // iat: 1111, - // exp: 2222, - // id: 2, - // }); - - const stubFindOne = sinon.stub(repository, "findOne"); - stubFindOne - .withArgs({ - where: { id: 2 }, - relations: ["permissions"], - }) - .returns({ - name: "admin", - permissions: [{ id: 1, name: "admins" }], - isAdmin: () => true, - }); - - stubFindOne - .withArgs({ - where: { id: 1 }, - relations: ["permissions"], - }) - .returns({ - name: "user", - password: "somePassword", - updated_at: new Date(), - permissions: [{ id: 2, name: "users" }], - isAdmin: () => false, - }); - - // const result = await UserService.update(ctx, req as any); - // expect(result.name).to.be.equal("updatedName"); - // expect(result.email).to.be.equal("updatedEmail@gmail.com"); - // stubPayload.restore(); - stubFindOne.restore(); - }); - - it("Should throw error if user is not admin", async () => { - const dataToUpdate = { - name: "updatedName", - email: "updatedEmail@gmail.com", - }; - - const payload = { - iat: 1111, - exp: 2222, - id: 2, - }; - - stubFindOne - .withArgs({ - where: { id: 2 }, - relations: ["permissions"], - }) - .returns({ - name: "notAdmin", - permissions: [{ id: 2, name: "users" }], - isAdmin: () => false, - }); - - try { - await new UserService().update( - ctx.db, - ctx.crypto, - payload, - dataToUpdate, - 1 - ); - } catch (err: any) { - expect(err.message).to.be.equal(ClientResponse.ACCESS_DENIED); - } - }); - - // TODO: Allowing admins to change another users not implemented, so asking user will always exist - it.skip("Should not found user", async () => { - const dataToUpdate = { - name: "updatedName", - email: "updatedEmail@gmail.com", - }; - - const req = { - body: dataToUpdate, - params: { id: 777 }, - }; - - // const stubPayload = sinon.stub(JWTUtils, "getPayload"); - // stubPayload.returns({ - // iat: 1111, - // exp: 2222, - // id: 1, - // }); - - const stubFindOne = sinon.stub(repository, "findOne"); - stubFindOne - .withArgs({ - where: { id: 1 }, - relations: ["permissions"], - }) - .returns({ - name: "admin", - permissions: [{ id: 1, name: "admins" }], - isAdmin: () => true, - }); - - stubFindOne - .withArgs({ - where: { id: 777 }, - relations: ["permissions"], - }) - .returns(undefined); - - try { - // await UserService.update(ctx, req as any); - throw new Error("Line above should throw error"); - } catch (err: any) { - expect(err.message.toLowerCase()).to.include("not found"); - } - // stubPayload.restore(); - stubFindOne.restore(); - }); - - // TODO: Currently not implemented - it.skip("Should update new password", async () => { - let dataToUpdate: any; - let req: any; - - dataToUpdate = { - name: "updatedName", - email: "updatedEmail@gmail.com", - newPassword: "newPassword", - password: "somePassword", - }; - - req = { - body: dataToUpdate, - params: { id: 1 }, - }; - - // const stubPayload = sinon.stub(JWTUtils, "getPayload"); - // stubPayload.returns({ - // iat: 1111, - // exp: 2222, - // id: 1, - // }); - - const stubFindOne = sinon.stub(repository, "findOne"); - stubFindOne - .withArgs({ - where: { id: 1 }, - relations: ["permissions"], - }) - .returns({ - name: "user", - email: "email@gmail.com", - password: "somePassword", - updated_at: new Date(), - isAdmin: () => false, - }); - - // await UserService.update(ctx, req as any); - verify(bcryptMock.hash("newPassword", 10)).called(); - - stubFindOne - .withArgs({ - where: { id: 1 }, - relations: ["permissions"], - }) - .returns({ - name: "user", - email: "email@gmail.com", - password: "somePassword", - updated_at: new Date(), - isAdmin: () => false, - }); - - dataToUpdate = { - name: "updatedName", - email: "updatedEmail@gmail.com", - newPassword: "somePassword", - password: "somePassword", - }; - - req = { - body: dataToUpdate, - params: { id: 1 }, - }; - - // await UserService.update(ctx, req as any); - verify(bcryptMock.hash("newPassword", 10)).once(); - - // stubPayload.restore(); - stubFindOne.restore(); - }); - - // TODO: Currently not implemented - it.skip("Default user should not update permissions", async () => { - let dataToUpdate: any; - let req: any; - - dataToUpdate = { - name: "updatedName", - email: "updatedEmail@gmail.com", - password: "somePassword", - permissions: [ - { - id: 1, - name: "admins", - }, - { - id: 2, - name: "users", - }, - ], - }; - - req = { - body: dataToUpdate, - params: { id: 1 }, - }; - - // const stubPayload = sinon.stub(JWTUtils, "getPayload"); - // stubPayload.returns({ - // iat: 1111, - // exp: 2222, - // id: 1, - // }); - - const stubFindOne = sinon.stub(repository, "findOne"); - stubFindOne - .withArgs({ - where: { id: 1 }, - relations: ["permissions"], - }) - .returns({ - name: "user", - email: "email@gmail.com", - password: "somePassword", - updated_at: new Date(), - isAdmin: () => false, - }); - - try { - // await UserService.update(ctx, req as any); - } catch (err: any) { - expect(err.message.toLowerCase()).to.include( - "only admins allowed to change user permissions" - ); - } - - // stubPayload.restore(); - stubFindOne.restore(); - }); - - // TODO: Currently not implemented - it.skip("Should update permissions", async () => { - let dataToUpdate: any; - let req: any; - - dataToUpdate = { - name: "updatedName", - email: "updatedEmail@gmail.com", - password: "somePassword", - permissions: [ - { - id: 1, - name: "admins", - }, - { - id: 2, - name: "users", - }, - ], - }; - - req = { - body: dataToUpdate, - params: { id: 1 }, - }; - - // const stubPayload = sinon.stub(JWTUtils, "getPayload"); - // stubPayload.returns({ - // iat: 1111, - // exp: 2222, - // id: 2, - // }); - - const stubFindOne = sinon.stub(repository, "findOne"); - - stubFindOne - .withArgs({ - where: { id: 1 }, - relations: ["permissions"], - }) - .returns({ - name: "user", - email: "email@gmail.com", - password: "somePassword", - updated_at: new Date(), - isAdmin: () => false, - }); - - stubFindOne - .withArgs({ - where: { id: 2 }, - relations: ["permissions"], - }) - .returns({ - name: "admin", - email: "admin@gmail.com", - updated_at: new Date(), - permissions: [{ id: 1, name: "admins" }], - isAdmin: () => true, - }); - - // const result = await UserService.update(ctx, req as any); - // expect(result.name).to.be.equal("updatedName"); - // expect(result.email).to.be.equal("updatedEmail@gmail.com"); - // expect(result.permissions![0].id).to.be.equal(1); - // expect(result.permissions![0].name).to.be.equal("admins"); - // expect(result.permissions![1].id).to.be.equal(2); - // expect(result.permissions![1].name).to.be.equal("users"); - - // stubPayload.restore(); - stubFindOne.restore(); - }); - - // TODO: Currently not implemented - it.skip("Should throw error if permission does not exists", async () => { - let dataToUpdate: any; - let req: any; - - dataToUpdate = { - name: "updatedName", - email: "updatedEmail@gmail.com", - password: "somePassword", - permissions: [ - { - id: 3, - name: "doesNotExists", - }, - ], - }; - - req = { - body: dataToUpdate, - params: { id: 1 }, - }; - - // const stubPayload = sinon.stub(JWTUtils, "getPayload"); - // stubPayload.returns({ - // iat: 1111, - // exp: 2222, - // id: 2, - // }); - - const stubFindOne = sinon.stub(repository, "findOne"); - - stubFindOne - .withArgs({ - where: { id: 1 }, - relations: ["permissions"], - }) - .returns({ - name: "user", - email: "email@gmail.com", - password: "somePassword", - updated_at: new Date(), - isAdmin: () => false, - }); - - stubFindOne - .withArgs({ - where: { id: 2 }, - relations: ["permissions"], - }) - .returns({ - name: "admin", - email: "admin@gmail.com", - updated_at: new Date(), - permissions: [{ id: 1, name: "admins" }], - isAdmin: () => true, - }); - - sinon.stub(repository, "exists").returns(false); - - try { - // await UserService.update(ctx, req as any); - throw new Error("Line above should throw error"); - } catch (err: any) { - expect(err.message.toLowerCase()).to.include("3"); // group id - expect(err.message.toLowerCase()).to.include("doesnotexists"); // name - expect(err.message.toLowerCase()).to.include("does not exists"); // msg - } - - // stubPayload.restore(); - stubFindOne.restore(); - }); - }); -}); diff --git a/server/test/unit/src/utils/jwt-utils.test.ts b/server/test/unit/src/utils/jwt-utils.test.ts deleted file mode 100644 index 09b5617..0000000 --- a/server/test/unit/src/utils/jwt-utils.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import fs from "fs"; - -import { mock, instance, when } from "ts-mockito"; -import { expect } from "chai"; - -import path from "path"; -import crypto from "crypto"; -import { JWTUtils } from "../../../../src/utils/JWTUtils"; - -// Create a mock instance of bcrypt -const cryptoMock = mock(); -const buffer = Buffer.from("goodSecret", "utf-8"); - -// Stub bcrypt.hash to return a resolved promise -when(cryptoMock.randomBytes(512)).thenReturn(buffer); - -// Use the mock instance in your tests -const cryptoInstance = instance(cryptoMock); -// Use test paths to not interrupt production part -const testWritePath = path.resolve(process.cwd(), "storage/test/"); -const filePath = path.resolve(testWritePath, ".secret.json"); - -const options = { - length: 512, - cryptoInstance: cryptoInstance, - writePath: testWritePath, -}; - -describe("JWTUtils", () => { - beforeEach(async () => { - if (fs.existsSync(filePath)) { - fs.rmSync(filePath); - } - }); - - after(async () => { - if (fs.existsSync(testWritePath)) { - fs.rm(testWritePath, { recursive: true, force: true }, () => null); - } - }); - - describe("generateSecret", () => { - it("Should generate secret and write it to the file", async () => { - // Check return result - const result = JWTUtils.generateSecret(options); - - expect(result).to.be.equal(buffer.toString("base64")); - - // Read file for result - const file = fs.readFileSync(filePath); - const data = JSON.parse(file.toString()); - - expect(data).to.have.property("jwt_secret"); - expect(data.jwt_secret).to.be.equal(buffer.toString("base64")); - }); - - it("Should create folder if not exists", async () => { - // This covers functionality of "fs", but added to make 100% coverage :) - if (fs.existsSync(testWritePath)) { - fs.rmdirSync(testWritePath); - } - - JWTUtils.generateSecret(options); - expect(fs.existsSync(testWritePath)).to.be.equal(true); - }); - }); - - describe("getSecret", () => { - it("Should get secret from file", async () => { - if (fs.existsSync(filePath)) { - fs.rmSync(filePath); - } - // From previous tests we already know, that this will work fine - JWTUtils.generateSecret(options); - - const data = JWTUtils.getSecret(options); - expect(data).to.be.equal(buffer.toString("base64")); - }); - - it("Should write secret to file if it not exists", async () => { - if (fs.existsSync(filePath)) { - fs.rmSync(filePath); - } - - expect(fs.existsSync(filePath)).to.be.equal(false); - - const data = JWTUtils.getSecret(options); - - expect(data).to.be.equal(buffer.toString("base64")); - expect(fs.existsSync(filePath)).to.be.equal(true); - }); - - it("Should write secret to file if it is empty", async () => { - if (fs.existsSync(filePath)) { - fs.rmSync(filePath); - } - fs.writeFileSync(filePath, ""); - - const data = JWTUtils.getSecret(options); - - expect(data).to.be.equal(buffer.toString("base64")); - expect(fs.existsSync(filePath)).to.be.equal(true); - }); - }); -});