diff --git a/.github/ISSUE_TEMPLATE/individual-feature.md b/.github/ISSUE_TEMPLATE/individual-feature.md
index 0736148092..43059d4f53 100644
--- a/.github/ISSUE_TEMPLATE/individual-feature.md
+++ b/.github/ISSUE_TEMPLATE/individual-feature.md
@@ -1,11 +1,11 @@
---
name: Individual Feature
-about:
- This template helps you to create individual features within a high-level requirement.
+about: This template helps you to create individual features within a high-level requirement.
This is equivalent to a Feature within an Epic.
-title: '[REQ ID] [FEAT ID]'
+title: "[REQ ID] [FEAT ID]"
labels: chore.Feature
assignees: ''
+
---
# [Feature Name]
@@ -16,14 +16,8 @@ assignees: ''
Enter some information about the feature you are specifying.
-## Functional Requirements
-
-1. [Link to FR 1]()
-2. [Link to FR 2]()
- ...
-
-## Non-Functional Requirements
+## Features
-1. [Link to NFR 1]()
-2. [Link to NFR 2]()
- ...
+1. [Link to Feature 1]()
+2. [Link to Feature 2]()
+...
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..3ec544c7a4
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+node_modules/
+.env
\ No newline at end of file
diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json
new file mode 100644
index 0000000000..3f31950765
--- /dev/null
+++ b/node_modules/.package-lock.json
@@ -0,0 +1,6 @@
+{
+ "name": "cs3219-ay2425s1-project-g05",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {}
+}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000000..3f31950765
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,6 @@
+{
+ "name": "cs3219-ay2425s1-project-g05",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {}
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000000..0967ef424b
--- /dev/null
+++ b/package.json
@@ -0,0 +1 @@
+{}
diff --git a/peer-prep/.gitignore b/peer-prep/.gitignore
index a547bf36d8..314cd3319f 100644
--- a/peer-prep/.gitignore
+++ b/peer-prep/.gitignore
@@ -22,3 +22,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?
+
+.env
+.env.local
diff --git a/peer-prep/.vite/deps_temp_16910cd1/package.json b/peer-prep/.vite/deps_temp_16910cd1/package.json
new file mode 100644
index 0000000000..3dbc1ca591
--- /dev/null
+++ b/peer-prep/.vite/deps_temp_16910cd1/package.json
@@ -0,0 +1,3 @@
+{
+ "type": "module"
+}
diff --git a/peer-prep/README.md b/peer-prep/README.md
index 74872fd4af..780c92d8b4 100644
--- a/peer-prep/README.md
+++ b/peer-prep/README.md
@@ -18,11 +18,11 @@ export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
- project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ project: ["./tsconfig.node.json", "./tsconfig.app.json"],
tsconfigRootDir: import.meta.dirname,
},
},
-})
+});
```
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
@@ -31,11 +31,11 @@ export default tseslint.config({
```js
// eslint.config.js
-import react from 'eslint-plugin-react'
+import react from "eslint-plugin-react";
export default tseslint.config({
// Set the react version
- settings: { react: { version: '18.3' } },
+ settings: { react: { version: "18.3" } },
plugins: {
// Add the react plugin
react,
@@ -44,7 +44,7 @@ export default tseslint.config({
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
- ...react.configs['jsx-runtime'].rules,
+ ...react.configs["jsx-runtime"].rules,
},
-})
+});
```
diff --git a/peer-prep/index.html b/peer-prep/index.html
index e4b78eae12..e0d1c84080 100644
--- a/peer-prep/index.html
+++ b/peer-prep/index.html
@@ -1,4 +1,4 @@
-
+
diff --git a/peer-prep/package-lock.json b/peer-prep/package-lock.json
index 06bce73901..839a6e5d84 100644
--- a/peer-prep/package-lock.json
+++ b/peer-prep/package-lock.json
@@ -10,8 +10,11 @@
"dependencies": {
"@mantine/core": "^7.12.2",
"@mantine/hooks": "^7.12.2",
+ "@mantine/notifications": "^7.13.0",
+ "@tabler/icons-react": "^3.17.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-quill": "^2.0.0",
"react-router-dom": "^6.26.2"
},
"devDependencies": {
@@ -28,7 +31,8 @@
"postcss-simple-vars": "^7.0.1",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.1",
- "vite": "^5.4.1"
+ "vite": "^5.4.1",
+ "vite-plugin-mkcert": "^1.17.6"
}
},
"node_modules/@ampproject/remapping": {
@@ -1019,10 +1023,9 @@
}
},
"node_modules/@mantine/core": {
- "version": "7.12.2",
- "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.12.2.tgz",
- "integrity": "sha512-FrMHOKq4s3CiPIxqZ9xnVX7H4PEGNmbtHMvWO/0YlfPgoV0Er/N/DNJOFW1ys4WSnidPTayYeB41riyxxGOpRQ==",
- "license": "MIT",
+ "version": "7.13.0",
+ "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.13.0.tgz",
+ "integrity": "sha512-aQpx3Q69ATDhVopBNkWS0sql93ZaPqeA5jTgqU7GxZvJdkpG87vbKYgp4cDV/gqr7BYu4kel0smeHYuPemiZ8Q==",
"dependencies": {
"@floating-ui/react": "^0.26.9",
"clsx": "^2.1.1",
@@ -1032,16 +1035,38 @@
"type-fest": "^4.12.0"
},
"peerDependencies": {
- "@mantine/hooks": "7.12.2",
+ "@mantine/hooks": "7.13.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
},
"node_modules/@mantine/hooks": {
- "version": "7.12.2",
- "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.12.2.tgz",
- "integrity": "sha512-dVMw8jpM0hAzc8e7/GNvzkk9N0RN/m+PKycETB3H6lJGuXJJSRR4wzzgQKpEhHwPccktDpvb4rkukKDq2jA8Fg==",
- "license": "MIT",
+ "version": "7.13.0",
+ "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.13.0.tgz",
+ "integrity": "sha512-oQpwSi0gajH3UR1DFa9MQ+zeYy75xbc8Im9jIIepLbiJXtIcPK+yll1BMxNwPQLYU1pYI6ZgUazI2PoykVNmsg==",
+ "peerDependencies": {
+ "react": "^18.2.0"
+ }
+ },
+ "node_modules/@mantine/notifications": {
+ "version": "7.13.0",
+ "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-7.13.0.tgz",
+ "integrity": "sha512-EEgKFxUK/4s2FKTYb6hKFbcsLwnccSe/8GsJ18DWGiLRxFq9DsgWCzfPAe+PVcGTITYfK7nbKxEF5l+KLdPOtQ==",
+ "dependencies": {
+ "@mantine/store": "7.13.0",
+ "react-transition-group": "4.4.5"
+ },
+ "peerDependencies": {
+ "@mantine/core": "7.13.0",
+ "@mantine/hooks": "7.13.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0"
+ }
+ },
+ "node_modules/@mantine/store": {
+ "version": "7.13.0",
+ "resolved": "https://registry.npmjs.org/@mantine/store/-/store-7.13.0.tgz",
+ "integrity": "sha512-ldYJGMcmqTxffQMCQZZWNtXKlG649S3BGM8ukeZ6FLZckVXLQAR2o+G5EkETNihh0sJKR7DVsYHltL5hyxYLkg==",
"peerDependencies": {
"react": "^18.2.0"
}
@@ -1084,6 +1109,161 @@
"node": ">= 8"
}
},
+ "node_modules/@octokit/auth-token": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz",
+ "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/core": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz",
+ "integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==",
+ "dev": true,
+ "dependencies": {
+ "@octokit/auth-token": "^4.0.0",
+ "@octokit/graphql": "^7.1.0",
+ "@octokit/request": "^8.3.1",
+ "@octokit/request-error": "^5.1.0",
+ "@octokit/types": "^13.0.0",
+ "before-after-hook": "^2.2.0",
+ "universal-user-agent": "^6.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/endpoint": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.5.tgz",
+ "integrity": "sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==",
+ "dev": true,
+ "dependencies": {
+ "@octokit/types": "^13.1.0",
+ "universal-user-agent": "^6.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/graphql": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.0.tgz",
+ "integrity": "sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ==",
+ "dev": true,
+ "dependencies": {
+ "@octokit/request": "^8.3.0",
+ "@octokit/types": "^13.0.0",
+ "universal-user-agent": "^6.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/openapi-types": {
+ "version": "22.2.0",
+ "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
+ "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==",
+ "dev": true
+ },
+ "node_modules/@octokit/plugin-paginate-rest": {
+ "version": "11.3.1",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.1.tgz",
+ "integrity": "sha512-ryqobs26cLtM1kQxqeZui4v8FeznirUsksiA+RYemMPJ7Micju0WSkv50dBksTuZks9O5cg4wp+t8fZ/cLY56g==",
+ "dev": true,
+ "dependencies": {
+ "@octokit/types": "^13.5.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "@octokit/core": "5"
+ }
+ },
+ "node_modules/@octokit/plugin-request-log": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-4.0.1.tgz",
+ "integrity": "sha512-GihNqNpGHorUrO7Qa9JbAl0dbLnqJVrV8OXe2Zm5/Y4wFkZQDfTreBzVmiRfJVfE4mClXdihHnbpyyO9FSX4HA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "@octokit/core": "5"
+ }
+ },
+ "node_modules/@octokit/plugin-rest-endpoint-methods": {
+ "version": "13.2.2",
+ "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.2.2.tgz",
+ "integrity": "sha512-EI7kXWidkt3Xlok5uN43suK99VWqc8OaIMktY9d9+RNKl69juoTyxmLoWPIZgJYzi41qj/9zU7G/ljnNOJ5AFA==",
+ "dev": true,
+ "dependencies": {
+ "@octokit/types": "^13.5.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "peerDependencies": {
+ "@octokit/core": "^5"
+ }
+ },
+ "node_modules/@octokit/request": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz",
+ "integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==",
+ "dev": true,
+ "dependencies": {
+ "@octokit/endpoint": "^9.0.1",
+ "@octokit/request-error": "^5.1.0",
+ "@octokit/types": "^13.1.0",
+ "universal-user-agent": "^6.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/request-error": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz",
+ "integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==",
+ "dev": true,
+ "dependencies": {
+ "@octokit/types": "^13.1.0",
+ "deprecation": "^2.0.0",
+ "once": "^1.4.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/rest": {
+ "version": "20.1.1",
+ "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.1.1.tgz",
+ "integrity": "sha512-MB4AYDsM5jhIHro/dq4ix1iWTLGToIGk6cWF5L6vanFaMble5jTX/UBQyiv05HsWnwUtY8JrfHy2LWfKwihqMw==",
+ "dev": true,
+ "dependencies": {
+ "@octokit/core": "^5.0.2",
+ "@octokit/plugin-paginate-rest": "11.3.1",
+ "@octokit/plugin-request-log": "^4.0.0",
+ "@octokit/plugin-rest-endpoint-methods": "13.2.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/@octokit/types": {
+ "version": "13.5.1",
+ "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.1.tgz",
+ "integrity": "sha512-F41lGiWBKPIWPBgjSvaDXTTQptBujnozENAK3S//nj7xsFdYdirImKlBB/hTjr+Vii68SM+8jG3UJWRa6DMuDA==",
+ "dev": true,
+ "dependencies": {
+ "@octokit/openapi-types": "^22.2.0"
+ }
+ },
"node_modules/@remix-run/router": {
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz",
@@ -1317,6 +1497,30 @@
"win32"
]
},
+ "node_modules/@tabler/icons": {
+ "version": "3.17.0",
+ "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.17.0.tgz",
+ "integrity": "sha512-sCSfAQ0w93KSnSL7tS08n73CdIKpuHP8foeLMWgDKiZaCs8ZE//N3ytazCk651ZtruTtByI3b+ZDj7nRf+hHvA==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/codecalm"
+ }
+ },
+ "node_modules/@tabler/icons-react": {
+ "version": "3.17.0",
+ "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.17.0.tgz",
+ "integrity": "sha512-Ndm9Htv7KpIU1PYYrzs5EMhyA3aZGcgaxUp9Q1XOxcRZ+I0X+Ub2WS5f4bkRyDdL1s0++k2T9XRgmg2pG113sw==",
+ "dependencies": {
+ "@tabler/icons": "3.17.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/codecalm"
+ },
+ "peerDependencies": {
+ "react": ">= 16"
+ }
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1376,6 +1580,15 @@
"devOptional": true,
"license": "MIT"
},
+ "node_modules/@types/quill": {
+ "version": "1.3.10",
+ "resolved": "https://registry.npmjs.org/@types/quill/-/quill-1.3.10.tgz",
+ "integrity": "sha512-IhW3fPW+bkt9MLNlycw8u8fWb7oO7W5URC9MfZYHBlA24rex9rs23D5DETChu1zvgVdc5ka64ICjJOgQMr6Shw==",
+ "license": "MIT",
+ "dependencies": {
+ "parchment": "^1.1.2"
+ }
+ },
"node_modules/@types/react": {
"version": "18.3.8",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.8.tgz",
@@ -1729,6 +1942,23 @@
"dev": true,
"license": "Python-2.0"
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true
+ },
+ "node_modules/axios": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz",
+ "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==",
+ "dev": true,
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1736,6 +1966,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/before-after-hook": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
+ "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==",
+ "dev": true
+ },
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -1793,6 +2029,25 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/call-bind": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
+ "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -1849,6 +2104,15 @@
"node": ">=4"
}
},
+ "node_modules/clone": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+ "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -1875,6 +2139,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1921,7 +2197,6 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/debug": {
@@ -1942,6 +2217,26 @@
}
}
},
+ "node_modules/deep-equal": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.2.tgz",
+ "integrity": "sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-arguments": "^1.1.1",
+ "is-date-object": "^1.0.5",
+ "is-regex": "^1.1.4",
+ "object-is": "^1.1.5",
+ "object-keys": "^1.1.1",
+ "regexp.prototype.flags": "^1.5.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -1949,12 +2244,70 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/deprecation": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz",
+ "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==",
+ "dev": true
+ },
"node_modules/detect-node-es": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT"
},
+ "node_modules/dom-helpers": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
+ "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
+ "dependencies": {
+ "@babel/runtime": "^7.8.7",
+ "csstype": "^3.0.2"
+ }
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.27",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.27.tgz",
@@ -1962,6 +2315,27 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/es-define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
+ "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.2.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
@@ -2287,6 +2661,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/eventemitter3": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz",
+ "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==",
+ "license": "MIT"
+ },
+ "node_modules/extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
+ "license": "MIT"
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -2294,6 +2680,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/fast-diff": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.1.2.tgz",
+ "integrity": "sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==",
+ "license": "Apache-2.0"
+ },
"node_modules/fast-glob": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@@ -2412,6 +2804,40 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
+ "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
+ "dev": true,
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -2427,6 +2853,24 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -2437,6 +2881,25 @@
"node": ">=6.9.0"
}
},
+ "node_modules/get-intrinsic": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
+ "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/get-nonce": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
@@ -2472,6 +2935,18 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
@@ -2489,6 +2964,69 @@
"node": ">=4"
}
},
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
+ "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -2535,6 +3073,37 @@
"loose-envify": "^1.0.0"
}
},
+ "node_modules/is-arguments": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
+ "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+ "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+ "license": "MIT",
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2578,6 +3147,22 @@
"node": ">=8"
}
},
+ "node_modules/is-regex": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+ "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -2691,6 +3276,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "license": "MIT"
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -2744,6 +3335,27 @@
"node": ">=8.6"
}
},
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -2797,6 +3409,48 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-is": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
+ "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -2847,6 +3501,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/parchment": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/parchment/-/parchment-1.1.4.tgz",
+ "integrity": "sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -3053,6 +3713,22 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "dev": true
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -3084,6 +3760,34 @@
],
"license": "MIT"
},
+ "node_modules/quill": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/quill/-/quill-1.3.7.tgz",
+ "integrity": "sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "clone": "^2.1.1",
+ "deep-equal": "^1.0.1",
+ "eventemitter3": "^2.0.3",
+ "extend": "^3.0.2",
+ "parchment": "^1.1.4",
+ "quill-delta": "^3.6.2"
+ }
+ },
+ "node_modules/quill-delta": {
+ "version": "3.6.3",
+ "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-3.6.3.tgz",
+ "integrity": "sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==",
+ "license": "MIT",
+ "dependencies": {
+ "deep-equal": "^1.0.1",
+ "extend": "^3.0.2",
+ "fast-diff": "1.1.2"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@@ -3109,6 +3813,11 @@
"react": "^18.3.1"
}
},
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ },
"node_modules/react-number-format": {
"version": "5.4.2",
"resolved": "https://registry.npmjs.org/react-number-format/-/react-number-format-5.4.2.tgz",
@@ -3119,6 +3828,21 @@
"react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/react-quill": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz",
+ "integrity": "sha512-4qQtv1FtCfLgoD3PXAur5RyxuUbPXQGOHgTlFie3jtxp43mXDtzCKaOgQ3mLyZfi1PUlyjycfivKelFhy13QUg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/quill": "^1.3.10",
+ "lodash": "^4.17.4",
+ "quill": "^1.3.7"
+ },
+ "peerDependencies": {
+ "react": "^16 || ^17 || ^18",
+ "react-dom": "^16 || ^17 || ^18"
+ }
+ },
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
@@ -3248,12 +3972,45 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
+ "node_modules/react-transition-group": {
+ "version": "4.4.5",
+ "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
+ "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
+ "dependencies": {
+ "@babel/runtime": "^7.5.5",
+ "dom-helpers": "^5.0.1",
+ "loose-envify": "^1.4.0",
+ "prop-types": "^15.6.2"
+ },
+ "peerDependencies": {
+ "react": ">=16.6.0",
+ "react-dom": ">=16.6.0"
+ }
+ },
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
},
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
+ "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.6",
+ "define-properties": "^1.2.1",
+ "es-errors": "^1.3.0",
+ "set-function-name": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -3354,6 +4111,38 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+ "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -3561,6 +4350,12 @@
}
}
},
+ "node_modules/universal-user-agent": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
+ "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
+ "dev": true
+ },
"node_modules/update-browserslist-db": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
@@ -3752,6 +4547,24 @@
}
}
},
+ "node_modules/vite-plugin-mkcert": {
+ "version": "1.17.6",
+ "resolved": "https://registry.npmjs.org/vite-plugin-mkcert/-/vite-plugin-mkcert-1.17.6.tgz",
+ "integrity": "sha512-4JR1RN0HEg/w17eRQJ/Ve2pSa6KCVQcQO6yKtIaKQCFDyd63zGfXHWpygBkvvRSpqa0GcqNKf0fjUJ0HiJQXVQ==",
+ "dev": true,
+ "dependencies": {
+ "@octokit/rest": "^20.1.1",
+ "axios": "^1.7.4",
+ "debug": "^4.3.6",
+ "picocolors": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=v16.7.0"
+ },
+ "peerDependencies": {
+ "vite": ">=3"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -3778,6 +4591,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
diff --git a/peer-prep/package.json b/peer-prep/package.json
index 6c57046897..504932e590 100644
--- a/peer-prep/package.json
+++ b/peer-prep/package.json
@@ -12,8 +12,11 @@
"dependencies": {
"@mantine/core": "^7.12.2",
"@mantine/hooks": "^7.12.2",
+ "@mantine/notifications": "^7.13.0",
+ "@tabler/icons-react": "^3.17.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
+ "react-quill": "^2.0.0",
"react-router-dom": "^6.26.2"
},
"devDependencies": {
@@ -30,6 +33,7 @@
"postcss-simple-vars": "^7.0.1",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.1",
- "vite": "^5.4.1"
+ "vite": "^5.4.1",
+ "vite-plugin-mkcert": "^1.17.6"
}
}
diff --git a/peer-prep/postcss.config.cjs b/peer-prep/postcss.config.cjs
index ba1531a83f..bfba0ddfae 100644
--- a/peer-prep/postcss.config.cjs
+++ b/peer-prep/postcss.config.cjs
@@ -1,14 +1,14 @@
module.exports = {
- plugins: {
- 'postcss-preset-mantine': {},
- 'postcss-simple-vars': {
- variables: {
- 'mantine-breakpoint-xs': '36em',
- 'mantine-breakpoint-sm': '48em',
- 'mantine-breakpoint-md': '62em',
- 'mantine-breakpoint-lg': '75em',
- 'mantine-breakpoint-xl': '88em',
- },
- },
+ plugins: {
+ 'postcss-preset-mantine': {},
+ 'postcss-simple-vars': {
+ variables: {
+ 'mantine-breakpoint-xs': '36em',
+ 'mantine-breakpoint-sm': '48em',
+ 'mantine-breakpoint-md': '62em',
+ 'mantine-breakpoint-lg': '75em',
+ 'mantine-breakpoint-xl': '88em',
+ },
},
+ },
};
diff --git a/peer-prep/src/App.tsx b/peer-prep/src/App.tsx
index 82b54e3e96..4bd75c5904 100644
--- a/peer-prep/src/App.tsx
+++ b/peer-prep/src/App.tsx
@@ -1,20 +1,23 @@
-import '@mantine/core/styles.css';
-import { MantineProvider } from '@mantine/core';
-import { Routes, Route } from 'react-router-dom';
-import Home from './pages/Home';
-import Login from './pages/Login';
+// import "@mantine/core/styles.css";
+// import { MantineProvider } from "@mantine/core";
+// import { Routes, Route } from "react-router-dom";
+// import Home from "./pages/Home";
+// import Login from "./pages/Login";
-function App() {
+// function App() {
+// return (
+//
+// {" "}
+// {
+//
+//
+// }>
+// }>
+//
+//
+// }
+//
+// );
+// }
- return {
-
- }
-}
-
-export default App
+// export default App;
diff --git a/peer-prep/src/assets/loginimage.svg b/peer-prep/src/assets/loginimage.svg
new file mode 100644
index 0000000000..a99d170c3f
--- /dev/null
+++ b/peer-prep/src/assets/loginimage.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/peer-prep/src/assets/searchimage.svg b/peer-prep/src/assets/searchimage.svg
new file mode 100644
index 0000000000..d7f4e6d0ce
--- /dev/null
+++ b/peer-prep/src/assets/searchimage.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/peer-prep/src/components/ApplicationWrapper.tsx b/peer-prep/src/components/ApplicationWrapper.tsx
new file mode 100644
index 0000000000..d934e5b9ab
--- /dev/null
+++ b/peer-prep/src/components/ApplicationWrapper.tsx
@@ -0,0 +1,15 @@
+import { Outlet } from "react-router-dom";
+import { Navbar } from "./Navbar/Navbar";
+import { AuthProvider } from "../hooks/useAuth";
+
+// https://stackoverflow.com/questions/70833727/using-react-router-v6-i-need-a-navbar-to-permanently-be-there-but-cant-display
+export default function ApplicationWrapper() {
+ return (
+
+ );
+}
diff --git a/peer-prep/src/components/AvatarIcon/AvatarWithDetailsButton.tsx b/peer-prep/src/components/AvatarIcon/AvatarWithDetailsButton.tsx
new file mode 100644
index 0000000000..c6ebf855d5
--- /dev/null
+++ b/peer-prep/src/components/AvatarIcon/AvatarWithDetailsButton.tsx
@@ -0,0 +1,42 @@
+import { forwardRef } from "react";
+import { IconChevronRight } from "@tabler/icons-react";
+import { Group, Avatar, Text, Menu, UnstyledButton } from "@mantine/core";
+
+interface UserButtonProps extends React.ComponentPropsWithoutRef<"button"> {
+ image: string;
+ name: string;
+ email: string;
+ icon?: React.ReactNode;
+}
+
+const AvatarWithDetailsButton = forwardRef(
+ ({ image, name, email, icon, ...others }: UserButtonProps, ref) => (
+
+
+
+
+
+
+ {name}
+
+
+
+ {email}
+
+
+
+ {icon || }
+
+
+ )
+);
+
+export default AvatarWithDetailsButton;
diff --git a/peer-prep/src/components/Navbar/Navbar.module.css b/peer-prep/src/components/Navbar/Navbar.module.css
new file mode 100644
index 0000000000..de85cf8bd8
--- /dev/null
+++ b/peer-prep/src/components/Navbar/Navbar.module.css
@@ -0,0 +1,62 @@
+.icon {
+ font-size: larger;
+ font-weight: 700;
+}
+
+.header {
+ height: rem(60px);
+ padding-left: var(--mantine-spacing-md);
+ padding-right: var(--mantine-spacing-md);
+ border-bottom: rem(1px) solid
+ light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
+}
+
+.link {
+ display: flex;
+ align-items: center;
+ height: 100%;
+ padding-left: var(--mantine-spacing-md);
+ padding-right: var(--mantine-spacing-md);
+ text-decoration: none;
+ color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
+ font-weight: 500;
+ font-size: var(--mantine-font-size-sm);
+
+ @media (max-width: $mantine-breakpoint-sm) {
+ height: rem(42px);
+ width: 100%;
+ }
+
+ @mixin hover {
+ background-color: light-dark(
+ var(--mantine-color-gray-0),
+ var(--mantine-color-dark-6)
+ );
+ }
+}
+
+.subLink {
+ width: 100%;
+ padding: var(--mantine-spacing-xs) var(--mantine-spacing-md);
+ border-radius: var(--mantine-radius-md);
+
+ @mixin hover {
+ background-color: light-dark(
+ var(--mantine-color-gray-0),
+ var(--mantine-color-dark-7)
+ );
+ }
+}
+
+.dropdownFooter {
+ background-color: light-dark(
+ var(--mantine-color-gray-0),
+ var(--mantine-color-dark-7)
+ );
+ margin: calc(var(--mantine-spacing-md) * -1);
+ margin-top: var(--mantine-spacing-sm);
+ padding: var(--mantine-spacing-md) calc(var(--mantine-spacing-md) * 2);
+ padding-bottom: var(--mantine-spacing-xl);
+ border-top: rem(1px) solid
+ light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
+}
diff --git a/peer-prep/src/components/Navbar/Navbar.tsx b/peer-prep/src/components/Navbar/Navbar.tsx
new file mode 100644
index 0000000000..b5c87a388a
--- /dev/null
+++ b/peer-prep/src/components/Navbar/Navbar.tsx
@@ -0,0 +1,339 @@
+import {
+ HoverCard,
+ Group,
+ Button,
+ UnstyledButton,
+ Text,
+ SimpleGrid,
+ ThemeIcon,
+ Anchor,
+ Divider,
+ Center,
+ Box,
+ Burger,
+ Drawer,
+ Collapse,
+ ScrollArea,
+ rem,
+ useMantineTheme,
+ Menu,
+} from "@mantine/core";
+// import { MantineLogo } from "@mantinex/mantine-logo";
+import { useDisclosure } from "@mantine/hooks";
+import {
+ IconNotification,
+ IconCode,
+ IconBook,
+ IconChartPie3,
+ IconFingerprint,
+ IconCoin,
+ IconChevronDown,
+ IconArrowsLeftRight,
+ IconMessageCircle,
+ IconPhoto,
+ IconSearch,
+ IconSettings,
+ IconTrash,
+ IconLogout,
+ IconHome,
+} from "@tabler/icons-react";
+import classes from "./Navbar.module.css";
+import { Link } from "react-router-dom";
+import { useAuth } from "../../hooks/useAuth";
+import AvatarWithDetailsButton from "../AvatarIcon/AvatarWithDetailsButton";
+
+const mockdata = [
+ {
+ icon: IconCode,
+ title: "Open source",
+ description: "This Pokémon’s cry is very loud and distracting",
+ },
+ {
+ icon: IconCoin,
+ title: "Free for everyone",
+ description: "The fluid of Smeargle’s tail secretions changes",
+ },
+ {
+ icon: IconBook,
+ title: "Documentation",
+ description: "Yanma is capable of seeing 360 degrees without",
+ },
+ {
+ icon: IconFingerprint,
+ title: "Security",
+ description: "The shell’s rounded shape and the grooves on its.",
+ },
+ {
+ icon: IconChartPie3,
+ title: "Analytics",
+ description: "This Pokémon uses its flying ability to quickly chase",
+ },
+ {
+ icon: IconNotification,
+ title: "Notifications",
+ description: "Combusken battles with the intensely hot flames it spews",
+ },
+];
+
+export function Navbar() {
+ const [drawerOpened, { toggle: toggleDrawer, close: closeDrawer }] =
+ useDisclosure(false);
+ const [linksOpened, { toggle: toggleLinks }] = useDisclosure(false);
+ const theme = useMantineTheme();
+
+ const links = mockdata.map((item) => (
+
+
+
+
+
+
+
+ {item.title}
+
+
+ {item.description}
+
+
+
+
+ ));
+
+ const { user, logout } = useAuth();
+
+ return (
+
+
+
+ {/* */}
+ 🫂 PeerPrep
+
+
+
+ Home
+
+
+ {/*
+
+
+
+
+ Features
+
+
+
+
+
+
+
+
+ Features
+
+ View all
+
+
+
+
+
+
+ {links}
+
+
+
+
+
+
+ Get started
+
+
+ Their food sources have decreased, and their numbers
+
+
+ Get started
+
+
+
+ */}
+
+ Learn
+
+
+
+ Academy
+
+
+ Questions
+
+
+
+ {user ? (
+
+
+
+
+
+
+ Application
+
+ }
+ >
+
+ Dashboard
+
+
+
+
+ }
+ >
+ Messages
+
+ {/*
+ }
+ >
+ Gallery
+ */}
+
+
+ }
+ rightSection={
+
+ ⌘K
+
+ }
+ >
+ Search
+
+
+
+
+
+ }
+ >
+ Settings
+
+
+ {/* Danger zone
+
+ }
+ >
+ Transfer my data
+ */}
+ {/*
+ }
+ >
+ Delete my account
+ */}
+
+
+ }
+ onClick={logout}
+ >
+ Logout
+
+
+
+ ) : (
+
+
+ Log in
+
+
+ Sign up
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ Home
+
+ {/*
+
+
+ Features
+
+
+
+ */}
+ {links}
+
+ Learn
+
+
+
+ Academy
+
+
+
+
+
+
+ Log in
+
+ Sign up
+
+
+
+
+ );
+}
diff --git a/peer-prep/src/components/Questions/Category/Category.module.css b/peer-prep/src/components/Questions/Category/Category.module.css
new file mode 100644
index 0000000000..b3e0c0125a
--- /dev/null
+++ b/peer-prep/src/components/Questions/Category/Category.module.css
@@ -0,0 +1,13 @@
+.category {
+ background-color: light-dark(var(--mantine-color-blue-2), "blue");
+ padding: 0.25rem 0.5rem;
+ display: flex;
+ border-radius: var(--mantine-radius-sm);
+ align-items: center;
+ justify-content: center;
+}
+
+.category-text {
+ font-size: var(--mantine-font-size-xs);
+ text-transform: uppercase;
+}
diff --git a/peer-prep/src/components/Questions/Category/Category.tsx b/peer-prep/src/components/Questions/Category/Category.tsx
new file mode 100644
index 0000000000..46767d9d86
--- /dev/null
+++ b/peer-prep/src/components/Questions/Category/Category.tsx
@@ -0,0 +1,14 @@
+import { Box, Text } from "@mantine/core";
+
+import classes from "./Category.module.css";
+
+export default function CategoryDisplay({ category }: { category: string }) {
+ // based on the category prop, choose a color based on the first two letters
+ // of the category
+
+ return (
+
+ {category}
+
+ );
+}
diff --git a/peer-prep/src/components/Questions/Complexity/Complexity.tsx b/peer-prep/src/components/Questions/Complexity/Complexity.tsx
new file mode 100644
index 0000000000..2cfae6d94d
--- /dev/null
+++ b/peer-prep/src/components/Questions/Complexity/Complexity.tsx
@@ -0,0 +1,19 @@
+import { Rating } from "@mantine/core";
+import { Complexity } from "../../../types/question";
+
+export default function ComplexityDisplay({
+ complexity,
+}: {
+ complexity: Complexity;
+}) {
+ let rating = 0;
+ if (complexity == "EASY") {
+ rating = 1;
+ } else if (complexity == "MEDIUM") {
+ rating = 2;
+ } else if (complexity == "HARD") {
+ rating = 3;
+ }
+
+ return ;
+}
diff --git a/peer-prep/src/components/Questions/QuestionCard/QuestionCard.module.css b/peer-prep/src/components/Questions/QuestionCard/QuestionCard.module.css
new file mode 100644
index 0000000000..74e740f32e
--- /dev/null
+++ b/peer-prep/src/components/Questions/QuestionCard/QuestionCard.module.css
@@ -0,0 +1,32 @@
+.card {
+ max-width: 320px;
+ padding: 1rem;
+ border-radius: var(--mantine-radius-md);
+ border: rem(1px) solid
+ light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-7));
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ white-space: normal;
+}
+
+.contents {
+ height: 300px;
+ width: 280px;
+}
+
+.title {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ height: 60px;
+ max-width: 100%;
+}
+
+.description {
+ white-space: normal;
+ overflow-wrap: break-word;
+ height: 180px;
+ max-width: 100%;
+ white-space: "pre-wrap";
+ flex: 1,
+}
diff --git a/peer-prep/src/components/Questions/QuestionCard/QuestionCard.tsx b/peer-prep/src/components/Questions/QuestionCard/QuestionCard.tsx
new file mode 100644
index 0000000000..2cc8e91d1c
--- /dev/null
+++ b/peer-prep/src/components/Questions/QuestionCard/QuestionCard.tsx
@@ -0,0 +1,68 @@
+import {
+ Badge,
+ Box,
+ Button,
+ Divider,
+ Flex,
+ Group,
+ rem,
+ Stack,
+ Text,
+ Title,
+} from "@mantine/core";
+import { QuestionOlsd } from "../../../types/question";
+
+import classes from "./QuestionCard.module.css";
+import ComplexityDisplay from "../Complexity/Complexity";
+import { IconBrandLeetcode } from "@tabler/icons-react";
+import { Link, useNavigate } from "react-router-dom";
+import CategoryDisplay from "../Category/Category";
+
+interface QuestionCardProps {
+ question: QuestionOlsd | any;
+ isClickable?: boolean; // New prop to control click behavior
+}
+
+export default function QuestionCard({
+ question,
+ isClickable = false, // Default to false
+}: QuestionCardProps) {
+ const navigate = useNavigate();
+
+ const handleCardClick = () => {
+ if (isClickable) {
+ navigate(`/questions/edit/${question._id}`);
+ }
+ }
+
+ return (
+
+
+
+ {question.title}
+
+
+ {question.description.testDescription }
+
+
+ {question.categories.map((category, index) => (
+
+ ))}
+
+
+
+
+ {
+ window.open(question.link, "_blank", "noopener,noreferrer");
+ }}
+ >
+
+
+
+
+
+ );
+}
diff --git a/peer-prep/src/hooks/useApi.tsx b/peer-prep/src/hooks/useApi.tsx
new file mode 100644
index 0000000000..e28dc207f9
--- /dev/null
+++ b/peer-prep/src/hooks/useApi.tsx
@@ -0,0 +1,100 @@
+import { useState } from "react";
+import { User } from "../types/user";
+import { useLocalStorage } from "@mantine/hooks";
+import { useAuth } from "./useAuth";
+import { useNavigate } from "react-router-dom";
+
+export interface QuestionServerResponse {
+ success: boolean;
+ status: number;
+ data: T;
+ message?: string;
+}
+
+export interface UserServerResponse {
+ user?: T;
+ message: string;
+}
+
+// export interface ServerError {
+// message: string;
+// }
+
+export enum SERVICE {
+ USER,
+ QUESTION,
+}
+
+export default function useApi() {
+ // const [data, setData] = useState(null);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const navigate = useNavigate();
+
+ const [user, setUser] = useLocalStorage({
+ key: "user",
+ defaultValue: null,
+ });
+
+ async function fetchData(
+ url: string,
+ service: SERVICE,
+ options?: RequestInit,
+ suppressWarning?: boolean
+ ) {
+ setIsLoading(true);
+ try {
+ // todo make this nicer...
+ const response = await fetch(
+ `${
+ service === SERVICE.USER
+ ? import.meta.env.VITE_API_URL_USER
+ : import.meta.env.VITE_API_URL_QUESTION
+ }${url}`,
+ {
+ ...options,
+ headers: {
+ ...options?.headers,
+ // "x-api-key": import.meta.env.VITE_API_KEY as string,
+ // bearer token
+ // "Authorization": `Bearer ${accessToken}`,
+ },
+ credentials: "include",
+ }
+ );
+
+ // if the response status indicates not logged in, clear data and redirect to login
+ // if (response.status === 401) {
+ // //
+ // //
+ // return;
+ // }
+
+ if (response.status === 401) {
+ setUser(null);
+ navigate("/login");
+ }
+
+ const data: Q = await response.json();
+
+ if (!response.ok) {
+ console.log("RESPONSE NOT OK!");
+ // @ts-ignore
+ throw data;
+ }
+
+ // setData(data);
+ return data;
+ } catch (error) {
+ setError(error);
+
+ // throw error again
+ throw error;
+ } finally {
+ setIsLoading(false);
+ }
+ }
+
+ return { isLoading, error, fetchData };
+}
diff --git a/peer-prep/src/hooks/useAuth.tsx b/peer-prep/src/hooks/useAuth.tsx
new file mode 100644
index 0000000000..b5322b55cc
--- /dev/null
+++ b/peer-prep/src/hooks/useAuth.tsx
@@ -0,0 +1,170 @@
+import { useLocalStorage } from "@mantine/hooks";
+import { createContext, ReactElement, useContext, useMemo } from "react";
+import { useNavigate } from "react-router-dom";
+import { User } from "../types/user";
+import useApi, { SERVICE, UserServerResponse } from "./useApi";
+import { notifications } from "@mantine/notifications";
+
+export interface AuthContextType {
+ user: User | null;
+ login: (data: any) => void;
+ logout: () => void;
+ register: (data: any) => void;
+}
+
+const DEFAULT_TEMP_USER: User = {
+ email: "johndoe@gmail.com",
+ displayName: "John Doe",
+ isAdmin: false,
+ id: "123",
+ // password: "password",
+};
+
+const DEFAULT: AuthContextType = {
+ user: DEFAULT_TEMP_USER, // TODO: switch this out later
+ login: () => {
+ console.log("log");
+ },
+ logout: () => {},
+ register: () => {},
+};
+const AuthContext = createContext(DEFAULT);
+
+export const AuthProvider = ({
+ children,
+}: {
+ children: ReactElement | ReactElement[];
+}) => {
+ const [user, setUser] = useLocalStorage({
+ key: "user",
+ defaultValue: null,
+ });
+
+ const { fetchData, isLoading, error } = useApi();
+ const navigate = useNavigate();
+
+ // call this function when you want to authenticate the user
+ const login = async (data: { email: string; password: string }) => {
+ fetchData>(
+ `/user-service/users/login`,
+ SERVICE.USER,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(data),
+ }
+ )
+ .then((data) => {
+ // ok!
+ setUser(data.user || null);
+ console.log(data.user, "<<<<");
+ navigate("/dashboard", { replace: true });
+
+ notifications.show({
+ message: "Welcome to PeerPrep!",
+ title: "Login successful",
+ color: "green",
+ });
+ })
+ .catch((e) => {
+ console.log("ERROR:: Login failed", e);
+
+ notifications.show({
+ message: e.message || "Login failed",
+ title: "Error - Login failed!",
+ color: "red",
+ });
+ });
+ };
+
+ const register = async (data: {
+ email: string;
+ password: string;
+ displayName: string;
+ }) => {
+ fetchData(`/user-service/users`, SERVICE.USER, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(data),
+ })
+ .then((_) => {
+ // ok!
+ notifications.show({
+ message: "Registration successful",
+ color: "green",
+ });
+
+ // need to login to set cookie
+ login({
+ email: data.email,
+ password: data.password,
+ });
+ return;
+ })
+ .catch((e) => {
+ console.error("ERROR:: Registration failed");
+
+ notifications.show({
+ message: e.message || "Registration failed",
+ title: "Error - Registration failed!",
+ color: "red",
+ });
+ });
+ // const response = await fetch(`/user-service/users`);
+
+ // if (!response.ok) {
+ // console.error("ERROR:: Registration failed");
+ // return;
+ // }
+
+ // if (response.status === 201) {
+ // console.log("INFO:: Registration successful");
+
+ // // login the user
+ // }
+ };
+
+ // call this function to sign out logged in user
+ const logout = () => {
+ fetchData(`/user-service/users/logout`, SERVICE.USER, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({}),
+ })
+ .then((_) => {
+ // ok!
+ setUser(null);
+ navigate("/", { replace: true });
+
+ notifications.show({
+ message: "Logged out",
+ title: "Success",
+ color: "green",
+ });
+ })
+ .catch((e) => {
+ console.error("ERROR:: Logout failed");
+ });
+ };
+
+ const value = useMemo(
+ () => ({
+ user,
+ login,
+ logout,
+ register,
+ }),
+ [user]
+ );
+ return {children} ;
+};
+
+export const useAuth = () => {
+ return useContext(AuthContext);
+};
diff --git a/peer-prep/src/index.css b/peer-prep/src/index.css
index e69de29bb2..b59b020c4f 100644
--- a/peer-prep/src/index.css
+++ b/peer-prep/src/index.css
@@ -0,0 +1,12 @@
+/* always scrollbar */
+html {
+ overflow-y: scroll;
+}
+
+.ql-toolbar.ql-snow {
+ border-top: none !important;
+ border-left: none !important;
+ border-right: none !important;
+ /* border-bottom: 1px solid var(--mantine-color-gray-2) !important; */
+ border-bottom: none !important;
+}
diff --git a/peer-prep/src/main.tsx b/peer-prep/src/main.tsx
index f97de2eb89..739abc18b6 100644
--- a/peer-prep/src/main.tsx
+++ b/peer-prep/src/main.tsx
@@ -1,13 +1,133 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import App from './App.tsx'
-import './index.css'
-import { BrowserRouter } from 'react-router-dom'
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+// import App from "./App.tsx";
+import "./index.css";
+import {
+ BrowserRouter,
+ createBrowserRouter,
+ RouterProvider,
+} from "react-router-dom";
+import Home from "./pages/Home/HomePage.tsx";
+import LoginOrRegisterPage from "./pages/Login/LoginPage.tsx";
+import { Button, createTheme, MantineProvider } from "@mantine/core";
+import "@mantine/core/styles.css";
+import "@mantine/notifications/styles.css";
-createRoot(document.getElementById('root')!).render(
+import ApplicationWrapper from "./components/ApplicationWrapper.tsx";
+import ProtectedRouteWrapper from "./pages/ProtectedRouteWrapper.tsx";
+import { AuthProvider } from "./hooks/useAuth.tsx";
+import DashboardPage from "./pages/Dashboard/DashboardPage.tsx";
+import SearchingPage from "./pages/Session/Search/SearchingPage.tsx";
+import CreateSessionPage from "./pages/Session/Create/CreateSessionPage.tsx";
+import QuestionPage from "./pages/Questions/QuestionPage.tsx";
+import CreateQuestionPage from "./pages/Questions/CreateQuestionPage/CreateQuestionPage.tsx";
+import EditQuestionPage from "./pages/Questions/EditQuestionPage/EditQuestionPage.tsx";
+import { Notifications } from "@mantine/notifications";
+import AdminRouteWrapper from "./pages/AdminRouteWrapper.tsx";
+const router = createBrowserRouter([
+ {
+ path: "/",
+ element: ,
+ children: [
+ {
+ path: "/",
+ element: ,
+ },
+ {
+ path: "/login",
+ element: ,
+ },
+
+ // Protected routes below
+ {
+ path: "/",
+ element: ,
+ children: [
+ // admin-only routes
+ {
+ path: "/",
+ element: ,
+ children: [
+ {
+ path: "/questions",
+ children: [
+ {
+ path: "/questions",
+ element: ,
+ loader: async () =>
+ fetch(
+ `${
+ import.meta.env.VITE_API_URL_QUESTION
+ }/question-service`
+ ),
+ // fetch(
+ // `https://virtserver.swaggerhub.com/PeerPrep/question-service/1.0.0/api/question-service`
+ // ),
+ },
+ {
+ path: "/questions/create",
+ element: ,
+ },
+ {
+ path: "/questions/edit/:id",
+ element: ,
+ },
+ ],
+ },
+ ],
+ },
+
+ {
+ path: "/dashboard",
+ element: ,
+ },
+ {
+ path: "/learn",
+ element: learn! ,
+ },
+
+ {
+ path: "/session",
+ children: [
+ {
+ path: "/session/create",
+ element: ,
+ // loader: async () => {
+ // return await Promise.all([
+ // fetch(
+ // `${
+ // import.meta.env.VITE_API_URL
+ // }/question-service/categories`
+ // ),
+ // ]);
+ // },
+ },
+ {
+ path: "/session/search",
+ element: ,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+]);
+
+const theme = createTheme({
+ fontFamily: "Montserrat, sans-serif",
+ defaultRadius: "md",
+ cursorType: "pointer",
+});
+
+createRoot(document.getElementById("root")!).render(
-
+
+
+
+
+ {/*
-
- ,
-)
+ */}
+
+);
diff --git a/peer-prep/src/pages/AdminRouteWrapper.tsx b/peer-prep/src/pages/AdminRouteWrapper.tsx
new file mode 100644
index 0000000000..8efd22e8bf
--- /dev/null
+++ b/peer-prep/src/pages/AdminRouteWrapper.tsx
@@ -0,0 +1,28 @@
+import { Navigate, Outlet, useNavigate } from "react-router-dom";
+import { useAuth } from "../hooks/useAuth";
+import { notifications } from "@mantine/notifications";
+
+// https://stackoverflow.com/questions/70833727/using-react-router-v6-i-need-a-navbar-to-permanently-be-there-but-cant-display
+export default function AdminRouteWrapper() {
+ const { user } = useAuth();
+ const navigate = useNavigate();
+ if (!user || !user.isAdmin) {
+ // user is not admin
+ // show an error admin message
+ notifications.show({
+ message: "You are not authorized to view this page!",
+ title: "Authorization error!",
+ color: "red",
+ });
+
+ // navigate to previous page
+ // navigate(-1);
+ // return;
+ return ;
+ }
+ return (
+
+
+
+ );
+}
diff --git a/peer-prep/src/pages/Dashboard/DashboardPage.module.css b/peer-prep/src/pages/Dashboard/DashboardPage.module.css
new file mode 100644
index 0000000000..63a6c4e2cd
--- /dev/null
+++ b/peer-prep/src/pages/Dashboard/DashboardPage.module.css
@@ -0,0 +1,7 @@
+.question-list {
+ display: flex;
+ /* flex-direction: column; */
+ flex-wrap: wrap;
+ gap: 1rem;
+ margin-top: 1rem;
+}
diff --git a/peer-prep/src/pages/Dashboard/DashboardPage.tsx b/peer-prep/src/pages/Dashboard/DashboardPage.tsx
new file mode 100644
index 0000000000..9359d94fab
--- /dev/null
+++ b/peer-prep/src/pages/Dashboard/DashboardPage.tsx
@@ -0,0 +1,54 @@
+import {
+ Avatar,
+ Box,
+ Button,
+ Center,
+ Container,
+ Flex,
+ Group,
+ SimpleGrid,
+ Stack,
+ Text,
+ Title,
+} from "@mantine/core";
+import { useAuth } from "../../hooks/useAuth";
+import QuestionCard from "../../components/Questions/QuestionCard/QuestionCard";
+import { SAMPLE_QUESTIONS } from "../../types/question";
+
+import classes from "./DashboardPage.module.css";
+import { Link } from "react-router-dom";
+export default function DashboardPage() {
+ const { user } = useAuth();
+
+ return (
+ <>
+
+
+
+
+
+ {user?.displayName}
+ {user?.email}
+
+
+
+ Start a session
+
+
+
+
+
+
+ {/* */}
+
+
+ {SAMPLE_QUESTIONS.map((question, key) => (
+
+ ))}
+
+
+ {/* */}
+
+ >
+ );
+}
diff --git a/peer-prep/src/pages/Home.tsx b/peer-prep/src/pages/Home.tsx
deleted file mode 100644
index bcbbe36098..0000000000
--- a/peer-prep/src/pages/Home.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-export default function Home() {
- return (
-
-
Peer-Prep Home Page
-
- );
-}
\ No newline at end of file
diff --git a/peer-prep/src/pages/Home/HomePage.module.css b/peer-prep/src/pages/Home/HomePage.module.css
new file mode 100644
index 0000000000..9887a97f4a
--- /dev/null
+++ b/peer-prep/src/pages/Home/HomePage.module.css
@@ -0,0 +1,25 @@
+.header {
+ min-height: calc(100vh - 60px - var(--mantine-spacing-md));
+ /* background-color: var(--mantine-color-gray-0); */
+}
+
+.header-content {
+ min-height: 100vh;
+}
+.title {
+ text-align: center;
+ font-size: 24rem;
+}
+
+.description {
+ text-align: center;
+}
+
+.wrapper {
+ padding-top: calc(var(--mantine-spacing-xl) * 4);
+ padding-bottom: calc(var(--mantine-spacing-xl) * 4);
+}
+
+.shaded {
+ background-color: var(--mantine-color-gray-0);
+}
diff --git a/peer-prep/src/pages/Home/HomePage.tsx b/peer-prep/src/pages/Home/HomePage.tsx
new file mode 100644
index 0000000000..437f01acf1
--- /dev/null
+++ b/peer-prep/src/pages/Home/HomePage.tsx
@@ -0,0 +1,165 @@
+import {
+ Box,
+ Button,
+ Center,
+ Container,
+ Group,
+ rem,
+ SimpleGrid,
+ Stack,
+ Text,
+ ThemeIcon,
+ Title,
+} from "@mantine/core";
+import {
+ IconGauge,
+ IconCookie,
+ IconUser,
+ IconMessage2,
+ IconLock,
+} from "@tabler/icons-react";
+import classes from "./HomePage.module.css";
+
+export const MOCKDATA = [
+ {
+ icon: IconGauge,
+ title: "Extreme performance",
+ description:
+ "This dust is actually a powerful poison that will even make a pro wrestler sick, Regice cloaks itself with frigid air of -328 degrees Fahrenheit",
+ },
+ {
+ icon: IconUser,
+ title: "Privacy focused",
+ description:
+ "People say it can run at the same speed as lightning striking, Its icy body is so cold, it will not melt even if it is immersed in magma",
+ },
+ {
+ icon: IconCookie,
+ title: "No third parties",
+ description:
+ "They’re popular, but they’re rare. Trainers who show them off recklessly may be targeted by thieves",
+ },
+ {
+ icon: IconLock,
+ title: "Secure by default",
+ description:
+ "Although it still can’t fly, its jumping power is outstanding, in Alola the mushrooms on Paras don’t grow up quite right",
+ },
+ {
+ icon: IconMessage2,
+ title: "24/7 Support",
+ description:
+ "Rapidash usually can be seen casually cantering in the fields and plains, Skitty is known to chase around after its own tail",
+ },
+];
+
+interface FeatureProps {
+ icon: React.FC;
+ title: React.ReactNode;
+ description: React.ReactNode;
+}
+
+export default function Home() {
+ return (
+ <>
+
+
+
+
+
+ Welcome to PeerPrep!
+
+
+ {" "}
+ At PeerPrep, we want you to ace your coding interviews. But,
+ doing it alone is boring. Find someone to collaborate with on a
+ coding problem, with test cases, timed submission, and more!{" "}
+
+
+
+
+ Get started ➜
+
+
+
+
+
+
+
+
+
+
+ Integrate effortlessly with any technology stack
+
+
+
+
+ Every once in a while, you’ll see a Golbat that’s missing some
+ fangs. This happens when hunger drives it to try biting a
+ Steel-type Pokémon.
+
+
+
+
+ {MOCKDATA.map(Feature)}
+
+
+
+
+ >
+ );
+}
+
+export function Feature(
+ { icon: Icon, title, description }: FeatureProps,
+ index: number
+) {
+ return (
+
+
+
+
+
+ {title}
+
+
+ {description}
+
+
+ );
+}
+
+export function FeaturesGrid() {
+ const features = MOCKDATA.map((feature, index) => (
+
+ ));
+
+ return (
+
+
+ Integrate effortlessly with any technology stack
+
+
+
+
+ Every once in a while, you’ll see a Golbat that’s missing some fangs.
+ This happens when hunger drives it to try biting a Steel-type Pokémon.
+
+
+
+
+ {features}
+
+
+ );
+}
diff --git a/peer-prep/src/pages/Login.tsx b/peer-prep/src/pages/Login.tsx
deleted file mode 100644
index 8519d930f6..0000000000
--- a/peer-prep/src/pages/Login.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-export default function Login() {
- return (
-
-
Login to Peer-Prep
-
- );
-}
\ No newline at end of file
diff --git a/peer-prep/src/pages/Login/LoginPage.module.css b/peer-prep/src/pages/Login/LoginPage.module.css
new file mode 100644
index 0000000000..31cb50b7b2
--- /dev/null
+++ b/peer-prep/src/pages/Login/LoginPage.module.css
@@ -0,0 +1,40 @@
+.wrapper {
+ /* min-height: rem(900px);
+ background-size: cover;
+ background-image: url(https://images.unsplash.com/photo-1484242857719-4b9144542727?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1280&q=80); */
+}
+
+.form {
+ border-left: rem(1px) solid
+ light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-7));
+ /* min-height: rem(900px); */
+ min-height: calc(100vh - 60px - var(--mantine-spacing-md));
+ /* max-width: rem(450px); */
+ padding: 3rem;
+ padding-right: 4rem;
+ padding-left: 4rem;
+
+ justify-content: center;
+ flex-direction: column;
+
+ @media (max-width: $mantine-breakpoint-sm) {
+ max-width: 100%;
+ flex-grow: 1;
+ }
+}
+
+.left-image-container {
+ max-width: 1600px;
+
+ @media (max-width: $mantine-breakpoint-md) {
+ display: none;
+ }
+}
+/* .left-image {
+ max-width: 600px;
+} */
+
+.title {
+ color: light-dark(var(--mantine-color-black), var(--mantine-color-white));
+ font-family: Greycliff CF, var(--mantine-font-family);
+}
diff --git a/peer-prep/src/pages/Login/LoginPage.tsx b/peer-prep/src/pages/Login/LoginPage.tsx
new file mode 100644
index 0000000000..a8e13b4256
--- /dev/null
+++ b/peer-prep/src/pages/Login/LoginPage.tsx
@@ -0,0 +1,279 @@
+import {
+ Paper,
+ TextInput,
+ PasswordInput,
+ Checkbox,
+ Button,
+ Title,
+ Text,
+ Anchor,
+ Box,
+ Flex,
+ Center,
+ Stack,
+ Image,
+ Progress,
+ Group,
+} from "@mantine/core";
+import classes from "./LoginPage.module.css";
+import image from "../../assets/loginimage.svg";
+import { useEffect, useState } from "react";
+import { useAuth } from "../../hooks/useAuth";
+import { Navigate, useLocation } from "react-router-dom";
+import { IconCheck, IconX } from "@tabler/icons-react";
+
+function PasswordRequirement({
+ meets,
+ label,
+}: {
+ meets: boolean;
+ label: string;
+}) {
+ return (
+
+
+ {meets ? (
+
+ ) : (
+
+ )}
+ {label}
+
+
+ );
+}
+
+const requirements = [
+ { re: /[0-9]/, label: "Includes number" },
+ { re: /[a-zA-Z]/, label: "Includes alphabets" },
+ { label: "At least 8 characters", re: /.{8,}/ },
+];
+
+function getStrength(password: string) {
+ let multiplier = password.length > 5 ? 0 : 1;
+
+ requirements.forEach((requirement) => {
+ if (!requirement.re.test(password)) {
+ multiplier += 1;
+ }
+ });
+
+ return Math.max(100 - (100 / (requirements.length + 1)) * multiplier, 0);
+}
+
+export default function LoginOrRegisterPage() {
+ const [loginMode, setLoginMode] = useState(true); // true = log in, false = register
+
+ const { login, user, register } = useAuth();
+
+ if (user) {
+ return ;
+ }
+
+ // if register mode from query params, show register form
+ const { search } = useLocation();
+ const searchParams = new URLSearchParams(search);
+ const param = searchParams.get("register");
+ useEffect(() => {
+ if (param) {
+ setLoginMode(false);
+ } else {
+ setLoginMode(true);
+ }
+ }, [param]);
+
+ const [displayName, setDisplayName] = useState("");
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleLogin = () => {
+ // validate
+
+ // call
+ login({
+ email,
+ password,
+ });
+ };
+
+ const handleRegister = () => {
+ // validate
+
+ // call
+ setIsLoading(true);
+ console.log({ email, password, displayName });
+ register({
+ email,
+ password,
+ displayName,
+ });
+ };
+
+ const strength = getStrength(password);
+ const checks = requirements.map((requirement, index) => (
+
+ ));
+
+ const canRegister = requirements.every((requirement) =>
+ requirement.re.test(password)
+ );
+ const bars = Array(4)
+ .fill(0)
+ .map((_, index) => (
+ 0 && index === 0
+ ? 100
+ : strength >= ((index + 1) / 4) * 100
+ ? 100
+ : 0
+ }
+ color={strength > 80 ? "teal" : strength > 50 ? "yellow" : "red"}
+ key={index}
+ size={4}
+ />
+ ));
+
+ function onSubmit() {
+ if (loginMode) {
+ handleLogin();
+ } else {
+ handleRegister();
+ }
+ }
+
+ return (
+ //
+ );
+}
diff --git a/peer-prep/src/pages/ProtectedRouteWrapper.tsx b/peer-prep/src/pages/ProtectedRouteWrapper.tsx
new file mode 100644
index 0000000000..01bb77eafa
--- /dev/null
+++ b/peer-prep/src/pages/ProtectedRouteWrapper.tsx
@@ -0,0 +1,16 @@
+import { Navigate, Outlet } from "react-router-dom";
+import { useAuth } from "../hooks/useAuth";
+
+// https://stackoverflow.com/questions/70833727/using-react-router-v6-i-need-a-navbar-to-permanently-be-there-but-cant-display
+export default function ProtectedRouteWrapper() {
+ const { user } = useAuth();
+ if (!user) {
+ // user is not authenticated
+ return ;
+ }
+ return (
+
+
+
+ );
+}
diff --git a/peer-prep/src/pages/Questions/CreateQuestionPage/CreateQuestionPage.module.css b/peer-prep/src/pages/Questions/CreateQuestionPage/CreateQuestionPage.module.css
new file mode 100644
index 0000000000..4d8520e10d
--- /dev/null
+++ b/peer-prep/src/pages/Questions/CreateQuestionPage/CreateQuestionPage.module.css
@@ -0,0 +1,9 @@
+.quillEditor {
+ border-radius: var(--mantine-radius-sm);
+ border: 1px solid var(--mantine-color-gray-2) !important;
+}
+
+.testCaseHeader {
+ font-size: var(--input-label-size, var(--mantine-font-size-sm));
+ margin-top: 12px;
+}
diff --git a/peer-prep/src/pages/Questions/CreateQuestionPage/CreateQuestionPage.tsx b/peer-prep/src/pages/Questions/CreateQuestionPage/CreateQuestionPage.tsx
new file mode 100644
index 0000000000..e903c53fb3
--- /dev/null
+++ b/peer-prep/src/pages/Questions/CreateQuestionPage/CreateQuestionPage.tsx
@@ -0,0 +1,278 @@
+import { useEffect, useState } from "react";
+import {
+ TextInput,
+ Select,
+ Textarea,
+ Button,
+ Container,
+ Stack,
+ Center,
+ Input,
+ Stepper,
+ Text,
+ MultiSelect,
+ Card,
+ Switch,
+ Flex,
+ Divider,
+} from "@mantine/core";
+import ReactQuill from "react-quill";
+import "react-quill/dist/quill.snow.css";
+
+import classes from "./CreateQuestionPage.module.css";
+import { Question, QuestionOlsd, TestCase } from "../../../types/question";
+import useApi, { QuestionServerResponse, SERVICE } from "../../../hooks/useApi";
+import { notifications } from "@mantine/notifications";
+
+export default function CreateQuestionPage() {
+ const [name, setName] = useState("");
+ const [difficulty, setDifficulty] = useState("EASY");
+ const [categories, setCategories] = useState([]);
+ const [description, setDescription] = useState("");
+ const [testCases, setTestCases] = useState([]);
+
+ const [fetchedCategories, setFetchedCategories] = useState<
+ { value: string; label: string }[]
+ >([]);
+
+ // Mapping for difficulty display
+ const difficultyOptions = [
+ { value: "EASY", label: "Easy" },
+ { value: "MEDIUM", label: "Medium" },
+ { value: "HARD", label: "Hard" },
+ ];
+
+ useEffect(() => {
+ fetchCategories();
+ }, []);
+
+ const { fetchData, isLoading, error } = useApi();
+
+ const fetchCategories = async () => {
+ try {
+ const response = await fetchData>(
+ "/question-service/categories",
+ SERVICE.QUESTION
+ );
+
+ if (response.success) {
+ const categories = response.data;
+ const transformedCategories = categories.map((category: string) => ({
+ value: category.toUpperCase(),
+ label:
+ category.charAt(0).toUpperCase() + category.slice(1).toLowerCase(),
+ }));
+ setFetchedCategories(transformedCategories);
+ } else {
+ notifications.show({
+ message:
+ response.message ||
+ "Error fetching categories, please try again later.",
+ color: "red",
+ });
+ }
+ } catch (error: any) {
+ console.error("Error fetching categories", error);
+ notifications.show({
+ message: error.message,
+ color: "red",
+ });
+ }
+ };
+
+ const handleSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+
+ // Remove _id from test cases
+ const updatedTestCases = testCases.map(({ _id, ...rest }) => ({
+ ...rest,
+ }));
+
+ try {
+ const response = await fetchData>(
+ "/question-service",
+ SERVICE.QUESTION,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ title: name,
+ description: { testDescription: description },
+ categories,
+ difficulty,
+ testCases: updatedTestCases,
+ }),
+ }
+ );
+
+ if (response.success) {
+ alert("Question created successfully!");
+ window.location.href = "/questions";
+ } else {
+ notifications.show({
+ message:
+ response.message ||
+ "Error creating question, please try again later.",
+ color: "red",
+ });
+ }
+ } catch (error: any) {
+ console.log("Error creating question:", error);
+ notifications.show({
+ message: error.message,
+ color: "red",
+ });
+ }
+ };
+
+ const addTestCase = () => {
+ setTestCases([
+ ...testCases,
+ { testCode: "", isPublic: false, meta: {}, expectedOutput: "" },
+ ]);
+ };
+
+ const removeTestCase = (index: number) => {
+ setTestCases(testCases.filter((_, i) => i !== index));
+ };
+
+ const handleTestCaseChange = (
+ index: number,
+ field: keyof TestCase,
+ value: any
+ ) => {
+ const updatedTestCases = [...testCases];
+ // @ts-ignore
+ updatedTestCases[index][field] = value;
+ setTestCases(updatedTestCases);
+ };
+
+ const [active, setActive] = useState(1);
+
+ return (
+
+ Add New Question
+
+
+ );
+}
diff --git a/peer-prep/src/pages/Questions/EditQuestionPage/EditQuestionPage.module.css b/peer-prep/src/pages/Questions/EditQuestionPage/EditQuestionPage.module.css
new file mode 100644
index 0000000000..4d8520e10d
--- /dev/null
+++ b/peer-prep/src/pages/Questions/EditQuestionPage/EditQuestionPage.module.css
@@ -0,0 +1,9 @@
+.quillEditor {
+ border-radius: var(--mantine-radius-sm);
+ border: 1px solid var(--mantine-color-gray-2) !important;
+}
+
+.testCaseHeader {
+ font-size: var(--input-label-size, var(--mantine-font-size-sm));
+ margin-top: 12px;
+}
diff --git a/peer-prep/src/pages/Questions/EditQuestionPage/EditQuestionPage.tsx b/peer-prep/src/pages/Questions/EditQuestionPage/EditQuestionPage.tsx
new file mode 100644
index 0000000000..1126dccc8b
--- /dev/null
+++ b/peer-prep/src/pages/Questions/EditQuestionPage/EditQuestionPage.tsx
@@ -0,0 +1,352 @@
+import { useParams } from "react-router-dom";
+import { useState, useEffect } from "react";
+import {
+ TextInput,
+ Select,
+ Textarea,
+ Text,
+ Button,
+ Container,
+ MultiSelect,
+ Input,
+ Stack,
+ Flex,
+ Switch,
+ Card,
+ Center,
+ Divider,
+} from "@mantine/core";
+import ReactQuill from "react-quill";
+import "react-quill/dist/quill.snow.css";
+
+import classes from "./EditQuestionPage.module.css";
+import { Question, QuestionOlsd, TestCase } from "../../../types/question";
+import useApi, { QuestionServerResponse, SERVICE } from "../../../hooks/useApi";
+import { notifications } from "@mantine/notifications";
+
+export default function EditQuestionPage() {
+ const { id } = useParams<{ id: string }>();
+ const [name, setName] = useState("");
+ const [difficulty, setDifficulty] = useState(null);
+ const [categories, setCategories] = useState([]);
+ const [description, setDescription] = useState("");
+ const [testCases, setTestCases] = useState([]);
+
+ const [fetchedCategories, setFetchedCategories] = useState<
+ { value: string; label: string }[]
+ >([]);
+
+ // Mapping for difficulty display
+ const difficultyOptions = [
+ { value: "EASY", label: "Easy" },
+ { value: "MEDIUM", label: "Medium" },
+ { value: "HARD", label: "Hard" },
+ ];
+
+ useEffect(() => {
+ if (typeof id === "string") {
+ fetchQuestionDetails(id);
+ fetchCategories();
+ }
+ }, [id]);
+
+ const { fetchData, isLoading, error } = useApi();
+
+ const fetchQuestionDetails = async (questionId: string) => {
+ try {
+ const response = await fetchData>(
+ `/question-service/id/${questionId}`,
+ SERVICE.QUESTION
+ );
+
+ if (response.success) {
+ const question = response.data;
+ setName(question.title);
+ setDifficulty(question.difficulty);
+ setCategories(question.categories);
+ setDescription(question.description.testDescription);
+ setTestCases(question.testCases);
+ } else {
+ notifications.show({
+ message:
+ response.message ||
+ "Error fetching question details, please try again later.",
+ color: "red",
+ });
+ }
+ } catch (error: any) {
+ console.error("Error fetching question details:", error);
+ notifications.show({
+ message: error.message,
+ color: "red",
+ });
+ }
+ };
+
+ const fetchCategories = async () => {
+ try {
+ const response = await fetchData>(
+ "/question-service/categories",
+ SERVICE.QUESTION
+ );
+
+ if (response.success) {
+ const categories = response.data;
+ const transformedCategories = categories.map((category: string) => ({
+ value: category.toUpperCase(),
+ label:
+ category.charAt(0).toUpperCase() + category.slice(1).toLowerCase(),
+ }));
+ setFetchedCategories(transformedCategories);
+ } else {
+ notifications.show({
+ message:
+ response.message ||
+ "Error fetching categories, please try again later.",
+ color: "red",
+ });
+ }
+ } catch (error: any) {
+ console.error("Error fetching categories", error);
+ notifications.show({
+ message: error.message,
+ color: "red",
+ });
+ }
+ };
+
+ const handleSubmit = async (event: React.FormEvent) => {
+ event.preventDefault();
+
+ // Remove _id from test cases
+ const updatedTestCases = testCases.map(({ _id, ...rest }) => ({
+ ...rest,
+ }));
+
+ try {
+ const response = await fetchData>(
+ `/question-service/id/${id}`,
+ SERVICE.QUESTION,
+ {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ title: name,
+ description: { testDescription: description },
+ categories,
+ difficulty,
+ testCases: updatedTestCases,
+ }),
+ }
+ );
+
+ if (response.success) {
+ notifications.show({
+ message: "Question updated successfully!",
+ color: "green",
+ });
+ } else {
+ notifications.show({
+ message:
+ response.message ||
+ "Error updating question, please try again later.",
+ color: "red",
+ });
+ }
+ } catch (error: any) {
+ console.error("Error updating question:", error);
+ notifications.show({
+ message: error.message,
+ color: "red",
+ });
+ }
+ };
+
+ const handleDelete = async () => {
+ try {
+ const response = await fetchData>(
+ `/question-service/id/${id}`,
+ SERVICE.QUESTION,
+ {
+ method: "DELETE",
+ }
+ );
+
+ if (response.success) {
+ notifications.show({
+ message: "Question deleted successfully!",
+ color: "green",
+ });
+ window.location.href = "/questions";
+ } else {
+ notifications.show({
+ message:
+ response.message ||
+ "Error deleting question, please try again later.",
+ color: "red",
+ });
+ }
+ } catch (error: any) {
+ console.error("Error deleting question:", error);
+ notifications.show({
+ message: error.message,
+ color: "red",
+ });
+ }
+ };
+
+ const addTestCase = () => {
+ setTestCases([
+ ...testCases,
+ { testCode: "", isPublic: false, meta: {}, expectedOutput: "" },
+ ]);
+ };
+
+ const removeTestCase = (index: number) => {
+ setTestCases(testCases.filter((_, i) => i !== index));
+ };
+
+ const handleTestCaseChange = (
+ index: number,
+ field: keyof TestCase,
+ value: any
+ ) => {
+ const updatedTestCases = [...testCases];
+ // @ts-ignore
+ updatedTestCases[index][field] = value;
+ setTestCases(updatedTestCases);
+ };
+
+ return (
+
+ Edit Question
+
+ setName(event.currentTarget.value)}
+ required
+ />
+ setDifficulty(value)}
+ data={difficultyOptions}
+ required
+ />
+ setCategories(value)}
+ data={fetchedCategories}
+ multiple
+ required
+ />
+ setDescription(event.currentTarget.value)}
+ minRows={8}
+ required
+ />
+
+ {/*
+ setDescription(newDescription)}
+ style={{ height: "576px", marginTop: "12px" }}
+ modules={ modules }
+ >
+
+
+ */}
+
+
+ Test Cases
+ *
+
+
+
+ {testCases.map((testCase, index) => (
+
+
+ handleTestCaseChange(
+ index,
+ "testCode",
+ event.currentTarget.value
+ )
+ }
+ minRows={8}
+ required
+ />
+
+ handleTestCaseChange(
+ index,
+ "expectedOutput",
+ event.currentTarget.value
+ )
+ }
+ minRows={8}
+ required
+ />
+
+
+ handleTestCaseChange(
+ index,
+ "isPublic",
+ event.currentTarget.checked
+ )
+ }
+ />
+ removeTestCase(index)}>
+ Remove Test Case
+
+
+
+ ))}
+
+ Add Test Case
+
+
+
+
+
+
+
+ Update Question
+
+
+ Delete Question
+
+
+
+
+ );
+}
diff --git a/peer-prep/src/pages/Questions/QuestionPage.module.css b/peer-prep/src/pages/Questions/QuestionPage.module.css
new file mode 100644
index 0000000000..53bcd73347
--- /dev/null
+++ b/peer-prep/src/pages/Questions/QuestionPage.module.css
@@ -0,0 +1,8 @@
+.question-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ width: 66%;
+ justify-content: center;
+ margin: 0 auto;
+ row-gap: 2rem;
+}
\ No newline at end of file
diff --git a/peer-prep/src/pages/Questions/QuestionPage.tsx b/peer-prep/src/pages/Questions/QuestionPage.tsx
new file mode 100644
index 0000000000..ea5b988f33
--- /dev/null
+++ b/peer-prep/src/pages/Questions/QuestionPage.tsx
@@ -0,0 +1,72 @@
+import {
+ Box,
+ CloseButton,
+ Container,
+ Flex,
+ Input,
+ Stack,
+ Button,
+ Modal,
+ TextInput,
+ Select,
+ Textarea,
+} from "@mantine/core";
+import { IconSearch } from "@tabler/icons-react";
+import { useEffect, useState } from "react";
+import { Question, SAMPLE_QUESTIONS } from "../../types/question";
+import useApi, { QuestionServerResponse } from "../../hooks/useApi";
+import { Link, useLoaderData } from "react-router-dom";
+import QuestionCard from "../../components/Questions/QuestionCard/QuestionCard";
+
+import classes from "./QuestionPage.module.css";
+
+export default function QuestionPage() {
+ const [searchValue, setSearchValue] = useState("");
+ const [questions, setQuestions] = useState([]);
+
+ // const { isLoading, error, fetchData } = useApi();
+ const data = useLoaderData() as QuestionServerResponse;
+ console.log({ data });
+
+ useEffect(() => {
+ setQuestions(data.data);
+ }, [data]);
+
+ return (
+ <>
+
+
+ }
+ value={searchValue}
+ onChange={(event) => setSearchValue(event.currentTarget.value)}
+ rightSection={
+ setSearchValue("")}
+ style={{ display: searchValue ? undefined : "none" }}
+ />
+ }
+ />
+
+
+
+
+ {/*
+ Add New Question
+ */}
+
+ Add new question
+
+
+
+
+ {questions.map((question, key) => (
+
+ ))}
+
+ >
+ );
+}
diff --git a/peer-prep/src/pages/Session/Create/CreateSessionPage.module.css b/peer-prep/src/pages/Session/Create/CreateSessionPage.module.css
new file mode 100644
index 0000000000..ab8e83d571
--- /dev/null
+++ b/peer-prep/src/pages/Session/Create/CreateSessionPage.module.css
@@ -0,0 +1,15 @@
+.wrapper {
+ min-height: calc(100vh - 60px - var(--mantine-spacing-md));
+ align-items: center;
+
+ padding: 8rem;
+ padding-top: 2rem;
+
+ flex-direction: column;
+ gap: 2rem;
+}
+
+.criteria {
+ padding: 4rem;
+ padding-top: 2rem;
+}
diff --git a/peer-prep/src/pages/Session/Create/CreateSessionPage.tsx b/peer-prep/src/pages/Session/Create/CreateSessionPage.tsx
new file mode 100644
index 0000000000..7b4de60d80
--- /dev/null
+++ b/peer-prep/src/pages/Session/Create/CreateSessionPage.tsx
@@ -0,0 +1,191 @@
+import {
+ Box,
+ Button,
+ Checkbox,
+ Divider,
+ Flex,
+ Group,
+ MultiSelect,
+ SimpleGrid,
+ Stack,
+ Title,
+} from "@mantine/core";
+import classes from "./CreateSessionPage.module.css";
+import { useEffect, useState } from "react";
+import { capitalizeFirstLetter } from "../../../utils/utils";
+import { Link, useLoaderData } from "react-router-dom";
+import useApi, { QuestionServerResponse, SERVICE } from "../../../hooks/useApi";
+
+// Arrays
+// Algorithms
+// Databases
+// Data Structures
+// Brainteaser
+// Strings
+// Bit Manipulation
+// Recursion
+
+export default function CreateSessionPage() {
+ // TODO: query the question service to get a list of categories and difficulties
+ const { fetchData } = useApi();
+ useEffect(() => {
+ fetchData>(
+ `/question-service/categories`,
+ SERVICE.QUESTION
+ ).then((data) => {
+ if (data.success) {
+ setCategories(data.data);
+
+ console.log(data.data);
+ }
+ });
+ }, []);
+
+ const [categories, setCategories] = useState([
+ "Arrays",
+ "Algorithms",
+ "Databases",
+ "Data Structures",
+ "Brainteaser",
+ "Strings",
+ "Bit Manipulation",
+ "Recursion",
+ ]);
+ const [selectedCategories, setSelectedCategories] = useState([]);
+
+ const [difficulties, setDifficulties] = useState([
+ "easy",
+ "medium",
+ "hard",
+ ]);
+ const [selectedDifficulties, setSelectedDifficulties] = useState(
+ []
+ );
+
+ const canSearch =
+ selectedCategories.length > 0 && selectedDifficulties.length > 0;
+
+ // const data = useLoaderData() as ServerResponse<[string[]]>;
+
+ // useEffect(() => {
+ // console.log("data from preload:", data);
+ // if (data.data) {
+ // setCategories(data.data[0]);
+ // }
+ // }, [data]);
+ return (
+ <>
+
+ Create a session
+ {/* */}
+
+
+
+ Categories
+ setSelectedCategories(value)}
+ >
+
+ {/*
+
+
+ */}
+ {categories.map((category, index) => (
+
+ ))}
+
+
+
+ 0
+ }
+ label={
+ selectedCategories.length > 0
+ ? `${selectedCategories.length} selected`
+ : "Select all"
+ }
+ onChange={() => {
+ // if indeterminate, select all
+ // if all selected, deselect all
+ // if none selected, select all
+ if (selectedCategories.length === categories.length) {
+ setSelectedCategories([]);
+ } else {
+ setSelectedCategories(categories);
+ }
+ }}
+ />
+
+
+ Difficulties
+ setSelectedDifficulties(value)}
+ >
+
+ {/*
+
+
+ */}
+ {difficulties.map((difficulty, index) => (
+
+ ))}
+
+
+
+ 0
+ }
+ label={
+ selectedDifficulties.length > 0
+ ? `${selectedDifficulties.length} selected`
+ : "Select all"
+ }
+ onChange={() => {
+ // if indeterminate, select all
+ // if all selected, deselect all
+ // if none selected, select all
+ if (selectedDifficulties.length === difficulties.length) {
+ setSelectedDifficulties([]);
+ } else {
+ setSelectedDifficulties(difficulties);
+ }
+ }}
+ />
+
+
+
+ Begin search
+
+
+ >
+ );
+}
diff --git a/peer-prep/src/pages/Session/Search/SearchingPage.module.css b/peer-prep/src/pages/Session/Search/SearchingPage.module.css
new file mode 100644
index 0000000000..dc479e8a7c
--- /dev/null
+++ b/peer-prep/src/pages/Session/Search/SearchingPage.module.css
@@ -0,0 +1,30 @@
+.searchingWrapper {
+ min-height: calc(100vh - 60px - var(--mantine-spacing-md));
+ align-items: center;
+
+ padding: 8rem;
+
+ flex-direction: column;
+ gap: 2rem;
+}
+
+.searchingImage {
+ width: 320px;
+}
+
+/* Rotating 180degree, stop, then rotating again */
+.loadingSpinner {
+ animation: rotate 2s linear infinite;
+}
+
+@keyframes rotate {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 50% {
+ transform: rotate(180deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
diff --git a/peer-prep/src/pages/Session/Search/SearchingPage.tsx b/peer-prep/src/pages/Session/Search/SearchingPage.tsx
new file mode 100644
index 0000000000..354551c383
--- /dev/null
+++ b/peer-prep/src/pages/Session/Search/SearchingPage.tsx
@@ -0,0 +1,20 @@
+import { Flex, Image, Stack, Text, Title } from "@mantine/core";
+
+import classes from "./SearchingPage.module.css";
+
+import SearchingImage from "../../../assets/searchimage.svg";
+
+export default function SearchingPage() {
+ return (
+ <>
+
+
+
+ {" "}
+ ⏳ Loading...{" "}
+
+ You will be matched with a partner soon!
+
+ >
+ );
+}
diff --git a/peer-prep/src/types/question.ts b/peer-prep/src/types/question.ts
new file mode 100644
index 0000000000..575b276a71
--- /dev/null
+++ b/peer-prep/src/types/question.ts
@@ -0,0 +1,70 @@
+export type Complexity = "EASY" | "MEDIUM" | "HARD";
+
+export type TestCase = {
+ _id?: number;
+ testCode: string;
+ isPublic: boolean;
+ expectedOutput: string;
+ meta: { [key: string]: any };
+};
+
+export type QuestionOlsd = {
+ id: number,
+ title: string,
+ shortDescription: string,
+ description: string,
+ categories: string[],
+ complexity: Complexity,
+ link: string
+}
+
+export type Question = {
+ _id: string;
+ title: string;
+ description: { [key: string]: any };
+ categories: string[];
+ difficulty: Complexity;
+ isDeleted: boolean;
+ testCases: TestCase[];
+ __v: number;
+};
+
+export const SAMPLE_QUESTIONS: any[] = [{
+ id: 1,
+ title: "Reverse a string",
+ shortDescription: `Write a function that reverses a string. The input string is given as an array of characters s.
+You must do this by modifying the input array in-place with O(1) extra memory.`,
+ description: `Write a function that reverses a string. The input string is given as an array of characters s.
+You must do this by modifying the input array in-place with O(1) extra memory.
+
+Example 1:
+
+Input: s =
+["h","e","l","l","o"]
+Output:
+["o","l","l","e","h"]
+Example 2:
+
+Input: s =
+["H","a","n","n","a","h"]
+Output:
+["h","a","n","n","a","H"]
+
+Constraints:
+
+1 <= s.length <= 105
+s[i] is a printable ascii character. `,
+ categories: ["strings", "algorithms"],
+ complexity: "easy",
+ link: "https://leetcode.com/problems/reverse-string/"
+}, {
+ id: 2,
+ title: "Linked List Cycle Detection",
+ shortDescription: `Implement a function to detect if a linked list contains a cycle.`,
+ description: `Implement a function
+to detect if a linked
+list contains a cycle.`,
+ categories: ["linked lists", "algorithms"],
+ complexity: "medium",
+ link: "https://leetcode.com/problems/linked-list-cycle/"
+}]
\ No newline at end of file
diff --git a/peer-prep/src/types/user.ts b/peer-prep/src/types/user.ts
new file mode 100644
index 0000000000..3490c776ba
--- /dev/null
+++ b/peer-prep/src/types/user.ts
@@ -0,0 +1,7 @@
+export type User = {
+ email: string,
+ password?: string,
+ displayName: string,
+ isAdmin: boolean,
+ id: string,
+}
\ No newline at end of file
diff --git a/peer-prep/src/utils/utils.ts b/peer-prep/src/utils/utils.ts
new file mode 100644
index 0000000000..a9cfb16a7a
--- /dev/null
+++ b/peer-prep/src/utils/utils.ts
@@ -0,0 +1,6 @@
+/**
+ * Capitalize the first letter of a string
+ */
+export function capitalizeFirstLetter(str: string): string {
+ return str.charAt(0).toUpperCase() + str.slice(1);
+}
\ No newline at end of file
diff --git a/peer-prep/tsconfig.app.json b/peer-prep/tsconfig.app.json
index f0a235055d..3478f39409 100644
--- a/peer-prep/tsconfig.app.json
+++ b/peer-prep/tsconfig.app.json
@@ -15,10 +15,10 @@
"jsx": "react-jsx",
/* Linting */
- "strict": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
+ "strict": false,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
"noFallthroughCasesInSwitch": true
},
- "include": ["src"]
+ "include": ["src", "."]
}
diff --git a/peer-prep/tsconfig.json b/peer-prep/tsconfig.json
index 8b0d5d959f..6bc9bf19b9 100644
--- a/peer-prep/tsconfig.json
+++ b/peer-prep/tsconfig.json
@@ -6,5 +6,5 @@
],
"compilerOptions": {
"jsx": "react-jsx"
- },
+ }
}
diff --git a/peer-prep/vite.config.ts b/peer-prep/vite.config.ts
index 5a33944a9b..d69f833f28 100644
--- a/peer-prep/vite.config.ts
+++ b/peer-prep/vite.config.ts
@@ -1,7 +1,12 @@
-import { defineConfig } from 'vite'
+import { defineConfig, PluginOption } from 'vite'
import react from '@vitejs/plugin-react'
+import mkcert from 'vite-plugin-mkcert'
+export const _mkcert = mkcert() as PluginOption;
+
// https://vitejs.dev/config/
export default defineConfig({
- plugins: [react()],
+ plugins: [react(),
+ // mkcert() as PluginOption
+ ],
})
diff --git a/question-service/Dockerfile b/question-service/Dockerfile
new file mode 100644
index 0000000000..f74bcba084
--- /dev/null
+++ b/question-service/Dockerfile
@@ -0,0 +1,14 @@
+# https://github.com/awslabs/aws-lambda-web-adapter/tree/main/examples/expressjs
+FROM public.ecr.aws/docker/library/node:20
+
+# expose 8003 port for access to the container
+EXPOSE 8003
+
+# copy all files into the Docker container
+COPY . .
+
+# install node libraries
+RUN npm install
+
+# execute the server
+CMD ["node", "index.js"]
\ No newline at end of file
diff --git a/question-service/controller/question-controller.js b/question-service/controller/question-controller.js
new file mode 100644
index 0000000000..98cf8d34a4
--- /dev/null
+++ b/question-service/controller/question-controller.js
@@ -0,0 +1,308 @@
+import BadRequestError from "../errors/BadRequestError.js";
+import BaseError from "../errors/BaseError.js";
+import ConflictError from "../errors/ConflictError.js";
+import NotFoundError from "../errors/NotFoundError.js";
+import {
+ ormCreateQuestion as _createQuestion,
+ ormGetAllQuestions as _getAllQuestions,
+ ormGetQuestionById as _getQuestionById,
+ ormDeleteQuestionById as _deleteQuestionById,
+ ormUpdateQuestionById as _updateQuestionById,
+ ormGetFilteredQuestions as _getFilteredQuestions,
+ ormFindQuestion as _findQuestion,
+ ormGetQuestionsByDescription as _getQuestionsByDescription,
+ ormGetQuestionsByTitleAndDifficulty as _getQuestionByTitleAndDifficulty,
+ ormGetDistinctCategories as _getDistinctCategories,
+} from "../models/orm.js";
+
+const createQuestion = async (req, res, next) => {
+ try {
+ // CHECK WHETHER QUESTION WITH THE SAME DESCRIPTION ALREADY EXISTS
+ const duplicateDescriptionQuestions = await _getQuestionsByDescription(
+ req.body.description
+ );
+
+ if (duplicateDescriptionQuestions.length > 0) {
+ throw new ConflictError(
+ "A question with this description already exists"
+ );
+ }
+
+ // CHECK WHETHER QUESTION WITH THE SAME TITLE AND DIFFICULTY ALREADY EXISTS (ONLY CHECK NOT DELETED ONES)
+ const duplicateTitleAndDifficultyQuestions =
+ await _getQuestionByTitleAndDifficulty(
+ req.body.title,
+ req.body.difficulty
+ );
+
+ if (duplicateTitleAndDifficultyQuestions.length > 0) {
+ throw new ConflictError(
+ "A question with this title and difficulty already exists"
+ );
+ }
+
+ const question = await _createQuestion(req.body);
+ return res.status(201).json({ success: true, status: 201, data: question });
+ } catch (err) {
+ next(
+ err instanceof BaseError
+ ? err
+ : new BaseError(500, "Error creating the question")
+ );
+ }
+};
+
+const getAllQuestions = async (req, res, next) => {
+ try {
+ const questions = await _getAllQuestions(req.query);
+
+ if (questions.length === 0) {
+ throw new NotFoundError("No questions found");
+ }
+
+ return res
+ .status(200)
+ .json({ success: true, status: 200, data: questions });
+ } catch (err) {
+ next(
+ err instanceof BaseError
+ ? err
+ : new BaseError(500, "Error retrieving question")
+ );
+ }
+};
+
+const getQuestionById = async (req, res, next) => {
+ const { id } = req.params;
+
+ try {
+ const question = await _getQuestionById(id);
+
+ if (!question) {
+ throw new NotFoundError("Question not found");
+ }
+ return res.status(200).json({ success: true, status: 200, data: question });
+ } catch (err) {
+ next(
+ err instanceof BaseError
+ ? err
+ : new BaseError(500, "Error retrieving question")
+ );
+ }
+};
+
+const deleteQuestionById = async (req, res, next) => {
+ const { id } = req.params;
+
+ try {
+ const questionToDelete = await _getQuestionById(id);
+ if (!questionToDelete) {
+ throw new NotFoundError("Question not found");
+ }
+
+ const result = await _deleteQuestionById(id);
+
+ if (!result) {
+ throw new NotFoundError("Question not found");
+ }
+ return res
+ .status(200)
+ .json({ success: true, status: 200, message: "Question deleted!" });
+ } catch (err) {
+ next(
+ err instanceof BaseError
+ ? err
+ : new BaseError(500, "Error deleting question")
+ );
+ }
+};
+
+const updateQuestionById = async (req, res, next) => {
+ const { id } = req.params;
+ const { description, title, difficulty } = req.body;
+
+ try {
+ // CHECK WHETHER QUESTION TO UPDATE EXISTS
+ const questionToUpdate = await _getQuestionById(id);
+ if (!questionToUpdate) {
+ throw new NotFoundError("Question not found");
+ }
+
+ // CHECK FOR DUPLICATE DESCRIPTION IF PROVIDED
+ if (description) {
+ const duplicateDescriptionQuestions = await _getQuestionsByDescription(description);
+ const otherQuestionsWithSameDescription = duplicateDescriptionQuestions.filter(
+ (question) => question._id.toString() !== id
+ );
+
+ if (otherQuestionsWithSameDescription.length > 0) {
+ throw new ConflictError("A question with this description already exists");
+ }
+ }
+
+ // CHECK FOR DUPLICATE TITLE AND DIFFICULTY IF PROVIDED
+ if (title || difficulty) {
+ const titleToCheck = title || questionToUpdate.title;
+ const difficultyToCheck = difficulty || questionToUpdate.difficulty;
+
+ const duplicateTitleAndDifficultyQuestions =
+ await _getQuestionByTitleAndDifficulty(titleToCheck, difficultyToCheck);
+ const otherQuestionsWithSameTitleAndDifficulty =
+ duplicateTitleAndDifficultyQuestions.filter(
+ (question) => question._id.toString() !== id
+ );
+
+ if (otherQuestionsWithSameTitleAndDifficulty.length > 0) {
+ throw new ConflictError("A question with such title and difficulty already exists");
+ }
+ }
+
+ // UPDATE THE QUESTION
+ const updatedQuestion = await _updateQuestionById(id, req.body);
+ if (!updatedQuestion) {
+ throw new NotFoundError("Question not found");
+ }
+
+ return res.status(200).json({ success: true, status: 200, data: updatedQuestion });
+ } catch (err) {
+ console.log(err);
+ next(
+ err instanceof BaseError ? err : new BaseError(500, "Error updating question")
+ );
+ }
+};
+
+const getFilteredQuestions = async (req, res, next) => {
+ try {
+ const { categories, difficulty } = req.query;
+ if (categories) {
+ if (!Array.isArray(categories)) {
+ throw new BadRequestError("Categories should be an array!");
+ }
+ const distinctCategories = await _getDistinctCategories();
+ if (
+ categories.some(
+ (category) => !distinctCategories.includes(category.toUpperCase())
+ )
+ ) {
+ throw new BadRequestError("Category does not exist!");
+ }
+ }
+ if (difficulty) {
+ if (!Array.isArray(difficulty)) {
+ throw new BadRequestError("Difficulty should be an array!");
+ }
+ if (
+ difficulty.some(
+ (diff) => !["EASY", "MEDIUM", "HARD"].includes(diff.toUpperCase())
+ )
+ ) {
+ throw new BadRequestError(
+ "Difficulty should be either EASY, MEDIUM or HARD!"
+ );
+ }
+ }
+
+ const filteredQuestions = await _getFilteredQuestions({
+ categories,
+ difficulty,
+ });
+
+ if (filteredQuestions.length === 0) {
+ throw new NotFoundError("No questions with matching categories and difficulty found");
+ }
+
+ return res
+ .status(200)
+ .json({ success: true, status: 200, data: filteredQuestions });
+ } catch (err) {
+ next(
+ err instanceof BaseError
+ ? err
+ : new BaseError(500, "Error filtering question")
+ );
+ }
+};
+
+const findQuestion = async (req, res, next) => {
+ try {
+ const { categories, difficulty } = req.query;
+
+ if (categories) {
+ if (!Array.isArray(categories)) {
+ throw new BadRequestError("Categories should be an array!");
+ }
+ const distinctCategories = await _getDistinctCategories();
+ if (
+ categories.some(
+ (category) => !distinctCategories.includes(category.toUpperCase())
+ )
+ ) {
+ throw new BadRequestError("Category does not exist!");
+ }
+ }
+
+ if (difficulty) {
+ if (!Array.isArray(difficulty)) {
+ throw new BadRequestError("Difficulty should be an array!");
+ }
+ if (
+ difficulty.some(
+ (diff) => !["EASY", "MEDIUM", "HARD"].includes(diff.toUpperCase())
+ )
+ ) {
+ throw new BadRequestError(
+ "Difficulty should be either EASY, MEDIUM or HARD!"
+ );
+ }
+ }
+
+ const foundQuestion = await _findQuestion({ categories, difficulty });
+
+ if (!foundQuestion) {
+ console.log("No questions found");
+ throw new NotFoundError("No question with matching categories and difficulty found");
+ }
+
+ return res
+ .status(200)
+ .json({ success: true, status: 200, data: foundQuestion });
+ } catch (err) {
+ next(
+ err instanceof BaseError
+ ? err
+ : new BaseError(500, "Error finding question")
+ );
+ }
+};
+
+const getDistinctCategories = async (req, res, next) => {
+ try {
+ const distinctCategories = await _getDistinctCategories();
+
+ if (distinctCategories.length === 0) {
+ throw new NotFoundError("No categories found");
+ }
+
+ return res
+ .status(200)
+ .json({ success: true, status: 200, data: distinctCategories });
+ } catch (err) {
+ next(
+ err instanceof BaseError
+ ? err
+ : new BaseError(500, "Error retrieving distinct categories")
+ );
+ }
+};
+
+export {
+ createQuestion,
+ getAllQuestions,
+ getQuestionById,
+ deleteQuestionById,
+ updateQuestionById,
+ getFilteredQuestions,
+ findQuestion,
+ getDistinctCategories,
+};
diff --git a/question-service/errors/BadRequestError.js b/question-service/errors/BadRequestError.js
new file mode 100644
index 0000000000..f8c84b006c
--- /dev/null
+++ b/question-service/errors/BadRequestError.js
@@ -0,0 +1,11 @@
+import BaseError from "./BaseError.js";
+
+class BadRequestError extends BaseError {
+ constructor(message) {
+ super(400, message);
+ this.name = 'BadRequestError';
+ this.statusCode = 400;
+ }
+}
+
+export default BadRequestError;
\ No newline at end of file
diff --git a/question-service/errors/BaseError.js b/question-service/errors/BaseError.js
new file mode 100644
index 0000000000..f5ee83efe5
--- /dev/null
+++ b/question-service/errors/BaseError.js
@@ -0,0 +1,8 @@
+class BaseError extends Error {
+ constructor(statusCode, message) {
+ super(message);
+ this.statusCode = statusCode;
+ }
+}
+
+export default BaseError;
diff --git a/question-service/errors/ConflictError.js b/question-service/errors/ConflictError.js
new file mode 100644
index 0000000000..c9b620c475
--- /dev/null
+++ b/question-service/errors/ConflictError.js
@@ -0,0 +1,11 @@
+import BaseError from "./BaseError.js";
+
+class ConflictError extends BaseError {
+ constructor(message) {
+ super(409, message);
+ this.name = 'ConflictError';
+ this.statusCode = 409;
+ }
+}
+
+export default ConflictError;
\ No newline at end of file
diff --git a/question-service/errors/ForbiddenError.js b/question-service/errors/ForbiddenError.js
new file mode 100644
index 0000000000..0ae574ec28
--- /dev/null
+++ b/question-service/errors/ForbiddenError.js
@@ -0,0 +1,11 @@
+import BaseError from "./BaseError.js";
+
+class ForbiddenError extends BaseError {
+ constructor(message) {
+ super(403, message);
+ this.name = "UnauthorizedError";
+ this.statusCode = 403;
+ }
+}
+
+export default ForbiddenError;
\ No newline at end of file
diff --git a/question-service/errors/NotFoundError.js b/question-service/errors/NotFoundError.js
new file mode 100644
index 0000000000..1a63a86dc4
--- /dev/null
+++ b/question-service/errors/NotFoundError.js
@@ -0,0 +1,11 @@
+import BaseError from "./BaseError.js";
+
+class NotFoundError extends BaseError {
+ constructor(message) {
+ super(404, message);
+ this.name = 'NotFoundError';
+ this.statusCode = 404;
+ }
+}
+
+export default NotFoundError;
\ No newline at end of file
diff --git a/question-service/index.js b/question-service/index.js
new file mode 100644
index 0000000000..bfde4fe786
--- /dev/null
+++ b/question-service/index.js
@@ -0,0 +1,42 @@
+import bodyParser from "body-parser";
+import dotenv from "dotenv";
+import express from "express";
+import mongoose from "mongoose";
+import corsMiddleware from "./middlewares/cors.js";
+import errorHandler from "./middlewares/errorHandler.js";
+import loggingMiddleware from "./middlewares/logging.js";
+import router from "./router/router.js";
+import cookieParser from "cookie-parser";
+
+
+dotenv.config();
+const app = express();
+const port = process.env.DEV_PORT || 8003;
+
+
+app.use(corsMiddleware);
+app.use(cookieParser());
+app.use(loggingMiddleware);
+app.use(bodyParser.json());
+
+app.use("/api/question-service", router);
+
+app.use(errorHandler);
+
+// Test Route for Health Checks
+app.get("/healthz", (req, res) => {
+ res.status(200).json({ message: "Connected to /healthz route of question-service" });
+});
+
+// MongoDB connection
+mongoose.connect(process.env.DEV_URI, {});
+
+const connection = mongoose.connection;
+connection.once("open", () => {
+ console.log("MongoDB connection established successfully");
+});
+connection.on("error", (err) => {
+ console.log("MongoDB error: " + err);
+});
+
+app.listen(port, () => console.log(`question-service listening on port ${port}`));
\ No newline at end of file
diff --git a/question-service/middlewares/access-control.js b/question-service/middlewares/access-control.js
new file mode 100644
index 0000000000..34dc925c5e
--- /dev/null
+++ b/question-service/middlewares/access-control.js
@@ -0,0 +1,35 @@
+import ForbiddenError from "../errors/ForbiddenError.js";
+import jwt from 'jsonwebtoken';
+
+function verifyAccessToken(token) {
+ return jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
+ if (err) {
+ return null;
+ }
+ return user;
+ });
+}
+
+const checkAdmin = (req, res, next) => {
+ // TODO: Remove after isAdmin is stored in cookies
+ console.log(req.cookies);
+ if (!req.cookies.accessToken) {
+ throw new ForbiddenError("Access Token not found");
+ }
+ const user = verifyAccessToken(req.cookies.accessToken);
+ if (!user || !user.isAdmin) {
+ throw new ForbiddenError("Access Token verification failed");
+ }
+ const isAdmin = user.isAdmin;
+
+ // Assuming 'isAdmin' is stored in cookies
+ // const { isAdmin } = req.cookies;
+
+ if (isAdmin) {
+ return next();
+ } else {
+ throw new ForbiddenError("You are not authorized to perform this action");
+ }
+};
+
+export default checkAdmin;
diff --git a/question-service/middlewares/cors.js b/question-service/middlewares/cors.js
new file mode 100644
index 0000000000..0041d19b0b
--- /dev/null
+++ b/question-service/middlewares/cors.js
@@ -0,0 +1,29 @@
+import cors from "cors";
+
+const allowedOrigins = [
+ "http://localhost:5173", // frontend dev
+ "http://peerprep.s3-website-ap-southeast-1.amazonaws.com", // frontend prod
+ "http://peerprep-frontend-bucket.s3-website-ap-southeast-1.amazonaws.com", // frontend staging
+ "http://localhost:8000",
+ "http://localhost:8001",
+ "http://localhost:8002",
+ "http://localhost:8004",
+];
+
+// PORT 8000 - FRONTEND
+// PORT 5173 - FRONTEND DEV
+// PORT 8001 - USER SERVICE
+// PORT 8002 - MATCHING SERVICE
+// PORT 8003 - QUESTION SERVICE
+// PORT 8004 - COLLABORATION SERVICE
+
+const corsOptions = {
+ origin: allowedOrigins,
+ methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
+ allowedHeaders: ["Content-Type", "Authorization"],
+ credentials: true,
+};
+
+const corsMiddleware = cors(corsOptions);
+
+export default corsMiddleware;
diff --git a/question-service/middlewares/errorHandler.js b/question-service/middlewares/errorHandler.js
new file mode 100644
index 0000000000..34d2fac99d
--- /dev/null
+++ b/question-service/middlewares/errorHandler.js
@@ -0,0 +1,15 @@
+const errorHandler = (err, req, res, next) => {
+
+ const errStatus = err.statusCode || 500;
+ const errMsg = err.message || "Internal Server Error";
+
+ console.log(`Error ${errStatus}: ${errMsg}`);
+
+ res.status(errStatus).json({
+ success: false,
+ status: errStatus,
+ message: errMsg
+ });
+};
+
+export default errorHandler;
diff --git a/question-service/middlewares/logging.js b/question-service/middlewares/logging.js
new file mode 100644
index 0000000000..afe7b5afca
--- /dev/null
+++ b/question-service/middlewares/logging.js
@@ -0,0 +1,7 @@
+const loggingMiddleware = (req, res, next) => {
+ console.log(`Received ${req.method} request to ${req.url}`);
+ next();
+ };
+
+ export default loggingMiddleware;
+
\ No newline at end of file
diff --git a/question-service/middlewares/validation.js b/question-service/middlewares/validation.js
new file mode 100644
index 0000000000..5c84043211
--- /dev/null
+++ b/question-service/middlewares/validation.js
@@ -0,0 +1,136 @@
+import Joi from "joi";
+import BadRequestError from "../errors/BadRequestError.js";
+
+// Schema for testcase
+const joiTestCaseSchema = Joi.object({
+ testCode: Joi.string().trim().min(1).messages({
+ "string.empty": "All testCode cannot be empty",
+ "string.min": "All testCode must be at least 1 character long",
+ }).required().messages({
+ "string.empty": "Test code cannot be empty",
+ "any.required": "Test code is required",
+ }),
+ isPublic: Joi.boolean().required().messages({
+ "boolean.base": "isPublic must be a boolean",
+ "any.required": "isPublic is required",
+ }),
+ meta: Joi.any().optional(), // assuming optional for now
+ expectedOutput: Joi.string().trim().min(1).messages({
+ "string.empty": "All expectedOutput cannot be empty",
+ "string.min": "All expectedOutput must be at least 1 character long",
+ }).required().messages({
+ "string.empty": "Expected output cannot be empty",
+ "any.required": "Expected output is required",
+ }),
+}).messages({
+ 'object.base': 'Each test case should have testCode, isPublic, and expectedOutput',
+});
+
+// Partial schema for question - for create
+const joiQuestionSchema = Joi.object({
+ title: Joi.string().required().messages({
+ "string.empty": "Title is required",
+ "any.required": "Title is required",
+ }),
+ description: Joi.object().required().messages({
+ "object.base": "Description is required as an object",
+ "any.required": "Description is required",
+ }),
+ categories: Joi.array()
+ .items(Joi.string().trim().min(1).messages({
+ "string.empty": "Each category cannot be empty",
+ "string.min": "Each category must be at least 1 character long",
+ }))
+ .min(1)
+ .required()
+ .messages({
+ "array.base": "Categories must be an array",
+ "array.min": "At least one topic is required",
+ "any.required": "Categories are required",
+ }),
+ difficulty: Joi.string()
+ .valid("HARD", "MEDIUM", "EASY")
+ .required()
+ .messages({
+ "any.only": "Difficulty must be either HARD, MEDIUM, or EASY",
+ "any.required": "Difficulty is required",
+ }),
+ testCases: Joi.array()
+ .items(joiTestCaseSchema)
+ .min(1)
+ .required()
+ .messages({
+ "array.base": "Test cases must be an array",
+ "array.min": "At least one test case is required",
+ 'any.required': 'Test cases are required and should have testCode, isPublic, and expectedOutput',
+ }),
+ isDeleted: Joi.boolean().default(false),
+});
+
+const joiPartialQuestionSchema = Joi.object({
+ title: Joi.string().trim().min(1).messages({
+ "string.empty": "Title cannot be empty",
+ "string.min": "Title must be at least 1 character long",
+ }).optional().messages({
+ "string.empty": "Title cannot be empty",
+ }),
+ description: Joi.object().optional().messages({
+ "object.base": "Description must be an object",
+ }),
+ categories: Joi.array()
+ .items(Joi.string().trim().min(1).messages({
+ "string.empty": "Each category cannot be empty",
+ "string.min": "Each category must be at least 1 character long",
+ }))
+ .optional()
+ .messages({
+ "array.base": "Categories must be an array",
+ "array.min": "At least one topic is required",
+ }),
+ difficulty: Joi.string().valid("EASY", "MEDIUM", "HARD").optional().messages({
+ "any.only": "Difficulty must be either HARD, MEDIUM, or EASY",
+ }),
+ testCases: Joi.array().items(joiTestCaseSchema).optional().messages({
+ "array.base": "Test cases must be an array",
+ "array.min": "At least one test case is required",
+ }),
+ isDeleted: Joi.boolean().optional(),
+});
+
+// VALIDATION MIDDLEWARE - CREATE QUESTION
+const validateNewQuestion = (req, res, next) => {
+ const questionToCreate = req.body;
+ console.log(questionToCreate);
+ questionToCreate.difficulty = questionToCreate.difficulty.toUpperCase();
+ const { error } = joiQuestionSchema.validate(req.body);
+
+ if (error) {
+ console.log(error);
+ throw new BadRequestError(error.details[0].message);
+ }
+
+ next();
+};
+
+// VALIDATION MIDDLEWARE - UPDATE QUESTION
+const validateUpdatedQuestion = (req, res, next) => {
+ const questionToUpdate = req.body;
+ console.log(questionToUpdate);
+ if (questionToUpdate.difficulty) {
+ questionToUpdate.difficulty = questionToUpdate.difficulty.toUpperCase();
+ }
+ if (questionToUpdate.categories) {
+ questionToUpdate.categories = questionToUpdate.categories.map((category) =>
+ category.toUpperCase()
+ );
+ }
+ const { error } = joiPartialQuestionSchema.validate(req.body);
+
+ if (error) {
+ throw new BadRequestError(error.details[0].message);
+ }
+
+ next();
+};
+
+export { validateNewQuestion, validateUpdatedQuestion } ;
diff --git a/question-service/models/model.js b/question-service/models/model.js
new file mode 100644
index 0000000000..b0c9b94219
--- /dev/null
+++ b/question-service/models/model.js
@@ -0,0 +1,64 @@
+import mongoose from "mongoose";
+const Schema = mongoose.Schema;
+
+const testCaseSchema = new Schema({
+ testCode: {
+ type: String,
+ required: [true, "Test code is required"],
+ },
+ isPublic:{
+ type: Boolean,
+ required: [true, "isPublic status is required"]
+ },
+ meta: {
+ type: Schema.Types.Mixed,
+ // required: [true, "Meta is required"], -- Assuming optional for now
+ },
+ expectedOutput: {
+ type: String,
+ required: [true, "Expected output is required"],
+ },
+});
+
+const questionSchema = new Schema({
+ title: {
+ type: String,
+ required: [true, "Title is required"],
+ },
+ description: {
+ type: Object,
+ required: [true, "Description is required"]
+ },
+ categories: {
+ type: [String],
+ required: [true, "Topic is required"],
+ validate: {
+ validator: (value) => {
+ return value.length > 0;
+ },
+ },
+ },
+ difficulty: {
+ type: String,
+ enum: {
+ values: ["EASY", "MEDIUM", "HARD"],
+ },
+ required: true,
+ },
+ testCases: {
+ type: [testCaseSchema],
+ required: [true, "Test cases are required"],
+ validate: {
+ validator: (value) => {
+ return value.length > 0;
+ },
+ },
+ },
+ isDeleted: {
+ type: Boolean,
+ default: false,
+ },
+});
+
+const Question = mongoose.model("Question", questionSchema);
+export default Question;
diff --git a/question-service/models/orm.js b/question-service/models/orm.js
new file mode 100644
index 0000000000..db72c481a4
--- /dev/null
+++ b/question-service/models/orm.js
@@ -0,0 +1,67 @@
+import {
+ createQuestion,
+ getAllQuestions,
+ getQuestionById,
+ deleteQuestionById,
+ updateQuestionById,
+ getFilteredQuestions,
+ getQuestionsByDescription,
+ getQuestionsByTitleAndDifficulty,
+ getDistinctCategories,
+} from "./repository.js";
+
+const ormCreateQuestion = async (question) => {
+ return createQuestion(question);
+};
+
+const ormGetAllQuestions = async () => {
+ return getAllQuestions();
+};
+
+const ormGetQuestionById = async (id) => {
+ return getQuestionById(id);
+};
+
+const ormDeleteQuestionById = async (id) => {
+ return deleteQuestionById(id);
+};
+
+const ormUpdateQuestionById = async (id, question) => {
+ return updateQuestionById(id, question);
+};
+
+const ormGetFilteredQuestions = async (query) => {
+ return getFilteredQuestions(query);
+};
+
+const ormFindQuestion = async (query) => {
+ const filteredQuestions = await ormGetFilteredQuestions(query);
+ const randomIndex = Math.floor(Math.random() * filteredQuestions.length);
+ const randomQuestion = filteredQuestions[randomIndex];
+ return randomQuestion;
+};
+
+const ormGetQuestionsByDescription = async (description) => {
+ return getQuestionsByDescription(description);
+};
+
+const ormGetQuestionsByTitleAndDifficulty = async (title, difficulty) => {
+ return getQuestionsByTitleAndDifficulty(title, difficulty);
+};
+
+const ormGetDistinctCategories = async () => {
+ return getDistinctCategories();
+};
+
+export {
+ ormCreateQuestion,
+ ormGetAllQuestions,
+ ormGetQuestionById,
+ ormDeleteQuestionById,
+ ormUpdateQuestionById,
+ ormGetFilteredQuestions,
+ ormFindQuestion,
+ ormGetQuestionsByDescription,
+ ormGetQuestionsByTitleAndDifficulty,
+ ormGetDistinctCategories,
+};
diff --git a/question-service/models/repository.js b/question-service/models/repository.js
new file mode 100644
index 0000000000..6244a730fe
--- /dev/null
+++ b/question-service/models/repository.js
@@ -0,0 +1,80 @@
+import Question from "./model.js";
+
+const createQuestion = async (question) => {
+ const newQuestion = new Question(question);
+ newQuestion.difficulty = question.difficulty.toUpperCase();
+ newQuestion.categories = question.categories.map((category) =>
+ category.toUpperCase()
+ );
+
+ return newQuestion.save();
+};
+
+const getAllQuestions = async () => {
+ return Question.find({ isDeleted: false });
+};
+
+const getQuestionById = async (id) => {
+ return Question.findById(id);
+};
+
+const deleteQuestionById = async (id) => {
+ // soft delete
+ return Question.findByIdAndUpdate(id, { isDeleted: true }, { new: true });
+};
+
+const updateQuestionById = async (id, question) => {
+ return Question.findByIdAndUpdate(id, question, { new: true });
+};
+
+const getFilteredQuestions = async (body) => {
+ const { categories, difficulty } = body;
+ let filter = { isDeleted: false };
+ if (categories) {
+ filter.categories = {
+ $in: categories.map((category) => category.toUpperCase()),
+ };
+ }
+ if (difficulty) {
+ filter.difficulty = {
+ $in: difficulty.map((difficulty) => difficulty.toUpperCase()),
+ };
+ }
+ return Question.find(filter);
+};
+
+const getQuestionsByDescription = async (description) => {
+ return Question.find({ description: description, isDeleted: false });
+};
+
+const getQuestionsByTitleAndDifficulty = async (title, difficulty) => {
+ return Question.find({
+ title: title,
+ difficulty: difficulty.toUpperCase(),
+ isDeleted: false,
+ });
+};
+
+const getDistinctCategories = async () => {
+ const distinctCategories = await Question.aggregate([
+ { $match: { isDeleted: false } },
+ { $unwind: "$categories" },
+ { $group: { _id: "$categories" } },
+ { $sort: { _id: 1 } },
+ ]);
+
+ const categories = distinctCategories.map((item) => item._id);
+ return categories;
+};
+
+export {
+ createQuestion,
+ getAllQuestions,
+ getQuestionById,
+ deleteQuestionById,
+ updateQuestionById,
+ getFilteredQuestions,
+ getQuestionsByDescription,
+ getQuestionsByTitleAndDifficulty,
+ getDistinctCategories,
+};
diff --git a/question-service/package-lock.json b/question-service/package-lock.json
new file mode 100644
index 0000000000..67706ccc17
--- /dev/null
+++ b/question-service/package-lock.json
@@ -0,0 +1,1461 @@
+{
+ "name": "question-service",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "question-service",
+ "version": "1.0.0",
+ "license": "ISC",
+ "dependencies": {
+ "body-parser": "^1.20.3",
+ "cookie-parser": "^1.4.6",
+ "cors": "^2.8.5",
+ "dotenv": "^16.4.5",
+ "express": "^4.21.0",
+ "joi": "^17.13.3",
+ "jsonwebtoken": "^9.0.2",
+ "mongoose": "^8.6.3"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.5"
+ }
+ },
+ "node_modules/@hapi/hoek": {
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
+ "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="
+ },
+ "node_modules/@hapi/topo": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz",
+ "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==",
+ "dependencies": {
+ "@hapi/hoek": "^9.0.0"
+ }
+ },
+ "node_modules/@mongodb-js/saslprep": {
+ "version": "1.1.9",
+ "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz",
+ "integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==",
+ "dependencies": {
+ "sparse-bitfield": "^3.0.3"
+ }
+ },
+ "node_modules/@sideway/address": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
+ "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==",
+ "dependencies": {
+ "@hapi/hoek": "^9.0.0"
+ }
+ },
+ "node_modules/@sideway/formula": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
+ "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="
+ },
+ "node_modules/@sideway/pinpoint": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz",
+ "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="
+ },
+ "node_modules/@types/webidl-conversions": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
+ "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="
+ },
+ "node_modules/@types/whatwg-url": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
+ "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
+ "dependencies": {
+ "@types/webidl-conversions": "*"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.13.0",
+ "raw-body": "2.5.2",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/bson": {
+ "version": "6.8.0",
+ "resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz",
+ "integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==",
+ "engines": {
+ "node": ">=16.20.1"
+ }
+ },
+ "node_modules/buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
+ "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-parser": {
+ "version": "1.4.6",
+ "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.6.tgz",
+ "integrity": "sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==",
+ "dependencies": {
+ "cookie": "0.4.1",
+ "cookie-signature": "1.0.6"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/cookie-parser/node_modules/cookie": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
+ "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
+ },
+ "node_modules/cors": {
+ "version": "2.8.5",
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+ "dependencies": {
+ "object-assign": "^4",
+ "vary": "^1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.4.5",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
+ "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/ecdsa-sig-formatter": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
+ "dependencies": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
+ "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+ "dependencies": {
+ "get-intrinsic": "^1.2.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.21.0",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz",
+ "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.3",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.6.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.3.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.10",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.13.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.19.0",
+ "serve-static": "1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
+ "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
+ "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ignore-by-default": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/joi": {
+ "version": "17.13.3",
+ "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
+ "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==",
+ "dependencies": {
+ "@hapi/hoek": "^9.3.0",
+ "@hapi/topo": "^5.1.0",
+ "@sideway/address": "^4.1.5",
+ "@sideway/formula": "^3.0.1",
+ "@sideway/pinpoint": "^2.0.0"
+ }
+ },
+ "node_modules/jsonwebtoken": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
+ "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
+ "dependencies": {
+ "jws": "^3.2.2",
+ "lodash.includes": "^4.3.0",
+ "lodash.isboolean": "^3.0.3",
+ "lodash.isinteger": "^4.0.4",
+ "lodash.isnumber": "^3.0.3",
+ "lodash.isplainobject": "^4.0.6",
+ "lodash.isstring": "^4.0.1",
+ "lodash.once": "^4.0.0",
+ "ms": "^2.1.1",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/jsonwebtoken/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/jwa": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
+ "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
+ "dependencies": {
+ "buffer-equal-constant-time": "1.0.1",
+ "ecdsa-sig-formatter": "1.0.11",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/jws": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
+ "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
+ "dependencies": {
+ "jwa": "^1.4.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/kareem": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.6.3.tgz",
+ "integrity": "sha512-C3iHfuGUXK2u8/ipq9LfjFfXFxAZMQJJq7vLS45r3D9Y2xQ/m4S8zaR4zMLFWh9AsNPXmcFfUDhTEO8UIC/V6Q==",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/lodash.includes": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
+ },
+ "node_modules/lodash.isboolean": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
+ },
+ "node_modules/lodash.isinteger": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
+ },
+ "node_modules/lodash.isnumber": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
+ },
+ "node_modules/lodash.isplainobject": {
+ "version": "4.0.6",
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
+ },
+ "node_modules/lodash.isstring": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
+ },
+ "node_modules/lodash.once": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/memory-pager": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
+ "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg=="
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/mongodb": {
+ "version": "6.8.0",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.8.0.tgz",
+ "integrity": "sha512-HGQ9NWDle5WvwMnrvUxsFYPd3JEbqD3RgABHBQRuoCEND0qzhsd0iH5ypHsf1eJ+sXmvmyKpP+FLOKY8Il7jMw==",
+ "dependencies": {
+ "@mongodb-js/saslprep": "^1.1.5",
+ "bson": "^6.7.0",
+ "mongodb-connection-string-url": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=16.20.1"
+ },
+ "peerDependencies": {
+ "@aws-sdk/credential-providers": "^3.188.0",
+ "@mongodb-js/zstd": "^1.1.0",
+ "gcp-metadata": "^5.2.0",
+ "kerberos": "^2.0.1",
+ "mongodb-client-encryption": ">=6.0.0 <7",
+ "snappy": "^7.2.2",
+ "socks": "^2.7.1"
+ },
+ "peerDependenciesMeta": {
+ "@aws-sdk/credential-providers": {
+ "optional": true
+ },
+ "@mongodb-js/zstd": {
+ "optional": true
+ },
+ "gcp-metadata": {
+ "optional": true
+ },
+ "kerberos": {
+ "optional": true
+ },
+ "mongodb-client-encryption": {
+ "optional": true
+ },
+ "snappy": {
+ "optional": true
+ },
+ "socks": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/mongodb-connection-string-url": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz",
+ "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==",
+ "dependencies": {
+ "@types/whatwg-url": "^11.0.2",
+ "whatwg-url": "^13.0.0"
+ }
+ },
+ "node_modules/mongoose": {
+ "version": "8.6.3",
+ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.6.3.tgz",
+ "integrity": "sha512-++yRmm7hjMbqVA/8WeiygTnEfrFbiy+OBjQi49GFJIvCQuSYE56myyQWo4j5hbpcHjhHQU8NukMNGTwAWFWjIw==",
+ "dependencies": {
+ "bson": "^6.7.0",
+ "kareem": "2.6.3",
+ "mongodb": "6.8.0",
+ "mpath": "0.9.0",
+ "mquery": "5.0.0",
+ "ms": "2.1.3",
+ "sift": "17.1.3"
+ },
+ "engines": {
+ "node": ">=16.20.1"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mongoose"
+ }
+ },
+ "node_modules/mongoose/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/mpath": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
+ "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/mquery": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/mquery/-/mquery-5.0.0.tgz",
+ "integrity": "sha512-iQMncpmEK8R8ncT8HJGsGc9Dsp8xcgYMVSbs5jgnm1lFHTZqMJTUWTDx1LBO8+mK3tPNZWFLBghQEIOULSTHZg==",
+ "dependencies": {
+ "debug": "4.x"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/mquery/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/mquery/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/nodemon": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.5.tgz",
+ "integrity": "sha512-V5UtfYc7hjFD4SI3EzD5TR8ChAHEZ+Ns7Z5fBk8fAbTVAj+q3G+w7sHJrHxXBkVn6ApLVTljau8wfHwqmGUjMw==",
+ "dev": true,
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "debug": "^4",
+ "ignore-by-default": "^1.0.1",
+ "minimatch": "^3.1.2",
+ "pstree.remy": "^1.1.8",
+ "semver": "^7.5.3",
+ "simple-update-notifier": "^2.0.0",
+ "supports-color": "^5.5.0",
+ "touch": "^3.1.0",
+ "undefsafe": "^2.0.5"
+ },
+ "bin": {
+ "nodemon": "bin/nodemon.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/nodemon"
+ }
+ },
+ "node_modules/nodemon/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/nodemon/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
+ "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.10",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
+ "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
+ },
+ "node_modules/picomatch": {
+ "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"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/pstree.remy": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/qs": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+ "dependencies": {
+ "side-channel": "^1.0.6"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "node_modules/semver": {
+ "version": "7.6.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+ "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.19.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
+ "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.4",
+ "object-inspect": "^1.13.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/sift": {
+ "version": "17.1.3",
+ "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz",
+ "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ=="
+ },
+ "node_modules/simple-update-notifier": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sparse-bitfield": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
+ "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
+ "dependencies": {
+ "memory-pager": "^1.0.2"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/touch": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true,
+ "bin": {
+ "nodetouch": "bin/nodetouch.js"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz",
+ "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==",
+ "dependencies": {
+ "punycode": "^2.3.0"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/undefsafe": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz",
+ "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==",
+ "dependencies": {
+ "tr46": "^4.1.1",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ }
+ }
+}
diff --git a/question-service/package.json b/question-service/package.json
new file mode 100644
index 0000000000..90ccf7bd99
--- /dev/null
+++ b/question-service/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "question-service",
+ "main": "index.js",
+ "version": "1.0.0",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "dev": "nodemon index.js"
+ },
+ "type": "module",
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "description": "",
+ "dependencies": {
+ "body-parser": "^1.20.3",
+ "cookie-parser": "^1.4.6",
+ "cors": "^2.8.5",
+ "dotenv": "^16.4.5",
+ "express": "^4.21.0",
+ "joi": "^17.13.3",
+ "jsonwebtoken": "^9.0.2",
+ "mongoose": "^8.6.3"
+ },
+ "devDependencies": {
+ "nodemon": "^3.1.5"
+ }
+}
diff --git a/question-service/router/router.js b/question-service/router/router.js
new file mode 100644
index 0000000000..46abf12836
--- /dev/null
+++ b/question-service/router/router.js
@@ -0,0 +1,33 @@
+import express from "express";
+import { createQuestion, getAllQuestions, getQuestionById, deleteQuestionById, updateQuestionById, getFilteredQuestions, findQuestion, getDistinctCategories } from "../controller/question-controller.js";
+import checkAdmin from "../middlewares/access-control.js";
+import { validateNewQuestion, validateUpdatedQuestion } from "../middlewares/validation.js";
+
+const router = express.Router();
+
+// CREATE NEW QUESTION
+router.post('/', checkAdmin, validateNewQuestion, createQuestion);
+
+// GET ALL QUESTIONS
+router.route("/").get(getAllQuestions);
+
+// GET QUESTION BY ID
+router.route("/id/:id").get(getQuestionById);
+
+// DELETE QUESTION BY ID
+router.route("/id/:id").delete(checkAdmin, deleteQuestionById);
+
+// UPDATE QUESTION BY ID
+router.route("/id/:id").put(checkAdmin, validateUpdatedQuestion, updateQuestionById);
+
+// GET ALL QUESTIONS BY CATEGORY & DIFFICULTY (CAN HAVE MULTIPLE/NO CATEGORIES/DIFFICULTIES)
+router.route("/filter/").get(getFilteredQuestions);
+
+// GET A RANDOM QUESTION BY CATEGORY &/OR DIFFICULTY (CAN HAVE MULTIPLE/NO CATEGORIES/DIFFICULTIES)
+// IF BOTH CATEGORIES & DIFFICULTY ARE PROVIDED, NEED TO SATISFY EITHER ONE OF EACH
+router.route("/random/").get(findQuestion);
+
+// GET ALL DISTINCT CATEGORIES
+router.route("/categories").get(getDistinctCategories);
+
+ export default router;
diff --git a/user-service/Dockerfile b/user-service/Dockerfile
new file mode 100644
index 0000000000..d6d43664b6
--- /dev/null
+++ b/user-service/Dockerfile
@@ -0,0 +1,15 @@
+# https://github.com/awslabs/aws-lambda-web-adapter/tree/main/examples/expressjs
+FROM public.ecr.aws/docker/library/node:20
+
+# expose 8001 port for access to the container
+ENV PORT=8001
+EXPOSE 8001
+
+# copy all files into the Docker container
+COPY . .
+
+# install node libraries
+RUN npm install
+
+# execute the server
+CMD ["node", "server.js"]
diff --git a/user-service/controllers/auth-controller.js b/user-service/controllers/auth-controller.js
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/user-service/controllers/user-controller.js b/user-service/controllers/user-controller.js
index e69de29bb2..f2a134052f 100644
--- a/user-service/controllers/user-controller.js
+++ b/user-service/controllers/user-controller.js
@@ -0,0 +1,213 @@
+import { ormCreateUser, ormDeleteUser, ormFindUser, ormUpdateUser } from '../models/user-orm.js';
+import { comparePassword, hashPassword, generateAccessToken, checkPasswordStrength } from '../services.js';
+
+export async function loginUser(req, res) {
+ try {
+ const { email, password } = req.body;
+
+ // Check if email and password are provided
+ if (!email || !password) {
+ return res.status(400).json({ message: "Email and password are required" })
+ }
+
+ // Check if user exists
+ const user = await ormFindUser(email);
+ console.log(user)
+ if (!user) {
+ return res.status(401).json({ message: "Incorrect email or password" })
+ }
+
+ // Delete password field from user object
+ const returnedUser = { ...user }
+ delete returnedUser.password
+
+ // Check if password is correct
+ const isCorrectPassword = await comparePassword(password, user.password);
+ if (!isCorrectPassword) {
+ return res.status(401).json({ message: "Incorrect email or password" })
+ }
+
+ // Generate access token
+ const accessToken = generateAccessToken(user);
+ console.log(accessToken)
+ if (process.env.NODE_ENV === 'DEV') {
+ res.cookie('accessToken', accessToken, { httpOnly: true, sameSite: 'none', secure: true }); // 60 minutes
+ } else {
+ res.cookie('accessToken', accessToken, { httpOnly: true }); // 60 minutes
+ }
+
+ return res.status(200).json({ message: "Login successful", user: returnedUser })
+ } catch (error) {
+ console.log(error)
+ return res.status(500).json({ message: "Unknown server error" })
+ }
+}
+
+export async function logoutUser(req, res) {
+ try {
+ // Clear access token cookie
+ res.clearCookie('accessToken');
+ return res.status(200).json({ message: "Logout successful" })
+ } catch (error) {
+ console.log(error)
+ return res.status(500).json({ message: "Unknown server error" })
+ }
+}
+
+export async function createUser(req, res) {
+ try {
+ const { email, password, displayName } = req.body;
+
+ // Check if email, password and displayName are provided
+ if (!email || !password || !displayName) {
+ return res.status(400).json({ message: "Email, password and displayName are required" })
+ }
+
+ // Check if user already exists
+ const existingUser = await ormFindUser(email);
+ console.log(existingUser)
+ if (existingUser) {
+ return res.status(409).json({ message: "Email already exists" })
+ }
+
+ // Check if password is strong enough
+ if (!checkPasswordStrength(password)) {
+ return res.status(400).json({
+ message: "Password does not meet strength requirement. "
+ + "Passwords should have minimum 8 characters, with at least alphabets and numbers",
+ });
+ }
+
+ // Hash password and create user
+ const hashedPassword = hashPassword(password);
+ const user = await ormCreateUser(email, hashedPassword, displayName);
+ if (!user) {
+ return res.status(500).json({ message: "Unknown server error" })
+ }
+
+ // Delete password field from user object
+ const returnedUser = { ...user }
+ delete returnedUser.password
+
+ return res.status(201).json({ message: "New user created successfully", user: returnedUser })
+ } catch (error) {
+ console.log(error)
+ return res.status(500).json({ message: "Unknown server error" })
+ }
+}
+
+export async function deleteUser(req, res) {
+ try {
+ const email = req.user.email; // Delete the user specified from token
+
+ // Check if email is provided
+ if (!email) {
+ return res.status(400).json({ message: "Email is required" })
+ }
+
+ // Check if user exists
+ const existingUser = await ormFindUser(email);
+ console.log(existingUser)
+ if (!existingUser) {
+ return res.status(404).json({ message: "User not found" })
+ }
+
+ // Delete user
+ const user = await ormDeleteUser(email);
+ if (!user) {
+ return res.status(500).json({ message: "Unknown server error" })
+ }
+
+ // Delete password field from user object
+ const returnedUser = { ...user }
+ delete returnedUser.password
+
+ // Clear access token cookie
+ res.clearCookie('accessToken');
+
+ return res.status(200).json({ message: "User deleted successfully", user: returnedUser })
+ } catch (error) {
+ console.log(error)
+ return res.status(500).json({ message: "Unknown server error" })
+ }
+}
+
+export async function changePassword(req, res) {
+ try {
+ const { password, newPassword } = req.body;
+ const email = req.user.email; // Change password for logged in user from token
+
+ // Check if email, password and newPassword are provided
+ if (!email || !password || !newPassword) {
+ return res.status(400).json({ message: "Email, password and newPassword are required" })
+ }
+
+ // Check if user exists
+ const existingUser = await ormFindUser(email);
+ console.log(existingUser)
+ if (!existingUser) {
+ return res.status(404).json({ message: "User not found" })
+ }
+
+ // Check if password is correct
+ const isCorrectPassword = await comparePassword(password, existingUser.password);
+ if (!isCorrectPassword) {
+ return res.status(401).json({ message: "Incorrect old password needed to change password" })
+ }
+
+ // Check if password is strong enough
+ if (!checkPasswordStrength(newPassword)) {
+ return res.status(400).json({
+ message: "Password does not meet strength requirement. "
+ + "Passwords should have minimum 8 characters, with at least alphabets and numbers",
+ });
+ }
+
+ // Hash new password and update user
+ const hashedNewPassword = hashPassword(newPassword);
+ const updatedUser = await ormUpdateUser(email, { password: hashedNewPassword });
+ if (!updatedUser) {
+ return res.status(500).json({ message: "Unknown server error" })
+ }
+
+ // Delete password field from user object
+ const returnedUser = { ...updatedUser }
+ delete returnedUser.password
+
+ return res.status(200).json({ message: "User password updated successfully", user: returnedUser })
+ } catch (error) {
+ return res.status(500).json({ message: "Unknown server error" })
+ }
+}
+
+export async function changeDisplayName(req, res) {
+ try {
+ const { newDisplayName } = req.body;
+ const email = req.user.email; // Change displayName for logged in user from token
+
+ // Check if email and newDisplayName are provided
+ if (!email || !newDisplayName) {
+ return res.status(400).json({ message: "Email and newDisplayName are required" })
+ }
+
+ // Check if user exists
+ const existingUser = await ormFindUser(email);
+ console.log(existingUser)
+ if (!existingUser) {
+ return res.status(404).json({ message: "User not found" })
+ }
+
+ const updatedUser = await ormUpdateUser(email, { displayName: newDisplayName });
+ if (!updatedUser) {
+ return res.status(500).json({ message: "Unknown server error" })
+ }
+
+ // Delete password field from user object
+ const returnedUser = { ...updatedUser }
+ delete returnedUser.password
+
+ return res.status(200).json({ message: "User display name updated successfully", user: returnedUser })
+ } catch (error) {
+ return res.status(500).json({ message: "Unknown server error" })
+ }
+}
\ No newline at end of file
diff --git a/user-service/middlewares/access-control.js b/user-service/middlewares/access-control.js
index e69de29bb2..a31f788f09 100644
--- a/user-service/middlewares/access-control.js
+++ b/user-service/middlewares/access-control.js
@@ -0,0 +1,43 @@
+import { verifyAccessToken } from "../services.js";
+
+export function verifyAuthMiddleware(req, res, next) {
+ const { accessToken } = req.cookies;
+
+ // Check if access token is in cookie
+ if (!accessToken) {
+ return res.status(401).json({ message: 'No token provided, you must be logged in first!' });
+ }
+
+ // Verify access token
+ const result = verifyAccessToken(accessToken);
+ console.log(result)
+ if (!result) {
+ return res.status(403).json({ message: 'Invalid token provided' })
+ }
+
+ // Set user object in request
+ req.user = { email: result.email, displayName: result.displayName, isAdmin: result.isAdmin };
+
+ return next();
+}
+
+export function verifyIsAdminMiddleware(req, res, next) {
+ const { accessToken } = req.cookies;
+
+ // Check if access token is in cookie
+ if (!accessToken) {
+ return res.status(401).json({ message: 'No token provided, you must be logged in first!' });
+ }
+
+ // Verify access token
+ const result = verifyAccessToken(accessToken);
+ console.log(result)
+ if (!result) {
+ return res.status(403).json({ message: 'Invalid token provided' })
+ }
+
+ // Set isAdmin in request
+ req.isAdmin = result.isAdmin;
+
+ return next();
+}
\ No newline at end of file
diff --git a/user-service/middlewares/cors.js b/user-service/middlewares/cors.js
new file mode 100644
index 0000000000..1828ef044b
--- /dev/null
+++ b/user-service/middlewares/cors.js
@@ -0,0 +1,20 @@
+import cors from "cors"
+
+const allowedOrigins = [
+ "http://localhost:5173", // frontend dev
+ "http://peerprep.s3-website-ap-southeast-1.amazonaws.com", // frontend prod
+ "http://peerprep-frontend-bucket.s3-website-ap-southeast-1.amazonaws.com", // frontend staging
+ "http://localhost:8001", // user service
+ "http://localhost:8002", // matching service
+ "http://localhost:8003", // question service
+ "http://localhost:8004", // collaboration service
+];
+
+const corsOptions = {
+ origin: allowedOrigins,
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
+ allowedHeaders: ['Content-Type', 'Authorization', 'X-Amz-Date', 'X-Api-Key' , 'X-Amz-Security-Token'],
+ credentials: true,
+};
+
+export default cors(corsOptions);
diff --git a/user-service/models/repository.js b/user-service/models/repository.js
index e69de29bb2..136da08bb3 100644
--- a/user-service/models/repository.js
+++ b/user-service/models/repository.js
@@ -0,0 +1,18 @@
+import UserModel from "../models/user-model.js"
+
+
+export async function _createUser(param) {
+ return UserModel.create(param);
+}
+
+export async function _deleteUser(param) {
+ return UserModel.findOneAndDelete({ email: param });
+}
+
+export async function _findUser(param) {
+ return UserModel.findOne({ email: param });
+}
+
+export async function _updateUser({ email, prop }) {
+ return UserModel.findOneAndUpdate({ email: email }, prop, { new: true });
+}
\ No newline at end of file
diff --git a/user-service/models/user-model.js b/user-service/models/user-model.js
index e69de29bb2..8c83427783 100644
--- a/user-service/models/user-model.js
+++ b/user-service/models/user-model.js
@@ -0,0 +1,29 @@
+import mongoose from "mongoose";
+
+const userSchema = mongoose.Schema(
+ {
+ email: {
+ type: String,
+ required: true
+ },
+ password: {
+ type: String,
+ required: true
+ },
+ displayName: {
+ type: String,
+ required: true
+ },
+ isAdmin: {
+ type: Boolean,
+ required: true,
+ default: false,
+ }
+ },
+ {
+ timestamps: true
+ }
+);
+
+const UserModel = mongoose.model("User", userSchema)
+export default UserModel;
\ No newline at end of file
diff --git a/user-service/models/user-orm.js b/user-service/models/user-orm.js
index e69de29bb2..cb3592e67b 100644
--- a/user-service/models/user-orm.js
+++ b/user-service/models/user-orm.js
@@ -0,0 +1,41 @@
+import { _createUser, _deleteUser, _findUser, _updateUser } from "./repository.js"
+
+export async function ormCreateUser(email, password, displayName) {
+ try {
+ const user = await _createUser({ email, password, displayName });
+ return user.toObject();
+ } catch (error) {
+ console.log(`Error: could not create user due to: ${error}`);
+ return undefined;
+ }
+};
+
+export async function ormDeleteUser(email) {
+ try {
+ const user = await _deleteUser(email);
+ return user.toObject();
+ } catch (error) {
+ console.log(`Error: could not delete user due to: ${error}`);
+ return undefined;
+ }
+};
+
+export async function ormFindUser(email) {
+ try {
+ const user = await _findUser(email);
+ return user.toObject();
+ } catch (error) {
+ console.log(`Error: could not find user due to: ${error}`);
+ return undefined;
+ }
+};
+
+export async function ormUpdateUser(email, prop) {
+ try {
+ const user = await _updateUser({ email, prop });
+ return user.toObject();
+ } catch (error) {
+ console.log(`Error: could not update user due to: ${error}`);
+ return undefined;
+ }
+};
\ No newline at end of file
diff --git a/user-service/package-lock.json b/user-service/package-lock.json
index 0903ba5526..c3cc08b76c 100644
--- a/user-service/package-lock.json
+++ b/user-service/package-lock.json
@@ -14,7 +14,9 @@
"cors": "^2.8.5",
"express": "^4.21.0",
"jsonwebtoken": "^9.0.2",
- "mongoose": "^8.6.3",
+ "mongoose": "^8.6.2"
+ },
+ "devDependencies": {
"nodemon": "^3.1.7"
}
},
@@ -119,6 +121,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
@@ -172,6 +175,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
"engines": {
"node": ">=8"
},
@@ -215,6 +219,7 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
"dependencies": {
"fill-range": "^7.1.1"
},
@@ -265,6 +270,7 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
@@ -330,9 +336,9 @@
}
},
"node_modules/cookie": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
- "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
+ "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"engines": {
"node": ">= 0.6"
}
@@ -349,6 +355,14 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/cookie-parser/node_modules/cookie": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
+ "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/cookie-signature": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
@@ -519,18 +533,11 @@
"node": ">= 0.10.0"
}
},
- "node_modules/express/node_modules/cookie": {
- "version": "0.6.0",
- "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
- "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
- "engines": {
- "node": ">= 0.6"
- }
- },
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
"dependencies": {
"to-regex-range": "^5.0.1"
},
@@ -602,6 +609,7 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
@@ -681,6 +689,7 @@
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
"dependencies": {
"is-glob": "^4.0.1"
},
@@ -703,6 +712,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"
}
@@ -818,7 +828,8 @@
"node_modules/ignore-by-default": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
- "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
+ "dev": true
},
"node_modules/inflight": {
"version": "1.0.6",
@@ -847,6 +858,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
"dependencies": {
"binary-extensions": "^2.0.0"
},
@@ -858,6 +870,7 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -874,6 +887,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
"dependencies": {
"is-extglob": "^2.1.1"
},
@@ -885,6 +899,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"
}
@@ -1166,9 +1181,9 @@
}
},
"node_modules/mongoose": {
- "version": "8.6.3",
- "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.6.3.tgz",
- "integrity": "sha512-++yRmm7hjMbqVA/8WeiygTnEfrFbiy+OBjQi49GFJIvCQuSYE56myyQWo4j5hbpcHjhHQU8NukMNGTwAWFWjIw==",
+ "version": "8.6.2",
+ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.6.2.tgz",
+ "integrity": "sha512-ErbDVvuUzUfyQpXvJ6sXznmZDICD8r6wIsa0VKjJtB6/LZncqwUn5Um040G1BaNo6L3Jz+xItLSwT0wZmSmUaQ==",
"dependencies": {
"bson": "^6.7.0",
"kareem": "2.6.3",
@@ -1291,6 +1306,7 @@
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz",
"integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==",
+ "dev": true,
"dependencies": {
"chokidar": "^3.5.2",
"debug": "^4",
@@ -1318,6 +1334,7 @@
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dev": true,
"dependencies": {
"ms": "^2.1.3"
},
@@ -1333,7 +1350,8 @@
"node_modules/nodemon/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true
},
"node_modules/nopt": {
"version": "5.0.0",
@@ -1353,6 +1371,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -1432,6 +1451,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"
},
@@ -1454,7 +1474,8 @@
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
- "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
+ "dev": true
},
"node_modules/punycode": {
"version": "2.3.1",
@@ -1517,6 +1538,7 @@
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
"dependencies": {
"picomatch": "^2.2.1"
},
@@ -1681,6 +1703,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
"integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
+ "dev": true,
"dependencies": {
"semver": "^7.5.3"
},
@@ -1740,6 +1763,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"
},
@@ -1767,6 +1791,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"
},
@@ -1786,6 +1811,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
"integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
+ "dev": true,
"bin": {
"nodetouch": "bin/nodetouch.js"
}
@@ -1816,7 +1842,8 @@
"node_modules/undefsafe": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
- "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
+ "dev": true
},
"node_modules/unpipe": {
"version": "1.0.0",
diff --git a/user-service/package.json b/user-service/package.json
index ec1153d4a9..3e20ddfcd4 100644
--- a/user-service/package.json
+++ b/user-service/package.json
@@ -17,7 +17,9 @@
"cors": "^2.8.5",
"express": "^4.21.0",
"jsonwebtoken": "^9.0.2",
- "mongoose": "^8.6.3",
+ "mongoose": "^8.6.2"
+ },
+ "devDependencies": {
"nodemon": "^3.1.7"
}
}
diff --git a/user-service/routers/auth-router..js b/user-service/routers/auth-router..js
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/user-service/routers/user-router.js b/user-service/routers/user-router.js
index e69de29bb2..6c1e935bd2 100644
--- a/user-service/routers/user-router.js
+++ b/user-service/routers/user-router.js
@@ -0,0 +1,15 @@
+import express from "express"
+import { loginUser, logoutUser, createUser, deleteUser, changePassword, changeDisplayName } from "../controllers/user-controller.js"
+import { verifyAuthMiddleware } from "../middlewares/access-control.js"
+
+const userRouter = express.Router()
+
+userRouter
+ .post("/login", loginUser)
+ .post("/logout", logoutUser)
+ .post("/", createUser)
+ .delete("/", verifyAuthMiddleware, deleteUser)
+ .put("/changePassword", verifyAuthMiddleware, changePassword)
+ .put("/changeDisplayName", verifyAuthMiddleware, changeDisplayName)
+
+export default userRouter
\ No newline at end of file
diff --git a/user-service/server.js b/user-service/server.js
index e69de29bb2..d22ad3ff16 100644
--- a/user-service/server.js
+++ b/user-service/server.js
@@ -0,0 +1,32 @@
+import express from "express"
+import cookieParser from 'cookie-parser';
+import userRouter from "./routers/user-router.js"
+import corsMiddleware from "./middlewares/cors.js"
+import { connectToDB } from "./services.js";
+
+// Create express app
+const app = express()
+
+// Initialise middlewares
+app.use(cookieParser())
+app.use(express.json())
+app.use(express.urlencoded({ extended: true }))
+app.use(corsMiddleware);
+
+// Initialise routers
+app.use("/api/user-service/users", userRouter)
+
+// Test route
+app.get("/", (req, res) => {
+ res.status(200).json({ message: "Connected to / route of user-service" })
+})
+
+// Connect to DB, then listen at port
+await connectToDB().then(() => {
+ console.log("MongoDB Connected!");
+ app.listen(process.env.PORT);
+ console.log(`User service listening at port ${process.env.PORT}`)
+}).catch((error) => {
+ console.log("Failed to connect to DB");
+ console.log(error);
+});
\ No newline at end of file
diff --git a/user-service/services.js b/user-service/services.js
index e69de29bb2..9c55782d5e 100644
--- a/user-service/services.js
+++ b/user-service/services.js
@@ -0,0 +1,35 @@
+import bcrypt from 'bcrypt';
+import jwt from 'jsonwebtoken';
+import { connect } from 'mongoose';
+
+export async function connectToDB() {
+ let mongoDBUri = process.env.MONGO_PROD_URI;
+ await connect(mongoDBUri);
+}
+
+export async function comparePassword(password, hashedPassword) {
+ return await bcrypt.compare(password, hashedPassword);
+}
+
+export function checkPasswordStrength(password) {
+ const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d@$!%*?&]{8,}$/;
+ return password.match(passwordRegex) !== null;
+}
+
+export function hashPassword(password) {
+ const salt = bcrypt.genSaltSync(10);
+ return bcrypt.hashSync(password, salt);
+}
+
+export function generateAccessToken({ email, displayName, isAdmin }) {
+ return jwt.sign({ email: email, displayName: displayName, isAdmin: isAdmin }, process.env.ACCESS_TOKEN_SECRET, { expiresIn: '15m' });
+}
+
+export function verifyAccessToken(token) {
+ return jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
+ if (err) {
+ return null;
+ }
+ return user;
+ });
+}
\ No newline at end of file