diff --git a/.codeclimate.yml b/.codeclimate.yml index 10764a49..cc58e345 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -49,9 +49,3 @@ plugins: enabled: false nodesecurity: enabled: true - sass-lint: - enabled: true - rules: - nesting-depth: - - 2 - - max-depth: 5 diff --git a/README.md b/README.md index 833d3cb6..eac2da98 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![Maintainability](https://api.codeclimate.com/v1/badges/77078c9bd93bd99d5840/maintainability)](https://codeclimate.com/github/bcgov/nr-permitconnect-navigator-service/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/77078c9bd93bd99d5840/test_coverage)](https://codeclimate.com/github/bcgov/nr-permitconnect-navigator-service/test_coverage) -A clean Vue 3 frontend & backend scaffold example +NR PermitConnect Navigator Service To learn more about the **Common Services** available visit the [Common Services Showcase](https://bcgov.github.io/common-service-showcase/) page. @@ -21,6 +21,8 @@ app/ - Application Root ├── src/ - Node.js web application │ ├── components/ - Components Layer │ ├── controllers/ - Controller Layer +│ ├── db/ - Database Layer +│ ├── interfaces/ - Typescript interface definitions │ ├── middleware/ - Middleware Layer │ ├── routes/ - Routes Layer │ ├── services/ - Services Layer @@ -39,7 +41,7 @@ frontend/ - Frontend Root │ ├── types/ - Typescript type definitions │ ├── utils/ - Utility components │ └── views/ - View Layer -└── tests/ - Node.js web application tests +└── tests/ - Vitest web application tests CODE-OF-CONDUCT.md - Code of Conduct COMPLIANCE.yaml - BCGov PIA/STRA compliance status CONTRIBUTING.md - Contributing Guidelines @@ -50,18 +52,23 @@ SECURITY.md - Security Policy and Reporting ## Documentation -- [Application Readme](frontend/README.md) +- [Application Readme](app/README.md) +- [Frontend Readme](frontend/README.md) - [Product Roadmap](https://github.com/bcgov/nr-permitconnect-navigator-service/wiki/Product-Roadmap) - [Product Wiki](https://github.com/bcgov/nr-permitconnect-navigator-service/wiki) - [Security Reporting](SECURITY.md) ## Quick Start Dev Guide -You can quickly run this application in development mode after cloning by opening two terminal windows and running the following commands (assuming you have already set up local configuration as well). Refer to the [Application Readme](app/README.md) and [Frontend Readme](app/frontend/README.md) for more details. +You can quickly run this application in development mode after cloning by opening two terminal windows and running the following commands (assuming you have already set up local configuration as well). Refer to the [Application Readme](app/README.md) and [Frontend Readme](/frontend/README.md) for more details. + +- Create `.env` in the root directory with the following + - `DATABASE_URL="your_connection_string"` ``` cd app npm i +npm run prisma:migrate npm run serve ``` diff --git a/SECURITY.md b/SECURITY.md index 6e15f72d..b5918469 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,6 +1,6 @@ # Security Policies and Procedures -This document outlines security procedures and general policies for the NR Permitting Navigator Service +This document outlines security procedures and general policies for the NR PermitConnect Navigator Service project. - [Supported Versions](#supported-versions) @@ -10,7 +10,7 @@ project. ## Supported Versions -At this time, only the latest version of NR Permitting Navigator Service is supported. +At this time, only the latest version of NR PermitConnect Navigator Service is supported. | Version | Supported | | ------- | ------------------ | @@ -19,8 +19,8 @@ At this time, only the latest version of NR Permitting Navigator Service is supp ## Reporting a Bug -The `CSS` team and community take all security bugs in `NR Permitting Navigator Service` seriously. -Thank you for improving the security of `NR Permitting Navigator Service`. We appreciate your efforts and +The `CSS` team and community take all security bugs in `NR PermitConnect Navigator Service` seriously. +Thank you for improving the security of `NR PermitConnect Navigator Service`. We appreciate your efforts and responsible disclosure and will make every effort to acknowledge your contributions. diff --git a/app/app.ts b/app/app.ts index 1edecbc3..3decfda8 100644 --- a/app/app.ts +++ b/app/app.ts @@ -30,7 +30,18 @@ app.use(compression()); app.use(cors(DEFAULTCORS)); app.use(express.json({ limit: config.get('server.bodyLimit') })); app.use(express.urlencoded({ extended: true })); -app.use(helmet()); +app.use( + helmet({ + contentSecurityPolicy: { + directives: { + 'default-src': [ + "'self'", // eslint-disable-line + new URL(config.get('frontend.oidc.authority')).origin + ] + } + } + }) +); // Skip if running tests if (process.env.NODE_ENV !== 'test') { diff --git a/app/package-lock.json b/app/package-lock.json index b5e1e727..80b79de7 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -10,15 +10,6 @@ "license": "Apache-2.0", "dependencies": { "@prisma/client": "^5.7.0", - "@types/compression": "^1.7.5", - "@types/config": "^3.3.3", - "@types/cors": "^2.8.16", - "@types/express": "^4.17.21", - "@types/jest": "^29.5.8", - "@types/jsonwebtoken": "^9.0.5", - "@types/node": "^20.9.0", - "@types/pg": "^8.10.9", - "@types/uuid": "^9.0.7", "api-problem": "^9.0.2", "axios": "^1.5.1", "compression": "^1.7.4", @@ -37,6 +28,16 @@ "winston-transport": "^4.6.0" }, "devDependencies": { + "@types/compression": "^1.7.5", + "@types/config": "^3.3.3", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/express-serve-static-core": "^4.17.41", + "@types/jest": "^29.5.11", + "@types/jsonwebtoken": "^9.0.5", + "@types/node": "^20.10.5", + "@types/pg": "^8.10.9", + "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.9.0", "@typescript-eslint/parser": "^6.9.0", "eslint": "^8.52.0", @@ -78,6 +79,7 @@ "version": "7.22.13", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, "dependencies": { "@babel/highlight": "^7.22.13", "chalk": "^2.4.2" @@ -90,6 +92,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -101,6 +104,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -114,6 +118,7 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -121,12 +126,14 @@ "node_modules/@babel/code-frame/node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true }, "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, "engines": { "node": ">=0.8.0" } @@ -135,6 +142,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, "engines": { "node": ">=4" } @@ -143,6 +151,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -349,6 +358,7 @@ "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, "engines": { "node": ">=6.9.0" } @@ -380,6 +390,7 @@ "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", @@ -393,6 +404,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -404,6 +416,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -417,6 +430,7 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -424,12 +438,14 @@ "node_modules/@babel/highlight/node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true }, "node_modules/@babel/highlight/node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, "engines": { "node": ">=0.8.0" } @@ -438,6 +454,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, "engines": { "node": ">=4" } @@ -446,6 +463,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -1144,6 +1162,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, "dependencies": { "jest-get-type": "^29.6.3" }, @@ -1230,6 +1249,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, "dependencies": { "@sinclair/typebox": "^0.27.8" }, @@ -1311,6 +1331,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", @@ -1499,7 +1520,8 @@ "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==" + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true }, "node_modules/@sinonjs/commons": { "version": "3.0.0", @@ -1584,6 +1606,7 @@ "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -1593,6 +1616,7 @@ "version": "1.7.5", "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.5.tgz", "integrity": "sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg==", + "dev": true, "dependencies": { "@types/express": "*" } @@ -1600,20 +1624,23 @@ "node_modules/@types/config": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/@types/config/-/config-3.3.3.tgz", - "integrity": "sha512-BB8DBAud88EgiAKlz8WQStzI771Kb6F3j4dioRJ4GD+tP4tzcZyMlz86aXuZT4s9hyesFORehMQE6eqtA5O+Vg==" + "integrity": "sha512-BB8DBAud88EgiAKlz8WQStzI771Kb6F3j4dioRJ4GD+tP4tzcZyMlz86aXuZT4s9hyesFORehMQE6eqtA5O+Vg==", + "dev": true }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, "dependencies": { "@types/node": "*" } }, "node_modules/@types/cors": { - "version": "2.8.16", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.16.tgz", - "integrity": "sha512-Trx5or1Nyg1Fq138PCuWqoApzvoSLWzZ25ORBiHMbbUT42g578lH1GT4TwYDbiUOLFuDsCkfLneT2105fsFWGg==", + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, "dependencies": { "@types/node": "*" } @@ -1622,6 +1649,7 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", @@ -1633,6 +1661,7 @@ "version": "4.17.41", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", + "dev": true, "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -1652,17 +1681,20 @@ "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", - "integrity": "sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ==" + "integrity": "sha512-zONci81DZYCZjiLe0r6equvZut0b+dBRPBN5kBDjsONnutYNtJMoWQ9uR2RkL1gLG9NMTzvf+29e5RFfPbeKhQ==", + "dev": true }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.2.tgz", "integrity": "sha512-8toY6FgdltSdONav1XtUHl4LN1yTmLza+EuDazb/fEmRNCwjyqNVIQWs2IfC74IqjHkREs/nQ2FWq5kZU9IC0w==", + "dev": true, "dependencies": { "@types/istanbul-lib-coverage": "*" } @@ -1671,14 +1703,16 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.3.tgz", "integrity": "sha512-1nESsePMBlf0RPRffLZi5ujYh7IH1BWL4y9pr+Bn3cJBdxz+RTP8bUFljLz9HvzhhOSWKdyBZ4DIivdL6rvgZg==", + "dev": true, "dependencies": { "@types/istanbul-lib-report": "*" } }, "node_modules/@types/jest": { - "version": "29.5.8", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.8.tgz", - "integrity": "sha512-fXEFTxMV2Co8ZF5aYFJv+YeA08RTYJfhtN5c9JSv/mFEMe+xxjufCb+PHL+bJcMs/ebPUsBu+UNTEz+ydXrR6g==", + "version": "29.5.11", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.11.tgz", + "integrity": "sha512-S2mHmYIVe13vrm6q4kN6fLYYAka15ALQki/vgDC3mIukEOx8WJlv0kQPM+d4w8Gp6u0uSdKND04IlTXBv0rwnQ==", + "dev": true, "dependencies": { "expect": "^29.0.0", "pretty-format": "^29.0.0" @@ -1700,6 +1734,7 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", "integrity": "sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==", + "dev": true, "dependencies": { "@types/node": "*" } @@ -1707,12 +1742,13 @@ "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true }, "node_modules/@types/node": { - "version": "20.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz", - "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==", + "version": "20.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz", + "integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==", "dependencies": { "undici-types": "~5.26.4" } @@ -1721,6 +1757,7 @@ "version": "8.10.9", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.9.tgz", "integrity": "sha512-UksbANNE/f8w0wOMxVKKIrLCbEMV+oM1uKejmwXr39olg4xqcfBDbXxObJAt6XxHbDa4XTKOlUEcEltXDX+XLQ==", + "dev": true, "dependencies": { "@types/node": "*", "pg-protocol": "*", @@ -1731,6 +1768,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.1.tgz", "integrity": "sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==", + "dev": true, "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", @@ -1748,6 +1786,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz", "integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==", + "dev": true, "engines": { "node": ">=12" } @@ -1756,6 +1795,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz", "integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==", + "dev": true, "dependencies": { "obuf": "~1.1.2" }, @@ -1767,6 +1807,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.0.1.tgz", "integrity": "sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==", + "dev": true, "engines": { "node": ">=12" } @@ -1775,6 +1816,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz", "integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==", + "dev": true, "engines": { "node": ">=12" } @@ -1782,12 +1824,14 @@ "node_modules/@types/qs": { "version": "6.9.10", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.10.tgz", - "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==" + "integrity": "sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==", + "dev": true }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true }, "node_modules/@types/semver": { "version": "7.5.4", @@ -1799,6 +1843,7 @@ "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, "dependencies": { "@types/mime": "^1", "@types/node": "*" @@ -1808,6 +1853,7 @@ "version": "1.15.5", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "dev": true, "dependencies": { "@types/http-errors": "*", "@types/mime": "*", @@ -1817,7 +1863,8 @@ "node_modules/@types/stack-utils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.2.tgz", - "integrity": "sha512-g7CK9nHdwjK2n0ymT2CW698FuWJRIx+RP6embAzZ2Qi8/ilIrA1Imt2LVSeHUzKvpoi7BhmmQcXz95eS0f2JXw==" + "integrity": "sha512-g7CK9nHdwjK2n0ymT2CW698FuWJRIx+RP6embAzZ2Qi8/ilIrA1Imt2LVSeHUzKvpoi7BhmmQcXz95eS0f2JXw==", + "dev": true }, "node_modules/@types/strip-bom": { "version": "3.0.0", @@ -1839,12 +1886,14 @@ "node_modules/@types/uuid": { "version": "9.0.7", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.7.tgz", - "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==" + "integrity": "sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==", + "dev": true }, "node_modules/@types/yargs": { "version": "17.0.29", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.29.tgz", "integrity": "sha512-nacjqA3ee9zRF/++a3FUY1suHTFKZeHba2n8WeDw9cCVdmzmHpIxyzOJBcpHvvEmS8E9KqWlSnWHUkOrkhWcvA==", + "dev": true, "dependencies": { "@types/yargs-parser": "*" } @@ -1852,7 +1901,8 @@ "node_modules/@types/yargs-parser": { "version": "21.0.2", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.2.tgz", - "integrity": "sha512-5qcvofLPbfjmBfKaLfj/+f+Sbd6pN4zl7w7VSVI5uz7m9QZTuB2aZAa2uo1wHFBNN2x6g/SoTkXmd8mQnQF2Cw==" + "integrity": "sha512-5qcvofLPbfjmBfKaLfj/+f+Sbd6pN4zl7w7VSVI5uz7m9QZTuB2aZAa2uo1wHFBNN2x6g/SoTkXmd8mQnQF2Cw==", + "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "6.9.0", @@ -2145,6 +2195,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -2616,6 +2667,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -2773,6 +2825,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2842,6 +2895,7 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, "funding": [ { "type": "github", @@ -2922,6 +2976,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -3448,6 +3503,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -6047,6 +6103,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, "dependencies": { "@jest/expect-utils": "^29.7.0", "jest-get-type": "^29.6.3", @@ -6339,6 +6396,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -6733,7 +6791,8 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true }, "node_modules/graphemer": { "version": "1.4.0", @@ -6754,6 +6813,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -7225,6 +7285,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "engines": { "node": ">=0.12.0" } @@ -7669,6 +7730,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, "dependencies": { "chalk": "^4.0.0", "diff-sequences": "^29.6.3", @@ -7728,6 +7790,7 @@ "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } @@ -7774,6 +7837,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, "dependencies": { "chalk": "^4.0.0", "jest-diff": "^29.7.0", @@ -7788,6 +7852,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", @@ -7976,6 +8041,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", @@ -8069,7 +8135,8 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true }, "node_modules/js-yaml": { "version": "4.1.0", @@ -8467,6 +8534,7 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -8739,7 +8807,8 @@ "node_modules/obuf": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==" + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true }, "node_modules/on-finished": { "version": "2.4.1", @@ -9040,6 +9109,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/pg-numeric/-/pg-numeric-1.0.2.tgz", "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==", + "dev": true, "engines": { "node": ">=4" } @@ -9090,6 +9160,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "engines": { "node": ">=8.6" }, @@ -9208,7 +9279,8 @@ "node_modules/postgres-range": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.3.tgz", - "integrity": "sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==" + "integrity": "sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==", + "dev": true }, "node_modules/prelude-ls": { "version": "1.2.1", @@ -9250,6 +9322,7 @@ "version": "29.7.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", @@ -9263,6 +9336,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, "engines": { "node": ">=10" }, @@ -9434,7 +9508,8 @@ "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true }, "node_modules/readable-stream": { "version": "3.6.2", @@ -9970,6 +10045,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, "engines": { "node": ">=8" } @@ -10069,6 +10145,7 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, "dependencies": { "escape-string-regexp": "^2.0.0" }, @@ -10080,6 +10157,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, "engines": { "node": ">=8" } @@ -10285,6 +10363,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -10474,6 +10553,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "dependencies": { "is-number": "^7.0.0" }, diff --git a/app/package.json b/app/package.json index be3cc849..472ada1f 100644 --- a/app/package.json +++ b/app/package.json @@ -40,19 +40,12 @@ "migrate:up": "knex migrate:up", "postmigrate:up": "npm run prisma:sync", "seed": "knex seed:run", - "prisma:sync": "prisma db pull" + "prisma:sync": "prisma db pull", + "postprisma:sync": "npm run prisma:generate", + "prisma:generate": "prisma generate" }, "dependencies": { "@prisma/client": "^5.7.0", - "@types/compression": "^1.7.5", - "@types/config": "^3.3.3", - "@types/cors": "^2.8.16", - "@types/express": "^4.17.21", - "@types/jest": "^29.5.8", - "@types/jsonwebtoken": "^9.0.5", - "@types/node": "^20.9.0", - "@types/pg": "^8.10.9", - "@types/uuid": "^9.0.7", "api-problem": "^9.0.2", "axios": "^1.5.1", "compression": "^1.7.4", @@ -71,6 +64,16 @@ "winston-transport": "^4.6.0" }, "devDependencies": { + "@types/compression": "^1.7.5", + "@types/config": "^3.3.3", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/express-serve-static-core": "^4.17.41", + "@types/jest": "^29.5.11", + "@types/jsonwebtoken": "^9.0.5", + "@types/node": "^20.10.5", + "@types/pg": "^8.10.9", + "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.9.0", "@typescript-eslint/parser": "^6.9.0", "eslint": "^8.52.0", diff --git a/app/src/components/utils.ts b/app/src/components/utils.ts index c396314a..8c4fe070 100644 --- a/app/src/components/utils.ts +++ b/app/src/components/utils.ts @@ -3,10 +3,25 @@ import { existsSync, readFileSync } from 'fs'; import { join } from 'path'; import { getLogger } from './log'; -import { ChefsFormConfig, ChefsFormConfigData } from '../types/ChefsFormConfig'; -import { YRN } from '../types/YRN'; + +import type { ChefsFormConfig, ChefsFormConfigData, YRN } from '../types'; + const log = getLogger(module.filename); +/** + * @function addDashesToUuid + * Yields a lowercase uuid `str` that has dashes inserted, or `str` if not a string. + * @param {string} str The input string uuid + * @returns {string} The string `str` but with dashes inserted, or `str` if not a string. + */ +export function addDashesToUuid(str: string): string { + if (str.length === 32) { + return `${str.slice(0, 8)}-${str.slice(8, 12)}-${str.slice(12, 16)}-${str.slice(16, 20)}-${str.slice( + 20 + )}`.toLowerCase(); + } else return str; +} + /** * @function fromYrn * Converts a YRN to boolean @@ -77,6 +92,46 @@ export function isTruthy(value: unknown) { return value === true || value === 1 || (isStr && trueStrings.includes(value.toLowerCase())); } +/** + * @function mixedQueryToArray + * Standardizes query params to yield an array of unique string values + * @param {string|string[]} param The query param to process + * @returns {string[]} A unique, non-empty array of string values, or undefined if empty + */ +export function mixedQueryToArray(param: string | Array): Array | undefined { + // Short circuit undefined if param is falsy + if (!param) return undefined; + + const parsed = Array.isArray(param) ? param.flatMap((p) => parseCSV(p)) : parseCSV(param); + const unique = [...new Set(parsed)]; + + return unique.length ? unique : undefined; +} + +/** + * @function parseCSV + * Converts a comma separated value string into an array of string values + * @param {string} value The CSV string to parse + * @returns {string[]} An array of string values, or `value` if it is not a string + */ +export function parseCSV(value: string): Array { + return value.split(',').map((s) => s.trim()); +} + +/** + * @function parseIdentityKeyClaims + * Returns an array of strings representing potential identity key claims + * Array will always end with the last value as 'sub' + * @returns {string[]} An array of string values, or `value` if it is not a string + */ +export function parseIdentityKeyClaims(): Array { + const claims: Array = []; + if (config.has('server.oidc.identityKey')) { + claims.push(...parseCSV(config.get('server.oidc.identityKey'))); + } + return claims.concat('sub'); +} + /** * @function readIdpList * Acquires the list of identity providers to be used diff --git a/app/src/controllers/chefs.ts b/app/src/controllers/chefs.ts index d0908741..bb22ac9f 100644 --- a/app/src/controllers/chefs.ts +++ b/app/src/controllers/chefs.ts @@ -1,13 +1,12 @@ import config from 'config'; import { chefsService } from '../services'; -import { isTruthy } from '../components/utils'; +import { addDashesToUuid, isTruthy } from '../components/utils'; import { IdentityProvider } from '../components/constants'; -import type { NextFunction, Request, Response } from 'express'; import type { JwtPayload } from 'jsonwebtoken'; -import type { ChefsFormConfig, ChefsFormConfigData } from '../types/ChefsFormConfig'; -import type { ChefsSubmissionFormExport } from '../types/ChefsSubmissionFormExport'; +import type { NextFunction, Request, Response } from '../interfaces/IExpress'; +import type { ChefsFormConfig, ChefsFormConfigData, ChefsSubmissionFormExport } from '../types'; const controller = { getFormExport: async (req: Request, res: Response, next: NextFunction) => { @@ -50,9 +49,13 @@ const controller = { } }, - getSubmission: async (req: Request, res: Response, next: NextFunction) => { + getSubmission: async ( + req: Request<{ submissionId: string }, { formId: string }>, + res: Response, + next: NextFunction + ) => { try { - const response = await chefsService.getSubmission(req.query.formId as string, req.params.submissionId); + const response = await chefsService.getSubmission(addDashesToUuid(req.query.formId), req.params.submissionId); res.status(200).send(response); } catch (e: unknown) { next(e); diff --git a/app/src/controllers/index.ts b/app/src/controllers/index.ts index e9fba46e..306703e8 100644 --- a/app/src/controllers/index.ts +++ b/app/src/controllers/index.ts @@ -1 +1,2 @@ export { default as chefsController } from './chefs'; +export { default as userController } from './user'; diff --git a/app/src/controllers/user.ts b/app/src/controllers/user.ts new file mode 100644 index 00000000..0ac8cba0 --- /dev/null +++ b/app/src/controllers/user.ts @@ -0,0 +1,29 @@ +import { addDashesToUuid, mixedQueryToArray, isTruthy } from '../components/utils'; +import { userService } from '../services'; + +import type { NextFunction, Request, Response } from 'express'; + +const controller = { + searchUsers: async (req: Request, res: Response, next: NextFunction) => { + try { + const userIds = mixedQueryToArray(req.query.userId as string); + + const response = await userService.searchUsers({ + userId: userIds ? userIds.map((id) => addDashesToUuid(id)) : userIds, + identityId: mixedQueryToArray(req.query.identityId as string), + idp: mixedQueryToArray(req.query.idp as string), + username: req.query.username as string, + email: req.query.email as string, + firstName: req.query.firstName as string, + fullName: req.query.fullName as string, + lastName: req.query.lastName as string, + active: isTruthy(req.query.active as string) + }); + res.status(200).send(response); + } catch (e: unknown) { + next(e); + } + } +}; + +export default controller; diff --git a/app/src/db/dataConnection.ts b/app/src/db/dataConnection.ts new file mode 100644 index 00000000..6f297304 --- /dev/null +++ b/app/src/db/dataConnection.ts @@ -0,0 +1,26 @@ +import config from 'config'; +import { PrismaClient } from '@prisma/client'; + +let prisma: PrismaClient; + +const db = { + host: config.get('server.db.host'), + user: config.get('server.db.username'), + password: config.get('server.db.password'), + database: config.get('server.db.database'), + port: config.get('server.db.port'), + poolMax: config.get('server.db.poolMax') +}; + +// @ts-expect-error 2458 +if (!prisma) { + const datasourceUrl = `postgresql://${db.user}:${db.password}@${db.host}:${db.port}/${db.database}?&connection_limit=${db.poolMax}`; + prisma = new PrismaClient({ + // TODO: https://www.prisma.io/docs/orm/prisma-client/observability-and-logging/logging#event-based-logging + log: ['error', 'warn'], + errorFormat: 'pretty', + datasourceUrl: datasourceUrl + }); +} + +export default prisma; diff --git a/app/src/db/migrations/20231212000000_init.ts b/app/src/db/migrations/20231212000000_init.ts index f946bb71..20ddc91f 100644 --- a/app/src/db/migrations/20231212000000_init.ts +++ b/app/src/db/migrations/20231212000000_init.ts @@ -36,7 +36,7 @@ export async function up(knex: Knex): Promise { knex.schema.createTable('submission', (table) => { table.uuid('submissionId').primary(); table.uuid('assignedToUserId').references('userId').inTable('user').onUpdate('CASCADE').onDelete('CASCADE'); - table.text('confirmationId'); + table.text('confirmationId').notNullable(); table.text('contactEmail'); table.text('contactPhoneNumber'); table.text('contactFirstName'); @@ -53,8 +53,8 @@ export async function up(knex: Knex): Promise { table.text('relatedPermits'); table.boolean('updatedAai'); table.text('waitingOn'); - table.timestamp('submittedAt', { useTz: true }); - table.text('submittedBy'); + table.timestamp('submittedAt', { useTz: true }).notNullable(); + table.text('submittedBy').notNullable(); table.timestamp('bringForwardDate', { useTz: true }); table.text('notes'); stamps(knex, table); diff --git a/app/src/db/models/identity_provider.ts b/app/src/db/models/identity_provider.ts new file mode 100644 index 00000000..dfe11ec0 --- /dev/null +++ b/app/src/db/models/identity_provider.ts @@ -0,0 +1,26 @@ +import { Prisma } from '@prisma/client'; + +import type { IStamps } from '../../interfaces/IStamps'; +import type { IdentityProvider } from '../../types'; + +// Define a type +const _identityProvider = Prisma.validator()({}); +type DBIdentityProvider = Omit, keyof IStamps>; + +export default { + toDBModel(input: IdentityProvider): DBIdentityProvider { + return { + idp: input.idp, + active: input.active + }; + }, + + fromDBModel(input: DBIdentityProvider | null): IdentityProvider | null { + if (!input) return null; + + return { + idp: input.idp, + active: input.active + }; + } +}; diff --git a/app/src/db/models/index.ts b/app/src/db/models/index.ts new file mode 100644 index 00000000..b97eaeea --- /dev/null +++ b/app/src/db/models/index.ts @@ -0,0 +1,3 @@ +export { default as identity_provider } from './identity_provider'; +export { default as submission } from './submission'; +export { default as user } from './user'; diff --git a/app/src/db/models/submission.ts b/app/src/db/models/submission.ts new file mode 100644 index 00000000..3dcc44a1 --- /dev/null +++ b/app/src/db/models/submission.ts @@ -0,0 +1,89 @@ +import { Prisma } from '@prisma/client'; +import { default as user } from './user'; +import disconnectRelation from '../utils/disconnectRelation'; +import { fromYrn, toYrn } from '../../components/utils'; + +import type { IStamps } from '../../interfaces/IStamps'; +import type { ChefsSubmissionForm } from '../../types'; + +// Define types +const _submission = Prisma.validator()({}); +const _submissionWithRelations = Prisma.validator()({ + include: { user: true } +}); + +type UserRelation = { + user: + | { + connect: { + userId: string; + }; + } + | { + disconnect: boolean; + }; +}; +type DBSubmission = Omit, 'assignedToUserId' | keyof IStamps> & + UserRelation; + +type Submission = Prisma.submissionGetPayload; + +export default { + toDBModel(input: ChefsSubmissionForm): DBSubmission { + return { + submissionId: input.submissionId, + confirmationId: input.confirmationId, + contactEmail: input.contactEmail, + contactPhoneNumber: input.contactPhoneNumber, + contactFirstName: input.contactFirstName, + contactLastName: input.contactLastName, + intakeStatus: input.intakeStatus, + projectName: input.projectName, + queuePriority: input.queuePriority, + singleFamilyUnits: input.singleFamilyUnits, + streetAddress: input.streetAddress, + atsClientNumber: input.atsClientNumber, + addedToATS: fromYrn(input.addedToATS), + financiallySupported: fromYrn(input.financiallySupported), + applicationStatus: input.applicationStatus, + relatedPermits: input.relatedPermits, + updatedAai: fromYrn(input.updatedAai), + waitingOn: input.waitingOn, + submittedAt: new Date(input.submittedAt), + submittedBy: input.submittedBy, + bringForwardDate: input.bringForwardDate ? new Date(input.bringForwardDate) : null, + notes: input.notes, + user: input.user?.userId ? { connect: { userId: input.user.userId } } : disconnectRelation + }; + }, + + fromDBModel(input: Submission | null): ChefsSubmissionForm | null { + if (!input) return null; + + return { + submissionId: input.submissionId as string, + confirmationId: input.confirmationId as string, + contactEmail: input.contactEmail, + contactPhoneNumber: input.contactPhoneNumber, + contactFirstName: input.contactFirstName, + contactLastName: input.contactLastName, + intakeStatus: input.intakeStatus, + projectName: input.projectName, + queuePriority: input.queuePriority, + singleFamilyUnits: input.singleFamilyUnits, + streetAddress: input.streetAddress, + atsClientNumber: input.atsClientNumber, + addedToATS: toYrn(input.addedToATS as boolean | null), + financiallySupported: toYrn(input.financiallySupported as boolean | null), + applicationStatus: input.applicationStatus, + relatedPermits: input.relatedPermits, + updatedAai: toYrn(input.updatedAai as boolean | null), + waitingOn: input.waitingOn, + submittedAt: input.submittedAt?.toISOString() as string, + submittedBy: input.submittedBy as string, + bringForwardDate: input.bringForwardDate?.toISOString() ?? null, + notes: input.notes, + user: user.fromDBModel(input.user) + }; + } +}; diff --git a/app/src/db/models/user.ts b/app/src/db/models/user.ts new file mode 100644 index 00000000..84af776c --- /dev/null +++ b/app/src/db/models/user.ts @@ -0,0 +1,40 @@ +import { Prisma } from '@prisma/client'; + +import type { IStamps } from '../../interfaces/IStamps'; +import type { User } from '../../types'; + +// Define types +const _user = Prisma.validator()({}); +type DBUser = Omit, keyof IStamps>; + +export default { + toDBModel(input: User): DBUser { + return { + userId: input.userId as string, + identityId: input.identityId, + idp: input.idp, + username: input.username, + email: input.email, + firstName: input.firstName, + fullName: input.fullName, + lastName: input.lastName, + active: input.active + }; + }, + + fromDBModel(input: DBUser | null): User | null { + if (!input) return null; + + return { + userId: input.userId, + identityId: input.identityId as string, + idp: input.idp, + username: input.username, + email: input.email, + firstName: input.firstName, + fullName: input.fullName, + lastName: input.lastName, + active: input.active + }; + } +}; diff --git a/app/src/db/prisma/schema.prisma b/app/src/db/prisma/schema.prisma index 9a703580..738b3ddb 100644 --- a/app/src/db/prisma/schema.prisma +++ b/app/src/db/prisma/schema.prisma @@ -32,14 +32,14 @@ model knex_migrations_lock { model submission { submissionId String @id @db.Uuid assignedToUserId String? @db.Uuid - confirmationId String? + confirmationId String contactEmail String? contactPhoneNumber String? contactFirstName String? contactLastName String? intakeStatus String? projectName String? - queuePriority String? + queuePriority Int? singleFamilyUnits String? streetAddress String? atsClientNumber String? @@ -49,8 +49,8 @@ model submission { relatedPermits String? updatedAai Boolean? waitingOn String? - submittedAt DateTime? @db.Timestamptz(6) - submittedBy String? + submittedAt DateTime @db.Timestamptz(6) + submittedBy String bringForwardDate DateTime? @db.Timestamptz(6) notes String? createdBy String? @default("00000000-0000-0000-0000-000000000000") diff --git a/app/src/db/utils/disconnectRelation.ts b/app/src/db/utils/disconnectRelation.ts new file mode 100644 index 00000000..56396f9a --- /dev/null +++ b/app/src/db/utils/disconnectRelation.ts @@ -0,0 +1,5 @@ +const disconnectRelation = { + disconnect: true +}; + +export default disconnectRelation; diff --git a/app/src/interfaces/IExpress.ts b/app/src/interfaces/IExpress.ts new file mode 100644 index 00000000..4377289b --- /dev/null +++ b/app/src/interfaces/IExpress.ts @@ -0,0 +1,18 @@ +import * as core from 'express-serve-static-core'; + +import type { CurrentUser } from '../types/CurrentUser'; + +interface Query extends core.Query {} + +interface Params extends core.ParamsDictionary {} + +export interface Request

extends core.Request { + currentUser?: CurrentUser; + params: P; + query: Q; + body: B; +} + +export interface Response extends core.Response {} + +export interface NextFunction extends core.NextFunction {} diff --git a/app/src/interfaces/IStamps.ts b/app/src/interfaces/IStamps.ts index 56e5b8f3..9b1a3094 100644 --- a/app/src/interfaces/IStamps.ts +++ b/app/src/interfaces/IStamps.ts @@ -1,6 +1,6 @@ export interface IStamps { - createdBy?: string; - createdAt?: string; - updatedBy?: string; - updatedAt?: string; + createdBy: string | null; + createdAt: string | null; + updatedBy: string | null; + updatedAt: string | null; } diff --git a/app/src/middleware/authentication.ts b/app/src/middleware/authentication.ts index 5ac5aaad..cbf0eff9 100644 --- a/app/src/middleware/authentication.ts +++ b/app/src/middleware/authentication.ts @@ -4,9 +4,10 @@ import config from 'config'; import jwt from 'jsonwebtoken'; import { AuthType } from '../components/constants'; +import { userService } from '../services'; -import type { CurrentUser } from '../types/CurrentUser'; -import type { NextFunction, Request, Response } from 'express'; +import type { CurrentUser } from '../types'; +import type { NextFunction, Request, Response } from '../interfaces/IExpress'; /** * @function _spkiWrapper @@ -54,7 +55,7 @@ export const currentUser = async (req: Request, res: Response, next: NextFunctio if (isValid) { currentUser.tokenPayload = typeof isValid === 'object' ? isValid : jwt.decode(bearerToken); - //await userService.login(currentUser.tokenPayload); + await userService.login(currentUser.tokenPayload as jwt.JwtPayload); } else { throw new Error('Invalid authorization token'); } diff --git a/app/src/middleware/requireSomeAuth.ts b/app/src/middleware/requireSomeAuth.ts index f487f689..b1ef0e3d 100644 --- a/app/src/middleware/requireSomeAuth.ts +++ b/app/src/middleware/requireSomeAuth.ts @@ -2,7 +2,7 @@ import Problem from 'api-problem'; import { AuthType } from '../components/constants'; -import type { NextFunction, Request, Response } from 'express'; +import type { NextFunction, Request, Response } from '../interfaces/IExpress'; /** * @function requireSomeAuth diff --git a/app/src/routes/v1/chefs.ts b/app/src/routes/v1/chefs.ts index adbcc5a7..fc24caf9 100644 --- a/app/src/routes/v1/chefs.ts +++ b/app/src/routes/v1/chefs.ts @@ -3,7 +3,7 @@ import { chefsController } from '../../controllers'; import { requireChefsFormConfigData } from '../../middleware/requireChefsFormConfigData'; import { requireSomeAuth } from '../../middleware/requireSomeAuth'; -import type { NextFunction, Request, Response } from 'express'; +import type { NextFunction, Request, Response } from '../../interfaces/IExpress'; const router = express.Router(); router.use(requireSomeAuth); diff --git a/app/src/routes/v1/index.ts b/app/src/routes/v1/index.ts index a9089a8d..cac356e3 100644 --- a/app/src/routes/v1/index.ts +++ b/app/src/routes/v1/index.ts @@ -1,6 +1,7 @@ import { currentUser } from '../../middleware/authentication'; import express from 'express'; import chefs from './chefs'; +import user from './user'; const router = express.Router(); router.use(currentUser); @@ -8,11 +9,12 @@ router.use(currentUser); // Base v1 Responder router.get('/', (_req, res) => { res.status(200).json({ - endpoints: ['/chefs'] + endpoints: ['/chefs', '/user'] }); }); /** CHEFS Router */ router.use('/chefs', chefs); +router.use('/user', user); export default router; diff --git a/app/src/routes/v1/user.ts b/app/src/routes/v1/user.ts new file mode 100644 index 00000000..8dff378d --- /dev/null +++ b/app/src/routes/v1/user.ts @@ -0,0 +1,15 @@ +import express from 'express'; +import { userController } from '../../controllers'; +import { requireSomeAuth } from '../../middleware/requireSomeAuth'; + +import type { NextFunction, Request, Response } from 'express'; + +const router = express.Router(); +router.use(requireSomeAuth); + +// Submission endpoint +router.get('/', (req: Request, res: Response, next: NextFunction): void => { + userController.searchUsers(req, res, next); +}); + +export default router; diff --git a/app/src/services/chefs.ts b/app/src/services/chefs.ts index 19fa6a17..62af1b71 100644 --- a/app/src/services/chefs.ts +++ b/app/src/services/chefs.ts @@ -1,15 +1,13 @@ /* eslint-disable no-useless-catch */ import axios from 'axios'; import config from 'config'; -import { PrismaClient } from '@prisma/client'; -import { NIL } from 'uuid'; -import { fromYrn, getChefsApiKey, toYrn } from '../components/utils'; +import { getChefsApiKey } from '../components/utils'; +import prisma from '../db/dataConnection'; +import { submission } from '../db/models'; import type { AxiosInstance, AxiosRequestConfig } from 'axios'; -import type { ChefsSubmissionForm } from '../types/ChefsSubmissionForm'; - -const prisma = new PrismaClient(); +import type { ChefsSubmissionForm } from '../types'; /** * @function chefsAxios @@ -40,23 +38,21 @@ const service = { getSubmission: async (formId: string, submissionId: string) => { try { - // Try to pull data from our DB - let result = await prisma.submission.findUnique({ + // Check if record exists in our db + const count = await prisma.submission.count({ where: { submissionId: submissionId } }); // Pull submission data from CHEFS and store to our DB if it doesn't exist - if (!result) { + if (!count) { const response = (await chefsAxios(formId).get(`submissions/${submissionId}`)).data; const status = (await chefsAxios(formId).get(`submissions/${submissionId}/status`)).data; - // TODO: Assigned to correct user - result = await prisma.submission.create({ + await prisma.submission.create({ data: { submissionId: response.submission.id, - assignedToUserId: NIL, //status[0].assignedToUserId, confirmationId: response.submission.confirmationId, contactEmail: response.submission.submission.data.contactEmail, contactPhoneNumber: response.submission.submission.data.contactPhoneNumber, @@ -64,30 +60,33 @@ const service = { contactLastName: response.submission.submission.data.contactLastName, intakeStatus: status[0].code, projectName: response.submission.submission.data.projectName, - queuePriority: response.submission.submission.data.queuePriority, + queuePriority: parseInt(response.submission.submission.data.queuePriority), singleFamilyUnits: response.submission.submission.data.singleFamilyUnits, streetAddress: response.submission.submission.data.streetAddress, - atsClientNumber: null, - addedToATS: null, - financiallySupported: null, - applicationStatus: null, - relatedPermits: null, - updatedAai: null, - waitingOn: null, submittedAt: response.submission.createdAt, - submittedBy: response.submission.createdBy, - bringForwardDate: null, - notes: null + submittedBy: response.submission.createdBy } }); } - return { - ...result, - addedToATS: toYrn(result.addedToATS), - financiallySupported: toYrn(result.financiallySupported), - updatedAai: toYrn(result.updatedAai) - }; + const result = await prisma.submission.findUnique({ + where: { + submissionId: submissionId + }, + include: { + user: true + } + }); + + return submission.fromDBModel(result); + } catch (e: unknown) { + throw e; + } + }, + + getSubmissionStatus: async (formId: string, formSubmissionId: string) => { + try { + return (await chefsAxios(formId).get(`submissions/${formSubmissionId}/status`)).data; } catch (e: unknown) { throw e; } @@ -96,12 +95,7 @@ const service = { updateSubmission: async (data: ChefsSubmissionForm) => { try { await prisma.submission.update({ - data: { - ...data, - addedToATS: fromYrn(data.addedToATS), - financiallySupported: fromYrn(data.financiallySupported), - updatedAai: fromYrn(data.updatedAai) - }, + data: submission.toDBModel(data), where: { submissionId: data.submissionId } diff --git a/app/src/services/index.ts b/app/src/services/index.ts index a8f25756..060b691d 100644 --- a/app/src/services/index.ts +++ b/app/src/services/index.ts @@ -1 +1,2 @@ export { default as chefsService } from './chefs'; +export { default as userService } from './user'; diff --git a/app/src/services/user.ts b/app/src/services/user.ts index 906906a6..8ff188a2 100644 --- a/app/src/services/user.ts +++ b/app/src/services/user.ts @@ -1,274 +1,328 @@ -// const { v4: uuidv4, NIL: SYSTEM_USER } = require('uuid'); - -// const { parseIdentityKeyClaims } = require('../components/utils'); - -// const { IdentityProvider, User } = require('../db/models'); -// const utils = require('../db/models/utils'); - -// /** -// * The User DB Service -// */ -// const service = { -// /** -// * @function _tokenToUser -// * Transforms JWT payload contents into a User Model object -// * @param {object} token The decoded JWT payload -// * @returns {object} An equivalent User model object -// */ -// _tokenToUser: (token) => { -// const identityId = parseIdentityKeyClaims() -// .map((idKey) => token[idKey]) -// .filter((claims) => claims) // Drop falsy values from array -// .concat(undefined)[0]; // Set undefined as last element of array - -// return { -// identityId: identityId, -// username: token.identity_provider_identity ? token.identity_provider_identity : token.preferred_username, -// firstName: token.given_name, -// fullName: token.name, -// lastName: token.family_name, -// email: token.email, -// idp: token.identity_provider -// }; -// }, - -// /** -// * @function createIdp -// * Create an identity provider record -// * @param {string} idp The identity provider code -// * @param {object} [etrx=undefined] An optional Objection Transaction object -// * @returns {Promise} The result of running the insert operation -// * @throws The error encountered upon db transaction failure -// */ -// createIdp: async (idp, etrx = undefined) => { -// let trx; -// try { -// trx = etrx ? etrx : await IdentityProvider.startTransaction(); - -// const obj = { -// idp: idp, -// createdBy: SYSTEM_USER -// }; - -// const response = await IdentityProvider.query(trx).insertAndFetch(obj); -// if (!etrx) await trx.commit(); -// return response; -// } catch (err) { -// if (!etrx && trx) await trx.rollback(); -// throw err; -// } -// }, - -// /** -// * @function createUser -// * Create a user DB record -// * @param {object} data Incoming user data -// * @param {object} [etrx=undefined] An optional Objection Transaction object -// * @returns {Promise} The result of running the insert operation -// * @throws The error encountered upon db transaction failure -// */ -// createUser: async (data, etrx = undefined) => { -// let trx; -// try { -// let response; -// trx = etrx ? etrx : await User.startTransaction(); - -// const exists = await User.query(trx).where({ identityId: data.identityId, idp: data.idp }).first(); - -// if (exists) { -// response = exists; -// } else { -// // else add new user -// if (data.idp) { -// // add idp if not in db -// const identityProvider = await service.readIdp(data.idp, trx); -// if (!identityProvider) await service.createIdp(data.idp, trx); -// } - -// response = await User.query(trx) -// .insert({ -// userId: uuidv4(), -// identityId: data.identityId, -// username: data.username, -// fullName: data.fullName, -// email: data.email, -// firstName: data.firstName, -// lastName: data.lastName, -// idp: data.idp, -// createdBy: data.userId -// }) -// .returning('*'); -// } - -// if (!etrx) await trx.commit(); -// return response; -// } catch (err) { -// if (!etrx && trx) await trx.rollback(); -// throw err; -// } -// }, - -// /** -// * @function getCurrentUserId -// * Gets userId (primary identifier of a user in COMS db) of currentUser. -// * if request is basic auth returns `defaultValue` -// * @param {object} identityId the identity field of the current user -// * @param {string} [defaultValue=undefined] An optional default return value -// * @returns {string} The current userId if applicable, or `defaultValue` -// */ -// getCurrentUserId: async (identityId, defaultValue = undefined) => { -// // TODO: Consider conditionally skipping when identityId is undefined? -// const user = await User.query().select('userId').where('identityId', identityId).first(); - -// return user && user.userId ? user.userId : defaultValue; -// }, - -// /** -// * @function listIdps -// * Lists all known identity providers -// * @param {boolean} [params.active] Optional boolean on user active status -// * @returns {Promise} The result of running the find operation -// */ -// listIdps: (params) => { -// return IdentityProvider.query().modify('filterActive', params.active).modify('orderDefault'); -// }, - -// /** -// * @function login -// * Parse the user token and update the user table if necessary -// * @param {object} token The decoded JWT token payload -// * @returns {Promise} The result of running the login operation -// */ -// login: async (token) => { -// const newUser = service._tokenToUser(token); -// // wrap with db transaction -// return await utils.trxWrapper(async (trx) => { -// // check if user exists in db -// const oldUser = await User.query(trx).where({ identityId: newUser.identityId, idp: newUser.idp }).first(); - -// if (!oldUser) { -// // Add user to system -// return await service.createUser(newUser, trx); -// } else { -// // Update user data if necessary -// return await service.updateUser(oldUser.userId, newUser, trx); -// } -// }); -// }, - -// /** -// * @function readIdp -// * Gets an identity provider record -// * @param {string} code The identity provider code -// * @returns {Promise} The result of running the find operation -// * @throws The error encountered upon db transaction failure -// */ -// readIdp: async (code, etrx = undefined) => { -// let trx; -// try { -// trx = etrx ? etrx : await IdentityProvider.startTransaction(); - -// const response = await IdentityProvider.query(trx).findById(code); - -// if (!etrx) await trx.commit(); -// return response; -// } catch (err) { -// if (!etrx && trx) await trx.rollback(); -// throw err; -// } -// }, - -// /** -// * @function readUser -// * Gets a user record -// * @param {string} userId The userId uuid -// * @returns {Promise} The result of running the find operation -// * @throws If no record is found -// */ -// readUser: (userId) => { -// return User.query().findById(userId).throwIfNotFound(); -// }, - -// /** -// * @function searchUsers -// * Search and filter for specific users -// * @param {string|string[]} [params.userId] Optional string or array of uuids representing the user subject -// * @param {string|string[]} [params.identityId] Optional string or array of uuids representing the user identity -// * @param {string|string[]} [params.idp] Optional string or array of identity providers -// * @param {string} [params.username] Optional username string to match on -// * @param {string} [params.email] Optional email string to match on -// * @param {string} [params.firstName] Optional firstName string to match on -// * @param {string} [params.fullName] Optional fullName string to match on -// * @param {string} [params.lastName] Optional lastName string to match on -// * @param {boolean} [params.active] Optional boolean on user active status -// * @param {string} [params.search] Optional search string to match on in username, email and fullName -// * @returns {Promise} The result of running the find operation -// */ -// searchUsers: (params) => { -// return User.query() -// .modify('filterUserId', params.userId) -// .modify('filterIdentityId', params.identityId) -// .modify('filterIdp', params.idp) -// .modify('filterUsername', params.username) -// .modify('filterEmail', params.email) -// .modify('filterFirstName', params.firstName) -// .modify('filterFullName', params.fullName) -// .modify('filterLastName', params.lastName) -// .modify('filterActive', params.active) -// .modify('filterSearch', params.search) -// .whereNotNull('identityId') -// .modify('orderLastFirstAscending'); -// }, - -// /** -// * @function updateUser -// * Updates a user record only if there are changed values -// * @param {string} userId The userId uuid -// * @param {object} data Incoming user data -// * @param {object} [etrx=undefined] An optional Objection Transaction object -// * @returns {Promise} The result of running the patch operation -// * @throws The error encountered upon db transaction failure -// */ -// updateUser: async (userId, data, etrx = undefined) => { -// let trx; -// try { -// // Check if any user values have changed -// const oldUser = await service.readUser(userId); -// const diff = Object.entries(data).some(([key, value]) => oldUser[key] !== value); - -// if (diff) { -// // Patch existing user -// trx = etrx ? etrx : await User.startTransaction(); - -// if (data.idp) { -// const identityProvider = await service.readIdp(data.idp, trx); -// if (!identityProvider) await service.createIdp(data.idp, trx); -// } - -// const obj = { -// identityId: data.identityId, -// username: data.username, -// fullName: data.fullName, -// email: data.email, -// firstName: data.firstName, -// lastName: data.lastName, -// idp: data.idp, -// updatedBy: data.userId -// }; - -// // TODO: add support for updating userId primary key in the event it changes -// const response = await User.query(trx).patchAndFetchById(userId, obj); -// if (!etrx) await trx.commit(); -// return response; -// } else { -// // Nothing to update -// return oldUser; -// } -// } catch (err) { -// if (!etrx && trx) await trx.rollback(); -// throw err; -// } -// } -// }; - -// export default service; +import jwt from 'jsonwebtoken'; +import { Prisma } from '@prisma/client'; +import { v4, NIL } from 'uuid'; + +import prisma from '../db/dataConnection'; +import { identity_provider, user } from '../db/models'; +import { parseIdentityKeyClaims } from '../components/utils'; + +import type { User, UserSearchParameters } from '../types'; + +const trxWrapper = (etrx: Prisma.TransactionClient | undefined = undefined) => (etrx ? etrx : prisma); + +/** + * The User DB Service + */ +const service = { + /** + * @function _tokenToUser + * Transforms JWT payload contents into a User Model object + * @param {object} token The decoded JWT payload + * @returns {object} An equivalent User model object + */ + _tokenToUser: (token: jwt.JwtPayload) => { + const identityId = parseIdentityKeyClaims() + .map((idKey) => token[idKey]) + .filter((claims) => claims) // Drop falsy values from array + .concat(undefined)[0]; // Set undefined as last element of array + + return { + identityId: identityId, + username: token.identity_provider_identity ? token.identity_provider_identity : token.preferred_username, + firstName: token.given_name, + fullName: token.name, + lastName: token.family_name, + email: token.email, + idp: token.identity_provider, + active: true + }; + }, + + /** + * @function createIdp + * Create an identity provider record + * @param {string} idp The identity provider code + * @param {object} [etrx=undefined] An optional Prisma Transaction object + * @returns {Promise} The result of running the insert operation + * @throws The error encountered upon db transaction failure + */ + createIdp: async (idp: string, etrx: Prisma.TransactionClient | undefined = undefined) => { + const obj = { + idp: idp, + active: true, + createdBy: NIL + }; + + const response = trxWrapper(etrx).identity_provider.create({ data: identity_provider.toDBModel(obj) }); + + return response; + }, + + /** + * @function createUser + * Create a user DB record + * @param {object} data Incoming user data + * @param {object} [etrx=undefined] An optional Prisma Transaction object + * @returns {Promise} The result of running the insert operation + * @throws The error encountered upon db transaction failure + */ + createUser: async (data: User, etrx: Prisma.TransactionClient | undefined = undefined) => { + let response; + + // Logical function + const _createUser = async (data: User, trx: Prisma.TransactionClient) => { + const exists = await trx.user.findFirst({ + where: { + identityId: data.identityId, + idp: data.idp + } + }); + + if (exists) { + response = exists; + } else { + if (data.idp) { + const identityProvider = await service.readIdp(data.idp, trx); + if (!identityProvider) await service.createIdp(data.idp, trx); + } + + const newUser = { + userId: v4(), + identityId: data.identityId, + username: data.username, + fullName: data.fullName, + email: data.email, + firstName: data.firstName, + lastName: data.lastName, + idp: data.idp, + createdBy: data.userId, + active: true + }; + + response = await trx.user.create({ + data: user.toDBModel(newUser) + }); + } + }; + + // Call with proper transaction + if (etrx) { + await _createUser(data, etrx); + } else { + await prisma.$transaction(async (trx) => { + await _createUser(data, trx); + }); + } + + return response; + }, + + /** + * @function getCurrentUserId + * Gets userId (primary identifier of a user in db) of currentUser. + * if request is basic auth returns `defaultValue` + * @param {object} identityId the identity field of the current user + * @param {string} [defaultValue=undefined] An optional default return value + * @returns {string} The current userId if applicable, or `defaultValue` + */ + getCurrentUserId: async (identityId: string, defaultValue = undefined) => { + // TODO: Consider conditionally skipping when identityId is undefined? + const user = await prisma.user.findFirst({ + where: { + identityId: identityId + } + }); + + return user && user.userId ? user.userId : defaultValue; + }, + + /** + * @function listIdps + * Lists all known identity providers + * @param {boolean} [active] Optional boolean on user active status + * @returns {Promise} The result of running the find operation + */ + listIdps: (active: boolean) => { + return prisma.identity_provider.findMany({ + where: { + active: active + } + }); + }, + + /** + * @function login + * Parse the user token and update the user table if necessary + * @param {object} token The decoded JWT token payload + * @returns {Promise} The result of running the login operation + */ + login: async (token: jwt.JwtPayload) => { + const newUser = service._tokenToUser(token); + + let response; + await prisma.$transaction(async (trx) => { + const oldUser = await trx.user.findFirst({ + where: { + identityId: newUser.identityId, + idp: newUser.idp + } + }); + + if (!oldUser) { + response = await service.createUser(newUser, trx); + } else { + response = await service.updateUser(oldUser.userId, newUser, trx); + } + }); + + return response; + }, + + /** + * @function readIdp + * Gets an identity provider record + * @param {string} code The identity provider code + * @param {object} [etrx=undefined] An optional Prisma Transaction object + * @returns {Promise} The result of running the find operation + * @throws The error encountered upon db transaction failure + */ + readIdp: async (code: string, etrx: Prisma.TransactionClient | undefined = undefined) => { + const response = await trxWrapper(etrx).identity_provider.findUnique({ + where: { + idp: code + } + }); + + return identity_provider.fromDBModel(response); + }, + + /** + * @function readUser + * Gets a user record + * @param {string} userId The userId uuid + * @returns {Promise} The result of running the find operation + * @throws If no record is found + */ + readUser: async (userId: string) => { + return await prisma.user.findUnique({ + where: { + userId: userId + } + }); + }, + + /** + * @function searchUsers + * Search and filter for specific users + * @param {string[]} [params.userId] Optional array of uuids representing the user subject + * @param {string[]} [params.identityId] Optionalarray of uuids representing the user identity + * @param {string[]} [params.idp] Optional array of identity providers + * @param {string} [params.username] Optional username string to match on + * @param {string} [params.email] Optional email string to match on + * @param {string} [params.firstName] Optional firstName string to match on + * @param {string} [params.fullName] Optional fullName string to match on + * @param {string} [params.lastName] Optional lastName string to match on + * @param {boolean} [params.active] Optional boolean on user active status + * @returns {Promise} The result of running the find operation + */ + searchUsers: async (params: UserSearchParameters) => { + const response = await prisma.user.findMany({ + where: { + OR: [ + { + userId: { in: params.userId, mode: 'insensitive' } + }, + { + identityId: { in: params.identityId, mode: 'insensitive' } + }, + { + idp: { in: params.idp, mode: 'insensitive' } + }, + { + username: { contains: params.username, mode: 'insensitive' } + }, + { + email: { contains: params.email, mode: 'insensitive' } + }, + { + firstName: { contains: params.firstName, mode: 'insensitive' } + }, + { + fullName: { contains: params.fullName, mode: 'insensitive' } + }, + { + lastName: { contains: params.lastName, mode: 'insensitive' } + }, + { + active: params.active + } + ] + } + }); + + return response.map((x) => user.fromDBModel(x)); + }, + + /** + * @function updateUser + * Updates a user record only if there are changed values + * @param {string} userId The userId uuid + * @param {object} data Incoming user data + * @param {object} [etrx=undefined] An optional Prisma Transaction object + * @returns {Promise} The result of running the patch operation + * @throws The error encountered upon db transaction failure + */ + updateUser: async (userId: string, data: User, etrx: Prisma.TransactionClient | undefined = undefined) => { + // Check if any user values have changed + const oldUser = await service.readUser(userId); + const diff = Object.entries(data).some(([key, value]) => oldUser && oldUser[key as keyof User] !== value); + + let response; + + if (diff) { + const _updateUser = async (userId: string, data: User, trx: Prisma.TransactionClient | undefined = undefined) => { + // Patch existing user + if (data.idp) { + const identityProvider = await service.readIdp(data.idp, trx); + if (!identityProvider) await service.createIdp(data.idp, trx); + } + + const obj = { + identityId: data.identityId, + username: data.username, + fullName: data.fullName, + email: data.email, + firstName: data.firstName, + lastName: data.lastName, + idp: data.idp, + active: data.active, + updatedBy: data.userId + }; + + // TODO: Add support for updating userId primary key in the event it changes + response = await trx?.user.update({ + data: user.toDBModel(obj), + where: { + userId: userId + } + }); + }; + + // Call with proper transaction + if (etrx) { + await _updateUser(userId, data, etrx); + } else { + await prisma.$transaction(async (trx) => { + await _updateUser(userId, data, trx); + }); + } + + return response; + } else { + // Nothing to update + return oldUser; + } + } +}; + +export default service; diff --git a/app/src/types/ChefsSubmissionForm.ts b/app/src/types/ChefsSubmissionForm.ts index be44c291..f04f747d 100644 --- a/app/src/types/ChefsSubmissionForm.ts +++ b/app/src/types/ChefsSubmissionForm.ts @@ -1,28 +1,29 @@ +import { User } from './User'; import { YRN } from './YRN'; import { IStamps } from '../interfaces/IStamps'; export type ChefsSubmissionForm = { - submissionId: string; - assignedToUserId?: string; + submissionId: string; // Primary Key confirmationId: string; - contactEmail?: string; - contactPhoneNumber?: string; - contactFirstName?: string; - contactLastName?: string; - intakeStatus?: string; - projectName?: string; - queuePriority?: string; - singleFamilyUnits?: string; - streetAddress?: string; - atsClientNumber?: string; + contactEmail: string | null; + contactPhoneNumber: string | null; + contactFirstName: string | null; + contactLastName: string | null; + intakeStatus: string | null; + projectName: string | null; + queuePriority: number | null; + singleFamilyUnits: string | null; + streetAddress: string | null; + atsClientNumber: string | null; addedToATS: YRN; financiallySupported: YRN; - applicationStatus?: string; - relatedPermits?: string; + applicationStatus: string | null; + relatedPermits: string | null; updatedAai: YRN; - waitingOn?: string; + waitingOn: string | null; submittedAt: string; submittedBy: string; - bringForwardDate?: string; - notes?: string; -} & IStamps; + bringForwardDate: string | null; + notes: string | null; + user: User | null; +} & Partial; diff --git a/app/src/types/IdentityProvider.ts b/app/src/types/IdentityProvider.ts new file mode 100644 index 00000000..b00302fb --- /dev/null +++ b/app/src/types/IdentityProvider.ts @@ -0,0 +1,6 @@ +import { IStamps } from '../interfaces/IStamps'; + +export type IdentityProvider = { + idp: string; // Primary Key + active: boolean; +} & Partial; diff --git a/app/src/types/Request.d.ts b/app/src/types/Request.d.ts deleted file mode 100644 index 8400f797..00000000 --- a/app/src/types/Request.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare namespace Express { - // Extend the Express Request interface - export interface Request { - currentUser?: import('./CurrentUser').CurrentUser; - } -} diff --git a/app/src/types/User.ts b/app/src/types/User.ts new file mode 100644 index 00000000..c9b3f7a4 --- /dev/null +++ b/app/src/types/User.ts @@ -0,0 +1,13 @@ +import { IStamps } from '../interfaces/IStamps'; + +export type User = { + userId?: string; // Primary Key + identityId: string; + idp: string | null; + username: string; + email: string | null; + firstName: string | null; + fullName: string | null; + lastName: string | null; + active: boolean; +} & Partial; diff --git a/app/src/types/UserSearchParameters.ts b/app/src/types/UserSearchParameters.ts new file mode 100644 index 00000000..a49b61aa --- /dev/null +++ b/app/src/types/UserSearchParameters.ts @@ -0,0 +1,11 @@ +export type UserSearchParameters = { + userId?: string[]; + identityId?: string[]; + idp?: string[]; + username?: string; + email?: string; + firstName?: string; + fullName?: string; + lastName?: string; + active?: boolean; +}; diff --git a/app/src/types/index.ts b/app/src/types/index.ts new file mode 100644 index 00000000..5a317bab --- /dev/null +++ b/app/src/types/index.ts @@ -0,0 +1,8 @@ +export type { ChefsFormConfig, ChefsFormConfigData } from './ChefsFormConfig'; +export type { ChefsSubmissionForm } from './ChefsSubmissionForm'; +export type { ChefsSubmissionFormExport } from './ChefsSubmissionFormExport'; +export type { CurrentUser } from './CurrentUser'; +export type { IdentityProvider } from './IdentityProvider'; +export type { User } from './User'; +export type { UserSearchParameters } from './UserSearchParameters'; +export type { YRN } from './YRN'; diff --git a/frontend/README.md b/frontend/README.md index 3a7949ef..00e3eb29 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,6 +1,6 @@ -# app +# NR PermitConnect Navigator Service Frontend -This template should help get you started developing with Vue 3 in Vite. +This template should help get you started developing with NR PermitConnect Navigator Service ## Recommended IDE Setup @@ -30,7 +30,7 @@ npm install ### Compile and Hot-Reload for Development ```sh -npm run dev +npm run serve ``` ### Type-Check, Compile and Minify for Production diff --git a/frontend/index.html b/frontend/index.html index 2a91d76b..b5601930 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,7 +5,7 @@ - NR Permitting Navigator Service + NR PermitConnect Navigator Service diff --git a/frontend/src/components/form/EditableDropdown.vue b/frontend/src/components/form/EditableDropdown.vue new file mode 100644 index 00000000..d82976eb --- /dev/null +++ b/frontend/src/components/form/EditableDropdown.vue @@ -0,0 +1,54 @@ + + + diff --git a/frontend/src/components/form/index.ts b/frontend/src/components/form/index.ts index 2bccdc93..f7339d2f 100644 --- a/frontend/src/components/form/index.ts +++ b/frontend/src/components/form/index.ts @@ -1,6 +1,7 @@ export { default as Calendar } from './Calendar.vue'; export { default as CopyToClipboard } from './CopyToClipboard.vue'; export { default as Dropdown } from './Dropdown.vue'; +export { default as EditableDropdown } from './EditableDropdown.vue'; export { default as GridRow } from './GridRow.vue'; export { default as Password } from './Password.vue'; export { default as TextArea } from './TextArea.vue'; diff --git a/frontend/src/components/layout/Header.vue b/frontend/src/components/layout/Header.vue index faa7f9a9..1798a25f 100644 --- a/frontend/src/components/layout/Header.vue +++ b/frontend/src/components/layout/Header.vue @@ -17,7 +17,7 @@ import { LoginButton } from '@/components/layout';
-

NR Permitting Navigator Service

+

NR PermitConnect Navigator Service

diff --git a/frontend/src/components/submission/SubmissionForm.vue b/frontend/src/components/submission/SubmissionForm.vue index fa8ecdda..b1c7f0e3 100644 --- a/frontend/src/components/submission/SubmissionForm.vue +++ b/frontend/src/components/submission/SubmissionForm.vue @@ -1,12 +1,18 @@