diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..87f599b --- /dev/null +++ b/compose.yml @@ -0,0 +1,20 @@ +services: + db: + container_name: investments-db + image: postgres:15.2 + restart: always + environment: + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + ports: + - 5433:5432 + volumes: + - postgres_data:/var/lib/posgresql/data + networks: + - investments + +volumes: + postgres_data: + +networks: + investments: diff --git a/server/.gitignore b/server/.gitignore index 4b56acf..78cff21 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -54,3 +54,5 @@ pids # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +/generated/prisma diff --git a/server/eslint.config.ts b/server/eslint.config.ts index ba6bed0..0073422 100644 --- a/server/eslint.config.ts +++ b/server/eslint.config.ts @@ -13,6 +13,7 @@ export default [ '@typescript-eslint/no-extraneous-class': 'off', '@typescript-eslint/parameter-properties': 'off', '@typescript-eslint/class-methods-use-this': 'off', + '@typescript-eslint/strict-boolean-expressions': 'off', }, }, ]; diff --git a/server/package-lock.json b/server/package-lock.json index 6a74598..dd4fdc7 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,8 +9,19 @@ "version": "0.0.0", "dependencies": { "@nestjs/common": "11.1.6", + "@nestjs/config": "4.0.2", "@nestjs/core": "11.1.6", + "@nestjs/jwt": "11.0.0", + "@nestjs/mapped-types": "*", + "@nestjs/passport": "11.0.5", "@nestjs/platform-express": "11.1.6", + "@nestjs/swagger": "11.2.0", + "@prisma/client": "^6.14.0", + "bcrypt": "6.0.0", + "class-transformer": "0.5.1", + "class-validator": "0.14.2", + "cookie-parser": "1.4.7", + "passport-jwt": "4.0.1", "reflect-metadata": "0.2.2", "rxjs": "7.8.2" }, @@ -20,12 +31,16 @@ "@nestjs/testing": "11.1.6", "@swc/cli": "0.7.8", "@swc/core": "1.13.3", + "@types/bcrypt": "6.0.0", + "@types/cookie-parser": "1.4.9", "@types/express": "5.0.3", "@types/node": "22.17.1", + "@types/passport-jwt": "4.0.1", "@typescript-eslint/parser": "8.39.0", "eslint": "9.33.0", "eslint-config-ksv741": "0.2.0", "jiti": "^2.5.1", + "prisma": "^6.14.0", "source-map-support": "0.5.21", "ts-loader": "9.5.2", "ts-node": "10.9.2", @@ -1031,6 +1046,12 @@ "node": ">=8" } }, + "node_modules/@microsoft/tsdoc": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", + "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", + "license": "MIT" + }, "node_modules/@napi-rs/nice": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.4.tgz", @@ -1574,6 +1595,33 @@ } } }, + "node_modules/@nestjs/config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-4.0.2.tgz", + "integrity": "sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==", + "license": "MIT", + "dependencies": { + "dotenv": "16.4.7", + "dotenv-expand": "12.0.1", + "lodash": "4.17.21" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/config/node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/@nestjs/core": { "version": "11.1.6", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-11.1.6.tgz", @@ -1615,6 +1663,49 @@ } } }, + "node_modules/@nestjs/jwt": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.0.tgz", + "integrity": "sha512-v7YRsW3Xi8HNTsO+jeHSEEqelX37TVWgwt+BcxtkG/OfXJEOs6GZdbdza200d6KqId1pJQZ6UPj1F0M6E+mxaA==", + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "9.0.7", + "jsonwebtoken": "9.0.2" + }, + "peerDependencies": { + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + } + }, + "node_modules/@nestjs/mapped-types": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz", + "integrity": "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, + "node_modules/@nestjs/passport": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz", + "integrity": "sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "passport": "^0.5.0 || ^0.6.0 || ^0.7.0" + } + }, "node_modules/@nestjs/platform-express": { "version": "11.1.6", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.6.tgz", @@ -1653,6 +1744,39 @@ "typescript": ">=4.8.2" } }, + "node_modules/@nestjs/swagger": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-11.2.0.tgz", + "integrity": "sha512-5wolt8GmpNcrQv34tIPUtPoV1EeFbCetm40Ij3+M0FNNnf2RJ3FyWfuQvI8SBlcJyfaounYVTKzKHreFXsUyOg==", + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.15.1", + "@nestjs/mapped-types": "2.1.0", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "8.2.0", + "swagger-ui-dist": "5.21.0" + }, + "peerDependencies": { + "@fastify/static": "^8.0.0", + "@nestjs/common": "^11.0.1", + "@nestjs/core": "^11.0.1", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/testing": { "version": "11.1.6", "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-11.1.6.tgz", @@ -1735,6 +1859,91 @@ "npm": ">=5.10.0" } }, + "node_modules/@prisma/client": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.14.0.tgz", + "integrity": "sha512-8E/Nk3eL5g7RQIg/LUj1ICyDmhD053STjxrPxUtCRybs2s/2sOEcx9NpITuAOPn07HEpWBfhAVe1T/HYWXUPOw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.14.0.tgz", + "integrity": "sha512-IwC7o5KNNGhmblLs23swnfBjADkacBb7wvyDXUWLwuvUQciKJZqyecU0jw0d7JRkswrj+XTL8fdr0y2/VerKQQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.16.12", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.14.0.tgz", + "integrity": "sha512-j4Lf+y+5QIJgQD4sJWSbkOD7geKx9CakaLp/TyTy/UDu9Wo0awvWCBH/BAxTHUaCpIl9USA5VS/KJhDqKJSwug==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.14.0.tgz", + "integrity": "sha512-LhJjqsALFEcoAtF07nSaOkVguaxw/ZsgfROIYZ8bAZDobe7y8Wy+PkYQaPOK1iLSsFgV2MhCO/eNrI1gdSOj6w==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.14.0", + "@prisma/engines-version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49", + "@prisma/fetch-engine": "6.14.0", + "@prisma/get-platform": "6.14.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49.tgz", + "integrity": "sha512-EgN9ODJpiX45yvwcngoStp3uQPJ3l+AEVoQ6dMMO2QvmwIlnxfApzKmJQExzdo7/hqQANrz5txHJdGYHzOnGHA==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.14.0.tgz", + "integrity": "sha512-MPzYPOKMENYOaY3AcAbaKrfvXVlvTc6iHmTXsp9RiwCX+bPyfDMqMFVUSVXPYrXnrvEzhGHfyiFy0PRLHPysNg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.14.0", + "@prisma/engines-version": "6.14.0-25.717184b7b35ea05dfa71a3236b7af656013e1e49", + "@prisma/get-platform": "6.14.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.14.0.tgz", + "integrity": "sha512-7VjuxKNwjnBhKfqPpMeWiHEa2sVjYzmHdl1slW6STuUCe9QnOY0OY1ljGSvz6wpG4U8DfbDqkG1yofd/1GINww==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.14.0" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1742,6 +1951,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@sindresorhus/is": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", @@ -1755,6 +1971,13 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/@stylistic/eslint-plugin": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.2.0.tgz", @@ -2136,6 +2359,16 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -2157,6 +2390,16 @@ "@types/node": "*" } }, + "node_modules/@types/cookie-parser": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.9.tgz", + "integrity": "sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -2239,6 +2482,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", + "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -2250,12 +2502,43 @@ "version": "22.17.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.1.tgz", "integrity": "sha512-y3tBaz+rjspDTylNjAX37jEC3TETEFGNJL6uQDxwF9/8GLLIjW1rvVHlynyuUKMnMr1Roq8jOv3vkopBjC4/VA==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/jsonwebtoken": "*", + "@types/passport-strategy": "*" + } + }, + "node_modules/@types/passport-strategy": { + "version": "0.2.38", + "resolved": "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.38.tgz", + "integrity": "sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/passport": "*" + } + }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", @@ -2293,6 +2576,12 @@ "@types/send": "*" } }, + "node_modules/@types/validator": { + "version": "13.15.2", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.2.tgz", + "integrity": "sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==", + "license": "MIT" + }, "node_modules/@typescript-eslint/parser": { "version": "8.39.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz", @@ -3388,7 +3677,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/aria-query": { @@ -3664,6 +3952,20 @@ ], "license": "MIT" }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/bin-version": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bin-version/-/bin-version-6.0.0.tgz", @@ -3822,6 +4124,12 @@ "node": "*" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -3848,6 +4156,35 @@ "node": ">= 0.8" } }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, "node_modules/cacheable-lookup": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", @@ -3984,7 +4321,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "readdirp": "^4.0.1" @@ -4006,6 +4343,33 @@ "node": ">=6.0" } }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.2.tgz", + "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.11.1", + "validator": "^13.9.0" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -4137,6 +4501,13 @@ "typedarray": "^0.0.6" } }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/consola": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", @@ -4177,6 +4548,25 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, "node_modules/cookie-signature": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", @@ -4379,6 +4769,16 @@ "node": ">=0.10.0" } }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/defaults": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/defaults/-/defaults-2.0.2.tgz", @@ -4438,6 +4838,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "devOptional": true, + "license": "MIT" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -4447,6 +4854,13 @@ "node": ">= 0.8" } }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -4470,6 +4884,33 @@ "node": ">=0.10.0" } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.1.tgz", + "integrity": "sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4491,12 +4932,32 @@ "dev": true, "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/effect": { + "version": "3.16.12", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz", + "integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.199", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.199.tgz", @@ -4511,6 +4972,16 @@ "dev": true, "license": "MIT" }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -5826,6 +6297,13 @@ "node": ">= 0.6" } }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/ext-list": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", @@ -5853,6 +6331,29 @@ "node": ">=4" } }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6365,6 +6866,24 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, "node_modules/glob": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", @@ -7336,7 +7855,7 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -7353,7 +7872,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -7423,6 +7941,28 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -7439,6 +7979,27 @@ "node": ">=4.0" } }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -7493,6 +8054,12 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.12.12", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.12.tgz", + "integrity": "sha512-aWVR6xXYYRvnK0v/uIwkf5Lthq9Jpn0N8TISW/oDTWlYB2sOimuiLn9Q26aUw4KxkJoiT8ACdiw44Y8VwKFIfQ==", + "license": "MIT" + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -7549,7 +8116,42 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true, + "license": "MIT" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "license": "MIT" }, "node_modules/lodash.merge": { @@ -7559,6 +8161,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -7930,6 +8538,15 @@ "dev": true, "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", + "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -7940,6 +8557,24 @@ "lodash": "^4.17.21" } }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -7973,6 +8608,26 @@ "node": ">=8" } }, + "node_modules/nypm": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.1.tgz", + "integrity": "sha512-hlacBiRiv1k9hZFiphPUkfSQ/ZfQzZDzC+8z0wL3lvDAOUu/2NnChkKuMoMjNur/9OpKuz2QsIeiPVN0xM5Q0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.2", + "pathe": "^2.0.3", + "pkg-types": "^2.2.0", + "tinyexec": "^1.0.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -8094,6 +8749,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -8304,6 +8966,43 @@ "node": ">= 0.8" } }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "license": "MIT", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -8367,6 +9066,19 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==", + "peer": true + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -8374,6 +9086,13 @@ "dev": true, "license": "MIT" }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "devOptional": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -8404,6 +9123,18 @@ "@napi-rs/nice": "^1.0.1" } }, + "node_modules/pkg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.2.0.tgz", + "integrity": "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -8434,6 +9165,32 @@ "node": ">= 0.8.0" } }, + "node_modules/prisma": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.14.0.tgz", + "integrity": "sha512-QEuCwxu+Uq9BffFw7in8In+WfbSUN0ewnaSUKloLkbJd42w6EyFckux4M0f7VwwHlM3A8ssaz4OyniCXlsn0WA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.14.0", + "@prisma/engines": "6.14.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -8469,6 +9226,23 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "devOptional": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -8552,6 +9326,17 @@ "node": ">= 0.8" } }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -8577,7 +9362,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">= 14.18.0" @@ -8974,7 +9759,6 @@ "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -9683,6 +10467,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-ui-dist": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.21.0.tgz", + "integrity": "sha512-E0K3AB6HvQd8yQNSMR7eE5bk+323AUxjtCz/4ZNKiahOlPhPJxqn3UPIGs00cyY/dhrTDJ61L7C/a8u6zhGrZg==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -9842,6 +10635,13 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", @@ -10151,7 +10951,7 @@ "version": "5.9.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -10219,7 +11019,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/universalify": { @@ -10323,6 +11122,16 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -10330,6 +11139,15 @@ "dev": true, "license": "MIT" }, + "node_modules/validator": { + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/server/package.json b/server/package.json index f3e4912..5c52a79 100644 --- a/server/package.json +++ b/server/package.json @@ -7,13 +7,28 @@ "build": "nest build", "start": "nest start", "start:dev": "nest start --watch", + "prisma:validate": "prisma validate", + "prisma:push": "prisma db push", + "prisma:generate": "prisma generate", + "prisma:studio": "prisma studio", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", "typecheck": "tsc --noEmit" }, "dependencies": { "@nestjs/common": "11.1.6", + "@nestjs/config": "4.0.2", "@nestjs/core": "11.1.6", + "@nestjs/jwt": "11.0.0", + "@nestjs/mapped-types": "*", + "@nestjs/passport": "11.0.5", "@nestjs/platform-express": "11.1.6", + "@nestjs/swagger": "11.2.0", + "@prisma/client": "^6.14.0", + "bcrypt": "6.0.0", + "class-transformer": "0.5.1", + "class-validator": "0.14.2", + "cookie-parser": "1.4.7", + "passport-jwt": "4.0.1", "reflect-metadata": "0.2.2", "rxjs": "7.8.2" }, @@ -23,12 +38,16 @@ "@nestjs/testing": "11.1.6", "@swc/cli": "0.7.8", "@swc/core": "1.13.3", + "@types/bcrypt": "6.0.0", + "@types/cookie-parser": "1.4.9", "@types/express": "5.0.3", "@types/node": "22.17.1", + "@types/passport-jwt": "4.0.1", "@typescript-eslint/parser": "8.39.0", "eslint": "9.33.0", "eslint-config-ksv741": "0.2.0", "jiti": "^2.5.1", + "prisma": "^6.14.0", "source-map-support": "0.5.21", "ts-loader": "9.5.2", "ts-node": "10.9.2", diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma new file mode 100644 index 0000000..7dcd692 --- /dev/null +++ b/server/prisma/schema.prisma @@ -0,0 +1,19 @@ +generator client { + provider = "prisma-client-js" + output = "../node_modules/.prisma/client" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model Account { + id String @id @default(uuid()) + email String @unique + password String + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@map("accounts") +} diff --git a/server/src/app.controller.ts b/server/src/app.controller.ts deleted file mode 100644 index cce879e..0000000 --- a/server/src/app.controller.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Controller, Get } from '@nestjs/common'; -import { AppService } from './app.service'; - -@Controller() -export class AppController { - constructor(private readonly appService: AppService) {} - - @Get() - getHello(): string { - return this.appService.getHello(); - } -} diff --git a/server/src/app.module.ts b/server/src/app.module.ts deleted file mode 100644 index 8662803..0000000 --- a/server/src/app.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Module } from '@nestjs/common'; -import { AppController } from './app.controller'; -import { AppService } from './app.service'; - -@Module({ - imports: [], - controllers: [AppController], - providers: [AppService], -}) -export class AppModule {} diff --git a/server/src/app.service.ts b/server/src/app.service.ts deleted file mode 100644 index 927d7cc..0000000 --- a/server/src/app.service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - getHello(): string { - return 'Hello World!'; - } -} diff --git a/server/src/configs/jwt.config.ts b/server/src/configs/jwt.config.ts new file mode 100644 index 0000000..9c957c0 --- /dev/null +++ b/server/src/configs/jwt.config.ts @@ -0,0 +1,15 @@ +import type { ConfigService } from '@nestjs/config'; +import type { JwtModuleOptions } from '@nestjs/jwt'; + +export function getJwtConfig(configService: ConfigService): JwtModuleOptions { + return { + secret: configService.getOrThrow('JWT_SECRET'), + signOptions: { + algorithm: 'HS256', + }, + verifyOptions: { + algorithms: ['HS256'], + ignoreExpiration: false, + }, + }; +} diff --git a/server/src/main.ts b/server/src/main.ts index ff7d2ee..bd1ba50 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -1,8 +1,39 @@ +import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; -import { AppModule } from './app.module'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import * as process from 'node:process'; +import * as cookieParser from 'cookie-parser'; +import { AppModule } from './modules/app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); + + const config = new DocumentBuilder() + .setTitle('Investments Control API') + .setDescription('Documentation for **Investments Control API**') + .setVersion(process.env.npm_package_version ?? '0.0.0') + .setContact('Krylov Sergey', 'https://ksv741.tech', 'ksv741@mail.ru') + .build(); + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('/docs', app, document, { + jsonDocumentUrl: 'docs/swagger.json', + yamlDocumentUrl: 'docs/swagger.yaml', + customSiteTitle: 'Investments Control API', + }); + + app.use(cookieParser()); + app.useGlobalPipes( + new ValidationPipe({ + transform: true, + }), + ); + + app.enableCors({ + origin: 'http://localhost:3001', + credentials: true, + exposedHeaders: ['set-cookie'], + }); + await app.listen(process.env.PORT ?? 3000); } void bootstrap(); diff --git a/server/src/modules/app.module.ts b/server/src/modules/app.module.ts new file mode 100644 index 0000000..d9f2b42 --- /dev/null +++ b/server/src/modules/app.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { PrismaModule } from './prisma/prisma.module'; +import { AuthModule } from './auth/auth.module'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + }), + PrismaModule, + AuthModule, + ], +}) +export class AppModule {} diff --git a/server/src/modules/auth/auth.controller.ts b/server/src/modules/auth/auth.controller.ts new file mode 100644 index 0000000..80e4a8a --- /dev/null +++ b/server/src/modules/auth/auth.controller.ts @@ -0,0 +1,109 @@ +import { + Body, + Controller, Get, HttpCode, HttpStatus, + Post, Req, Res, +} from '@nestjs/common'; +import { + ApiBadRequestResponse, + ApiConflictResponse, ApiNotFoundResponse, + ApiOkResponse, + ApiOperation, ApiUnauthorizedResponse, +} from '@nestjs/swagger'; +import { Account } from '@prisma/client'; +import type { Response } from 'express'; +import { AuthService } from './auth.service'; +import { Authorization } from './decorators/authorization.decorator'; +import { Authorized } from './decorators/authorized.decorator'; +import { + LoginDto, + LoginValidationDto, + IncorrectLoginDto, + SuccessLoginDto, +} from './dto/login'; +import { CurrentProfileDto } from './dto/profile'; +import { + RegisterValidationDto, + ConflictRegisterDto, + RegisterDto, +} from './dto/register'; +import { Request } from './types/request'; + +@Controller('auth') +export class AuthController { + constructor(private readonly authService: AuthService) {} + + @ApiOperation({ + summary: 'Регистрация', + description: 'Регистрация аккаунта', + }) + @ApiOkResponse({ description: 'Успешная регистрация', type: RegisterDto }) + @ApiBadRequestResponse({ + description: 'Ошибка валидации', + type: RegisterValidationDto, + }) + @ApiConflictResponse({ + description: 'Ошибка создания аккаунта', + type: ConflictRegisterDto, + }) + @Post('register') + public async register(@Body() body: RegisterDto) { + return this.authService.register(body); + } + + @ApiOperation({ + summary: 'Логин', + description: 'Вход в аккаунт', + }) + @ApiOkResponse({ description: 'Успешный вход в аккаунт', type: SuccessLoginDto }) + @ApiBadRequestResponse({ + description: 'Неверный логин или пароль', + type: IncorrectLoginDto, + }) + @ApiBadRequestResponse({ + description: 'Ошибка валидации', + type: LoginValidationDto, + }) + @Post('login') + public async login( + @Res({ passthrough: true }) response: Response, + @Body() body: LoginDto, + ) { + return this.authService.login(response, body); + } + + @ApiOperation({ + summary: 'Логаут', + description: 'Выход из аккаунта', + }) + @ApiOkResponse({ description: 'Успешный выход из аккаунт', type: Boolean }) + @Authorization() + @Post('logout') + public logout(@Res({ passthrough: true }) response: Response) { + return this.authService.logout(response); + } + + @ApiOperation({ + summary: 'Получить профиль', + description: 'Получить данные о своем профиле', + }) + @ApiOkResponse({ description: 'Успешное обновление токенов', type: CurrentProfileDto }) + @ApiUnauthorizedResponse({ description: 'Пользователь не авторизован' }) + @Authorization() + @Get('me') + @HttpCode(HttpStatus.OK) + public me(@Authorized('id') user: Account['id']) { + return { id: user }; + } + + @ApiOperation({ summary: 'Обновление access и refresh токенов' }) + @ApiOkResponse({ description: 'Успешное обновление токенов', type: SuccessLoginDto }) + @ApiUnauthorizedResponse({ description: 'Пользователь не авторизован' }) + @ApiNotFoundResponse({ description: '' }) + @Post('refresh') + public async refresh( + @Req() request: Request, + @Res({ passthrough: true }) response: Response, + ) { + return this.authService.refresh(request, response); + } +} diff --git a/server/src/modules/auth/auth.module.ts b/server/src/modules/auth/auth.module.ts new file mode 100644 index 0000000..c031dae --- /dev/null +++ b/server/src/modules/auth/auth.module.ts @@ -0,0 +1,22 @@ +import { Module } from '@nestjs/common'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +import { getJwtConfig } from '../../configs/jwt.config'; +import { AuthService } from './auth.service'; +import { AuthController } from './auth.controller'; +import { JwtStrategy } from './strategies/jwt.strategies'; + +@Module({ + imports: [ + PassportModule, + JwtModule.registerAsync({ + imports: [ConfigModule], + useFactory: getJwtConfig, + inject: [ConfigService], + }), + ], + controllers: [AuthController], + providers: [AuthService, JwtStrategy], +}) +export class AuthModule {} diff --git a/server/src/modules/auth/auth.service.ts b/server/src/modules/auth/auth.service.ts new file mode 100644 index 0000000..442bb1c --- /dev/null +++ b/server/src/modules/auth/auth.service.ts @@ -0,0 +1,154 @@ +import { + BadRequestException, ConflictException, Injectable, NotFoundException, UnauthorizedException, +} from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { JwtService } from '@nestjs/jwt'; +import { Account } from '@prisma/client'; +import * as bcrypt from 'bcrypt'; +import type { Response } from 'express'; +import { PrismaService } from '../prisma/prisma.service'; +import { LoginDto, SuccessLoginDto } from './dto/login'; +import { RegisterDto } from './dto/register'; +import { JwtPayload } from './types/jwt-payload'; +import { Request } from './types/request'; + +@Injectable() +export class AuthService { + private readonly COOKIE_DOMAIN: string; + + private readonly JWT_ACCESS_TOKEN_TTL: string; + + private readonly JWT_REFRESH_TOKEN_TTL: string; + + constructor( + private readonly configService: ConfigService, + private readonly prismaService: PrismaService, + private readonly jwtService: JwtService, + ) { + this.COOKIE_DOMAIN = this.configService.getOrThrow('COOKIE_DOMAIN'); + this.JWT_ACCESS_TOKEN_TTL = this.configService.getOrThrow('JWT_ACCESS_TOKEN_TTL'); + this.JWT_REFRESH_TOKEN_TTL = this.configService.getOrThrow('JWT_REFRESH_TOKEN_TTL'); + } + + public async register(dto: RegisterDto) { + const { password, email } = dto; + + const salt = await bcrypt.genSalt(); + const hashedPassword = await bcrypt.hash(password, salt); + + const existingUser = await this.prismaService.account.findUnique({ + where: { email }, + }); + + if (existingUser) { + throw new ConflictException('Пользователь с такой почтой уже зарегистрирован'); + } + + const user = await this.prismaService.account.create({ + data: { + email, + password: hashedPassword, + }, + }); + + return { + id: user.id, + email: user.email, + }; + } + + public async login(response: Response, dto: LoginDto): Promise { + const { email, password } = dto; + const existingUser = await this.prismaService.account.findUnique({ + where: { email }, + }); + + if (!existingUser) { + throw new BadRequestException('Неверный логин или пароль'); + } + + const isMatch = await bcrypt.compare(password, existingUser.password); + if (!isMatch) { + throw new BadRequestException('Неверный логин или пароль'); + } + + const { accessToken } = this.auth(response, existingUser.id); + + return { + accessToken, + }; + } + + public logout(response: Response) { + this.setCookie(response, 'refreshToken', new Date(0)); + + return true; + } + + public async validate(id: string) { + const existingUser = await this.prismaService.account.findUnique({ + where: { + id, + }, + }); + + if (!existingUser) { + throw new NotFoundException(); + } + + return existingUser; + } + + public async refresh(req: Request, res: Response): Promise { + const { refreshToken } = req.cookies; + + if (!refreshToken) { + throw new UnauthorizedException(); + } + + const payload: JwtPayload = await this.jwtService.verifyAsync(refreshToken); + const existingUser = await this.prismaService.account.findUnique({ + where: { id: payload.id }, + }); + + if (!existingUser) { + throw new NotFoundException('Пользователь не нейден'); + } + + return this.auth(res, existingUser.id); + } + + private auth(response: Response, id: Account['id']) { + const { accessToken, refreshToken } = this.generateTokens(id); + + this.setCookie(response, refreshToken, new Date(Date.now() + 60 * 60 * 24 * 7)); + + return { accessToken }; + } + + private generateTokens(id: Account['id']) { + const payload: JwtPayload = { id }; + + const accessToken = this.jwtService.sign(payload, { + expiresIn: this.JWT_ACCESS_TOKEN_TTL, + }); + const refreshToken = this.jwtService.sign(payload, { + expiresIn: this.JWT_REFRESH_TOKEN_TTL, + }); + + return { + accessToken, + refreshToken, + }; + } + + private setCookie(res: Response, value: string, expires: Date) { + res.cookie('refreshToken', value, { + httpOnly: true, + domain: this.COOKIE_DOMAIN, + expires, + secure: false, // todo + sameSite: 'lax', // todo + }); + } +} diff --git a/server/src/modules/auth/decorators/authorization.decorator.ts b/server/src/modules/auth/decorators/authorization.decorator.ts new file mode 100644 index 0000000..2c15073 --- /dev/null +++ b/server/src/modules/auth/decorators/authorization.decorator.ts @@ -0,0 +1,6 @@ +import { applyDecorators, UseGuards } from '@nestjs/common'; +import { JwtGuard } from '../guards/jwt.guard'; + +export function Authorization() { + return applyDecorators(UseGuards(JwtGuard)); +} diff --git a/server/src/modules/auth/decorators/authorized.decorator.ts b/server/src/modules/auth/decorators/authorized.decorator.ts new file mode 100644 index 0000000..99dbb3d --- /dev/null +++ b/server/src/modules/auth/decorators/authorized.decorator.ts @@ -0,0 +1,14 @@ +import type { ExecutionContext } from '@nestjs/common'; +import { createParamDecorator } from '@nestjs/common'; +import type { Account } from '@prisma/client'; +import type { Request } from '../types/request'; + +export const Authorized = createParamDecorator( + (data: keyof Account | undefined, ctx: ExecutionContext) => { + const req: Request = ctx.switchToHttp().getRequest(); + + const { user } = req; + + return data ? user[data] : user; + }, +); diff --git a/server/src/modules/auth/dto/login/incorrect-login.dto.ts b/server/src/modules/auth/dto/login/incorrect-login.dto.ts new file mode 100644 index 0000000..fc391f5 --- /dev/null +++ b/server/src/modules/auth/dto/login/incorrect-login.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class IncorrectLoginDto { + @ApiProperty({ example: 400 }) + statusCode: number; + + @ApiProperty({ example: 'Неверный логин или пароль' }) + message: string[] | string; + + @ApiProperty({ example: 'Bad Request' }) + error: string; +} diff --git a/server/src/modules/auth/dto/login/index.ts b/server/src/modules/auth/dto/login/index.ts new file mode 100644 index 0000000..e19ad3d --- /dev/null +++ b/server/src/modules/auth/dto/login/index.ts @@ -0,0 +1,4 @@ +export { IncorrectLoginDto } from './incorrect-login.dto'; +export { LoginValidationDto } from './login-validation.dto'; +export { LoginDto } from './login.dto'; +export { SuccessLoginDto } from './success-login.dto'; diff --git a/server/src/modules/auth/dto/login/login-validation.dto.ts b/server/src/modules/auth/dto/login/login-validation.dto.ts new file mode 100644 index 0000000..ba04277 --- /dev/null +++ b/server/src/modules/auth/dto/login/login-validation.dto.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class LoginValidationDto { + @ApiProperty({ example: 400 }) + statusCode: number; + + @ApiProperty({ + example: [ + 'Email должен быть строкой.', + 'Некорректный формат email.', + 'Email обязателен для заполнения.', + 'Пароль должен быть строкой.', + 'Пароль обязателен для заполнения.', + 'Пароль должен содержать минимум 6 символов.', + ], + type: [String], + }) + message: string[]; + + @ApiProperty({ example: 'Bad Request' }) + error: string; +} diff --git a/server/src/modules/auth/dto/login/login.dto.ts b/server/src/modules/auth/dto/login/login.dto.ts new file mode 100644 index 0000000..72f7249 --- /dev/null +++ b/server/src/modules/auth/dto/login/login.dto.ts @@ -0,0 +1,31 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsEmail, + IsNotEmpty, + IsString, + MinLength, +} from 'class-validator'; + +export class LoginDto { + @ApiProperty({ + description: 'Email', + example: 'mail@example.com', + type: String, + }) + @IsString({ message: 'Email должен быть строкой.' }) + @IsEmail({}, { message: 'Некорректный формат email.' }) + @IsNotEmpty({ message: 'Email обязателен для заполнения.' }) + email: string; + + @ApiProperty({ + description: 'Пароль', + example: '123456', + type: String, + }) + @IsString({ message: 'Пароль должен быть строкой.' }) + @IsNotEmpty({ message: 'Пароль обязателен для заполнения.' }) + @MinLength(6, { + message: 'Пароль должен содержать минимум 6 символов.', + }) + password: string; +} diff --git a/server/src/modules/auth/dto/login/success-login.dto.ts b/server/src/modules/auth/dto/login/success-login.dto.ts new file mode 100644 index 0000000..6e4f206 --- /dev/null +++ b/server/src/modules/auth/dto/login/success-login.dto.ts @@ -0,0 +1,9 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class SuccessLoginDto { + @ApiProperty({ + description: 'Access токен', + type: String, + }) + accessToken: string; +} diff --git a/server/src/modules/auth/dto/profile/current-profile.dto.ts b/server/src/modules/auth/dto/profile/current-profile.dto.ts new file mode 100644 index 0000000..2e7042d --- /dev/null +++ b/server/src/modules/auth/dto/profile/current-profile.dto.ts @@ -0,0 +1,10 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Account } from '@prisma/client'; + +export class CurrentProfileDto { + @ApiProperty({ + description: 'Индетификатор аккаунта', + type: String, + }) + id: Account['id']; +} diff --git a/server/src/modules/auth/dto/profile/index.ts b/server/src/modules/auth/dto/profile/index.ts new file mode 100644 index 0000000..68bedb2 --- /dev/null +++ b/server/src/modules/auth/dto/profile/index.ts @@ -0,0 +1 @@ +export { CurrentProfileDto } from './current-profile.dto'; diff --git a/server/src/modules/auth/dto/register/conflict-register.dto.ts b/server/src/modules/auth/dto/register/conflict-register.dto.ts new file mode 100644 index 0000000..4781e41 --- /dev/null +++ b/server/src/modules/auth/dto/register/conflict-register.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class ConflictRegisterDto { + @ApiProperty({ example: 400 }) + statusCode: number; + + @ApiProperty({ example: 'Пользователь с такой почтой уже зарегистрирован' }) + message: string[] | string; + + @ApiProperty({ example: 'Conflict' }) + error: string; +} diff --git a/server/src/modules/auth/dto/register/index.ts b/server/src/modules/auth/dto/register/index.ts new file mode 100644 index 0000000..0e0fd1d --- /dev/null +++ b/server/src/modules/auth/dto/register/index.ts @@ -0,0 +1,3 @@ +export { ConflictRegisterDto } from './conflict-register.dto'; +export { RegisterValidationDto } from './register-validation.dto'; +export { RegisterDto } from './register.dto'; diff --git a/server/src/modules/auth/dto/register/register-validation.dto.ts b/server/src/modules/auth/dto/register/register-validation.dto.ts new file mode 100644 index 0000000..b71c4b6 --- /dev/null +++ b/server/src/modules/auth/dto/register/register-validation.dto.ts @@ -0,0 +1,22 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class RegisterValidationDto { + @ApiProperty({ example: 400 }) + statusCode: number; + + @ApiProperty({ + example: [ + 'Email должен быть строкой.', + 'Некорректный формат email.', + 'Email обязателен для заполнения.', + 'Пароль должен быть строкой.', + 'Пароль обязателен для заполнения.', + 'Пароль должен содержать минимум 6 символов.', + ], + type: [String], + }) + message: string[]; + + @ApiProperty({ example: 'Bad Request' }) + error: string; +} diff --git a/server/src/modules/auth/dto/register/register.dto.ts b/server/src/modules/auth/dto/register/register.dto.ts new file mode 100644 index 0000000..3d2813e --- /dev/null +++ b/server/src/modules/auth/dto/register/register.dto.ts @@ -0,0 +1,31 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { + IsEmail, + IsNotEmpty, + IsString, + MinLength, +} from 'class-validator'; + +export class RegisterDto { + @ApiProperty({ + description: 'Email', + example: 'mail@example.com', + type: String, + }) + @IsString({ message: 'Email должен быть строкой.' }) + @IsEmail({}, { message: 'Некорректный формат email.' }) + @IsNotEmpty({ message: 'Email обязателен для заполнения.' }) + email: string; + + @ApiProperty({ + description: 'Пароль', + example: '123456', + type: String, + }) + @IsString({ message: 'Пароль должен быть строкой.' }) + @IsNotEmpty({ message: 'Пароль обязателен для заполнения.' }) + @MinLength(6, { + message: 'Пароль должен содержать минимум 6 символов.', + }) + password: string; +} diff --git a/server/src/modules/auth/guards/jwt.guard.ts b/server/src/modules/auth/guards/jwt.guard.ts new file mode 100644 index 0000000..d594762 --- /dev/null +++ b/server/src/modules/auth/guards/jwt.guard.ts @@ -0,0 +1,3 @@ +import { AuthGuard } from '@nestjs/passport'; + +export class JwtGuard extends AuthGuard('jwt') {} diff --git a/server/src/modules/auth/strategies/jwt.strategies.ts b/server/src/modules/auth/strategies/jwt.strategies.ts new file mode 100644 index 0000000..2066a42 --- /dev/null +++ b/server/src/modules/auth/strategies/jwt.strategies.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { AuthService } from '../auth.service'; + +type JwtPayload = { + id: string; +}; + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor( + private readonly authService: AuthService, + private readonly configService: ConfigService, + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + ignoreExpiration: false, + secretOrKey: configService.getOrThrow('JWT_SECRET'), + algorithms: ['HS256'], + }); + } + + async validate(payload: JwtPayload) { + return this.authService.validate(payload.id); + } +} diff --git a/server/src/modules/auth/types/jwt-payload.ts b/server/src/modules/auth/types/jwt-payload.ts new file mode 100644 index 0000000..ce360aa --- /dev/null +++ b/server/src/modules/auth/types/jwt-payload.ts @@ -0,0 +1,5 @@ +import type { Account } from '@prisma/client'; + +export type JwtPayload = { + id: Account['id']; +}; diff --git a/server/src/modules/auth/types/request.ts b/server/src/modules/auth/types/request.ts new file mode 100644 index 0000000..e0728a2 --- /dev/null +++ b/server/src/modules/auth/types/request.ts @@ -0,0 +1,10 @@ +import type { Account } from '@prisma/client'; + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export interface Request { + user: Account; + cookies: { + [key: string]: unknown; + refreshToken?: string; + }; +} diff --git a/server/src/modules/prisma/prisma.module.ts b/server/src/modules/prisma/prisma.module.ts new file mode 100644 index 0000000..d30ce68 --- /dev/null +++ b/server/src/modules/prisma/prisma.module.ts @@ -0,0 +1,10 @@ +import { Global, Module } from '@nestjs/common'; + +import { PrismaService } from './prisma.service'; + +@Global() +@Module({ + providers: [PrismaService], + exports: [PrismaService], +}) +export class PrismaModule {} diff --git a/server/src/modules/prisma/prisma.service.ts b/server/src/modules/prisma/prisma.service.ts new file mode 100644 index 0000000..39f208e --- /dev/null +++ b/server/src/modules/prisma/prisma.service.ts @@ -0,0 +1,14 @@ +import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; +// import { PrismaClient } from '../../../prisma/generated/prisma/client'; + +@Injectable() +export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { + public async onModuleInit() { + await this.$connect(); + } + + public async onModuleDestroy() { + await this.$disconnect; + } +}