diff --git a/.vscode/settings.json b/.vscode/settings.json index 7a067b7c..9c43a83f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,5 @@ { - "cssModules.camelCase": true + "cssModules.camelCase": true, + "editor.formatOnSave": true, + "files.autoSave": "afterDelay" } diff --git a/README.md b/README.md index 6842e48e..df3b6863 100644 --- a/README.md +++ b/README.md @@ -15,15 +15,19 @@ An interplanetary town square that maintains connections between cultures and id - Samarjit Bhogal ([@SamarjitBhogal](https://github.com/SamarjitBhogal)) - Marcus Lages ([@MarcusLages](https://github.com/MarcusLages)) -## Core Technologies +## Technologies -The project is built using Typescript, and split into two modules, the client and server. +The project is built using [Typescript](https://www.typescriptlang.org/), and split into two modules, the client and server. **Client (built with [Vite](https://vitejs.dev/)):** - [React](https://react.dev/) +- [React Bootstrap](https://react-bootstrap.netlify.app/) +- [React Icons](https://react-icons.github.io/react-icons/) - [Wouter](https://www.npmjs.com/package/wouter) - [CSS Modules](https://github.com/css-modules/css-modules) (implemented by Vite) +- [Bootstrap](https://getbootstrap.com/) +- [axios](https://axios-http.com/) **Server (built with [esno](https://www.npmjs.com/package/esno)):** @@ -32,10 +36,21 @@ The project is built using Typescript, and split into two modules, the client an - [Express](https://expressjs.com/) - [Express File Routing](https://www.npmjs.com/package/express-file-routing) - [Joi](https://joi.dev/) +- [bcrypt](https://github.com/kelektiv/node.bcrypt.js) +- [http-status-codes](https://www.npmjs.com/package/http-status-codes) +- [nodemailer](https://www.nodemailer.com/) +- [JWT](https://jwt.io/) -**Repository Management:** +**Development Utilities:** - [Prettier](https://prettier.io/) for consistent code formatting, see `.prettierrc`. +- [morgan](https://expressjs.com/en/resources/middleware/morgan.html) for request logging +- [picocolors](https://www.npmjs.com/package/picocolors) for adding colour to request logging + +## Code Attributions + +- Regex escape utility: [(`./server/utils/regex.ts:10`)](https://github.com/Tianyou-Xie/2800_202410_BBY07/blob/dev/server/src/utils/regex.ts#L10) + > https://github.com/component/escape-regexp/blob/master/index.js ## Environment Variables @@ -51,15 +66,18 @@ Both the server and client utilize a `.env` file. **Client Variables:** -(TODO) +| Key | Usage | +| ---- | ------------------------------ | +| PORT | Port used for the frontend app | **Server Variables:** -MongoDB variables coming soon (TODO) - -| Key | Usage | -| ---- | -------------------------------- | -| PORT | Port used for the express server | +| Key | Usage | +| ---------- | --------------------------------------------- | +| PORT | Port used for the express server | +| MONGO_URL | The MongoDB connection string | +| JWT_TTL | The JWT token expiry time, in seconds | +| JWT_SECRET | The secret used to sign and verify JWT tokens | ## Running Locally & Deployment diff --git a/client/index.html b/client/index.html index e5caa36b..f05b4738 100644 --- a/client/index.html +++ b/client/index.html @@ -3,10 +3,11 @@ + Skynet -
+
diff --git a/client/package-lock.json b/client/package-lock.json index 22e29814..3d881abb 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -6,18 +6,39 @@ "": { "name": "skynet-client", "dependencies": { + "@popperjs/core": "^2.11.8", + "axios": "^1.6.8", + "bootstrap": "^5.3.3", + "dotenv": "^16.4.5", "react": "^18.3.1", + "react-bootstrap": "^2.10.2", "react-dom": "^18.3.1", + "react-icons": "^5.2.1", + "react-if": "^4.1.5", + "react-toastify": "^10.0.5", "wouter": "^3.1.2" }, "devDependencies": { + "@types/node": "^20.12.12", "@types/react": "^18.3.1", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react-swc": "^3.6.0", + "sass": "^1.77.1", "typescript": "^5.4.5", "vite": "^5.2.11" } }, + "node_modules/@babel/runtime": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", + "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", @@ -386,6 +407,68 @@ "node": ">=12" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.3.tgz", + "integrity": "sha512-5bUZ93dmvHFcmfUcEN7qzYe8yQQ8JY+nHN6m9/iSDCQ/QmCiE0kWXYwhurjw5ch6I8WokQzx66xKIMHBAa4NNA==", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0" + } + }, + "node_modules/@restart/hooks": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", + "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@restart/ui": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.6.9.tgz", + "integrity": "sha512-mUbygUsJcRurjZCt1f77gg4DpheD1D+Sc7J3JjAkysUj7t8m4EBJVOqWC9788Qtbc69cJ+HlJc6jBguKwS8Mcw==", + "dependencies": { + "@babel/runtime": "^7.21.0", + "@popperjs/core": "^2.11.6", + "@react-aria/ssr": "^3.5.0", + "@restart/hooks": "^0.4.9", + "@types/warning": "^3.0.0", + "dequal": "^2.0.3", + "dom-helpers": "^5.2.0", + "uncontrollable": "^8.0.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, + "node_modules/@restart/ui/node_modules/uncontrollable": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz", + "integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==", + "peerDependencies": { + "react": ">=16.14.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.17.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", @@ -798,6 +881,14 @@ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", "dev": true }, + "node_modules/@swc/helpers": { + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.11.tgz", + "integrity": "sha512-YNlnKRWF2sVojTpIyzwou9XoTNbzbzONwRhOoniEioF1AtaitTvVZblaQRrAzChWQ1bLYyYSWzM18y4WwgzJ+A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@swc/types": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.6.tgz", @@ -813,17 +904,24 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/@types/node": { + "version": "20.12.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", + "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "dev": true + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" }, "node_modules/@types/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz", "integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==", - "dev": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -838,6 +936,19 @@ "@types/react": "*" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/warning": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", + "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==" + }, "node_modules/@vitejs/plugin-react-swc": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.6.0.tgz", @@ -850,11 +961,164 @@ "vite": "^4 || ^5" } }, + "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/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "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/bootstrap": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", + "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, + "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==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "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/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/esbuild": { "version": "0.20.2", @@ -894,6 +1158,50 @@ "@esbuild/win32-x64": "0.20.2" } }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "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==", + "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", @@ -908,6 +1216,74 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "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/immutable": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", + "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", + "dev": true + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "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/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -924,6 +1300,25 @@ "loose-envify": "cli.js" } }, + "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/mitt": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", @@ -947,12 +1342,41 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "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/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "dev": true }, + "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/postcss": { "version": "8.4.38", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", @@ -981,6 +1405,33 @@ "node": "^10 || ^12 || >=14" } }, + "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/prop-types-extra": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", + "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", + "dependencies": { + "react-is": "^16.3.2", + "warning": "^4.0.0" + }, + "peerDependencies": { + "react": ">=0.14.0" + } + }, + "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==" + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -992,6 +1443,35 @@ "node": ">=0.10.0" } }, + "node_modules/react-bootstrap": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.2.tgz", + "integrity": "sha512-UvB7mRqQjivdZNxJNEA2yOQRB7L9N43nBnKc33K47+cH90/ujmnMwatTCwQLu83gLhrzAl8fsa6Lqig/KLghaA==", + "dependencies": { + "@babel/runtime": "^7.22.5", + "@restart/hooks": "^0.4.9", + "@restart/ui": "^1.6.8", + "@types/react-transition-group": "^4.4.6", + "classnames": "^2.3.2", + "dom-helpers": "^5.2.1", + "invariant": "^2.2.4", + "prop-types": "^15.8.1", + "prop-types-extra": "^1.1.0", + "react-transition-group": "^4.4.5", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "@types/react": ">=16.14.8", + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -1004,6 +1484,82 @@ "react": "^18.3.1" } }, + "node_modules/react-icons": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz", + "integrity": "sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-if": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/react-if/-/react-if-4.1.5.tgz", + "integrity": "sha512-Uk+Ub2gC83PAakuU4+7iLdTEP4LPi2ihNEPCtz/vr8SLGbzkMApbpYbkDZ5z9zYXurd0gg+EK/bpOLFFC1r1eQ==", + "workspaces": [ + "demo/" + ], + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": "^16.x || ^17.x || ^18.x" + } + }, + "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-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-toastify": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz", + "integrity": "sha512-mNKt2jBXJg4O7pSdbNUfDdTsK9FIdikfsIE/yUCxbAEXl4HMyJaivrVFcn3Elvt5xvCQYhUZm+hqTIu1UXM3Pw==", + "dependencies": { + "clsx": "^2.1.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "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/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/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/regexparam": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-3.0.0.tgz", @@ -1047,6 +1603,23 @@ "fsevents": "~2.3.2" } }, + "node_modules/sass": { + "version": "1.77.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.1.tgz", + "integrity": "sha512-OMEyfirt9XEfyvocduUIOlUSkWOXS/LAt6oblR/ISXCTukyavjex+zQNm51pPCOiFKY1QpWvEH1EeCkgyV3I6w==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -1064,6 +1637,23 @@ "node": ">=0.10.0" } }, + "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/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/typescript": { "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", @@ -1077,6 +1667,26 @@ "node": ">=14.17" } }, + "node_modules/uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "dependencies": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/use-sync-external-store": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", @@ -1140,6 +1750,14 @@ } } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/wouter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/wouter/-/wouter-3.1.2.tgz", diff --git a/client/package.json b/client/package.json index e83f5e3c..eae230b3 100644 --- a/client/package.json +++ b/client/package.json @@ -9,14 +9,24 @@ "dev": "vite" }, "dependencies": { + "@popperjs/core": "^2.11.8", + "axios": "^1.6.8", + "bootstrap": "^5.3.3", + "dotenv": "^16.4.5", "react": "^18.3.1", + "react-bootstrap": "^2.10.2", "react-dom": "^18.3.1", + "react-icons": "^5.2.1", + "react-if": "^4.1.5", + "react-toastify": "^10.0.5", "wouter": "^3.1.2" }, "devDependencies": { + "@types/node": "^20.12.12", "@types/react": "^18.3.1", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react-swc": "^3.6.0", + "sass": "^1.77.1", "typescript": "^5.4.5", "vite": "^5.2.11" } diff --git a/client/public/favicon.ico b/client/public/favicon.ico new file mode 100644 index 00000000..b12e3ecd Binary files /dev/null and b/client/public/favicon.ico differ diff --git a/client/src/app.tsx b/client/src/app.tsx new file mode 100644 index 00000000..57a51957 --- /dev/null +++ b/client/src/app.tsx @@ -0,0 +1,83 @@ +import { ToastContainer } from 'react-toastify'; +import { Switch, Route, Redirect, useLocation } from 'wouter'; +import About from './pages/about/about'; +import Changepassword from './pages/changepassword/changepassword'; +import Forgetpassword from './pages/forgetpassword/forgetpassword'; +import GeneralFeed from './pages/general-feed/general-feed'; +import Home from './pages/home/home'; +import Login from './pages/login/login-component'; +import MyFeed from './pages/my-feed/my-feed'; +import Signup from './pages/signup/signup-component'; +import Test from './pages/test-page/test-page'; +import UserSettings from './pages/user-settings/user-settings'; +import Resetpassword from './pages/resetpassword/resetpassword'; +import { useEffect, useState } from 'react'; +import { Auth } from './lib/auth'; + +import './index.css'; +import { Else, If, Then } from 'react-if'; + +export const App = () => { + const [authorized, setAuthorized] = useState(undefined); + const [loc] = useLocation(); + + useEffect(() => { + Auth.resaveToken(); + }, []); + + useEffect(() => { + Auth.isAuthorized().then((v) => setAuthorized(v === true)); + }, [loc]); + + const commonRoutes = ( + <> + + + + + {(params) => } + + + 404 Not Found + + ); + + return ( + <> + + + + + + + + + + + + {commonRoutes} + + + + + + + + + {commonRoutes} + + } /> + + + + +
+

Loading...

+
+
+
+
+
+ + ); +}; diff --git a/client/src/assets/fonts/BITSUMIS.TTF b/client/src/assets/fonts/BITSUMIS.TTF new file mode 100644 index 00000000..6d59a26b Binary files /dev/null and b/client/src/assets/fonts/BITSUMIS.TTF differ diff --git a/client/src/assets/fonts/BabaPro-Bold.ttf b/client/src/assets/fonts/BabaPro-Bold.ttf new file mode 100644 index 00000000..e7131f6a Binary files /dev/null and b/client/src/assets/fonts/BabaPro-Bold.ttf differ diff --git a/client/src/assets/fonts/FjallaOne-Regular.ttf b/client/src/assets/fonts/FjallaOne-Regular.ttf new file mode 100644 index 00000000..5f0d9f8a Binary files /dev/null and b/client/src/assets/fonts/FjallaOne-Regular.ttf differ diff --git a/client/src/assets/fonts/TT-Octosquares-Trial-Regular.ttf b/client/src/assets/fonts/TT-Octosquares-Trial-Regular.ttf new file mode 100644 index 00000000..2265d9ec Binary files /dev/null and b/client/src/assets/fonts/TT-Octosquares-Trial-Regular.ttf differ diff --git a/client/src/assets/images/SkynetLogo.png b/client/src/assets/images/SkynetLogo.png new file mode 100644 index 00000000..3561d4b7 Binary files /dev/null and b/client/src/assets/images/SkynetLogo.png differ diff --git a/client/src/assets/images/icons/android-chrome-192x192.png b/client/src/assets/images/icons/android-chrome-192x192.png new file mode 100644 index 00000000..02c9853e Binary files /dev/null and b/client/src/assets/images/icons/android-chrome-192x192.png differ diff --git a/client/src/assets/images/icons/android-chrome-512x512.png b/client/src/assets/images/icons/android-chrome-512x512.png new file mode 100644 index 00000000..a0a50325 Binary files /dev/null and b/client/src/assets/images/icons/android-chrome-512x512.png differ diff --git a/client/src/assets/images/icons/apple-touch-icon.png b/client/src/assets/images/icons/apple-touch-icon.png new file mode 100644 index 00000000..7c52e522 Binary files /dev/null and b/client/src/assets/images/icons/apple-touch-icon.png differ diff --git a/client/src/assets/images/icons/favicon-16x16.png b/client/src/assets/images/icons/favicon-16x16.png new file mode 100644 index 00000000..72e9b751 Binary files /dev/null and b/client/src/assets/images/icons/favicon-16x16.png differ diff --git a/client/src/assets/images/icons/favicon-32x32.png b/client/src/assets/images/icons/favicon-32x32.png new file mode 100644 index 00000000..a27df0fa Binary files /dev/null and b/client/src/assets/images/icons/favicon-32x32.png differ diff --git a/client/src/assets/images/icons/favicon.ico b/client/src/assets/images/icons/favicon.ico new file mode 100644 index 00000000..0350f251 Binary files /dev/null and b/client/src/assets/images/icons/favicon.ico differ diff --git a/client/src/assets/images/icons/site.webmanifest b/client/src/assets/images/icons/site.webmanifest new file mode 100644 index 00000000..45dc8a20 --- /dev/null +++ b/client/src/assets/images/icons/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/client/src/components/Header/Header.module.css b/client/src/components/Header/Header.module.css new file mode 100644 index 00000000..16a41598 --- /dev/null +++ b/client/src/components/Header/Header.module.css @@ -0,0 +1,17 @@ +.header-container { + background-color: #242C38; + padding-bottom: 0px; +} + +.return-icon { + font-size: 30pt; + color: #2196F3; +} + +.page-title { + font-size: 2.5em; + font-family: Bitsumishi; + display: flex; + justify-content: end; + color: #2196F3 !important; +} \ No newline at end of file diff --git a/client/src/components/Header/Header.tsx b/client/src/components/Header/Header.tsx new file mode 100644 index 00000000..30d49879 --- /dev/null +++ b/client/src/components/Header/Header.tsx @@ -0,0 +1,26 @@ +import styles from './Header.module.css'; +import { IoArrowBackCircleOutline } from 'react-icons/io5'; + +import Container from 'react-bootstrap/Container'; +import Navbar from 'react-bootstrap/Navbar'; + +interface Props { + pageName: string +} + +const Header = (props: Props) => { + return ( + + + + {props.pageName} + + + ); +}; + +export default Header; + +function handlePageReturn() { + history.back(); +} \ No newline at end of file diff --git a/client/src/components/Hotbar/Hotbar.module.css b/client/src/components/Hotbar/Hotbar.module.css new file mode 100644 index 00000000..333e1cf2 --- /dev/null +++ b/client/src/components/Hotbar/Hotbar.module.css @@ -0,0 +1,183 @@ +.nav-container p { + margin: 0; + padding: 0; +} + +.nav-container li a { + margin-right: 80px; + margin-top: -14px; + min-width: 80px; + padding: 1px; + text-decoration: none; + text-align: center; + flex-basis: 100%; + color: #2196F3; + font-family: TTOctosquares; +} + +.nav-container { + position: fixed; + bottom: 0%; + left: 0%; + width: 100vw; + margin-bottom: 105px; +} + +.nav { + position: relative; + width: 100vw; +} + +.circle { + position: absolute; + border: none; + background-color: #242C38; + padding: 60px; + border-radius: 50%; + z-index: 1; +} + +.circle-right { + right: 0%; + transform: translate(50%, 10%); +} + +.circle-left { + left: 0%; + transform: translate(-50%, 10%); +} + +.bar { + border: none; + position: absolute; + background-color: #242C38; + padding: 60px; + width: 100vw; + top: 50px; +} + +.nav-icon { + position: absolute; + z-index: 2; + font-size: 2em; + color: #2196F3; +} + +.icon-right { + right: 0%; +} + +.icon-left { + left: 0%; +} + +.menu-container { + position: absolute; + background-color: #2196F3; + z-index: 5; + padding: 10px; + border-radius: 50%; + right: 50%; + transform: translate(50%); +} + +.menu-backdrop { + position: absolute; + /*temporary fix for clear background*/ + background-color: transparent; + z-index: 3; + width: 110px; + border-radius: 50%; + padding: 50px; + right: 50%; + top: 0px; + transform: translate(50%); +} + +.menu-icon, .close-icon, .menu-sub-icons { + font-size: 4em; +} + +.nav-container ul.submenu { + position: absolute; + right: 50%; + top: 0%; + width: 140px; + transform: translateX(-50%); +} + +.nav-container input:checked ~ ul.submenu > li { + transform: rotate(calc((180deg / 5) * var(--item))) translateX(0px); + opacity: 1; +} + +.nav-container ul.submenu > li { + --item: 1; + position: absolute; + opacity: 0; + transform: rotate(calc((180deg / 5) * var(--item))) translateX(120px); + transform-origin: right center; + display: flex; + align-self: center; + transition: transform .7s, opacity .2s; +} + +.nav-container ul.submenu li:nth-child(1) { + --item: 1; +} + +.nav-container ul.submenu li:nth-child(2) { + --item: 2; +} + +.nav-container ul.submenu li:nth-child(3) { + --item: 3; +} + +.nav-container ul.submenu li:nth-child(4) { + --item: 4; +} + +.nav-container ul.submenu li:nth-child(1) a { + transform: rotate(-36deg); +} + +.nav-container ul.submenu li:nth-child(2) a { + transform: rotate(-72deg); +} + +.nav-container ul.submenu li:nth-child(3) a { + transform: rotate(-108deg); +} + +.nav-container ul.submenu li:nth-child(4) a { + transform: rotate(-144deg); +} + +.back-drop-div { + pointer-events: none; + background-color: rgba(255, 255, 255, 0.4); + position: absolute; + right: 50%; + bottom: 0%; + width: 400px; + height: 250px; + border-radius: 50% 50% 0 0; + transform: translateY(50%) translateX(52%); + backdrop-filter: blur(1px); + border: none; + box-shadow: 0px 0px 40px 40px rgba(255, 255, 255, 0.4); + + opacity: 0; + transition: transform .7s, opacity .7s; +} + +.nav-container input:checked ~ .back-drop-div { + opacity: 1; +} + +.menu-label { + position: absolute; + right: 50%; + transform: translateX(50%); +} \ No newline at end of file diff --git a/client/src/components/Hotbar/Hotbar.tsx b/client/src/components/Hotbar/Hotbar.tsx new file mode 100644 index 00000000..88d4cda7 --- /dev/null +++ b/client/src/components/Hotbar/Hotbar.tsx @@ -0,0 +1,88 @@ +import { useState } from 'react'; + +import styles from './Hotbar.module.css'; +import './hotbar-animation.css'; + +import { GoHomeFill } from 'react-icons/go'; +import { IoMdPerson } from 'react-icons/io'; +import { IoMenuSharp } from 'react-icons/io5'; +import { IoCloseCircleOutline } from 'react-icons/io5'; +import { IoIosAddCircle } from 'react-icons/io'; +import { MdFeed } from 'react-icons/md'; +import { AiFillMessage } from 'react-icons/ai'; +import { MdPeople } from 'react-icons/md'; + +const Hotbar = () => { + const [isChecked, setCheck] = useState(false); + + function toggleMenu() { + let menuIcon = document.getElementById('menu-icon'); + let closeIcon = document.getElementById('close-icon'); + if (menuIcon !== null && closeIcon !== null) { + menuIcon.classList.toggle('non-active'); + menuIcon.classList.toggle('active'); + closeIcon.classList.toggle('non-active'); + closeIcon.classList.toggle('active'); + } + setCheck(!isChecked); + } + + return ( + <> +
+
+
+ {/*

menu - menu - menu

*/} +
+ +
+ +
+ +
+ +
+ + + +
+
+
+ + + +
+
+ + ); +}; + +export default Hotbar; diff --git a/client/src/components/Hotbar/hotbar-animation.css b/client/src/components/Hotbar/hotbar-animation.css new file mode 100644 index 00000000..0b175aa0 --- /dev/null +++ b/client/src/components/Hotbar/hotbar-animation.css @@ -0,0 +1,39 @@ +#menu-icon { + position: absolute; +} + +#menu-icon.non-active { + animation: fade-out 300ms forwards; +} + +#close-icon.active { + animation: fade-in 300ms forwards; +} + +#close-icon.non-active { + animation: fade-out 300ms forwards; +} + +#menu-icon.active { + animation: fade-in 300ms forwards; +} + +@keyframes fade-out { + 0% { + opacity: 100%; + } + + 100% { + opacity: 0%; + } +} + +@keyframes fade-in { + 0% { + opacity: 0%; + } + + 100% { + opacity: 100%; + } +} \ No newline at end of file diff --git a/client/src/components/ModalConfirmation/ModalConfirmation.module.css b/client/src/components/ModalConfirmation/ModalConfirmation.module.css new file mode 100644 index 00000000..a1c38195 --- /dev/null +++ b/client/src/components/ModalConfirmation/ModalConfirmation.module.css @@ -0,0 +1,17 @@ +.modal button { + box-shadow: none; +} + +.header-font { + font-family: Bitsumishi; + font-size: 2em; +} + +.body-font { + font-family: TTOctosquares; + font-size: 14pt; +} + +.footer-font { + font-family: BabaPro; +} \ No newline at end of file diff --git a/client/src/components/ModalConfirmation/ModalConfirmation.tsx b/client/src/components/ModalConfirmation/ModalConfirmation.tsx new file mode 100644 index 00000000..f710f3c6 --- /dev/null +++ b/client/src/components/ModalConfirmation/ModalConfirmation.tsx @@ -0,0 +1,60 @@ +import styles from './ModalConfirmation.module.css'; + +import Modal from 'react-bootstrap/Modal'; +import Button from 'react-bootstrap/Button'; + +/** + * + */ +interface Props { + title: string; + header?: JSX.Element; + body: JSX.Element; + footer?: JSX.Element; + disableFooter: boolean; + show: boolean; + onHide: () => void; + onContinue?: any; +} + +/** + * + * @param props + * @returns + */ +const ModalConfirmation = (props: Props) => { + return ( + <> + + {!props.header ? ( + + {props.title} + + ) : ( +
{props.header}
+ )} + {props.body} + {!props.disableFooter ? ( + !props.footer ? ( + +
+ + +
+
+ ) : ( +
{props.footer}
+ ) + ) : ( + <> + )} +
+ + ); +}; + +export default ModalConfirmation; diff --git a/client/src/components/Page/Page.module.css b/client/src/components/Page/Page.module.css new file mode 100644 index 00000000..e26d791f --- /dev/null +++ b/client/src/components/Page/Page.module.css @@ -0,0 +1,38 @@ +.page-container { + width: 100%; + max-width: 100vw; + padding: 0px; + + display: grid; + grid-template-columns: 100%; + grid-template-rows: 84px 1fr 90px; + grid-template-areas: + 'header' + 'content' + 'navbar'; +} + +.noHeader { + grid-template-areas: + 'content' + 'content' + 'navbar'; +} + +.header { + z-index: 1; + grid-area: header; + position: fixed; + width: 100%; +} + +.content { + width: 100%; + padding: 0; + margin: 0; + grid-area: content; +} + +.navbar { + grid-area: navbar; +} diff --git a/client/src/components/Page/Page.tsx b/client/src/components/Page/Page.tsx new file mode 100644 index 00000000..aa4942d0 --- /dev/null +++ b/client/src/components/Page/Page.tsx @@ -0,0 +1,43 @@ +import styles from './Page.module.css'; + +import Header from '../Header/Header'; +import Hotbar from '..//Hotbar/Hotbar'; + +interface PageProp { + content: JSX.Element | JSX.Element[]; + pageName?: string; + noHeader?: boolean; +} + +/** + * Component for a page with content wrapped with header and hotbar. + * + * @param props.pageName string - (Optional) Name of the page + * @param props.content JSX.Element | JSX.Element[] - Content that will be added in the middle + * @param props.noHeader boolean - (Optional) Takes off the header + * @returns JSX.Element + */ +const Page = (props: PageProp) => { + const noHeader = props.noHeader ? styles.noHeader : ''; + const pageClass = styles.pageContainer + noHeader; + + return ( +
+ {props.noHeader ? ( + <> + ) : ( +
+
+
+ )} +
+
{props.content}
+
+
+ +
+
+ ); +}; + +export default Page; diff --git a/client/src/components/Post/Post.module.css b/client/src/components/Post/Post.module.css new file mode 100644 index 00000000..e072d210 --- /dev/null +++ b/client/src/components/Post/Post.module.css @@ -0,0 +1,60 @@ +.link { + color: inherit; + text-decoration: none; +} + +.post-container { + font-family: TTOctosquares; + max-width: 750px; + width: 100%; +} + +.user-container { + background-color: #000; + color: white; + padding: 2px 8px; + border-radius: 10px; + margin: 2px 0px; + width: max-content; + box-shadow: 4px 5px 0px var(--soft-shadow); + text-align: left; + font-size: 1rem; +} + +.para-container { + background: var(--signup-background); + color: #000; + padding: 10px; + padding-bottom: 5px; + border-radius: 10px; + margin: 2px 0px; + width: 100%; + border: 2px solid var(--soft-shadow); + box-shadow: 4px 5px 0px var(--soft-shadow); + text-align: left; + word-break: break-word; + font-size: 1.1rem; +} + +.post-date { + font-size: 0.6em; + color: var(--soft-shadow); +} + +.icons-container { + display: flex; + font-size: 1.75rem; + gap: 0.75rem; +} + +.icons-container button { + background-color: transparent; + padding: 5px; + width: auto; + box-shadow: none; + border: none; +} + +.share { + margin-right: auto; +} diff --git a/client/src/components/Post/Post.tsx b/client/src/components/Post/Post.tsx new file mode 100644 index 00000000..025f64de --- /dev/null +++ b/client/src/components/Post/Post.tsx @@ -0,0 +1,102 @@ +import { useState } from 'react'; + +import styles from './Post.module.css'; + +import { Container } from 'react-bootstrap'; + +// Icons +import { FaRegHeart } from 'react-icons/fa'; // //Empty heart +import { FaHeart } from 'react-icons/fa'; // // Filled heart +import { RiShareBoxLine } from 'react-icons/ri'; // +import { FaRegBookmark } from 'react-icons/fa'; // //Empty bookmark +import { FaBookmark } from 'react-icons/fa'; // //Filled bookmark +import { FaRocketchat } from 'react-icons/fa'; // +import { Link } from 'wouter'; + +interface PostProp { + username: string; + userURL: string; + text: string; + postURL: string; + createdAt?: Date; +} + +interface UserProp { + username: string; + userURL: string; + imageURL?: string; +} + +/** + * Component representing the user part of the post (image and username). + * + * @param props.username - Username of the author of the post (got from the Post's props). + * @param props.imageURL - (TODO) Will represent the user's profile pictures as an URL or image file. + * @param props.userURL - (TODO) Will link to the user's profile page. + */ +const User = (props: UserProp): JSX.Element => { + return ( + <> + + + {props.username} + + + + ); +}; + +/** + * Post component representing the thumbnail post of an user. + * + * @param props.username string - Username of the author of the post + * @param props.text string - Text of the post + * @param props.postURL string - URL of the complete version of the post with comments and more information + * @param props.createdAt Date - (optional) Date in which the post was created + */ +const Post = (props: PostProp): JSX.Element => { + const [bookmarked, setBookmarked] = useState(false); + const [liked, setLiked] = useState(false); + + function onShare() {} + + function onBookmark() { + setBookmarked(!bookmarked); + } + + function onComment() {} + + function onLike() { + setLiked(!liked); + } + + return ( +
+ +
+ +

{props.text}

+ {props.createdAt ? ( +

{props.createdAt.toLocaleDateString()}

+ ) : undefined} + +
+ + + + +
+
+
+ ); +}; + +export default Post; diff --git a/client/src/components/para/Para.module.css b/client/src/components/para/Para.module.css deleted file mode 100644 index b25fc051..00000000 --- a/client/src/components/para/Para.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.para-red { - color: red; -} diff --git a/client/src/components/para/Para.tsx b/client/src/components/para/Para.tsx deleted file mode 100644 index 9c8cce89..00000000 --- a/client/src/components/para/Para.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import styles from './Para.module.css'; - -interface Props { - text: string; -} - -const Para = (props: Props) => { - return

{props.text}

; -}; - -export default Para; diff --git a/client/src/index.css b/client/src/index.css index b2fb9531..8cbb32e2 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -1,3 +1,15 @@ +:root { + --signup-background: #f8f4e5; + --signup-input-focus: #ebe2c2; + --signup-button-active: #ebe2c2; + + --login-background: #f8f4e5; + --login-input-focus: #ebe2c2; + --login-button-active: #ebe2c2; + + --soft-shadow: #b8b199; +} + *, *::before, *::after { @@ -5,3 +17,38 @@ padding: 0; box-sizing: border-box; } + +html { + width: 100%; + height: 100%; +} + +body { + width: 100%; + height: 100%; + overflow-x: hidden; + /*This margin bottom is needed to make hotbar not interfere with other componenet*/ + /*If this margin bottom is doing something else bad please talk to sam. Thank you!*/ + margin-bottom: 20px; + background-color: aliceblue; +} + +@font-face { + font-family: Fjalla One; + src: url(./assets/fonts/FjallaOne-Regular.ttf); +} + +@font-face { + font-family: TTOctosquares; + src: url(./assets/fonts/TT-Octosquares-Trial-Regular.ttf); +} + +@font-face { + font-family: Bitsumishi; + src: url(./assets/fonts/BITSUMIS.TTF); +} + +@font-face { + font-family: BabaPro; + src: url(./assets/fonts/BabaPro-Bold.ttf); +} diff --git a/client/src/index.tsx b/client/src/index.tsx index dbe79e49..294db163 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -1,20 +1,10 @@ -import React from 'react'; import ReactDOM from 'react-dom/client'; -import { Route, Switch } from 'wouter'; -import IndexPage from './pages'; -import GoodbyePage from './pages/goodbye'; -import About from './pages/about'; - +import 'bootstrap/dist/css/bootstrap.min.css'; +import 'bootstrap/dist/js/bootstrap.min.js'; +import 'react-toastify/dist/ReactToastify.css'; import './index.css'; -ReactDOM.createRoot(document.getElementById('root')!).render( - - - - - - 404 Not Found - - , -); +import { App } from './app'; + +ReactDOM.createRoot(document.getElementById('root')!).render(); diff --git a/client/src/lib/auth.ts b/client/src/lib/auth.ts new file mode 100644 index 00000000..d6bc0e02 --- /dev/null +++ b/client/src/lib/auth.ts @@ -0,0 +1,73 @@ +import { api } from './axios'; + +/** + * Utility namespace for JWT token authorization. + */ +export namespace Auth { + const LS_KEY = 'jwt'; + + /** + * Formats the given token into an authorization header value. + * + * @param token the JWT token to format + * @returns the prefixed token, for an authorization header + */ + export function formatToken(token: string) { + return `Bearer ${token}`; + } + + /** + * Saves the token into the API axios instance and local storage. + * + * @param token the JWT token to save + */ + export function saveToken(token: string) { + api.defaults.headers.common.Authorization = formatToken(token); + localStorage.setItem(LS_KEY, token); + } + + /** + * Removes the current authorization header and removes the + * token from local storage. + */ + export function loseToken() { + api.defaults.headers.common.Authorization = ''; + localStorage.removeItem(LS_KEY); + } + + /** + * Retrieves the current token from local storage, if it exists. + * + * @returns the token, if one exists + */ + export function getToken() { + return localStorage.getItem(LS_KEY); + } + + /** + * Retrieves the token from local storage, and then sets + * the retrieved token. + * + * @returns whether any token was set + */ + export function resaveToken() { + const token = getToken(); + if (!token) return false; + + saveToken(token); + return true; + } + + /** + * Returns whether there is an active and valid token + * currently set. + */ + export async function isAuthorized() { + try { + await api.get('/user/login'); + return true; + } catch { + return false; + } + } +} diff --git a/client/src/lib/axios.ts b/client/src/lib/axios.ts new file mode 100644 index 00000000..d911b150 --- /dev/null +++ b/client/src/lib/axios.ts @@ -0,0 +1,32 @@ +import axios, { AxiosError, HttpStatusCode } from 'axios'; + +const isLocal = location.hostname === 'localhost' || location.hostname === '127.0.0.1'; +const isDev = import.meta.env.DEV; + +/** + * API Axios instance to access server API routes. + * + * This automatically handles the authorization header + * and URL host. + */ +export const api = axios.create({ + baseURL: isLocal + ? 'http://localhost:3000/' + : isDev + ? 'https://api.dev.skynetwork.app' + : 'https://api.skynetwork.app', +}); + +api.interceptors.response.use( + (res) => { + if (res.status === HttpStatusCode.Unauthorized) location.assign('/login'); + return res; + }, + (err) => { + if (!(err instanceof AxiosError)) return Promise.reject(err); + if (err.response?.status === HttpStatusCode.Unauthorized && location.pathname !== '/login') + // location.assign('/login'); + console.log(err); + return Promise.reject(err); + }, +); diff --git a/client/src/lib/callPosts.ts b/client/src/lib/callPosts.ts new file mode 100644 index 00000000..e69de29b diff --git a/client/src/pages/about/about.module.css b/client/src/pages/about/about.module.css new file mode 100644 index 00000000..94f248f2 --- /dev/null +++ b/client/src/pages/about/about.module.css @@ -0,0 +1,3 @@ +ul { + font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif; +} \ No newline at end of file diff --git a/client/src/pages/about/about.tsx b/client/src/pages/about/about.tsx new file mode 100644 index 00000000..6d591bd3 --- /dev/null +++ b/client/src/pages/about/about.tsx @@ -0,0 +1,18 @@ +const About = () => { + return ( + + + Team Name: BBY-07 Team Members: +
    +
  • Ole Lammers
  • +
  • Kamal Dolikay
  • +
  • Marcus V Lages
  • +
  • Tianyou Xie
  • +
  • Samarjit Bhogal
  • +
+ + + ); +}; + +export default About; diff --git a/client/src/pages/about/index.tsx b/client/src/pages/about/index.tsx deleted file mode 100644 index 3fa7a97b..00000000 --- a/client/src/pages/about/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -const About = () => { - return ( - - - Team Name: BBY-07 - Team Members: -
    -
  • Ole Lammers
  • -
  • Kamal Dolikay
  • -
  • Marcus V Lages
  • -
  • Tianyou Xie
  • -
  • Samarjit Bhogal
  • -
- - - ); -} - -export default About; \ No newline at end of file diff --git a/client/src/pages/changepassword/changepassword.module.css b/client/src/pages/changepassword/changepassword.module.css new file mode 100644 index 00000000..220389b0 --- /dev/null +++ b/client/src/pages/changepassword/changepassword.module.css @@ -0,0 +1,74 @@ +.changepassword-container { + display: grid; + align-items: center; + justify-items: center; + height: 80vh; + } + + .changepassword-form { + background: var(--signup-background); + padding: 48px 59px; + border: 2px solid black; + box-shadow: 4px 5px 0px #000; + } + + .changepassword-upperdiv { + background: var(--signup-background); + padding: 4px 0px; + border: 2px solid black; + box-shadow: 4px 3px 0px #000; + } + + .changepassword-bottomdiv { + background: var(--signup-background); + padding: 4px 0px; + border: 2px solid black; + box-shadow: 4px 4px 0px #000; + } + + .input { + display: block; + width: 100%; + line-height: 28pt; + margin-bottom: 20pt; + box-shadow: 4px 5px; + } + + .input:focus { + background: var(--signup-input-focus); + } + + .button { + padding: 2px; + width: 50%; + box-shadow: 4px 5px; + } + + .button:active { + box-shadow: 4px 4px var(--signup-button-active)5; + transform: translateY(5px); + } + + .select { + width: 100%; + margin-bottom: 20pt; + box-shadow: 4px 5px; + padding: 6px; + } + + .img { + max-width: 250px !important; + } + + .h1 { + font-family: Bitsumishi; + } + + .h5 { + font-family: BabaPro; + } + + .message { + font-family: BabaPro; + color: red; + } \ No newline at end of file diff --git a/client/src/pages/changepassword/changepassword.tsx b/client/src/pages/changepassword/changepassword.tsx new file mode 100644 index 00000000..80affabe --- /dev/null +++ b/client/src/pages/changepassword/changepassword.tsx @@ -0,0 +1,80 @@ +import styles from './changepassword.module.css'; +import logoUrl from '../../assets/images/SkynetLogo.png'; +import React, { useState } from 'react'; +import { api } from '../../lib/axios'; + +const Changepassword = () => { + const [password, setPassword] = useState(''); + const [newpassword, setNewPassword] = useState(''); + const [confirmpassword, setConfirmPassword] = useState(''); + const [message, setMessage] = useState(''); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + try { + const response = await api.patch('/user/changepassword', { + password, + newpassword, + confirmpassword, + }); + setMessage(response.data.message); + } catch (error: any) { + setMessage(error.response.data.message); + } + }; + + return ( +
+
+ Skynet Logo +

CHANGE PASSWORD

+ {/*
FORGET YOUR PASSWORD? WE ARE HERE TO HELP!
*/} +
+
+
+ setPassword(event.target.value)} + required + /> +
+ setNewPassword(event.target.value)} + required + /> +
+ setConfirmPassword(event.target.value)} + required + /> +
+
+ +
+
+
+
+
+
{message}
+
+
+ ); +}; + +export default Changepassword; diff --git a/client/src/pages/forgetpassword/forgetpassword.module.css b/client/src/pages/forgetpassword/forgetpassword.module.css new file mode 100644 index 00000000..1a2bb163 --- /dev/null +++ b/client/src/pages/forgetpassword/forgetpassword.module.css @@ -0,0 +1,74 @@ +.forgetpassword-container { + display: grid; + align-items: center; + justify-items: center; + height: 80vh; + } + + .forgetpassword-form { + background: var(--signup-background); + padding: 48px 59px; + border: 2px solid black; + box-shadow: 4px 5px 0px #000; + } + + .forgetpassword-upperdiv { + background: var(--signup-background); + padding: 4px 0px; + border: 2px solid black; + box-shadow: 4px 3px 0px #000; + } + + .forgetpassword-bottomdiv { + background: var(--signup-background); + padding: 4px 0px; + border: 2px solid black; + box-shadow: 4px 4px 0px #000; + } + + .input { + display: block; + width: 100%; + line-height: 28pt; + margin-bottom: 20pt; + box-shadow: 4px 5px; + } + + .input:focus { + background: var(--signup-input-focus); + } + + .button { + padding: 2px; + width: 50%; + box-shadow: 4px 5px; + } + + .button:active { + box-shadow: 4px 4px var(--signup-button-active)5; + transform: translateY(5px); + } + + .select { + width: 100%; + margin-bottom: 20pt; + box-shadow: 4px 5px; + padding: 6px; + } + + .img { + max-width: 250px !important; + } + + .h1 { + font-family: Bitsumishi; + } + + .h5 { + font-family: BabaPro; + } + + .message { + font-family: BabaPro; + color: red; + } \ No newline at end of file diff --git a/client/src/pages/forgetpassword/forgetpassword.tsx b/client/src/pages/forgetpassword/forgetpassword.tsx new file mode 100644 index 00000000..58010c43 --- /dev/null +++ b/client/src/pages/forgetpassword/forgetpassword.tsx @@ -0,0 +1,53 @@ +import styles from './forgetpassword.module.css'; +import logoUrl from '../../assets/images/SkynetLogo.png'; +import React, { useState } from 'react'; +import { api } from '../../lib/axios'; + +const Forgetpassword = () => { + const [email, setEmail] = useState(''); + const [message, setMessage] = useState(''); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + try { + const response = await api.post('/user/forgetpassword', { email }); + setMessage(response.data.message); + } catch (error: any) { + setMessage(error.response.data.message); + } + }; + + return ( +
+
+ Skynet Logo +

SKY.NET

+
FORGET YOUR PASSWORD? WE ARE HERE TO HELP!
+
+
+
+ setEmail(event.target.value)} + required + /> +
+
+ +
+
+
+
+
{message}
+
+
+ ); +}; + +export default Forgetpassword; diff --git a/client/src/pages/general-feed/general-feed.module.css b/client/src/pages/general-feed/general-feed.module.css new file mode 100644 index 00000000..e69de29b diff --git a/client/src/pages/general-feed/general-feed.tsx b/client/src/pages/general-feed/general-feed.tsx new file mode 100644 index 00000000..def4aad7 --- /dev/null +++ b/client/src/pages/general-feed/general-feed.tsx @@ -0,0 +1,54 @@ +import { useEffect } from 'react'; +import { api } from '../../lib/axios'; + +import Page from '../../components/Page/Page'; +import Post from '../../components/Post/Post'; + +const GeneralFeed = () => { + const displayedPosts: JSX.Element[] = []; + + // interface Reference { + // authorId: { username: string; userURL: string }; + // content: string; + // likeCount: number; + // commentCount: number; + // repostCount: number; + // createdAt: Date; + // } + + // const [planets, setPlanets] = useState>([]); + useEffect(() => { + async function fetchPost() { + try { + const res = await api.post('/post/6646a3b24b2d7d6a935eeea3'); + console.log(res); + } catch (err) { + console.log(err); + } + } + + fetchPost(); + }, []); + + const dummyPost = ( + + ); + + for (let i = 1; i < 10; i++) { + displayedPosts.push({ ...dummyPost, key: i.toString() }); + } + + return ( + + ); +}; + +export default GeneralFeed; diff --git a/client/src/pages/goodbye/index.tsx b/client/src/pages/goodbye/index.tsx deleted file mode 100644 index 15427d96..00000000 --- a/client/src/pages/goodbye/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import Para from '../../components/para/Para'; - -const GoodbyePage = () => { - return ; -}; - -export default GoodbyePage; diff --git a/client/src/pages/home/home.module.css b/client/src/pages/home/home.module.css new file mode 100644 index 00000000..1846d0a2 --- /dev/null +++ b/client/src/pages/home/home.module.css @@ -0,0 +1,34 @@ +.main-container { + background-color: var(--signup-background); + border: 2px solid black; + box-shadow: 4px 5px 0px black; + padding: 12px; + + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: center; + text-align: center; + gap: 1rem; +} + +.title { + font-family: Bitsumishi; +} + +.planets { + font-family: TTOctosquares; + font-size: 1.2rem; + border: 2px solid black; + box-shadow: 4px 5px 0px black; +} + +.planets:active { + box-shadow: 0px 0px var(--signup-button-active); + transform: translateY(5px); +} + +.planets-link { + text-decoration: none; + color: inherit; +} diff --git a/client/src/pages/home/home.tsx b/client/src/pages/home/home.tsx new file mode 100644 index 00000000..71625baf --- /dev/null +++ b/client/src/pages/home/home.tsx @@ -0,0 +1,41 @@ +import styles from './home.module.css'; + +import Page from '../../components/Page/Page'; +import { Link } from 'wouter'; + +interface PlanetProps { + planet: string; + url: string; +} + +const Planets = (props: PlanetProps) => { + return ( + +
{props.planet}
+ + ); +}; + +const Home = () => { + const mainContainer = ( +
+
+

Choose your planet

+ + + + + + + + + + +
+
+ ); + + return ; +}; + +export default Home; diff --git a/client/src/pages/index.tsx b/client/src/pages/index.tsx deleted file mode 100644 index cc4994e0..00000000 --- a/client/src/pages/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import Para from '../components/para/Para'; - -const IndexPage = () => { - return ; -}; - -export default IndexPage; diff --git a/client/src/pages/login/login-component.tsx b/client/src/pages/login/login-component.tsx new file mode 100644 index 00000000..c9ee9dc7 --- /dev/null +++ b/client/src/pages/login/login-component.tsx @@ -0,0 +1,57 @@ +import { useState } from 'react'; +import { api } from '../../lib/axios'; +import { useLocation } from 'wouter'; +import { toast } from 'react-toastify'; +import { Auth } from '../../lib/auth'; +import LoginHtml from './login-html'; + +const Login = () => { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + + const [_, setLocation] = useLocation(); + + const submitForm = async (e: any) => { + e.preventDefault(); + + const newUser = { + email: email, + password: password, + }; + + try { + const { data: res } = await api.post('/user/login', newUser); + if (res.success !== true || !res.value) throw 'Invalid password'; + + const token = res.value; + Auth.saveToken(token); + setLocation('/home'); + toast.success('login successfully'); + } catch (error: any) { + let err = error.response.data.success; + console.log(err); + toast.error('🦄 Wrong Credentials', { + position: 'top-right', + autoClose: 55000, + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + progress: undefined, + theme: 'colored', + }); + } + }; + + return ( + + ); +}; + +export default Login; diff --git a/client/src/pages/login/login-html.tsx b/client/src/pages/login/login-html.tsx new file mode 100644 index 00000000..cb3a2656 --- /dev/null +++ b/client/src/pages/login/login-html.tsx @@ -0,0 +1,54 @@ +import { useLocation } from 'wouter'; +import styles from './login.module.css'; +import logoUrl from '../../assets/images/SkynetLogo.png'; + +const LoginHtml = ({ email, password, setEmail, setPassword, submitForm }: any) => { + const [_, navigate] = useLocation(); + return ( +
+
+ Skynet Logo +

SKY.NET

+
Stay Connected Across The Galaxy
+
+
+
+ setEmail(e.target.value)} + /> + setPassword(e.target.value)} + /> +
+ Forget your password? + +
+
+
+ New User? Signup Below + +
+
+
+
+
+ ); +}; + +export default LoginHtml; diff --git a/client/src/pages/login/login.module.css b/client/src/pages/login/login.module.css new file mode 100644 index 00000000..a0e9d03b --- /dev/null +++ b/client/src/pages/login/login.module.css @@ -0,0 +1,86 @@ +.login-container { + display: grid; + align-items: center; + justify-items: center; +} + +.login-form { + background: var(--login-background); + padding: 29px 59px; + border: 2px solid black; + box-shadow: 4px 5px 0px #000; +} + +.login-upperdiv { + background: var(--login-background); + padding: 4px 0px; + border: 2px solid black; + box-shadow: 4px 3px 0px #000; +} + +.login-bottomdiv { + background: var(--login-background); + padding: 4px 0px; + border: 2px solid black; + box-shadow: 4px 4px 0px #000; +} + +.login-button-login { + box-shadow: 4px 5px gray !important; + background: black !important; + color: white !important; +} + +.login-button-login:active { + box-shadow: 4px 4px var(--login-button-active) 5 !important; + transform: translateY(5px) !important; +} + +.input, +.button { + font-size: 14pt; + font-family: 'BabaPro'; + background: var(--login-background); + border: 2px solid black; + outline: none; + padding-left: 5px; +} + +.input { + display: block; + width: 100%; + line-height: 28pt; + margin-bottom: 20pt; + box-shadow: 4px 5px; +} + +.button { + padding: 2px; + width: 50%; + box-shadow: 4px 5px; +} + +.button:active { + box-shadow: 4px 4px var(--login-button-active) 5; + transform: translateY(5px); +} + +.img { + max-width: 250px !important; +} + +.input:focus { + background: var(--login-input-focus); +} + +.h1 { + font-family: Bitsumishi; +} + +.h5 { + font-family: BabaPro; +} + +.span { + font-family: TTOctosquares; +} diff --git a/client/src/pages/my-feed/my-feed.module.css b/client/src/pages/my-feed/my-feed.module.css new file mode 100644 index 00000000..e69de29b diff --git a/client/src/pages/my-feed/my-feed.tsx b/client/src/pages/my-feed/my-feed.tsx new file mode 100644 index 00000000..c57b8bd5 --- /dev/null +++ b/client/src/pages/my-feed/my-feed.tsx @@ -0,0 +1,26 @@ +import styles from './my-feed.module.css'; +import Page from '../../components/Page/Page'; +import Post from '../../components/Post/Post'; + +const MyFeed = () => { + const displayedPosts: JSX.Element[] = []; + + const dummyPost = ( + + ); + + for (let i = 1; i < 10; i++) { + displayedPosts.push(dummyPost); + } + + return ( + + ); +}; + +export default MyFeed; diff --git a/client/src/pages/resetpassword/resetpassword.module.css b/client/src/pages/resetpassword/resetpassword.module.css new file mode 100644 index 00000000..47f74e5f --- /dev/null +++ b/client/src/pages/resetpassword/resetpassword.module.css @@ -0,0 +1,74 @@ +.resetpassword-container { + display: grid; + align-items: center; + justify-items: center; + height: 80vh; + } + + .resetpassword-form { + background: var(--signup-background); + padding: 48px 59px; + border: 2px solid black; + box-shadow: 4px 5px 0px #000; + } + + .resetpassword-upperdiv { + background: var(--signup-background); + padding: 4px 0px; + border: 2px solid black; + box-shadow: 4px 3px 0px #000; + } + + .resetpassword-bottomdiv { + background: var(--signup-background); + padding: 4px 0px; + border: 2px solid black; + box-shadow: 4px 4px 0px #000; + } + + .input { + display: block; + width: 100%; + line-height: 28pt; + margin-bottom: 20pt; + box-shadow: 4px 5px; + } + + .input:focus { + background: var(--signup-input-focus); + } + + .button { + padding: 2px; + width: 50%; + box-shadow: 4px 5px; + } + + .button:active { + box-shadow: 4px 4px var(--signup-button-active)5; + transform: translateY(5px); + } + + .select { + width: 100%; + margin-bottom: 20pt; + box-shadow: 4px 5px; + padding: 6px; + } + + .img { + max-width: 250px !important; + } + + .h1 { + font-family: Bitsumishi; + } + + .h5 { + font-family: BabaPro; + } + + .message { + font-family: BabaPro; + color: red; + } \ No newline at end of file diff --git a/client/src/pages/resetpassword/resetpassword.tsx b/client/src/pages/resetpassword/resetpassword.tsx new file mode 100644 index 00000000..e100955a --- /dev/null +++ b/client/src/pages/resetpassword/resetpassword.tsx @@ -0,0 +1,84 @@ +import styles from './resetpassword.module.css'; +import logoUrl from '../../assets/images/SkynetLogo.png'; +import React, {useState} from 'react'; +import { api } from '../../lib/axios'; + +interface Props { + token: string; +} + +const Resetpassword: React.FC = ({ token }) => { + const [password, setPassword] = useState(''); + const [confirmpassword, setConfirmPassword] = useState(''); + const [message, setMessage] = useState(''); + const [resetSuccess, setResetSuccess] = useState(false); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + try { + const response = await api.patch(`/user/resetpassword/${token}`, { + password, + confirmpassword + }); + setMessage(response.data.message); + if (response.data.message === 'Password has been reset successfully.') { + setResetSuccess(true); + } + } catch (error: any) { + setMessage(error.response.data.message); + } + }; + + return ( +
+
+ Skynet Logo +

RESET YOUR PASSWORD

+
+
+
+ setPassword(event.target.value)} + required + /> +
+ setConfirmPassword(event.target.value)} + required + /> +
+
+ +
+
+
+
+
+
+ {message} + {resetSuccess && ( + + {' '} + Let's{' '} + setResetSuccess(false)}> + log in + + + )} +
+
+
+ ); +}; + +export default Resetpassword; diff --git a/client/src/pages/signup/signup-component.tsx b/client/src/pages/signup/signup-component.tsx new file mode 100644 index 00000000..c6596ae3 --- /dev/null +++ b/client/src/pages/signup/signup-component.tsx @@ -0,0 +1,95 @@ +import { useState, useEffect } from 'react'; +import { toast } from 'react-toastify'; +import { useLocation } from 'wouter'; +import { api } from '../../lib/axios'; +import { Auth } from '../../lib/auth'; +import SignupHtml from './signup-html'; + +const Signup = () => { + interface Planet { + _id: string; + name: string; + } + const [planets, setPlanets] = useState>([]); + const [username, setUsername] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [location, setLocation] = useState(''); + const [_, navigate] = useLocation(); + + useEffect(() => { + const fetchPlanets = async () => { + const { data: res } = await api.get('/planet'); + try { + // console.log(res); + setPlanets(res.value); + setLocation(res.value[0]._id); + } catch (error) { + console.log(error); + } + }; + + fetchPlanets(); + }, []); + + const submitForm = async (e: any) => { + e.preventDefault(); + + const geoLoc = await new Promise((res) => { + navigator.geolocation.getCurrentPosition( + (p) => res(p), + () => res(undefined), + ); + }); + + const newUser = { + email: email, + userName: username, + password: password, + location: { + latitude: geoLoc ? geoLoc.coords.latitude : 0, + longitude: geoLoc ? geoLoc.coords.longitude : 0, + planetId: location, + }, + }; + + try { + const { data: res } = await api.post('/user/signup', newUser); + const token = res.value; + Auth.saveToken(token); + navigate('/'); + toast.success('User created successfully'); + } catch (err: any) { + toast.error(`🦄 ${err.response.data.error}`, { + position: 'top-right', + hideProgressBar: false, + closeOnClick: true, + pauseOnHover: true, + draggable: true, + progress: undefined, + theme: 'colored', + }); + } + + // if (!res.success) throw res.error; + // else alert('new user created'); + }; + + return ( + + ); +}; + +export default Signup; diff --git a/client/src/pages/signup/signup-html.tsx b/client/src/pages/signup/signup-html.tsx new file mode 100644 index 00000000..f7d23023 --- /dev/null +++ b/client/src/pages/signup/signup-html.tsx @@ -0,0 +1,90 @@ +import { useLocation } from 'wouter'; +import styles from './signup.module.css'; +import logoUrl from '../../assets/images/SkynetLogo.png'; + +const SignupHtml = ({ + planets, + username, + email, + password, + location, + setPlanets, + setUsername, + setEmail, + setPassword, + setLocation, + submitForm, +}: any) => { + const [_, navigate] = useLocation(); + return ( +
+
+ Skynet Logo +

SKY.NET

+
STAY CONNECTED ACROSS THE GALAXY
+
+
+
+ setUsername(e.target.value)} + /> + setEmail(e.target.value)} + /> + setPassword(e.target.value)} + /> + +
+ +
+
+
+ Already a User. Login Below + +
+
+
+
+
+ ); +}; + +export default SignupHtml; diff --git a/client/src/pages/signup/signup.module.css b/client/src/pages/signup/signup.module.css new file mode 100644 index 00000000..45e5ec86 --- /dev/null +++ b/client/src/pages/signup/signup.module.css @@ -0,0 +1,94 @@ +.signup-container { + display: grid; + align-items: center; + justify-items: center; +} + +.signup-form { + background: var(--signup-background); + padding: 29px 59px; + border: 2px solid black; + box-shadow: 4px 5px 0px #000; +} + +.signup-upperdiv { + background: var(--signup-background); + padding: 4px 0px; + border: 2px solid black; + box-shadow: 4px 3px 0px #000; +} + +.signup-bottomdiv { + background: var(--signup-background); + padding: 4px 0px; + border: 2px solid black; + box-shadow: 4px 4px 0px #000; +} + +.signup-button-login { + box-shadow: 4px 5px gray !important; + background: black !important; + color: white !important; +} + +.signup-button-login:active { + box-shadow: 4px 4px var(--signup-button-active) 5 !important; + transform: translateY(5px) !important; +} + +input, +.button, +.select { + font-size: 14pt; + font-family: 'BabaPro'; + background: var(--signup-background); + border: 2px solid black; + outline: none; + padding-left: 5px; +} + +.input { + display: block; + width: 100%; + line-height: 28pt; + margin-bottom: 20pt; + box-shadow: 4px 5px; +} + +.button { + padding: 2px; + width: 50%; + box-shadow: 4px 5px; +} + +.button:active { + box-shadow: 4px 4px var(--signup-button-active) 5; + transform: translateY(5px); +} + +.select { + width: 100%; + margin-bottom: 20pt; + box-shadow: 4px 5px; + padding: 6px; +} + +.img { + max-width: 250px !important; +} + +.input:focus { + background: var(--signup-input-focus); +} + +.h1 { + font-family: Bitsumishi; +} + +.h5 { + font-family: BabaPro; +} + +.span { + font-family: TTOctosquares; +} diff --git a/client/src/pages/test-page/test-page.module.css b/client/src/pages/test-page/test-page.module.css new file mode 100644 index 00000000..3316f867 --- /dev/null +++ b/client/src/pages/test-page/test-page.module.css @@ -0,0 +1,4 @@ +.page-container { + max-width: 100vw; + padding: 0%; +} \ No newline at end of file diff --git a/client/src/pages/test-page/test-page.tsx b/client/src/pages/test-page/test-page.tsx new file mode 100644 index 00000000..60c9823a --- /dev/null +++ b/client/src/pages/test-page/test-page.tsx @@ -0,0 +1,24 @@ +import styles from './test-page.module.css'; +import Container from 'react-bootstrap/Container'; + +import Header from '../../components/Header/Header'; +import Hotbar from '../../components/Hotbar/Hotbar'; +import Post from '../../components/Post/Post'; + +// PAGE FOR TESTING COMPONENTS +// Feel free to edit it +const Test = () => { + return ( + + + +
+ + + + + + ); +}; + +export default Test; diff --git a/client/src/pages/user-settings/options/change-password.tsx b/client/src/pages/user-settings/options/change-password.tsx new file mode 100644 index 00000000..59258250 --- /dev/null +++ b/client/src/pages/user-settings/options/change-password.tsx @@ -0,0 +1,124 @@ +import ModalConfirmation from '../../../components/ModalConfirmation/ModalConfirmation'; +import Button from 'react-bootstrap/Button'; +import logoUrl from '../../../assets/images/SkynetLogo.png'; + +interface Props { + passBody1: { + showPassBody1: boolean, + setShowPass1: any + }, + passBody2: { + showPassBody2: boolean, + setShowPass2: any + changePassword: any, + password: string, + setPassword: any, + newPassword: string, + setNewPassword: any, + confPassword: string, + setConfPassword: any + }, +} + +const ChangePasswordModal = (props: Props) => { + return ( + <> + {/* First Modal for password change */} + props.passBody1.setShowPass1(false)} + body={ +

+ Are you sure you want to change your password? +
+ This action cannot be undone. +

+ } + disableFooter={false} + footer={ +
+ + +
+ } + /> + + {/* Second Modal for password change */} + props.passBody2.setShowPass2(false)} + disableFooter={true} + header={ + <> + Skynet Logo +

Change Password

+ + } + body={ + <> +
+
+
+ props.passBody2.setPassword(event.target.value)} + required + /> +
+ props.passBody2.setNewPassword(event.target.value)} + required + /> +
+ props.passBody2.setConfPassword(event.target.value)} + required + /> +
+
+ +
+
+ +
+
+
+
+ + } + /> + + ); +}; + +export default ChangePasswordModal; diff --git a/client/src/pages/user-settings/options/delete-account.tsx b/client/src/pages/user-settings/options/delete-account.tsx new file mode 100644 index 00000000..9311f384 --- /dev/null +++ b/client/src/pages/user-settings/options/delete-account.tsx @@ -0,0 +1,90 @@ +import styles from '../user-settings.module.css'; + +import ModalConfirmation from '../../../components/ModalConfirmation/ModalConfirmation'; +import Button from 'react-bootstrap/Button'; + +interface Props { + deleteBody1: { + showDeleteBody1: boolean, + setShowDelete1: any + }, + deleteBody2: { + showDeleteBody2: boolean, + setShowDelete2: any + deleteAccount: any, + confInput: string, + setConfInput: any + }, +} + +const DeleteAccountModal = (props: Props) => { + return ( + <> + props.deleteBody1.setShowDelete1(false)} + body={ +

+ Are you sure you want to delete your account? +
+ This action cannot be undone. +

+ } + disableFooter={false} + footer={ +
+ + +
+ } + /> + + props.deleteBody2.setShowDelete2(false)} + body={ + <> +

Type in the following phrase to delete this account:
I-WANT-TO-DELTE-THIS-ACCOUNT

+
+ props.deleteBody2.setConfInput(event.target.value)} + required + /> +
+ + +
+
+ + } + disableFooter={true} + /> + + ); +}; + +export default DeleteAccountModal; diff --git a/client/src/pages/user-settings/options/your-info.tsx b/client/src/pages/user-settings/options/your-info.tsx new file mode 100644 index 00000000..074b5de2 --- /dev/null +++ b/client/src/pages/user-settings/options/your-info.tsx @@ -0,0 +1,67 @@ +import { api } from '../../../lib/axios'; +import { useState } from 'react'; + +import ModalConfirmation from '../../../components/ModalConfirmation/ModalConfirmation'; +import Button from 'react-bootstrap/Button'; + +interface Props { + infoBody: { + showInfoBody: boolean, + setInfoBody: any + } +} + +const YourInfoModal = (props: Props) => { + const userInfo = async () => { + try { + const response = await api.get('/user/'); + const data = response.data.value; + document.getElementById('username')!.innerHTML = data.userName; + document.getElementById('email')!.innerHTML = data.email; + + if (data.bio) { + document.getElementById('bio')!.innerHTML = "Bio: " + data.bio; + } else { + document.getElementById('bio')!.innerHTML = "Follower Count: " + data.followerCount; + } + + } catch (error: any) { + console.log('could not get information'); + } + }; + userInfo(); + + return ( + <> + props.infoBody.setInfoBody(false)} + body={ + <> +

+ Username:
+ Email:
+
+

+ + } + disableFooter={false} + footer={ +
+ +
+ } + /> + + ); +}; + +export default YourInfoModal; \ No newline at end of file diff --git a/client/src/pages/user-settings/user-settings.module.css b/client/src/pages/user-settings/user-settings.module.css new file mode 100644 index 00000000..bf62dc02 --- /dev/null +++ b/client/src/pages/user-settings/user-settings.module.css @@ -0,0 +1,27 @@ +.setting-body { + font-size: x-large; + font-family: BabaPro; + background-color: aliceblue; +} + +.setting-title { + font-size: 30pt; + font-family: Bitsumishi; +} + +.danger-zone, .danger-zone * { + background-color: #242C38; + color: red; +} + +.group-item, .group-item * { + background-color: aliceblue; +} + +.clickable { + cursor: pointer; +} + +.delete-btn { + background-color: red; +} \ No newline at end of file diff --git a/client/src/pages/user-settings/user-settings.tsx b/client/src/pages/user-settings/user-settings.tsx new file mode 100644 index 00000000..0d5dac16 --- /dev/null +++ b/client/src/pages/user-settings/user-settings.tsx @@ -0,0 +1,199 @@ +import { useState } from 'react'; +import { api } from '../../lib/axios'; +import { useLocation } from 'wouter'; +import { toast } from 'react-toastify'; + +import styles from './user-settings.module.css'; + +import ListGroup from 'react-bootstrap/ListGroup'; +import Nav from 'react-bootstrap/Nav'; +import Page from '../../components/Page/Page'; +import ChangePasswordModal from './options/change-password'; +import DeleteAccountModal from './options/delete-account'; +import YourInfoModal from './options/your-info'; +import { Auth } from '../../lib/auth'; + +const UserSettings = () => { + const [_, setLocation] = useLocation(); + + // variables responsible for showing change password modal + const [showPassBody1, setShowPass1] = useState(false); + const [showPassBody2, setShowPass2] = useState(false); + + // variables responsible for showing delete account modal + const [showDeleteBody1, setShowDelete1] = useState(false); + const [showDeleteBody2, setShowDelete2] = useState(false); + + // variables responsible for showing your info modal + const [showInfoBody, setInfoBody] = useState(false); + + //variables resposible for grabbing the form fields present in the change password modal + const [password, setPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + + // varriables responsible for grabbing form fields in the delete account modal + const [confInput, setConfInput] = useState(''); + + const changePassword = async (event: React.FormEvent) => { + event.preventDefault(); + try { + const response = await api.patch('/user/changepassword', { + password: password, + newpassword: newPassword, + confirmpassword: confirmPassword, + }); + toast.success('Password changed!'); + } catch (error: any) { + toast.error('Could not change password.'); + } + }; + + const deleteAccount = async (event: React.FormEvent) => { + event.preventDefault(); + if (confInput === 'I-WANT-TO-DELTE-THIS-ACCOUNT') { + try { + const response = await api.post('/user/deleteaccount/delete'); + setLocation('/login'); + } catch (error: any) { + alert(error.response.data.message); + } + } else { + toast.error('Invalid Phrase'); + } + }; + + function logout() { + console.log('here'); + Auth.loseToken(); + setLocation('/login'); + } + + // defining values for the Change Password Modal + const passBody1 = { + showPassBody1: showPassBody1, + setShowPass1: setShowPass1, + }; + const passBody2 = { + showPassBody2: showPassBody2, + setShowPass2: setShowPass2, + changePassword: changePassword, + password: password, + setPassword: setPassword, + newPassword: newPassword, + setNewPassword: setNewPassword, + confPassword: confirmPassword, + setConfPassword: setConfirmPassword, + }; + + // defining values for the delete account Modal + const deleteBody1 = { + showDeleteBody1: showDeleteBody1, + setShowDelete1: setShowDelete1, + }; + const deleteBody2 = { + showDeleteBody2: showDeleteBody2, + setShowDelete2: setShowDelete2, + deleteAccount: deleteAccount, + confInput: confInput, + setConfInput: setConfInput, + }; + + //defining values from the info modal + const infoBody = { + showInfoBody: showInfoBody, + setInfoBody: setInfoBody, + }; + + return ( + <> + + +

Account

+ + + Followers + + + Following + + setInfoBody(true)}> + Your Info + + +
+ +

History

+ + + Saved + + + Liked + + + Commented Posts + + +
+ +

General

+ + + About + + + FAQs + + + Support + + +
+ +
+ +
+
+ +

Danger Zone

+ + + Change Username + + setShowPass1(true)}> + Change Password + + + Change Email + + setShowDelete1(true)}> + DELETE ACCOUNT + + +
+ + } + /> + + + + + + ); +}; + +export default UserSettings; diff --git a/client/vite.config.ts b/client/vite.config.ts index af98b566..343eee04 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,7 +1,13 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react-swc'; +import dotenv from 'dotenv'; + +dotenv.config(); export default defineConfig({ plugins: [react()], css: { modules: { localsConvention: 'camelCaseOnly' } }, + server: { + port: parseInt(process.env.VITE_PORT!), + }, }); diff --git a/server/index.ts b/server/index.ts index b98b1edc..4d470710 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,8 +1,12 @@ -import './src/load-env.js'; - +import { isDev } from './src/load-env'; +import './src/load-env'; import express from 'express'; import createRouter from 'express-file-routing'; import path from 'path'; +import mongoose from 'mongoose'; +import cors from 'cors'; + +import { requestLogger } from './src/middlewares/log.js'; const PROJECT_ROOT = path.join(__dirname, 'src'); const PORT = process.env.PORT; @@ -10,6 +14,15 @@ const PORT = process.env.PORT; (async () => { const app = express(); + app.use(cors()); + + app.use(express.urlencoded({ extended: true })); + app.use(express.json()); + app.use(requestLogger); + + const mongoUrl = process.env.MONGO_URL!; + await mongoose.connect(mongoUrl); + await createRouter(app, { directory: path.join(PROJECT_ROOT, 'routes') }); app.listen(PORT, () => { diff --git a/server/package-lock.json b/server/package-lock.json index f60aba6b..c01ca6f0 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -6,16 +6,28 @@ "": { "name": "skynet-server", "dependencies": { + "bcrypt": "^5.1.1", + "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", "express-file-routing": "^3.0.3", + "http-status-codes": "^2.3.0", "joi": "^17.13.1", - "mongoose": "^8.3.3" + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.3.3", + "nodemailer": "^6.9.13", + "picocolors": "^1.0.1" }, "devDependencies": { + "@types/bcrypt": "^5.0.2", + "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.6", + "@types/morgan": "^1.9.9", "@types/node": "^20.12.8", + "@types/nodemailer": "^6.4.15", "esno": "^4.7.0", + "morgan": "^1.10.0", "typescript": "^5.4.5" } }, @@ -400,6 +412,25 @@ "@hapi/hoek": "^9.0.0" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, "node_modules/@mongodb-js/saslprep": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.6.tgz", @@ -426,6 +457,15 @@ "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" }, + "node_modules/@types/bcrypt": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", + "integrity": "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -445,6 +485,15 @@ "@types/node": "*" } }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/express": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", @@ -475,12 +524,30 @@ "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "dev": true }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.6.tgz", + "integrity": "sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, + "node_modules/@types/morgan": { + "version": "1.9.9", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.9.tgz", + "integrity": "sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "20.12.8", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.8.tgz", @@ -490,6 +557,15 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/nodemailer": { + "version": "6.4.15", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.15.tgz", + "integrity": "sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.15", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", @@ -536,6 +612,11 @@ "@types/webidl-conversions": "*" } }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -548,11 +629,104 @@ "node": ">= 0.6" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "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==" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/bcrypt": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", + "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.11", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/body-parser": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", @@ -576,6 +750,15 @@ "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==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, "node_modules/bson": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/bson/-/bson-6.7.0.tgz", @@ -584,6 +767,11 @@ "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", @@ -610,6 +798,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -642,6 +856,18 @@ "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", @@ -666,6 +892,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -683,6 +914,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, "node_modules/dotenv": { "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", @@ -694,11 +933,24 @@ "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/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -871,6 +1123,33 @@ "node": ">= 0.6" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -893,6 +1172,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -923,6 +1221,25 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -967,6 +1284,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -993,6 +1315,44 @@ "node": ">= 0.8" } }, + "node_modules/http-status-codes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", + "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==" + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1004,6 +1364,15 @@ "node": ">=0.10.0" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -1017,6 +1386,14 @@ "node": ">= 0.10" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, "node_modules/joi": { "version": "17.13.1", "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.1.tgz", @@ -1029,6 +1406,51 @@ "@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", @@ -1037,6 +1459,63 @@ "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/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1093,6 +1572,59 @@ "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==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mongodb": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.5.0.tgz", @@ -1173,6 +1705,34 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dev": true, + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/mpath": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", @@ -1226,6 +1786,90 @@ "node": ">= 0.6" } }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/nodemailer": { + "version": "6.9.13", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.13.tgz", + "integrity": "sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.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.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -1245,6 +1889,23 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1253,11 +1914,24 @@ "node": ">= 0.8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -1314,6 +1988,19 @@ "node": ">= 0.8" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -1323,6 +2010,20 @@ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1347,6 +2048,17 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/send": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", @@ -1389,6 +2101,11 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -1432,6 +2149,11 @@ "resolved": "https://registry.npmjs.org/sift/-/sift-16.0.1.tgz", "integrity": "sha512-Wv6BjQ5zbhW7VFefWusVP33T/EM0vYikCaQ2qR8yULbsilAT8/wQaXvuQ3ptGLpoKx+lihJE3y2UTgKDyyNHZQ==" }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, "node_modules/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -1448,6 +2170,54 @@ "node": ">= 0.8" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -1525,6 +2295,11 @@ "node": ">= 0.8" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -1560,6 +2335,24 @@ "engines": { "node": ">=16" } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "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==" + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } } diff --git a/server/package.json b/server/package.json index b5cf3f4b..21188121 100644 --- a/server/package.json +++ b/server/package.json @@ -7,16 +7,28 @@ "dev": "esno watch index.ts" }, "dependencies": { + "bcrypt": "^5.1.1", + "cors": "^2.8.5", "dotenv": "^16.4.5", "express": "^4.19.2", "express-file-routing": "^3.0.3", + "http-status-codes": "^2.3.0", "joi": "^17.13.1", - "mongoose": "^8.3.3" + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.3.3", + "nodemailer": "^6.9.13", + "picocolors": "^1.0.1" }, "devDependencies": { + "@types/bcrypt": "^5.0.2", + "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.6", + "@types/morgan": "^1.9.9", "@types/node": "^20.12.8", + "@types/nodemailer": "^6.4.15", "esno": "^4.7.0", + "morgan": "^1.10.0", "typescript": "^5.4.5" } } diff --git a/server/src/@types/express.d.ts b/server/src/@types/express.d.ts new file mode 100644 index 00000000..4c8d46a9 --- /dev/null +++ b/server/src/@types/express.d.ts @@ -0,0 +1,10 @@ +import { HydratedDocument } from 'mongoose'; +import { IUser } from '../models/user'; + +declare global { + namespace Express { + export interface Request { + user?: HydratedDocument; + } + } +} diff --git a/server/src/@types/model.d.ts b/server/src/@types/model.d.ts new file mode 100644 index 00000000..02d69dde --- /dev/null +++ b/server/src/@types/model.d.ts @@ -0,0 +1,14 @@ +import { Types } from 'mongoose'; + +type AllowedTypes = undefined | string | boolean | number | Types.ObjectId | Date; +type TypeToPrimitive = T extends Types.ObjectId ? string : T extends Date ? number : T; + +/** + * Utility type to convert interfaces that represent a Mongoose document + * into a JSON document. + * + * This represents a document as it may be submitted by a client for insertion. + */ +export type RawDocument = { + [K in keyof T]: T[K] extends AllowedTypes ? TypeToPrimitive : T[K] extends object ? RawDocument : never; +}; diff --git a/server/src/load-env.ts b/server/src/load-env.ts index 8902e236..ebd3de70 100644 --- a/server/src/load-env.ts +++ b/server/src/load-env.ts @@ -1,3 +1,5 @@ import { config } from 'dotenv'; config(); + +export const isDev = () => process.env.NODE_ENV === 'development'; diff --git a/server/src/middlewares/log.ts b/server/src/middlewares/log.ts new file mode 100644 index 00000000..b4a7770d --- /dev/null +++ b/server/src/middlewares/log.ts @@ -0,0 +1,28 @@ +import morgan from 'morgan'; +import pc from 'picocolors'; + +/** + * Middleware to log requests with some parameters to the console. Used for + * debugging and inspection. + */ +export const requestLogger = morgan((tokens, req, res) => { + const responseCode = parseInt(tokens.status(req, res) || ''); + const responseTime = parseInt(tokens['response-time'](req, res) || ''); + return `${pc.blue(tokens.method(req, res))} ${pc.bold('@')} ${pc.blue(tokens.url(req, res))} ${pc.bold('|>')} ${ + isNaN(responseCode) + ? pc.strikethrough(pc.red('Response Code')) + : responseCode < 200 + ? pc.blue(responseCode) + : responseCode < 400 + ? pc.green(responseCode) + : pc.red(responseCode) + } in ${ + isNaN(responseTime) + ? pc.strikethrough(pc.red('Response Time')) + : responseTime < 100 + ? pc.green(responseTime) + : responseTime < 150 + ? pc.yellow(responseTime) + : pc.red(responseTime) + }ms`; +}); diff --git a/server/src/middlewares/require-login.ts b/server/src/middlewares/require-login.ts new file mode 100644 index 00000000..8c15b823 --- /dev/null +++ b/server/src/middlewares/require-login.ts @@ -0,0 +1,21 @@ +import { NextFunction, Request, Response } from 'express'; +import { Resolve } from '../utils/express'; +import { AuthToken } from '../utils/auth-token'; + +/** + * Middleware to ensure that the request includes a valid authorization token. + * If it does not, it will resolve the request with an unautorized response. + */ +export async function requireLogin(req: Request, res: Response, next: NextFunction) { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer')) + return Resolve(res).unauthorized('Authentication is required.'); + + const [_, token] = authHeader.split(' '); + + const user = await AuthToken.verifyToUser(token); + if (!user) return Resolve(res).unauthorized('Invalid token provided.'); + + req.user = user; + next(); +} diff --git a/server/src/models/comment-relationship.ts b/server/src/models/comment-relationship.ts new file mode 100644 index 00000000..0dd6168d --- /dev/null +++ b/server/src/models/comment-relationship.ts @@ -0,0 +1,16 @@ +import { model, Schema, Types } from 'mongoose'; + +export interface ICommentRelationship { + parentPost: Types.ObjectId; + childPost: Types.ObjectId; +} + +const schema = new Schema( + { + parentPost: { type: 'ObjectID', ref: 'Post', required: true, index: true }, + childPost: { type: 'ObjectID', ref: 'Post', required: true, index: true }, + }, + { timestamps: { createdAt: true, updatedAt: false } }, +); + +export const CommentRelationship = model('CommentRelationship', schema); diff --git a/server/src/models/follow-relationship.ts b/server/src/models/follow-relationship.ts new file mode 100644 index 00000000..bc472efe --- /dev/null +++ b/server/src/models/follow-relationship.ts @@ -0,0 +1,16 @@ +import { model, Schema, Types } from 'mongoose'; + +export interface IFollowRelationship { + initiateUserId: Types.ObjectId; + targetUserId: Types.ObjectId; +} + +const schema = new Schema( + { + initiateUserId: { type: 'ObjectID', ref: 'User', required: true, index: true }, + targetUserId: { type: 'ObjectID', ref: 'User', required: true, index: true }, + }, + { timestamps: { createdAt: true, updatedAt: false } }, +); + +export const FollowRelationship = model('FollowRelationship', schema); diff --git a/server/src/models/like-interaction.ts b/server/src/models/like-interaction.ts new file mode 100644 index 00000000..1a782b49 --- /dev/null +++ b/server/src/models/like-interaction.ts @@ -0,0 +1,16 @@ +import { model, Schema, Types } from 'mongoose'; + +export interface ILikeInteraction { + postId: Types.ObjectId; + userId: Types.ObjectId; +} + +const schema = new Schema( + { + postId: { type: 'ObjectID', ref: 'Post', required: true, index: true }, + userId: { type: 'ObjectID', ref: 'User', required: true, index: true }, + }, + { timestamps: { createdAt: true, updatedAt: false } }, +); + +export const LikeInteraction = model('LikeInteraction', schema); diff --git a/server/src/models/location.ts b/server/src/models/location.ts new file mode 100644 index 00000000..c85b376e --- /dev/null +++ b/server/src/models/location.ts @@ -0,0 +1,20 @@ +import Joi from 'joi'; +import { Schema, Types } from 'mongoose'; + +export interface ILocation { + planetId: Types.ObjectId; + latitude: number; + longitude: number; +} + +export const LocationSchema = new Schema({ + planetId: { type: 'ObjectId', ref: 'Planet', required: true, index: true }, + latitude: { type: 'number', required: true }, + longitude: { type: 'number', required: true }, +}); + +export const RawLocationSchema = Joi.object({ + planetId: Joi.string().trim().required(), + latitude: Joi.number().min(-90).max(90).required(), + longitude: Joi.number().min(-180).max(180).required(), +}); diff --git a/server/src/models/media.ts b/server/src/models/media.ts new file mode 100644 index 00000000..a8f98a43 --- /dev/null +++ b/server/src/models/media.ts @@ -0,0 +1,20 @@ +import Joi from 'joi'; +import { Schema } from 'mongoose'; + +export interface IMedia { + mediaType: string; + mediaUrl: string; + mediaAlt?: string; +} + +export const MediaSchema = new Schema({ + mediaType: { type: 'string', required: true }, + mediaUrl: { type: 'string', required: true }, + mediaAlt: { type: 'string' }, +}); + +export const RawMediaSchema = Joi.array().items({ + mediaType: Joi.string().trim().required(), + mediaUrl: Joi.string().trim().required(), + mediaAlt: Joi.string().trim(), +}); diff --git a/server/src/models/message.ts b/server/src/models/message.ts new file mode 100644 index 00000000..e1f282dc --- /dev/null +++ b/server/src/models/message.ts @@ -0,0 +1,27 @@ +import { Schema, Types } from 'mongoose'; +import { IMedia, MediaSchema } from './media'; +import { ILocation, LocationSchema } from './location'; + +export interface IMessage { + senderId: Types.ObjectId; + receiverId: Types.ObjectId; + createdAt: Date; + media: Array; + content: string; + location: ILocation; +} + +const schema = new Schema( + { + senderId: { type: 'ObjectId', ref: 'User', required: true }, + receiverId: { type: 'ObjectId', ref: 'User', required: true }, + content: { type: 'string', required: true }, + location: { type: LocationSchema, required: true }, + media: { + required: true, + default: [], + type: [MediaSchema], + }, + }, + { timestamps: { createdAt: true, updatedAt: false } }, +); diff --git a/server/src/models/planet.ts b/server/src/models/planet.ts new file mode 100644 index 00000000..a51afe6d --- /dev/null +++ b/server/src/models/planet.ts @@ -0,0 +1,11 @@ +import { model, Schema } from 'mongoose'; + +export interface IPlanet { + name: string; +} + +const schema = new Schema({ + name: { type: 'string', required: true }, +}); + +export const PlanetModel = model('Planet', schema); diff --git a/server/src/models/post.ts b/server/src/models/post.ts new file mode 100644 index 00000000..b236a313 --- /dev/null +++ b/server/src/models/post.ts @@ -0,0 +1,35 @@ +import { model, Schema, Types } from 'mongoose'; +import { ILocation, LocationSchema } from './location'; +import { IMedia, MediaSchema } from './media'; + +export interface IPost { + authorId: Types.ObjectId; + content: string; + likeCount: number; + commentCount: number; + repostCount: number; + createdAt: Date; + location: ILocation; + media: Array; + deleted: boolean; +} + +const schema = new Schema( + { + authorId: { type: 'ObjectID', ref: 'User', required: true, index: true }, + content: { type: 'string', required: true }, + likeCount: { type: 'number', required: true, default: 0 }, + commentCount: { type: 'number', required: true, default: 0 }, + repostCount: { type: 'number', required: true, default: 0 }, + location: { type: LocationSchema, required: true }, + deleted: { type: 'boolean', required: true, default: false }, + media: { + required: true, + default: [], + type: [MediaSchema], + }, + }, + { timestamps: { createdAt: true, updatedAt: false } }, +); + +export const PostModel = model('Post', schema); diff --git a/server/src/models/repost-relationship.ts b/server/src/models/repost-relationship.ts new file mode 100644 index 00000000..84799b6c --- /dev/null +++ b/server/src/models/repost-relationship.ts @@ -0,0 +1,16 @@ +import { model, Schema, Types } from 'mongoose'; + +export interface IRepostRelationship { + targetPost: Types.ObjectId; + repostPost: Types.ObjectId; +} + +const schema = new Schema( + { + targetPost: { type: 'ObjectID', ref: 'Post', required: true }, + repostPost: { type: 'ObjectID', ref: 'Post', required: true, index: true }, + }, + { timestamps: { createdAt: true, updatedAt: false } }, +); + +export const RepostRelationship = model('RepostRelationship', schema); diff --git a/server/src/models/token.ts b/server/src/models/token.ts new file mode 100644 index 00000000..2ae5ef14 --- /dev/null +++ b/server/src/models/token.ts @@ -0,0 +1,21 @@ +import { model, Schema, Types } from 'mongoose'; + +export interface Token { + userId: Types.ObjectId; + email: string; + passwordResetToken?: string; + passwordResetExpires?: Date; + createdAt: Date; +} + +const schema = new Schema( + { + userId: { type: 'ObjectID', ref: 'User', required: true }, + email: { type: 'string', required: true }, + passwordResetToken: { type: 'string' }, + passwordResetExpires: { type: 'date' }, + }, + { timestamps: { createdAt: true, updatedAt: false } }, +); + +export const TokenModel = model('Token', schema); diff --git a/server/src/models/user.ts b/server/src/models/user.ts new file mode 100644 index 00000000..d7a83257 --- /dev/null +++ b/server/src/models/user.ts @@ -0,0 +1,38 @@ +import { model, Schema, Types } from 'mongoose'; +import { ILocation, LocationSchema } from './location'; + +export interface IUser { + email: string; + password: string; + userName: string; + bio?: string; + location: ILocation; + birthDate?: Date; + avatarUrl?: string; + followerCount: number; + followingCount: number; + postCount: number; + admin: boolean; + savedPosts: Array; + createdAt: Date; +} + +const schema = new Schema( + { + email: { type: 'string', required: true }, + password: { type: 'string', required: true }, + userName: { type: 'string', required: true }, + bio: { type: 'string' }, + location: { type: LocationSchema, required: true }, + birthDate: { type: 'date' }, + avatarUrl: { type: 'string' }, + followerCount: { type: 'number', required: true, default: 0 }, + followingCount: { type: 'number', required: true, default: 0 }, + postCount: { type: 'number', required: true, default: 0 }, + savedPosts: { type: ['ObjectId'], required: true, default: [] }, + admin: { type: 'boolean', required: true, default: false }, + }, + { timestamps: { createdAt: true, updatedAt: false } }, +); + +export const UserModel = model('User', schema); diff --git a/server/src/routes/planet/[nameOrId].ts b/server/src/routes/planet/[nameOrId].ts new file mode 100644 index 00000000..62ea8954 --- /dev/null +++ b/server/src/routes/planet/[nameOrId].ts @@ -0,0 +1,20 @@ +import { Handler } from 'express'; +import { PlanetModel } from '../../models/planet'; +import { escapeRegex } from '../../utils/regex'; +import mongoose from 'mongoose'; +import { Resolve } from '../../utils/express'; + +export const get: Handler = async (req, res) => { + const nameOrId = req.params.nameOrId; + + const isId = mongoose.isValidObjectId(nameOrId); + let planet; + if (isId) planet = await PlanetModel.findById(nameOrId).lean().exec(); + else + planet = await PlanetModel.findOne({ + name: new RegExp(`^${escapeRegex(nameOrId)}$`, 'i'), + }); + + if (planet) Resolve(res).okWith(planet); + else Resolve(res).notFound('No planet by that name or ID was found.'); +}; diff --git a/server/src/routes/planet/index.ts b/server/src/routes/planet/index.ts new file mode 100644 index 00000000..1ed147ef --- /dev/null +++ b/server/src/routes/planet/index.ts @@ -0,0 +1,8 @@ +import { Handler } from 'express'; +import { PlanetModel } from '../../models/planet'; +import { Resolve } from '../../utils/express'; + +export const get: Handler = async (_, res) => { + const planets = await PlanetModel.find({}).lean().exec(); + Resolve(res).okWith(planets); +}; diff --git a/server/src/routes/post/[id]/comment.ts b/server/src/routes/post/[id]/comment.ts new file mode 100644 index 00000000..2e478cff --- /dev/null +++ b/server/src/routes/post/[id]/comment.ts @@ -0,0 +1,97 @@ +import { Handler } from 'express'; +import { requireLogin } from '../../../middlewares/require-login'; +import mongoose from 'mongoose'; +import { assertRequestBody, Resolve } from '../../../utils/express'; +import { PostModel } from '../../../models/post'; +import { RawDocument } from '../../../@types/model'; +import { ILocation, RawLocationSchema } from '../../../models/location'; +import { IMedia, RawMediaSchema } from '../../../models/media'; +import Joi from 'joi'; +import { CommentRelationship } from '../../../models/comment-relationship'; + +interface PostBody { + content: string; + location?: RawDocument; + media?: RawDocument; +} + +export const get: Handler = async (req, res) => { + const parentPostId = req.params.id; + + const rawLimit = req.query.limit; + let limit = typeof rawLimit === 'string' ? parseInt(rawLimit) : NaN; + if (isNaN(limit)) limit = 10; + limit = Math.max(0, Math.min(limit, 100)); + + if (!mongoose.isValidObjectId(parentPostId)) return Resolve(res).badRequest('Invalid post ID provided.'); + + const relationships = await CommentRelationship.find({ parentPost: parentPostId }).limit(limit); + const comments = await Promise.all( + relationships.map(async (v) => { + return await PostModel.findById(v.childPost).lean(); + }), + ); + + Resolve(res).okWith(comments); +}; + +export const post: Handler[] = [ + requireLogin, + async (req, res) => { + const parentPostId = req.params.id; + if (!mongoose.isValidObjectId(parentPostId)) return Resolve(res).badRequest('Invalid post ID provided.'); + + const parentPost = await PostModel.findById(parentPostId); + if (!parentPost) return Resolve(res).notFound('Invalid post ID provided.'); + + if (parentPost.deleted) return Resolve(res).gone('The given post is deleted.'); + + const body = assertRequestBody( + req, + res, + Joi.object({ + content: Joi.string().trim().required(), + location: RawLocationSchema, + media: RawMediaSchema, + }), + ); + + if (!body) return; + + const currentUser = req.user!; + const session = await mongoose.startSession(); + + try { + const commentRelationship = await session.withTransaction(async () => { + const comment = new PostModel({ + authorId: currentUser._id, + content: body.content, + media: body.media, + location: body.location ?? currentUser.location, + }); + + const relationship = new CommentRelationship({ + parentPost: parentPost._id, + childPost: comment._id, + }); + + await comment.save({ session }); + await relationship.save({ session }); + + currentUser.postCount++; + await currentUser.save({ session }); + + parentPost.commentCount++; + await parentPost.save({ session }); + + return relationship; + }); + + Resolve(res).created(commentRelationship, 'Comment created successfully.'); + } catch { + Resolve(res).error('Error occured while trying to create this comment.'); + } finally { + await session.endSession(); + } + }, +]; diff --git a/server/src/routes/post/[id]/index.ts b/server/src/routes/post/[id]/index.ts new file mode 100644 index 00000000..eded3550 --- /dev/null +++ b/server/src/routes/post/[id]/index.ts @@ -0,0 +1,63 @@ +import { Handler } from 'express'; +import mongoose from 'mongoose'; +import { requireLogin } from '../../../middlewares/require-login'; +import { PostModel } from '../../../models/post'; +import { Resolve } from '../../../utils/express'; +import { CommentRelationship } from '../../../models/comment-relationship'; +import { LikeInteraction } from '../../../models/like-interaction'; + +export const get: Handler = async (req, res) => { + const id = req.params.id; + if (!mongoose.isValidObjectId(id)) return Resolve(res).badRequest('Invalid post ID provided.'); + + const post = await PostModel.findById(id); + if (!post) return Resolve(res).notFound('Invalid post ID provided.'); + + if (post.deleted) return Resolve(res).gone('Post has been deleted.'); + return Resolve(res).okWith(post); +}; + +export const del: Handler[] = [ + requireLogin, + async (req, res) => { + const id = req.params.id; + if (!mongoose.isValidObjectId(id)) return Resolve(res).badRequest('Invalid post ID provided.'); + + const post = await PostModel.findById(id); + if (!post) return Resolve(res).notFound('Invalid post ID provided.'); + + const currentUser = req.user!; + if (!post.authorId.equals(currentUser._id)) return Resolve(res).forbidden('You cannot delete this post.'); + + if (post.deleted) return Resolve(res).gone('Post is already deleted.'); + + const session = await mongoose.startSession(); + + try { + const deletedPost = await session.withTransaction(async () => { + await post.updateOne({ deleted: true, content: '', likeCount: 0 }, { session }); + await currentUser.updateOne({ $inc: { postCount: -1 } }, { session }); + + const commentOfRelationship = await CommentRelationship.findOne({ childPost: post._id }); + if (commentOfRelationship) { + await commentOfRelationship.deleteOne({ session }); + await PostModel.updateOne( + { _id: commentOfRelationship.parentPost }, + { $inc: { commentCount: -1 } }, + { session }, + ); + } + + await LikeInteraction.deleteMany({ postId: post._id }, { session }); + + return post; + }); + + Resolve(res).okWith(deletedPost, 'Post has been deleted.'); + } catch { + Resolve(res).error('Error occured while trying to delete this post.'); + } finally { + session.endSession(); + } + }, +]; diff --git a/server/src/routes/post/[id]/like.ts b/server/src/routes/post/[id]/like.ts new file mode 100644 index 00000000..56299ab0 --- /dev/null +++ b/server/src/routes/post/[id]/like.ts @@ -0,0 +1,94 @@ +import { Handler } from 'express'; +import { requireLogin } from '../../../middlewares/require-login'; +import { PostModel } from '../../../models/post'; +import mongoose from 'mongoose'; +import { Resolve } from '../../../utils/express'; +import { LikeInteraction } from '../../../models/like-interaction'; + +export const get: Handler[] = [ + requireLogin, + async (req, res) => { + const id = req.params.id; + if (!mongoose.isValidObjectId(id)) return Resolve(res).badRequest('Invalid post ID provided.'); + + const currentUserId = req.user!.id; + const existingInteraction = await LikeInteraction.findOne({ postId: id, userId: currentUserId }); + if (existingInteraction) Resolve(res).okWith(existingInteraction); + else Resolve(res).notFound('Post is not liked.'); + }, +]; + +export const post: Handler[] = [ + requireLogin, + async (req, res) => { + const id = req.params.id; + if (!mongoose.isValidObjectId(id)) return Resolve(res).badRequest('Invalid post ID provided.'); + + const post = await PostModel.findById(id); + if (!post) return Resolve(res).notFound('Invalid post ID provided.'); + + if (post.deleted) return Resolve(res).gone('The given post is deleted.'); + + const currentUserId = req.user!._id; + const existingInteraction = await LikeInteraction.exists({ postId: id, userId: currentUserId }).lean(); + if (existingInteraction) return Resolve(res).badRequest('Post is already liked.'); + + const session = await mongoose.startSession(); + + try { + const interaction = await session.withTransaction(async () => { + const interaction = new LikeInteraction({ + postId: post._id, + userId: currentUserId, + }); + + await interaction.save({ session }); + + post.likeCount++; + await post.save({ session }); + + return interaction; + }); + + Resolve(res).okWith(interaction); + } catch { + Resolve(res).error('Error occured while trying to like this post.'); + } finally { + await session.endSession(); + } + }, +]; + +export const del: Handler[] = [ + requireLogin, + async (req, res) => { + const id = req.params.id; + if (!mongoose.isValidObjectId(id)) return Resolve(res).badRequest('Invalid post ID provided.'); + + const post = await PostModel.findById(id); + if (!post) return Resolve(res).notFound('Invalid post ID provided.'); + + const currentUserId = req.user!.id; + const interaction = await LikeInteraction.findOne({ postId: id, userId: currentUserId }); + if (!interaction) return Resolve(res).badRequest('Post is not liked.'); + + const session = await mongoose.startSession(); + + try { + await session.withTransaction(async () => { + await interaction.deleteOne({ session }); + + post.likeCount--; + await post.save({ session }); + + return interaction; + }); + + Resolve(res).okWith(interaction); + } catch { + Resolve(res).error('Error occured while trying to like this post.'); + } finally { + await session.endSession(); + } + }, +]; diff --git a/server/src/routes/post/[id]/save.ts b/server/src/routes/post/[id]/save.ts new file mode 100644 index 00000000..76740f8b --- /dev/null +++ b/server/src/routes/post/[id]/save.ts @@ -0,0 +1,57 @@ +import { Handler } from 'express'; +import { requireLogin } from '../../../middlewares/require-login'; +import mongoose from 'mongoose'; +import { Resolve } from '../../../utils/express'; +import { PostModel } from '../../../models/post'; + +export const get: Handler[] = [ + requireLogin, + async (req, res) => { + const id = req.params.id; + if (!mongoose.isValidObjectId(id)) return Resolve(res).badRequest('Invalid post ID provided.'); + + const user = req.user!; + Resolve(res).okWith(!!user.savedPosts.find((v) => v.equals(id))); + }, +]; + +export const post: Handler[] = [ + requireLogin, + async (req, res) => { + const id = req.params.id; + if (!mongoose.isValidObjectId(id)) return Resolve(res).badRequest('Invalid post ID provided.'); + + const validPost = await PostModel.findById(id); + if (!validPost) return Resolve(res).notFound('Invalid post ID provided.'); + + if (validPost.deleted) return Resolve(res).gone('The given post is deleted.'); + + const user = req.user!; + if (user.savedPosts.includes(validPost._id)) return Resolve(res).conflict('Post is already saved.'); + + user.savedPosts.push(validPost._id); + await user.save(); + + Resolve(res).okWith(user.savedPosts); + }, +]; + +export const del: Handler[] = [ + requireLogin, + async (req, res) => { + const id = req.params.id; + if (!mongoose.isValidObjectId(id)) return Resolve(res).badRequest('Invalid post ID provided.'); + + const validPost = await PostModel.exists({ _id: id }); + if (!validPost) return Resolve(res).notFound('Invalid post ID provided.'); + + const user = req.user!; + const index = user.savedPosts.indexOf(validPost._id); + if (index === -1) return Resolve(res).badRequest('Post is not saved!'); + + user.savedPosts.splice(index, 1); + await user.save(); + + Resolve(res).okWith(user.savedPosts); + }, +]; diff --git a/server/src/routes/post/index.ts b/server/src/routes/post/index.ts new file mode 100644 index 00000000..ad62c954 --- /dev/null +++ b/server/src/routes/post/index.ts @@ -0,0 +1,58 @@ +import Joi from 'joi'; +import { Handler } from 'express'; +import { requireLogin } from '../../middlewares/require-login'; +import { assertRequestBody, Resolve } from '../../utils/express'; +import { IMedia, RawMediaSchema } from '../../models/media'; +import { ILocation, RawLocationSchema } from '../../models/location'; +import { RawDocument } from '../../@types/model'; +import { PostModel } from '../../models/post'; +import mongoose from 'mongoose'; + +interface PostBody { + content: string; + location?: RawDocument; + media?: RawDocument; +} + +export const post: Handler[] = [ + requireLogin, + async (req, res) => { + const body = assertRequestBody( + req, + res, + Joi.object({ + content: Joi.string().trim().required(), + location: RawLocationSchema, + media: RawMediaSchema, + }), + ); + + if (!body) return; + + const currentUser = req.user!; + const session = await mongoose.startSession(); + + try { + const post = await session.withTransaction(async () => { + const post = new PostModel({ + authorId: currentUser._id, + content: body.content, + media: body.media, + location: body.location ?? currentUser.location, + }); + + currentUser.postCount++; + await currentUser.save({ session }); + + await post.save({ session }); + return post; + }); + + Resolve(res).created(post, 'Post created successfully.'); + } catch { + Resolve(res).error('Error occured while trying to create this post.'); + } finally { + await session.endSession(); + } + }, +]; diff --git a/server/src/routes/user/[id].ts b/server/src/routes/user/[id].ts new file mode 100644 index 00000000..16af1d09 --- /dev/null +++ b/server/src/routes/user/[id].ts @@ -0,0 +1,52 @@ +import Joi from 'joi'; +import mongoose from 'mongoose'; +import { Handler } from 'express'; +import { UserModel } from '../../models/user'; +import { requireLogin } from '../../middlewares/require-login'; +import { assertRequestBody, Resolve } from '../../utils/express'; + +export const get: Handler = async (req, res) => { + const id = req.params.id; + if (!mongoose.isValidObjectId(id)) return Resolve(res).badRequest('Invalid user ID provided.'); + + const user = await UserModel.findById(id).lean().select('-admin -email -password'); + if (!user) Resolve(res).notFound('No user found by the given ID.'); + else Resolve(res).okWith(user); +}; + +interface PatchBody { + userName?: string; + email?: string; + bio?: string; +} + +export const patch: Handler[] = [ + requireLogin, + async (req, res) => { + const id = req.params.id; + if (!mongoose.isValidObjectId(id)) return Resolve(res).badRequest('Invalid user ID provided'); + + const user = req.user!; + if (!user._id.equals(id)) return Resolve(res).forbidden('You are not authorized to modify this user.'); + + const body = assertRequestBody( + req, + res, + Joi.object({ + userName: Joi.string().trim(), + email: Joi.string().trim().email(), + bio: Joi.string().trim(), + }), + ); + + if (!body) return; + + if (body.email) { + const existing = await UserModel.findOne({ email: body.email }).lean(); + if (existing) return Resolve(res).conflict('Email already exists.'); + } + + await user.updateOne(body); + Resolve(res).ok(); + }, +]; diff --git a/server/src/routes/user/changeEmail.ts b/server/src/routes/user/changeEmail.ts new file mode 100644 index 00000000..70b416d8 --- /dev/null +++ b/server/src/routes/user/changeEmail.ts @@ -0,0 +1,29 @@ +import Joi from 'joi'; +import { Handler } from 'express'; +import { Resolve } from '../../utils/express'; +import { requireLogin } from '../../middlewares/require-login'; + +interface EmailBody { + newEmail: string, + confirmEmail: string +} + +export const patch: Handler[] = [ + requireLogin, + async (req, res) => { + const user = req.user!; + + const emailSchema = Joi.object({ + newEmail: Joi.string().trim().email().required(), + confirmEmail: Joi.string().trim().email().required() + }); + + const bodyValidationResult = emailSchema.validate(req.body); + if (bodyValidationResult.error) return res.status(400).json({ error: bodyValidationResult.error.message }); + + const value = bodyValidationResult.value; + + const result = await user.updateOne({ email: value.newEmail }); + return Resolve(res).okWith(result, 'Email changed successfully.'); + } +]; \ No newline at end of file diff --git a/server/src/routes/user/changeUsername.ts b/server/src/routes/user/changeUsername.ts new file mode 100644 index 00000000..8ad3a173 --- /dev/null +++ b/server/src/routes/user/changeUsername.ts @@ -0,0 +1,27 @@ +import Joi from 'joi'; +import { Handler } from 'express'; +import { Resolve } from '../../utils/express'; +import { requireLogin } from '../../middlewares/require-login'; + +interface UsernameBody { + newUsername: string +} + +export const patch: Handler[] = [ + requireLogin, + async (req, res) => { + const user = req.user!; + + const emailSchema = Joi.object({ + newUsername: Joi.string().trim().required() + }); + + const bodyValidationResult = emailSchema.validate(req.body); + if (bodyValidationResult.error) return res.status(400).json({ error: bodyValidationResult.error.message }); + + const value = bodyValidationResult.value; + + const result = await user.updateOne({ userName: value.newUsername }); + return Resolve(res).okWith(result, 'Username changed successfully.'); + } +]; \ No newline at end of file diff --git a/server/src/routes/user/changepassword.ts b/server/src/routes/user/changepassword.ts new file mode 100644 index 00000000..bde47f2e --- /dev/null +++ b/server/src/routes/user/changepassword.ts @@ -0,0 +1,46 @@ +import Joi from 'joi'; +import { Handler } from 'express'; +import { createHash } from '../../utils/bcrypt'; +import { compareToHashed } from '../../utils/bcrypt'; +import { Resolve } from '../../utils/express'; +import { requireLogin } from '../../middlewares/require-login'; + +interface PostBody { + password: string; + newpassword: string; + confirmpassword: string; +} + +export const patch: Handler[] = [ + requireLogin, + async (req, res) => { + const user = req.user!; + + const bodySchema = Joi.object({ + password: Joi.string().trim().required().messages({ + 'string.base': 'The given password must be a string.', + 'any.required': 'Password is required.', + }), + newpassword: Joi.string().trim().required().messages({ + 'string.base': 'The given password must be a string.', + 'any.required': 'New password is required.', + }), + confirmpassword: Joi.string().trim().required().valid(Joi.ref('newpassword')).messages({ + 'string.base': 'The given password must be a string.', + 'any.required': 'Confirm password is required.', + 'any.only': 'New passwords do not match.', + }), + }); + + const bodyValidationResult = bodySchema.validate(req.body); + if (bodyValidationResult.error) return Resolve(res).badRequest(bodyValidationResult.error.message); + + const { value: body } = bodyValidationResult; + + const passwordsMatch = await compareToHashed(body.password, user.password); + if (!passwordsMatch) return Resolve(res).forbidden('Password is incorrect.'); + + await user.updateOne({ password: await createHash(body.newpassword) }); + return Resolve(res).okWith(patch, 'Password changed successfully.'); + }, +]; \ No newline at end of file diff --git a/server/src/routes/user/deleteaccount/delete.ts b/server/src/routes/user/deleteaccount/delete.ts new file mode 100644 index 00000000..22180986 --- /dev/null +++ b/server/src/routes/user/deleteaccount/delete.ts @@ -0,0 +1,22 @@ +import mongoose from 'mongoose'; +import { Handler } from 'express'; +import { requireLogin } from '../../../middlewares/require-login'; +import { Resolve } from '../../../utils/express'; +import { UserModel } from '../../../models/user'; + +export const post: Handler[] = [ + requireLogin, + async (req, res) => { + + console.log(req.user); + + const userID = req.user?.id; + const User = mongoose.model('User', UserModel.schema); + + const user = await UserModel.findById(userID); + if (!user) return res.status(404).json({ error: 'No user found by the given ID.' }); + + const result = await User.findByIdAndDelete(userID); + Resolve(res).okWith("Account Deleted!"); + } +]; \ No newline at end of file diff --git a/server/src/routes/user/forgetpassword.ts b/server/src/routes/user/forgetpassword.ts new file mode 100644 index 00000000..cf0a1778 --- /dev/null +++ b/server/src/routes/user/forgetpassword.ts @@ -0,0 +1,61 @@ +import Joi from 'joi'; +import { Handler } from 'express'; +import { UserModel } from '../../models/user'; +import { TokenModel } from '../../models/token'; +import { Resolve } from '../../utils/express'; +import crypto from 'crypto'; +import {sendEmail} from '../../utils/email'; + +interface PostBody { + email: string; +} + +export const post: Handler = async (req, res) => { + const bodySchema = Joi.object({ + email: Joi.string().trim().email().required().messages({ + 'string.base': 'The given email must be a string.', + 'string.email': 'The given email is invalid.', + 'any.required': 'Email is required.', + }), + }); + + const bodyValidationResult = bodySchema.validate(req.body); + if (bodyValidationResult.error) return res.status(400).json({ error: bodyValidationResult.error.message }); + + const { value: body } = bodyValidationResult; + + const existingUser = await UserModel.findOne({ email: body.email }); + if (!existingUser) return Resolve(res).created(post, 'Sorry, no user with that email exists.'); + + const resetToken = crypto.randomBytes(32).toString('hex'); + const passwordResetToken = crypto.createHash('sha256').update(resetToken).digest('hex'); + const passwordResetExpires = Date.now() + 10 * 60 * 1000; + + const token = new TokenModel({ + userId: existingUser._id, + email: body.email, + passwordResetToken, + passwordResetExpires: new Date(passwordResetExpires), + }); + + await token.save(); + + const resetURL = `${req.protocol}://localhost:8000/resetpassword/${resetToken}`; + const message = `We have received a request to reset the password for your account. + \nYou can reset your password using the following link:\n${resetURL} + \nThe link is only valid for 10 minutes.\nIf you did not make this request, simply ignore this email.`; + + try { + await sendEmail({ + email: existingUser.email, + subject: 'Password Reset Request', + text: message + }); + Resolve(res).created(post, 'Password rest link has been sent your email.'); + } catch (error) { + token.deleteOne(); + console.log(error); + return res.status(500).json({ error: 'There was an error sending the email. Try again later.' }); + } + +}; \ No newline at end of file diff --git a/server/src/routes/user/index.ts b/server/src/routes/user/index.ts new file mode 100644 index 00000000..a94f330e --- /dev/null +++ b/server/src/routes/user/index.ts @@ -0,0 +1,9 @@ +import { Handler } from 'express'; +import { requireLogin } from '../../middlewares/require-login'; +import { Resolve } from '../../utils/express'; + +export const get: Handler[] = [ + requireLogin, + (req, res) => + req.user ? Resolve(res).okWith(req.user) : Resolve(res).unauthorized('Failed to authorize a correct user.'), +]; diff --git a/server/src/routes/user/login.ts b/server/src/routes/user/login.ts new file mode 100644 index 00000000..c8db9eb9 --- /dev/null +++ b/server/src/routes/user/login.ts @@ -0,0 +1,35 @@ +import Joi from 'joi'; +import { Handler } from 'express'; +import { UserModel } from '../../models/user'; +import { compareToHashed } from '../../utils/bcrypt'; +import { assertRequestBody, Resolve } from '../../utils/express'; +import { AuthToken } from '../../utils/auth-token'; +import { requireLogin } from '../../middlewares/require-login'; + +interface PostBody { + email: string; + password: string; +} + +export const get: Handler[] = [requireLogin, async (_, res) => Resolve(res).ok()]; + +export const post: Handler = async (req, res) => { + const body = assertRequestBody( + req, + res, + Joi.object({ + email: Joi.string().trim().email().required(), + password: Joi.string().trim().required(), + }), + ); + + if (!body) return; + + const existingUser = await UserModel.findOne({ email: body.email }); + if (!existingUser) return Resolve(res).notFound('No user with that email exists.'); + + const passwordsMatch = await compareToHashed(body.password, existingUser.password); + if (!passwordsMatch) return Resolve(res).unauthorized('Password is incorrect.'); + + Resolve(res).okWith(AuthToken.signAs(existingUser)); +}; diff --git a/server/src/routes/user/resetpassword/[token].ts b/server/src/routes/user/resetpassword/[token].ts new file mode 100644 index 00000000..14428630 --- /dev/null +++ b/server/src/routes/user/resetpassword/[token].ts @@ -0,0 +1,46 @@ +import Joi from 'joi'; +import { Handler } from 'express'; +import { UserModel } from '../../../models/user'; // Import the missing module from the correct file path +import { createHash } from '../../../utils/bcrypt'; +import { Resolve } from '../../../utils/express'; +import { TokenModel } from '../../../models/token'; +import crypto from 'crypto'; + +interface PostBody { + password: string; + confirmpassword: string; +} + +export const patch: Handler = async (req, res) => { + const token = crypto.createHash('sha256').update(req.params.token).digest('hex'); + + const tokenUser = await TokenModel.findOne({ passwordResetToken: token, passwordResetExpires: { $gt: Date.now() } }); + if (!tokenUser) return Resolve(res).created(patch, 'Token is invalid or has expired!'); + + const user = await UserModel.findById(tokenUser.userId); + if (!user) return Resolve(res).created(patch, 'No valid user found. Please try again.'); + + const bodySchema = Joi.object({ + password: Joi.string().trim().required().messages({ + 'string.base': 'The given password must be a string.', + 'any.required': 'Password is required.', + }), + confirmpassword: Joi.string().trim().required().valid(Joi.ref('password')).messages({ + 'string.base': 'The given password must be a string.', + 'any.required': 'Confirm password is required.', + 'string.notEqual': 'New passwords do not match.', + }), + }); + + const bodyValidationResult = bodySchema.validate(req.body); + if (bodyValidationResult.error) return Resolve(res).created(patch, 'New passwords do not match.'); + + const { value: body } = bodyValidationResult; + + await user.updateOne({ password: await createHash(body.password) }); + + await tokenUser.deleteOne(); + + return Resolve(res).created(patch, 'Password has been reset successfully.'); + +}; \ No newline at end of file diff --git a/server/src/routes/user/signup.ts b/server/src/routes/user/signup.ts new file mode 100644 index 00000000..b1879f12 --- /dev/null +++ b/server/src/routes/user/signup.ts @@ -0,0 +1,60 @@ +import Joi from 'joi'; +import mongoose from 'mongoose'; +import { Handler } from 'express'; +import { UserModel } from '../../models/user'; +import { createHash } from '../../utils/bcrypt'; +import { PlanetModel } from '../../models/planet'; +import { assertRequestBody, Resolve } from '../../utils/express'; +import { ILocation, RawLocationSchema } from '../../models/location'; +import { RawDocument } from '../../@types/model'; +import { AuthToken } from '../../utils/auth-token'; + +interface PostBody { + email: string; + userName: string; + password: string; + location: RawDocument; +} + +const inflightEmails = new Set(); + +export const post: Handler = async (req, res) => { + const body = assertRequestBody( + req, + res, + Joi.object({ + email: Joi.string().trim().email().required(), + userName: Joi.string().trim().required(), + password: Joi.string().required(), + location: RawLocationSchema, + }), + ); + + if (!body) return; + + if (inflightEmails.has(body.email)) return Resolve(res).conflict('Email is currently registering.'); + inflightEmails.add(body.email); + + try { + const existingEmail = await UserModel.findOne({ email: body.email }).lean().exec(); + if (existingEmail) return Resolve(res).conflict('Email is already registered.'); + + const isPlanetId = mongoose.isValidObjectId(body.location.planetId); + if (!isPlanetId) return Resolve(res).badRequest('The given planet ID is invalid.'); + + const planet = await PlanetModel.findById(body.location.planetId).lean().exec(); + if (!planet) return Resolve(res).badRequest('The given location does not exist.'); + + const user = new UserModel({ + email: body.email, + userName: body.userName, + password: await createHash(body.password), + location: body.location, + }); + + await user.save(); + Resolve(res).okWith(AuthToken.signAs(user)); + } finally { + inflightEmails.delete(body.email); + } +}; diff --git a/server/src/utils/auth-token.ts b/server/src/utils/auth-token.ts new file mode 100644 index 00000000..88a0be75 --- /dev/null +++ b/server/src/utils/auth-token.ts @@ -0,0 +1,47 @@ +import jwt from 'jsonwebtoken'; +import mongoose, { HydratedDocument } from 'mongoose'; +import { IUser, UserModel } from '../models/user'; + +/** + * The interface describing the payload + * type of the JWT token. + */ +interface JWTPayload { + userId: string; +} + +/** + * Utilities related to signing and verifying JWT payloads. + */ +export namespace AuthToken { + /** + * Verifies the given token with the JWT secret, and + * resolves it into a hydrated user model. + * + * @param token the token to verify + * @returns the resolved user, or nothing if the token was invalid or contained an invalid payload + */ + export async function verifyToUser(token: string) { + try { + const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload; + if (!payload || !('userId' in payload)) return; + + const userId = payload.userId; + if (!mongoose.isValidObjectId(userId)) return; + + return await UserModel.findById(userId); + } catch {} + } + + /** + * Signs a new token with the JWT secret and expiry time. + * + * @param user the user to sign the token with + * @returns the signed token + */ + export function signAs(user: HydratedDocument) { + return jwt.sign({ userId: user._id.toString() } satisfies JWTPayload, process.env.JWT_SECRET!, { + expiresIn: parseInt(process.env.JWT_TTL!), + }); + } +} diff --git a/server/src/utils/bcrypt.ts b/server/src/utils/bcrypt.ts new file mode 100644 index 00000000..2c4591e5 --- /dev/null +++ b/server/src/utils/bcrypt.ts @@ -0,0 +1,24 @@ +import bc from 'bcrypt'; + +const SALT_ROUNDS = 10; + +/** + * Hashes the given string with the default amount of salt rounds. + * + * @param str the string to hash + * @returns the hashed string, with the salt included + */ +export async function createHash(str: string) { + return await bc.hash(str, SALT_ROUNDS); +} + +/** + * Compares the given plaintext string to the given hashed string. + * + * @param plaintext the plaintext string + * @param hashed the hashed string to compare against + * @returns whether the two strings match + */ +export async function compareToHashed(plaintext: string, hashed: string) { + return await bc.compare(plaintext, hashed); +} diff --git a/server/src/utils/email.ts b/server/src/utils/email.ts new file mode 100644 index 00000000..d77bcd2e --- /dev/null +++ b/server/src/utils/email.ts @@ -0,0 +1,23 @@ +import nodemailer from 'nodemailer'; + +const sendEmail = async (option: { email: string, subject: string, text: string }) => { + const transporter = nodemailer.createTransport({ + host: process.env.EMAIL_HOST, + port: process.env.EMAIL_PORT, + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, + }, + } as nodemailer.TransportOptions); + + const emailOptions = { + from: 'Sky.net Support ', + to: option.email, + subject: option.subject, + text: option.text, + } + + await transporter.sendMail(emailOptions); +} + +export { sendEmail }; \ No newline at end of file diff --git a/server/src/utils/express.ts b/server/src/utils/express.ts new file mode 100644 index 00000000..a16e2e83 --- /dev/null +++ b/server/src/utils/express.ts @@ -0,0 +1,242 @@ +import Joi from 'joi'; +import { Request, Response } from 'express'; +import { getReasonPhrase, StatusCodes } from 'http-status-codes'; + +/** + * Represents the basic information included in a JSON response. + */ +interface BaseResponseBody { + /** Whether the operation was a success or not, infered from the status. */ + success: boolean; + + /** The status code of the response. */ + statusCode: number; + + /** The status message of the response. */ + statusMessage: string; +} + +interface SuccessResponseBody extends BaseResponseBody { + success: true; + + /** The message to send along with the response, if needed. */ + message?: string; + + /** The value to send along with the response, if there is one. */ + value?: unknown; +} + +interface FailureResponseBody extends BaseResponseBody { + success: false; + + /** The error message provided to the client. */ + error: string; +} + +/** + * Represents the data sent with a response when using the resolver. + */ +type ResponseBody = SuccessResponseBody | FailureResponseBody; + +/** + * Utility class to resolve Express responses with a reponse body + * template. + */ +export class Resolver { + /** + * Represents the current body of this response resolver. + * This will be used when sending the request. + */ + private body: ResponseBody = Resolver.responseOf(StatusCodes.OK); + + /** + * Creates a new resolver with the specified response object. + * By default, the response is that of a `200 OK` status code. + */ + public constructor(private readonly res: Response) {} + + /** + * Indicates that the request went okay, and provided a value. + * + * @see {@link StatusCodes.OK} + * @param value the value to send along with the response + * @param message the optional message to send along with the response + */ + public okWith(value: unknown, message?: string) { + this.body = Resolver.responseOf(StatusCodes.OK, message, value); + this.send(); + } + + /** + * Indicates that the request went okay, but no value is provided. + * + * @see {@link StatusCodes.OK} + * @param message the optional message to send along with the response + */ + public ok(message?: string) { + this.body = Resolver.responseOf(StatusCodes.OK, message); + this.send(); + } + + /** + * Indicates that the request resulted in a resource creation. + * + * @see {@link StatusCodes.CREATED} + * @param value the optional value to send along with the response + * @param message the optional message to send along with the response + */ + public created(value?: unknown, message?: string) { + this.body = Resolver.responseOf(StatusCodes.CREATED, message, value); + this.send(); + } + + /** + * Indicates that, due to client error, the server cannot handle this request. + * + * @see {@link StatusCodes.BAD_REQUEST} + * @param message the message specifying further details about the failure + */ + public badRequest(message: string) { + this.body = Resolver.responseOf(StatusCodes.BAD_REQUEST, message); + this.send(); + } + + /** + * Semantically indicates that the client is not "authenticated", + * the identity of the client is unknown to the server. + * + * @see {@link StatusCodes.UNAUTHORIZED} + * @param message the message specifying further details about the failure + */ + public unauthorized(message?: string) { + this.body = Resolver.responseOf(StatusCodes.UNAUTHORIZED, message); + this.send(); + } + + /** + * Indicates that the client cannot access the resource they are trying + * to access with this request, but the server is aware of the identity + * of the client. + * + * @see {@link StatusCodes.FORBIDDEN} + * @param message the message specifying further details about the failure + */ + public forbidden(message?: string) { + this.body = Resolver.responseOf(StatusCodes.FORBIDDEN, message); + this.send(); + } + + /** + * Indicates that the server encountered some sort of error while processing + * the request. + * + * @see {@link StatusCodes.INTERNAL_SERVER_ERROR} + * @param message the message specifying further details about the failure + */ + public error(message?: string) { + this.body = Resolver.responseOf(StatusCodes.INTERNAL_SERVER_ERROR, message); + this.send(); + } + + /** + * Indicates that there is no resource where the client is trying + * to access one. + * + * @see {@link StatusCodes.NOT_FOUND} + * @param message the message specifying further details about the failure + */ + public notFound(message?: string) { + this.body = Resolver.responseOf(StatusCodes.NOT_FOUND, message); + this.send(); + } + + /** + * Indicates that there was a conflict with the current state of the resource. + * + * @see {@link StatusCodes.CONFLICT} + * @param message the message specifying details about the conflict origins + */ + public conflict(message: string) { + this.body = Resolver.responseOf(StatusCodes.CONFLICT, message); + this.send(); + } + + /** + * Indicates that the specified resource was removed, likely permanently. + * + * @see {@link StatusCodes.GONE} + * @param message the message specifying detials about the removal + */ + public gone(message: string) { + this.body = Resolver.responseOf(StatusCodes.GONE, message); + this.send(); + } + + /** + * Sends the response with the specified custom, but typed, body. + * + * @param body the body to send the response with + */ + public custom(body: ResponseBody) { + this.body = body; + this.send(); + } + + /** + * Sends the response, specifying the code and attaching the body. + * Only sends if the request is still writable. + * + * This is only needed if you need to send the default response on initiation, + * all other methods automatically send the response. + */ + public send() { + if (this.res.writable) this.res.status(this.body.statusCode).json(this.body); + } + + /** + * Returns a response body based on the specified code and details. + * + * @param code the status code of the response + * @param message the details of the response, depending on the status code, this will be `error` or `message`. + * @param value the value of the response, only inserted if the response is a success + * @returns the constructed response body + */ + public static responseOf(code: number | StatusCodes, message?: string, value?: unknown): ResponseBody { + const statusCode = code; + const statusMessage = getReasonPhrase(code); + // Status code range: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status + const success = code < 300; + + const base = { statusCode, statusMessage, value }; + if (success) return { ...base, success: true, message }; + else return { ...base, success: false, error: message ?? statusMessage }; + } +} + +/** + * Creates a new resolver for the given response. + * + * This does not send the response, you must either use `send` or one + * of the utility methods provided. + * + * @returns the resolver + */ +export function Resolve(res: Response) { + return new Resolver(res); +} + +/** + * Given the specified request parameters, and a schema, this will ensure that the body of the request matches the schema. + * + * If it does not match, it will resolve the response with a status of `400`, with the Joi validation message as the error. + * + * @param req the request object + * @param res the response object + * @param schema the schema to validate the body against + * @returns the validated value of the schema, or nothing if the validation failed and the response was resolved + */ +export function assertRequestBody(req: Request, res: Response, schema: Joi.Schema) { + const bodyValidationResult = schema.validate(req.body); + if (bodyValidationResult.error) return Resolve(res).badRequest(bodyValidationResult.error.message); + else return bodyValidationResult.value; +} diff --git a/server/src/utils/regex.ts b/server/src/utils/regex.ts new file mode 100644 index 00000000..9167a5c3 --- /dev/null +++ b/server/src/utils/regex.ts @@ -0,0 +1,11 @@ +/** + * Escapes any special characters that would modify a regular expression. + * + * Source: https://github.com/component/escape-regexp/blob/master/index.js + * + * @param str the string to escape + * @returns the escaped string + */ +export function escapeRegex(str: string) { + return str.replace(/[-[\]{}()*+?.,\\/^$|#\s]/g, '\\$&'); +}