diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..473e451 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[{package.json,*.yml}] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..959df33 --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ + +# default PORT +PORT = 8080 + +# prefix +GLOBAL_PREFIX = /api + +# db connection +DATABASE_URL = file:./dev.db + +# JWT +JWT_PRIVATE_KEY = secretOrPrivateKey +JWT_TIME_EXPIRATION = 24h + +# cookie +COOKIE_NAME = access_token +COOKIE_PRIVATE_KEY = cookieOrPrivateKey diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0c59ee2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ + +node_modules +build +dist + +# Keep environment variables out of version control +.env diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..791b30a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ + +# dev +node_modules +prisma +build +dist + +# files +*-lock.* +tsconfig.* diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..5db6cf0 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "tabWidth": 2, + "printWidth": 120, + "singleQuote": true, + "semi": true, + "useTabs": true, + "trailingComma": "all", + "jsxSingleQuote": true +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..a9b4303 --- /dev/null +++ b/README.md @@ -0,0 +1,306 @@ +# REST API Example + +This example shows how to implement a REST API using `Express` and `Prisma Client`. It uses a SQLite database file with some initial dummy data which you can find at `./prisma/dev.db`. + +### Getting started + +### 1. Install dependencies + +Clone this repository: + +```shell +git clone https://github.com/kevin-sg/rest-express-prisma.git +``` + +Install pnpm dependencies: + +```shell +cd rest-express-prisma +pnpm install +``` + +### 2. Create and seed the database + +Database environment variables `.env`: + +``` +# default SQLite +DATABASE_URL = file:./dev.db +``` + +The seed file in `prisma/seed.ts` will be executed and your database will be populated with the sample data. +Execute seed command: + +```shell +pnpm prisma:seed +``` + +
Optional: Seed the database with endpoint + +This request will reset the database and populate the database with data: + +```shell +curl http://localhost:8080/seed +``` + +
+ +### 3. Start the REST API server + +```shell +pnpm dev +``` + +The server is now running on `http://localhost:8080`. + +## Explore the data in Prisma Studio. + +Prisma comes with a built-in GUI to view and edit the data in your database. You can open it using the following command: + +```shell +pnpm prisma:studio +``` + +## Reset the database + +- Drop the database +- Create a new database +- Apply migrations +- Seed the database with data + +```shell +pnpm prisma:reset +``` + +### Environment example + +Create a new `.env`: + +```shell +# default PORT +PORT = 8080 + +# prefix +GLOBAL_PREFIX = /api + +# db connection +DATABASE_URL = file:./dev.db + +# JWT +JWT_PRIVATE_KEY = secretOrPrivateKey +JWT_TIME_EXPIRATION = 24h + +# cookie +COOKIE_NAME = access_token +COOKIE_PRIVATE_KEY = cookieOrPrivateKey +``` + +## Using the REST API + +You can access the REST API of the server using the following endpoints: + +Global prefix of environment: +`GLOBAL_PREFIX = /api` + +### `auth`: + +- `GET` + + - `/auth/revalidate` Request and revalidate new `token` + - `/auth/logout` Close session and `cookie` cleaning + +- `POST`: + - `/auth/login` Login and user authentication + - Body: + - `email: String` (required): The email of user + - `password: String` (required): The password of user + +### `user`: + +- `GET` + + - `/user` Fetch all active user and requires authentication + - `/user/:id` Fetch a single user by `id` and requires authentication + +- `POST`: + + - `/user` Fetch a create to user + - Body: + - `name: String` (required): The name of user + - `lastName: String` (optional): The last name of user + - `email: String` (required): The email of user + - `password: String` (required): The password of user + +- `PUT`: + + - `/user` Update user and requires authentication + - Body: + - `name: String` (optional): The name of user + - `lastName: String` (optional): The last name of user + - `email: String` (optional): The email of user + - `password: String` (optional): The password of user + +- `DELETE`: + - `/user` Disable a user and requires authentication + +### `post`: + +- `GET` + + - `/post` Fetch all published post and requires authentication + - `/post/:id` Fetch a single post by `id`, returns all posts and requires authentication + +- `POST`: + + - `/post` Create a new post and requires authentication + - Body: + - `title: String` (required): The unique name of the post + - `content: String` (optional): The content of post + - `published: Boolean` (optional): The status of the post and the default is `true` + - `userId: String` (optional): The `userId` is provided automatically by the user + +- `PUT`: + + - `/post/:id` Update post by `id` and requires authentication + - Body: + - `title: String` (optional): The unique name of the post + - `content: String` (optional): The content of post + - `published: Boolean` (optional): The status of the post and the default is `true` + - `userId: String` (optional): The `userId` is provided automatically by the user + +- `DELETE`: + - `/post` Delete active post and require authentication + +### Demo account + +When executing the seed command, two demo accounts are generated: + +``` +# demo 1 +email: tom@prisma.io +password: @Demo123 + +# demo 2 +email: bob@express.com +password: @Demo123 +``` + +### Switch to another database (e.g. PostgreSQL, MongoDB) + +If you want to try this example with another database than SQLite, you can adjust the the database connection in `prisma/schema.prisma` by reconfiguring the `datasource` block. + +Learn more about the different connection configurations in the [docs](https://www.prisma.io/docs/reference/database-reference/connection-urls). + +
Expand for an overview of example configurations with different databases + +### PostgreSQL + +For PostgreSQL, the connection URL has the following structure: + +```prisma +datasource db { + provider = "postgresql" + url = "postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=SCHEMA" +} +``` + +### MongoDB + +Here is an example connection string with a local MongoDB database: + +```prisma +datasource db { + provider = "mongodb" + url = "mongodb://USERNAME:PASSWORD@HOST/DATABASE?authSource=admin&retryWrites=true&w=majority" +} +``` + +
+ +### Project structure and files + +``` +rest-express-prisma +├── README.md +├── package.json +├── pnpm-lock.yaml +├── prisma +│   ├── dev.db +│   ├── migrations +│   │   ├── 20230522152146_initial +│   │   │   └── migration.sql +│   │   ├── 20230522172659_second +│   │   │   └── migration.sql +│   │   ├── 20230522203222_third +│   │   │   └── migration.sql +│   │   ├── 20230523224611_fourth +│   │   │   └── migration.sql +│   │   ├── 20230524163001_fifth +│   │   │   └── migration.sql +│   │   └── migration_lock.toml +│   ├── prisma-client.ts +│   ├── schema.prisma +│   └── seed.ts +├── src +│   ├── controllers +│   │   ├── abs.controller.ts +│   │   ├── auth.controller.ts +│   │   ├── index.ts +│   │   ├── post.controller.ts +│   │   ├── seed.controller.ts +│   │   └── user.controller.ts +│   ├── dto +│   │   ├── index.ts +│   │   ├── post +│   │   │   ├── create-post.dto.ts +│   │   │   ├── index.ts +│   │   │   └── update-post.dto.ts +│   │   └── user +│   │   ├── create-user.dto.ts +│   │   ├── index.ts +│   │   ├── login-user.dto.ts +│   │   └── update-user.dto.ts +│   ├── index.ts +│   ├── middleware +│   │   ├── authenticate.middleware.ts +│   │   ├── index.ts +│   │   └── validation.middleware.ts +│   ├── models +│   │   ├── http-exception.model.ts +│   │   ├── index.ts +│   │   ├── post.model.ts +│   │   └── user.model.ts +│   ├── routers +│   │   ├── auth.router.ts +│   │   ├── constant.ts +│   │   ├── index.ts +│   │   ├── post.router.ts +│   │   ├── seed.router.ts +│   │   └── user.router.ts +│   ├── services +│   │   ├── auth.service.ts +│   │   ├── index.ts +│   │   ├── post.service.ts +│   │   ├── seed.service.ts +│   │   └── user.service.ts +│   └── utils +│   ├── cookie.util.ts +│   ├── error-handler.util.ts +│   ├── http-status.util.ts +│   ├── index.ts +│   ├── jwt.util.ts +│   └── logger.util.ts +├── tsconfig.json +└── types + ├── environment.d.ts + ├── express.d.ts + └── jwt.d.ts +``` + +### Next steps + +- Check out the [Prisma docs](https://www.prisma.io/docs/getting-started) +- Guide for new user of [pnpm docs](https://pnpm.io/es/cli/add) +- Create issues and ask questions on [Kevin S.](https://github.com/kevin-sg/rest-express-prisma.git) + +**Author**: [Kevin S.](https://github.com/kevin-sg) diff --git a/package.json b/package.json new file mode 100644 index 0000000..5fd163b --- /dev/null +++ b/package.json @@ -0,0 +1,59 @@ +{ + "name": "rest-express-prisma", + "version": "1.0.0", + "description": "Node.js, Express.js & Prisma", + "main": "index.js", + "scripts": { + "postinstall": "tsc", + "start": "node build/src/index.js", + "dev": "tsnd -r tsconfig-paths/register src/index.ts", + "build": "tsc", + "prisma:format": "prisma generate", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:seed": "prisma db seed", + "prisma:studio": "prisma studio", + "prisma:reset": "prisma migrate reset", + "prettier:write": "prettier --write .", + "prettier:check": "prettier --check ." + }, + "prisma": { + "seed": "tsnd -r tsconfig-paths/register prisma/seed.ts" + }, + "dependencies": { + "@prisma/client": "^4.14.1", + "bcrypt": "^5.1.0", + "class-sanitizer": "^1.0.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", + "cookie-parser": "^1.4.6", + "cors": "^2.8.5", + "dotenv": "^16.0.3", + "express": "^4.18.2", + "jsonwebtoken": "^9.0.0" + }, + "devDependencies": { + "@types/bcrypt": "^5.0.0", + "@types/cookie-parser": "^1.4.3", + "@types/cors": "^2.8.13", + "@types/express": "4.17.17", + "@types/express-serve-static-core": "4.17.35", + "@types/jsonwebtoken": "^9.0.2", + "@types/morgan": "^1.9.4", + "@types/node": "^20.2.1", + "morgan": "^1.10.0", + "prettier": "^2.8.8", + "prisma": "^4.14.1", + "ts-node-dev": "^2.0.0", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.0.4", + "winston": "^3.8.2" + }, + "keywords": [ + "node", + "express", + "prisma" + ], + "author": "kevin S.", + "license": "MIT" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..c459cbd --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1610 @@ +lockfileVersion: '6.0' + +dependencies: + '@prisma/client': + specifier: ^4.14.1 + version: 4.14.1(prisma@4.14.1) + bcrypt: + specifier: ^5.1.0 + version: 5.1.0 + class-sanitizer: + specifier: ^1.0.1 + version: 1.0.1 + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.14.0 + version: 0.14.0 + cookie-parser: + specifier: ^1.4.6 + version: 1.4.6 + cors: + specifier: ^2.8.5 + version: 2.8.5 + dotenv: + specifier: ^16.0.3 + version: 16.0.3 + express: + specifier: ^4.18.2 + version: 4.18.2 + jsonwebtoken: + specifier: ^9.0.0 + version: 9.0.0 + +devDependencies: + '@types/bcrypt': + specifier: ^5.0.0 + version: 5.0.0 + '@types/cookie-parser': + specifier: ^1.4.3 + version: 1.4.3 + '@types/cors': + specifier: ^2.8.13 + version: 2.8.13 + '@types/express': + specifier: 4.17.17 + version: 4.17.17 + '@types/express-serve-static-core': + specifier: 4.17.35 + version: 4.17.35 + '@types/jsonwebtoken': + specifier: ^9.0.2 + version: 9.0.2 + '@types/morgan': + specifier: ^1.9.4 + version: 1.9.4 + '@types/node': + specifier: ^20.2.1 + version: 20.2.1 + morgan: + specifier: ^1.10.0 + version: 1.10.0 + prettier: + specifier: ^2.8.8 + version: 2.8.8 + prisma: + specifier: ^4.14.1 + version: 4.14.1 + ts-node-dev: + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.2.1)(typescript@5.0.4) + tsconfig-paths: + specifier: ^4.2.0 + version: 4.2.0 + typescript: + specifier: ^5.0.4 + version: 5.0.4 + winston: + specifier: ^3.8.2 + version: 3.8.2 + +packages: + + /@colors/colors@1.5.0: + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + dev: true + + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: true + + /@dabh/diagnostics@2.0.3: + resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==} + dependencies: + colorspace: 1.1.4 + enabled: 2.0.0 + kuler: 2.0.0 + dev: true + + /@jridgewell/resolve-uri@3.1.1: + resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + dev: true + + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@mapbox/node-pre-gyp@1.0.10: + resolution: {integrity: sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==} + hasBin: true + dependencies: + detect-libc: 2.0.1 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.6.11 + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.5.1 + tar: 6.1.15 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /@prisma/client@4.14.1(prisma@4.14.1): + resolution: {integrity: sha512-TZIswkeX1ccsHG/eN2kICzg/csXll0osK3EHu1QKd8VJ3XLcXozbNELKkCNfsCUvKJAwPdDtFCzF+O+raIVldw==} + engines: {node: '>=14.17'} + requiresBuild: true + peerDependencies: + prisma: '*' + peerDependenciesMeta: + prisma: + optional: true + dependencies: + '@prisma/engines-version': 4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c + prisma: 4.14.1 + dev: false + + /@prisma/engines-version@4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c: + resolution: {integrity: sha512-3jum8/YSudeSN0zGW5qkpz+wAN2V/NYCQ+BPjvHYDfWatLWlQkqy99toX0GysDeaUoBIJg1vaz2yKqiA3CFcQw==} + dev: false + + /@prisma/engines@4.14.1: + resolution: {integrity: sha512-APqFddPVHYmWNKqc+5J5SqrLFfOghKOLZxobmguDUacxOwdEutLsbXPVhNnpFDmuQWQFbXmrTTPoRrrF6B1MWA==} + requiresBuild: true + + /@tsconfig/node10@1.0.9: + resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} + dev: true + + /@tsconfig/node12@1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + dev: true + + /@tsconfig/node14@1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + dev: true + + /@tsconfig/node16@1.0.4: + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + dev: true + + /@types/bcrypt@5.0.0: + resolution: {integrity: sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==} + dependencies: + '@types/node': 20.2.1 + dev: true + + /@types/body-parser@1.19.2: + resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} + dependencies: + '@types/connect': 3.4.35 + '@types/node': 20.2.1 + dev: true + + /@types/connect@3.4.35: + resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} + dependencies: + '@types/node': 20.2.1 + dev: true + + /@types/cookie-parser@1.4.3: + resolution: {integrity: sha512-CqSKwFwefj4PzZ5n/iwad/bow2hTCh0FlNAeWLtQM3JA/NX/iYagIpWG2cf1bQKQ2c9gU2log5VUCrn7LDOs0w==} + dependencies: + '@types/express': 4.17.17 + dev: true + + /@types/cors@2.8.13: + resolution: {integrity: sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==} + dependencies: + '@types/node': 20.2.1 + dev: true + + /@types/express-serve-static-core@4.17.35: + resolution: {integrity: sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==} + dependencies: + '@types/node': 20.2.1 + '@types/qs': 6.9.7 + '@types/range-parser': 1.2.4 + '@types/send': 0.17.1 + dev: true + + /@types/express@4.17.17: + resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==} + dependencies: + '@types/body-parser': 1.19.2 + '@types/express-serve-static-core': 4.17.35 + '@types/qs': 6.9.7 + '@types/serve-static': 1.15.1 + dev: true + + /@types/jsonwebtoken@9.0.2: + resolution: {integrity: sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==} + dependencies: + '@types/node': 20.2.1 + dev: true + + /@types/mime@1.3.2: + resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==} + dev: true + + /@types/mime@3.0.1: + resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==} + dev: true + + /@types/morgan@1.9.4: + resolution: {integrity: sha512-cXoc4k+6+YAllH3ZHmx4hf7La1dzUk6keTR4bF4b4Sc0mZxU/zK4wO7l+ZzezXm/jkYj/qC+uYGZrarZdIVvyQ==} + dependencies: + '@types/node': 20.2.1 + dev: true + + /@types/node@20.2.1: + resolution: {integrity: sha512-DqJociPbZP1lbZ5SQPk4oag6W7AyaGMO6gSfRwq3PWl4PXTwJpRQJhDq4W0kzrg3w6tJ1SwlvGZ5uKFHY13LIg==} + dev: true + + /@types/qs@6.9.7: + resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} + dev: true + + /@types/range-parser@1.2.4: + resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==} + dev: true + + /@types/send@0.17.1: + resolution: {integrity: sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==} + dependencies: + '@types/mime': 1.3.2 + '@types/node': 20.2.1 + dev: true + + /@types/serve-static@1.15.1: + resolution: {integrity: sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==} + dependencies: + '@types/mime': 3.0.1 + '@types/node': 20.2.1 + dev: true + + /@types/strip-bom@3.0.0: + resolution: {integrity: sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==} + dev: true + + /@types/strip-json-comments@0.0.30: + resolution: {integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==} + dev: true + + /@types/triple-beam@1.3.2: + resolution: {integrity: sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==} + dev: true + + /@types/validator@13.7.17: + resolution: {integrity: sha512-aqayTNmeWrZcvnG2MG9eGYI6b7S5fl+yKgPs6bAjOTwPS316R5SxBGKvtSExfyoJU7pIeHJfsHI0Ji41RVMkvQ==} + dev: false + + /abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + dev: false + + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + dev: false + + /acorn-walk@8.2.0: + resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn@8.8.2: + resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: false + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /aproba@2.0.0: + resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + dev: false + + /are-we-there-yet@2.0.0: + resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} + engines: {node: '>=10'} + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.2 + dev: false + + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + dev: true + + /array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + dev: false + + /async@3.2.4: + resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + /basic-auth@2.0.1: + resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} + engines: {node: '>= 0.8'} + dependencies: + safe-buffer: 5.1.2 + dev: true + + /bcrypt@5.1.0: + resolution: {integrity: sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==} + engines: {node: '>= 10.0.0'} + requiresBuild: true + dependencies: + '@mapbox/node-pre-gyp': 1.0.10 + node-addon-api: 5.1.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /body-parser@1.20.1: + resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.1 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + dev: false + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + + /bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + dev: false + + /call-bind@1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.2.1 + dev: false + + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /chownr@2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + dev: false + + /class-sanitizer@1.0.1: + resolution: {integrity: sha512-E4lgSXP3nbJo5aflAsV9H/sauXxMbUr8XdriYEI6Cc3L5CtnEkSmxoAS8Rbj90Yq/s/S1ceoXGARjOnOgyyKQQ==} + dependencies: + validator: 13.9.0 + dev: false + + /class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + dev: false + + /class-validator@0.14.0: + resolution: {integrity: sha512-ct3ltplN8I9fOwUd8GrP8UQixwff129BkEtuWDKL5W45cQuLd19xqmTLu5ge78YDm/fdje6FMt0hGOhl0lii3A==} + dependencies: + '@types/validator': 13.7.17 + libphonenumber-js: 1.10.30 + validator: 13.9.0 + dev: false + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + dev: true + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + dev: true + + /color-support@1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + dev: false + + /color@3.2.1: + resolution: {integrity: sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==} + dependencies: + color-convert: 1.9.3 + color-string: 1.9.1 + dev: true + + /colorspace@1.1.4: + resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} + dependencies: + color: 3.2.1 + text-hex: 1.0.0 + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + /console-control-strings@1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + dev: false + + /content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + dev: false + + /cookie-parser@1.4.6: + resolution: {integrity: sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==} + engines: {node: '>= 0.8.0'} + dependencies: + cookie: 0.4.1 + cookie-signature: 1.0.6 + dev: false + + /cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + dev: false + + /cookie@0.4.1: + resolution: {integrity: sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==} + engines: {node: '>= 0.6'} + dev: false + + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: false + + /cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + dev: false + + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + dev: true + + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: false + + /delegates@1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + dev: false + + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dev: false + + /detect-libc@2.0.1: + resolution: {integrity: sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==} + engines: {node: '>=8'} + dev: false + + /diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dev: true + + /dotenv@16.0.3: + resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} + engines: {node: '>=12'} + dev: false + + /dynamic-dedupe@0.3.0: + resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==} + dependencies: + xtend: 4.0.2 + dev: true + + /ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: false + + /enabled@2.0.0: + resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} + dev: true + + /encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + dev: false + + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: false + + /etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + dev: false + + /express@4.18.2: + resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.1 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.5.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.2.0 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: 2.0.7 + qs: 6.11.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0 + serve-static: 1.15.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /fecha@4.2.3: + resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /finalhandler@1.2.0: + resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /fn.name@1.1.0: + resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} + dev: true + + /forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + dev: false + + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + dev: false + + /fs-minipass@2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + dev: false + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + + /gauge@3.0.2: + resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} + engines: {node: '>=10'} + dependencies: + aproba: 2.0.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + dev: false + + /get-intrinsic@1.2.1: + resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-proto: 1.0.1 + has-symbols: 1.0.3 + dev: false + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + /has-proto@1.0.1: + resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==} + engines: {node: '>= 0.4'} + dev: false + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: false + + /has-unicode@2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + dev: false + + /has@1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + dev: false + + /https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + dev: false + + /is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + dev: true + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-core-module@2.12.1: + resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==} + dependencies: + has: 1.0.3 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: false + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + dev: true + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + dev: true + + /jsonwebtoken@9.0.0: + resolution: {integrity: sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==} + engines: {node: '>=12', npm: '>=6'} + dependencies: + jws: 3.2.2 + lodash: 4.17.21 + ms: 2.1.3 + semver: 7.5.1 + dev: false + + /jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + + /jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + dev: false + + /kuler@2.0.0: + resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + dev: true + + /libphonenumber-js@1.10.30: + resolution: {integrity: sha512-PLGc+xfrQrkya/YK2/5X+bPpxRmyJBHM+xxz9krUdSgk4Vs2ZwxX5/Ow0lv3r9PDlDtNWb4u+it8MY5rZ0IyGw==} + dev: false + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: false + + /logform@2.5.1: + resolution: {integrity: sha512-9FyqAm9o9NKKfiAKfZoYo9bGXXuwMkxQiQttkT4YjjVtQVIQtK6LmVtlxmCaFswo6N4AfEkHqZTV0taDtPotNg==} + dependencies: + '@colors/colors': 1.5.0 + '@types/triple-beam': 1.3.2 + fecha: 4.2.3 + ms: 2.1.3 + safe-stable-stringify: 2.4.3 + triple-beam: 1.3.0 + dev: true + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: false + + /make-dir@3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + dependencies: + semver: 6.3.0 + dev: false + + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + dev: true + + /media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + dev: false + + /merge-descriptors@1.0.1: + resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + dev: false + + /methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + dev: false + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: true + + /minipass@3.3.6: + resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} + engines: {node: '>=8'} + dependencies: + yallist: 4.0.0 + dev: false + + /minipass@5.0.0: + resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} + engines: {node: '>=8'} + dev: false + + /minizlib@2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.6 + yallist: 4.0.0 + dev: false + + /mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + + /morgan@1.10.0: + resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==} + engines: {node: '>= 0.8.0'} + dependencies: + basic-auth: 2.0.1 + debug: 2.6.9 + depd: 2.0.0 + on-finished: 2.3.0 + on-headers: 1.0.2 + transitivePeerDependencies: + - supports-color + dev: true + + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: false + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + dev: false + + /node-addon-api@5.1.0: + resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} + dev: false + + /node-fetch@2.6.11: + resolution: {integrity: sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + + /nopt@5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + dependencies: + abbrev: 1.1.1 + dev: false + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /npmlog@5.0.1: + resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + dependencies: + are-we-there-yet: 2.0.0 + console-control-strings: 1.1.0 + gauge: 3.0.2 + set-blocking: 2.0.0 + dev: false + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + dev: false + + /object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + dev: false + + /on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: true + + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: false + + /on-headers@1.0.2: + resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} + engines: {node: '>= 0.8'} + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + + /one-time@1.0.0: + resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + dependencies: + fn.name: 1.1.0 + dev: true + + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + dev: false + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /path-to-regexp@0.1.7: + resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + dev: false + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + dev: true + + /prisma@4.14.1: + resolution: {integrity: sha512-z6hxzTMYqT9SIKlzD08dhzsLUpxjFKKsLpp5/kBDnSqiOjtUyyl/dC5tzxLcOa3jkEHQ8+RpB/fE3w8bgNP51g==} + engines: {node: '>=14.17'} + hasBin: true + requiresBuild: true + dependencies: + '@prisma/engines': 4.14.1 + + /proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + dev: false + + /qs@6.11.0: + resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: false + + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + dev: false + + /raw-body@2.5.1: + resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /resolve@1.22.2: + resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} + hasBin: true + dependencies: + is-core-module: 2.12.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: false + + /safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + dev: true + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + /safe-stable-stringify@2.4.3: + resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==} + engines: {node: '>=10'} + dev: true + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + + /semver@6.3.0: + resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} + hasBin: true + dev: false + + /semver@7.5.1: + resolution: {integrity: sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: false + + /send@0.18.0: + resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /serve-static@1.15.0: + resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 1.0.2 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.18.0 + transitivePeerDependencies: + - supports-color + dev: false + + /set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + dev: false + + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + dev: false + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.1 + object-inspect: 1.12.3 + dev: false + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + dev: false + + /simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + dependencies: + is-arrayish: 0.3.2 + dev: true + + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + + /stack-trace@0.0.10: + resolution: {integrity: sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==} + dev: true + + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: false + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: false + + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + dependencies: + safe-buffer: 5.2.1 + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: false + + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: true + + /strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: true + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /tar@6.1.15: + resolution: {integrity: sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==} + engines: {node: '>=10'} + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 5.0.0 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + dev: false + + /text-hex@1.0.0: + resolution: {integrity: sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==} + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + dev: false + + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + + /tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: true + + /triple-beam@1.3.0: + resolution: {integrity: sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==} + dev: true + + /ts-node-dev@2.0.0(@types/node@20.2.1)(typescript@5.0.4): + resolution: {integrity: sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==} + engines: {node: '>=0.8.0'} + hasBin: true + peerDependencies: + node-notifier: '*' + typescript: '*' + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + chokidar: 3.5.3 + dynamic-dedupe: 0.3.0 + minimist: 1.2.8 + mkdirp: 1.0.4 + resolve: 1.22.2 + rimraf: 2.7.1 + source-map-support: 0.5.21 + tree-kill: 1.2.2 + ts-node: 10.9.1(@types/node@20.2.1)(typescript@5.0.4) + tsconfig: 7.0.0 + typescript: 5.0.4 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + - '@types/node' + dev: true + + /ts-node@10.9.1(@types/node@20.2.1)(typescript@5.0.4): + resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.9 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.2.1 + acorn: 8.8.2 + acorn-walk: 8.2.0 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.0.4 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + + /tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + dev: true + + /tsconfig@7.0.0: + resolution: {integrity: sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==} + dependencies: + '@types/strip-bom': 3.0.0 + '@types/strip-json-comments': 0.0.30 + strip-bom: 3.0.0 + strip-json-comments: 2.0.1 + dev: true + + /type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + dev: false + + /typescript@5.0.4: + resolution: {integrity: sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==} + engines: {node: '>=12.20'} + hasBin: true + dev: true + + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + dev: false + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + /utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + dev: false + + /v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + dev: true + + /validator@13.9.0: + resolution: {integrity: sha512-B+dGG8U3fdtM0/aNK4/X8CXq/EcxU2WPrPEkJGslb47qyHsxmbggTWK0yEA4qnYVNF+nxNlN88o14hIcPmSIEA==} + engines: {node: '>= 0.10'} + dev: false + + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + dev: false + + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false + + /wide-align@1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + dependencies: + string-width: 4.2.3 + dev: false + + /winston-transport@4.5.0: + resolution: {integrity: sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q==} + engines: {node: '>= 6.4.0'} + dependencies: + logform: 2.5.1 + readable-stream: 3.6.2 + triple-beam: 1.3.0 + dev: true + + /winston@3.8.2: + resolution: {integrity: sha512-MsE1gRx1m5jdTTO9Ld/vND4krP2To+lgDoMEHGGa4HIlAUyXJtfc7CxQcGXVyz2IBpw5hbFkj2b/AtUdQwyRew==} + engines: {node: '>= 12.0.0'} + dependencies: + '@colors/colors': 1.5.0 + '@dabh/diagnostics': 2.0.3 + async: 3.2.4 + is-stream: 2.0.1 + logform: 2.5.1 + one-time: 1.0.0 + readable-stream: 3.6.2 + safe-stable-stringify: 2.4.3 + stack-trace: 0.0.10 + triple-beam: 1.3.0 + winston-transport: 4.5.0 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: true + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: false + + /yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + dev: true diff --git a/prisma/dev.db b/prisma/dev.db new file mode 100644 index 0000000..85d5c2f Binary files /dev/null and b/prisma/dev.db differ diff --git a/prisma/migrations/20230522152146_initial/migration.sql b/prisma/migrations/20230522152146_initial/migration.sql new file mode 100644 index 0000000..40a239c --- /dev/null +++ b/prisma/migrations/20230522152146_initial/migration.sql @@ -0,0 +1,19 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "email" TEXT NOT NULL, + "name" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "Post" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "title" TEXT NOT NULL, + "content" TEXT, + "published" BOOLEAN NOT NULL DEFAULT false, + "authorId" INTEGER NOT NULL, + CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); diff --git a/prisma/migrations/20230522172659_second/migration.sql b/prisma/migrations/20230522172659_second/migration.sql new file mode 100644 index 0000000..102e731 --- /dev/null +++ b/prisma/migrations/20230522172659_second/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "lastname" TEXT; diff --git a/prisma/migrations/20230522203222_third/migration.sql b/prisma/migrations/20230522203222_third/migration.sql new file mode 100644 index 0000000..cc00344 --- /dev/null +++ b/prisma/migrations/20230522203222_third/migration.sql @@ -0,0 +1,33 @@ +/* + Warnings: + + - You are about to drop the column `authorId` on the `Post` table. All the data in the column will be lost. + - You are about to drop the column `lastname` on the `User` table. All the data in the column will be lost. + - Added the required column `userId` to the `Post` table without a default value. This is not possible if the table is not empty. + +*/ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Post" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "title" TEXT NOT NULL, + "content" TEXT, + "published" BOOLEAN NOT NULL DEFAULT false, + "userId" INTEGER NOT NULL, + CONSTRAINT "Post_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_Post" ("content", "id", "published", "title") SELECT "content", "id", "published", "title" FROM "Post"; +DROP TABLE "Post"; +ALTER TABLE "new_Post" RENAME TO "Post"; +CREATE TABLE "new_User" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "email" TEXT NOT NULL, + "name" TEXT NOT NULL, + "lastName" TEXT +); +INSERT INTO "new_User" ("email", "id", "name") SELECT "email", "id", "name" FROM "User"; +DROP TABLE "User"; +ALTER TABLE "new_User" RENAME TO "User"; +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/prisma/migrations/20230523224611_fourth/migration.sql b/prisma/migrations/20230523224611_fourth/migration.sql new file mode 100644 index 0000000..5a6f61c --- /dev/null +++ b/prisma/migrations/20230523224611_fourth/migration.sql @@ -0,0 +1,16 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_User" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "email" TEXT NOT NULL, + "name" TEXT NOT NULL, + "lastName" TEXT, + "password" TEXT, + "isActive" BOOLEAN NOT NULL DEFAULT true +); +INSERT INTO "new_User" ("email", "id", "lastName", "name") SELECT "email", "id", "lastName", "name" FROM "User"; +DROP TABLE "User"; +ALTER TABLE "new_User" RENAME TO "User"; +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/prisma/migrations/20230524163001_fifth/migration.sql b/prisma/migrations/20230524163001_fifth/migration.sql new file mode 100644 index 0000000..effcfeb --- /dev/null +++ b/prisma/migrations/20230524163001_fifth/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN "token" TEXT; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..e5e5c47 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/prisma/prisma-client.ts b/prisma/prisma-client.ts new file mode 100644 index 0000000..23f9a86 --- /dev/null +++ b/prisma/prisma-client.ts @@ -0,0 +1,12 @@ +import { PrismaClient, Post, User } from '@prisma/client' + +const globalForPrisma = global as unknown as { + prisma: PrismaClient | undefined +} + +const prisma = globalForPrisma.prisma ?? new PrismaClient({ log: ['query'] }) + +if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma + +export { Post, User} +export default prisma diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..af2392e --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,31 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String + lastName String? + password String? + isActive Boolean @default(true) + token String? + posts Post[] +} + +model Post { + id Int @id @default(autoincrement()) + title String + content String? + published Boolean @default(false) + user User @relation(fields: [userId], references: [id]) + userId Int +} diff --git a/prisma/seed.ts b/prisma/seed.ts new file mode 100644 index 0000000..52002c5 --- /dev/null +++ b/prisma/seed.ts @@ -0,0 +1,5 @@ +import { SeedService} from '../src/services/seed.service' + +(async () => { + await SeedService.instance.insertData(); +})() diff --git a/src/controllers/abs.controller.ts b/src/controllers/abs.controller.ts new file mode 100644 index 0000000..18e9b12 --- /dev/null +++ b/src/controllers/abs.controller.ts @@ -0,0 +1,13 @@ +import type { Request, Response, NextFunction } from 'express'; + +export abstract class BaseController { + abstract create(req: Request, res: Response, next: NextFunction): Promise; + + abstract findAll(req: Request, res: Response, next: NextFunction): Promise; + + abstract findOne(req: Request, res: Response, next: NextFunction): Promise; + + abstract update(req: Request, res: Response, next: NextFunction): Promise; + + abstract remove(req: Request, res: Response, next: NextFunction): Promise; +} diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts new file mode 100644 index 0000000..02d1071 --- /dev/null +++ b/src/controllers/auth.controller.ts @@ -0,0 +1,46 @@ +import { cookieConfig } from '@/utils'; +import { AuthService } from '@/services'; + +import type { Request, Response, NextFunction } from 'express'; + +export class AuthController { + private static _instance: AuthController; + + private constructor() {} + + public static get instance() { + return this._instance || (this._instance = new this()); + } + + public async login(req: Request, res: Response, next: NextFunction): Promise { + try { + const { email = '', password = '@' } = req.body; + + const newSession = await AuthService.instance.login(email, password); + res.status(201).cookie(process.env.COOKIE_NAME, newSession.token, cookieConfig).json(newSession); + } catch (err) { + next(err); + } + } + + public async logout(req: Request, res: Response, next: NextFunction): Promise { + try { + await AuthService.instance.logout(Number(req.user?.id)); + res + .status(200) + .clearCookie(process.env.COOKIE_NAME) + .json({ code: 'session successfully closed', status: res.statusCode }); + } catch (err) { + next(err); + } + } + + public async revalidateSession(req: Request, res: Response, next: NextFunction): Promise { + try { + const newSession = await AuthService.instance.revalidate(Number(req.user?.id)); + res.status(200).cookie(process.env.COOKIE_NAME, newSession.token).json(newSession); + } catch (err) { + next(err); + } + } +} diff --git a/src/controllers/index.ts b/src/controllers/index.ts new file mode 100644 index 0000000..752d40a --- /dev/null +++ b/src/controllers/index.ts @@ -0,0 +1,4 @@ +export * from './auth.controller'; +export * from './post.controller'; +export * from './seed.controller'; +export * from './user.controller'; diff --git a/src/controllers/post.controller.ts b/src/controllers/post.controller.ts new file mode 100644 index 0000000..4def0e6 --- /dev/null +++ b/src/controllers/post.controller.ts @@ -0,0 +1,68 @@ +import { PostService } from '@/services'; +import { BaseController } from './abs.controller'; + +import type { Request, Response, NextFunction } from 'express'; + +export class PostController extends BaseController { + private static _instance: PostController; + + private constructor() { + super(); + } + + public static get instance(): PostController { + return this._instance || (this._instance = new this()); + } + + public async create(req: Request, res: Response, next: NextFunction): Promise { + const { user, body } = req; + + try { + const newPost = await PostService.instance.createPost(Number(user?.id), body); + res.status(201).json(newPost); + } catch (err) { + next(err); + } + } + + public async findAll(req: Request, res: Response, next: NextFunction): Promise { + try { + const posts = await PostService.instance.getAllPosts(); + res.status(200).json(posts); + } catch (err) { + console.error(err); + next(err); + } + } + + public async findOne(req: Request, res: Response, next: NextFunction): Promise { + try { + const post = await PostService.instance.getPostById(Number(req.params?.id)); + res.status(200).json(post); + } catch (err) { + next(err); + } + } + + public async update(req: Request, res: Response, next: NextFunction): Promise { + const { user, params, body } = req; + + try { + const updatedPost = await PostService.instance.updatePost(Number(user?.id), Number(params?.id), body); + res.status(201).json(updatedPost); + } catch (err) { + next(err); + } + } + + public async remove(req: Request, res: Response, next: NextFunction): Promise { + const { user, params } = req; + + try { + await PostService.instance.removePost(Number(user?.id), Number(params?.id)); + res.status(200).json({ message: 'the post was successfully deleted' }); + } catch (err) { + next(err); + } + } +} diff --git a/src/controllers/seed.controller.ts b/src/controllers/seed.controller.ts new file mode 100644 index 0000000..5547e8f --- /dev/null +++ b/src/controllers/seed.controller.ts @@ -0,0 +1,21 @@ +import { SeedService } from '@/services'; + +import type { NextFunction, Request, Response } from 'express'; + +export class SeedController { + private static _instance: SeedController; + + private constructor() {} + + public static get instance(): SeedController { + return this._instance || (this._instance = new this()); + } + + public async executeSeed(req: Request, res: Response, next: NextFunction): Promise { + try { + await SeedService.instance.insertData(); + } catch (err) { + next(err); + } + } +} diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts new file mode 100644 index 0000000..e5c8fcc --- /dev/null +++ b/src/controllers/user.controller.ts @@ -0,0 +1,65 @@ +import { BaseController } from './abs.controller'; + +import { cookieConfig } from '@/utils'; +import { UserService } from '@/services'; + +import type { Request, Response, NextFunction } from 'express'; + +export class UserController extends BaseController { + private static _instance: UserController; + + private constructor() { + super(); + } + + public static get instance() { + return this._instance || (this._instance = new this()); + } + + public async create(req: Request, res: Response, next: NextFunction): Promise { + try { + const { email = '', name = '', lastName = '', password = '@' } = req.body; + + const newUser = await UserService.instance.createUser({ email, name, lastName, password }); + res.status(201).cookie(process.env.COOKIE_NAME, newUser.token, cookieConfig).json(newUser); + } catch (err) { + next(err); + } + } + + public async findAll(req: Request, res: Response, next: NextFunction): Promise { + try { + const users = await UserService.instance.getAllUsers(); + res.status(200).json(users); + } catch (err) { + next(err); + } + } + + public async findOne(req: Request, res: Response, next: NextFunction): Promise { + try { + const user = await UserService.instance.getUserById(Number(req.params?.id)); + res.status(200).json(user); + } catch (err) { + next(err); + } + } + + public async update(req: Request, res: Response, next: NextFunction): Promise { + try { + const updatedUser = await UserService.instance.updateUser(Number(req.user.id), req.body); + res.status(201).cookie(process.env.COOKIE_NAME, updatedUser.token, cookieConfig).json(updatedUser); + } catch (err) { + next(err); + } + } + + public async remove(req: Request, res: Response, next: NextFunction): Promise { + try { + await UserService.instance.removeUser(Number(req.user?.id)); + res.status(200).json({ code: 'the user was successfully deleted', status: res.statusCode }); + } catch (err) { + next(err); + } + } +} diff --git a/src/dto/index.ts b/src/dto/index.ts new file mode 100644 index 0000000..69f3a4b --- /dev/null +++ b/src/dto/index.ts @@ -0,0 +1,2 @@ +export * from './user'; +export * from './post'; diff --git a/src/dto/post/create-post.dto.ts b/src/dto/post/create-post.dto.ts new file mode 100644 index 0000000..5e4adf9 --- /dev/null +++ b/src/dto/post/create-post.dto.ts @@ -0,0 +1,24 @@ +import { IsString, MinLength, MaxLength, IsOptional, IsBoolean, IsInt, IsNumber, IsPositive } from 'class-validator'; + +export class CreatePostDto { + @IsString() + @MinLength(4) + @MaxLength(50) + title: string; + + @IsOptional() + @IsString() + @MaxLength(250) + @MinLength(4) + content?: string; + + @IsBoolean() + @IsOptional() + published?: boolean; + + @IsInt() + @IsNumber() + @IsOptional() + @IsPositive() + userId?: number; +} diff --git a/src/dto/post/index.ts b/src/dto/post/index.ts new file mode 100644 index 0000000..4c8c5b7 --- /dev/null +++ b/src/dto/post/index.ts @@ -0,0 +1,2 @@ +export * from './create-post.dto'; +export * from './update-post.dto'; diff --git a/src/dto/post/update-post.dto.ts b/src/dto/post/update-post.dto.ts new file mode 100644 index 0000000..544a5d5 --- /dev/null +++ b/src/dto/post/update-post.dto.ts @@ -0,0 +1,25 @@ +import { IsString, MinLength, MaxLength, IsOptional, IsBoolean, IsNumber, IsInt, IsPositive } from 'class-validator'; + +export class UpdatePostDto { + @IsOptional() + @IsString() + @MaxLength(50) + @MinLength(4) + title?: string; + + @IsOptional() + @IsString() + @MaxLength(250) + @MinLength(4) + content?: string; + + @IsBoolean() + @IsOptional() + published?: boolean; + + @IsInt() + @IsNumber() + @IsOptional() + @IsPositive() + userId?: boolean; +} diff --git a/src/dto/user/create-user.dto.ts b/src/dto/user/create-user.dto.ts new file mode 100644 index 0000000..b05d477 --- /dev/null +++ b/src/dto/user/create-user.dto.ts @@ -0,0 +1,29 @@ +import { IsOptional, IsString, IsEmail, MaxLength, MinLength, Matches, IsBoolean } from 'class-validator'; + +export class CreateUserDto { + @IsEmail() + @IsString() + email: string; + + @IsString() + @MinLength(6) + @MaxLength(50) + @Matches(/(?:(?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, { + message: 'The password must have a Uppercase, lowercase letter and a number', + }) + password: string; + + @IsString() + @MinLength(3) + @MaxLength(50) + name: string; + + @IsOptional() + @IsString() + @MinLength(3) + lastName?: string; + + @IsBoolean() + @IsOptional() + isActive?: boolean; +} diff --git a/src/dto/user/index.ts b/src/dto/user/index.ts new file mode 100644 index 0000000..27a54a1 --- /dev/null +++ b/src/dto/user/index.ts @@ -0,0 +1,3 @@ +export * from './create-user.dto'; +export * from './login-user.dto'; +export * from './update-user.dto'; diff --git a/src/dto/user/login-user.dto.ts b/src/dto/user/login-user.dto.ts new file mode 100644 index 0000000..58b8e38 --- /dev/null +++ b/src/dto/user/login-user.dto.ts @@ -0,0 +1,15 @@ +import { IsEmail, IsString, Matches, MaxLength, MinLength } from 'class-validator'; + +export class LoginUserDto { + @IsString() + @IsEmail() + email: string; + + @IsString() + @MinLength(6) + @MaxLength(50) + @Matches(/(?:(?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, { + message: 'The password must have a Uppercase, lowercase letter and a number', + }) + password: string; +} diff --git a/src/dto/user/update-user.dto.ts b/src/dto/user/update-user.dto.ts new file mode 100644 index 0000000..934c2ad --- /dev/null +++ b/src/dto/user/update-user.dto.ts @@ -0,0 +1,29 @@ +import { IsEmail, IsOptional, IsString, Matches, MaxLength, MinLength } from 'class-validator'; + +export class UpdateUserDto { + @IsEmail() + @IsOptional() + @IsString() + email?: string; + + @IsOptional() + @IsString() + @MaxLength(50) + @MinLength(6) + @Matches(/(?:(?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/, { + message: 'The password must have a Uppercase, lowercase letter and a number', + }) + password?: string; + + @IsOptional() + @IsString() + @MaxLength(50) + @MinLength(3) + name?: string; + + @IsOptional() + @IsOptional() + @IsString() + @MinLength(3) + lastName?: string; +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..13c578d --- /dev/null +++ b/src/index.ts @@ -0,0 +1,44 @@ +import * as dotenv from 'dotenv'; +dotenv.config(); +import cookieParser from 'cookie-parser'; +import cors from 'cors'; +import express, { type Request, type Response } from 'express'; +import morgan from 'morgan'; + +import { routers } from './routers'; +import { logger, errorHandler } from './utils'; + +(() => { + const app = express(); + + // * middleware + app.use(cors()); + app.use(morgan('dev')); + app.use(cookieParser(process.env.COOKIE_PRIVATE_KEY)); + app.use(express.json()); + + // * routers + app.use(process.env.GLOBAL_PREFIX, routers); + app.use(process.env.GLOBAL_PREFIX, (req: Request, res: Response) => { + const URL = `${req.protocol}://${req.hostname}:${process.env.PORT}${req.baseUrl}`; + + const endpoint = { + auth: `${URL}/auth`, + user: `${URL}/user`, + post: `${URL}/post`, + }; + + res.json(endpoint); + }); + app.use('/', (req: Request, res: Response) => { + res.json({ status: 'API is running on /api' }); + }); + + // * exception handler + app.use(errorHandler); + + const PORT = +process.env.PORT! || 3000; + app.listen(PORT, () => { + logger.info(`The connection URL is: http://localhost:${PORT}`); + }); +})(); diff --git a/src/middleware/authenticate.middleware.ts b/src/middleware/authenticate.middleware.ts new file mode 100644 index 0000000..b21d4ca --- /dev/null +++ b/src/middleware/authenticate.middleware.ts @@ -0,0 +1,19 @@ +import { cookieConfig, verifyJwt } from '@/utils'; + +import type { Request, Response, NextFunction } from 'express'; + +export const authenticateMiddleware = async (req: Request, res: Response, next: NextFunction): Promise => { + const cookieToken: string = req.cookies[process.env.COOKIE_NAME] || ''; + + try { + const user = await verifyJwt(cookieToken); + if (user) { + req.user = { id: user.id, email: user.email }; + } + + next(); + } catch (err) { + res.clearCookie(process.env.COOKIE_NAME, cookieConfig); + next(err); + } +}; diff --git a/src/middleware/index.ts b/src/middleware/index.ts new file mode 100644 index 0000000..cd157e7 --- /dev/null +++ b/src/middleware/index.ts @@ -0,0 +1,2 @@ +export * from './validation.middleware'; +export * from './authenticate.middleware'; diff --git a/src/middleware/validation.middleware.ts b/src/middleware/validation.middleware.ts new file mode 100644 index 0000000..0bc3749 --- /dev/null +++ b/src/middleware/validation.middleware.ts @@ -0,0 +1,27 @@ +import { validate } from 'class-validator'; +import { sanitize } from 'class-sanitizer'; +import { ClassConstructor, plainToInstance } from 'class-transformer'; + +import { HttpStatusCode, logger } from '@/utils'; + +import type { RequestHandler } from 'express'; + +export const dtoValidationMiddleware = (dtoClass: ClassConstructor): RequestHandler => { + return function (req, res, next) { + const dtoObject = plainToInstance(dtoClass, req.body); + logger.warn(dtoObject); + + validate(dtoObject).then((errs) => { + if (errs.length) { + const validationErrors = errs.map((error) => Object.values(error.constraints)).flat(); + res + .status(400) + .json({ statusCode: res.statusCode, message: validationErrors, error: HttpStatusCode[res.statusCode] }); + } else { + sanitize(dtoObject); + req.body = dtoObject; + next(); + } + }); + }; +}; diff --git a/src/models/http-exception.model.ts b/src/models/http-exception.model.ts new file mode 100644 index 0000000..c961a6b --- /dev/null +++ b/src/models/http-exception.model.ts @@ -0,0 +1,15 @@ +import { HttpStatusCode } from '@/utils'; + +export class HttpException extends Error { + public readonly message: string; + public readonly httpCode: HttpStatusCode; + + constructor(message: string, httpCode: HttpStatusCode) { + super(message); + + this.message = message; + this.httpCode = httpCode; + + Error.captureStackTrace(this); + } +} diff --git a/src/models/index.ts b/src/models/index.ts new file mode 100644 index 0000000..67fb6f9 --- /dev/null +++ b/src/models/index.ts @@ -0,0 +1,3 @@ +export * from './http-exception.model'; +export * from './post.model'; +export * from './user.model'; diff --git a/src/models/post.model.ts b/src/models/post.model.ts new file mode 100644 index 0000000..90ade2c --- /dev/null +++ b/src/models/post.model.ts @@ -0,0 +1,7 @@ +export interface PostModel { + // id?: number; + title: string; + content?: string; + published: boolean; + userId: number; +} diff --git a/src/models/user.model.ts b/src/models/user.model.ts new file mode 100644 index 0000000..859bde2 --- /dev/null +++ b/src/models/user.model.ts @@ -0,0 +1,11 @@ +export interface UserModel { + name: string; + lastName?: string; + email: string; + password?: string; + isActive?: boolean; + + token?: string; + + // posts?: PostModel[]; +} diff --git a/src/routers/auth.router.ts b/src/routers/auth.router.ts new file mode 100644 index 0000000..8fc6a9d --- /dev/null +++ b/src/routers/auth.router.ts @@ -0,0 +1,14 @@ +import { Router } from 'express'; + +import { AuthPath } from './constant'; +import { LoginUserDto } from '@/dto'; +import { AuthController } from '@/controllers'; +import { dtoValidationMiddleware, authenticateMiddleware } from '@/middleware'; + +const authRouter = Router(); + +authRouter.post(AuthPath.Login, dtoValidationMiddleware(LoginUserDto), AuthController.instance.login); +authRouter.get(AuthPath.Logout, authenticateMiddleware, AuthController.instance.logout); +authRouter.get(AuthPath.Revalidate, authenticateMiddleware, AuthController.instance.revalidateSession); + +export default Router().use(authRouter); diff --git a/src/routers/constant.ts b/src/routers/constant.ts new file mode 100644 index 0000000..2f3916e --- /dev/null +++ b/src/routers/constant.ts @@ -0,0 +1,19 @@ +export const enum AuthPath { + Login = '/auth/login', + Logout = '/auth/logout', + Revalidate = '/auth/revalidate', +} + +export const enum UserPath { + Base = '/user', + ById = '/user/:id', +} + +export const enum PostPath { + Base = '/post', + ById = '/post/:id', +} + +export const enum SeedPath { + Base = '/seed', +} diff --git a/src/routers/index.ts b/src/routers/index.ts new file mode 100644 index 0000000..d33952f --- /dev/null +++ b/src/routers/index.ts @@ -0,0 +1,13 @@ +import { Router } from 'express'; + +export * from './constant'; +import a from './auth.router'; +import p from './post.router'; +import s from './seed.router'; +import u from './user.router'; + +const allRouters = Router(); + +allRouters.use(a).use(u).use(p).use(s); + +export const routers = Router().use(allRouters); diff --git a/src/routers/post.router.ts b/src/routers/post.router.ts new file mode 100644 index 0000000..5407a20 --- /dev/null +++ b/src/routers/post.router.ts @@ -0,0 +1,24 @@ +import { Router } from 'express'; + +import { PostPath } from './constant'; +import { PostController } from '@/controllers'; +import { CreatePostDto, UpdatePostDto } from '@/dto'; +import { dtoValidationMiddleware, authenticateMiddleware } from '@/middleware'; + +const postRouter = Router(); + +postRouter.get(PostPath.Base, authenticateMiddleware, PostController.instance.findAll); +postRouter.get(PostPath.ById, authenticateMiddleware, PostController.instance.findOne); +postRouter.post( + PostPath.Base, + [authenticateMiddleware, dtoValidationMiddleware(CreatePostDto)], + PostController.instance.create, +); +postRouter.put( + PostPath.ById, + [authenticateMiddleware, dtoValidationMiddleware(UpdatePostDto)], + PostController.instance.update, +); +postRouter.delete(PostPath.ById, authenticateMiddleware, PostController.instance.remove); + +export default postRouter; diff --git a/src/routers/seed.router.ts b/src/routers/seed.router.ts new file mode 100644 index 0000000..b78c5df --- /dev/null +++ b/src/routers/seed.router.ts @@ -0,0 +1,10 @@ +import { Router } from 'express'; + +import { SeedPath } from './constant'; +import { SeedController } from '@/controllers'; + +const seedRouter = Router(); + +seedRouter.get(SeedPath.Base, SeedController.instance.executeSeed); + +export default seedRouter; diff --git a/src/routers/user.router.ts b/src/routers/user.router.ts new file mode 100644 index 0000000..5e97e95 --- /dev/null +++ b/src/routers/user.router.ts @@ -0,0 +1,20 @@ +import { Router } from 'express'; + +import { UserPath } from './constant'; +import { UserController } from '@/controllers'; +import { CreateUserDto, UpdateUserDto } from '@/dto'; +import { dtoValidationMiddleware, authenticateMiddleware } from '@/middleware'; + +const userRouter = Router(); + +userRouter.get(UserPath.Base, authenticateMiddleware, UserController.instance.findAll); +userRouter.get(UserPath.ById, authenticateMiddleware, UserController.instance.findOne); +userRouter.post(UserPath.Base, dtoValidationMiddleware(CreateUserDto), UserController.instance.create); +userRouter.put( + UserPath.Base, + [authenticateMiddleware, dtoValidationMiddleware(UpdateUserDto)], + UserController.instance.update, +); +userRouter.delete(UserPath.Base, authenticateMiddleware, UserController.instance.remove); + +export default userRouter; diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts new file mode 100644 index 0000000..7cbf3fd --- /dev/null +++ b/src/services/auth.service.ts @@ -0,0 +1,74 @@ +import bc from 'bcrypt'; + +import { HttpStatusCode, encryptJwt } from '@/utils'; +import { HttpException, type UserModel } from '@/models'; +import prismaRepository, { type User } from '~prisma/prisma-client'; + +export class AuthService { + public static _instance: AuthService; + + private constructor() {} + + public static get instance() { + return this._instance || (this._instance = new this()); + } + + public async login(email: string, password: string): Promise { + try { + const hasUser = await prismaRepository.user.findUnique({ where: { email: email?.toLowerCase() } }); + if (!hasUser) { + await prismaRepository.$disconnect(); + throw new HttpException('please enter another email address', HttpStatusCode.BAD_REQUEST); + } + + const isValidPass = await bc.compare(password?.trim(), hasUser.password); + if (!isValidPass) { + await prismaRepository.$disconnect(); + throw new HttpException('the password is incorrect', HttpStatusCode.BAD_REQUEST); + } + + const token = await encryptJwt({ id: hasUser.id, email: hasUser.email }); + + return { ...hasUser, token }; + } catch (err) { + await prismaRepository.$disconnect(); + throw err; + } + } + + public async logout(id: number): Promise { + try { + const hasUser = await prismaRepository.user.findUnique({ + where: { id: Number(id) }, + }); + if (!hasUser) { + prismaRepository.$disconnect(); + throw new HttpException(HttpStatusCode[401], HttpStatusCode.UNAUTHORIZED); + } + + return hasUser; + } catch (err) { + prismaRepository.$disconnect(); + throw err; + } + } + + public async revalidate(id: number): Promise { + try { + const hasUser = await prismaRepository.user.findUnique({ + where: { id: Number(id) }, + select: { id: true, name: true, email: true }, + }); + if (!hasUser) { + throw new HttpException(HttpStatusCode[401], HttpStatusCode.UNAUTHORIZED); + } + + const token = await encryptJwt({ id: hasUser.id, email: hasUser.email }); + + return { ...hasUser, token }; + } catch (err) { + prismaRepository.$disconnect(); + throw err; + } + } +} diff --git a/src/services/index.ts b/src/services/index.ts new file mode 100644 index 0000000..45b6033 --- /dev/null +++ b/src/services/index.ts @@ -0,0 +1,4 @@ +export * from './auth.service'; +export * from './post.service'; +export * from './seed.service'; +export * from './user.service'; diff --git a/src/services/post.service.ts b/src/services/post.service.ts new file mode 100644 index 0000000..02aa3b3 --- /dev/null +++ b/src/services/post.service.ts @@ -0,0 +1,111 @@ +import { HttpStatusCode } from '@/utils'; +import { HttpException, type PostModel } from '@/models'; +import prismaRepository, { type Post } from '~prisma/prisma-client'; + +export class PostService { + public static _instance: PostService; + + private constructor() {} + + public static get instance() { + return this._instance || (this._instance = new this()); + } + + public async createPost(userId: number, postData: PostModel): Promise { + try { + const hasPost = await prismaRepository.post.findMany({ where: { title: postData.title.toLowerCase() } }); + if (hasPost?.length) { + await prismaRepository.$disconnect(); + throw new HttpException('please enter another name for the title', HttpStatusCode.BAD_REQUEST); + } + + const payload = { + title: postData.title.toLowerCase(), + content: postData.content?.toLowerCase() || '', + published: true, + userId: Number(userId), + }; + + return await prismaRepository.post.create({ data: payload }); + } catch (err) { + await prismaRepository.$disconnect(); + throw err; + } + } + + public async getAllPosts(): Promise { + try { + const posts = await prismaRepository.post.findMany({ where: { published: true } }); + if (!posts.length) { + prismaRepository.$disconnect(); + throw new HttpException(HttpStatusCode[422], HttpStatusCode.UNPROCESSABLE_ENTITY); + } + + return posts; + } catch (err) { + prismaRepository.$disconnect(); + throw err; + } + } + + public async getPostById(id: number): Promise { + try { + const user = await prismaRepository.post.findUnique({ + where: { id: Number(id) }, + include: { user: { select: { id: true, email: true } } }, + }); + if (!user) { + prismaRepository.$disconnect(); + throw new HttpException(HttpStatusCode[422], HttpStatusCode.UNPROCESSABLE_ENTITY); + } + + return user; + } catch (err) { + prismaRepository.$disconnect(); + throw err; + } + } + + public async updatePost(userId: number, postId: number, postData: PostModel): Promise { + console.log(userId); + try { + const hasPost = await this.getPostById(Number(postId)); + if (hasPost.userId !== Number(userId)) { + await prismaRepository.$disconnect(); + throw new HttpException(HttpStatusCode[401], HttpStatusCode.UNAUTHORIZED); + } + + const payload = { + title: postData.title?.toLowerCase() || hasPost.title, + content: postData.content?.toLowerCase() || hasPost.content, + }; + + const updatePost = await prismaRepository.post.update({ where: { id: Number(postId) }, data: { ...payload } }); + if (!updatePost) { + await prismaRepository.$disconnect(); + throw new HttpException(HttpStatusCode[422], HttpStatusCode.UNPROCESSABLE_ENTITY); + } + + return updatePost; + } catch (err) { + await prismaRepository.$disconnect(); + throw err; + } + } + + public async removePost(userId: number, postId: number): Promise { + try { + const hasPost = await this.getPostById(postId); + + if (hasPost.userId !== Number(userId)) { + await prismaRepository.$disconnect(); + throw new HttpException(HttpStatusCode[401], HttpStatusCode.UNAUTHORIZED); + } + + return await prismaRepository.post.delete({ where: { id: Number(postId) } }); + } catch (err) { + await prismaRepository.$disconnect(); + throw err; + } + } +} diff --git a/src/services/seed.service.ts b/src/services/seed.service.ts new file mode 100644 index 0000000..e960134 --- /dev/null +++ b/src/services/seed.service.ts @@ -0,0 +1,74 @@ +import bc from 'bcrypt'; + +import { logger } from '@/utils'; +import prisma from '~prisma/prisma-client'; + +export class SeedService { + private static _instance: SeedService; + + private constructor() {} + + public static get instance(): SeedService { + return this._instance || (this._instance = new this()); + } + + public async insertData(): Promise { + try { + const deleteUsers = prisma.user.deleteMany(); + const deletePosts = prisma.post.deleteMany(); + // The transaction runs synchronously so deleteUsers must run last. + await prisma.$transaction([deletePosts, deleteUsers]); + + const tom = await prisma.user.upsert({ + where: { email: 'tom@prisma.io' }, + update: {}, + create: { + email: 'tom@prisma.io', + name: 'Tom', + lastName: 'Claus', + password: bc.hashSync('@Demo123', bc.genSaltSync(10)), + isActive: true, + posts: { + create: { + title: 'Check out Prisma with Express.js', + content: 'https://www.prisma.io/express', + published: true, + }, + }, + }, + }); + const bob = await prisma.user.upsert({ + where: { email: 'bob@express.com' }, + update: {}, + create: { + email: 'bob@express.com', + name: 'Bob', + lastName: 'Otto', + password: bc.hashSync('@Demo123', bc.genSaltSync(10)), + isActive: true, + posts: { + create: [ + { + title: 'Follow Prisma on Twitter', + content: 'https://twitter.com/prisma', + published: true, + }, + { + title: 'Follow Nexus on Twitter', + content: 'https://twitter.com/nexusgql', + published: true, + }, + ], + }, + }, + }); + + await prisma.$disconnect(); + logger.info('SEED EXECUTED!!'); + } catch (err: any) { + logger.error(err.message); + await prisma.$disconnect(); + process.exit(1); + } + } +} diff --git a/src/services/user.service.ts b/src/services/user.service.ts new file mode 100644 index 0000000..bf90f25 --- /dev/null +++ b/src/services/user.service.ts @@ -0,0 +1,126 @@ +import bc from 'bcrypt'; + +import { HttpStatusCode, encryptJwt } from '@/utils'; +import { HttpException, type UserModel } from '@/models'; +import prismaRepository, { type User } from '~prisma/prisma-client'; + +export class UserService { + public static _instance: UserService; + + private constructor() {} + + public static get instance() { + return this._instance || (this._instance = new this()); + } + + public async createUser(userData: UserModel): Promise { + try { + const hasUser = await prismaRepository.user.findUnique({ where: { email: userData.email.toLowerCase() } }); + if (hasUser) { + await prismaRepository.$disconnect(); + throw new HttpException('please enter another email address', HttpStatusCode.BAD_REQUEST); + } + + const hashPss = await bc.hash(userData.password, bc.genSaltSync(10)); + + const newUser = await prismaRepository.user.create({ + data: { ...userData, password: hashPss }, + select: { id: true, name: true, lastName: true, email: true }, + }); + await prismaRepository.$disconnect(); + + const token = await encryptJwt({ id: newUser.id, email: newUser.email }); + + return { ...newUser, token }; + } catch (err) { + await prismaRepository.$disconnect(); + throw err; + } + } + + public async getAllUsers(): Promise { + try { + const users = await prismaRepository.user.findMany({ + where: { isActive: true }, + select: { id: true, name: true, email: true, lastName: true }, + }); + if (!users.length) { + prismaRepository.$disconnect(); + throw new HttpException(HttpStatusCode[422], HttpStatusCode.UNPROCESSABLE_ENTITY); + } + return users; + } catch (err) { + prismaRepository.$disconnect(); + throw err; + } + } + + public async getUserById(id: number): Promise { + try { + const user = await prismaRepository.user.findUnique({ + where: { id: Number(id) }, + select: { + id: true, + name: true, + lastName: true, + email: true, + password: true, + posts: { select: { id: true, title: true, content: true, published: true } }, + }, + }); + if (!user) { + throw new HttpException(HttpStatusCode[422], HttpStatusCode.UNPROCESSABLE_ENTITY); + } + + return user; + } catch (err) { + prismaRepository.$disconnect(); + throw err; + } + } + + public async updateUser(id: number, userData: UserModel): Promise { + try { + const hasUser = await this.getUserById(id); + + const isValidPass = await bc.compare(userData.password!.trim(), hasUser.password!); + if (!isValidPass) { + await prismaRepository.$disconnect(); + throw new HttpException('the password is incorrect', HttpStatusCode.UNAUTHORIZED); + } + + const hashPass = await bc.hash(userData.password, bc.genSaltSync(10)); + + const updateUser = await prismaRepository.user.update({ + where: { id: Number(id) }, + select: { id: true, name: true, lastName: true, email: true, token: true }, + data: { ...userData, password: hashPass }, + }); + if (!updateUser) { + await prismaRepository.$disconnect(); + throw new HttpException(HttpStatusCode[422], HttpStatusCode.UNPROCESSABLE_ENTITY); + } + + const token = await encryptJwt({ id: updateUser.id, email: updateUser.email }); + + return { ...updateUser, token }; + } catch (err) { + await prismaRepository.$disconnect(); + throw err; + } + } + + public async removeUser(id: number): Promise { + try { + const hasUser = await this.getUserById(id); + if (!hasUser.isActive) { + throw new HttpException('the user is disabled', HttpStatusCode.BAD_REQUEST); + } + + return await prismaRepository.user.update({ where: { id: Number(id) }, data: { isActive: false } }); + } catch (err) { + await prismaRepository.$disconnect(); + throw err; + } + } +} diff --git a/src/utils/cookie.util.ts b/src/utils/cookie.util.ts new file mode 100644 index 0000000..d1f2741 --- /dev/null +++ b/src/utils/cookie.util.ts @@ -0,0 +1,9 @@ +export const formatTypeToken = (token: string): string => { + return `Bearer ${token}`; +}; + +export const cookieConfig = { + httpOnly: true, + path: process.env.GLOBAL_PREFIX, + secure: process.env.NODE_ENV === 'production', +}; diff --git a/src/utils/error-handler.util.ts b/src/utils/error-handler.util.ts new file mode 100644 index 0000000..e370c4a --- /dev/null +++ b/src/utils/error-handler.util.ts @@ -0,0 +1,19 @@ +import { logger } from './logger.util'; +import { HttpException } from '@/models'; +import { HttpStatusCode } from './http-status.util'; + +import type { Request, Response, NextFunction } from 'express'; + +export const errorHandler = async (err: Error, req: Request, res: Response, next: NextFunction) => { + logger.error(err.message); + + if (err instanceof HttpException) { + return res + .status(err.httpCode) + .json({ code: err.message ? err.message : HttpStatusCode[res.statusCode], status: err.httpCode }); + } else { + // For unhandled errors. + res.status(500).json({ code: HttpStatusCode[res.statusCode], status: HttpStatusCode.INTERNAL_SERVER_ERROR }); + next(); + } +}; diff --git a/src/utils/http-status.util.ts b/src/utils/http-status.util.ts new file mode 100644 index 0000000..1e8e8a5 --- /dev/null +++ b/src/utils/http-status.util.ts @@ -0,0 +1,380 @@ +'use strict'; + +/** + * Hypertext Transfer Protocol (HTTP) response status codes. + * @see {@link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes} + */ +export enum HttpStatusCode { + /** + * The server has received the request headers and the client should proceed to send the request body + * (in the case of a request for which a body needs to be sent; for example, a POST request). + * Sending a large request body to a server after a request has been rejected for inappropriate headers would be inefficient. + * To have a server check the request's headers, a client must send Expect: 100-continue as a header in its initial request + * and receive a 100 Continue status code in response before sending the body. The response 417 Expectation Failed indicates the request should not be continued. + */ + CONTINUE = 100, + + /** + * The requester has asked the server to switch protocols and the server has agreed to do so. + */ + SWITCHING_PROTOCOLS = 101, + + /** + * A WebDAV request may contain many sub-requests involving file operations, requiring a long time to complete the request. + * This code indicates that the server has received and is processing the request, but no response is available yet. + * This prevents the client from timing out and assuming the request was lost. + */ + PROCESSING = 102, + + /** + * Standard response for successful HTTP requests. + * The actual response will depend on the request method used. + * In a GET request, the response will contain an entity corresponding to the requested resource. + * In a POST request, the response will contain an entity describing or containing the result of the action. + */ + OK = 200, + + /** + * The request has been fulfilled, resulting in the creation of a new resource. + */ + CREATED = 201, + + /** + * The request has been accepted for processing, but the processing has not been completed. + * The request might or might not be eventually acted upon, and may be disallowed when processing occurs. + */ + ACCEPTED = 202, + + /** + * SINCE HTTP/1.1 + * The server is a transforming proxy that received a 200 OK from its origin, + * but is returning a modified version of the origin's response. + */ + NON_AUTHORITATIVE_INFORMATION = 203, + + /** + * The server successfully processed the request and is not returning any content. + */ + NO_CONTENT = 204, + + /** + * The server successfully processed the request, but is not returning any content. + * Unlike a 204 response, this response requires that the requester reset the document view. + */ + RESET_CONTENT = 205, + + /** + * The server is delivering only part of the resource (byte serving) due to a range header sent by the client. + * The range header is used by HTTP clients to enable resuming of interrupted downloads, + * or split a download into multiple simultaneous streams. + */ + PARTIAL_CONTENT = 206, + + /** + * The message body that follows is an XML message and can contain a number of separate response codes, + * depending on how many sub-requests were made. + */ + MULTI_STATUS = 207, + + /** + * The members of a DAV binding have already been enumerated in a preceding part of the (multistatus) response, + * and are not being included again. + */ + ALREADY_REPORTED = 208, + + /** + * The server has fulfilled a request for the resource, + * and the response is a representation of the result of one or more instance-manipulations applied to the current instance. + */ + IM_USED = 226, + + /** + * Indicates multiple options for the resource from which the client may choose (via agent-driven content negotiation). + * For example, this code could be used to present multiple video format options, + * to list files with different filename extensions, or to suggest word-sense disambiguation. + */ + MULTIPLE_CHOICES = 300, + + /** + * This and all future requests should be directed to the given URI. + */ + MOVED_PERMANENTLY = 301, + + /** + * This is an example of industry practice contradicting the standard. + * The HTTP/1.0 specification (RFC 1945) required the client to perform a temporary redirect + * (the original describing phrase was "Moved Temporarily"), but popular browsers implemented 302 + * with the functionality of a 303 See Other. Therefore, HTTP/1.1 added status codes 303 and 307 + * to distinguish between the two behaviours. However, some Web applications and frameworks + * use the 302 status code as if it were the 303. + */ + FOUND = 302, + + /** + * SINCE HTTP/1.1 + * The response to the request can be found under another URI using a GET method. + * When received in response to a POST (or PUT/DELETE), the client should presume that + * the server has received the data and should issue a redirect with a separate GET message. + */ + SEE_OTHER = 303, + + /** + * Indicates that the resource has not been modified since the version specified by the request headers If-Modified-Since or If-None-Match. + * In such case, there is no need to retransmit the resource since the client still has a previously-downloaded copy. + */ + NOT_MODIFIED = 304, + + /** + * SINCE HTTP/1.1 + * The requested resource is available only through a proxy, the address for which is provided in the response. + * Many HTTP clients (such as Mozilla and Internet Explorer) do not correctly handle responses with this status code, primarily for security reasons. + */ + USE_PROXY = 305, + + /** + * No longer used. Originally meant "Subsequent requests should use the specified proxy." + */ + SWITCH_PROXY = 306, + + /** + * SINCE HTTP/1.1 + * In this case, the request should be repeated with another URI; however, future requests should still use the original URI. + * In contrast to how 302 was historically implemented, the request method is not allowed to be changed when reissuing the original request. + * For example, a POST request should be repeated using another POST request. + */ + TEMPORARY_REDIRECT = 307, + + /** + * The request and all future requests should be repeated using another URI. + * 307 and 308 parallel the behaviors of 302 and 301, but do not allow the HTTP method to change. + * So, for example, submitting a form to a permanently redirected resource may continue smoothly. + */ + PERMANENT_REDIRECT = 308, + + /** + * The server cannot or will not process the request due to an apparent client error + * (e.g., malformed request syntax, too large size, invalid request message framing, or deceptive request routing). + */ + BAD_REQUEST = 400, + + /** + * Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet + * been provided. The response must include a WWW-Authenticate header field containing a challenge applicable to the + * requested resource. See Basic access authentication and Digest access authentication. 401 semantically means + * "unauthenticated",i.e. the user does not have the necessary credentials. + */ + UNAUTHORIZED = 401, + + /** + * Reserved for future use. The original intention was that this code might be used as part of some form of digital + * cash or micro payment scheme, but that has not happened, and this code is not usually used. + * Google Developers API uses this status if a particular developer has exceeded the daily limit on requests. + */ + PAYMENT_REQUIRED = 402, + + /** + * The request was valid, but the server is refusing action. + * The user might not have the necessary permissions for a resource. + */ + FORBIDDEN = 403, + + /** + * The requested resource could not be found but may be available in the future. + * Subsequent requests by the client are permissible. + */ + NOT_FOUND = 404, + + /** + * A request method is not supported for the requested resource; + * for example, a GET request on a form that requires data to be presented via POST, or a PUT request on a read-only resource. + */ + METHOD_NOT_ALLOWED = 405, + + /** + * The requested resource is capable of generating only content not acceptable according to the Accept headers sent in the request. + */ + NOT_ACCEPTABLE = 406, + + /** + * The client must first authenticate itself with the proxy. + */ + PROXY_AUTHENTICATION_REQUIRED = 407, + + /** + * The server timed out waiting for the request. + * According to HTTP specifications: + * "The client did not produce a request within the time that the server was prepared to wait. The client MAY repeat the request without modifications at any later time." + */ + REQUEST_TIMEOUT = 408, + + /** + * Indicates that the request could not be processed because of conflict in the request, + * such as an edit conflict between multiple simultaneous updates. + */ + CONFLICT = 409, + + /** + * Indicates that the resource requested is no longer available and will not be available again. + * This should be used when a resource has been intentionally removed and the resource should be purged. + * Upon receiving a 410 status code, the client should not request the resource in the future. + * Clients such as search engines should remove the resource from their indices. + * Most use cases do not require clients and search engines to purge the resource, and a "404 Not Found" may be used instead. + */ + GONE = 410, + + /** + * The request did not specify the length of its content, which is required by the requested resource. + */ + LENGTH_REQUIRED = 411, + + /** + * The server does not meet one of the preconditions that the requester put on the request. + */ + PRECONDITION_FAILED = 412, + + /** + * The request is larger than the server is willing or able to process. Previously called "Request Entity Too Large". + */ + PAYLOAD_TOO_LARGE = 413, + + /** + * The URI provided was too long for the server to process. Often the result of too much data being encoded as a query-string of a GET request, + * in which case it should be converted to a POST request. + * Called "Request-URI Too Long" previously. + */ + URI_TOO_LONG = 414, + + /** + * The request entity has a media type which the server or resource does not support. + * For example, the client uploads an image as image/svg+xml, but the server requires that images use a different format. + */ + UNSUPPORTED_MEDIA_TYPE = 415, + + /** + * The client has asked for a portion of the file (byte serving), but the server cannot supply that portion. + * For example, if the client asked for a part of the file that lies beyond the end of the file. + * Called "Requested Range Not Satisfiable" previously. + */ + RANGE_NOT_SATISFIABLE = 416, + + /** + * The server cannot meet the requirements of the Expect request-header field. + */ + EXPECTATION_FAILED = 417, + + /** + * This code was defined in 1998 as one of the traditional IETF April Fools' jokes, in RFC 2324, Hyper Text Coffee Pot Control Protocol, + * and is not expected to be implemented by actual HTTP servers. The RFC specifies this code should be returned by + * teapots requested to brew coffee. This HTTP status is used as an Easter egg in some websites, including Google.com. + */ + I_AM_A_TEAPOT = 418, + + /** + * The request was directed at a server that is not able to produce a response (for example because a connection reuse). + */ + MISDIRECTED_REQUEST = 421, + + /** + * The request was well-formed but was unable to be followed due to semantic errors. + */ + UNPROCESSABLE_ENTITY = 422, + + /** + * The resource that is being accessed is locked. + */ + LOCKED = 423, + + /** + * The request failed due to failure of a previous request (e.g., a PROPPATCH). + */ + FAILED_DEPENDENCY = 424, + + /** + * The client should switch to a different protocol such as TLS/1.0, given in the Upgrade header field. + */ + UPGRADE_REQUIRED = 426, + + /** + * The origin server requires the request to be conditional. + * Intended to prevent "the 'lost update' problem, where a client + * GETs a resource's state, modifies it, and PUTs it back to the server, + * when meanwhile a third party has modified the state on the server, leading to a conflict." + */ + PRECONDITION_REQUIRED = 428, + + /** + * The user has sent too many requests in a given amount of time. Intended for use with rate-limiting schemes. + */ + TOO_MANY_REQUESTS = 429, + + /** + * The server is unwilling to process the request because either an individual header field, + * or all the header fields collectively, are too large. + */ + REQUEST_HEADER_FIELDS_TOO_LARGE = 431, + + /** + * A server operator has received a legal demand to deny access to a resource or to a set of resources + * that includes the requested resource. The code 451 was chosen as a reference to the novel Fahrenheit 451. + */ + UNAVAILABLE_FOR_LEGAL_REASONS = 451, + + /** + * A generic error message, given when an unexpected condition was encountered and no more specific message is suitable. + */ + INTERNAL_SERVER_ERROR = 500, + + /** + * The server either does not recognize the request method, or it lacks the ability to fulfill the request. + * Usually this implies future availability (e.g., a new feature of a web-service API). + */ + NOT_IMPLEMENTED = 501, + + /** + * The server was acting as a gateway or proxy and received an invalid response from the upstream server. + */ + BAD_GATEWAY = 502, + + /** + * The server is currently unavailable (because it is overloaded or down for maintenance). + * Generally, this is a temporary state. + */ + SERVICE_UNAVAILABLE = 503, + + /** + * The server was acting as a gateway or proxy and did not receive a timely response from the upstream server. + */ + GATEWAY_TIMEOUT = 504, + + /** + * The server does not support the HTTP protocol version used in the request + */ + HTTP_VERSION_NOT_SUPPORTED = 505, + + /** + * Transparent content negotiation for the request results in a circular reference. + */ + VARIANT_ALSO_NEGOTIATES = 506, + + /** + * The server is unable to store the representation needed to complete the request. + */ + INSUFFICIENT_STORAGE = 507, + + /** + * The server detected an infinite loop while processing the request. + */ + LOOP_DETECTED = 508, + + /** + * Further extensions to the request are required for the server to fulfill it. + */ + NOT_EXTENDED = 510, + + /** + * The client needs to authenticate to gain network access. + * Intended for use by intercepting proxies used to control access to the network (e.g., "captive portals" used + * to require agreement to Terms of Service before granting full Internet access via a Wi-Fi hotspot). + */ + NETWORK_AUTHENTICATION_REQUIRED = 511, +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..929d06b --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,5 @@ +export * from './cookie.util'; +export * from './error-handler.util'; +export * from './http-status.util'; +export * from './jwt.util'; +export * from './logger.util'; diff --git a/src/utils/jwt.util.ts b/src/utils/jwt.util.ts new file mode 100644 index 0000000..b64f1bf --- /dev/null +++ b/src/utils/jwt.util.ts @@ -0,0 +1,29 @@ +import jwt, { type JwtPayload } from 'jsonwebtoken'; + +import { HttpException } from '@/models'; +import { HttpStatusCode } from './http-status.util'; + +const SECRET_KEY = process.env.JWT_PRIVATE_KEY || 'secret'; +const EXPIRES_IN = process.env.JWT_TIME_EXPIRATION || '30d'; + +export const verifyJwt = (token: string): Promise => { + return new Promise((res, rej): void => { + return jwt.verify(token, SECRET_KEY, (err, decoded) => { + if (err) { + rej(new HttpException(err.message, HttpStatusCode.UNAUTHORIZED)); + } + res(decoded as JwtPayload); + }); + }); +}; + +export const encryptJwt = (payload: JwtPayload): Promise => { + return new Promise((res, rej): void => { + return jwt.sign(payload, process.env.JWT_PRIVATE_KEY!, { expiresIn: EXPIRES_IN }, (err, encoded) => { + if (err) { + rej(new HttpException(err.message, HttpStatusCode.UNAUTHORIZED)); + } + res(encoded); + }); + }); +}; diff --git a/src/utils/logger.util.ts b/src/utils/logger.util.ts new file mode 100644 index 0000000..307bdc6 --- /dev/null +++ b/src/utils/logger.util.ts @@ -0,0 +1,51 @@ +import winston from 'winston'; + +const customLevels = { + levels: { + trace: 0, + input: 1, + verbose: 2, + prompt: 3, + debug: 4, + info: 5, + data: 6, + help: 7, + warn: 8, + error: 9, + }, + + colors: { + trace: 'magenta', + input: 'grey', + verbose: 'cyan', + prompt: 'grey', + debug: 'blue', + info: 'green', + data: 'grey', + help: 'cyan', + warn: 'yellow', + error: 'red', + }, +}; + +// * Using the printf format. +const customFormat = winston.format.printf(({ level, message, timestamp }) => { + return `[${level}] ${timestamp} - ${message}`; +}); + +winston.addColors(customLevels.colors); + +export const logger = winston.createLogger({ + level: 'info', + format: winston.format.combine( + winston.format((info) => { + info.level = info.level.toUpperCase(); + return info; + })(), + winston.format.colorize({ all: true }), + winston.format.timestamp({ format: 'HH:mm:ss' }), + customFormat, + ), + + transports: [new winston.transports.Console()], +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..33e6eb2 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,115 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "lib": ["ES6"], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + "baseUrl": ".", /* Specify the base directory to resolve non-relative module names. */ + "paths": { + "@/*":["src/*"], + "~prisma/*":["prisma/*"] + }, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + "typeRoots": ["node_modules/@types", "node_modules/@prisma"], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./build", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + "strictNullChecks": false, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + "strictBindCallApply": false, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["src/**/*.ts", "types/*.ts", "prisma/prisma-client.ts"], + "exclude": ["node_modules"], + "files": ["types/express.d.ts"] +} diff --git a/types/environment.d.ts b/types/environment.d.ts new file mode 100644 index 0000000..09c3a25 --- /dev/null +++ b/types/environment.d.ts @@ -0,0 +1,12 @@ +namespace NodeJS { + interface ProcessEnv { + PORT?: string; + GLOBAL_PREFIX?: string; + DATABASE_URL?: string; + JWT_PRIVATE_KEY?: string; + JWT_TIME_EXPIRATION?: string; + COOKIE_NAME?: string; + COOKIE_PRIVATE_KEY?: string; + NODE_ENV?: 'development' | 'production'; + } +} diff --git a/types/express.d.ts b/types/express.d.ts new file mode 100644 index 0000000..9c6cd2d --- /dev/null +++ b/types/express.d.ts @@ -0,0 +1,23 @@ +import type { Request, Express } from 'express-serve-static-core'; + +interface QParams { + id?: string; + email?: string; +} + +interface PayloadResponse { + code: string; + status: number; +} + +declare module 'express-serve-static-core' { + export interface Request { + params?: QParams | { [x: string]: string }; + body?: any; + user?: { id?: number; email?: string }; + } + + export type Response

= Response

; +} + +export {}; diff --git a/types/jwt.d.ts b/types/jwt.d.ts new file mode 100644 index 0000000..45b45b0 --- /dev/null +++ b/types/jwt.d.ts @@ -0,0 +1,10 @@ +import type { JwtPayload } from 'jsonwebtoken'; + +declare module 'jsonwebtoken' { + export interface JwtPayload { + id?: number; + email?: string; + } +} + +export {};