feat: major code refactor, add login and register endpoints with swagger
All checks were successful
Update changelog / changelog (push) Successful in 27s
All checks were successful
Update changelog / changelog (push) Successful in 27s
This commit is contained in:
@@ -1,3 +1,6 @@
|
|||||||
|
# Server config
|
||||||
|
ACCESS_TOKEN_PRIVATE_KEY=CHANGE_ME_TO_SOMETHING_RANDOM
|
||||||
|
|
||||||
# TypeORM specific
|
# TypeORM specific
|
||||||
# Please make sure these match with docker-compose.yml, or your own postgres server.
|
# Please make sure these match with docker-compose.yml, or your own postgres server.
|
||||||
PG_USER=kitty
|
PG_USER=kitty
|
||||||
|
|||||||
578
package-lock.json
generated
578
package-lock.json
generated
@@ -10,14 +10,21 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"swagger-jsdoc": "^6.2.8",
|
"swagger-jsdoc": "^6.2.8",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"typeorm": "0.3.27"
|
"typeorm": "0.3.27",
|
||||||
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.19.1",
|
"@types/express": "^5.0.6",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
|
"@types/lodash": "^4.17.21",
|
||||||
|
"@types/node": "^22.19.3",
|
||||||
|
"nodemon": "^3.1.11",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.2"
|
||||||
}
|
}
|
||||||
@@ -181,22 +188,135 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/body-parser": {
|
||||||
|
"version": "1.19.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||||
|
"integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/connect": "*",
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/connect": {
|
||||||
|
"version": "3.4.38",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||||
|
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/express": {
|
||||||
|
"version": "5.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz",
|
||||||
|
"integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/body-parser": "*",
|
||||||
|
"@types/express-serve-static-core": "^5.0.0",
|
||||||
|
"@types/serve-static": "^2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/express-serve-static-core": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*",
|
||||||
|
"@types/qs": "*",
|
||||||
|
"@types/range-parser": "*",
|
||||||
|
"@types/send": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/http-errors": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/jsonwebtoken": {
|
||||||
|
"version": "9.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
|
||||||
|
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/ms": "*",
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-FOvQ0YPD5NOfPgMzJihoT+Za5pdkDJWcbpuj1DjaKZIr/gxodQjY/uWEFlTNqW2ugXHUiL8lRQgw63dzKHZdeQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/ms": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.19.1",
|
"version": "22.19.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.3.tgz",
|
||||||
"integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==",
|
"integrity": "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/qs": {
|
||||||
|
"version": "6.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||||
|
"integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/range-parser": {
|
||||||
|
"version": "1.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
||||||
|
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/send": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/serve-static": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/http-errors": "*",
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/accepts": {
|
"node_modules/accepts": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||||
@@ -269,6 +389,20 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/anymatch": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"normalize-path": "^3.0.0",
|
||||||
|
"picomatch": "^2.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/app-root-path": {
|
"node_modules/app-root-path": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz",
|
||||||
@@ -332,6 +466,19 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/binary-extensions": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
|
||||||
@@ -365,6 +512,19 @@
|
|||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/braces": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fill-range": "^7.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/buffer": {
|
"node_modules/buffer": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
@@ -389,6 +549,12 @@
|
|||||||
"ieee754": "^1.2.1"
|
"ieee754": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"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/bytes": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
@@ -451,6 +617,31 @@
|
|||||||
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
|
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/chokidar": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"anymatch": "~3.1.2",
|
||||||
|
"braces": "~3.0.2",
|
||||||
|
"glob-parent": "~5.1.2",
|
||||||
|
"is-binary-path": "~2.1.0",
|
||||||
|
"is-glob": "~4.0.1",
|
||||||
|
"normalize-path": "~3.0.0",
|
||||||
|
"readdirp": "~3.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cliui": {
|
"node_modules/cliui": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||||
@@ -749,6 +940,15 @@
|
|||||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@@ -875,6 +1075,19 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fill-range": {
|
||||||
|
"version": "7.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||||
|
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"to-regex-range": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/finalhandler": {
|
"node_modules/finalhandler": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
|
||||||
@@ -947,6 +1160,21 @@
|
|||||||
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||||
|
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
@@ -1022,6 +1250,19 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/glob-parent": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"is-glob": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gopd": {
|
"node_modules/gopd": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
@@ -1034,6 +1275,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/has-flag": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/has-property-descriptors": {
|
"node_modules/has-property-descriptors": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
|
||||||
@@ -1141,6 +1392,13 @@
|
|||||||
],
|
],
|
||||||
"license": "BSD-3-Clause"
|
"license": "BSD-3-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/ignore-by-default": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/inflight": {
|
"node_modules/inflight": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
@@ -1167,6 +1425,19 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-binary-path": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"binary-extensions": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-callable": {
|
"node_modules/is-callable": {
|
||||||
"version": "1.2.7",
|
"version": "1.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
||||||
@@ -1179,6 +1450,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-extglob": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-fullwidth-code-point": {
|
"node_modules/is-fullwidth-code-point": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
@@ -1188,6 +1469,29 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-glob": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-extglob": "^2.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-number": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-promise": {
|
"node_modules/is-promise": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||||
@@ -1248,6 +1552,55 @@
|
|||||||
"js-yaml": "bin/js-yaml.js"
|
"js-yaml": "bin/js-yaml.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jsonwebtoken": {
|
||||||
|
"version": "9.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||||
|
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jws": "^4.0.1",
|
||||||
|
"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/jwa": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||||
|
"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": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jwa": "^2.0.1",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.get": {
|
"node_modules/lodash.get": {
|
||||||
"version": "4.4.2",
|
"version": "4.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
|
||||||
@@ -1255,6 +1608,18 @@
|
|||||||
"deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
|
"deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.",
|
||||||
"license": "MIT"
|
"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.isequal": {
|
"node_modules/lodash.isequal": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||||
@@ -1262,12 +1627,42 @@
|
|||||||
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||||
"license": "MIT"
|
"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.mergewith": {
|
"node_modules/lodash.mergewith": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
|
||||||
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
|
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
|
||||||
"license": "MIT"
|
"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/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "10.4.3",
|
"version": "10.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
@@ -1375,6 +1770,69 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nodemon": {
|
||||||
|
"version": "3.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz",
|
||||||
|
"integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chokidar": "^3.5.2",
|
||||||
|
"debug": "^4",
|
||||||
|
"ignore-by-default": "^1.0.1",
|
||||||
|
"minimatch": "^3.1.2",
|
||||||
|
"pstree.remy": "^1.1.8",
|
||||||
|
"semver": "^7.5.3",
|
||||||
|
"simple-update-notifier": "^2.0.0",
|
||||||
|
"supports-color": "^5.5.0",
|
||||||
|
"touch": "^3.1.0",
|
||||||
|
"undefsafe": "^2.0.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"nodemon": "bin/nodemon.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/nodemon"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nodemon/node_modules/brace-expansion": {
|
||||||
|
"version": "1.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||||
|
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/nodemon/node_modules/minimatch": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/normalize-path": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-inspect": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.4",
|
"version": "1.13.4",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
@@ -1563,6 +2021,19 @@
|
|||||||
"split2": "^4.1.0"
|
"split2": "^4.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/picomatch": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/possible-typed-array-names": {
|
"node_modules/possible-typed-array-names": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||||
@@ -1624,6 +2095,13 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pstree.remy": {
|
||||||
|
"version": "1.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||||
|
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.14.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||||
@@ -1663,6 +2141,19 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readdirp": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picomatch": "^2.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reflect-metadata": {
|
"node_modules/reflect-metadata": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
|
||||||
@@ -1720,6 +2211,18 @@
|
|||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/semver": {
|
||||||
|
"version": "7.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
|
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/send": {
|
"node_modules/send": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
|
||||||
@@ -1905,6 +2408,19 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/simple-update-notifier": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"semver": "^7.5.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/split2": {
|
"node_modules/split2": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||||
@@ -2035,6 +2551,19 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/supports-color": {
|
||||||
|
"version": "5.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||||
|
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"has-flag": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/swagger-jsdoc": {
|
"node_modules/swagger-jsdoc": {
|
||||||
"version": "6.2.8",
|
"version": "6.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
|
||||||
@@ -2148,6 +2677,19 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/to-regex-range": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-number": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/toidentifier": {
|
"node_modules/toidentifier": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
@@ -2157,6 +2699,16 @@
|
|||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/touch": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"nodetouch": "bin/nodetouch.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ts-node": {
|
"node_modules/ts-node": {
|
||||||
"version": "10.9.2",
|
"version": "10.9.2",
|
||||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||||
@@ -2363,6 +2915,13 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undefsafe": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
@@ -2684,6 +3243,15 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.20.0 || >=14"
|
"node": "^12.20.0 || >=14"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
package.json
19
package.json
@@ -1,27 +1,34 @@
|
|||||||
{
|
{
|
||||||
"name": "kittyBE",
|
"name": "kittyBE",
|
||||||
"version": "0.0.1",
|
"version": "0.0.0",
|
||||||
"description": "Your go-to place for short and memorable URLs.",
|
"description": "Your go-to place for short and memorable URLs.",
|
||||||
"type": "commonjs",
|
"type": "commonjs",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.19.1",
|
"@types/express": "^5.0.6",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
|
"@types/lodash": "^4.17.21",
|
||||||
|
"@types/node": "^22.19.3",
|
||||||
|
"nodemon": "^3.1.11",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"swagger-jsdoc": "^6.2.8",
|
"swagger-jsdoc": "^6.2.8",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"typeorm": "0.3.27"
|
"typeorm": "0.3.27",
|
||||||
|
"zod": "^4.2.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "ts-node src/index.ts",
|
"start": "ts-node src/app.ts",
|
||||||
|
"server": "ts-node src/app.ts",
|
||||||
"typeorm": "typeorm-ts-node-commonjs",
|
"typeorm": "typeorm-ts-node-commonjs",
|
||||||
"newMigration": "typeorm-ts-node-commonjs migration:generate -p -d ./src/data-source.ts",
|
"newMigration": "typeorm-ts-node-commonjs migration:generate -p -d ./src/data-source.ts",
|
||||||
"pendingMigration": "typeorm-ts-node-commonjs migration:run -d ./src/data-source.ts",
|
"pendingMigration": "typeorm-ts-node-commonjs migration:run -d ./src/data-source.ts"
|
||||||
"server": "ts-node src/app.ts"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
65
src/app.ts
65
src/app.ts
@@ -1,34 +1,70 @@
|
|||||||
import * as express from 'express';
|
import * as dotenv from 'dotenv';
|
||||||
import router from './routes';
|
|
||||||
import * as dotenv from "dotenv";
|
|
||||||
dotenv.config({ quiet: true });
|
dotenv.config({ quiet: true });
|
||||||
|
|
||||||
const app = express();
|
import express from 'express';
|
||||||
|
import { version } from '../package.json';
|
||||||
|
import miscRouter from './routes/miscRoutes';
|
||||||
|
import userRouter from './routes/userRoutes';
|
||||||
|
import { AppDataSource } from './data-source'
|
||||||
|
import inferUser from './middleware/inferUser';
|
||||||
|
|
||||||
|
AppDataSource.initialize().then(async () => {
|
||||||
|
|
||||||
|
await AppDataSource.runMigrations();
|
||||||
|
|
||||||
|
const app: express.Express = express();
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(inferUser);
|
||||||
|
app.use(miscRouter, userRouter);
|
||||||
|
|
||||||
|
if (process.env['DEBUG'] === 'true') {
|
||||||
const swaggerJsdocOpts = {
|
const swaggerJsdocOpts = {
|
||||||
failOnErrors: true,
|
failOnErrors: true,
|
||||||
definition: {
|
definition: {
|
||||||
openapi: '3.0.0',
|
openapi: '3.0.4',
|
||||||
info: {
|
info: {
|
||||||
title: 'kittyurl',
|
title: 'kittyurl API',
|
||||||
version: '0.0.1'
|
description: 'A Typescript API for the kittyurl url shortener project.',
|
||||||
|
version: version,
|
||||||
|
contact: {
|
||||||
|
name: 'Git repository for entire project',
|
||||||
|
url: 'https://gitea.7o7.cx/kittyteam/kittyurl'
|
||||||
|
},
|
||||||
|
license: {
|
||||||
|
name: 'AGPLv3',
|
||||||
|
url: 'https://www.gnu.org/licenses/agpl-3.0.en.html'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
apis: ['./src/routes/*.ts']
|
components: {
|
||||||
|
securitySchemes: {
|
||||||
|
BearerJWT: {
|
||||||
|
type: 'http',
|
||||||
|
scheme: 'bearer',
|
||||||
|
bearerFormat: 'JWT',
|
||||||
|
description: 'JWT Authorization header using the Bearer scheme.<br/>Enter your JWT from /api/v1/user/signIn to authorize.'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
security: [
|
||||||
|
{ bearerAuth: [] }
|
||||||
|
],
|
||||||
|
},
|
||||||
|
apis: ['./src/routes/*.ts', './src/schemas/*.ts']
|
||||||
};
|
};
|
||||||
const swaggerUi = require('swagger-ui-express');
|
const swaggerUi = require('swagger-ui-express');
|
||||||
const swaggerJsdoc = require('swagger-jsdoc');
|
const swaggerJsdoc = require('swagger-jsdoc');
|
||||||
const swaggerSpec = swaggerJsdoc(swaggerJsdocOpts);
|
const swaggerSpec = swaggerJsdoc(swaggerJsdocOpts);
|
||||||
|
app.use('/kttydocs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
|
||||||
app.use(express.json());
|
app.get('/kttydocs.json', (req: express.Request, res: express.Response) => {
|
||||||
app.use(router);
|
res.setHeader('Content-Type', 'application/json');
|
||||||
if (process.env.DEBUG === "true") {
|
res.send(swaggerSpec);
|
||||||
app.use('/swagger', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle 404s
|
// Handle 404s
|
||||||
// https://stackoverflow.com/a/9802006
|
// https://stackoverflow.com/a/9802006
|
||||||
app.use(function(req, res) {
|
app.use(function(req: express.Request, res: express.Response) {
|
||||||
res.status(404);
|
res.status(404);
|
||||||
|
|
||||||
if (req.accepts('json')) {
|
if (req.accepts('json')) {
|
||||||
@@ -40,3 +76,4 @@ app.use(function(req, res) {
|
|||||||
});
|
});
|
||||||
app.listen(6567, () => console.log('(HTTP Server) Listening on port 6567.'));
|
app.listen(6567, () => console.log('(HTTP Server) Listening on port 6567.'));
|
||||||
|
|
||||||
|
}).catch(error => console.log(error))
|
||||||
|
|||||||
186
src/controllers/authController.ts
Normal file
186
src/controllers/authController.ts
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { User } from '../entities/User';
|
||||||
|
import * as ms from '../schemas/miscSchema';
|
||||||
|
import * as as from '../schemas/authSchema';
|
||||||
|
import { UserService } from '../services/userService';
|
||||||
|
import { generateSha512 } from '../tools/hasher';
|
||||||
|
import * as jwt from '../tools/jwt';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles requests for user logon
|
||||||
|
*
|
||||||
|
* @param {Request} req The Express request
|
||||||
|
* @param {Response} res The Express resource
|
||||||
|
*/
|
||||||
|
export async function loginUserHandler(
|
||||||
|
req: Request<{}, {}, as.LoginRequestDTO['body']>,
|
||||||
|
res: Response
|
||||||
|
) {
|
||||||
|
const userService: UserService = new UserService();
|
||||||
|
const requestBody = req.body;
|
||||||
|
|
||||||
|
// Compare against what exists in the DB
|
||||||
|
let existingUser: User | null = await userService.findByCredentials({
|
||||||
|
name: requestBody.name
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingUser === null) {
|
||||||
|
// User not found? Return an error.
|
||||||
|
const error: ms.ErrorDTO = {
|
||||||
|
status: 'error',
|
||||||
|
error: 'User does not exist'
|
||||||
|
};
|
||||||
|
return res.status(404)
|
||||||
|
.json(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For the reasoning behind this, check the long
|
||||||
|
// comment inside of createUserHandler().
|
||||||
|
// TL;DR: This is a fail-safe in case of
|
||||||
|
// a server shutdown mid user creation.
|
||||||
|
if (existingUser.dirtyPasswordHashBit) {
|
||||||
|
existingUser.passwordHash = generateSha512(`${existingUser.id}:${existingUser.passwordHash}`);
|
||||||
|
existingUser.dirtyPasswordHashBit = false;
|
||||||
|
userService.save(existingUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute salted SHA512
|
||||||
|
const passwordHashed: string = generateSha512(`${existingUser.id}:${requestBody.password}`);
|
||||||
|
|
||||||
|
if (passwordHashed !== existingUser.passwordHash) {
|
||||||
|
// User not found? Return an error.
|
||||||
|
const error: ms.ErrorDTO = {
|
||||||
|
status: 'error',
|
||||||
|
error: 'Username <-> password pair not found'
|
||||||
|
};
|
||||||
|
return res.status(404)
|
||||||
|
.json(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// User found? Generate a JWT.
|
||||||
|
const token = jwt.signJwt(
|
||||||
|
{ sub: existingUser.id, role: existingUser.role },
|
||||||
|
'accessTokenPrivateKey',
|
||||||
|
{ expiresIn: requestBody.ttl ?? as.DEFAULT_TOKEN_LIFETIME }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Respond with UserInfoDTO
|
||||||
|
const userResponse: as.UserInfoDTO = {
|
||||||
|
status: 'ok',
|
||||||
|
name: requestBody.name,
|
||||||
|
role: existingUser.role,
|
||||||
|
token: token,
|
||||||
|
ttl: requestBody.ttl ?? as.DEFAULT_TOKEN_LIFETIME
|
||||||
|
}
|
||||||
|
return res.status(200)
|
||||||
|
.send(userResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles requests for user registration
|
||||||
|
*
|
||||||
|
* @param {Request} req The Express request
|
||||||
|
* @param {Response} res The Express resource
|
||||||
|
*/
|
||||||
|
export async function createUserHandler(
|
||||||
|
req: Request<{}, {}, as.LoginRequestDTO['body']>,
|
||||||
|
res: Response
|
||||||
|
) {
|
||||||
|
// Ideally, there would be a registration DTO which would incorporate
|
||||||
|
// some sort of CAPTCHA or CSRF token.
|
||||||
|
|
||||||
|
const userService = new UserService();
|
||||||
|
const requestBody = req.body;
|
||||||
|
|
||||||
|
// Compare against what exists in the DB
|
||||||
|
let usersFound: number = await userService.countAll(); // CAN THIS BE REPLACED WITH userService.lastId()??????
|
||||||
|
let role: number = 0; // Let the default role be a standard user.
|
||||||
|
|
||||||
|
// No users found? Make the new (and only) user an Administrator.
|
||||||
|
if (usersFound == 0)
|
||||||
|
role = 1;
|
||||||
|
// Users found? The new user should NOT be an Administrator.
|
||||||
|
// Since role by default is 0 (non-admin), nothing needs to change.
|
||||||
|
|
||||||
|
// Check if user already exists. If yes, return an error.
|
||||||
|
let existingUserId: number = await userService.findIdByCredentials({
|
||||||
|
name: requestBody.name
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingUserId >= 0) {
|
||||||
|
const error: ms.ErrorDTO = {
|
||||||
|
status: 'error',
|
||||||
|
error: 'User already exists'
|
||||||
|
};
|
||||||
|
return res.status(403)
|
||||||
|
.json(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise we're free to add him/her.
|
||||||
|
let user: User = {
|
||||||
|
id: undefined,
|
||||||
|
name: requestBody.name,
|
||||||
|
passwordHash: requestBody.password,
|
||||||
|
dirtyPasswordHashBit: true,
|
||||||
|
role: role,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
links: []
|
||||||
|
};
|
||||||
|
await userService.add(user);
|
||||||
|
|
||||||
|
// Note how we're setting the retrieved password as the password hash,
|
||||||
|
// without hashing it first. This is because we don't know ahead of time
|
||||||
|
// what id will the user have (which we could use to salt the password).
|
||||||
|
// We also could hash the password with a user's name,
|
||||||
|
// but then we would either need to deem username unchangeable,
|
||||||
|
// or prompt the user for password once he/she wants to change his/her name.
|
||||||
|
// Out of the three not ideal solutions, the first has been incorporated,
|
||||||
|
// thus we add the user, then retrieve his/her id, to then salt user's
|
||||||
|
// password with it later. This requires setting a "dirty bit" and storing
|
||||||
|
// the password as received, so in a way in "plain text". Then recalculate
|
||||||
|
// the password and remove the dirty bit here, or, in a miniscule case if
|
||||||
|
// it so happens the server crashes or gets restarted while registering a user,
|
||||||
|
// the password will be recalculated on login attempt if dirty bit is set.
|
||||||
|
// TODO: explore alternative options with queryRunner's transactions?
|
||||||
|
|
||||||
|
// Check if user added successfully.
|
||||||
|
// If so, recalculate the salted hash.
|
||||||
|
const insertedUser: User | null = await userService.findByCredentials({
|
||||||
|
name: requestBody.name
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return an error if - for whatever reason - the user has not been added.
|
||||||
|
if (insertedUser === null) {
|
||||||
|
const error: ms.ErrorDTO = {
|
||||||
|
status: 'error',
|
||||||
|
error: 'Could not add a new user',
|
||||||
|
code: 'server_error'
|
||||||
|
}
|
||||||
|
return res.status(500)
|
||||||
|
.json(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewrite the user's password to use the salted hash
|
||||||
|
const passwordHashed: string = generateSha512(`${insertedUser.id}:${requestBody.password}`);
|
||||||
|
insertedUser.passwordHash = passwordHashed;
|
||||||
|
insertedUser.dirtyPasswordHashBit = false;
|
||||||
|
await userService.save(insertedUser);
|
||||||
|
|
||||||
|
// Generate a JWT.
|
||||||
|
const token = jwt.signJwt(
|
||||||
|
{ sub: insertedUser.id, role: role },
|
||||||
|
'accessTokenPrivateKey',
|
||||||
|
{ expiresIn: requestBody.ttl ?? as.DEFAULT_TOKEN_LIFETIME }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Respond with UserInfoDTO
|
||||||
|
const userResponse: as.UserInfoDTO = {
|
||||||
|
status: 'ok',
|
||||||
|
name: requestBody.name,
|
||||||
|
role: role,
|
||||||
|
token: token,
|
||||||
|
ttl: requestBody.ttl ?? as.DEFAULT_TOKEN_LIFETIME
|
||||||
|
}
|
||||||
|
return res.status(201)
|
||||||
|
.send(userResponse);
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import "reflect-metadata"
|
import "reflect-metadata";
|
||||||
import { DataSource } from "typeorm"
|
import { DataSource } from "typeorm";
|
||||||
import * as dotenv from "dotenv";
|
import * as dotenv from "dotenv";
|
||||||
dotenv.config();
|
dotenv.config({ quiet: true });
|
||||||
|
|
||||||
export const AppDataSource = new DataSource({
|
export const AppDataSource = new DataSource({
|
||||||
type: "postgres",
|
type: "postgres",
|
||||||
host: process.env.PG_HOST ?? "localhost",
|
host: process.env.PG_HOST ?? "localhost",
|
||||||
port: parseInt(process.env.PG_PORT, 10) || 5432,
|
port: parseInt(process.env.PG_PORT!, 10) || 5432,
|
||||||
username: process.env.PG_USER ?? "kitty",
|
username: process.env.PG_USER ?? "kitty",
|
||||||
password: process.env.PG_PASS ?? "CHANGEME", // Please change your password inside of the .env file!
|
password: process.env.PG_PASS ?? "CHANGEME", // Please change your password inside of the .env file!
|
||||||
database: process.env.PG_DB ?? "kittyurl",
|
database: process.env.PG_DB ?? "kittyurl",
|
||||||
|
|||||||
@@ -1,48 +1,49 @@
|
|||||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinTable } from "typeorm"
|
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinTable } from 'typeorm'
|
||||||
import { User } from "./User"
|
import { User } from './User'
|
||||||
|
|
||||||
@Entity("links")
|
@Entity("links")
|
||||||
export class Link {
|
export class Link {
|
||||||
|
|
||||||
// Unique link id.
|
// Unique link id.
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id: number
|
id: number;
|
||||||
|
|
||||||
// Experimental: subdomain which should be a part of the short url.
|
// Experimental: subdomain which should be a part of the short url.
|
||||||
// For instance in the URL "abc.example.com/def", abc is the subdomain.
|
// For instance in the URL "abc.example.com/def", abc is the subdomain.
|
||||||
// "def.example.com/def" won't resolve to the URL that "abc.example.com/def" does.
|
// "def.example.com/def" won't resolve to the URL that "abc.example.com/def" does.
|
||||||
@Column({ nullable: true })
|
// https://stackoverflow.com/a/67535817
|
||||||
subdomain: string | null
|
@Column({ type: 'varchar', nullable: true })
|
||||||
|
subdomain: string | null;
|
||||||
|
|
||||||
// Shortened Uri.
|
// Shortened Uri.
|
||||||
@Column()
|
@Column()
|
||||||
shortUri: string
|
shortUri: string;
|
||||||
|
|
||||||
// URL to which the user should be redirected
|
// URL to which the user should be redirected
|
||||||
@Column()
|
@Column()
|
||||||
fullUrl: string
|
fullUrl: string;
|
||||||
|
|
||||||
// Unix timestamp of link creation date.
|
// Unix timestamp of link creation date.
|
||||||
@Column('bigint')
|
@Column('bigint')
|
||||||
createDate: number
|
createDate: number;
|
||||||
|
|
||||||
// Unix timestamp of when the link should expire.
|
// Unix timestamp of when the link should expire.
|
||||||
// If null, the link will never expire unless deleted.
|
// If null, the link will never expire unless deleted.
|
||||||
@Column('bigint', { nullable: true })
|
@Column('bigint', { nullable: true })
|
||||||
expiryDate: number | null
|
expiryDate: number | null;
|
||||||
|
|
||||||
// Aggregated amount of visits.
|
// Aggregated amount of visits.
|
||||||
@Column('bigint')
|
@Column('bigint')
|
||||||
visits: number
|
visits: number;
|
||||||
|
|
||||||
// Link privacy:
|
// Link privacy:
|
||||||
// - true, if link is private
|
// - true, if link is private
|
||||||
// - false, if link can be shown in a list of recent links publicly.
|
// - false, if link can be shown in a list of recent links publicly.
|
||||||
@Column()
|
@Column()
|
||||||
privacy: boolean
|
privacy: boolean;
|
||||||
|
|
||||||
// User to which the shortened URL belongs.
|
// User to which the shortened URL belongs.
|
||||||
@ManyToOne(() => User, (user) => user.links, { nullable: true })
|
@ManyToOne(() => User, (user) => user.links, { nullable: true })
|
||||||
@JoinTable()
|
@JoinTable()
|
||||||
author: User | null
|
author: User | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,35 @@
|
|||||||
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm"
|
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm'
|
||||||
import { Link } from "./Link"
|
import { Link } from './Link'
|
||||||
|
|
||||||
@Entity("users")
|
@Entity("users")
|
||||||
export class User {
|
export class User {
|
||||||
|
|
||||||
// Unique user id.
|
// Unique user id.
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id: number
|
id: number | undefined; // Is this a good idea?
|
||||||
|
|
||||||
// User name, must be unique.
|
// User name, must be unique.
|
||||||
@Column({ unique: true })
|
@Column({ unique: true })
|
||||||
name: string
|
name: string;
|
||||||
|
|
||||||
// Salted password hash.
|
// Salted password hash.
|
||||||
@Column()
|
@Column()
|
||||||
passwordHash: string
|
passwordHash: string;
|
||||||
|
|
||||||
|
// Used to tell, whether password hash should
|
||||||
|
// be recalculated (salted) on next login.
|
||||||
|
@Column()
|
||||||
|
dirtyPasswordHashBit: boolean;
|
||||||
|
|
||||||
// User role:
|
// User role:
|
||||||
// - 0 - means unprivileged user,
|
// - 0 - means unprivileged user,
|
||||||
// - 1 - means administrative user.
|
// - 1 - means administrative user.
|
||||||
@Column()
|
@Column()
|
||||||
role: number
|
role: number;
|
||||||
|
|
||||||
// Account creation date as a Unix timestamp.
|
// Account creation date as a Unix timestamp.
|
||||||
@Column('bigint')
|
@Column('bigint')
|
||||||
createdAt: number
|
createdAt: number;
|
||||||
|
|
||||||
// List of shortened URLs which belong to the user.
|
// List of shortened URLs which belong to the user.
|
||||||
@OneToMany(() => Link, (link) => link.author)
|
@OneToMany(() => Link, (link) => link.author)
|
||||||
|
|||||||
25
src/index.ts
25
src/index.ts
@@ -1,25 +0,0 @@
|
|||||||
import { AppDataSource } from "./data-source"
|
|
||||||
import { User } from "./entities/User"
|
|
||||||
import { smokeTest } from "./smoke-test";
|
|
||||||
|
|
||||||
AppDataSource.initialize().then(async () => {
|
|
||||||
|
|
||||||
// console.log("Inserting a new user into the database...");
|
|
||||||
// const user = new User();
|
|
||||||
// // user.firstName = "Timber";
|
|
||||||
// // user.lastName = "Saw";
|
|
||||||
// // user.age = 25;
|
|
||||||
// await AppDataSource.manager.save(user);
|
|
||||||
// console.log("Saved a new user with id: " + user.id);
|
|
||||||
|
|
||||||
// console.log("Loading users from the database...");
|
|
||||||
// const users = await AppDataSource.manager.find(User);
|
|
||||||
// console.log("Loaded users: ", users);
|
|
||||||
|
|
||||||
// console.log("Here you can setup and run express / fastify / any other framework.");
|
|
||||||
|
|
||||||
await AppDataSource.runMigrations();
|
|
||||||
|
|
||||||
await smokeTest(AppDataSource);
|
|
||||||
|
|
||||||
}).catch(error => console.log(error))
|
|
||||||
50
src/middleware/inferUser.ts
Normal file
50
src/middleware/inferUser.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// Heavily based on:
|
||||||
|
// https://github.com/TomDoesTech/REST-API-Tutorial-Updated/blob/7b5f040e1acd94d267df585516b33ee7e3b75f70/src/middleware/deserializeUser.ts
|
||||||
|
import { get } from 'lodash';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { verifyJwt } from '../tools/jwt';
|
||||||
|
|
||||||
|
const inferUser = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
) => {
|
||||||
|
|
||||||
|
const accessToken = get(req, 'headers.authorization', '').replace(
|
||||||
|
/^Bearer\s/,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!accessToken) return next();
|
||||||
|
|
||||||
|
const { decoded } = verifyJwt(accessToken, 'accessTokenPublicKey');
|
||||||
|
|
||||||
|
// console.log('decoded user:', decoded);
|
||||||
|
|
||||||
|
if (decoded) {
|
||||||
|
res.locals.user = decoded;
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// refresh token handling is not (yet) implemented
|
||||||
|
const refreshToken = get(req, 'headers.x-refresh');
|
||||||
|
|
||||||
|
if (expired && refreshToken) {
|
||||||
|
const newAccessToken = await reIssueAccessToken({ refreshToken });
|
||||||
|
|
||||||
|
if (newAccessToken) {
|
||||||
|
res.setHeader('x-access-token', newAccessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = verifyJwt(newAccessToken as string, 'accessTokenPublicKey');
|
||||||
|
|
||||||
|
res.locals.user = result.decoded;
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
return next();
|
||||||
|
};
|
||||||
|
|
||||||
|
export default inferUser;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
export class RevisedMigration1765488793696 implements MigrationInterface {
|
export class InitialMigration1767018509215 implements MigrationInterface {
|
||||||
name = 'RevisedMigration1765488793696'
|
name = 'InitialMigration1767018509215'
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`
|
await queryRunner.query(`
|
||||||
@@ -23,6 +23,7 @@ export class RevisedMigration1765488793696 implements MigrationInterface {
|
|||||||
"id" SERIAL NOT NULL,
|
"id" SERIAL NOT NULL,
|
||||||
"name" character varying NOT NULL,
|
"name" character varying NOT NULL,
|
||||||
"passwordHash" character varying NOT NULL,
|
"passwordHash" character varying NOT NULL,
|
||||||
|
"dirtyPasswordHashBit" boolean NOT NULL,
|
||||||
"role" integer NOT NULL,
|
"role" integer NOT NULL,
|
||||||
"createdAt" bigint NOT NULL,
|
"createdAt" bigint NOT NULL,
|
||||||
CONSTRAINT "UQ_51b8b26ac168fbe7d6f5653e6cf" UNIQUE ("name"),
|
CONSTRAINT "UQ_51b8b26ac168fbe7d6f5653e6cf" UNIQUE ("name"),
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { Router } from 'express';
|
|
||||||
|
|
||||||
const router = Router();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
*
|
|
||||||
* /:
|
|
||||||
* get:
|
|
||||||
* description: Hello world!
|
|
||||||
* tags: [Default]
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: Returns "Hello world!"
|
|
||||||
*/
|
|
||||||
router.get('/', (req, res) => {
|
|
||||||
res.send("Hello world!");
|
|
||||||
});
|
|
||||||
|
|
||||||
export default router;
|
|
||||||
30
src/routes/miscRoutes.ts
Normal file
30
src/routes/miscRoutes.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Router, Request, Response } from 'express';
|
||||||
|
|
||||||
|
const miscRouter = Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
*
|
||||||
|
* /:
|
||||||
|
* get:
|
||||||
|
* description: Hello world!
|
||||||
|
* tags: [Default]
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Returns "Hello world!"
|
||||||
|
*/
|
||||||
|
miscRouter.get('/', (req: Request, res: Response) => res.send("Hello world!"));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* /healthcheck:
|
||||||
|
* get:
|
||||||
|
* tags: [Healthcheck]
|
||||||
|
* description: Provides a response when backend is running
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: Backend is online
|
||||||
|
*/
|
||||||
|
miscRouter.get('/healthcheck', (req: Request, res: Response) => res.sendStatus(200));
|
||||||
|
|
||||||
|
export default miscRouter;
|
||||||
72
src/routes/userRoutes.ts
Normal file
72
src/routes/userRoutes.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { Router, Request, Response } from 'express';
|
||||||
|
import validateSchema from '../tools/validateSchema';
|
||||||
|
import * as ac from '../controllers/authController';
|
||||||
|
import * as as from '../schemas/authSchema';
|
||||||
|
|
||||||
|
const userRouter = Router();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
*
|
||||||
|
* /api/v1/user/signUp:
|
||||||
|
* post:
|
||||||
|
* description: Add user
|
||||||
|
* tags: [User]
|
||||||
|
* summary: Register a user
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/LoginRequestDTO'
|
||||||
|
* produces:
|
||||||
|
* - application/json
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: User created successfully
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/UserInfoDTO'
|
||||||
|
* 400:
|
||||||
|
* description: Bad request
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/ErrorDTO'
|
||||||
|
*/
|
||||||
|
userRouter.post('/api/v1/user/signUp', validateSchema(as.loginRequestSchema), ac.createUserHandler);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
*
|
||||||
|
* /api/v1/user/signIn:
|
||||||
|
* post:
|
||||||
|
* description: Log in
|
||||||
|
* tags: [User]
|
||||||
|
* summary: Log in to an account
|
||||||
|
* requestBody:
|
||||||
|
* required: true
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/LoginRequestDTO'
|
||||||
|
* produces:
|
||||||
|
* - application/json
|
||||||
|
* responses:
|
||||||
|
* 200:
|
||||||
|
* description: User logged in successfully
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/UserInfoDTO'
|
||||||
|
* 400:
|
||||||
|
* description: Wrong password/non-existent user
|
||||||
|
* content:
|
||||||
|
* application/json:
|
||||||
|
* schema:
|
||||||
|
* $ref: '#/components/schemas/ErrorDTO'
|
||||||
|
*/
|
||||||
|
userRouter.post('/api/v1/user/signIn', validateSchema(as.loginRequestSchema), ac.loginUserHandler);
|
||||||
|
|
||||||
|
export default userRouter;
|
||||||
82
src/schemas/authSchema.ts
Normal file
82
src/schemas/authSchema.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
// Maybe load this from .env? 24 hours in seconds.
|
||||||
|
export const DEFAULT_TOKEN_LIFETIME = 86_400;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* components:
|
||||||
|
* schemas:
|
||||||
|
* LoginRequestDTO:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - name
|
||||||
|
* - password
|
||||||
|
* properties:
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* default: mail@example.com or username
|
||||||
|
* password:
|
||||||
|
* type: string
|
||||||
|
* default: sha512(Hunter2)
|
||||||
|
* ttl:
|
||||||
|
* type: number
|
||||||
|
* default: 86400 # 24 hours * 60 minutes * 60 seconds
|
||||||
|
*/
|
||||||
|
const loginRequestSchemaBody = z.object({
|
||||||
|
name: z.string({
|
||||||
|
error: (e) => e.input === undefined ? 'Name is required' : 'Name must be a string'
|
||||||
|
})
|
||||||
|
.min( 3, 'Name is too short (try something longer than 3 characters)')
|
||||||
|
.max(64, 'Name is too long (try something shorter than 64 characters)'),
|
||||||
|
password: z.hash('sha512', {
|
||||||
|
error: (e) => e.input === undefined ? 'Password is required' : 'Password must be a SHA512 hash'
|
||||||
|
}),
|
||||||
|
ttl: z.number('TTL must be a number between 120 and 2 592 000 seconds')
|
||||||
|
.min( 120, 'TTL is too short (try something longer than 120 seconds)') // 120s
|
||||||
|
.max(2_592_000, 'TTL is too long (try something shorter than 30 days)') // 30d
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
|
// export type LoginRequestBodyDTO = z.TypeOf<typeof loginRequestSchemaBody>;
|
||||||
|
|
||||||
|
export const loginRequestSchema = z.object({
|
||||||
|
body: loginRequestSchemaBody
|
||||||
|
});
|
||||||
|
export type LoginRequestDTO = z.TypeOf<typeof loginRequestSchema>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @swagger
|
||||||
|
* components:
|
||||||
|
* schemas:
|
||||||
|
* UserInfoDTO:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - status
|
||||||
|
* - name
|
||||||
|
* - role
|
||||||
|
* - token
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* default: ok on success otherwise ErrorDTO with error
|
||||||
|
* name:
|
||||||
|
* type: string
|
||||||
|
* default: username
|
||||||
|
* role:
|
||||||
|
* type: number
|
||||||
|
* default: 0 # 0 - standard user, 1 - administrator
|
||||||
|
* token:
|
||||||
|
* type: string
|
||||||
|
* default: JWT
|
||||||
|
* ttl:
|
||||||
|
* type: number
|
||||||
|
* default: 86400 # 24 hours * 60 minutes * 60 seconds
|
||||||
|
*/
|
||||||
|
export type UserInfoDTO = {
|
||||||
|
status: 'ok';
|
||||||
|
name: string;
|
||||||
|
role: number;
|
||||||
|
token: string;
|
||||||
|
ttl: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
26
src/schemas/miscSchema.ts
Normal file
26
src/schemas/miscSchema.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* @openapi
|
||||||
|
* components:
|
||||||
|
* schemas:
|
||||||
|
* ErrorDTO:
|
||||||
|
* type: object
|
||||||
|
* required:
|
||||||
|
* - status
|
||||||
|
* - error
|
||||||
|
* properties:
|
||||||
|
* status:
|
||||||
|
* type: string
|
||||||
|
* default: error
|
||||||
|
* error:
|
||||||
|
* type: string
|
||||||
|
* default: error message
|
||||||
|
* code:
|
||||||
|
* type: string
|
||||||
|
* default: error code (may not be returned for every request)
|
||||||
|
*/
|
||||||
|
export type ErrorDTO = {
|
||||||
|
status: 'error';
|
||||||
|
error: string;
|
||||||
|
code?: string | undefined;
|
||||||
|
};
|
||||||
113
src/services/userService.ts
Normal file
113
src/services/userService.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { User } from '../entities/User';
|
||||||
|
import { AppDataSource } from '../data-source';
|
||||||
|
|
||||||
|
export type UserCredentials = {
|
||||||
|
name?: string | undefined;
|
||||||
|
password?: string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UserService {
|
||||||
|
dataSource = AppDataSource;
|
||||||
|
userRepo = this.dataSource.getRepository(User);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Phased out in favor of in-controller logic.
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * Register a new user. Checks if a user with provided name exists.
|
||||||
|
// * Returns `true` if user has been registered successfully.
|
||||||
|
// *
|
||||||
|
// * @param {string} name Username
|
||||||
|
// * @param {string} password User's password
|
||||||
|
// * @param {(null|number)} role User's role (0 | null = standard user, 1 = admin)
|
||||||
|
// * @return {Promise<boolean>} True if an account has been registered successfully.
|
||||||
|
// */
|
||||||
|
// async register(name: string, password: string, role: number | null): Promise<boolean> {
|
||||||
|
//
|
||||||
|
// // Check if user by this name already exists.
|
||||||
|
// // If so, return here to not create a new account.
|
||||||
|
// if (await this.userRepo.existsBy({ name: name }))
|
||||||
|
// return false;
|
||||||
|
//
|
||||||
|
// // TODO: insert "dirty" entity...
|
||||||
|
// // await this.userRepo.insert();
|
||||||
|
//
|
||||||
|
// // TODO: salt the password...
|
||||||
|
//
|
||||||
|
// return true;
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds entity id by any credentials passed.
|
||||||
|
* Returns user id (typically number > 0) on success,
|
||||||
|
* -1 on no match, and -2 on multiple matches (if that were to ever occur).
|
||||||
|
*
|
||||||
|
* @param {UserCredentials} creds The creds
|
||||||
|
* @return {Promise<number>} User id
|
||||||
|
*/
|
||||||
|
async findIdByCredentials(creds: UserCredentials): Promise<number> {
|
||||||
|
|
||||||
|
let users: User[] = await this.userRepo.find({
|
||||||
|
where: { ...creds }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (users.length === 0) return -1; // no match
|
||||||
|
else if (users.length === 1) return users[0].id!; // exact match, return id
|
||||||
|
else return -2; // sus, too many matches
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the user entity by any credentials passed.
|
||||||
|
* Returns user on success, otherwise null.
|
||||||
|
*
|
||||||
|
* @param {UserCredentials} creds The creds
|
||||||
|
* @return {(Promise<User|null>)} User entity
|
||||||
|
*/
|
||||||
|
async findByCredentials(creds: UserCredentials): Promise<User | null> {
|
||||||
|
|
||||||
|
let users: User[] = await this.userRepo.find({
|
||||||
|
where: { ...creds }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (users.length !== 1) return null; // no match, or too many matches, sus
|
||||||
|
else return users[0]; // exact match, return user
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counts all the user entities in DB.
|
||||||
|
* Used to tell whether the next created account should be an admin
|
||||||
|
* (No users in DB? That means most likely it's a fresh instance.
|
||||||
|
* Then the next user should be an admin.)
|
||||||
|
*
|
||||||
|
* @return {Promise<number>} Number of user entities in DB (0...).
|
||||||
|
*/
|
||||||
|
async countAll(): Promise<number> {
|
||||||
|
return await this.userRepo.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simply insert a new user entity.
|
||||||
|
*
|
||||||
|
* @param {User} user The user to insert
|
||||||
|
*/
|
||||||
|
async add(user: User): Promise<any> {
|
||||||
|
await this.userRepo.insert(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simply save user entity to DB.
|
||||||
|
* New user passed? New entity will get created.
|
||||||
|
* Existing entity passed? It'll get updated.
|
||||||
|
*
|
||||||
|
* @param {User} user The user to insert or update
|
||||||
|
*/
|
||||||
|
async save(user: User) {
|
||||||
|
// assert(user.id !== undefined, new Error("Passed user MUST contain an id!"));
|
||||||
|
await this.userRepo.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// export default new UserService();
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Connection } from "typeorm";
|
|
||||||
import { User } from "./entities/User";
|
|
||||||
|
|
||||||
export async function smokeTest(connection: Connection) {
|
|
||||||
const user = new User();
|
|
||||||
|
|
||||||
user.name = "admin";
|
|
||||||
user.role = "admin";
|
|
||||||
user.createdAt = Date.now();
|
|
||||||
user.passwordHash = "pretend this is a hash";
|
|
||||||
|
|
||||||
await connection.manager.save(user);
|
|
||||||
}
|
|
||||||
13
src/tools/hasher.ts
Normal file
13
src/tools/hasher.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// Credits:
|
||||||
|
// https://mojoauth.com/hashing/sha-512-in-typescript/
|
||||||
|
import { createHash } from 'crypto';
|
||||||
|
/**
|
||||||
|
* Generates a SHA-512 hash for the given input string.
|
||||||
|
* @param input - The input string to hash.
|
||||||
|
* @returns The SHA-512 hash as a hexadecimal string.
|
||||||
|
*/
|
||||||
|
export function generateSha512(input: string): string {
|
||||||
|
const hash = createHash('sha512');
|
||||||
|
hash.update(input);
|
||||||
|
return hash.digest('hex');
|
||||||
|
}
|
||||||
117
src/tools/jwt.ts
Normal file
117
src/tools/jwt.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
// Heavily based on:
|
||||||
|
// https://github.com/TomDoesTech/REST-API-Tutorial-Updated/blob/7b5f040e1acd94d267df585516b33ee7e3b75f70/src/utils/jwt.utils.ts
|
||||||
|
import * as dotenv from 'dotenv';
|
||||||
|
dotenv.config({ quiet: true });
|
||||||
|
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { DEFAULT_TOKEN_LIFETIME } from '../schemas/authSchema';
|
||||||
|
|
||||||
|
type JwtStatus = {
|
||||||
|
valid: boolean;
|
||||||
|
expired: boolean;
|
||||||
|
decoded: string | jwt.JwtPayload | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the environmental string from .env.
|
||||||
|
* Supports rewriting names to UPPER_SNAKE_CASE if isGlobal is set.
|
||||||
|
*
|
||||||
|
* @param {string} key The key
|
||||||
|
* @param {boolean} [isGlobal=true] Indicates if global
|
||||||
|
* @return {(string|undefined)} The environment string.
|
||||||
|
*/
|
||||||
|
export function getEnvString(
|
||||||
|
key: string,
|
||||||
|
isGlobal: boolean = true
|
||||||
|
): string | undefined {
|
||||||
|
let keyName: string = '';
|
||||||
|
|
||||||
|
if (isGlobal) {
|
||||||
|
// Global values are DECLARED_LIKE_THIS=...
|
||||||
|
for (let i: number = 0; i < key.length; i++) {
|
||||||
|
if (key[i].toLowerCase() === key[i]) {
|
||||||
|
// If is lowercase, skip.
|
||||||
|
keyName += key[i];
|
||||||
|
} else {
|
||||||
|
// If is uppercase, convert to snake case.
|
||||||
|
keyName += `_${key[i].toLowerCase()}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
keyName.toUpperCase();
|
||||||
|
} else {
|
||||||
|
// Non-global keys are parsed as passed
|
||||||
|
keyName = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return process.env[keyName];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign a JWT containing sub (number), role (number, 0/1), iat/exp (unix timestamp) claims.
|
||||||
|
*
|
||||||
|
* @param {Object} object The object
|
||||||
|
* @param {('accessTokenPrivateKey'|'refreshTokenPrivateKey')} keyName The key name
|
||||||
|
* @param {} options?:jwt.SignOptions|undefined The sign options undefined
|
||||||
|
* @return {string} JWT string
|
||||||
|
*/
|
||||||
|
export function signJwt(
|
||||||
|
object: Object,
|
||||||
|
keyName: 'accessTokenPrivateKey' | 'refreshTokenPrivateKey',
|
||||||
|
options?: jwt.SignOptions | undefined
|
||||||
|
): string {
|
||||||
|
|
||||||
|
// refresh tokens aren't (yet) supported
|
||||||
|
// const signingKey = Buffer.from(
|
||||||
|
// process.env[keyName]!,
|
||||||
|
// 'base64'
|
||||||
|
// ).toString('utf8');
|
||||||
|
|
||||||
|
const secret: string = getEnvString(keyName, true)!;
|
||||||
|
|
||||||
|
// Use the default expiration time of 24 hours.
|
||||||
|
if (options === undefined)
|
||||||
|
options = { expiresIn: DEFAULT_TOKEN_LIFETIME };
|
||||||
|
|
||||||
|
return jwt.sign(object, secret, {
|
||||||
|
...options, // (options && options)?
|
||||||
|
// algorithm: 'RS256', // requires a valid private key, not a secret
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a JWT against one of the keys.
|
||||||
|
* Returns JwtStatus, which contains fields for checking validity, expiry and decoded subject claim (id).
|
||||||
|
*
|
||||||
|
* @param {string} token The token
|
||||||
|
* @param {('accessTokenPublicKey'|'refreshTokenPublicKey')} keyName The key name
|
||||||
|
* @return {JwtStatus} JWT status.
|
||||||
|
*/
|
||||||
|
export function verifyJwt(
|
||||||
|
token: string,
|
||||||
|
keyName: 'accessTokenPublicKey' | 'refreshTokenPublicKey'
|
||||||
|
): JwtStatus {
|
||||||
|
|
||||||
|
// refresh tokens aren't (yet) supported
|
||||||
|
// const publicKey = Buffer.from(
|
||||||
|
// process.env[keyName]!,
|
||||||
|
// 'base64'
|
||||||
|
// ).toString('utf8');
|
||||||
|
|
||||||
|
const secret: string = getEnvString(keyName, true)!;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded: string | jwt.JwtPayload = jwt.verify(token, secret);
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
expired: false,
|
||||||
|
decoded,
|
||||||
|
};
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('JWT verify error:', e);
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
expired: e.message === 'jwt expired',
|
||||||
|
decoded: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/tools/validateSchema.ts
Normal file
40
src/tools/validateSchema.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// Heavily based on:
|
||||||
|
// https://github.com/TomDoesTech/REST-API-Tutorial-Updated/blob/7b5f040e1acd94d267df585516b33ee7e3b75f70/src/middleware/validateResource.ts
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { ErrorDTO } from '../schemas/miscSchema';
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
const validate =
|
||||||
|
(schema: z.ZodObject) =>
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
schema.parse({
|
||||||
|
body: req.body,
|
||||||
|
query: req.query,
|
||||||
|
params: req.params,
|
||||||
|
});
|
||||||
|
next();
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e instanceof z.ZodError) {
|
||||||
|
let errorResponse: ErrorDTO = {
|
||||||
|
status: 'error',
|
||||||
|
error: e.issues[0]?.message ?? 'Unknown error',
|
||||||
|
code: e.issues[0]?.code
|
||||||
|
};
|
||||||
|
return res.status(400)
|
||||||
|
.json(errorResponse);
|
||||||
|
} else {
|
||||||
|
console.log('Generic error triggered:', e);
|
||||||
|
let errorResponse: ErrorDTO = {
|
||||||
|
status: 'error',
|
||||||
|
error: 'Unknown error',
|
||||||
|
code: 'generic-error'
|
||||||
|
};
|
||||||
|
return res.status(400)
|
||||||
|
.json(errorResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default validate;
|
||||||
@@ -10,6 +10,10 @@
|
|||||||
"outDir": "./build",
|
"outDir": "./build",
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"sourceMap": true
|
"sourceMap": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"strict": true,
|
||||||
|
"strictPropertyInitialization": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user