├── .gitignore ├── LICENSE ├── README.md ├── api-gateway ├── .gitignore ├── package.json ├── src │ ├── config.json │ ├── index.ts │ ├── middlewares.ts │ └── utils.ts ├── tsconfig.json └── yarn.lock ├── config ├── Dockerfile └── plugins │ ├── custom-auth.js │ ├── oidc.js │ ├── package-lock.json │ └── package.json ├── docker-compose.yaml ├── keycloak-docker-compose └── keycloak-docker-compose.yml ├── kong-docker-compose └── kong-docker-compose.yml └── services ├── auth ├── .env.example ├── .gitignore ├── package.json ├── prisma │ ├── migrations │ │ ├── 20240228073719_init │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── src │ ├── config.ts │ ├── controllers │ │ ├── index.ts │ │ ├── userLogin.ts │ │ ├── userRegistration.ts │ │ ├── verifyEmail.ts │ │ └── verifyToken.ts │ ├── index.ts │ ├── prisma.ts │ └── schemas.ts ├── tsconfig.json └── yarn.lock ├── cart ├── .env.example ├── .gitignore ├── package.json ├── src │ ├── config.ts │ ├── controllers │ │ ├── addToCart.ts │ │ ├── clearCart.ts │ │ ├── getMyCart.ts │ │ └── index.ts │ ├── events │ │ └── onKeyExpires.ts │ ├── index.ts │ ├── receiver.ts │ ├── redis.ts │ ├── schemas.ts │ └── services │ │ └── index.ts ├── tsconfig.json └── yarn.lock ├── email ├── .env.example ├── .gitignore ├── package.json ├── prisma │ ├── migrations │ │ ├── 20240228074009_init │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── src │ ├── config.ts │ ├── controllers │ │ ├── getEmails.ts │ │ ├── index.ts │ │ └── sendEmail.ts │ ├── index.ts │ ├── prisma.ts │ ├── receiver.ts │ └── schemas.ts ├── tsconfig.json └── yarn.lock ├── inventory ├── .env.example ├── .gitignore ├── package.json ├── prisma │ ├── migrations │ │ ├── 20240228074051_init │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── src │ ├── controllers │ │ ├── createInventory.ts │ │ ├── getInventoryById.ts │ │ ├── getInventoryDetails.ts │ │ ├── index.ts │ │ └── updateInventory.ts │ ├── index.ts │ ├── prisma.ts │ └── schemas.ts ├── tsconfig.json └── yarn.lock ├── order ├── .env.example ├── .gitignore ├── package.json ├── prisma │ ├── migrations │ │ ├── 20240228074156_init │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── src │ ├── config.ts │ ├── controllers │ │ ├── checkout.ts │ │ ├── getOrderById.ts │ │ ├── getOrders.ts │ │ └── index.ts │ ├── index.ts │ ├── prisma.ts │ ├── queue.ts │ └── schemas.ts ├── tsconfig.json └── yarn.lock ├── product ├── .env.example ├── .gitignore ├── package.json ├── prisma │ ├── migrations │ │ ├── 20240228074230_init │ │ │ └── migration.sql │ │ └── migration_lock.toml │ └── schema.prisma ├── src │ ├── config.ts │ ├── controllers │ │ ├── createProduct.ts │ │ ├── getProductDetails.ts │ │ ├── getProducts.ts │ │ ├── index.ts │ │ └── updateProduct.ts │ ├── index.ts │ ├── prisma.ts │ └── schemas.ts ├── tsconfig.json └── yarn.lock └── user ├── .env.example ├── .gitignore ├── package.json ├── prisma ├── migrations │ ├── 20240228074306_init │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── src ├── config.ts ├── controllers │ ├── createUser.ts │ ├── getUserById.ts │ └── index.ts ├── index.ts ├── prisma.ts └── schemas.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # practical-microservice-workshop 2 | 3 | ## Run Kong and necessary services 4 | 5 | ```bash 6 | cd kong-docker-compose && docker compose -f .\kong-docker-compose.yml up 7 | ``` 8 | 9 | ## Run Keycloak 10 | 11 | ```bash 12 | 13 | cd docker keycloak-docker-compose && docker compose -f ./keycloak-docker-compose.yml up 14 | ``` 15 | 16 | ## Run microservices dependency 17 | 18 | ```bash 19 | docker compose up 20 | ``` 21 | -------------------------------------------------------------------------------- /api-gateway/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Keep environment variables out of version control 3 | .env 4 | -------------------------------------------------------------------------------- /api-gateway/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "product", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "ts-node-dev -r tsconfig-paths/register ./src/index.ts", 8 | "build": "tsc && tsc-alias" 9 | }, 10 | "dependencies": { 11 | "axios": "^1.6.7", 12 | "cors": "^2.8.5", 13 | "dotenv": "^16.4.5", 14 | "express": "^4.18.2", 15 | "express-rate-limit": "^7.1.5", 16 | "helmet": "^7.1.0", 17 | "morgan": "^1.10.0", 18 | "zod": "^3.22.4" 19 | }, 20 | "devDependencies": { 21 | "@types/cors": "^2.8.17", 22 | "@types/express": "^4.17.21", 23 | "@types/node": "^20.11.19", 24 | "ts-node-dev": "^2.0.0", 25 | "tsc": "^2.0.4", 26 | "tsc-alias": "^1.8.8", 27 | "tsconfig-paths": "^4.2.0", 28 | "typescript": "^5.3.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /api-gateway/src/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "services": { 3 | "product": { 4 | "url": "http://localhost:4001", 5 | "routes": [ 6 | { 7 | "path": "/products", 8 | "methods": [ 9 | "get" 10 | ], 11 | "middlewares": [] 12 | }, 13 | { 14 | "path": "/products", 15 | "methods": [ 16 | "post" 17 | ], 18 | "middlewares": [ 19 | "auth" 20 | ] 21 | }, 22 | { 23 | "path": "/products/:id", 24 | "methods": [ 25 | "get" 26 | ], 27 | "middlewares": [] 28 | } 29 | ] 30 | }, 31 | "inventory": { 32 | "url": "http://localhost:4002", 33 | "routes": [ 34 | { 35 | "path": "/inventories/:id", 36 | "methods": [ 37 | "put" 38 | ], 39 | "middlewares": [ 40 | "auth" 41 | ] 42 | }, 43 | { 44 | "path": "/inventories/:id/details", 45 | "methods": [ 46 | "get" 47 | ], 48 | "middlewares": [ 49 | "auth" 50 | ] 51 | } 52 | ] 53 | }, 54 | "auth": { 55 | "url": "http://localhost:4003", 56 | "routes": [ 57 | { 58 | "path": "/auth/register", 59 | "methods": [ 60 | "post" 61 | ], 62 | "middlewares": [] 63 | }, 64 | { 65 | "path": "/auth/login", 66 | "methods": [ 67 | "post" 68 | ], 69 | "middlewares": [] 70 | }, 71 | { 72 | "path": "/auth/verify-email", 73 | "methods": [ 74 | "post" 75 | ], 76 | "middlewares": [] 77 | }, 78 | { 79 | "path": "/auth/verify-token", 80 | "methods": [ 81 | "post" 82 | ], 83 | "middlewares": [] 84 | } 85 | ] 86 | }, 87 | "user": { 88 | "url": "http://localhost:4004", 89 | "routes": [ 90 | { 91 | "path": "/users/:id", 92 | "methods": [ 93 | "get" 94 | ], 95 | "middlewares": [ 96 | "auth" 97 | ] 98 | } 99 | ] 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /api-gateway/src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import dotenv from 'dotenv'; 3 | import helmet from 'helmet'; 4 | import rateLimit from 'express-rate-limit'; 5 | import morgan from 'morgan'; 6 | import { configureRoutes } from './utils'; 7 | 8 | dotenv.config(); 9 | 10 | const app = express(); 11 | 12 | // security middleware 13 | app.use(helmet()); 14 | 15 | // Rate limiting middleware 16 | const limiter = rateLimit({ 17 | windowMs: 15 * 60 * 1000, // 15 minutes 18 | max: 100, // limit each IP to 100 requests per windowMs 19 | handler: (_req, res) => { 20 | res 21 | .status(429) 22 | .json({ message: 'Too many requests, please try again later.' }); 23 | }, 24 | }); 25 | app.use('/api', limiter); 26 | 27 | // request logger 28 | app.use(morgan('dev')); 29 | app.use(express.json()); 30 | 31 | // TODO: Auth middleware 32 | 33 | // routes 34 | configureRoutes(app); 35 | 36 | // health check 37 | app.get('/health', (_req, res) => { 38 | res.json({ message: 'API Gateway is running' }); 39 | }); 40 | 41 | // 404 handler 42 | app.use((_req, res) => { 43 | res.status(404).json({ message: 'Not Found' }); 44 | }); 45 | 46 | // error handler 47 | app.use((err, _req, res, _next) => { 48 | console.error(err.stack); 49 | res.status(500).json({ message: 'Internal Server Error' }); 50 | }); 51 | 52 | const PORT = process.env.PORT || 8081; 53 | app.listen(PORT, () => { 54 | console.log(`API Gateway is running on port ${PORT}`); 55 | }); 56 | -------------------------------------------------------------------------------- /api-gateway/src/middlewares.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { Request, Response, NextFunction } from 'express'; 3 | 4 | const auth = async (req: Request, res: Response, next: NextFunction) => { 5 | if (!req.headers['authorization']) { 6 | return res.status(401).json({ message: 'Unauthorized' }); 7 | } 8 | 9 | try { 10 | const token = req.headers['authorization']?.split(' ')[1]; 11 | const { data } = await axios.post( 12 | 'http://localhost:4003/auth/verify-token', 13 | { 14 | accessToken: token, 15 | headers: { 16 | ip: req.ip, 17 | 'user-agent': req.headers['user-agent'], 18 | }, 19 | } 20 | ); 21 | 22 | req.headers['x-user-id'] = data.user.id; 23 | req.headers['x-user-email'] = data.user.email; 24 | req.headers['x-user-name'] = data.user.name; 25 | req.headers['x-user-role'] = data.user.role; 26 | 27 | next(); 28 | } catch (error) { 29 | console.log('[auth middleware]', error); 30 | return res.status(401).json({ message: 'Unauthorized' }); 31 | } 32 | }; 33 | 34 | const middlewares = { auth }; 35 | export default middlewares; 36 | -------------------------------------------------------------------------------- /api-gateway/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Express, Request, Response } from 'express'; 2 | import config from './config.json'; 3 | import axios from 'axios'; 4 | import middlewares from './middlewares'; 5 | 6 | export const createHandler = ( 7 | hostname: string, 8 | path: string, 9 | method: string 10 | ) => { 11 | return async (req: Request, res: Response) => { 12 | try { 13 | let url = `${hostname}${path}`; 14 | req.params && 15 | Object.keys(req.params).forEach((param) => { 16 | url = url.replace(`:${param}`, req.params[param]); 17 | }); 18 | 19 | const { data } = await axios({ 20 | method, 21 | url, 22 | data: req.body, 23 | headers: { 24 | origin: 'http://localhost:8081', 25 | 'x-user-id': req.headers['x-user-id'] || '', 26 | 'x-user-email': req.headers['x-user-email'] || '', 27 | 'x-user-name': req.headers['x-user-name'] || '', 28 | 'x-user-role': req.headers['x-user-role'] || '', 29 | 'user-agent': req.headers['user-agent'], 30 | }, 31 | }); 32 | 33 | res.json(data); 34 | } catch (error) { 35 | console.log(error); 36 | if (error instanceof axios.AxiosError) { 37 | return res 38 | .status(error.response?.status || 500) 39 | .json(error.response?.data); 40 | } 41 | return res.status(500).json({ message: 'Internal Server Error' }); 42 | } 43 | }; 44 | }; 45 | 46 | export const getMiddlewares = (names: string[]) => { 47 | return names.map((name) => middlewares[name]); 48 | }; 49 | 50 | export const configureRoutes = (app: Express) => { 51 | Object.entries(config.services).forEach(([_name, service]) => { 52 | const hostname = service.url; 53 | service.routes.forEach((route) => { 54 | route.methods.forEach((method) => { 55 | const endpoint = `/api${route.path}`; 56 | const middleware = getMiddlewares(route.middlewares); 57 | const handler = createHandler(hostname, route.path, method); 58 | app[method](endpoint, middleware, handler); 59 | }); 60 | }); 61 | }); 62 | }; 63 | -------------------------------------------------------------------------------- /api-gateway/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "lib": ["ES6"], 6 | "allowJs": true, 7 | "module": "CommonJS", 8 | "rootDir": ".", 9 | "outDir": "./dist", 10 | "esModuleInterop": true, 11 | "strict": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "allowSyntheticDefaultImports": true, 17 | "typeRoots": ["./src/types", "./node_modules/@types"], 18 | "sourceMap": true, 19 | "types": ["node", "express"], 20 | "noImplicitAny": false, 21 | "baseUrl": "./src", 22 | "paths": { 23 | "@/*": ["*"] 24 | } 25 | }, 26 | "include": ["src/**/*"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /config/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM kong:latest 2 | USER root 3 | 4 | RUN apt-get update && apt-get install -y nodejs npm && npm install -g kong-pdk 5 | 6 | # custom auth plugin for custom auth server 7 | RUN mkdir -p /usr/local/kong/js-plugins 8 | COPY ./plugins /usr/local/kong/js-plugins 9 | RUN cd /usr/local/kong/js-plugins && npm install 10 | 11 | 12 | 13 | USER kong -------------------------------------------------------------------------------- /config/plugins/custom-auth.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | class CustomAuth { 3 | constructor(config) { 4 | this.config = config; 5 | } 6 | 7 | async access(kong) { 8 | try { 9 | kong.log.notice(`🚆🚆🚆🚆🚆🚆 Hello Custom Auth`); 10 | const headers = await kong.request.get_headers(); 11 | kong.log.notice(`🚆🚆🚆🚆🚆🚆 header Response: ${headers}`); 12 | const token_place = this.config.token_place || "Authorization"; 13 | const authHeader = 14 | headers[token_place.toLowerCase()] && 15 | headers[token_place.toLowerCase()][0]; 16 | const token = authHeader ? authHeader.split(" ")[1] : null; 17 | 18 | kong.log.notice(`🚆🚆🚆🚆🚆🚆 token Response: ${token}`); 19 | 20 | if (!token) { 21 | return await kong.response.exit( 22 | 401, 23 | JSON.stringify({ 24 | message: "Unauthorized", 25 | }) 26 | ); 27 | } 28 | 29 | const data = await axios.post( 30 | this.config.validation_endpoint, // http://auth:4005/api/v1/checkpoint 31 | { 32 | accessToken: token, 33 | } 34 | // { 35 | // headers: { 36 | // Authorization: `Bearer ${token}`, 37 | // }, 38 | // } 39 | ); 40 | 41 | if (data.status !== 200) { 42 | return await kong.response.exit( 43 | 401, 44 | JSON.stringify({ 45 | message: "Unauthorized", 46 | }) 47 | ); 48 | } 49 | 50 | kong.log.notice( 51 | `🚆🚆🚆🚆🚆🚆 Auth API Response: ${JSON.stringify(data.data)}` 52 | ); 53 | 54 | 55 | // Set user data in headers 56 | kong.service.request.set_header("X-User-ID", data.data.user.id); 57 | kong.service.request.set_header("X-User-Email", data.data.user.email); 58 | 59 | return; 60 | } catch (error) { 61 | const message = error.message || "Unauthorized"; 62 | 63 | return await kong.response.exit(500, JSON.stringify({ message })); 64 | } 65 | } 66 | } 67 | 68 | module.exports = { 69 | Plugin: CustomAuth, 70 | Schema: [ 71 | { 72 | validation_endpoint: { 73 | type: "string", 74 | required: true, 75 | description: 76 | "The URL of the external authentication server's validation endpoint.", 77 | }, 78 | }, 79 | { 80 | token_place: { 81 | type: "string", 82 | required: false, 83 | default: "Authorization", 84 | }, 85 | }, 86 | ], 87 | Version: "1.0.0", 88 | Priority: 0, 89 | }; -------------------------------------------------------------------------------- /config/plugins/oidc.js: -------------------------------------------------------------------------------- 1 | const axios = require("axios"); 2 | const qs = require("qs"); 3 | class Oidc { 4 | constructor(config) { 5 | this.config = config; 6 | } 7 | 8 | async access(kong) { 9 | try { 10 | const headers = await kong.request.get_headers(); 11 | const authHeader = headers["authorization"]; 12 | const token = authHeader ? authHeader[0].split(" ")[1].trim() : ""; 13 | 14 | const keycloak_introspection_url = this.config.keycloak_introspection_url; 15 | const client_id = this.config.client_id; 16 | const client_secret = this.config.client_secret; 17 | 18 | kong.log.notice(` 19 | 🍳🍳🍳🍳🍳🍳🍳 20 | token = ${token.length} 21 | keycloak_introspection_url = ${keycloak_introspection_url} 22 | client_id = ${client_id} 23 | client_secret = ${client_secret} 24 | `); 25 | 26 | if (!token) { 27 | return await kong.response.exit( 28 | 401, 29 | JSON.stringify({ 30 | message: "Unauthorized. No Token Found!", 31 | }) 32 | ); 33 | } 34 | 35 | const data = qs.stringify({ 36 | client_id, 37 | client_secret, 38 | token, 39 | }); 40 | 41 | const response = await axios.post(keycloak_introspection_url, data, { 42 | headers: { 43 | "Content-Type": "application/x-www-form-urlencoded", 44 | }, 45 | }); 46 | 47 | kong.log.notice( 48 | `🚆🚆🚆🚆🚆🚆 Kong Introspection API Response: ${JSON.stringify( 49 | response.data 50 | )}` 51 | ); 52 | 53 | if (!response.data.active) { 54 | return await kong.response.exit( 55 | 401, 56 | JSON.stringify({ 57 | message: "Unauthorized. Invalid Token!", 58 | }) 59 | ); 60 | } 61 | 62 | kong.log.notice(`🥰🥰 Request sent to the Upstream server`); 63 | 64 | // Set user data in headers 65 | kong.service.request.set_header("X-User-ID", response.data.sid); 66 | kong.service.request.set_header("X-User-Email", response.data.email); 67 | 68 | return; 69 | } catch (error) { 70 | const message = error.message || "Something Went Wrong!"; 71 | return await kong.response.exit(500, JSON.stringify({ message })); 72 | } 73 | } 74 | } 75 | 76 | module.exports = { 77 | Plugin: Oidc, 78 | Schema: [ 79 | { 80 | keycloak_introspection_url: { 81 | type: "string", 82 | required: true, 83 | description: 84 | "The URL of the external authentication server's validation endpoint.", 85 | }, 86 | }, 87 | { 88 | client_id: { 89 | type: "string", 90 | required: true, 91 | }, 92 | }, 93 | { 94 | client_secret: { 95 | type: "string", 96 | required: true, 97 | }, 98 | }, 99 | ], 100 | Version: "1.0.0", 101 | Priority: 0, 102 | }; 103 | -------------------------------------------------------------------------------- /config/plugins/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "customauthplugin", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "customauthplugin", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "axios": "^1.6.5", 13 | "cookie": "0.5.0", 14 | "js-yaml": "^4.1.0", 15 | "jsonwebtoken": "8.5.1", 16 | "qs": "^6.11.2" 17 | } 18 | }, 19 | "node_modules/argparse": { 20 | "version": "2.0.1", 21 | "license": "Python-2.0" 22 | }, 23 | "node_modules/asynckit": { 24 | "version": "0.4.0", 25 | "license": "MIT" 26 | }, 27 | "node_modules/axios": { 28 | "version": "1.6.5", 29 | "license": "MIT", 30 | "dependencies": { 31 | "follow-redirects": "^1.15.4", 32 | "form-data": "^4.0.0", 33 | "proxy-from-env": "^1.1.0" 34 | } 35 | }, 36 | "node_modules/buffer-equal-constant-time": { 37 | "version": "1.0.1", 38 | "license": "BSD-3-Clause" 39 | }, 40 | "node_modules/call-bind": { 41 | "version": "1.0.5", 42 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", 43 | "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", 44 | "dependencies": { 45 | "function-bind": "^1.1.2", 46 | "get-intrinsic": "^1.2.1", 47 | "set-function-length": "^1.1.1" 48 | }, 49 | "funding": { 50 | "url": "https://github.com/sponsors/ljharb" 51 | } 52 | }, 53 | "node_modules/combined-stream": { 54 | "version": "1.0.8", 55 | "license": "MIT", 56 | "dependencies": { 57 | "delayed-stream": "~1.0.0" 58 | }, 59 | "engines": { 60 | "node": ">= 0.8" 61 | } 62 | }, 63 | "node_modules/cookie": { 64 | "version": "0.5.0", 65 | "license": "MIT", 66 | "engines": { 67 | "node": ">= 0.6" 68 | } 69 | }, 70 | "node_modules/define-data-property": { 71 | "version": "1.1.1", 72 | "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", 73 | "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", 74 | "dependencies": { 75 | "get-intrinsic": "^1.2.1", 76 | "gopd": "^1.0.1", 77 | "has-property-descriptors": "^1.0.0" 78 | }, 79 | "engines": { 80 | "node": ">= 0.4" 81 | } 82 | }, 83 | "node_modules/delayed-stream": { 84 | "version": "1.0.0", 85 | "license": "MIT", 86 | "engines": { 87 | "node": ">=0.4.0" 88 | } 89 | }, 90 | "node_modules/ecdsa-sig-formatter": { 91 | "version": "1.0.11", 92 | "license": "Apache-2.0", 93 | "dependencies": { 94 | "safe-buffer": "^5.0.1" 95 | } 96 | }, 97 | "node_modules/follow-redirects": { 98 | "version": "1.15.4", 99 | "funding": [ 100 | { 101 | "type": "individual", 102 | "url": "https://github.com/sponsors/RubenVerborgh" 103 | } 104 | ], 105 | "license": "MIT", 106 | "engines": { 107 | "node": ">=4.0" 108 | }, 109 | "peerDependenciesMeta": { 110 | "debug": { 111 | "optional": true 112 | } 113 | } 114 | }, 115 | "node_modules/form-data": { 116 | "version": "4.0.0", 117 | "license": "MIT", 118 | "dependencies": { 119 | "asynckit": "^0.4.0", 120 | "combined-stream": "^1.0.8", 121 | "mime-types": "^2.1.12" 122 | }, 123 | "engines": { 124 | "node": ">= 6" 125 | } 126 | }, 127 | "node_modules/function-bind": { 128 | "version": "1.1.2", 129 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 130 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 131 | "funding": { 132 | "url": "https://github.com/sponsors/ljharb" 133 | } 134 | }, 135 | "node_modules/get-intrinsic": { 136 | "version": "1.2.2", 137 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", 138 | "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", 139 | "dependencies": { 140 | "function-bind": "^1.1.2", 141 | "has-proto": "^1.0.1", 142 | "has-symbols": "^1.0.3", 143 | "hasown": "^2.0.0" 144 | }, 145 | "funding": { 146 | "url": "https://github.com/sponsors/ljharb" 147 | } 148 | }, 149 | "node_modules/gopd": { 150 | "version": "1.0.1", 151 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", 152 | "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", 153 | "dependencies": { 154 | "get-intrinsic": "^1.1.3" 155 | }, 156 | "funding": { 157 | "url": "https://github.com/sponsors/ljharb" 158 | } 159 | }, 160 | "node_modules/has-property-descriptors": { 161 | "version": "1.0.1", 162 | "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", 163 | "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", 164 | "dependencies": { 165 | "get-intrinsic": "^1.2.2" 166 | }, 167 | "funding": { 168 | "url": "https://github.com/sponsors/ljharb" 169 | } 170 | }, 171 | "node_modules/has-proto": { 172 | "version": "1.0.1", 173 | "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", 174 | "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", 175 | "engines": { 176 | "node": ">= 0.4" 177 | }, 178 | "funding": { 179 | "url": "https://github.com/sponsors/ljharb" 180 | } 181 | }, 182 | "node_modules/has-symbols": { 183 | "version": "1.0.3", 184 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 185 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", 186 | "engines": { 187 | "node": ">= 0.4" 188 | }, 189 | "funding": { 190 | "url": "https://github.com/sponsors/ljharb" 191 | } 192 | }, 193 | "node_modules/hasown": { 194 | "version": "2.0.0", 195 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", 196 | "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", 197 | "dependencies": { 198 | "function-bind": "^1.1.2" 199 | }, 200 | "engines": { 201 | "node": ">= 0.4" 202 | } 203 | }, 204 | "node_modules/js-yaml": { 205 | "version": "4.1.0", 206 | "license": "MIT", 207 | "dependencies": { 208 | "argparse": "^2.0.1" 209 | }, 210 | "bin": { 211 | "js-yaml": "bin/js-yaml.js" 212 | } 213 | }, 214 | "node_modules/jsonwebtoken": { 215 | "version": "8.5.1", 216 | "license": "MIT", 217 | "dependencies": { 218 | "jws": "^3.2.2", 219 | "lodash.includes": "^4.3.0", 220 | "lodash.isboolean": "^3.0.3", 221 | "lodash.isinteger": "^4.0.4", 222 | "lodash.isnumber": "^3.0.3", 223 | "lodash.isplainobject": "^4.0.6", 224 | "lodash.isstring": "^4.0.1", 225 | "lodash.once": "^4.0.0", 226 | "ms": "^2.1.1", 227 | "semver": "^5.6.0" 228 | }, 229 | "engines": { 230 | "node": ">=4", 231 | "npm": ">=1.4.28" 232 | } 233 | }, 234 | "node_modules/jwa": { 235 | "version": "1.4.1", 236 | "license": "MIT", 237 | "dependencies": { 238 | "buffer-equal-constant-time": "1.0.1", 239 | "ecdsa-sig-formatter": "1.0.11", 240 | "safe-buffer": "^5.0.1" 241 | } 242 | }, 243 | "node_modules/jws": { 244 | "version": "3.2.2", 245 | "license": "MIT", 246 | "dependencies": { 247 | "jwa": "^1.4.1", 248 | "safe-buffer": "^5.0.1" 249 | } 250 | }, 251 | "node_modules/lodash.includes": { 252 | "version": "4.3.0", 253 | "license": "MIT" 254 | }, 255 | "node_modules/lodash.isboolean": { 256 | "version": "3.0.3", 257 | "license": "MIT" 258 | }, 259 | "node_modules/lodash.isinteger": { 260 | "version": "4.0.4", 261 | "license": "MIT" 262 | }, 263 | "node_modules/lodash.isnumber": { 264 | "version": "3.0.3", 265 | "license": "MIT" 266 | }, 267 | "node_modules/lodash.isplainobject": { 268 | "version": "4.0.6", 269 | "license": "MIT" 270 | }, 271 | "node_modules/lodash.isstring": { 272 | "version": "4.0.1", 273 | "license": "MIT" 274 | }, 275 | "node_modules/lodash.once": { 276 | "version": "4.1.1", 277 | "license": "MIT" 278 | }, 279 | "node_modules/mime-db": { 280 | "version": "1.52.0", 281 | "license": "MIT", 282 | "engines": { 283 | "node": ">= 0.6" 284 | } 285 | }, 286 | "node_modules/mime-types": { 287 | "version": "2.1.35", 288 | "license": "MIT", 289 | "dependencies": { 290 | "mime-db": "1.52.0" 291 | }, 292 | "engines": { 293 | "node": ">= 0.6" 294 | } 295 | }, 296 | "node_modules/ms": { 297 | "version": "2.1.3", 298 | "license": "MIT" 299 | }, 300 | "node_modules/object-inspect": { 301 | "version": "1.13.1", 302 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", 303 | "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", 304 | "funding": { 305 | "url": "https://github.com/sponsors/ljharb" 306 | } 307 | }, 308 | "node_modules/proxy-from-env": { 309 | "version": "1.1.0", 310 | "license": "MIT" 311 | }, 312 | "node_modules/qs": { 313 | "version": "6.11.2", 314 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", 315 | "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", 316 | "dependencies": { 317 | "side-channel": "^1.0.4" 318 | }, 319 | "engines": { 320 | "node": ">=0.6" 321 | }, 322 | "funding": { 323 | "url": "https://github.com/sponsors/ljharb" 324 | } 325 | }, 326 | "node_modules/safe-buffer": { 327 | "version": "5.2.1", 328 | "funding": [ 329 | { 330 | "type": "github", 331 | "url": "https://github.com/sponsors/feross" 332 | }, 333 | { 334 | "type": "patreon", 335 | "url": "https://www.patreon.com/feross" 336 | }, 337 | { 338 | "type": "consulting", 339 | "url": "https://feross.org/support" 340 | } 341 | ], 342 | "license": "MIT" 343 | }, 344 | "node_modules/semver": { 345 | "version": "5.7.2", 346 | "license": "ISC", 347 | "bin": { 348 | "semver": "bin/semver" 349 | } 350 | }, 351 | "node_modules/set-function-length": { 352 | "version": "1.1.1", 353 | "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", 354 | "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", 355 | "dependencies": { 356 | "define-data-property": "^1.1.1", 357 | "get-intrinsic": "^1.2.1", 358 | "gopd": "^1.0.1", 359 | "has-property-descriptors": "^1.0.0" 360 | }, 361 | "engines": { 362 | "node": ">= 0.4" 363 | } 364 | }, 365 | "node_modules/side-channel": { 366 | "version": "1.0.4", 367 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 368 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 369 | "dependencies": { 370 | "call-bind": "^1.0.0", 371 | "get-intrinsic": "^1.0.2", 372 | "object-inspect": "^1.9.0" 373 | }, 374 | "funding": { 375 | "url": "https://github.com/sponsors/ljharb" 376 | } 377 | } 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /config/plugins/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "customauthplugin", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "clacks.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node customauthplugin.js" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "axios": "^1.6.5", 15 | "cookie": "0.5.0", 16 | "js-yaml": "^4.1.0", 17 | "jsonwebtoken": "8.5.1", 18 | "qs": "^6.11.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | # docker-compose.yaml 2 | # postgres 3 | # pgadmin 4 | 5 | version: "3.8" 6 | 7 | services: 8 | postgres: 9 | image: postgres:13 10 | container_name: postgres 11 | restart: on-failure 12 | environment: 13 | POSTGRES_USER: postgres 14 | POSTGRES_PASSWORD: postgres 15 | POSTGRES_DB: postgres 16 | ports: 17 | - "5433:5432" 18 | volumes: 19 | - postgres:/var/lib/postgresql/data 20 | healthcheck: 21 | test: ["CMD", "pg_isready", "-U", "auth"] 22 | interval: 30s 23 | timeout: 30s 24 | retries: 3 25 | 26 | pgadmin: 27 | image: dpage/pgadmin4 28 | container_name: pgadmin 29 | restart: on-failure 30 | environment: 31 | PGADMIN_DEFAULT_EMAIL: admin@example.com 32 | PGADMIN_DEFAULT_PASSWORD: admin 33 | ports: 34 | - "5050:80" 35 | - "5051:443" 36 | 37 | mailhog: 38 | image: mailhog/mailhog 39 | container_name: mailhog2 40 | ports: 41 | - "1025:1025" 42 | - "8025:8025" 43 | volumes: 44 | - mailhog:/var/lib/mailhog 45 | 46 | redis-stack: 47 | image: redis/redis-stack:latest 48 | ports: 49 | - "6379:6379" 50 | - "8002:8001" 51 | volumes: 52 | - redis-stack:/var/lib/redis-stack 53 | environment: 54 | - REDIS_ARGS=--save 900 1 55 | 56 | rabbitmq: 57 | image: rabbitmq:3.8-management 58 | ports: 59 | - "5672:5672" # RabbitMQ main port 60 | - "15672:15672" # RabbitMQ management UI port 61 | volumes: 62 | - rabbitmq_data:/var/lib/rabbitmq 63 | 64 | # ------------------------------------------ 65 | 66 | volumes: 67 | postgres: 68 | mailhog: 69 | redis-stack: 70 | rabbitmq_data: 71 | kong_data: 72 | 73 | 74 | -------------------------------------------------------------------------------- /keycloak-docker-compose/keycloak-docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | services: 4 | postgres: 5 | image: postgres:14.1-alpine 6 | environment: 7 | POSTGRES_USER: keycloak 8 | POSTGRES_PASSWORD: your_desired_password 9 | POSTGRES_DB: keycloak 10 | volumes: 11 | - postgres_data:/var/lib/postgresql/data 12 | 13 | keycloak: 14 | image: bitnami/keycloak:latest 15 | environment: 16 | DB_VENDOR: POSTGRES 17 | DB_ADDR: postgres 18 | DB_DATABASE: keycloak 19 | DB_USER: keycloak 20 | DB_PASSWORD: your_desired_password 21 | KEYCLOAK_ADMIN_USER: admin 22 | KEYCLOAK_ADMIN_PASSWORD: admin 23 | ports: 24 | - 8081:8080 25 | depends_on: 26 | - postgres 27 | 28 | volumes: 29 | postgres_data: {} 30 | -------------------------------------------------------------------------------- /kong-docker-compose/kong-docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | volumes: 4 | kong_data: {} 5 | keycloak-datastore: {} 6 | 7 | networks: 8 | keycloak-net: 9 | kong-net: 10 | name: kong-net 11 | driver: bridge 12 | # ipam: 13 | # config: 14 | # - subnet: 172.1.1.0/24 15 | 16 | services: 17 | ####################################### 18 | # Postgres: The database used by Kong 19 | ####################################### 20 | kong-database: 21 | image: postgres:9.6 22 | container_name: kong-postgres 23 | restart: on-failure 24 | networks: 25 | - kong-net 26 | volumes: 27 | - kong_data:/var/lib/postgresql/data 28 | environment: 29 | POSTGRES_USER: kong 30 | POSTGRES_PASSWORD: kong 31 | POSTGRES_DB: kong 32 | ports: 33 | - "5432:5432" 34 | healthcheck: 35 | test: ["CMD", "pg_isready", "-U", "kong"] 36 | interval: 30s 37 | timeout: 30s 38 | retries: 3 39 | 40 | ####################################### 41 | # Kong database migration 42 | ####################################### 43 | kong-migration: 44 | build: 45 | context: ../config 46 | dockerfile: Dockerfile 47 | command: kong migrations bootstrap 48 | networks: 49 | - kong-net 50 | restart: on-failure 51 | environment: 52 | KONG_DATABASE: postgres 53 | KONG_PG_HOST: kong-database 54 | KONG_PG_DATABASE: kong 55 | KONG_PG_USER: kong 56 | KONG_PG_PASSWORD: kong 57 | depends_on: 58 | - kong-database 59 | 60 | ####################################### 61 | # Kong: The API Gateway 62 | ####################################### 63 | kong: 64 | build: 65 | context: ../config 66 | dockerfile: Dockerfile 67 | container_name: kong 68 | restart: on-failure 69 | networks: 70 | # kong-net: 71 | # ipv4_address: 172.1.1.40 72 | - kong-net 73 | environment: 74 | KONG_DATABASE: postgres 75 | KONG_PG_HOST: kong-database 76 | KONG_PG_DATABASE: kong 77 | KONG_PG_USER: kong 78 | KONG_PG_PASSWORD: kong 79 | KONG_ADMIN_LISTEN: 0.0.0.0:8001, 0.0.0.0:8444 ssl # to secure the admin API comment this line. it is not recommended to expose the admin API from localhost or outside the cluster 80 | KONG_PLUGINSERVER_NAMES: js 81 | KONG_PLUGINSERVER_JS_SOCKET: /usr/local/kong/js_pluginserver.sock 82 | KONG_PLUGINSERVER_JS_START_CMD: /usr/local/bin/kong-js-pluginserver -v --plugins-directory /usr/local/kong/js-plugins 83 | KONG_PLUGINSERVER_JS_QUERY_CMD: /usr/local/bin/kong-js-pluginserver --plugins-directory /usr/local/kong/js-plugins --dump-all-plugins 84 | KONG_PLUGINS: bundled,oidc 85 | depends_on: 86 | - kong-database 87 | - kong-migration 88 | healthcheck: 89 | test: ["CMD", "kong", "health"] 90 | interval: 10s 91 | timeout: 10s 92 | retries: 10 93 | ports: 94 | - "8000:8000" 95 | - "8001:8001" 96 | - "8443:8443" 97 | - "8444:8444" 98 | 99 | ####################################### 100 | # Konga database prepare 101 | ####################################### 102 | konga-prepare: 103 | image: pantsel/konga:latest 104 | command: "-c prepare -a postgres -u postgresql://kong:kong@kong-database:5432/konga" 105 | networks: 106 | - kong-net 107 | restart: on-failure 108 | depends_on: 109 | - kong-database 110 | 111 | ####################################### 112 | # Konga: Kong GUI 113 | ####################################### 114 | konga: 115 | image: pantsel/konga:latest 116 | restart: always 117 | networks: 118 | - kong-net 119 | environment: 120 | DB_ADAPTER: postgres 121 | DB_URI: postgresql://kong:kong@kong-database:5432/konga 122 | NODE_ENV: production 123 | depends_on: 124 | - kong-database 125 | ports: 126 | - "1337:1337" 127 | -------------------------------------------------------------------------------- /services/auth/.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://postgres:postgres@localhost:5433/auth_db?schema=public" 2 | JWT_SECRET=My_Secret_Key -------------------------------------------------------------------------------- /services/auth/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Keep environment variables out of version control 3 | .env 4 | -------------------------------------------------------------------------------- /services/auth/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "auth", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "ts-node-dev -r tsconfig-paths/register ./src/index.ts", 8 | "build": "tsc && tsc-alias", 9 | "migrate:dev": "prisma migrate dev", 10 | "migrate:prod": "prisma migrate deploy" 11 | }, 12 | "dependencies": { 13 | "@prisma/client": "^5.10.1", 14 | "axios": "^1.6.7", 15 | "bcryptjs": "^2.4.3", 16 | "cors": "^2.8.5", 17 | "dotenv": "^16.4.5", 18 | "express": "^4.18.2", 19 | "jsonwebtoken": "^9.0.2", 20 | "morgan": "^1.10.0", 21 | "zod": "^3.22.4" 22 | }, 23 | "devDependencies": { 24 | "@types/bcryptjs": "^2.4.6", 25 | "@types/cors": "^2.8.17", 26 | "@types/express": "^4.17.21", 27 | "@types/node": "^20.11.19", 28 | "prisma": "^5.10.1", 29 | "ts-node-dev": "^2.0.0", 30 | "tsc": "^2.0.4", 31 | "tsc-alias": "^1.8.8", 32 | "tsconfig-paths": "^4.2.0", 33 | "typescript": "^5.3.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /services/auth/prisma/migrations/20240228073719_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN'); 3 | 4 | -- CreateEnum 5 | CREATE TYPE "AccountStatus" AS ENUM ('PENDING', 'ACTIVE', 'INACTIVE', 'SUSPENDED'); 6 | 7 | -- CreateEnum 8 | CREATE TYPE "LoginAttempt" AS ENUM ('SUCCESS', 'FAILED'); 9 | 10 | -- CreateEnum 11 | CREATE TYPE "VerificationStatus" AS ENUM ('PENDING', 'USED', 'EXPIRED'); 12 | 13 | -- CreateEnum 14 | CREATE TYPE "VerificationCodeType" AS ENUM ('ACCOUNT_ACTIVATION', 'PASSWORD_RESET', 'EMAIL_CHANGE', 'PHONE_CHANGE', 'TWO_FACTOR_AUTH', 'TWO_FACTOR_AUTH_DISABLE'); 15 | 16 | -- CreateTable 17 | CREATE TABLE "User" ( 18 | "id" TEXT NOT NULL, 19 | "name" TEXT NOT NULL, 20 | "email" TEXT NOT NULL, 21 | "password" TEXT NOT NULL, 22 | "role" "Role" NOT NULL DEFAULT 'USER', 23 | "verified" BOOLEAN NOT NULL DEFAULT false, 24 | "status" "AccountStatus" NOT NULL DEFAULT 'PENDING', 25 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 26 | "updatedAt" TIMESTAMP(3) NOT NULL, 27 | 28 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 29 | ); 30 | 31 | -- CreateTable 32 | CREATE TABLE "LoginHistory" ( 33 | "id" TEXT NOT NULL, 34 | "userId" TEXT NOT NULL, 35 | "ipAddress" TEXT, 36 | "userAgent" TEXT, 37 | "attempt" "LoginAttempt" NOT NULL DEFAULT 'SUCCESS', 38 | "loginAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 39 | 40 | CONSTRAINT "LoginHistory_pkey" PRIMARY KEY ("id") 41 | ); 42 | 43 | -- CreateTable 44 | CREATE TABLE "VerificationCode" ( 45 | "id" TEXT NOT NULL, 46 | "userId" TEXT NOT NULL, 47 | "status" "VerificationStatus" NOT NULL DEFAULT 'PENDING', 48 | "code" TEXT NOT NULL, 49 | "type" "VerificationCodeType" NOT NULL DEFAULT 'ACCOUNT_ACTIVATION', 50 | "issuedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 51 | "expiresAt" TIMESTAMP(3) NOT NULL, 52 | "verifiedAt" TIMESTAMP(3), 53 | 54 | CONSTRAINT "VerificationCode_pkey" PRIMARY KEY ("id") 55 | ); 56 | 57 | -- CreateIndex 58 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 59 | 60 | -- AddForeignKey 61 | ALTER TABLE "LoginHistory" ADD CONSTRAINT "LoginHistory_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 62 | 63 | -- AddForeignKey 64 | ALTER TABLE "VerificationCode" ADD CONSTRAINT "VerificationCode_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 65 | -------------------------------------------------------------------------------- /services/auth/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /services/auth/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | } 10 | 11 | datasource db { 12 | provider = "postgresql" 13 | url = env("DATABASE_URL") 14 | } 15 | 16 | enum Role { 17 | USER 18 | ADMIN 19 | } 20 | 21 | enum AccountStatus { 22 | PENDING 23 | ACTIVE 24 | INACTIVE 25 | SUSPENDED 26 | } 27 | 28 | model User { 29 | id String @id @default(cuid()) 30 | name String 31 | email String @unique 32 | password String 33 | role Role @default(USER) 34 | verified Boolean @default(false) 35 | status AccountStatus @default(PENDING) 36 | createdAt DateTime @default(now()) 37 | updatedAt DateTime @updatedAt 38 | loginHistories LoginHistory[] 39 | verificationCodes VerificationCode[] 40 | } 41 | 42 | enum LoginAttempt { 43 | SUCCESS 44 | FAILED 45 | } 46 | 47 | model LoginHistory { 48 | id String @id @default(cuid()) 49 | userId String 50 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 51 | ipAddress String? 52 | userAgent String? 53 | attempt LoginAttempt @default(SUCCESS) 54 | loginAt DateTime @default(now()) 55 | } 56 | 57 | enum VerificationStatus { 58 | PENDING 59 | USED 60 | EXPIRED 61 | } 62 | 63 | enum VerificationCodeType { 64 | ACCOUNT_ACTIVATION 65 | PASSWORD_RESET 66 | EMAIL_CHANGE 67 | PHONE_CHANGE 68 | TWO_FACTOR_AUTH 69 | TWO_FACTOR_AUTH_DISABLE 70 | } 71 | 72 | model VerificationCode { 73 | id String @id @default(cuid()) 74 | userId String 75 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 76 | status VerificationStatus @default(PENDING) 77 | code String 78 | type VerificationCodeType @default(ACCOUNT_ACTIVATION) 79 | issuedAt DateTime @default(now()) 80 | expiresAt DateTime 81 | verifiedAt DateTime? 82 | } 83 | -------------------------------------------------------------------------------- /services/auth/src/config.ts: -------------------------------------------------------------------------------- 1 | export const USER_SERVICE = 2 | process.env.USER_SERVICE_URL || 'http://localhost:4000'; 3 | 4 | export const EMAIL_SERVICE = 5 | process.env.EMAIL_SERVICE_URL || 'http://localhost:4005'; 6 | -------------------------------------------------------------------------------- /services/auth/src/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export { default as verifyToken } from './verifyToken'; 2 | export { default as userRegistration } from './userRegistration'; 3 | export { default as userLogin } from './userLogin'; 4 | export { default as verifyEmail } from './verifyEmail'; 5 | -------------------------------------------------------------------------------- /services/auth/src/controllers/userLogin.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcryptjs'; 2 | import jwt from 'jsonwebtoken'; 3 | import { Response, Request, NextFunction } from 'express'; 4 | import prisma from '@/prisma'; 5 | import { UserLoginSchema } from '@/schemas'; 6 | import { LoginAttempt } from '@prisma/client'; 7 | 8 | type LoginHistory = { 9 | userId: string; 10 | userAgent: string | undefined; 11 | ipAddress: string | undefined; 12 | attempt: LoginAttempt; 13 | }; 14 | 15 | const createLoginHistory = async (info: LoginHistory) => { 16 | await prisma.loginHistory.create({ 17 | data: { 18 | userId: info.userId, 19 | userAgent: info.userAgent, 20 | ipAddress: info.ipAddress, 21 | attempt: info.attempt, 22 | }, 23 | }); 24 | }; 25 | 26 | const userLogin = async (req: Request, res: Response, next: NextFunction) => { 27 | try { 28 | const ipAddress = 29 | (req.headers['x-forwarded-for'] as string) || req.ip || ''; 30 | const userAgent = req.headers['user-agent'] || ''; 31 | 32 | // Validate the request body 33 | const parsedBody = UserLoginSchema.safeParse(req.body); 34 | if (!parsedBody.success) { 35 | return res.status(400).json({ errors: parsedBody.error.errors }); 36 | } 37 | 38 | // check if the user exists 39 | const user = await prisma.user.findUnique({ 40 | where: { 41 | email: parsedBody.data.email, 42 | }, 43 | }); 44 | if (!user) { 45 | return res.status(400).json({ message: 'Invalid credentials' }); 46 | } 47 | 48 | // compare password 49 | const isMatch = await bcrypt.compare( 50 | parsedBody.data.password, 51 | user.password 52 | ); 53 | if (!isMatch) { 54 | await createLoginHistory({ 55 | userId: user.id, 56 | userAgent, 57 | ipAddress, 58 | attempt: 'FAILED', 59 | }); 60 | return res.status(400).json({ message: 'Invalid credentials' }); 61 | } 62 | 63 | // check if the user is verified 64 | if (!user.verified) { 65 | await createLoginHistory({ 66 | userId: user.id, 67 | userAgent, 68 | ipAddress, 69 | attempt: 'FAILED', 70 | }); 71 | return res.status(400).json({ message: 'User not verified' }); 72 | } 73 | 74 | // check if the account is active 75 | if (user.status !== 'ACTIVE') { 76 | await createLoginHistory({ 77 | userId: user.id, 78 | userAgent, 79 | ipAddress, 80 | attempt: 'FAILED', 81 | }); 82 | return res.status(400).json({ 83 | message: `Your account is ${user.status.toLocaleLowerCase()}`, 84 | }); 85 | } 86 | 87 | console.log("JWT_SECRET", process.env.JWT_SECRET) 88 | // generate access token 89 | const accessToken = jwt.sign( 90 | { userId: user.id, email: user.email, name: user.name, role: user.role }, 91 | process.env.JWT_SECRET ?? 'My_Secret_Key', 92 | { expiresIn: '2h' } 93 | ); 94 | 95 | await createLoginHistory({ 96 | userId: user.id, 97 | userAgent, 98 | ipAddress, 99 | attempt: 'SUCCESS', 100 | }); 101 | 102 | return res.status(200).json({ 103 | accessToken, 104 | }); 105 | } catch (error) { 106 | next(error); 107 | } 108 | }; 109 | 110 | export default userLogin; 111 | -------------------------------------------------------------------------------- /services/auth/src/controllers/userRegistration.ts: -------------------------------------------------------------------------------- 1 | import { Response, Request, NextFunction } from 'express'; 2 | import prisma from '@/prisma'; 3 | import { UserCreateSchema } from '@/schemas'; 4 | import bcrypt from 'bcryptjs'; 5 | import axios from 'axios'; 6 | import { EMAIL_SERVICE, USER_SERVICE } from '@/config'; 7 | 8 | const generateVerificationCode = () => { 9 | // Get current timestamp in milliseconds 10 | const timestamp = new Date().getTime().toString(); 11 | 12 | // Generate a random 2-digit number 13 | const randomNum = Math.floor(10 + Math.random() * 90); // Ensures 2-digit random number 14 | 15 | // Combine timestamp and random number and extract last 5 digits 16 | let code = (timestamp + randomNum).slice(-5); 17 | 18 | return code; // 19 | }; 20 | 21 | const userRegistration = async ( 22 | req: Request, 23 | res: Response, 24 | next: NextFunction 25 | ) => { 26 | try { 27 | // Validate the request body 28 | const parsedBody = UserCreateSchema.safeParse(req.body); 29 | if (!parsedBody.success) { 30 | return res.status(400).json({ errors: parsedBody.error.errors }); 31 | } 32 | 33 | // check if the user already exists 34 | const existingUser = await prisma.user.findUnique({ 35 | where: { 36 | email: parsedBody.data.email, 37 | }, 38 | }); 39 | if (existingUser) { 40 | return res.status(400).json({ message: 'User already exists' }); 41 | } 42 | 43 | // hash the password 44 | const salt = await bcrypt.genSalt(10); 45 | const hashedPassword = await bcrypt.hash(parsedBody.data.password, salt); 46 | 47 | // create the auth user 48 | const user = await prisma.user.create({ 49 | data: { 50 | ...parsedBody.data, 51 | password: hashedPassword, 52 | }, 53 | select: { 54 | id: true, 55 | email: true, 56 | name: true, 57 | role: true, 58 | status: true, 59 | verified: true, 60 | }, 61 | }); 62 | console.log('User created: ', user); 63 | 64 | // create the user profile by calling the user service 65 | await axios.post(`${USER_SERVICE}/users`, { 66 | authUserId: user.id, 67 | name: user.name, 68 | email: user.email, 69 | }); 70 | 71 | // generate verification code 72 | const code = generateVerificationCode(); 73 | await prisma.verificationCode.create({ 74 | data: { 75 | userId: user.id, 76 | code, 77 | expiresAt: new Date(Date.now() + 1000 * 60 * 60 * 24), // 24 hours 78 | }, 79 | }); 80 | 81 | // send verification email 82 | await axios.post(`${EMAIL_SERVICE}/emails/send`, { 83 | recipient: user.email, 84 | subject: 'Email Verification', 85 | body: `Your verification code is ${code}`, 86 | source: 'user-registration', 87 | }); 88 | 89 | return res.status(201).json({ 90 | message: 'User created. Check your email for verification code', 91 | user, 92 | }); 93 | } catch (error) { 94 | next(error); 95 | } 96 | }; 97 | 98 | export default userRegistration; 99 | -------------------------------------------------------------------------------- /services/auth/src/controllers/verifyEmail.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import jwt from 'jsonwebtoken'; 3 | import prisma from '@/prisma'; 4 | import { EmailVerificationSchema } from '@/schemas'; 5 | import axios from 'axios'; 6 | import { EMAIL_SERVICE } from '@/config'; 7 | 8 | const verifyEmail = async (req: Request, res: Response, next: NextFunction) => { 9 | try { 10 | // Validate the request body 11 | const parsedBody = EmailVerificationSchema.safeParse(req.body); 12 | if (!parsedBody.success) { 13 | return res.status(400).json({ errors: parsedBody.error.errors }); 14 | } 15 | 16 | // check if the user with email exists 17 | const user = await prisma.user.findUnique({ 18 | where: { email: parsedBody.data.email }, 19 | }); 20 | if (!user) { 21 | return res.status(404).json({ message: 'User not found' }); 22 | } 23 | 24 | // find the verification code 25 | const verificationCode = await prisma.verificationCode.findFirst({ 26 | where: { 27 | userId: user.id, 28 | code: parsedBody.data.code, 29 | }, 30 | }); 31 | if (!verificationCode) { 32 | return res.status(404).json({ message: 'Invalid verification code' }); 33 | } 34 | 35 | // if the code has expired 36 | if (verificationCode.expiresAt < new Date()) { 37 | return res.status(400).json({ message: 'Verification code expired' }); 38 | } 39 | 40 | // update user status to verified 41 | await prisma.user.update({ 42 | where: { id: user.id }, 43 | data: { verified: true, status: 'ACTIVE' }, 44 | }); 45 | 46 | // update verification code status to used 47 | await prisma.verificationCode.update({ 48 | where: { id: verificationCode.id }, 49 | data: { status: 'USED', verifiedAt: new Date() }, 50 | }); 51 | 52 | // send success email 53 | await axios.post(`${EMAIL_SERVICE}/emails/send`, { 54 | recipient: user.email, 55 | subject: 'Email Verified', 56 | body: 'Your email has been verified successfully', 57 | source: 'verify-email', 58 | }); 59 | 60 | return res.status(200).json({ message: 'Email verified successfully' }); 61 | } catch (error) { 62 | next(error); 63 | } 64 | }; 65 | 66 | export default verifyEmail; 67 | -------------------------------------------------------------------------------- /services/auth/src/controllers/verifyToken.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import jwt from 'jsonwebtoken'; 3 | import prisma from '@/prisma'; 4 | import { AccessTokenSchema } from '@/schemas'; 5 | 6 | const verifyToken = async (req: Request, res: Response, next: NextFunction) => { 7 | try { 8 | // Validate the request body 9 | const parsedBody = AccessTokenSchema.safeParse(req.body); 10 | if (!parsedBody.success) { 11 | return res.status(400).json({ errors: parsedBody.error.errors }); 12 | } 13 | 14 | 15 | console.log("JWT_SECRET", process.env.JWT_SECRET) 16 | 17 | const { accessToken } = parsedBody.data; 18 | const decoded = jwt.verify(accessToken, process.env.JWT_SECRET as string); 19 | 20 | const user = await prisma.user.findUnique({ 21 | where: { id: (decoded as any).userId }, 22 | select: { 23 | id: true, 24 | email: true, 25 | name: true, 26 | role: true, 27 | }, 28 | }); 29 | 30 | if (!user) { 31 | return res.status(401).json({ message: 'Unauthorized' }); 32 | } 33 | 34 | return res.status(200).json({ message: 'Authorized', user }); 35 | } catch (error) { 36 | next(error); 37 | } 38 | }; 39 | 40 | export default verifyToken; 41 | -------------------------------------------------------------------------------- /services/auth/src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import dotenv from 'dotenv'; 3 | import cors from 'cors'; 4 | import morgan from 'morgan'; 5 | import { 6 | userLogin, 7 | userRegistration, 8 | verifyEmail, 9 | verifyToken, 10 | } from './controllers'; 11 | 12 | dotenv.config(); 13 | 14 | const app = express(); 15 | app.use(express.json()); 16 | app.use(cors()); 17 | app.use(morgan('dev')); 18 | 19 | app.get('/health', (_req, res) => { 20 | res.status(200).json({ status: 'UP' }); 21 | }); 22 | 23 | // app.use((req, res, next) => { 24 | // const allowedOrigins = ['http://localhost:8081', 'http://127.0.0.1:8081']; 25 | // const origin = req.headers.origin || ''; 26 | 27 | // if (allowedOrigins.includes(origin)) { 28 | // res.setHeader('Access-Control-Allow-Origin', origin); 29 | // next(); 30 | // } else { 31 | // res.status(403).json({ message: 'Forbidden' }); 32 | // } 33 | // }); 34 | 35 | // routes 36 | app.post('/auth/register', userRegistration); 37 | app.post('/auth/login', userLogin); 38 | app.post('/auth/verify-token', verifyToken); 39 | app.post('/auth/verify-email', verifyEmail); 40 | 41 | // 404 handler 42 | app.use((_req, res) => { 43 | res.status(404).json({ message: 'Not found' }); 44 | }); 45 | 46 | // Error handler 47 | app.use((err, _req, res, _next) => { 48 | console.error(err.stack); 49 | res.status(500).json({ message: 'Internal server error' }); 50 | }); 51 | 52 | const port = process.env.PORT || 4003; 53 | const serviceName = process.env.SERVICE_NAME || 'Auth-Service'; 54 | 55 | app.listen(port, () => { 56 | console.log(`${serviceName} is running on port ${port}`); 57 | }); 58 | -------------------------------------------------------------------------------- /services/auth/src/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | const prisma = new PrismaClient(); 3 | export default prisma; 4 | -------------------------------------------------------------------------------- /services/auth/src/schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const UserCreateSchema = z.object({ 4 | email: z.string().email(), 5 | password: z.string().min(6).max(255), 6 | name: z.string().min(3).max(255), 7 | }); 8 | 9 | export const UserLoginSchema = z.object({ 10 | email: z.string().email(), 11 | password: z.string(), 12 | }); 13 | 14 | export const AccessTokenSchema = z.object({ 15 | accessToken: z.string(), 16 | }); 17 | 18 | export const EmailVerificationSchema = z.object({ 19 | email: z.string().email(), 20 | code: z.string(), 21 | }); 22 | -------------------------------------------------------------------------------- /services/auth/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "lib": ["ES6"], 6 | "allowJs": true, 7 | "module": "CommonJS", 8 | "rootDir": ".", 9 | "outDir": "./dist", 10 | "esModuleInterop": true, 11 | "strict": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "allowSyntheticDefaultImports": true, 17 | "typeRoots": ["./src/types", "./node_modules/@types"], 18 | "sourceMap": true, 19 | "types": ["node", "express"], 20 | "noImplicitAny": false, 21 | "baseUrl": "./src", 22 | "paths": { 23 | "@/*": ["*"] 24 | } 25 | }, 26 | "include": ["src/**/*"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /services/cart/.env.example: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stack-Learner/practical-microservice-workshop/22fa111ba74c6a88b9676ce9605b2bc8f88e9b8c/services/cart/.env.example -------------------------------------------------------------------------------- /services/cart/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Keep environment variables out of version control 3 | .env 4 | -------------------------------------------------------------------------------- /services/cart/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "product", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "ts-node-dev -r tsconfig-paths/register ./src/index.ts", 8 | "build": "tsc && tsc-alias" 9 | }, 10 | "dependencies": { 11 | "amqplib": "^0.10.3", 12 | "axios": "^1.6.7", 13 | "cors": "^2.8.5", 14 | "dotenv": "^16.4.5", 15 | "express": "^4.18.2", 16 | "express-rate-limit": "^7.1.5", 17 | "helmet": "^7.1.0", 18 | "ioredis": "^5.3.2", 19 | "morgan": "^1.10.0", 20 | "uuid": "^9.0.1", 21 | "zod": "^3.22.4" 22 | }, 23 | "devDependencies": { 24 | "@types/cors": "^2.8.17", 25 | "@types/express": "^4.17.21", 26 | "@types/ioredis": "^5.0.0", 27 | "@types/node": "^20.11.19", 28 | "@types/uuid": "^9.0.8", 29 | "ts-node-dev": "^2.0.0", 30 | "tsc": "^2.0.4", 31 | "tsc-alias": "^1.8.8", 32 | "tsconfig-paths": "^4.2.0", 33 | "typescript": "^5.3.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /services/cart/src/config.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | dotenv.config({ 3 | path: '.env', 4 | }); 5 | 6 | export const REDIS_PORT = process.env.REDIS_PORT 7 | ? parseInt(process.env.REDIS_PORT) 8 | : 6379; 9 | export const REDIS_HOST = process.env.REDIS_HOST || 'localhost'; 10 | 11 | export const CART_TTL = process.env.CART_TTL 12 | ? parseInt(process.env.CART_TTL) 13 | : 900; 14 | 15 | export const INVENTORY_SERVICE = 16 | process.env.INVENTORY_SERVICE_URL || 'http://localhost:4002'; 17 | -------------------------------------------------------------------------------- /services/cart/src/controllers/addToCart.ts: -------------------------------------------------------------------------------- 1 | import { CART_TTL, INVENTORY_SERVICE } from '@/config'; 2 | import redis from '@/redis'; 3 | import { CartItemSchema } from '@/schemas'; 4 | import axios from 'axios'; 5 | import { Request, Response, NextFunction } from 'express'; 6 | import { v4 as uuid } from 'uuid'; 7 | 8 | const addToCart = async (req: Request, res: Response, next: NextFunction) => { 9 | try { 10 | // validate request body 11 | const parsedBody = CartItemSchema.safeParse(req.body); 12 | if (!parsedBody.success) { 13 | return res.status(400).json({ errors: parsedBody.error.errors }); 14 | } 15 | 16 | let cartSessionId = (req.headers['x-cart-session-id'] as string) || null; 17 | 18 | // check if cart session id is present in the request header and exists in the store 19 | if (cartSessionId) { 20 | const exists = await redis.exists(`sessions:${cartSessionId}`); 21 | console.log('Session Exists: ', exists); 22 | 23 | if (!exists) { 24 | cartSessionId = null; 25 | } 26 | } 27 | 28 | // if cart session id is not present, create a new one 29 | if (cartSessionId === null) { 30 | cartSessionId = uuid(); 31 | console.log('New Session ID: ', cartSessionId); 32 | 33 | // set the cart session id in the redis store 34 | await redis.setex(`sessions:${cartSessionId}`, CART_TTL, cartSessionId); 35 | 36 | // set the cart session id in the response header 37 | res.setHeader('x-cart-session-id', cartSessionId); 38 | } 39 | 40 | // check if the inventory is available 41 | const { data } = await axios.get( 42 | `${INVENTORY_SERVICE}/inventories/${parsedBody.data.inventoryId}` 43 | ); 44 | if (Number(data.quantity) < parsedBody.data.quantity) { 45 | return res.status(400).json({ message: 'Inventory not available' }); 46 | } 47 | 48 | // add item to the cart 49 | // TODO: Check if the product already exists in the cart 50 | // Logic: parsedBody.data.quantity - existingQuantity 51 | await redis.hset( 52 | `cart:${cartSessionId}`, 53 | parsedBody.data.productId, 54 | JSON.stringify({ 55 | inventoryId: parsedBody.data.inventoryId, 56 | quantity: parsedBody.data.quantity, 57 | }) 58 | ); 59 | 60 | // update inventory 61 | await axios.put( 62 | `${INVENTORY_SERVICE}/inventories/${parsedBody.data.inventoryId}`, 63 | { 64 | quantity: parsedBody.data.quantity, 65 | actionType: 'OUT', 66 | } 67 | ); 68 | 69 | return res 70 | .status(200) 71 | .json({ message: 'Item added to cart', cartSessionId }); 72 | } catch (error) { 73 | next(error); 74 | } 75 | }; 76 | 77 | export default addToCart; 78 | -------------------------------------------------------------------------------- /services/cart/src/controllers/clearCart.ts: -------------------------------------------------------------------------------- 1 | import redis from '@/redis'; 2 | import { Request, Response, NextFunction } from 'express'; 3 | 4 | const clearCart = async (req: Request, res: Response, next: NextFunction) => { 5 | try { 6 | const cartSessionId = (req.headers['x-cart-session-id'] as string) || null; 7 | 8 | if (!cartSessionId) { 9 | return res.status(200).json({ message: 'Cart is empty' }); 10 | } 11 | 12 | // check if the session id exists in the store 13 | const exist = await redis.exists(`sessions:${cartSessionId}`); 14 | if (!exist) { 15 | delete req.headers['x-cart-session-id']; 16 | return res.status(200).json({ message: 'Cart is empty' }); 17 | } 18 | 19 | // clear the cart 20 | 21 | await redis.del(`sessions:${cartSessionId}`); 22 | await redis.del(`cart:${cartSessionId}`); 23 | 24 | delete req.headers['x-cart-session-id']; 25 | 26 | res.status(200).json({ message: 'Cart cleared' }); 27 | } catch (error) { 28 | next(error); 29 | } 30 | }; 31 | 32 | export default clearCart; 33 | -------------------------------------------------------------------------------- /services/cart/src/controllers/getMyCart.ts: -------------------------------------------------------------------------------- 1 | import redis from '@/redis'; 2 | import { Request, Response, NextFunction } from 'express'; 3 | 4 | const getMyCart = async (req: Request, res: Response, next: NextFunction) => { 5 | try { 6 | const cartSessionId = (req.headers['x-cart-session-id'] as string) || null; 7 | 8 | if (!cartSessionId) { 9 | return res.status(200).json({ data: [] }); 10 | } 11 | 12 | // check if the session id exists in the store 13 | const session = await redis.exists(`sessions:${cartSessionId}`); 14 | if (!session) { 15 | await redis.del(`cart:${cartSessionId}`); 16 | return res.status(200).json({ data: [] }); 17 | } 18 | 19 | const items = await redis.hgetall(`cart:${cartSessionId}`); 20 | if (Object.keys(items).length === 0) { 21 | return res.status(200).json({ data: [] }); 22 | } 23 | 24 | // format the items 25 | const formattedItems = Object.keys(items).map((key) => { 26 | const { quantity, inventoryId } = JSON.parse(items[key]) as { 27 | inventoryId: string; 28 | quantity: number; 29 | }; 30 | return { 31 | inventoryId, 32 | quantity, 33 | productId: key, 34 | }; 35 | }); 36 | 37 | res.status(200).json({ items: formattedItems }); 38 | } catch (error) { 39 | next(error); 40 | } 41 | }; 42 | 43 | export default getMyCart; 44 | -------------------------------------------------------------------------------- /services/cart/src/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export { default as addToCart } from './addToCart'; 2 | export { default as getMyCart } from './getMyCart'; 3 | export { default as clearCart } from './clearCart'; 4 | -------------------------------------------------------------------------------- /services/cart/src/events/onKeyExpires.ts: -------------------------------------------------------------------------------- 1 | import { REDIS_HOST, REDIS_PORT } from '@/config'; 2 | import { clearCart } from '@/services'; 3 | import { Redis } from 'ioredis'; 4 | 5 | const redis = new Redis({ 6 | host: REDIS_HOST, 7 | port: REDIS_PORT, 8 | }); 9 | 10 | const CHANNEL_KEY = '__keyevent@0__:expired'; 11 | redis.config('SET', 'notify-keyspace-events', 'Ex'); 12 | redis.subscribe(CHANNEL_KEY); 13 | 14 | redis.on('message', async (ch, message) => { 15 | if (ch === CHANNEL_KEY) { 16 | console.log('Key expired: ', message); 17 | const cartKey = message.split(':').pop(); 18 | if (!cartKey) return; 19 | 20 | clearCart(cartKey); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /services/cart/src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import dotenv from 'dotenv'; 3 | import helmet from 'helmet'; 4 | import rateLimit from 'express-rate-limit'; 5 | import morgan from 'morgan'; 6 | import { addToCart, clearCart, getMyCart } from './controllers'; 7 | import './events/onKeyExpires'; 8 | import './receiver'; 9 | 10 | dotenv.config(); 11 | 12 | const app = express(); 13 | 14 | // security middleware 15 | app.use(helmet()); 16 | 17 | // Rate limiting middleware 18 | const limiter = rateLimit({ 19 | windowMs: 15 * 60 * 1000, // 15 minutes 20 | max: 100, // limit each IP to 100 requests per windowMs 21 | handler: (_req, res) => { 22 | res 23 | .status(429) 24 | .json({ message: 'Too many requests, please try again later.' }); 25 | }, 26 | }); 27 | app.use('/api', limiter); 28 | 29 | // request logger 30 | app.use(morgan('dev')); 31 | app.use(express.json()); 32 | 33 | // TODO: Auth middleware 34 | 35 | // routes 36 | app.post('/cart/add-to-cart', addToCart); 37 | app.get('/cart/me', getMyCart); 38 | app.get('/cart/clear', clearCart); 39 | 40 | // health check 41 | app.get('/health', (_req, res) => { 42 | res.json({ message: `${serviceName} is running` }); 43 | }); 44 | 45 | // 404 handler 46 | app.use((_req, res) => { 47 | res.status(404).json({ message: 'Not Found' }); 48 | }); 49 | 50 | // error handler 51 | app.use((err, _req, res, _next) => { 52 | console.error(err.stack); 53 | res.status(500).json({ message: 'Internal Server Error' }); 54 | }); 55 | 56 | const port = process.env.PORT || 4004; 57 | const serviceName = process.env.SERVICE_NAME || 'Cart-Service'; 58 | 59 | app.listen(port, () => { 60 | console.log(`${serviceName} is running on port ${port}`); 61 | }); 62 | -------------------------------------------------------------------------------- /services/cart/src/receiver.ts: -------------------------------------------------------------------------------- 1 | import amqp from 'amqplib'; 2 | import redis from './redis'; 3 | 4 | const receiveFromQueue = async ( 5 | queue: string, 6 | callback: (message: string) => void 7 | ) => { 8 | const connection = await amqp.connect('amqp://localhost'); 9 | const channel = await connection.createChannel(); 10 | 11 | const exchange = 'order'; 12 | await channel.assertExchange(exchange, 'direct', { durable: true }); 13 | 14 | const q = await channel.assertQueue(queue, { durable: true }); 15 | await channel.bindQueue(q.queue, exchange, queue); 16 | 17 | channel.consume( 18 | q.queue, 19 | (msg) => { 20 | if (msg) { 21 | callback(msg.content.toString()); 22 | } 23 | }, 24 | { noAck: true } 25 | ); 26 | }; 27 | 28 | receiveFromQueue('clear-cart', (msg) => { 29 | console.log('Received from queue: clear-cart'); 30 | const parsedMessage = JSON.parse(msg); 31 | 32 | const cartSessionId = parsedMessage.cartSessionId; 33 | redis.del(`session:${cartSessionId}`); 34 | redis.del(`cart:${cartSessionId}`); 35 | 36 | console.log('Cart cleared'); 37 | }); 38 | -------------------------------------------------------------------------------- /services/cart/src/redis.ts: -------------------------------------------------------------------------------- 1 | import { Redis } from 'ioredis'; 2 | import { REDIS_HOST, REDIS_PORT } from './config'; 3 | 4 | const redis = new Redis({ 5 | host: REDIS_HOST, 6 | port: REDIS_PORT, 7 | }); 8 | 9 | export default redis; 10 | -------------------------------------------------------------------------------- /services/cart/src/schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const CartItemSchema = z.object({ 4 | productId: z.string(), 5 | inventoryId: z.string(), 6 | quantity: z.number(), 7 | }); 8 | -------------------------------------------------------------------------------- /services/cart/src/services/index.ts: -------------------------------------------------------------------------------- 1 | import { INVENTORY_SERVICE } from '@/config'; 2 | import redis from '@/redis'; 3 | import axios from 'axios'; 4 | 5 | export const clearCart = async (id: string) => { 6 | try { 7 | const data = await redis.hgetall(`cart:${id}`); 8 | if (Object.keys(data).length === 0) { 9 | return; 10 | } 11 | 12 | const items = Object.keys(data).map((key) => { 13 | const { quantity, inventoryId } = JSON.parse(data[key]) as { 14 | inventoryId: string; 15 | quantity: number; 16 | }; 17 | return { 18 | inventoryId, 19 | quantity, 20 | productId: key, 21 | }; 22 | }); 23 | 24 | // update inventory 25 | const requests = items.map((item) => { 26 | return axios.put(`${INVENTORY_SERVICE}/inventories/${item.inventoryId}`, { 27 | quantity: item.quantity, 28 | actionType: 'IN', 29 | }); 30 | }); 31 | 32 | Promise.all(requests); 33 | console.log('Inventory updated'); 34 | 35 | // clear the cart 36 | await redis.del(`cart:${id}`); 37 | } catch (error) { 38 | console.log(error); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /services/cart/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "lib": ["ES6"], 6 | "allowJs": true, 7 | "module": "CommonJS", 8 | "rootDir": ".", 9 | "outDir": "./dist", 10 | "esModuleInterop": true, 11 | "strict": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "allowSyntheticDefaultImports": true, 17 | "typeRoots": ["./src/types", "./node_modules/@types"], 18 | "sourceMap": true, 19 | "types": ["node", "express"], 20 | "noImplicitAny": false, 21 | "baseUrl": "./src", 22 | "paths": { 23 | "@/*": ["*"] 24 | } 25 | }, 26 | "include": ["src/**/*"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /services/email/.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://postgres:postgres@localhost:5433/mail_db?schema=public" -------------------------------------------------------------------------------- /services/email/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Keep environment variables out of version control 3 | .env 4 | -------------------------------------------------------------------------------- /services/email/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "email", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "ts-node-dev -r tsconfig-paths/register ./src/index.ts", 8 | "build": "tsc && tsc-alias", 9 | "migrate:dev": "prisma migrate dev", 10 | "migrate:prod": "prisma migrate deploy" 11 | }, 12 | "dependencies": { 13 | "@prisma/client": "^5.10.1", 14 | "amqplib": "^0.10.3", 15 | "axios": "^1.6.7", 16 | "bcryptjs": "^2.4.3", 17 | "cors": "^2.8.5", 18 | "dotenv": "^16.4.5", 19 | "express": "^4.18.2", 20 | "jsonwebtoken": "^9.0.2", 21 | "morgan": "^1.10.0", 22 | "nodemailer": "^6.9.10", 23 | "zod": "^3.22.4" 24 | }, 25 | "devDependencies": { 26 | "@types/amqplib": "^0.10.5", 27 | "@types/bcryptjs": "^2.4.6", 28 | "@types/cors": "^2.8.17", 29 | "@types/express": "^4.17.21", 30 | "@types/node": "^20.11.19", 31 | "@types/nodemailer": "^6.4.14", 32 | "prisma": "^5.10.1", 33 | "ts-node-dev": "^2.0.0", 34 | "tsc": "^2.0.4", 35 | "tsc-alias": "^1.8.8", 36 | "tsconfig-paths": "^4.2.0", 37 | "typescript": "^5.3.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /services/email/prisma/migrations/20240228074009_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Email" ( 3 | "id" TEXT NOT NULL, 4 | "sender" TEXT NOT NULL, 5 | "recipient" TEXT NOT NULL, 6 | "subject" TEXT NOT NULL, 7 | "body" TEXT NOT NULL, 8 | "source" TEXT NOT NULL, 9 | "sentAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 10 | 11 | CONSTRAINT "Email_pkey" PRIMARY KEY ("id") 12 | ); 13 | -------------------------------------------------------------------------------- /services/email/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /services/email/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | } 10 | 11 | datasource db { 12 | provider = "postgresql" 13 | url = env("DATABASE_URL") 14 | } 15 | 16 | model Email { 17 | id String @id @default(cuid()) 18 | sender String 19 | recipient String 20 | subject String 21 | body String 22 | source String 23 | sentAt DateTime @default(now()) 24 | } 25 | -------------------------------------------------------------------------------- /services/email/src/config.ts: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer'; 2 | 3 | export const transporter = nodemailer.createTransport({ 4 | host: process.env.SMTP_HOST || 'host.docker.internal', 5 | port: parseInt(process.env.SMTP_PORT || '1025'), 6 | }); 7 | 8 | export const defaultSender = 9 | process.env.DEFAULT_SENDER_EMAIL || 'admin@example.com'; 10 | -------------------------------------------------------------------------------- /services/email/src/controllers/getEmails.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import prisma from '@/prisma'; 3 | 4 | const getEmails = async (_req: Request, res: Response, next: NextFunction) => { 5 | try { 6 | const emails = await prisma.email.findMany(); 7 | res.json(emails); 8 | } catch (error) { 9 | next(error); 10 | } 11 | }; 12 | 13 | export default getEmails; 14 | -------------------------------------------------------------------------------- /services/email/src/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export { default as getEmails } from './getEmails'; 2 | export { default as sendEmail } from './sendEmail'; 3 | -------------------------------------------------------------------------------- /services/email/src/controllers/sendEmail.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import prisma from '@/prisma'; 3 | import { EmailCreateSchema } from '@/schemas'; 4 | import { defaultSender, transporter } from '@/config'; 5 | 6 | const sendEmail = async (req: Request, res: Response, next: NextFunction) => { 7 | try { 8 | // Validate the request body 9 | const parsedBody = EmailCreateSchema.safeParse(req.body); 10 | if (!parsedBody.success) { 11 | return res.status(400).json({ errors: parsedBody.error.errors }); 12 | } 13 | 14 | // create mail option 15 | const { sender, recipient, subject, body, source } = parsedBody.data; 16 | const from = sender || defaultSender; 17 | const emailOption = { 18 | from, 19 | to: recipient, 20 | subject, 21 | text: body, 22 | }; 23 | 24 | // send the email 25 | const { rejected } = await transporter.sendMail(emailOption); 26 | if (rejected.length) { 27 | console.log('Email rejected: ', rejected); 28 | return res.status(500).json({ message: 'Failed' }); 29 | } 30 | 31 | await prisma.email.create({ 32 | data: { 33 | sender: from, 34 | recipient, 35 | subject, 36 | body, 37 | source, 38 | }, 39 | }); 40 | 41 | return res.status(200).json({ message: 'Email sent' }); 42 | } catch (error) { 43 | next(error); 44 | } 45 | }; 46 | 47 | export default sendEmail; 48 | -------------------------------------------------------------------------------- /services/email/src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import dotenv from 'dotenv'; 3 | import cors from 'cors'; 4 | import morgan from 'morgan'; 5 | import { getEmails, sendEmail } from './controllers'; 6 | import './receiver'; 7 | 8 | dotenv.config(); 9 | 10 | const app = express(); 11 | app.use(express.json()); 12 | app.use(cors()); 13 | app.use(morgan('dev')); 14 | 15 | app.get('/health', (_req, res) => { 16 | res.status(200).json({ status: 'UP' }); 17 | }); 18 | 19 | // app.use((req, res, next) => { 20 | // const allowedOrigins = ['http://localhost:8081', 'http://127.0.0.1:8081']; 21 | // const origin = req.headers.origin || ''; 22 | 23 | // if (allowedOrigins.includes(origin)) { 24 | // res.setHeader('Access-Control-Allow-Origin', origin); 25 | // next(); 26 | // } else { 27 | // res.status(403).json({ message: 'Forbidden' }); 28 | // } 29 | // }); 30 | 31 | // routes 32 | app.post('/emails/send', sendEmail); 33 | app.get('/emails', getEmails); 34 | 35 | // 404 handler 36 | app.use((_req, res) => { 37 | res.status(404).json({ message: 'Not found' }); 38 | }); 39 | 40 | // Error handler 41 | app.use((err, _req, res, _next) => { 42 | console.error(err.stack); 43 | res.status(500).json({ message: 'Internal server error' }); 44 | }); 45 | 46 | const port = process.env.PORT || 4005; 47 | const serviceName = process.env.SERVICE_NAME || 'Email-Service'; 48 | 49 | app.listen(port, () => { 50 | console.log(`${serviceName} is running on port ${port}`); 51 | }); 52 | -------------------------------------------------------------------------------- /services/email/src/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | const prisma = new PrismaClient(); 3 | export default prisma; 4 | -------------------------------------------------------------------------------- /services/email/src/receiver.ts: -------------------------------------------------------------------------------- 1 | import amqp from 'amqplib'; 2 | import { defaultSender, transporter } from './config'; 3 | import prisma from './prisma'; 4 | 5 | const receiveFromQueue = async ( 6 | queue: string, 7 | callback: (message: string) => void 8 | ) => { 9 | const connection = await amqp.connect('amqp://localhost'); 10 | const channel = await connection.createChannel(); 11 | 12 | const exchange = 'order'; 13 | await channel.assertExchange(exchange, 'direct', { durable: true }); 14 | 15 | const q = await channel.assertQueue(queue, { durable: true }); 16 | await channel.bindQueue(q.queue, exchange, queue); 17 | 18 | channel.consume( 19 | q.queue, 20 | (msg) => { 21 | if (msg) { 22 | callback(msg.content.toString()); 23 | } 24 | }, 25 | { noAck: true } 26 | ); 27 | }; 28 | 29 | receiveFromQueue('send-email', async (msg) => { 30 | const parsedBody = JSON.parse(msg); 31 | 32 | const { userEmail, grandTotal, id } = parsedBody; 33 | const from = defaultSender; 34 | const subject = 'Order Confirmation'; 35 | const body = `Thank you for your order. Your order id is ${id}. Your order total is $${grandTotal}`; 36 | 37 | const emailOption = { 38 | from, 39 | to: userEmail, 40 | subject, 41 | text: body, 42 | }; 43 | 44 | // send the email 45 | const { rejected } = await transporter.sendMail(emailOption); 46 | if (rejected.length) { 47 | console.log('Email rejected: ', rejected); 48 | return; 49 | } 50 | 51 | await prisma.email.create({ 52 | data: { 53 | sender: from, 54 | recipient: userEmail, 55 | subject: 'Order Confirmation', 56 | body, 57 | source: 'OrderConfirmation', 58 | }, 59 | }); 60 | console.log('Email sent'); 61 | }); 62 | -------------------------------------------------------------------------------- /services/email/src/schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const EmailCreateSchema = z.object({ 4 | recipient: z.string().email(), 5 | subject: z.string(), 6 | body: z.string(), 7 | source: z.string(), 8 | sender: z.string().email().optional(), 9 | }); 10 | -------------------------------------------------------------------------------- /services/email/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "lib": ["ES6"], 6 | "allowJs": true, 7 | "module": "CommonJS", 8 | "rootDir": ".", 9 | "outDir": "./dist", 10 | "esModuleInterop": true, 11 | "strict": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "allowSyntheticDefaultImports": true, 17 | "typeRoots": ["./src/types", "./node_modules/@types"], 18 | "sourceMap": true, 19 | "types": ["node", "express"], 20 | "noImplicitAny": false, 21 | "baseUrl": "./src", 22 | "paths": { 23 | "@/*": ["*"] 24 | } 25 | }, 26 | "include": ["src/**/*"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /services/inventory/.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://postgres:postgres@localhost:5433/inventory_db?schema=public" -------------------------------------------------------------------------------- /services/inventory/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Keep environment variables out of version control 3 | .env 4 | -------------------------------------------------------------------------------- /services/inventory/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inventory", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "ts-node-dev -r tsconfig-paths/register ./src/index.ts", 8 | "build": "tsc && tsc-alias", 9 | "migrate:dev": "prisma migrate dev", 10 | "migrate:prod": "prisma migrate deploy" 11 | }, 12 | "dependencies": { 13 | "@prisma/client": "^5.10.1", 14 | "axios": "^1.6.7", 15 | "cors": "^2.8.5", 16 | "dotenv": "^16.4.5", 17 | "express": "^4.18.2", 18 | "morgan": "^1.10.0", 19 | "zod": "^3.22.4" 20 | }, 21 | "devDependencies": { 22 | "@types/cors": "^2.8.17", 23 | "@types/express": "^4.17.21", 24 | "@types/node": "^20.11.19", 25 | "prisma": "^5.10.1", 26 | "ts-node-dev": "^2.0.0", 27 | "tsc": "^2.0.4", 28 | "tsc-alias": "^1.8.8", 29 | "tsconfig-paths": "^4.2.0", 30 | "typescript": "^5.3.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /services/inventory/prisma/migrations/20240228074051_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "ActionType" AS ENUM ('IN', 'OUT'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "Inventory" ( 6 | "id" TEXT NOT NULL, 7 | "sku" TEXT NOT NULL, 8 | "productId" TEXT NOT NULL, 9 | "quantity" INTEGER NOT NULL, 10 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 11 | "updatedAt" TIMESTAMP(3) NOT NULL, 12 | 13 | CONSTRAINT "Inventory_pkey" PRIMARY KEY ("id") 14 | ); 15 | 16 | -- CreateTable 17 | CREATE TABLE "History" ( 18 | "id" TEXT NOT NULL, 19 | "actionType" "ActionType" NOT NULL, 20 | "quantityChanged" INTEGER NOT NULL, 21 | "lastQuantity" INTEGER NOT NULL, 22 | "newQuantity" INTEGER NOT NULL, 23 | "inventoryId" TEXT NOT NULL, 24 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 25 | 26 | CONSTRAINT "History_pkey" PRIMARY KEY ("id") 27 | ); 28 | 29 | -- CreateIndex 30 | CREATE UNIQUE INDEX "Inventory_sku_key" ON "Inventory"("sku"); 31 | 32 | -- CreateIndex 33 | CREATE UNIQUE INDEX "Inventory_productId_key" ON "Inventory"("productId"); 34 | 35 | -- AddForeignKey 36 | ALTER TABLE "History" ADD CONSTRAINT "History_inventoryId_fkey" FOREIGN KEY ("inventoryId") REFERENCES "Inventory"("id") ON DELETE CASCADE ON UPDATE CASCADE; 37 | -------------------------------------------------------------------------------- /services/inventory/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /services/inventory/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | } 10 | 11 | datasource db { 12 | provider = "postgresql" 13 | url = env("DATABASE_URL") 14 | } 15 | 16 | model Inventory { 17 | id String @id @default(cuid()) 18 | sku String @unique 19 | productId String @unique 20 | quantity Int 21 | createdAt DateTime @default(now()) 22 | updatedAt DateTime @updatedAt 23 | histories History[] 24 | } 25 | 26 | enum ActionType { 27 | IN 28 | OUT 29 | } 30 | 31 | model History { 32 | id String @id @default(cuid()) 33 | actionType ActionType 34 | quantityChanged Int 35 | lastQuantity Int 36 | newQuantity Int 37 | inventory Inventory @relation(fields: [inventoryId], references: [id], onDelete: Cascade) 38 | inventoryId String 39 | createdAt DateTime @default(now()) 40 | } 41 | -------------------------------------------------------------------------------- /services/inventory/src/controllers/createInventory.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import prisma from '@/prisma'; 3 | import { InventoryCreateDTOSchema } from '@/schemas'; 4 | 5 | const createInventory = async ( 6 | req: Request, 7 | res: Response, 8 | next: NextFunction 9 | ) => { 10 | try { 11 | // Validate request body 12 | const parsedBody = InventoryCreateDTOSchema.safeParse(req.body); 13 | if (!parsedBody.success) { 14 | return res.status(400).json({ error: parsedBody.error.errors }); 15 | } 16 | 17 | // create inventory 18 | const inventory = await prisma.inventory.create({ 19 | data: { 20 | ...parsedBody.data, 21 | histories: { 22 | create: { 23 | actionType: 'IN', 24 | quantityChanged: parsedBody.data.quantity, 25 | lastQuantity: 0, 26 | newQuantity: parsedBody.data.quantity, 27 | }, 28 | }, 29 | }, 30 | select: { 31 | id: true, 32 | quantity: true, 33 | }, 34 | }); 35 | 36 | return res.status(201).json(inventory); 37 | } catch (error) { 38 | next(error); 39 | } 40 | }; 41 | 42 | export default createInventory; 43 | -------------------------------------------------------------------------------- /services/inventory/src/controllers/getInventoryById.ts: -------------------------------------------------------------------------------- 1 | import prisma from '@/prisma'; 2 | import { Request, Response, NextFunction } from 'express'; 3 | 4 | const getInventoryById = async ( 5 | req: Request, 6 | res: Response, 7 | next: NextFunction 8 | ) => { 9 | try { 10 | const { id } = req.params; 11 | const inventory = await prisma.inventory.findUnique({ 12 | where: { id }, 13 | select: { 14 | quantity: true, 15 | }, 16 | }); 17 | 18 | if (!inventory) { 19 | return res.status(404).json({ message: 'Inventory not found' }); 20 | } 21 | 22 | return res.status(200).json(inventory); 23 | } catch (err) { 24 | next(err); 25 | } 26 | }; 27 | 28 | export default getInventoryById; 29 | -------------------------------------------------------------------------------- /services/inventory/src/controllers/getInventoryDetails.ts: -------------------------------------------------------------------------------- 1 | import prisma from '@/prisma'; 2 | import { Request, Response, NextFunction } from 'express'; 3 | 4 | const getInventoryDetails = async ( 5 | req: Request, 6 | res: Response, 7 | next: NextFunction 8 | ) => { 9 | try { 10 | const { id } = req.params; 11 | const inventory = await prisma.inventory.findUnique({ 12 | where: { id }, 13 | include: { 14 | histories: { 15 | orderBy: { 16 | createdAt: 'desc', 17 | }, 18 | }, 19 | }, 20 | }); 21 | 22 | if (!inventory) { 23 | return res.status(404).json({ message: 'Inventory not found' }); 24 | } 25 | 26 | return res.status(200).json(inventory); 27 | } catch (err) { 28 | next(err); 29 | } 30 | }; 31 | 32 | export default getInventoryDetails; 33 | -------------------------------------------------------------------------------- /services/inventory/src/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export { default as createInventory } from './createInventory'; 2 | export { default as updateInventory } from './updateInventory'; 3 | export { default as getInventoryById } from './getInventoryById'; 4 | export { default as getInventoryDetails } from './getInventoryDetails'; 5 | -------------------------------------------------------------------------------- /services/inventory/src/controllers/updateInventory.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import prisma from '@/prisma'; 3 | import { InventoryUpdateDTOSchema } from '@/schemas'; 4 | 5 | const updateInventory = async ( 6 | req: Request, 7 | res: Response, 8 | next: NextFunction 9 | ) => { 10 | try { 11 | // check if the inventory exists 12 | const { id } = req.params; 13 | const inventory = await prisma.inventory.findUnique({ 14 | where: { id }, 15 | }); 16 | 17 | if (!inventory) { 18 | return res.status(404).json({ message: 'Inventory not found' }); 19 | } 20 | 21 | // update the inventory 22 | const parsedBody = InventoryUpdateDTOSchema.safeParse(req.body); 23 | if (!parsedBody.success) { 24 | return res.status(400).json(parsedBody.error.errors); 25 | } 26 | 27 | // find the last history 28 | const lastHistory = await prisma.history.findFirst({ 29 | where: { inventoryId: id }, 30 | orderBy: { createdAt: 'desc' }, 31 | }); 32 | 33 | // calculate the new quantity 34 | let newQuantity = inventory.quantity; 35 | if (parsedBody.data.actionType === 'IN') { 36 | newQuantity += parsedBody.data.quantity; 37 | } else if (parsedBody.data.actionType === 'OUT') { 38 | newQuantity -= parsedBody.data.quantity; 39 | } else { 40 | return res.status(400).json({ message: 'Invalid action type' }); 41 | } 42 | 43 | // update the inventory 44 | const updatedInventory = await prisma.inventory.update({ 45 | where: { id }, 46 | data: { 47 | quantity: newQuantity, 48 | histories: { 49 | create: { 50 | actionType: parsedBody.data.actionType, 51 | quantityChanged: parsedBody.data.quantity, 52 | lastQuantity: lastHistory?.newQuantity || 0, 53 | newQuantity, 54 | }, 55 | }, 56 | }, 57 | select: { 58 | id: true, 59 | quantity: true, 60 | }, 61 | }); 62 | 63 | return res.status(200).json(updatedInventory); 64 | } catch (error) { 65 | next(error); 66 | } 67 | }; 68 | 69 | export default updateInventory; 70 | -------------------------------------------------------------------------------- /services/inventory/src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import dotenv from 'dotenv'; 3 | import cors from 'cors'; 4 | import morgan from 'morgan'; 5 | 6 | import { 7 | createInventory, 8 | getInventoryById, 9 | getInventoryDetails, 10 | updateInventory, 11 | } from './controllers'; 12 | 13 | dotenv.config(); 14 | 15 | const app = express(); 16 | app.use(express.json()); 17 | app.use(cors()); 18 | app.use(morgan('dev')); 19 | 20 | app.get('/health', (_req, res) => { 21 | res.status(200).json({ status: 'UP' }); 22 | }); 23 | 24 | // app.use((req, res, next) => { 25 | // const allowedOrigins = ['http://localhost:8081', 'http://127.0.0.1:8081']; 26 | // const origin = req.headers.origin || ''; 27 | 28 | // if (allowedOrigins.includes(origin)) { 29 | // res.setHeader('Access-Control-Allow-Origin', origin); 30 | // next(); 31 | // } else { 32 | // res.status(403).json({ message: 'Forbidden' }); 33 | // } 34 | // }); 35 | 36 | // routes 37 | app.get('/inventories/:id/details', getInventoryDetails); 38 | app.get('/inventories/:id', getInventoryById); 39 | app.put('/inventories/:id', updateInventory); 40 | app.post('/inventories', createInventory); 41 | 42 | // 404 handler 43 | app.use((_req, res) => { 44 | res.status(404).json({ message: 'Not found' }); 45 | }); 46 | 47 | // Error handler 48 | app.use((err, _req, res, _next) => { 49 | console.error(err.stack); 50 | res.status(500).json({ message: 'Internal server error' }); 51 | }); 52 | 53 | const port = process.env.PORT || 4002; 54 | const serviceName = process.env.SERVICE_NAME || 'Inventory-Service'; 55 | 56 | app.listen(port, () => { 57 | console.log(`${serviceName} is running on port ${port}`); 58 | }); 59 | -------------------------------------------------------------------------------- /services/inventory/src/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | const prisma = new PrismaClient(); 3 | export default prisma; 4 | -------------------------------------------------------------------------------- /services/inventory/src/schemas.ts: -------------------------------------------------------------------------------- 1 | import { ActionType } from '@prisma/client'; 2 | import { z } from 'zod'; 3 | 4 | export const InventoryCreateDTOSchema = z.object({ 5 | productId: z.string(), 6 | sku: z.string(), 7 | quantity: z.number().int().optional().default(0), 8 | }); 9 | 10 | export const InventoryUpdateDTOSchema = z.object({ 11 | quantity: z.number().int(), 12 | actionType: z.nativeEnum(ActionType), 13 | }); 14 | -------------------------------------------------------------------------------- /services/inventory/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "lib": ["ES6"], 6 | "allowJs": true, 7 | "module": "CommonJS", 8 | "rootDir": ".", 9 | "outDir": "./dist", 10 | "esModuleInterop": true, 11 | "strict": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "allowSyntheticDefaultImports": true, 17 | "typeRoots": ["./src/types", "./node_modules/@types"], 18 | "sourceMap": true, 19 | "types": ["node", "express"], 20 | "noImplicitAny": false, 21 | "baseUrl": "./src", 22 | "paths": { 23 | "@/*": ["*"] 24 | } 25 | }, 26 | "include": ["src/**/*"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /services/order/.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://postgres:postgres@localhost:5433/order_db?schema=public" -------------------------------------------------------------------------------- /services/order/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Keep environment variables out of version control 3 | .env 4 | -------------------------------------------------------------------------------- /services/order/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "order", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "ts-node-dev -r tsconfig-paths/register ./src/index.ts", 8 | "build": "tsc && tsc-alias", 9 | "migrate:dev": "prisma migrate dev", 10 | "migrate:prod": "prisma migrate deploy" 11 | }, 12 | "dependencies": { 13 | "@prisma/client": "^5.10.1", 14 | "@types/amqplib": "^0.10.5", 15 | "amqplib": "^0.10.3", 16 | "axios": "^1.6.7", 17 | "cors": "^2.8.5", 18 | "dotenv": "^16.4.5", 19 | "express": "^4.18.2", 20 | "morgan": "^1.10.0", 21 | "zod": "^3.22.4" 22 | }, 23 | "devDependencies": { 24 | "@types/cors": "^2.8.17", 25 | "@types/express": "^4.17.21", 26 | "@types/node": "^20.11.19", 27 | "prisma": "^5.10.1", 28 | "ts-node-dev": "^2.0.0", 29 | "tsc": "^2.0.4", 30 | "tsc-alias": "^1.8.8", 31 | "tsconfig-paths": "^4.2.0", 32 | "typescript": "^5.3.3" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /services/order/prisma/migrations/20240228074156_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "OrderStatus" AS ENUM ('PENDING', 'CONFIRMED', 'SHIPPED', 'DELIVERED', 'CANCELLED'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "Order" ( 6 | "id" TEXT NOT NULL, 7 | "userId" TEXT NOT NULL, 8 | "userName" TEXT NOT NULL, 9 | "userEmail" TEXT NOT NULL, 10 | "subtotal" DOUBLE PRECISION NOT NULL DEFAULT 0, 11 | "tax" DOUBLE PRECISION NOT NULL DEFAULT 0, 12 | "grandTotal" DOUBLE PRECISION NOT NULL DEFAULT 0, 13 | "status" "OrderStatus" NOT NULL DEFAULT 'PENDING', 14 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 15 | "updatedAt" TIMESTAMP(3) NOT NULL, 16 | 17 | CONSTRAINT "Order_pkey" PRIMARY KEY ("id") 18 | ); 19 | 20 | -- CreateTable 21 | CREATE TABLE "OrderItem" ( 22 | "id" TEXT NOT NULL, 23 | "orderId" TEXT NOT NULL, 24 | "productId" TEXT NOT NULL, 25 | "productName" TEXT NOT NULL, 26 | "sku" TEXT NOT NULL, 27 | "price" DOUBLE PRECISION NOT NULL, 28 | "quantity" INTEGER NOT NULL, 29 | "total" DOUBLE PRECISION NOT NULL, 30 | 31 | CONSTRAINT "OrderItem_pkey" PRIMARY KEY ("id") 32 | ); 33 | 34 | -- AddForeignKey 35 | ALTER TABLE "OrderItem" ADD CONSTRAINT "OrderItem_orderId_fkey" FOREIGN KEY ("orderId") REFERENCES "Order"("id") ON DELETE CASCADE ON UPDATE CASCADE; 36 | -------------------------------------------------------------------------------- /services/order/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /services/order/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | } 10 | 11 | datasource db { 12 | provider = "postgresql" 13 | url = env("DATABASE_URL") 14 | } 15 | 16 | enum OrderStatus { 17 | PENDING 18 | CONFIRMED 19 | SHIPPED 20 | DELIVERED 21 | CANCELLED 22 | } 23 | 24 | model Order { 25 | id String @id @default(cuid()) 26 | userId String 27 | userName String 28 | userEmail String 29 | subtotal Float @default(0) 30 | tax Float @default(0) 31 | grandTotal Float @default(0) 32 | status OrderStatus @default(PENDING) 33 | createdAt DateTime @default(now()) 34 | updatedAt DateTime @updatedAt 35 | orderItems OrderItem[] 36 | } 37 | 38 | model OrderItem { 39 | id String @id @default(cuid()) 40 | orderId String 41 | order Order @relation(fields: [orderId], references: [id], onDelete: Cascade) 42 | productId String 43 | productName String 44 | sku String 45 | price Float 46 | quantity Int 47 | total Float 48 | } 49 | -------------------------------------------------------------------------------- /services/order/src/config.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | dotenv.config({ 3 | path: '.env', 4 | }); 5 | 6 | export const CART_SERVICE = 7 | process.env.CART_SERVICE_URL || 'http://localhost:4006'; 8 | export const EMAIL_SERVICE = 9 | process.env.EMAIL_SERVICE_URL || 'http://localhost:4005'; 10 | export const PRODUCT_SERVICE = 11 | process.env.PRODUCT_SERVICE_URL || 'http://localhost:4001'; 12 | 13 | export const QUEUE_URL = process.env.QUEUE_URL || 'amqp://localhost'; 14 | -------------------------------------------------------------------------------- /services/order/src/controllers/checkout.ts: -------------------------------------------------------------------------------- 1 | import { CART_SERVICE, EMAIL_SERVICE, PRODUCT_SERVICE } from '@/config'; 2 | import prisma from '@/prisma'; 3 | import sendToQueue from '@/queue'; 4 | import { OrderSchema, CartItemSchema } from '@/schemas'; 5 | import axios from 'axios'; 6 | import { Request, Response, NextFunction } from 'express'; 7 | import { z } from 'zod'; 8 | 9 | const checkout = async (req: Request, res: Response, next: NextFunction) => { 10 | try { 11 | // validate request 12 | const parsedBody = OrderSchema.safeParse(req.body); 13 | if (!parsedBody.success) { 14 | return res.status(400).json({ errors: parsedBody.error.errors }); 15 | } 16 | 17 | // get cart details 18 | const { data: cartData } = await axios.get(`${CART_SERVICE}/cart/me`, { 19 | headers: { 20 | 'x-cart-session-id': parsedBody.data.cartSessionId, 21 | }, 22 | }); 23 | const cartItems = z.array(CartItemSchema).safeParse(cartData.items); 24 | if (!cartItems.success) { 25 | return res.status(400).json({ errors: cartItems.error.errors }); 26 | } 27 | 28 | if (cartItems.data.length === 0) { 29 | return res.status(400).json({ message: 'Cart is empty' }); 30 | } 31 | 32 | // get product details from cart items 33 | const productDetails = await Promise.all( 34 | cartItems.data.map(async (item) => { 35 | const { data: product } = await axios.get( 36 | `${PRODUCT_SERVICE}/products/${item.productId}` 37 | ); 38 | return { 39 | productId: product.id as string, 40 | productName: product.name as string, 41 | sku: product.sku as string, 42 | price: product.price as number, 43 | quantity: item.quantity, 44 | total: product.price * item.quantity, 45 | }; 46 | }) 47 | ); 48 | 49 | const subtotal = productDetails.reduce((acc, item) => acc + item.total, 0); 50 | 51 | // TODO: will handle tax calculation later 52 | const tax = 0; 53 | const grandTotal = subtotal + tax; 54 | 55 | // create order 56 | const order = await prisma.order.create({ 57 | data: { 58 | userId: parsedBody.data.userId, 59 | userName: parsedBody.data.userName, 60 | userEmail: parsedBody.data.userEmail, 61 | subtotal, 62 | tax, 63 | grandTotal, 64 | orderItems: { 65 | create: productDetails.map((item) => ({ 66 | ...item, 67 | })), 68 | }, 69 | }, 70 | }); 71 | 72 | console.log('Order created: ', order.id); 73 | 74 | // clear cart 75 | // await axios.get(`${CART_SERVICE}/cart/clear`, { 76 | // headers: { 77 | // 'x-cart-session-id': parsedBody.data.cartSessionId, 78 | // }, 79 | // }); 80 | 81 | // send email 82 | // await axios.post(`${EMAIL_SERVICE}/emails/send`, { 83 | // recipient: parsedBody.data.userEmail, 84 | // subject: 'Order Confirmation', 85 | // body: `Thank you for your order. Your order id is ${order.id}. Your order total is $${grandTotal}`, 86 | // source: 'Checkout', 87 | // }); 88 | 89 | // send to queue 90 | sendToQueue('send-email', JSON.stringify(order)); 91 | sendToQueue( 92 | 'clear-cart', 93 | JSON.stringify({ cartSessionId: parsedBody.data.cartSessionId }) 94 | ); 95 | 96 | return res.status(201).json(order); 97 | } catch (error) { 98 | next(error); 99 | } 100 | }; 101 | 102 | export default checkout; 103 | -------------------------------------------------------------------------------- /services/order/src/controllers/getOrderById.ts: -------------------------------------------------------------------------------- 1 | import prisma from '@/prisma'; 2 | import { Request, Response, NextFunction } from 'express'; 3 | 4 | const getOrderById = async ( 5 | req: Request, 6 | res: Response, 7 | next: NextFunction 8 | ) => { 9 | try { 10 | const order = await prisma.order.findUnique({ 11 | where: { id: req.params.id }, 12 | include: { 13 | orderItems: true, 14 | }, 15 | }); 16 | 17 | if (!order) { 18 | res.status(404).json({ message: 'Order not found' }); 19 | } 20 | 21 | res.status(200).json(order); 22 | } catch (error) { 23 | next(error); 24 | } 25 | }; 26 | 27 | export default getOrderById; 28 | -------------------------------------------------------------------------------- /services/order/src/controllers/getOrders.ts: -------------------------------------------------------------------------------- 1 | import prisma from '@/prisma'; 2 | import { Request, Response, NextFunction } from 'express'; 3 | 4 | const getOrders = async (req: Request, res: Response, next: NextFunction) => { 5 | try { 6 | const orders = await prisma.order.findMany({}); 7 | res.status(200).json(orders); 8 | } catch (error) { 9 | next(error); 10 | } 11 | }; 12 | export default getOrders; 13 | -------------------------------------------------------------------------------- /services/order/src/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export { default as checkout } from './checkout'; 2 | export { default as getOrderById } from './getOrderById'; 3 | export { default as getOrders } from './getOrders'; 4 | -------------------------------------------------------------------------------- /services/order/src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import dotenv from 'dotenv'; 3 | import cors from 'cors'; 4 | import morgan from 'morgan'; 5 | import { checkout, getOrderById, getOrders } from './controllers'; 6 | 7 | dotenv.config(); 8 | 9 | const app = express(); 10 | app.use(express.json()); 11 | app.use(cors()); 12 | app.use(morgan('dev')); 13 | 14 | app.get('/health', (_req, res) => { 15 | res.status(200).json({ status: 'UP' }); 16 | }); 17 | 18 | // app.use((req, res, next) => { 19 | // const allowedOrigins = ['http://localhost:8081', 'http://127.0.0.1:8081']; 20 | // const origin = req.headers.origin || ''; 21 | 22 | // if (allowedOrigins.includes(origin)) { 23 | // res.setHeader('Access-Control-Allow-Origin', origin); 24 | // next(); 25 | // } else { 26 | // res.status(403).json({ message: 'Forbidden' }); 27 | // } 28 | // }); 29 | 30 | // routes 31 | app.post('/orders/checkout', checkout); 32 | app.get('/orders/:id', getOrderById); 33 | app.get('/orders', getOrders); 34 | 35 | // 404 handler 36 | app.use((_req, res) => { 37 | res.status(404).json({ message: 'Not found' }); 38 | }); 39 | 40 | // Error handler 41 | app.use((err, _req, res, _next) => { 42 | console.error(err.stack); 43 | res.status(500).json({ message: 'Internal server error' }); 44 | }); 45 | 46 | const port = process.env.PORT || 4007; 47 | const serviceName = process.env.SERVICE_NAME || 'Order-Service'; 48 | 49 | app.listen(port, () => { 50 | console.log(`${serviceName} is running on port ${port}`); 51 | }); 52 | -------------------------------------------------------------------------------- /services/order/src/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | const prisma = new PrismaClient(); 3 | export default prisma; 4 | -------------------------------------------------------------------------------- /services/order/src/queue.ts: -------------------------------------------------------------------------------- 1 | import amqp from 'amqplib'; 2 | import { QUEUE_URL } from './config'; 3 | 4 | const sendToQueue = async (queue: string, message: string) => { 5 | const connection = await amqp.connect(QUEUE_URL); 6 | const channel = await connection.createChannel(); 7 | 8 | const exchange = 'order'; 9 | await channel.assertExchange(exchange, 'direct', { durable: true }); 10 | 11 | channel.publish(exchange, queue, Buffer.from(message)); 12 | console.log(`Sent ${message} to ${queue}`); 13 | 14 | setTimeout(() => { 15 | connection.close(); 16 | }, 500); 17 | }; 18 | 19 | export default sendToQueue; 20 | -------------------------------------------------------------------------------- /services/order/src/schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const OrderSchema = z.object({ 4 | userId: z.string(), 5 | userName: z.string(), 6 | userEmail: z.string(), 7 | cartSessionId: z.string(), 8 | }); 9 | 10 | export const CartItemSchema = z.object({ 11 | productId: z.string(), 12 | inventoryId: z.string(), 13 | quantity: z.number(), 14 | }); 15 | -------------------------------------------------------------------------------- /services/order/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "lib": ["ES6"], 6 | "allowJs": true, 7 | "module": "CommonJS", 8 | "rootDir": ".", 9 | "outDir": "./dist", 10 | "esModuleInterop": true, 11 | "strict": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "allowSyntheticDefaultImports": true, 17 | "typeRoots": ["./src/types", "./node_modules/@types"], 18 | "sourceMap": true, 19 | "types": ["node", "express"], 20 | "noImplicitAny": false, 21 | "baseUrl": "./src", 22 | "paths": { 23 | "@/*": ["*"] 24 | } 25 | }, 26 | "include": ["src/**/*"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /services/product/.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://postgres:postgres@localhost:5433/product_db?schema=public" -------------------------------------------------------------------------------- /services/product/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Keep environment variables out of version control 3 | .env 4 | -------------------------------------------------------------------------------- /services/product/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "product", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "ts-node-dev -r tsconfig-paths/register ./src/index.ts", 8 | "build": "tsc && tsc-alias", 9 | "migrate:dev": "prisma migrate dev", 10 | "migrate:prod": "prisma migrate deploy" 11 | }, 12 | "dependencies": { 13 | "@prisma/client": "^5.10.1", 14 | "axios": "^1.6.7", 15 | "cors": "^2.8.5", 16 | "dotenv": "^16.4.5", 17 | "express": "^4.18.2", 18 | "morgan": "^1.10.0", 19 | "zod": "^3.22.4" 20 | }, 21 | "devDependencies": { 22 | "@types/cors": "^2.8.17", 23 | "@types/express": "^4.17.21", 24 | "@types/node": "^20.11.19", 25 | "prisma": "^5.10.1", 26 | "ts-node-dev": "^2.0.0", 27 | "tsc": "^2.0.4", 28 | "tsc-alias": "^1.8.8", 29 | "tsconfig-paths": "^4.2.0", 30 | "typescript": "^5.3.3" 31 | } 32 | } -------------------------------------------------------------------------------- /services/product/prisma/migrations/20240228074230_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "Status" AS ENUM ('DRAFT', 'PUBLISHED', 'UNLISTED'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "Product" ( 6 | "id" TEXT NOT NULL, 7 | "sku" TEXT NOT NULL, 8 | "name" TEXT NOT NULL, 9 | "description" TEXT, 10 | "price" DOUBLE PRECISION NOT NULL DEFAULT 0, 11 | "inventoryId" TEXT, 12 | "status" "Status" NOT NULL DEFAULT 'DRAFT', 13 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 14 | "updatedAt" TIMESTAMP(3) NOT NULL, 15 | 16 | CONSTRAINT "Product_pkey" PRIMARY KEY ("id") 17 | ); 18 | 19 | -- CreateIndex 20 | CREATE UNIQUE INDEX "Product_sku_key" ON "Product"("sku"); 21 | -------------------------------------------------------------------------------- /services/product/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /services/product/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | } 10 | 11 | datasource db { 12 | provider = "postgresql" 13 | url = env("DATABASE_URL") 14 | } 15 | 16 | enum Status { 17 | DRAFT 18 | PUBLISHED 19 | UNLISTED 20 | } 21 | 22 | model Product { 23 | id String @id @default(cuid()) 24 | sku String @unique 25 | name String 26 | description String? 27 | price Float @default(0) 28 | inventoryId String? 29 | status Status @default(DRAFT) 30 | createdAt DateTime @default(now()) 31 | updatedAt DateTime @updatedAt 32 | } 33 | -------------------------------------------------------------------------------- /services/product/src/config.ts: -------------------------------------------------------------------------------- 1 | export const INVENTORY_URL = 2 | process.env.INVENTORY_SERVICE_URL || 'http://localhost:4002'; 3 | -------------------------------------------------------------------------------- /services/product/src/controllers/createProduct.ts: -------------------------------------------------------------------------------- 1 | import prisma from '@/prisma'; 2 | import axios from 'axios'; 3 | import { NextFunction, Request, Response } from 'express'; 4 | import { ProductCreateDTOSchema } from '@/schemas'; 5 | import { INVENTORY_URL } from '@/config'; 6 | 7 | const createProduct = async ( 8 | req: Request, 9 | res: Response, 10 | next: NextFunction 11 | ) => { 12 | try { 13 | console.log("💖 User Information", req.headers["x-user-id"], req.headers["x-user-email"]) 14 | // Validate request body 15 | const parsedBody = ProductCreateDTOSchema.safeParse(req.body); 16 | if (!parsedBody.success) { 17 | return res 18 | .status(400) 19 | .json({ 20 | message: 'Invalid request body', 21 | errors: parsedBody.error.errors, 22 | }); 23 | } 24 | 25 | // check if product with the same sku already exists 26 | const existingProduct = await prisma.product.findFirst({ 27 | where: { 28 | sku: parsedBody.data.sku, 29 | }, 30 | }); 31 | 32 | if (existingProduct) { 33 | return res 34 | .status(400) 35 | .json({ message: 'Product with the same SKU already exists' }); 36 | } 37 | 38 | // Create product 39 | const product = await prisma.product.create({ 40 | data: parsedBody.data, 41 | }); 42 | console.log('Product created successfully', product.id); 43 | 44 | // Create inventory record for the product 45 | const { data: inventory } = await axios.post( 46 | `${INVENTORY_URL}/inventories`, 47 | { 48 | productId: product.id, 49 | sku: product.sku, 50 | } 51 | ); 52 | console.log('Inventory created successfully', inventory.id); 53 | 54 | // update product and store inventory id 55 | await prisma.product.update({ 56 | where: { id: product.id }, 57 | data: { 58 | inventoryId: inventory.id, 59 | }, 60 | }); 61 | console.log('Product updated successfully with inventory id', inventory.id); 62 | 63 | res.status(201).json({ ...product, inventoryId: inventory.id }); 64 | } catch (err) { 65 | next(err); 66 | } 67 | }; 68 | 69 | export default createProduct; 70 | -------------------------------------------------------------------------------- /services/product/src/controllers/getProductDetails.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import prisma from '@/prisma'; 3 | import axios from 'axios'; 4 | import { INVENTORY_URL } from '@/config'; 5 | 6 | const getProductDetails = async ( 7 | req: Request, 8 | res: Response, 9 | next: NextFunction 10 | ) => { 11 | try { 12 | const { id } = req.params; 13 | const product = await prisma.product.findUnique({ 14 | where: { id }, 15 | }); 16 | 17 | if (!product) { 18 | return res.status(404).json({ message: 'Product not found' }); 19 | } 20 | 21 | if (product.inventoryId === null) { 22 | const { data: inventory } = await axios.post( 23 | `${INVENTORY_URL}/inventories`, 24 | { 25 | productId: product.id, 26 | sku: product.sku, 27 | } 28 | ); 29 | console.log('Inventory created successfully', inventory.id); 30 | 31 | await prisma.product.update({ 32 | where: { id: product.id }, 33 | data: { 34 | inventoryId: inventory.id, 35 | }, 36 | }); 37 | console.log( 38 | 'Product updated successfully with inventory id', 39 | inventory.id 40 | ); 41 | 42 | return res.status(200).json({ 43 | ...product, 44 | inventoryId: inventory.id, 45 | stock: inventory.quantity || 0, 46 | stockStatus: inventory.quantity > 0 ? 'In stock' : 'Out of stock', 47 | }); 48 | } 49 | 50 | // fetch inventory 51 | const { data: inventory } = await axios.get( 52 | `${INVENTORY_URL}/inventories/${product.inventoryId}` 53 | ); 54 | 55 | return res.status(200).json({ 56 | ...product, 57 | stock: inventory.quantity || 0, 58 | stockStatus: inventory.quantity > 0 ? 'In stock' : 'Out of stock', 59 | }); 60 | } catch (err) { 61 | next(err); 62 | } 63 | }; 64 | 65 | export default getProductDetails; 66 | -------------------------------------------------------------------------------- /services/product/src/controllers/getProducts.ts: -------------------------------------------------------------------------------- 1 | import prisma from '@/prisma'; 2 | import { Request, Response, NextFunction } from 'express'; 3 | 4 | const getProducts = async ( 5 | req: Request, 6 | res: Response, 7 | next: NextFunction 8 | ) => { 9 | try { 10 | console.log("😲Request Header",{userId: req.headers['x-user-id'], email:req.headers['x-user-email'] }) 11 | 12 | const products = await prisma.product.findMany({ 13 | select: { 14 | id: true, 15 | sku: true, 16 | name: true, 17 | price: true, 18 | inventoryId: true, 19 | }, 20 | }); 21 | 22 | // TODO: Implement pagination 23 | // TODO: Implement filtering 24 | 25 | res.json({ data: products }); 26 | } catch (err) { 27 | next(err); 28 | } 29 | }; 30 | 31 | export default getProducts; 32 | -------------------------------------------------------------------------------- /services/product/src/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export { default as createProduct } from './createProduct'; 2 | export { default as getProducts } from './getProducts'; 3 | export { default as getProductDetails } from './getProductDetails'; 4 | export { default as updateProduct } from './updateProduct'; 5 | -------------------------------------------------------------------------------- /services/product/src/controllers/updateProduct.ts: -------------------------------------------------------------------------------- 1 | import prisma from '@/prisma'; 2 | import { ProductUpdateDTOSchema } from '@/schemas'; 3 | import { Request, Response, NextFunction } from 'express'; 4 | 5 | const updateProduct = async ( 6 | req: Request, 7 | res: Response, 8 | next: NextFunction 9 | ) => { 10 | try { 11 | // verify if the request body is valid 12 | const parsedBody = ProductUpdateDTOSchema.safeParse(req.body); 13 | if (!parsedBody.success) { 14 | return res.status(400).json({ errors: parsedBody.error.errors }); 15 | } 16 | 17 | // check if the product exists 18 | const product = await prisma.product.findUnique({ 19 | where: { 20 | id: req.params.id, 21 | }, 22 | }); 23 | 24 | if (!product) { 25 | return res.status(404).json({ message: 'Product not found' }); 26 | } 27 | 28 | // update the product 29 | const updatedProduct = await prisma.product.update({ 30 | where: { 31 | id: req.params.id, 32 | }, 33 | data: parsedBody.data, 34 | }); 35 | 36 | res.status(200).json({ data: updatedProduct }); 37 | } catch (error) { 38 | next(error); 39 | } 40 | }; 41 | 42 | export default updateProduct; 43 | -------------------------------------------------------------------------------- /services/product/src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import dotenv from 'dotenv'; 3 | import cors from 'cors'; 4 | import morgan from 'morgan'; 5 | import { 6 | createProduct, 7 | getProductDetails, 8 | getProducts, 9 | updateProduct, 10 | } from './controllers'; 11 | 12 | dotenv.config(); 13 | 14 | const app = express(); 15 | app.use(express.json()); 16 | app.use(cors()); 17 | app.use(morgan('dev')); 18 | 19 | app.get('/health', (_req, res) => { 20 | res.status(200).json({ status: 'UP' }); 21 | }); 22 | 23 | // app.use((req, res, next) => { 24 | // const allowedOrigins = ['http://localhost:8081', 'http://127.0.0.1:8081']; 25 | // const origin = req.headers.origin || ''; 26 | 27 | // if (allowedOrigins.includes(origin)) { 28 | // res.setHeader('Access-Control-Allow-Origin', origin); 29 | // next(); 30 | // } else { 31 | // res.status(403).json({ message: 'Forbidden' }); 32 | // } 33 | // }); 34 | 35 | // routes 36 | app.get('/products/:id', getProductDetails); 37 | app.put('/products/:id', updateProduct); 38 | app.get('/products', getProducts); 39 | app.post('/products', createProduct); 40 | 41 | // 404 handler 42 | app.use((_req, res) => { 43 | res.status(404).json({ message: 'Not found' }); 44 | }); 45 | 46 | // Error handler 47 | app.use((err, _req, res, _next) => { 48 | console.error(err.stack); 49 | res.status(500).json({ message: 'Internal server error' }); 50 | }); 51 | 52 | const port = process.env.PORT || 4001; 53 | const serviceName = process.env.SERVICE_NAME || 'Product-Service'; 54 | 55 | app.listen(port, () => { 56 | console.log(`${serviceName} is running on port ${port}`); 57 | }); 58 | -------------------------------------------------------------------------------- /services/product/src/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | const prisma = new PrismaClient(); 3 | export default prisma; 4 | -------------------------------------------------------------------------------- /services/product/src/schemas.ts: -------------------------------------------------------------------------------- 1 | import { Status } from '@prisma/client'; 2 | import { z } from 'zod'; 3 | 4 | export const ProductCreateDTOSchema = z.object({ 5 | sku: z.string().min(3).max(10), 6 | name: z.string().min(3).max(255), 7 | description: z.string().max(1000).optional(), 8 | price: z.number().optional().default(0), 9 | status: z.nativeEnum(Status).optional().default(Status.DRAFT), 10 | }); 11 | 12 | export const ProductUpdateDTOSchema = ProductCreateDTOSchema.omit({ 13 | sku: true, 14 | }).partial(); 15 | -------------------------------------------------------------------------------- /services/product/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "lib": ["ES6"], 6 | "allowJs": true, 7 | "module": "CommonJS", 8 | "rootDir": ".", 9 | "outDir": "./dist", 10 | "esModuleInterop": true, 11 | "strict": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "allowSyntheticDefaultImports": true, 17 | "typeRoots": ["./src/types", "./node_modules/@types"], 18 | "sourceMap": true, 19 | "types": ["node", "express"], 20 | "noImplicitAny": false, 21 | "baseUrl": "./src", 22 | "paths": { 23 | "@/*": ["*"] 24 | } 25 | }, 26 | "include": ["src/**/*"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /services/user/.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://postgres:postgres@localhost:5433/user_db?schema=public" -------------------------------------------------------------------------------- /services/user/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # Keep environment variables out of version control 3 | .env 4 | -------------------------------------------------------------------------------- /services/user/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "product", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "dev": "ts-node-dev -r tsconfig-paths/register ./src/index.ts", 8 | "build": "tsc && tsc-alias", 9 | "migrate:dev": "prisma migrate dev", 10 | "migrate:prod": "prisma migrate deploy" 11 | }, 12 | "dependencies": { 13 | "@prisma/client": "^5.10.1", 14 | "cors": "^2.8.5", 15 | "dotenv": "^16.4.5", 16 | "express": "^4.18.2", 17 | "morgan": "^1.10.0", 18 | "zod": "^3.22.4" 19 | }, 20 | "devDependencies": { 21 | "@types/cors": "^2.8.17", 22 | "@types/express": "^4.17.21", 23 | "@types/node": "^20.11.19", 24 | "prisma": "^5.10.1", 25 | "ts-node-dev": "^2.0.0", 26 | "tsc": "^2.0.4", 27 | "tsc-alias": "^1.8.8", 28 | "tsconfig-paths": "^4.2.0", 29 | "typescript": "^5.3.3" 30 | } 31 | } -------------------------------------------------------------------------------- /services/user/prisma/migrations/20240228074306_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" TEXT NOT NULL, 4 | "authUserId" TEXT NOT NULL, 5 | "email" TEXT NOT NULL, 6 | "name" TEXT NOT NULL, 7 | "address" TEXT, 8 | "phone" TEXT, 9 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 10 | "updatedAt" TIMESTAMP(3) NOT NULL, 11 | 12 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 13 | ); 14 | 15 | -- CreateIndex 16 | CREATE UNIQUE INDEX "User_authUserId_key" ON "User"("authUserId"); 17 | 18 | -- CreateIndex 19 | CREATE INDEX "User_authUserId_idx" ON "User"("authUserId"); 20 | -------------------------------------------------------------------------------- /services/user/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /services/user/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | generator client { 8 | provider = "prisma-client-js" 9 | } 10 | 11 | datasource db { 12 | provider = "postgresql" 13 | url = env("DATABASE_URL") 14 | } 15 | 16 | model User { 17 | id String @id @default(cuid()) 18 | authUserId String @unique 19 | email String 20 | name String 21 | address String? 22 | phone String? 23 | createdAt DateTime @default(now()) 24 | updatedAt DateTime @updatedAt 25 | 26 | @@index([authUserId]) 27 | } 28 | -------------------------------------------------------------------------------- /services/user/src/config.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stack-Learner/practical-microservice-workshop/22fa111ba74c6a88b9676ce9605b2bc8f88e9b8c/services/user/src/config.ts -------------------------------------------------------------------------------- /services/user/src/controllers/createUser.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import { UserCreateSchema } from '@/schemas'; 3 | import prisma from '@/prisma'; 4 | 5 | const createUser = async (req: Request, res: Response, next: NextFunction) => { 6 | try { 7 | // Validate the request body 8 | const parsedBody = UserCreateSchema.safeParse(req.body); 9 | if (!parsedBody.success) { 10 | return res.status(400).json({ message: parsedBody.error.errors }); 11 | } 12 | 13 | // check if the authUserId already exists 14 | const existingUser = await prisma.user.findUnique({ 15 | where: { authUserId: parsedBody.data.authUserId }, 16 | }); 17 | if (existingUser) { 18 | return res.status(400).json({ message: 'User already exists' }); 19 | } 20 | 21 | // Create a new user 22 | const user = await prisma.user.create({ 23 | data: parsedBody.data, 24 | }); 25 | 26 | return res.status(201).json(user); 27 | } catch (error) { 28 | next(error); 29 | } 30 | }; 31 | 32 | export default createUser; 33 | -------------------------------------------------------------------------------- /services/user/src/controllers/getUserById.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import prisma from '@/prisma'; 3 | import { User } from '@prisma/client'; 4 | 5 | // /users/:id?field=id|authUserId 6 | const getUserById = async (req: Request, res: Response, next: NextFunction) => { 7 | try { 8 | const { id } = req.params; 9 | const field = req.query.field as string; 10 | let user: User | null = null; 11 | 12 | if (field === 'authUserId') { 13 | user = await prisma.user.findUnique({ where: { authUserId: id } }); 14 | } else { 15 | user = await prisma.user.findUnique({ where: { id } }); 16 | } 17 | 18 | if (!user) { 19 | return res.status(404).json({ message: 'User not found' }); 20 | } 21 | return res.json(user); 22 | } catch (error) { 23 | next(error); 24 | } 25 | }; 26 | 27 | export default getUserById; 28 | -------------------------------------------------------------------------------- /services/user/src/controllers/index.ts: -------------------------------------------------------------------------------- 1 | export { default as createUser } from './createUser'; 2 | export { default as getUserById } from './getUserById'; 3 | -------------------------------------------------------------------------------- /services/user/src/index.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import dotenv from 'dotenv'; 3 | import cors from 'cors'; 4 | import morgan from 'morgan'; 5 | import { createUser, getUserById } from './controllers'; 6 | 7 | dotenv.config(); 8 | 9 | const app = express(); 10 | app.use(express.json()); 11 | app.use(cors()); 12 | app.use(morgan('dev')); 13 | 14 | app.get('/health', (_req, res) => { 15 | res.status(200).json({ status: 'UP' }); 16 | }); 17 | 18 | // app.use((req, res, next) => { 19 | // const allowedOrigins = ['http://localhost:8081', 'http://127.0.0.1:8081']; 20 | // const origin = req.headers.origin || ''; 21 | 22 | // if (allowedOrigins.includes(origin)) { 23 | // res.setHeader('Access-Control-Allow-Origin', origin); 24 | // next(); 25 | // } else { 26 | // res.status(403).json({ message: 'Forbidden' }); 27 | // } 28 | // }); 29 | 30 | // routes 31 | app.get('/users/:id', getUserById); 32 | app.post('/users', createUser); 33 | 34 | // 404 handler 35 | app.use((_req, res) => { 36 | res.status(404).json({ message: 'Not found' }); 37 | }); 38 | 39 | // Error handler 40 | app.use((err, _req, res, _next) => { 41 | console.error(err.stack); 42 | res.status(500).json({ message: 'Internal server error' }); 43 | }); 44 | 45 | const port = process.env.PORT || 4000; 46 | const serviceName = process.env.SERVICE_NAME || 'User-Service'; 47 | 48 | app.listen(port, () => { 49 | console.log(`${serviceName} is running on port ${port}`); 50 | }); 51 | -------------------------------------------------------------------------------- /services/user/src/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | const prisma = new PrismaClient(); 3 | export default prisma; 4 | -------------------------------------------------------------------------------- /services/user/src/schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const UserCreateSchema = z.object({ 4 | authUserId: z.string(), 5 | name: z.string(), 6 | email: z.string().email(), 7 | address: z.string().optional(), 8 | phone: z.string().optional(), 9 | }); 10 | 11 | export const UserUpdateSchema = UserCreateSchema.omit({ 12 | authUserId: true, 13 | }).partial(); 14 | -------------------------------------------------------------------------------- /services/user/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "lib": ["ES6"], 6 | "allowJs": true, 7 | "module": "CommonJS", 8 | "rootDir": ".", 9 | "outDir": "./dist", 10 | "esModuleInterop": true, 11 | "strict": true, 12 | "skipLibCheck": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "allowSyntheticDefaultImports": true, 17 | "typeRoots": ["./src/types", "./node_modules/@types"], 18 | "sourceMap": true, 19 | "types": ["node", "express"], 20 | "noImplicitAny": false, 21 | "baseUrl": "./src", 22 | "paths": { 23 | "@/*": ["*"] 24 | } 25 | }, 26 | "include": ["src/**/*"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /services/user/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@cspotcode/source-map-support@^0.8.0": 6 | version "0.8.1" 7 | resolved "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" 8 | integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== 9 | dependencies: 10 | "@jridgewell/trace-mapping" "0.3.9" 11 | 12 | "@jridgewell/resolve-uri@^3.0.3": 13 | version "3.1.2" 14 | resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" 15 | integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== 16 | 17 | "@jridgewell/sourcemap-codec@^1.4.10": 18 | version "1.4.15" 19 | resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" 20 | integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== 21 | 22 | "@jridgewell/trace-mapping@0.3.9": 23 | version "0.3.9" 24 | resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" 25 | integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== 26 | dependencies: 27 | "@jridgewell/resolve-uri" "^3.0.3" 28 | "@jridgewell/sourcemap-codec" "^1.4.10" 29 | 30 | "@nodelib/fs.scandir@2.1.5": 31 | version "2.1.5" 32 | resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" 33 | integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== 34 | dependencies: 35 | "@nodelib/fs.stat" "2.0.5" 36 | run-parallel "^1.1.9" 37 | 38 | "@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": 39 | version "2.0.5" 40 | resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" 41 | integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== 42 | 43 | "@nodelib/fs.walk@^1.2.3": 44 | version "1.2.8" 45 | resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" 46 | integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== 47 | dependencies: 48 | "@nodelib/fs.scandir" "2.1.5" 49 | fastq "^1.6.0" 50 | 51 | "@prisma/client@^5.10.1": 52 | version "5.10.2" 53 | resolved "https://registry.npmjs.org/@prisma/client/-/client-5.10.2.tgz#e087b40a4de8e3171eb9cbf0a873465cd2068e17" 54 | integrity sha512-ef49hzB2yJZCvM5gFHMxSFL9KYrIP9udpT5rYo0CsHD4P9IKj473MbhU1gjKKftiwWBTIyrt9jukprzZXazyag== 55 | 56 | "@prisma/debug@5.10.2": 57 | version "5.10.2" 58 | resolved "https://registry.npmjs.org/@prisma/debug/-/debug-5.10.2.tgz#74be81d8969978f4d53c1b4e76d61f04bfbc3951" 59 | integrity sha512-bkBOmH9dpEBbMKFJj8V+Zp8IZHIBjy3fSyhLhxj4FmKGb/UBSt9doyfA6k1UeUREsMJft7xgPYBbHSOYBr8XCA== 60 | 61 | "@prisma/engines-version@5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9": 62 | version "5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9" 63 | resolved "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9.tgz#1502335d4d72d2014cb25b8ad8a740a3a13400ea" 64 | integrity sha512-uCy/++3Jx/O3ufM+qv2H1L4tOemTNqcP/gyEVOlZqTpBvYJUe0tWtW0y3o2Ueq04mll4aM5X3f6ugQftOSLdFQ== 65 | 66 | "@prisma/engines@5.10.2": 67 | version "5.10.2" 68 | resolved "https://registry.npmjs.org/@prisma/engines/-/engines-5.10.2.tgz#a4851d90f76ad6d22e783d5fd2e2e8c0640f1e81" 69 | integrity sha512-HkSJvix6PW8YqEEt3zHfCYYJY69CXsNdhU+wna+4Y7EZ+AwzeupMnUThmvaDA7uqswiHkgm5/SZ6/4CStjaGmw== 70 | dependencies: 71 | "@prisma/debug" "5.10.2" 72 | "@prisma/engines-version" "5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9" 73 | "@prisma/fetch-engine" "5.10.2" 74 | "@prisma/get-platform" "5.10.2" 75 | 76 | "@prisma/fetch-engine@5.10.2": 77 | version "5.10.2" 78 | resolved "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.10.2.tgz#a061f6727d395c7033b55f9c6e92f8741a70d5c5" 79 | integrity sha512-dSmXcqSt6DpTmMaLQ9K8ZKzVAMH3qwGCmYEZr/uVnzVhxRJ1EbT/w2MMwIdBNq1zT69Rvh0h75WMIi0mrIw7Hg== 80 | dependencies: 81 | "@prisma/debug" "5.10.2" 82 | "@prisma/engines-version" "5.10.0-34.5a9203d0590c951969e85a7d07215503f4672eb9" 83 | "@prisma/get-platform" "5.10.2" 84 | 85 | "@prisma/get-platform@5.10.2": 86 | version "5.10.2" 87 | resolved "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.10.2.tgz#7af97b1d82e5574a474e3fbf6eaf04f4156bc535" 88 | integrity sha512-nqXP6vHiY2PIsebBAuDeWiUYg8h8mfjBckHh6Jezuwej0QJNnjDiOq30uesmg+JXxGk99nqyG3B7wpcOODzXvg== 89 | dependencies: 90 | "@prisma/debug" "5.10.2" 91 | 92 | "@tsconfig/node10@^1.0.7": 93 | version "1.0.9" 94 | resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" 95 | integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== 96 | 97 | "@tsconfig/node12@^1.0.7": 98 | version "1.0.11" 99 | resolved "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" 100 | integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== 101 | 102 | "@tsconfig/node14@^1.0.0": 103 | version "1.0.3" 104 | resolved "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" 105 | integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== 106 | 107 | "@tsconfig/node16@^1.0.2": 108 | version "1.0.4" 109 | resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" 110 | integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== 111 | 112 | "@types/body-parser@*": 113 | version "1.19.5" 114 | resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" 115 | integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== 116 | dependencies: 117 | "@types/connect" "*" 118 | "@types/node" "*" 119 | 120 | "@types/connect@*": 121 | version "3.4.38" 122 | resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" 123 | integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== 124 | dependencies: 125 | "@types/node" "*" 126 | 127 | "@types/cors@^2.8.17": 128 | version "2.8.17" 129 | resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" 130 | integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== 131 | dependencies: 132 | "@types/node" "*" 133 | 134 | "@types/express-serve-static-core@^4.17.33": 135 | version "4.17.43" 136 | resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz#10d8444be560cb789c4735aea5eac6e5af45df54" 137 | integrity sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg== 138 | dependencies: 139 | "@types/node" "*" 140 | "@types/qs" "*" 141 | "@types/range-parser" "*" 142 | "@types/send" "*" 143 | 144 | "@types/express@^4.17.21": 145 | version "4.17.21" 146 | resolved "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" 147 | integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== 148 | dependencies: 149 | "@types/body-parser" "*" 150 | "@types/express-serve-static-core" "^4.17.33" 151 | "@types/qs" "*" 152 | "@types/serve-static" "*" 153 | 154 | "@types/http-errors@*": 155 | version "2.0.4" 156 | resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" 157 | integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== 158 | 159 | "@types/mime@*": 160 | version "3.0.4" 161 | resolved "https://registry.npmjs.org/@types/mime/-/mime-3.0.4.tgz#2198ac274de6017b44d941e00261d5bc6a0e0a45" 162 | integrity sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw== 163 | 164 | "@types/mime@^1": 165 | version "1.3.5" 166 | resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" 167 | integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== 168 | 169 | "@types/node@*", "@types/node@^20.11.19": 170 | version "20.11.19" 171 | resolved "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz#b466de054e9cb5b3831bee38938de64ac7f81195" 172 | integrity sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ== 173 | dependencies: 174 | undici-types "~5.26.4" 175 | 176 | "@types/qs@*": 177 | version "6.9.11" 178 | resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz#208d8a30bc507bd82e03ada29e4732ea46a6bbda" 179 | integrity sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ== 180 | 181 | "@types/range-parser@*": 182 | version "1.2.7" 183 | resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" 184 | integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== 185 | 186 | "@types/send@*": 187 | version "0.17.4" 188 | resolved "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" 189 | integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== 190 | dependencies: 191 | "@types/mime" "^1" 192 | "@types/node" "*" 193 | 194 | "@types/serve-static@*": 195 | version "1.15.5" 196 | resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz#15e67500ec40789a1e8c9defc2d32a896f05b033" 197 | integrity sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ== 198 | dependencies: 199 | "@types/http-errors" "*" 200 | "@types/mime" "*" 201 | "@types/node" "*" 202 | 203 | "@types/strip-bom@^3.0.0": 204 | version "3.0.0" 205 | resolved "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2" 206 | integrity sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ== 207 | 208 | "@types/strip-json-comments@0.0.30": 209 | version "0.0.30" 210 | resolved "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" 211 | integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== 212 | 213 | accepts@~1.3.8: 214 | version "1.3.8" 215 | resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" 216 | integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== 217 | dependencies: 218 | mime-types "~2.1.34" 219 | negotiator "0.6.3" 220 | 221 | acorn-walk@^8.1.1: 222 | version "8.3.2" 223 | resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" 224 | integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== 225 | 226 | acorn@^8.4.1: 227 | version "8.11.3" 228 | resolved "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" 229 | integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== 230 | 231 | anymatch@~3.1.2: 232 | version "3.1.3" 233 | resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" 234 | integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== 235 | dependencies: 236 | normalize-path "^3.0.0" 237 | picomatch "^2.0.4" 238 | 239 | arg@^4.1.0: 240 | version "4.1.3" 241 | resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" 242 | integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== 243 | 244 | array-flatten@1.1.1: 245 | version "1.1.1" 246 | resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 247 | integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== 248 | 249 | array-union@^2.1.0: 250 | version "2.1.0" 251 | resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" 252 | integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== 253 | 254 | balanced-match@^1.0.0: 255 | version "1.0.2" 256 | resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 257 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 258 | 259 | basic-auth@~2.0.1: 260 | version "2.0.1" 261 | resolved "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" 262 | integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== 263 | dependencies: 264 | safe-buffer "5.1.2" 265 | 266 | binary-extensions@^2.0.0: 267 | version "2.2.0" 268 | resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" 269 | integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== 270 | 271 | body-parser@1.20.1: 272 | version "1.20.1" 273 | resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" 274 | integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== 275 | dependencies: 276 | bytes "3.1.2" 277 | content-type "~1.0.4" 278 | debug "2.6.9" 279 | depd "2.0.0" 280 | destroy "1.2.0" 281 | http-errors "2.0.0" 282 | iconv-lite "0.4.24" 283 | on-finished "2.4.1" 284 | qs "6.11.0" 285 | raw-body "2.5.1" 286 | type-is "~1.6.18" 287 | unpipe "1.0.0" 288 | 289 | brace-expansion@^1.1.7: 290 | version "1.1.11" 291 | resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 292 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 293 | dependencies: 294 | balanced-match "^1.0.0" 295 | concat-map "0.0.1" 296 | 297 | braces@^3.0.2, braces@~3.0.2: 298 | version "3.0.2" 299 | resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" 300 | integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== 301 | dependencies: 302 | fill-range "^7.0.1" 303 | 304 | buffer-from@^1.0.0: 305 | version "1.1.2" 306 | resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" 307 | integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== 308 | 309 | bytes@3.1.2: 310 | version "3.1.2" 311 | resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" 312 | integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== 313 | 314 | call-bind@^1.0.6: 315 | version "1.0.7" 316 | resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" 317 | integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== 318 | dependencies: 319 | es-define-property "^1.0.0" 320 | es-errors "^1.3.0" 321 | function-bind "^1.1.2" 322 | get-intrinsic "^1.2.4" 323 | set-function-length "^1.2.1" 324 | 325 | chokidar@^3.5.1, chokidar@^3.5.3: 326 | version "3.6.0" 327 | resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" 328 | integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== 329 | dependencies: 330 | anymatch "~3.1.2" 331 | braces "~3.0.2" 332 | glob-parent "~5.1.2" 333 | is-binary-path "~2.1.0" 334 | is-glob "~4.0.1" 335 | normalize-path "~3.0.0" 336 | readdirp "~3.6.0" 337 | optionalDependencies: 338 | fsevents "~2.3.2" 339 | 340 | commander@^9.0.0: 341 | version "9.5.0" 342 | resolved "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" 343 | integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== 344 | 345 | concat-map@0.0.1: 346 | version "0.0.1" 347 | resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 348 | integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== 349 | 350 | content-disposition@0.5.4: 351 | version "0.5.4" 352 | resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" 353 | integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== 354 | dependencies: 355 | safe-buffer "5.2.1" 356 | 357 | content-type@~1.0.4: 358 | version "1.0.5" 359 | resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" 360 | integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== 361 | 362 | cookie-signature@1.0.6: 363 | version "1.0.6" 364 | resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 365 | integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== 366 | 367 | cookie@0.5.0: 368 | version "0.5.0" 369 | resolved "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" 370 | integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== 371 | 372 | cors@^2.8.5: 373 | version "2.8.5" 374 | resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" 375 | integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== 376 | dependencies: 377 | object-assign "^4" 378 | vary "^1" 379 | 380 | create-require@^1.1.0: 381 | version "1.1.1" 382 | resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" 383 | integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== 384 | 385 | debug@2.6.9: 386 | version "2.6.9" 387 | resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 388 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== 389 | dependencies: 390 | ms "2.0.0" 391 | 392 | define-data-property@^1.1.2: 393 | version "1.1.4" 394 | resolved "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" 395 | integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== 396 | dependencies: 397 | es-define-property "^1.0.0" 398 | es-errors "^1.3.0" 399 | gopd "^1.0.1" 400 | 401 | depd@2.0.0, depd@~2.0.0: 402 | version "2.0.0" 403 | resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" 404 | integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== 405 | 406 | destroy@1.2.0: 407 | version "1.2.0" 408 | resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" 409 | integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== 410 | 411 | diff@^4.0.1: 412 | version "4.0.2" 413 | resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" 414 | integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== 415 | 416 | dir-glob@^3.0.1: 417 | version "3.0.1" 418 | resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" 419 | integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== 420 | dependencies: 421 | path-type "^4.0.0" 422 | 423 | dotenv@^16.4.5: 424 | version "16.4.5" 425 | resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" 426 | integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== 427 | 428 | dynamic-dedupe@^0.3.0: 429 | version "0.3.0" 430 | resolved "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz#06e44c223f5e4e94d78ef9db23a6515ce2f962a1" 431 | integrity sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ== 432 | dependencies: 433 | xtend "^4.0.0" 434 | 435 | ee-first@1.1.1: 436 | version "1.1.1" 437 | resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 438 | integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== 439 | 440 | encodeurl@~1.0.2: 441 | version "1.0.2" 442 | resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 443 | integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== 444 | 445 | es-define-property@^1.0.0: 446 | version "1.0.0" 447 | resolved "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" 448 | integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== 449 | dependencies: 450 | get-intrinsic "^1.2.4" 451 | 452 | es-errors@^1.3.0: 453 | version "1.3.0" 454 | resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" 455 | integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== 456 | 457 | escape-html@~1.0.3: 458 | version "1.0.3" 459 | resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 460 | integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== 461 | 462 | etag@~1.8.1: 463 | version "1.8.1" 464 | resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 465 | integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== 466 | 467 | express@^4.18.2: 468 | version "4.18.2" 469 | resolved "https://registry.npmjs.org/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" 470 | integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== 471 | dependencies: 472 | accepts "~1.3.8" 473 | array-flatten "1.1.1" 474 | body-parser "1.20.1" 475 | content-disposition "0.5.4" 476 | content-type "~1.0.4" 477 | cookie "0.5.0" 478 | cookie-signature "1.0.6" 479 | debug "2.6.9" 480 | depd "2.0.0" 481 | encodeurl "~1.0.2" 482 | escape-html "~1.0.3" 483 | etag "~1.8.1" 484 | finalhandler "1.2.0" 485 | fresh "0.5.2" 486 | http-errors "2.0.0" 487 | merge-descriptors "1.0.1" 488 | methods "~1.1.2" 489 | on-finished "2.4.1" 490 | parseurl "~1.3.3" 491 | path-to-regexp "0.1.7" 492 | proxy-addr "~2.0.7" 493 | qs "6.11.0" 494 | range-parser "~1.2.1" 495 | safe-buffer "5.2.1" 496 | send "0.18.0" 497 | serve-static "1.15.0" 498 | setprototypeof "1.2.0" 499 | statuses "2.0.1" 500 | type-is "~1.6.18" 501 | utils-merge "1.0.1" 502 | vary "~1.1.2" 503 | 504 | fast-glob@^3.2.9: 505 | version "3.3.2" 506 | resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" 507 | integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== 508 | dependencies: 509 | "@nodelib/fs.stat" "^2.0.2" 510 | "@nodelib/fs.walk" "^1.2.3" 511 | glob-parent "^5.1.2" 512 | merge2 "^1.3.0" 513 | micromatch "^4.0.4" 514 | 515 | fastq@^1.6.0: 516 | version "1.17.1" 517 | resolved "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" 518 | integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== 519 | dependencies: 520 | reusify "^1.0.4" 521 | 522 | fill-range@^7.0.1: 523 | version "7.0.1" 524 | resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" 525 | integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== 526 | dependencies: 527 | to-regex-range "^5.0.1" 528 | 529 | finalhandler@1.2.0: 530 | version "1.2.0" 531 | resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" 532 | integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== 533 | dependencies: 534 | debug "2.6.9" 535 | encodeurl "~1.0.2" 536 | escape-html "~1.0.3" 537 | on-finished "2.4.1" 538 | parseurl "~1.3.3" 539 | statuses "2.0.1" 540 | unpipe "~1.0.0" 541 | 542 | forwarded@0.2.0: 543 | version "0.2.0" 544 | resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" 545 | integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== 546 | 547 | fresh@0.5.2: 548 | version "0.5.2" 549 | resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 550 | integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== 551 | 552 | fs.realpath@^1.0.0: 553 | version "1.0.0" 554 | resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 555 | integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== 556 | 557 | fsevents@~2.3.2: 558 | version "2.3.3" 559 | resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" 560 | integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== 561 | 562 | function-bind@^1.1.2: 563 | version "1.1.2" 564 | resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" 565 | integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== 566 | 567 | get-intrinsic@^1.1.3, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: 568 | version "1.2.4" 569 | resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" 570 | integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== 571 | dependencies: 572 | es-errors "^1.3.0" 573 | function-bind "^1.1.2" 574 | has-proto "^1.0.1" 575 | has-symbols "^1.0.3" 576 | hasown "^2.0.0" 577 | 578 | glob-parent@^5.1.2, glob-parent@~5.1.2: 579 | version "5.1.2" 580 | resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" 581 | integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== 582 | dependencies: 583 | is-glob "^4.0.1" 584 | 585 | glob@^7.1.3: 586 | version "7.2.3" 587 | resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" 588 | integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== 589 | dependencies: 590 | fs.realpath "^1.0.0" 591 | inflight "^1.0.4" 592 | inherits "2" 593 | minimatch "^3.1.1" 594 | once "^1.3.0" 595 | path-is-absolute "^1.0.0" 596 | 597 | globby@^11.0.4: 598 | version "11.1.0" 599 | resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" 600 | integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== 601 | dependencies: 602 | array-union "^2.1.0" 603 | dir-glob "^3.0.1" 604 | fast-glob "^3.2.9" 605 | ignore "^5.2.0" 606 | merge2 "^1.4.1" 607 | slash "^3.0.0" 608 | 609 | gopd@^1.0.1: 610 | version "1.0.1" 611 | resolved "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" 612 | integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== 613 | dependencies: 614 | get-intrinsic "^1.1.3" 615 | 616 | has-property-descriptors@^1.0.1: 617 | version "1.0.2" 618 | resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" 619 | integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== 620 | dependencies: 621 | es-define-property "^1.0.0" 622 | 623 | has-proto@^1.0.1: 624 | version "1.0.3" 625 | resolved "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" 626 | integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== 627 | 628 | has-symbols@^1.0.3: 629 | version "1.0.3" 630 | resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" 631 | integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== 632 | 633 | hasown@^2.0.0: 634 | version "2.0.1" 635 | resolved "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz#26f48f039de2c0f8d3356c223fb8d50253519faa" 636 | integrity sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA== 637 | dependencies: 638 | function-bind "^1.1.2" 639 | 640 | http-errors@2.0.0: 641 | version "2.0.0" 642 | resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" 643 | integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== 644 | dependencies: 645 | depd "2.0.0" 646 | inherits "2.0.4" 647 | setprototypeof "1.2.0" 648 | statuses "2.0.1" 649 | toidentifier "1.0.1" 650 | 651 | iconv-lite@0.4.24: 652 | version "0.4.24" 653 | resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 654 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 655 | dependencies: 656 | safer-buffer ">= 2.1.2 < 3" 657 | 658 | ignore@^5.2.0: 659 | version "5.3.1" 660 | resolved "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" 661 | integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== 662 | 663 | inflight@^1.0.4: 664 | version "1.0.6" 665 | resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 666 | integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== 667 | dependencies: 668 | once "^1.3.0" 669 | wrappy "1" 670 | 671 | inherits@2, inherits@2.0.4: 672 | version "2.0.4" 673 | resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 674 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 675 | 676 | ipaddr.js@1.9.1: 677 | version "1.9.1" 678 | resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" 679 | integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== 680 | 681 | is-binary-path@~2.1.0: 682 | version "2.1.0" 683 | resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" 684 | integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== 685 | dependencies: 686 | binary-extensions "^2.0.0" 687 | 688 | is-core-module@^2.13.0: 689 | version "2.13.1" 690 | resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" 691 | integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== 692 | dependencies: 693 | hasown "^2.0.0" 694 | 695 | is-extglob@^2.1.1: 696 | version "2.1.1" 697 | resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 698 | integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== 699 | 700 | is-glob@^4.0.1, is-glob@~4.0.1: 701 | version "4.0.3" 702 | resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" 703 | integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== 704 | dependencies: 705 | is-extglob "^2.1.1" 706 | 707 | is-number@^7.0.0: 708 | version "7.0.0" 709 | resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" 710 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 711 | 712 | json5@^2.2.2: 713 | version "2.2.3" 714 | resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" 715 | integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== 716 | 717 | make-error@^1.1.1: 718 | version "1.3.6" 719 | resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" 720 | integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== 721 | 722 | media-typer@0.3.0: 723 | version "0.3.0" 724 | resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 725 | integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== 726 | 727 | merge-descriptors@1.0.1: 728 | version "1.0.1" 729 | resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 730 | integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== 731 | 732 | merge2@^1.3.0, merge2@^1.4.1: 733 | version "1.4.1" 734 | resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" 735 | integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== 736 | 737 | methods@~1.1.2: 738 | version "1.1.2" 739 | resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 740 | integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== 741 | 742 | micromatch@^4.0.4: 743 | version "4.0.5" 744 | resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" 745 | integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== 746 | dependencies: 747 | braces "^3.0.2" 748 | picomatch "^2.3.1" 749 | 750 | mime-db@1.52.0: 751 | version "1.52.0" 752 | resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" 753 | integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== 754 | 755 | mime-types@~2.1.24, mime-types@~2.1.34: 756 | version "2.1.35" 757 | resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" 758 | integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== 759 | dependencies: 760 | mime-db "1.52.0" 761 | 762 | mime@1.6.0: 763 | version "1.6.0" 764 | resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" 765 | integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== 766 | 767 | minimatch@^3.1.1: 768 | version "3.1.2" 769 | resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" 770 | integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== 771 | dependencies: 772 | brace-expansion "^1.1.7" 773 | 774 | minimist@^1.2.6: 775 | version "1.2.8" 776 | resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" 777 | integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== 778 | 779 | mkdirp@^1.0.4: 780 | version "1.0.4" 781 | resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" 782 | integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== 783 | 784 | morgan@^1.10.0: 785 | version "1.10.0" 786 | resolved "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" 787 | integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== 788 | dependencies: 789 | basic-auth "~2.0.1" 790 | debug "2.6.9" 791 | depd "~2.0.0" 792 | on-finished "~2.3.0" 793 | on-headers "~1.0.2" 794 | 795 | ms@2.0.0: 796 | version "2.0.0" 797 | resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 798 | integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== 799 | 800 | ms@2.1.3: 801 | version "2.1.3" 802 | resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" 803 | integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== 804 | 805 | mylas@^2.1.9: 806 | version "2.1.13" 807 | resolved "https://registry.npmjs.org/mylas/-/mylas-2.1.13.tgz#1e23b37d58fdcc76e15d8a5ed23f9ae9fc0cbdf4" 808 | integrity sha512-+MrqnJRtxdF+xngFfUUkIMQrUUL0KsxbADUkn23Z/4ibGg192Q+z+CQyiYwvWTsYjJygmMR8+w3ZDa98Zh6ESg== 809 | 810 | negotiator@0.6.3: 811 | version "0.6.3" 812 | resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" 813 | integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== 814 | 815 | normalize-path@^3.0.0, normalize-path@~3.0.0: 816 | version "3.0.0" 817 | resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" 818 | integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== 819 | 820 | object-assign@^4: 821 | version "4.1.1" 822 | resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 823 | integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== 824 | 825 | object-inspect@^1.13.1: 826 | version "1.13.1" 827 | resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" 828 | integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== 829 | 830 | on-finished@2.4.1: 831 | version "2.4.1" 832 | resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" 833 | integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== 834 | dependencies: 835 | ee-first "1.1.1" 836 | 837 | on-finished@~2.3.0: 838 | version "2.3.0" 839 | resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 840 | integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== 841 | dependencies: 842 | ee-first "1.1.1" 843 | 844 | on-headers@~1.0.2: 845 | version "1.0.2" 846 | resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" 847 | integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== 848 | 849 | once@^1.3.0: 850 | version "1.4.0" 851 | resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 852 | integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== 853 | dependencies: 854 | wrappy "1" 855 | 856 | parseurl@~1.3.3: 857 | version "1.3.3" 858 | resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" 859 | integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== 860 | 861 | path-is-absolute@^1.0.0: 862 | version "1.0.1" 863 | resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 864 | integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== 865 | 866 | path-parse@^1.0.7: 867 | version "1.0.7" 868 | resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" 869 | integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== 870 | 871 | path-to-regexp@0.1.7: 872 | version "0.1.7" 873 | resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 874 | integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== 875 | 876 | path-type@^4.0.0: 877 | version "4.0.0" 878 | resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" 879 | integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== 880 | 881 | picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: 882 | version "2.3.1" 883 | resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" 884 | integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== 885 | 886 | plimit-lit@^1.2.6: 887 | version "1.6.1" 888 | resolved "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz#a34594671b31ee8e93c72d505dfb6852eb72374a" 889 | integrity sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA== 890 | dependencies: 891 | queue-lit "^1.5.1" 892 | 893 | prisma@^5.10.1: 894 | version "5.10.2" 895 | resolved "https://registry.npmjs.org/prisma/-/prisma-5.10.2.tgz#aa63085c49dc74cdb5c3816e8dd1fb4d74a2aadd" 896 | integrity sha512-hqb/JMz9/kymRE25pMWCxkdyhbnIWrq+h7S6WysJpdnCvhstbJSNP/S6mScEcqiB8Qv2F+0R3yG+osRaWqZacQ== 897 | dependencies: 898 | "@prisma/engines" "5.10.2" 899 | 900 | proxy-addr@~2.0.7: 901 | version "2.0.7" 902 | resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" 903 | integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== 904 | dependencies: 905 | forwarded "0.2.0" 906 | ipaddr.js "1.9.1" 907 | 908 | qs@6.11.0: 909 | version "6.11.0" 910 | resolved "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" 911 | integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== 912 | dependencies: 913 | side-channel "^1.0.4" 914 | 915 | queue-lit@^1.5.1: 916 | version "1.5.2" 917 | resolved "https://registry.npmjs.org/queue-lit/-/queue-lit-1.5.2.tgz#83c24d4f4764802377b05a6e5c73017caf3f8747" 918 | integrity sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw== 919 | 920 | queue-microtask@^1.2.2: 921 | version "1.2.3" 922 | resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" 923 | integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== 924 | 925 | range-parser@~1.2.1: 926 | version "1.2.1" 927 | resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" 928 | integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== 929 | 930 | raw-body@2.5.1: 931 | version "2.5.1" 932 | resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" 933 | integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== 934 | dependencies: 935 | bytes "3.1.2" 936 | http-errors "2.0.0" 937 | iconv-lite "0.4.24" 938 | unpipe "1.0.0" 939 | 940 | readdirp@~3.6.0: 941 | version "3.6.0" 942 | resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" 943 | integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== 944 | dependencies: 945 | picomatch "^2.2.1" 946 | 947 | resolve@^1.0.0: 948 | version "1.22.8" 949 | resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" 950 | integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== 951 | dependencies: 952 | is-core-module "^2.13.0" 953 | path-parse "^1.0.7" 954 | supports-preserve-symlinks-flag "^1.0.0" 955 | 956 | reusify@^1.0.4: 957 | version "1.0.4" 958 | resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" 959 | integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== 960 | 961 | rimraf@^2.6.1: 962 | version "2.7.1" 963 | resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" 964 | integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== 965 | dependencies: 966 | glob "^7.1.3" 967 | 968 | run-parallel@^1.1.9: 969 | version "1.2.0" 970 | resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" 971 | integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== 972 | dependencies: 973 | queue-microtask "^1.2.2" 974 | 975 | safe-buffer@5.1.2: 976 | version "5.1.2" 977 | resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 978 | integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== 979 | 980 | safe-buffer@5.2.1: 981 | version "5.2.1" 982 | resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 983 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 984 | 985 | "safer-buffer@>= 2.1.2 < 3": 986 | version "2.1.2" 987 | resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 988 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 989 | 990 | send@0.18.0: 991 | version "0.18.0" 992 | resolved "https://registry.npmjs.org/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" 993 | integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== 994 | dependencies: 995 | debug "2.6.9" 996 | depd "2.0.0" 997 | destroy "1.2.0" 998 | encodeurl "~1.0.2" 999 | escape-html "~1.0.3" 1000 | etag "~1.8.1" 1001 | fresh "0.5.2" 1002 | http-errors "2.0.0" 1003 | mime "1.6.0" 1004 | ms "2.1.3" 1005 | on-finished "2.4.1" 1006 | range-parser "~1.2.1" 1007 | statuses "2.0.1" 1008 | 1009 | serve-static@1.15.0: 1010 | version "1.15.0" 1011 | resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" 1012 | integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== 1013 | dependencies: 1014 | encodeurl "~1.0.2" 1015 | escape-html "~1.0.3" 1016 | parseurl "~1.3.3" 1017 | send "0.18.0" 1018 | 1019 | set-function-length@^1.2.1: 1020 | version "1.2.1" 1021 | resolved "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz#47cc5945f2c771e2cf261c6737cf9684a2a5e425" 1022 | integrity sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g== 1023 | dependencies: 1024 | define-data-property "^1.1.2" 1025 | es-errors "^1.3.0" 1026 | function-bind "^1.1.2" 1027 | get-intrinsic "^1.2.3" 1028 | gopd "^1.0.1" 1029 | has-property-descriptors "^1.0.1" 1030 | 1031 | setprototypeof@1.2.0: 1032 | version "1.2.0" 1033 | resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" 1034 | integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== 1035 | 1036 | side-channel@^1.0.4: 1037 | version "1.0.5" 1038 | resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz#9a84546599b48909fb6af1211708d23b1946221b" 1039 | integrity sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ== 1040 | dependencies: 1041 | call-bind "^1.0.6" 1042 | es-errors "^1.3.0" 1043 | get-intrinsic "^1.2.4" 1044 | object-inspect "^1.13.1" 1045 | 1046 | slash@^3.0.0: 1047 | version "3.0.0" 1048 | resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" 1049 | integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== 1050 | 1051 | source-map-support@^0.5.12: 1052 | version "0.5.21" 1053 | resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" 1054 | integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== 1055 | dependencies: 1056 | buffer-from "^1.0.0" 1057 | source-map "^0.6.0" 1058 | 1059 | source-map@^0.6.0: 1060 | version "0.6.1" 1061 | resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 1062 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 1063 | 1064 | statuses@2.0.1: 1065 | version "2.0.1" 1066 | resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" 1067 | integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== 1068 | 1069 | strip-bom@^3.0.0: 1070 | version "3.0.0" 1071 | resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" 1072 | integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== 1073 | 1074 | strip-json-comments@^2.0.0: 1075 | version "2.0.1" 1076 | resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" 1077 | integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== 1078 | 1079 | supports-preserve-symlinks-flag@^1.0.0: 1080 | version "1.0.0" 1081 | resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" 1082 | integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== 1083 | 1084 | to-regex-range@^5.0.1: 1085 | version "5.0.1" 1086 | resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" 1087 | integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== 1088 | dependencies: 1089 | is-number "^7.0.0" 1090 | 1091 | toidentifier@1.0.1: 1092 | version "1.0.1" 1093 | resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" 1094 | integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== 1095 | 1096 | tree-kill@^1.2.2: 1097 | version "1.2.2" 1098 | resolved "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" 1099 | integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== 1100 | 1101 | ts-node-dev@^2.0.0: 1102 | version "2.0.0" 1103 | resolved "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz#bdd53e17ab3b5d822ef519928dc6b4a7e0f13065" 1104 | integrity sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w== 1105 | dependencies: 1106 | chokidar "^3.5.1" 1107 | dynamic-dedupe "^0.3.0" 1108 | minimist "^1.2.6" 1109 | mkdirp "^1.0.4" 1110 | resolve "^1.0.0" 1111 | rimraf "^2.6.1" 1112 | source-map-support "^0.5.12" 1113 | tree-kill "^1.2.2" 1114 | ts-node "^10.4.0" 1115 | tsconfig "^7.0.0" 1116 | 1117 | ts-node@^10.4.0: 1118 | version "10.9.2" 1119 | resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" 1120 | integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== 1121 | dependencies: 1122 | "@cspotcode/source-map-support" "^0.8.0" 1123 | "@tsconfig/node10" "^1.0.7" 1124 | "@tsconfig/node12" "^1.0.7" 1125 | "@tsconfig/node14" "^1.0.0" 1126 | "@tsconfig/node16" "^1.0.2" 1127 | acorn "^8.4.1" 1128 | acorn-walk "^8.1.1" 1129 | arg "^4.1.0" 1130 | create-require "^1.1.0" 1131 | diff "^4.0.1" 1132 | make-error "^1.1.1" 1133 | v8-compile-cache-lib "^3.0.1" 1134 | yn "3.1.1" 1135 | 1136 | tsc-alias@^1.8.8: 1137 | version "1.8.8" 1138 | resolved "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.8.tgz#48696af442b7656dd7905e37ae0bc332d80be3fe" 1139 | integrity sha512-OYUOd2wl0H858NvABWr/BoSKNERw3N9GTi3rHPK8Iv4O1UyUXIrTTOAZNHsjlVpXFOhpJBVARI1s+rzwLivN3Q== 1140 | dependencies: 1141 | chokidar "^3.5.3" 1142 | commander "^9.0.0" 1143 | globby "^11.0.4" 1144 | mylas "^2.1.9" 1145 | normalize-path "^3.0.0" 1146 | plimit-lit "^1.2.6" 1147 | 1148 | tsc@^2.0.4: 1149 | version "2.0.4" 1150 | resolved "https://registry.npmjs.org/tsc/-/tsc-2.0.4.tgz#5f6499146abea5dca4420b451fa4f2f9345238f5" 1151 | integrity sha512-fzoSieZI5KKJVBYGvwbVZs/J5za84f2lSTLPYf6AGiIf43tZ3GNrI1QzTLcjtyDDP4aLxd46RTZq1nQxe7+k5Q== 1152 | 1153 | tsconfig-paths@^4.2.0: 1154 | version "4.2.0" 1155 | resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" 1156 | integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== 1157 | dependencies: 1158 | json5 "^2.2.2" 1159 | minimist "^1.2.6" 1160 | strip-bom "^3.0.0" 1161 | 1162 | tsconfig@^7.0.0: 1163 | version "7.0.0" 1164 | resolved "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz#84538875a4dc216e5c4a5432b3a4dec3d54e91b7" 1165 | integrity sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw== 1166 | dependencies: 1167 | "@types/strip-bom" "^3.0.0" 1168 | "@types/strip-json-comments" "0.0.30" 1169 | strip-bom "^3.0.0" 1170 | strip-json-comments "^2.0.0" 1171 | 1172 | type-is@~1.6.18: 1173 | version "1.6.18" 1174 | resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" 1175 | integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== 1176 | dependencies: 1177 | media-typer "0.3.0" 1178 | mime-types "~2.1.24" 1179 | 1180 | typescript@^5.3.3: 1181 | version "5.3.3" 1182 | resolved "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" 1183 | integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== 1184 | 1185 | undici-types@~5.26.4: 1186 | version "5.26.5" 1187 | resolved "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" 1188 | integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== 1189 | 1190 | unpipe@1.0.0, unpipe@~1.0.0: 1191 | version "1.0.0" 1192 | resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 1193 | integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== 1194 | 1195 | utils-merge@1.0.1: 1196 | version "1.0.1" 1197 | resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 1198 | integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== 1199 | 1200 | v8-compile-cache-lib@^3.0.1: 1201 | version "3.0.1" 1202 | resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" 1203 | integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== 1204 | 1205 | vary@^1, vary@~1.1.2: 1206 | version "1.1.2" 1207 | resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 1208 | integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== 1209 | 1210 | wrappy@1: 1211 | version "1.0.2" 1212 | resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 1213 | integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== 1214 | 1215 | xtend@^4.0.0: 1216 | version "4.0.2" 1217 | resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" 1218 | integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== 1219 | 1220 | yn@3.1.1: 1221 | version "3.1.1" 1222 | resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" 1223 | integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== 1224 | 1225 | zod@^3.22.4: 1226 | version "3.22.4" 1227 | resolved "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" 1228 | integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg== 1229 | --------------------------------------------------------------------------------