diff --git a/.gitignore b/.gitignore index 4b95c1d0..6d3a7653 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ # Secret files **/*.env + +# Mac Files +**/.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index df3b6863..9f1a8c98 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,10 @@ The project is built using [Typescript](https://www.typescriptlang.org/), and sp - [CSS Modules](https://github.com/css-modules/css-modules) (implemented by Vite) - [Bootstrap](https://getbootstrap.com/) - [axios](https://axios-http.com/) +- [dotenv](https://www.dotenv.org/) +- [React If](https://https://www.npmjs.com/package/react-if/) +- [React Toastify](https://fkhadra.github.io/react-toastify/introduction/) +- [Socket IO](https://socket.io/) (Client) **Server (built with [esno](https://www.npmjs.com/package/esno)):** @@ -40,6 +44,12 @@ The project is built using [Typescript](https://www.typescriptlang.org/), and sp - [http-status-codes](https://www.npmjs.com/package/http-status-codes) - [nodemailer](https://www.nodemailer.com/) - [JWT](https://jwt.io/) +- [CORS](https://www.npmjs.com/package/cors) +- [dotenv](https://www.dotenv.org/) +- [Google Auth Library](https://cloud.google.com/nodejs/docs/reference/google-auth-library/latest) +- [Google APIs](https://www.npmjs.com/package/googleapis) +- [Http Status Codes](https://www.npmjs.com/package/http-status-codes) +- [Socket IO](https://socket.io/) (Server) **Development Utilities:** @@ -69,6 +79,7 @@ Both the server and client utilize a `.env` file. | Key | Usage | | ---- | ------------------------------ | | PORT | Port used for the frontend app | +| VITE_LOCALHOST | Host used for listening to Server Socket Events | **Server Variables:** diff --git a/client/package-lock.json b/client/package-lock.json index 3d881abb..038cc1b5 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -10,12 +10,18 @@ "axios": "^1.6.8", "bootstrap": "^5.3.3", "dotenv": "^16.4.5", + "is-mobile": "^4.0.0", + "konva": "^9.3.9", "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-konva": "^18.2.10", "react-toastify": "^10.0.5", + "socket.io-client": "^4.7.5", + "use-image": "^1.1.1", + "vite": "^5.2.11", "wouter": "^3.1.2" }, "devDependencies": { @@ -24,8 +30,7 @@ "@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" + "typescript": "^5.4.5" } }, "node_modules/@babel/runtime": { @@ -46,7 +51,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "aix" @@ -62,7 +66,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "android" @@ -78,7 +81,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "android" @@ -94,7 +96,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "android" @@ -110,7 +111,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -126,7 +126,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -142,7 +141,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -158,7 +156,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -174,7 +171,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -190,7 +186,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -206,7 +201,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "linux" @@ -222,7 +216,6 @@ "cpu": [ "loong64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -238,7 +231,6 @@ "cpu": [ "mips64el" ], - "dev": true, "optional": true, "os": [ "linux" @@ -254,7 +246,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -270,7 +261,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -286,7 +276,6 @@ "cpu": [ "s390x" ], - "dev": true, "optional": true, "os": [ "linux" @@ -302,7 +291,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -318,7 +306,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "netbsd" @@ -334,7 +321,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "openbsd" @@ -350,7 +336,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "sunos" @@ -366,7 +351,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -382,7 +366,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -398,7 +381,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -476,7 +458,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "android" @@ -489,7 +470,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "android" @@ -502,7 +482,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -515,7 +494,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -528,7 +506,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -541,7 +518,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -554,7 +530,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -567,7 +542,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -580,7 +554,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -593,7 +566,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -606,7 +578,6 @@ "cpu": [ "s390x" ], - "dev": true, "optional": true, "os": [ "linux" @@ -619,7 +590,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -632,7 +602,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -645,7 +614,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -658,7 +626,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -671,12 +638,16 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" ] }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, "node_modules/@swc/core": { "version": "1.4.17", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.4.17.tgz", @@ -901,14 +872,13 @@ "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" }, "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, + "devOptional": true, "dependencies": { "undici-types": "~5.26.4" } @@ -936,6 +906,14 @@ "@types/react": "*" } }, + "node_modules/@types/react-reconciler": { + "version": "0.28.8", + "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.8.tgz", + "integrity": "sha512-SN9c4kxXZonFhbX4hJrZy37yw9e7EIxcpHCxQv5JUS18wDE5ovkQKlqQEkufdJCCMfuI9BnjUJvhYeJ9x5Ra7g==", + "dependencies": { + "@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", @@ -965,7 +943,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, + "devOptional": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -993,7 +971,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8" }, @@ -1023,7 +1001,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, + "devOptional": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -1035,7 +1013,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, + "devOptional": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -1084,6 +1062,22 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "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/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1120,11 +1114,30 @@ "url": "https://dotenvx.com" } }, + "node_modules/engine.io-client": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", + "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/esbuild": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -1162,7 +1175,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, + "devOptional": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1206,7 +1219,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -1220,7 +1232,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, + "devOptional": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -1232,7 +1244,7 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", - "dev": true + "devOptional": true }, "node_modules/invariant": { "version": "2.2.4", @@ -1246,7 +1258,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, + "devOptional": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -1258,7 +1270,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -1267,7 +1279,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, + "devOptional": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -1275,20 +1287,55 @@ "node": ">=0.10.0" } }, + "node_modules/is-mobile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-4.0.0.tgz", + "integrity": "sha512-mlcHZA84t1qLSuWkt2v0I2l61PYdyQDt4aG1mLIXF5FDMm4+haBCxCPYSr/uwqQNRk1MiTizn0ypEuRAOLRAew==" + }, "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, + "devOptional": true, "engines": { "node": ">=0.12.0" } }, + "node_modules/its-fine": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-1.2.5.tgz", + "integrity": "sha512-fXtDA0X0t0eBYAGLVM5YsgJGsJ5jEmqZEPrGbzdf5awjv0xE7nqv3TVnvtUF060Tkes15DbDAKW/I48vsb6SyA==", + "dependencies": { + "@types/react-reconciler": "^0.28.0" + }, + "peerDependencies": { + "react": ">=18.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, + "node_modules/konva": { + "version": "9.3.9", + "resolved": "https://registry.npmjs.org/konva/-/konva-9.3.9.tgz", + "integrity": "sha512-0ACsA2kCGilptQqosTVdu5g1jAeONZI/W/L5U7afeRhoWqSV+HjcuBB+vsFzs64oL/02y+4/jd3K5RL0sLCxbQ==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/lavrton" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/konva" + }, + { + "type": "github", + "url": "https://github.com/sponsors/lavrton" + } + ] + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -1324,11 +1371,15 @@ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==" }, + "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/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", - "dev": true, "funding": [ { "type": "github", @@ -1346,7 +1397,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -1362,14 +1413,13 @@ "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 + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8.6" }, @@ -1381,7 +1431,6 @@ "version": "8.4.38", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", - "dev": true, "funding": [ { "type": "opencollective", @@ -1511,11 +1560,56 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, + "node_modules/react-konva": { + "version": "18.2.10", + "resolved": "https://registry.npmjs.org/react-konva/-/react-konva-18.2.10.tgz", + "integrity": "sha512-ohcX1BJINL43m4ynjZ24MxFI1syjBdrXhqVxYVDw2rKgr3yuS0x/6m1Y2Z4sl4T/gKhfreBx8KHisd0XC6OT1g==", + "funding": [ + { + "type": "patreon", + "url": "https://www.patreon.com/lavrton" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/konva" + }, + { + "type": "github", + "url": "https://github.com/sponsors/lavrton" + } + ], + "dependencies": { + "@types/react-reconciler": "^0.28.2", + "its-fine": "^1.1.1", + "react-reconciler": "~0.29.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "konva": "^8.0.1 || ^7.2.5 || ^9.0.0", + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, "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-reconciler": { + "version": "0.29.2", + "resolved": "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.29.2.tgz", + "integrity": "sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, "node_modules/react-toastify": { "version": "10.0.5", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz", @@ -1547,7 +1641,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, + "devOptional": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -1572,7 +1666,6 @@ "version": "4.17.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", - "dev": true, "dependencies": { "@types/estree": "1.0.5" }, @@ -1607,7 +1700,7 @@ "version": "1.77.1", "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.1.tgz", "integrity": "sha512-OMEyfirt9XEfyvocduUIOlUSkWOXS/LAt6oblR/ISXCTukyavjex+zQNm51pPCOiFKY1QpWvEH1EeCkgyV3I6w==", - "dev": true, + "devOptional": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -1628,11 +1721,36 @@ "loose-envify": "^1.1.0" } }, + "node_modules/socket.io-client": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", + "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -1641,7 +1759,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, + "devOptional": true, "dependencies": { "is-number": "^7.0.0" }, @@ -1685,7 +1803,16 @@ "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 + "devOptional": true + }, + "node_modules/use-image": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/use-image/-/use-image-1.1.1.tgz", + "integrity": "sha512-n4YO2k8AJG/BcDtxmBx8Aa+47kxY5m335dJiCQA5tTeVU4XdhrhqR6wT0WISRXwdMEOv5CSjqekDZkEMiiWaYQ==", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } }, "node_modules/use-sync-external-store": { "version": "1.2.2", @@ -1699,7 +1826,6 @@ "version": "5.2.11", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", - "dev": true, "dependencies": { "esbuild": "^0.20.1", "postcss": "^8.4.38", @@ -1770,6 +1896,34 @@ "peerDependencies": { "react": ">=16.8.0" } + }, + "node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "engines": { + "node": ">=0.4.0" + } } } } diff --git a/client/package.json b/client/package.json index eae230b3..eb6466fe 100644 --- a/client/package.json +++ b/client/package.json @@ -6,19 +6,25 @@ "scripts": { "build": "vite build", "start": "npm run build && vite preview", - "dev": "vite" + "dev": "vite --host" }, "dependencies": { "@popperjs/core": "^2.11.8", "axios": "^1.6.8", "bootstrap": "^5.3.3", "dotenv": "^16.4.5", + "is-mobile": "^4.0.0", + "konva": "^9.3.9", "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-konva": "^18.2.10", "react-toastify": "^10.0.5", + "socket.io-client": "^4.7.5", + "use-image": "^1.1.1", + "vite": "^5.2.11", "wouter": "^3.1.2" }, "devDependencies": { @@ -27,7 +33,6 @@ "@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" + "typescript": "^5.4.5" } } diff --git a/client/src/app.tsx b/client/src/app.tsx index 57a51957..60f5a279 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -1,32 +1,49 @@ import { ToastContainer } from 'react-toastify'; -import { Switch, Route, Redirect, useLocation } from 'wouter'; +import { Switch, Route, 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 ManageAccount from './pages/user-settings/options/manage-account'; +import Messages from './pages/messages/messages-component'; +import Policy from './pages/about/options/policy'; +import Terms from './pages/about/options/terms'; import { useEffect, useState } from 'react'; -import { Auth } from './lib/auth'; +import { Auth, UserAuthContext } from './lib/auth'; +import Cursors from './components/cursor/cursor'; -import './index.css'; import { Else, If, Then } from 'react-if'; +import { Loader } from './components/loader/loader'; +import PostPage from './pages/post-page/post-page'; +import UserPage from './pages/user-page/user-page'; +import ProfilePage from './pages/profile-page/profile-page'; +import { PlanetMap } from './pages/planet-map/planet-map'; + +import './index.css'; +import Home from './pages/home/home'; +import PostDetailPage from './pages/post/post'; +import Planets from './pages/planets/planets-component'; +import PlanetFeed from './pages/planet-feed/planet-feed'; export const App = () => { - const [authorized, setAuthorized] = useState(undefined); + const [loading, setLoading] = useState(true); + const [authenticatedUser, setAuthenticatedUser] = useState(); const [loc] = useLocation(); - useEffect(() => { - Auth.resaveToken(); - }, []); + useEffect(() => void Auth.resaveToken(), []); useEffect(() => { - Auth.isAuthorized().then((v) => setAuthorized(v === true)); + setLoading(true); + Auth.getAuthenticatedUser().then((user) => { + setAuthenticatedUser(user); + setLoading(false); + }); }, [loc]); const commonRoutes = ( @@ -34,10 +51,13 @@ export const App = () => { + + {(params) => } - + + {(params) => } 404 Not Found ); @@ -45,35 +65,42 @@ export const App = () => { return ( <> + - + - - - - - - - - {commonRoutes} - + - + {commonRoutes} - - } /> -
-

Loading...

-
+ + + + + + + + + + + + + + + + + {commonRoutes} + +
diff --git a/client/src/assets/images/amongus-black.webp b/client/src/assets/images/amongus-black.webp new file mode 100644 index 00000000..3332c523 Binary files /dev/null and b/client/src/assets/images/amongus-black.webp differ diff --git a/client/src/assets/images/amongus-blue.webp b/client/src/assets/images/amongus-blue.webp new file mode 100644 index 00000000..616d62d0 Binary files /dev/null and b/client/src/assets/images/amongus-blue.webp differ diff --git a/client/src/assets/images/amongus-green.webp b/client/src/assets/images/amongus-green.webp new file mode 100644 index 00000000..d4323ffb Binary files /dev/null and b/client/src/assets/images/amongus-green.webp differ diff --git a/client/src/assets/images/amongus-pink.webp b/client/src/assets/images/amongus-pink.webp new file mode 100644 index 00000000..0c30a416 Binary files /dev/null and b/client/src/assets/images/amongus-pink.webp differ diff --git a/client/src/assets/images/amongus-red.webp b/client/src/assets/images/amongus-red.webp new file mode 100644 index 00000000..e97233cb Binary files /dev/null and b/client/src/assets/images/amongus-red.webp differ diff --git a/client/src/assets/images/amongus-white.webp b/client/src/assets/images/amongus-white.webp new file mode 100644 index 00000000..e694791f Binary files /dev/null and b/client/src/assets/images/amongus-white.webp differ diff --git a/client/src/assets/images/amongus-yellow.webp b/client/src/assets/images/amongus-yellow.webp new file mode 100644 index 00000000..f090ecfe Binary files /dev/null and b/client/src/assets/images/amongus-yellow.webp differ diff --git a/client/src/assets/images/icons8-cursor-38.png b/client/src/assets/images/icons8-cursor-38.png new file mode 100644 index 00000000..7715a690 Binary files /dev/null and b/client/src/assets/images/icons8-cursor-38.png differ diff --git a/client/src/components/Comment/Comment.module.css b/client/src/components/Comment/Comment.module.css new file mode 100644 index 00000000..fe6a9945 --- /dev/null +++ b/client/src/components/Comment/Comment.module.css @@ -0,0 +1,102 @@ +.link { + color: inherit; + text-decoration: none; +} + +.commentContainer { + font-family: TTOctosquares; + max-width: 750px; + width: 100%; + margin-top: 20px; + display: flex; + flex-direction: column; +} + +.commentsList { + flex-grow: 1; +} + +.commentForm { + display: flex; + flex-direction: column; + margin-top: 20px; +} + +.commentTextArea { + width: 100%; + min-height: 50px; + padding: 10px; + font-size: 0.9rem; + /* Slightly smaller than the post text */ + resize: vertical; + border: 1px solid #ccc; + border-radius: 5px; + margin-bottom: 10px; + font-family: TTOctosquares; +} + +.commentButton { + width: fit-content; + align-self: flex-end; + padding: 5px 10px; + font-size: 1rem; + background-color: #007bff; + color: #fff; + border: none; + border-radius: 5px; + cursor: pointer; + font-family: TTOctosquares; +} + +.postContainer { + border-radius: 5px; + padding: 10px; + margin-bottom: 10px; + font-size: 0.8rem; + /* Slightly smaller than the post text */ + font-family: TTOctosquares; +} + +.userContainer { + padding: 2px 8px; + margin: 2px 0px; + width: max-content; + text-align: left; + font-size: 0.8rem; + /* Slightly smaller than the post text */ +} + +.paraContainer { + padding: 10px; + padding-bottom: 5px; + margin: 2px 0px; + width: 100%; + text-align: left; + word-break: break-word; + font-size: 0.9rem; + /* Slightly smaller than the post text */ +} + +.postDate { + font-size: 0.6em; + color: var(--soft-shadow); +} + +.iconsContainer { + display: flex; + font-size: 1.2rem; + /* Slightly smaller than the post icons */ + gap: 0.75rem; +} + +.iconsContainer button { + background-color: transparent; + padding: 5px; + width: auto; + box-shadow: none; + border: none; +} + +.share { + margin-right: auto; +} \ No newline at end of file diff --git a/client/src/components/Comment/Comment.tsx b/client/src/components/Comment/Comment.tsx new file mode 100644 index 00000000..d6c224bb --- /dev/null +++ b/client/src/components/Comment/Comment.tsx @@ -0,0 +1,240 @@ +import React, { useEffect, useState } from 'react'; +import { api } from '../../lib/axios'; +import styles from './Comment.module.css'; +import UIBox from '../UIBox/UIBox'; +import { FaRegHeart, FaHeart, FaRegBookmark, FaBookmark, FaRocketchat } from 'react-icons/fa'; +import { RiShareBoxLine } from 'react-icons/ri'; +import { Link } from 'wouter'; + +interface CommentProps { + postId: string; +} + +interface Comment { + _id: string; + authorId: string; + content: string; + createdAt: string; + userName: string; + repost: number; + like: number; + comment: number; + isSaved?: boolean; + isLiked?: boolean; +} + +interface UserProp { + username: string; + userURL: string; + imageURL?: string; +} + +const User = (props: UserProp): JSX.Element => { + return ( + + {props.username} + + } + /> + ); +}; + +const Comment: React.FC = ({ postId }) => { + const [comment, setComment] = useState(''); + const [comments, setComments] = useState([]); + const [page, setPage] = useState(1); + const [hasMore, setHasMore] = useState(true); + const [loading, setLoading] = useState(false); + const fetchComments = async (page: number) => { + setLoading(true); + try { + const response = await api.get(`/post/${postId}/comment`, { + params: { limit: 10, page }, + }); + if (response.data.success) { + const commentsWithStatus = await Promise.all( + response.data.value.map(async (comment: Comment) => { + const [savedRes, likedRes] = await Promise.all([ + api.get(`/post/${comment._id}/save`), + api.get(`/post/${comment._id}/like`), + ]); + return { + ...comment, + isSaved: savedRes.data.success ? savedRes.data.value : false, + isLiked: likedRes.data.value, + }; + }) + ); + setComments((prevComments) => [...prevComments, ...commentsWithStatus]); + setHasMore(commentsWithStatus.length === 10); // Assuming 10 is the limit + } else { + console.error(response.data.statusMessage); + } + } catch (error) { + console.error('Error fetching comments:', error); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchComments(page); + }, [page, postId]); + + const handleCommentChange = (event: React.ChangeEvent) => { + setComment(event.target.value); + }; + + const handleCommentSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + if (comment.trim()) { + try { + const response = await api.post(`/post/${postId}/comment`, { content: comment }); + if (response.data.success) { + const newComment = response.data.value; + const [savedRes, likedRes] = await Promise.all([ + api.get(`/post/${newComment._id}/save`), + api.get(`/post/${newComment._id}/like`), + ]); + newComment.isSaved = savedRes.data.success ? savedRes.data.value : false; + newComment.isLiked = likedRes.data.value; + const updatedComments = [newComment, ...comments].sort( + (a: Comment, b: Comment) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + ); + setComments(updatedComments); + setComment(''); + } else { + console.error(response.data.statusMessage); + } + } catch (error) { + console.error('Error posting comment:', error); + } + } + }; + + const handleBookmark = async (commentId: string, isSaved: boolean) => { + try { + if (isSaved) { + await api.delete(`/post/${commentId}/save`); + } else { + await api.post(`/post/${commentId}/save`); + } + setComments((prevComments) => + prevComments.map((comment) => + comment._id === commentId ? { ...comment, isSaved: !isSaved } : comment + ) + ); + } catch (error) { + console.error('Error updating save status:', error); + } + }; + + const handleLike = async (commentId: string, isLiked: boolean) => { + try { + if (isLiked) { + await api.delete(`/post/${commentId}/like`); + setComments((prevComments) => + prevComments.map((comment) => + comment._id === commentId ? { ...comment, like: comment.like - 1, isLiked: false } : comment + ) + ); + } else { + await api.post(`/post/${commentId}/like`); + setComments((prevComments) => + prevComments.map((comment) => + comment._id === commentId ? { ...comment, like: comment.like + 1, isLiked: true } : comment + ) + ); + } + } catch (error) { + console.error('Error updating like status:', error); + } + }; + + function formatDate(dateString: string): string { + const date = new Date(dateString); + const year = date.getFullYear(); + const month = date.getMonth() + 1; // Months are zero-indexed + const day = date.getDate(); + const hours = date.getHours(); + const minutes = date.getMinutes(); + const seconds = date.getSeconds(); + return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`; + } + + const loadMoreComments = () => { + if (hasMore && !loading) { + setPage((prevPage) => prevPage + 1); + } + }; + + return ( +
+
+ + } + curved + /> +
+ + +
+
+ + } + /> + + ); +}; + +export default PostPage; diff --git a/client/src/pages/post/post.module.css b/client/src/pages/post/post.module.css new file mode 100644 index 00000000..e69de29b diff --git a/client/src/pages/post/post.tsx b/client/src/pages/post/post.tsx new file mode 100644 index 00000000..c6f6f373 --- /dev/null +++ b/client/src/pages/post/post.tsx @@ -0,0 +1,94 @@ +import styles from './post.module.css'; + +import React, { useEffect, useState } from 'react'; +import { api } from '../../lib/axios'; +import Post from '../../components/Post/Post'; +import Page from '../../components/Page/Page'; +import Comment from '../../components/Comment/Comment'; + +interface Post { + _id: string; + authorId: string; + content: string; + likeCount: number; + commentCount: number; + repostCount: number; + location: { + planetId: string; + latitude: number; + longitude: number; + _id: string; + }; + media: any[]; + createdAt: Date; + deleted: boolean; + userName: string; +} + +interface PostResponse { + statusCode: number; + statusMessage: string; + value: Post; + success: boolean; +} + +interface Props { + id: string; +} + +const PostDetailPage: React.FC = ({ id }) => { + const [post, setPost] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchPost = async () => { + try { + const response = await api.get(`/post/${id}`); + if (response.data.success) { + setPost(response.data.value); + } else { + setError(response.data.statusMessage); + } + } catch (err) { + setError('Error fetching the post.'); + } + }; + + fetchPost(); + }, [id]); + + if (error) { + return
Error: {error}
; + } + + if (!post) { + return
Loading...
; + } + + const postDetail = ( + + ); + + return ( + + {postDetail} + + + } + /> + ); +}; + +export default PostDetailPage; diff --git a/client/src/pages/profile-page/profile-page.module.css b/client/src/pages/profile-page/profile-page.module.css new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/client/src/pages/profile-page/profile-page.module.css @@ -0,0 +1 @@ + diff --git a/client/src/pages/profile-page/profile-page.tsx b/client/src/pages/profile-page/profile-page.tsx new file mode 100644 index 00000000..41e1b47f --- /dev/null +++ b/client/src/pages/profile-page/profile-page.tsx @@ -0,0 +1,87 @@ +import styles from './profile-page.module.css'; +import Page from '../../components/Page/Page'; +import Post from '../../components/Post/Post'; +import Profile from '../../components/Profile/Profile'; +import { api } from '../../lib/axios'; +import { useContext, useEffect, useState } from 'react'; +import { UserAuthContext } from '../../lib/auth'; + +interface Post { + authorId: string; + commentCount: number; + content: string; + createdAt: Date; + deleted: false; + likeCount: number; + location: { + planetId: string; + latitude: number; + longitude: number; + _id: string; + }; + media: []; + repostCount: number; + __v: number; + _id: string; +} + +const ProfilePage = () => { + const user = useContext(UserAuthContext); + const [displayedPosts, setDisplayedPosts] = useState(Array()); + + useEffect(() => { + const displayPosts = async function () { + setDisplayedPosts(await getPosts()); + }; + displayPosts(); + }, [user]); + + async function getPosts() { + try { + if (!user) return; + const res = await api.get('/feed/' + user._id); + const postArray = res.data.value; + let postElements = postArray.map((post: Post) => { + return ( + + ); + }); + if (postArray.length == 0) { + postElements = [<>Nothing yet...]; + } + return postElements; + } catch (err) { + console.log(err); + } + } + + return ( + + + {displayedPosts} + + } + /> + ); +}; + +export default ProfilePage; diff --git a/client/src/pages/signup/signup-html.tsx b/client/src/pages/signup/signup-html.tsx index f7d23023..15c9d516 100644 --- a/client/src/pages/signup/signup-html.tsx +++ b/client/src/pages/signup/signup-html.tsx @@ -1,6 +1,7 @@ import { useLocation } from 'wouter'; import styles from './signup.module.css'; import logoUrl from '../../assets/images/SkynetLogo.png'; +import { GoogleAuthButton } from '../../components/google-auth-btn/google-auth-btn'; const SignupHtml = ({ planets, @@ -72,6 +73,13 @@ const SignupHtml = ({ + +
+ + + +
+
Already a User. Login Below + +
+ } + /> + + props.emailBody2.setEmailBody2(false)} + body={ + <> +

What would you like your new email to be?

+
+ props.emailBody2.setCurrEmail(event.target.value)} + required + /> + props.emailBody2.setEmailInput(event.target.value)} + required + /> + props.emailBody2.setConfEmailInput(event.target.value)} + required + /> +
+ + +
+
+ + } + disableFooter={true} + /> + + ); +}; + +export default ChangeEmailModal; diff --git a/client/src/pages/user-settings/options/change-password-modal.tsx b/client/src/pages/user-settings/options/change-password-modal.tsx new file mode 100644 index 00000000..eee3be1c --- /dev/null +++ b/client/src/pages/user-settings/options/change-password-modal.tsx @@ -0,0 +1,145 @@ +import { useState } from 'react'; + +import styles from '../user-settings.module.css'; + +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) => { + const clearFields = () => { + props.passBody2.setPassword(''); + props.passBody2.setNewPassword(''); + props.passBody2.setConfPassword(''); + }; + + 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/change-password.tsx b/client/src/pages/user-settings/options/change-password.tsx deleted file mode 100644 index 59258250..00000000 --- a/client/src/pages/user-settings/options/change-password.tsx +++ /dev/null @@ -1,124 +0,0 @@ -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/change-username.tsx b/client/src/pages/user-settings/options/change-username.tsx new file mode 100644 index 00000000..87672f5b --- /dev/null +++ b/client/src/pages/user-settings/options/change-username.tsx @@ -0,0 +1,95 @@ +import styles from '../user-settings.module.css'; + +import ModalConfirmation from '../../../components/ModalConfirmation/ModalConfirmation'; +import Button from 'react-bootstrap/Button'; + +interface Props { + usernameBody1: { + showNameBody1: boolean; + setNameBody1: any; + }; + usernameBody2: { + showNameBody2: boolean; + setNameBody2: any; + changeUsername: any; + nameInput: string; + setNameInput: any; + }; +} + +const ChangeNameModal = (props: Props) => { + const clearFields = () => { + props.usernameBody2.setNameInput(''); + }; + + return ( + <> + Are you sure you want to change your username?

} + disableFooter={false} + footer={ +
+ + +
+ } + /> + + props.usernameBody2.setNameBody2(false)} + body={ + <> +

What would you like your new username to be?

+
+ props.usernameBody2.setNameInput(event.target.value)} + required + /> +
+ + +
+
+ + } + disableFooter={true} + /> + + ); +}; + +export default ChangeNameModal; diff --git a/client/src/pages/user-settings/options/delete-account.tsx b/client/src/pages/user-settings/options/delete-account-modal.tsx similarity index 64% rename from client/src/pages/user-settings/options/delete-account.tsx rename to client/src/pages/user-settings/options/delete-account-modal.tsx index 9311f384..694fa0ac 100644 --- a/client/src/pages/user-settings/options/delete-account.tsx +++ b/client/src/pages/user-settings/options/delete-account-modal.tsx @@ -4,26 +4,29 @@ import ModalConfirmation from '../../../components/ModalConfirmation/ModalConfir import Button from 'react-bootstrap/Button'; interface Props { - deleteBody1: { - showDeleteBody1: boolean, - setShowDelete1: any - }, - deleteBody2: { - showDeleteBody2: boolean, - setShowDelete2: any - deleteAccount: any, - confInput: string, - setConfInput: any - }, + deleteBody1: { + showDeleteBody1: boolean; + setShowDelete1: any; + }; + deleteBody2: { + showDeleteBody2: boolean; + setShowDelete2: any; + deleteAccount: any; + confInput: string; + setConfInput: any; + }; } const DeleteAccountModal = (props: Props) => { + const clearFields = () => { + props.deleteBody2.setConfInput(''); + }; + return ( <> props.deleteBody1.setShowDelete1(false)} body={

Are you sure you want to delete your account? @@ -61,20 +64,34 @@ const DeleteAccountModal = (props: Props) => { onHide={() => props.deleteBody2.setShowDelete2(false)} body={ <> -

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

+

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

props.deleteBody2.setConfInput(event.target.value)} required /> -
- - +
diff --git a/client/src/pages/user-settings/options/manage-account.tsx b/client/src/pages/user-settings/options/manage-account.tsx new file mode 100644 index 00000000..82c8b86a --- /dev/null +++ b/client/src/pages/user-settings/options/manage-account.tsx @@ -0,0 +1,229 @@ +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import { api } from '../../../lib/axios'; +import { useLocation } from 'wouter'; +import { Auth } from '../../../lib/auth'; + +import styles from '../user-settings.module.css'; + +import ListGroup from 'react-bootstrap/ListGroup'; +import ChangePasswordModal from '../options/change-password-modal'; +import DeleteAccountModal from '../options/delete-account-modal'; +import ChangeNameModal from './change-username'; +import ChangeEmailModal from './change-email'; +import Page from '../../../components/Page/Page'; + +const ManageAccount = () => { + const [_, setLocation] = useLocation(); + + // variables responsible for showing change password modals + const [showPassBody1, setShowPass1] = useState(false); + const [showPassBody2, setShowPass2] = useState(false); + + // variables responsible for showing delete account modals + const [showDeleteBody1, setShowDelete1] = useState(false); + const [showDeleteBody2, setShowDelete2] = useState(false); + + // variables responsible for showing change username modals + const [showNameBody1, setNameBody1] = useState(false); + const [showNameBody2, setNameBody2] = useState(false); + + // variables responsible for showing change email modals + const [showEmailBody1, setEmailBody1] = useState(false); + const [showEmailBody2, setEmailBody2] = 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(''); + + // varriables responsible for grabbing form fields in the change password modal + const [nameInput, setNameInput] = useState(''); + + //variables resposible for grabbing the form fields present in the change email modal + const [currEmail, setCurrEmail] = useState(''); + const [emailInput, setEmailInput] = useState(''); + const [confEmailInput, setConfEmailInput] = 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(response.data.message); + wrapUpModal(); + } catch (error: any) { + toast.error(error.response.data.error); + } + }; + + const deleteAccount = async (event: React.FormEvent) => { + event.preventDefault(); + try { + const response = await api.post('/user/deleteaccount/delete', { + confirmationInput: confInput, + }); + toast.success(response.data.message); + wrapUpModal(); + logout(); + } catch (error: any) { + toast.error(error.response.data.error); + } + }; + + const changeUsername = async (event: React.FormEvent) => { + event.preventDefault(); + try { + const response = await api.patch('/user/changeUsername', { + newUsername: nameInput, + }); + toast.success(response.data.message); + wrapUpModal(); + } catch (error: any) { + toast.error(error.response.data.error); + } + }; + + const changeEmail = async (event: React.FormEvent) => { + event.preventDefault(); + try { + const response = await api.patch('/user/changeEmail', { + currEmail: currEmail, + newEmail: emailInput, + confirmEmail: confEmailInput, + }); + toast.success(response.data.message); + wrapUpModal(); + } catch (error: any) { + toast.error(error.response.data.error); + } + }; + + const wrapUpModal = () => { + setPassword(''); + setNewPassword(''); + setConfirmPassword(''); + setConfInput(''); + setNameInput(''); + setCurrEmail(''); + setEmailInput(''); + setConfEmailInput(''); + setShowPass2(false); + setShowDelete2(false); + setNameBody2(false); + setEmailBody2(false); + }; + + const logout = () => [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 for the change username Modal + const usernameBody1 = { + showNameBody1: showNameBody1, + setNameBody1: setNameBody1, + }; + const usernameBody2 = { + showNameBody2: showNameBody2, + setNameBody2: setNameBody2, + changeUsername: changeUsername, + nameInput: nameInput, + setNameInput: setNameInput, + }; + + // defining values for the change email Modal + const emailBody1 = { + showEmailBody1: showEmailBody1, + setEmailBody1: setEmailBody1, + }; + const emailBody2 = { + showEmailBody2: showEmailBody2, + setEmailBody2: setEmailBody2, + changeEmail: changeEmail, + currEmail: currEmail, + setCurrEmail: setCurrEmail, + emailInput: emailInput, + setEmailInput: setEmailInput, + confEmailInput: confEmailInput, + setConfEmailInput: setConfEmailInput, + }; + + return ( + <> + + +

Danger Zone

+ + setNameBody1(true)}> + Change Username + + setShowPass1(true)}> + Change Password + + setEmailBody1(true)}> + Change Email + + setShowDelete1(true)}> + DELETE ACCOUNT + + +
+ + } + /> + + + + + + + ); +}; + +export default ManageAccount; diff --git a/client/src/pages/user-settings/options/your-info.tsx b/client/src/pages/user-settings/options/your-info.tsx index 074b5de2..8deb016c 100644 --- a/client/src/pages/user-settings/options/your-info.tsx +++ b/client/src/pages/user-settings/options/your-info.tsx @@ -1,35 +1,19 @@ -import { api } from '../../../lib/axios'; -import { useState } from 'react'; +import { useContext } from 'react'; import ModalConfirmation from '../../../components/ModalConfirmation/ModalConfirmation'; import Button from 'react-bootstrap/Button'; +import { UserAuthContext } from '../../../lib/auth'; +import { Else, If, Then } from 'react-if'; interface Props { - infoBody: { - showInfoBody: boolean, - setInfoBody: any - } + 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(); + const user = useContext(UserAuthContext); return ( <> @@ -38,13 +22,16 @@ const YourInfoModal = (props: Props) => { show={props.infoBody.showInfoBody} onHide={() => props.infoBody.setInfoBody(false)} body={ - <> -

- Username:
- Email:
-
-

- + + +

Username: {user.userName}

+

Email: {user.email ?? 'Authenticated with Provider'}

+

Bio: {user.bio ?? 'No Bio Provided'}

+
+ +

An error occured.

+
+
} disableFooter={false} footer={ @@ -64,4 +51,4 @@ const YourInfoModal = (props: Props) => { ); }; -export default YourInfoModal; \ No newline at end of file +export default YourInfoModal; diff --git a/client/src/pages/user-settings/user-settings.module.css b/client/src/pages/user-settings/user-settings.module.css index bf62dc02..d37f6d5f 100644 --- a/client/src/pages/user-settings/user-settings.module.css +++ b/client/src/pages/user-settings/user-settings.module.css @@ -1,7 +1,11 @@ .setting-body { + width: 100%; font-size: x-large; font-family: BabaPro; - background-color: aliceblue; +} + +.setting-body p { + margin: 0; } .setting-title { @@ -9,19 +13,63 @@ font-family: Bitsumishi; } +.warning-color { + color: red !important; +} + +.danger-zone { + border: 2px solid black; + box-shadow: 4px 5px 0px #000; +} + .danger-zone, .danger-zone * { background-color: #242C38; - color: red; + color: red !important; +} + +.group-item-holder { + margin-bottom: 30px; + margin-left: 5px; + margin-right: 5px; + font-size: 16pt; + border: 2px solid black; + box-shadow: 4px 5px 0px #000; +} + +.group-item-holder, .group-item-holder * { + color: black; + background-color: var(--signup-background); } -.group-item, .group-item * { - background-color: aliceblue; +.group-item { + border-color: gray; +} + +.group-item-body { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; } .clickable { cursor: pointer; } -.delete-btn { - background-color: red; +.logout-btn { + border: 2px solid black; + box-shadow: 4px 5px 0px #000; +} + +.logout-btn h1 { + margin: 0; +} + +.logout-btn, .logout-btn > * { + background-color: #242C38; + color: red; +} + +.conf-input { + font-family: Fjalla One; } \ 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 index 0d5dac16..544bda89 100644 --- a/client/src/pages/user-settings/user-settings.tsx +++ b/client/src/pages/user-settings/user-settings.tsx @@ -1,104 +1,31 @@ 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 { MdOutlineArrowForwardIos } from 'react-icons/md'; 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'); - } - }; - + // calls loseToken() and logs the user out 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, @@ -108,89 +35,100 @@ const UserSettings = () => { return ( <> - -

Account

+ +
+

Account

- - Followers + + +

Followers

+ +
- - Following + + +

Following

+ +
setInfoBody(true)}> - Your Info - -
- - -

History

- - - Saved +
+

Your Info

+ +
- - Liked - - - Commented Posts + setLocation('/settings/manageAccount')}> +
+

Manage Account

+ +
-
- -

General

+
+
+

History

- - About + + +

Saved

+ +
- - FAQs + + +

Liked

+ +
- - Support + + +

Commented Posts

+ +
- - -
- -
-
- -

Danger Zone

+
+
+

General

- - Change Username + + +

About

+ +
- setShowPass1(true)}> - Change Password + + +

FAQs

+ +
- - Change Email - - setShowDelete1(true)}> - DELETE ACCOUNT + + +

Support

+ +
- +
+
+ +
} /> - - ); diff --git a/server/index.ts b/server/index.ts index 4d470710..1390faad 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,18 +1,41 @@ -import { isDev } from './src/load-env'; -import './src/load-env'; +import './src/environment'; import express from 'express'; import createRouter from 'express-file-routing'; import path from 'path'; import mongoose from 'mongoose'; import cors from 'cors'; +import http from 'http'; +import { Server } from 'socket.io'; import { requestLogger } from './src/middlewares/log.js'; +import { getServerHost, SERVER_PORT } from './src/environment'; const PROJECT_ROOT = path.join(__dirname, 'src'); -const PORT = process.env.PORT; (async () => { const app = express(); + const server = http.createServer(app); + const io = new Server(server, { + cors: { origin: '*' } /*cors: { origin: "http://localhost:8000", methods: ['GET', 'POST']}*/, + }); + + io.on('connection', (socket) => { + // console.log('New client connected'); + + socket.on('disconnect', () => { + // console.log('Client disconnected'); + }); + + socket.on('sendID', (convoID: string) => { + socket.join(convoID); + }); + + socket.on('sendMessage', (convoID: string) => { + socket.to(convoID).emit('displayMessage'); + }); + }); + + app.set('socketio', io); app.use(cors()); @@ -25,7 +48,7 @@ const PORT = process.env.PORT; await createRouter(app, { directory: path.join(PROJECT_ROOT, 'routes') }); - app.listen(PORT, () => { - console.log(`Server is running with port "${PORT}". (http://localhost:${PORT})`); + server.listen(SERVER_PORT, () => { + console.log(`Server is now running. (${getServerHost()})`); }); })(); diff --git a/server/package-lock.json b/server/package-lock.json index c01ca6f0..3a759d5c 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9,14 +9,19 @@ "bcrypt": "^5.1.1", "cors": "^2.8.5", "dotenv": "^16.4.5", + "esno": "^4.7.0", "express": "^4.19.2", "express-file-routing": "^3.0.3", + "google-auth-library": "^9.10.0", + "googleapis": "^137.1.0", "http-status-codes": "^2.3.0", "joi": "^17.13.1", "jsonwebtoken": "^9.0.2", "mongoose": "^8.3.3", + "morgan": "^1.10.0", "nodemailer": "^6.9.13", - "picocolors": "^1.0.1" + "picocolors": "^1.0.1", + "socket.io": "^4.7.5" }, "devDependencies": { "@types/bcrypt": "^5.0.2", @@ -26,8 +31,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" } }, @@ -38,7 +41,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "aix" @@ -54,7 +56,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "android" @@ -70,7 +71,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "android" @@ -86,7 +86,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "android" @@ -102,7 +101,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -118,7 +116,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "darwin" @@ -134,7 +131,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -150,7 +146,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "freebsd" @@ -166,7 +161,6 @@ "cpu": [ "arm" ], - "dev": true, "optional": true, "os": [ "linux" @@ -182,7 +176,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -198,7 +191,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "linux" @@ -214,7 +206,6 @@ "cpu": [ "loong64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -230,7 +221,6 @@ "cpu": [ "mips64el" ], - "dev": true, "optional": true, "os": [ "linux" @@ -246,7 +236,6 @@ "cpu": [ "ppc64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -262,7 +251,6 @@ "cpu": [ "riscv64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -278,7 +266,6 @@ "cpu": [ "s390x" ], - "dev": true, "optional": true, "os": [ "linux" @@ -294,7 +281,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "linux" @@ -310,7 +296,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "netbsd" @@ -326,7 +311,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "openbsd" @@ -342,7 +326,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "sunos" @@ -358,7 +341,6 @@ "cpu": [ "arm64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -374,7 +356,6 @@ "cpu": [ "ia32" ], - "dev": true, "optional": true, "os": [ "win32" @@ -390,7 +371,6 @@ "cpu": [ "x64" ], - "dev": true, "optional": true, "os": [ "win32" @@ -457,6 +437,11 @@ "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, "node_modules/@types/bcrypt": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@types/bcrypt/-/bcrypt-5.0.2.tgz", @@ -485,11 +470,15 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, "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": "*" } @@ -552,7 +541,6 @@ "version": "20.12.8", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.8.tgz", "integrity": "sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -678,6 +666,7 @@ "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==", + "deprecated": "This package is no longer supported.", "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" @@ -696,11 +685,37 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "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" }, @@ -711,8 +726,7 @@ "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 + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/bcrypt": { "version": "5.1.1", @@ -727,6 +741,14 @@ "node": ">= 10.0.0" } }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "engines": { + "node": "*" + } + }, "node_modules/body-parser": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", @@ -959,6 +981,63 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", + "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/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/engine.io/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/es-define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", @@ -982,7 +1061,6 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -1025,7 +1103,6 @@ "version": "4.7.0", "resolved": "https://registry.npmjs.org/esno/-/esno-4.7.0.tgz", "integrity": "sha512-81owrjxIxOwqcABt20U09Wn8lpBo9K6ttqbGvQcB3VYNLJyaV1fvKkDtpZd3Rj5BX3WXiGiJCjUevKQGNICzJg==", - "dev": true, "dependencies": { "tsx": "^4.7.1" }, @@ -1090,6 +1167,11 @@ "express": ">4.1.2" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/finalhandler": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", @@ -1154,7 +1236,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -1176,6 +1257,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.2", @@ -1191,6 +1273,95 @@ "node": ">=10" } }, + "node_modules/gaxios": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.6.0.tgz", + "integrity": "sha512-bpOZVQV5gthH/jVCSuYuokRo2bTKOcuBiVWpjmTn6C5Agl5zclGfTljuGsQZxwwDBkli+YhZhP4TdlqTnhOezQ==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/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/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/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/gcp-metadata": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", + "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", + "optional": true, + "peer": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata/node_modules/gaxios": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "optional": true, + "peer": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -1210,10 +1381,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.3.tgz", - "integrity": "sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==", - "dev": true, + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.5.tgz", + "integrity": "sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==", "dependencies": { "resolve-pkg-maps": "^1.0.0" }, @@ -1240,6 +1410,81 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/google-auth-library": { + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.10.0.tgz", + "integrity": "sha512-ol+oSa5NbcGdDqA+gZ3G3mev59OHBZksBTxY/tYwjtcp1H/scAFwJfSQU9/1RALoyZ7FslNbke8j4i3ipwlyuQ==", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-auth-library/node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-auth-library/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-auth-library/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/googleapis": { + "version": "137.1.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-137.1.0.tgz", + "integrity": "sha512-2L7SzN0FLHyQtFmyIxrcXhgust77067pkkduqkbIpDuj9JzVnByxsRrcRfUMFQam3rQkWW2B0f1i40IwKDWIVQ==", + "dependencies": { + "google-auth-library": "^9.0.0", + "googleapis-common": "^7.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/googleapis-common": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-7.2.0.tgz", + "integrity": "sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==", + "dependencies": { + "extend": "^3.0.2", + "gaxios": "^6.0.3", + "google-auth-library": "^9.7.0", + "qs": "^6.7.0", + "url-template": "^2.0.8", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -1251,6 +1496,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/gtoken/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/gtoken/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -1368,6 +1644,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -1394,6 +1671,17 @@ "node": ">=8" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/joi": { "version": "17.13.1", "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.1.tgz", @@ -1406,6 +1694,14 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -1709,7 +2005,6 @@ "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", @@ -1725,7 +2020,6 @@ "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" }, @@ -1855,6 +2149,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", "dependencies": { "are-we-there-yet": "^2.0.0", "console-control-strings": "^1.1.0", @@ -1893,7 +2188,6 @@ "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" } @@ -2005,7 +2299,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", - "dev": true, "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } @@ -2154,6 +2447,107 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" }, + "node_modules/socket.io": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", + "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.11.0" + } + }, + "node_modules/socket.io-adapter/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/socket.io-adapter/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/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/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/socket.io-parser/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/socket.io/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/socket.io/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/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -2238,13 +2632,12 @@ } }, "node_modules/tsx": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.8.2.tgz", - "integrity": "sha512-hmmzS4U4mdy1Cnzpl/NQiPUC2k34EcNSTZYVJThYKhdqTwuBeF+4cG9KUK/PFQ7KHaAaYwqlb7QfmsE2nuj+WA==", - "dev": true, + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.11.0.tgz", + "integrity": "sha512-vzGGELOgAupsNVssAmZjbUDfdm/pWP4R+Kg8TVdsonxbXk0bEpE1qh0yV6/QxUVXaVlNemgcPajGdJJ82n3stg==", "dependencies": { "esbuild": "~0.20.2", - "get-tsconfig": "^4.7.3" + "get-tsconfig": "^4.7.5" }, "bin": { "tsx": "dist/cli.mjs" @@ -2284,8 +2677,7 @@ "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 + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/unpipe": { "version": "1.0.0", @@ -2295,6 +2687,11 @@ "node": ">= 0.8" } }, + "node_modules/url-template": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", + "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2308,6 +2705,18 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -2349,6 +2758,26 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", diff --git a/server/package.json b/server/package.json index 21188121..e350f3c1 100644 --- a/server/package.json +++ b/server/package.json @@ -10,14 +10,19 @@ "bcrypt": "^5.1.1", "cors": "^2.8.5", "dotenv": "^16.4.5", + "esno": "^4.7.0", "express": "^4.19.2", "express-file-routing": "^3.0.3", + "google-auth-library": "^9.10.0", + "googleapis": "^137.1.0", "http-status-codes": "^2.3.0", "joi": "^17.13.1", "jsonwebtoken": "^9.0.2", "mongoose": "^8.3.3", + "morgan": "^1.10.0", "nodemailer": "^6.9.13", - "picocolors": "^1.0.1" + "picocolors": "^1.0.1", + "socket.io": "^4.7.5" }, "devDependencies": { "@types/bcrypt": "^5.0.2", @@ -27,8 +32,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/environment.ts b/server/src/environment.ts new file mode 100644 index 00000000..a1936d99 --- /dev/null +++ b/server/src/environment.ts @@ -0,0 +1,82 @@ +import { config } from 'dotenv'; + +config(); + +const LOCAL_DEV = 'development_local'; +const LOCAL_PROD = 'production_local'; +const DEV = 'development'; +const PROD = 'production'; + +/** + * The environment that the server is currently running in. + */ +export const NODE_ENV = process.env.NODE_ENV; +if (NODE_ENV === undefined) throw 'You must specify a NODE_ENV in the server environment variables.'; + +/** + * The port that the server is currently running on. + */ +export const SERVER_PORT = process.env.PORT; +if (!SERVER_PORT) throw 'You must specify a PORT in the server environment variables.'; + +/** + * Returns whether the server is running in development mode. + */ +export const isDev = () => NODE_ENV === LOCAL_DEV || NODE_ENV === DEV; + +/** + * Returns whether the server is running in production mode. + */ +export const isProd = () => NODE_ENV === LOCAL_PROD || NODE_ENV === PROD; + +/** + * Returns whether the server is running in local mode. + */ +export const isLocal = () => NODE_ENV === LOCAL_DEV || NODE_ENV === LOCAL_PROD; + +/** + * Returns the hostname of the client based on + * the NODE_ENV specified in the environment variables. + */ +export const getClientHost = () => { + let localClientPort = process.env.CLIENT_PORT; + if (isLocal() && !localClientPort) { + console.warn( + 'There is no client port specified, but you are running locally, please set CLIENT_PORT to the port your frontend is running on. (Using 8000 by default)', + ); + + localClientPort = '8000'; + } + + switch (NODE_ENV) { + case LOCAL_DEV: + return `http://127.0.0.1:${localClientPort}`; + case LOCAL_PROD: + return `http://127.0.0.1:${localClientPort}`; + case DEV: + return 'https://dev.skynetwork.app'; + case PROD: + return 'https://skynetwork.app'; + default: + throw 'Invalid NODE_ENV found in your environment variables!'; + } +}; + +/** + * Returns the hostname of the server based on + * the NODE_ENV specified in the environment variables. + */ +export const getServerHost = () => { + switch (NODE_ENV) { + case LOCAL_DEV: + return `http://127.0.0.1:${SERVER_PORT}`; + case LOCAL_PROD: + return `http://127.0.0.1:${SERVER_PORT}`; + case DEV: + return 'https://api.dev.skynetwork.app'; + case PROD: + return 'https://api.skynetwork.app'; + default: + throw 'Invalid NODE_ENV found in your environment variables!'; + } +}; diff --git a/server/src/lib/auth/adapters/basic-jwt.ts b/server/src/lib/auth/adapters/basic-jwt.ts new file mode 100644 index 00000000..83bc80d2 --- /dev/null +++ b/server/src/lib/auth/adapters/basic-jwt.ts @@ -0,0 +1,31 @@ +import jwt from 'jsonwebtoken'; +import mongoose from 'mongoose'; +import { UserModel } from '../../../models/user'; +import { Request } from 'express'; +import { JWT, JWTPayload } from '../../../utils/jwt'; +import { AuthAdapter } from '../auth-adapter'; + +/** + * Allows for creation and verification of JWT + * authentication tokens. + */ +export class BasicJWTAdapter implements AuthAdapter { + public parseToken(req: Request) { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer')) return; + + return authHeader.split(' ')[1]; + } + + public async verifyToUser(token: string) { + try { + const payload = jwt.verify(token, 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 {} + } +} diff --git a/server/src/lib/auth/adapters/google-oauth.ts b/server/src/lib/auth/adapters/google-oauth.ts new file mode 100644 index 00000000..99f713e8 --- /dev/null +++ b/server/src/lib/auth/adapters/google-oauth.ts @@ -0,0 +1,112 @@ +import { google } from 'googleapis'; +import { OAuth2Client } from 'google-auth-library'; +import { UserModel } from '../../../models/user'; +import { Request } from 'express'; +import { AuthAdapter } from '../auth-adapter'; +import { PlanetModel } from '../../../models/planet'; +import { getClientHost, isDev } from '../../../environment'; + +export class GoogleOAuthAdapter implements AuthAdapter { + /** + * The client ID used to access the OAuth client API. + */ + private static CLIENT_ID: string; + + /** + * The OAuth Secret used to access the OAuth Client API. + */ + private static SECRET: string; + + /** + * The redirect URL that the OAuth consent screen will redirect to. + */ + private static REDIRECT_URL: string; + + /** + * Generates a new Google OAuth client and google OAuth API client with the static variables. + */ + public static generateClients = () => { + const authClient = new OAuth2Client({ + clientId: this.CLIENT_ID, + clientSecret: this.SECRET, + redirectUri: this.REDIRECT_URL, + }); + + const googleClient = google.oauth2({ auth: authClient, version: 'v2' }); + + return { authClient, googleClient }; + }; + + /** + * Assigns static variables and provides helpful + * errors if any of the setup went wrong. + */ + static { + const id = process.env.GOOGLE_OAUTH_ID; + if (typeof id !== 'string') throw 'GOOGLE_OAUTH_ID is not present in environment variables.'; + + this.CLIENT_ID = id; + + const secret = process.env.GOOGLE_OAUTH_SECRET; + if (typeof secret !== 'string') throw 'GOOGLE_OAUTH_SECRET is not present in environment variables.'; + + this.SECRET = secret; + this.REDIRECT_URL = `${getClientHost()}/login`; + } + + public parseToken(req: Request) { + const authCode = req.query.code; + if (typeof authCode === 'string') return authCode; + } + + public async verifyToUser(token: string) { + const googleUser = await this.verifyToGoogleUser(token); + if (!googleUser) return; + + const googleId = googleUser.id; + if (!googleId) return; + + const foundUser = await UserModel.findOne({ 'sso.id': googleId }); + if (foundUser) return foundUser; + + const userName = googleUser.name; + if (!userName) return; + + const newUser = new UserModel({ + userName: userName, + avatarUrl: googleUser.picture, + sso: { provider: 'google', id: googleId }, + location: { + planetId: await PlanetModel.findOne({ name: 'Earth' }), + latitude: 0, + longitude: 0, + }, + }); + + try { + await newUser.save(); + return newUser; + } catch (err) { + console.log(err); + } + } + + /** + * Given the specified Google OAuth authentication code, + * returns a Google user. + */ + private async verifyToGoogleUser(authCode: string) { + try { + const { authClient, googleClient } = GoogleOAuthAdapter.generateClients(); + const tokenResponse = await authClient.getToken(authCode); + + const idToken = tokenResponse.tokens.id_token; + if (!idToken) return; + + authClient.setCredentials(tokenResponse.tokens); + const googleUser = await googleClient.userinfo.get(); + + return googleUser.data; + } catch {} + } +} diff --git a/server/src/lib/auth/auth-adapter.ts b/server/src/lib/auth/auth-adapter.ts new file mode 100644 index 00000000..1d14ed53 --- /dev/null +++ b/server/src/lib/auth/auth-adapter.ts @@ -0,0 +1,23 @@ +import { Request } from 'express'; +import { HydratedDocument } from 'mongoose'; +import { IUser } from '../../models/user'; + +/** + * An interface defining a user authorization method. + * + * An adapter should be stateless, meaning they can + * be reused with any number of requests. + */ +export interface AuthAdapter { + /** + * Parses an authorization token from a given + * Express request object, if one can be found. + */ + parseToken(req: Request): string | undefined; + + /** + * Verifies an authorization token into a hydrated database user, + * if one exists. + */ + verifyToUser(token: string): Promise | undefined | null>; +} diff --git a/server/src/lib/auth/auth-worker.ts b/server/src/lib/auth/auth-worker.ts new file mode 100644 index 00000000..ccdfb339 --- /dev/null +++ b/server/src/lib/auth/auth-worker.ts @@ -0,0 +1,52 @@ +import { Request } from 'express'; +import { BasicJWTAdapter } from './adapters/basic-jwt'; +import { GoogleOAuthAdapter } from './adapters/google-oauth'; +import { AuthAdapter } from './auth-adapter'; + +const registeredAdapters = [new GoogleOAuthAdapter(), new BasicJWTAdapter()]; + +/** + * Request authentication worker to authenticate + * request depending on the authentication method. + */ +export class AuthWorker { + private token?: string; + + /** + * Creates a new worker with the given request and adapter. + */ + private constructor(private req: Request, private adapter: AuthAdapter, tokenOverride?: string) { + this.token = tokenOverride ?? this.adapter.parseToken(this.req); + } + + /** + * Authenticates the request this worker was created from. If + * properly authenticated, adds the authenticated user to the request. + * + * @returns whether the request was authenticated + */ + public async authenticate() { + if (!this.token) return false; + + const user = await this.adapter.verifyToUser(this.token); + if (!user) return false; + + this.req.user = user; + return true; + } + + /** + * Automatically selects an adapter based on the given + * request. + */ + public static fromRequest(req: Request) { + let foundToken; + + for (const adapter of registeredAdapters) { + foundToken = adapter.parseToken(req); + if (foundToken) return new AuthWorker(req, adapter, foundToken); + } + + return new AuthWorker(req, registeredAdapters[0]); + } +} diff --git a/server/src/load-env.ts b/server/src/load-env.ts deleted file mode 100644 index ebd3de70..00000000 --- a/server/src/load-env.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { config } from 'dotenv'; - -config(); - -export const isDev = () => process.env.NODE_ENV === 'development'; diff --git a/server/src/middlewares/auth-protected.ts b/server/src/middlewares/auth-protected.ts new file mode 100644 index 00000000..bf66a642 --- /dev/null +++ b/server/src/middlewares/auth-protected.ts @@ -0,0 +1,16 @@ +import { NextFunction, Request, Response } from 'express'; +import { Resolve } from '../utils/express'; +import { AuthWorker } from '../lib/auth/auth-worker'; + +/** + * 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 authProtected(req: Request, res: Response, next: NextFunction) { + const worker = AuthWorker.fromRequest(req); + + const didAuthenticate = await worker.authenticate(); + if (!didAuthenticate) return Resolve(res).unauthorized('Authentication is required.'); + + next(); +} diff --git a/server/src/middlewares/require-login.ts b/server/src/middlewares/require-login.ts deleted file mode 100644 index 8c15b823..00000000 --- a/server/src/middlewares/require-login.ts +++ /dev/null @@ -1,21 +0,0 @@ -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/conversation.ts b/server/src/models/conversation.ts new file mode 100644 index 00000000..8d9f4a02 --- /dev/null +++ b/server/src/models/conversation.ts @@ -0,0 +1,22 @@ +import { model, Schema, Types } from 'mongoose'; +import { IMedia } from './media'; +import { ILocation } from './location'; + +export interface IConversation { + 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 }, + }, + { timestamps: { createdAt: true, updatedAt: false } }, +); + +export const ConversationModel = model('Conversation', schema); \ No newline at end of file diff --git a/server/src/models/deletedUser.ts b/server/src/models/deletedUser.ts new file mode 100644 index 00000000..a4567dd7 --- /dev/null +++ b/server/src/models/deletedUser.ts @@ -0,0 +1,44 @@ +import { model, ObjectId, Schema, Types } from 'mongoose'; +import { ILocation, LocationSchema } from './location'; +import { AuthProvider, SSOSchema } from './user'; + +interface IDeletedUser { + originID: ObjectId; + email?: string; + password?: string; + sso?: { provider: AuthProvider; id: 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( + { + originID: { type: Object, required: true }, + email: { type: 'string' }, + sso: { type: SSOSchema }, + password: { type: 'string' }, + 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 }, + // createdAt: {type: Date, default: new Date(), expires: 2592000} + createdAt: {type: Date, default: new Date()} + } +); + +export const DeletedUserModel = model('DeletedUser', schema); diff --git a/server/src/models/message.ts b/server/src/models/message.ts index e1f282dc..7c981b78 100644 --- a/server/src/models/message.ts +++ b/server/src/models/message.ts @@ -1,10 +1,10 @@ -import { Schema, Types } from 'mongoose'; +import { model, Schema, Types } from 'mongoose'; import { IMedia, MediaSchema } from './media'; import { ILocation, LocationSchema } from './location'; export interface IMessage { + conversationId: Types.ObjectId, senderId: Types.ObjectId; - receiverId: Types.ObjectId; createdAt: Date; media: Array; content: string; @@ -13,10 +13,10 @@ export interface IMessage { const schema = new Schema( { + conversationId: { type: 'ObjectId', ref: 'Conversation', required: true }, senderId: { type: 'ObjectId', ref: 'User', required: true }, - receiverId: { type: 'ObjectId', ref: 'User', required: true }, content: { type: 'string', required: true }, - location: { type: LocationSchema, required: true }, + // location: { type: LocationSchema, required: true }, media: { required: true, default: [], @@ -25,3 +25,5 @@ const schema = new Schema( }, { timestamps: { createdAt: true, updatedAt: false } }, ); + +export const MessageModel = model('Message', schema); \ No newline at end of file diff --git a/server/src/models/planet.ts b/server/src/models/planet.ts index a51afe6d..08f67c90 100644 --- a/server/src/models/planet.ts +++ b/server/src/models/planet.ts @@ -1,11 +1,28 @@ import { model, Schema } from 'mongoose'; +interface IPlanetVisual { + radius: number; + orbitRadius: number; + orbitDuration?: number; +} + export interface IPlanet { name: string; + visual: IPlanetVisual; } +const visualSchema = new Schema( + { + radius: { type: 'number', required: true }, + orbitRadius: { type: 'number', required: true }, + orbitDuration: { type: 'number' }, + }, + { _id: false }, +); + const schema = new Schema({ name: { type: 'string', required: true }, + visual: { type: visualSchema, required: true }, }); export const PlanetModel = model('Planet', schema); diff --git a/server/src/models/user.ts b/server/src/models/user.ts index d7a83257..119cc6c6 100644 --- a/server/src/models/user.ts +++ b/server/src/models/user.ts @@ -1,9 +1,19 @@ import { model, Schema, Types } from 'mongoose'; import { ILocation, LocationSchema } from './location'; +export enum AuthProvider { + GOOGLE = 'google', +} + +interface ISSO { + provider: AuthProvider; + id: string; +} + export interface IUser { - email: string; - password: string; + email?: string; + password?: string; + sso?: { provider: AuthProvider; id: string }; userName: string; bio?: string; location: ILocation; @@ -17,10 +27,16 @@ export interface IUser { createdAt: Date; } +export const SSOSchema = new Schema({ + provider: { type: 'string', required: true }, + id: { type: 'string', required: true }, +}); + const schema = new Schema( { - email: { type: 'string', required: true }, - password: { type: 'string', required: true }, + email: { type: 'string' }, + sso: { type: SSOSchema }, + password: { type: 'string' }, userName: { type: 'string', required: true }, bio: { type: 'string' }, location: { type: LocationSchema, required: true }, diff --git a/server/src/routes/feed/[planetOrUserId].ts b/server/src/routes/feed/[planetOrUserId].ts new file mode 100644 index 00000000..fda6657e --- /dev/null +++ b/server/src/routes/feed/[planetOrUserId].ts @@ -0,0 +1,28 @@ +import { Handler } from 'express'; +import { PostModel } from '../../models/post'; +import { Resolve } from '../../utils/express'; +import { PlanetModel } from '../../models/planet'; +import mongoose from 'mongoose'; +import { UserModel } from '../../models/user'; + +export const get: Handler = async (req, res) => { + const planetOrUserId = req.params.planetOrUserId; + if (!mongoose.isValidObjectId(planetOrUserId)) return Resolve(res).badRequest('Invalid planet ID specified.'); + + let search; + const existingPlanet = await PlanetModel.exists({ _id: planetOrUserId }).lean(); + if (existingPlanet) search = { 'location.planetId': existingPlanet._id }; + else { + const existingUser = await UserModel.exists({ _id: planetOrUserId }).lean(); + if (existingUser) search = { authorId: existingUser._id }; + else return Resolve(res).notFound('No planet or user by the given ID exists.'); + } + + const rawPage = parseInt(typeof req.query.page === 'string' ? req.query.page : ''); + const page = Math.max(1, typeof rawPage !== 'number' || isNaN(rawPage) ? 1 : rawPage); + const limit = 20; + const skip = (page - 1) * limit; + + const latestPosts = await PostModel.find(search).sort({ createdAt: -1 }).skip(skip).limit(limit).lean(); + Resolve(res).okWith(latestPosts); +}; diff --git a/server/src/routes/feed/index.ts b/server/src/routes/feed/index.ts new file mode 100644 index 00000000..6aa5ff6a --- /dev/null +++ b/server/src/routes/feed/index.ts @@ -0,0 +1,13 @@ +import { Handler } from 'express'; +import { PostModel } from '../../models/post'; +import { Resolve } from '../../utils/express'; + +export const get: Handler = async (req, res) => { + const rawPage = parseInt(typeof req.query.page === 'string' ? req.query.page : ''); + const page = Math.max(1, typeof rawPage !== 'number' || isNaN(rawPage) ? 1 : rawPage); + const limit = 20; + const skip = (page - 1) * limit; + + const latestPosts = await PostModel.find().sort({ likes: -1, createdAt: -1 }).skip(skip).limit(limit).lean(); + Resolve(res).okWith(latestPosts); +}; diff --git a/server/src/routes/post/[id]/comment.ts b/server/src/routes/post/[id]/comment.ts index 2e478cff..e28724ec 100644 --- a/server/src/routes/post/[id]/comment.ts +++ b/server/src/routes/post/[id]/comment.ts @@ -1,5 +1,5 @@ import { Handler } from 'express'; -import { requireLogin } from '../../../middlewares/require-login'; +import { authProtected } from '../../../middlewares/auth-protected'; import mongoose from 'mongoose'; import { assertRequestBody, Resolve } from '../../../utils/express'; import { PostModel } from '../../../models/post'; @@ -8,6 +8,7 @@ import { ILocation, RawLocationSchema } from '../../../models/location'; import { IMedia, RawMediaSchema } from '../../../models/media'; import Joi from 'joi'; import { CommentRelationship } from '../../../models/comment-relationship'; +import { UserModel } from '../../../models/user'; interface PostBody { content: string; @@ -23,12 +24,36 @@ export const get: Handler = async (req, res) => { if (isNaN(limit)) limit = 10; limit = Math.max(0, Math.min(limit, 100)); + + const rawPage = req.query.page; + let page = typeof rawPage === 'string' ? parseInt(rawPage) : NaN; + if (isNaN(page)) page = 1; + page = Math.max(1, page); + if (!mongoose.isValidObjectId(parentPostId)) return Resolve(res).badRequest('Invalid post ID provided.'); - const relationships = await CommentRelationship.find({ parentPost: parentPostId }).limit(limit); + const relationships = await CommentRelationship.find({ parentPost: parentPostId }) + .sort({ createdAt: -1 }) + .skip((page - 1) * limit) + .limit(limit); + const comments = await Promise.all( relationships.map(async (v) => { - return await PostModel.findById(v.childPost).lean(); + const comment = await PostModel.findById(v.childPost).lean(); + if (comment) { + const user = await UserModel.findById(comment.authorId).lean(); + const post = await PostModel.findById(comment._id).lean(); + if (user && post) { + return { + ...comment, + userName: user.userName, + repost: post.repostCount, + like: post.likeCount, + comment: post.commentCount, + }; + } + } + return comment; }), ); @@ -36,7 +61,7 @@ export const get: Handler = async (req, res) => { }; export const post: Handler[] = [ - requireLogin, + authProtected, async (req, res) => { const parentPostId = req.params.id; if (!mongoose.isValidObjectId(parentPostId)) return Resolve(res).badRequest('Invalid post ID provided.'); @@ -87,9 +112,23 @@ export const post: Handler[] = [ return relationship; }); - Resolve(res).created(commentRelationship, 'Comment created successfully.'); - } catch { - Resolve(res).error('Error occured while trying to create this comment.'); + // Fetch the newly created comment with user information + const newComment = await PostModel.findById(commentRelationship.childPost).lean(); + if (!newComment) throw new Error('Failed to fetch the newly created comment.'); + const user = await UserModel.findById(newComment.authorId).lean(); + if (!user) throw new Error('Failed to fetch the user information of the comment author.'); + const responseComment = { + ...newComment, + userName: user.userName, + repost: newComment.repostCount, + like: newComment.likeCount, + comment: newComment.commentCount, + }; + + Resolve(res).created(responseComment, 'Comment created successfully.'); + } catch (error) { + console.error('Error creating comment:', error); + Resolve(res).error('Error occurred 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 index eded3550..e55724da 100644 --- a/server/src/routes/post/[id]/index.ts +++ b/server/src/routes/post/[id]/index.ts @@ -1,10 +1,11 @@ import { Handler } from 'express'; import mongoose from 'mongoose'; -import { requireLogin } from '../../../middlewares/require-login'; +import { authProtected } from '../../../middlewares/auth-protected'; import { PostModel } from '../../../models/post'; import { Resolve } from '../../../utils/express'; import { CommentRelationship } from '../../../models/comment-relationship'; import { LikeInteraction } from '../../../models/like-interaction'; +import { UserModel } from '../../../models/user'; export const get: Handler = async (req, res) => { const id = req.params.id; @@ -14,11 +15,20 @@ export const get: Handler = async (req, res) => { 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); + + const user = await UserModel.findById(post.authorId).select('userName'); + if (!user) return Resolve(res).notFound('User not found.'); + + const postWithUser = { + ...post.toObject(), + userName: user.userName + }; + + return Resolve(res).okWith(postWithUser); }; export const del: Handler[] = [ - requireLogin, + authProtected, async (req, res) => { const id = req.params.id; if (!mongoose.isValidObjectId(id)) return Resolve(res).badRequest('Invalid post ID provided.'); diff --git a/server/src/routes/post/[id]/like.ts b/server/src/routes/post/[id]/like.ts index 56299ab0..f76c9cd4 100644 --- a/server/src/routes/post/[id]/like.ts +++ b/server/src/routes/post/[id]/like.ts @@ -1,25 +1,24 @@ import { Handler } from 'express'; -import { requireLogin } from '../../../middlewares/require-login'; +import { authProtected } from '../../../middlewares/auth-protected'; 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, + authProtected, 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.'); + const existingInteraction = await LikeInteraction.exists({ postId: id, userId: currentUserId }); + Resolve(res).okWith(!!existingInteraction); }, ]; export const post: Handler[] = [ - requireLogin, + authProtected, async (req, res) => { const id = req.params.id; if (!mongoose.isValidObjectId(id)) return Resolve(res).badRequest('Invalid post ID provided.'); @@ -60,7 +59,7 @@ export const post: Handler[] = [ ]; export const del: Handler[] = [ - requireLogin, + authProtected, async (req, res) => { const id = req.params.id; if (!mongoose.isValidObjectId(id)) return Resolve(res).badRequest('Invalid post ID provided.'); @@ -86,7 +85,7 @@ export const del: Handler[] = [ Resolve(res).okWith(interaction); } catch { - Resolve(res).error('Error occured while trying to like this post.'); + Resolve(res).error('Error occured while trying to unlike this post.'); } finally { await session.endSession(); } diff --git a/server/src/routes/post/[id]/save.ts b/server/src/routes/post/[id]/save.ts index 76740f8b..fb6a17f6 100644 --- a/server/src/routes/post/[id]/save.ts +++ b/server/src/routes/post/[id]/save.ts @@ -1,11 +1,11 @@ import { Handler } from 'express'; -import { requireLogin } from '../../../middlewares/require-login'; +import { authProtected } from '../../../middlewares/auth-protected'; import mongoose from 'mongoose'; import { Resolve } from '../../../utils/express'; import { PostModel } from '../../../models/post'; export const get: Handler[] = [ - requireLogin, + authProtected, async (req, res) => { const id = req.params.id; if (!mongoose.isValidObjectId(id)) return Resolve(res).badRequest('Invalid post ID provided.'); @@ -16,7 +16,7 @@ export const get: Handler[] = [ ]; export const post: Handler[] = [ - requireLogin, + authProtected, async (req, res) => { const id = req.params.id; if (!mongoose.isValidObjectId(id)) return Resolve(res).badRequest('Invalid post ID provided.'); @@ -37,7 +37,7 @@ export const post: Handler[] = [ ]; export const del: Handler[] = [ - requireLogin, + authProtected, async (req, res) => { const id = req.params.id; if (!mongoose.isValidObjectId(id)) return Resolve(res).badRequest('Invalid post ID provided.'); diff --git a/server/src/routes/post/index.ts b/server/src/routes/post/index.ts index ad62c954..c29b1308 100644 --- a/server/src/routes/post/index.ts +++ b/server/src/routes/post/index.ts @@ -1,6 +1,6 @@ import Joi from 'joi'; import { Handler } from 'express'; -import { requireLogin } from '../../middlewares/require-login'; +import { authProtected } from '../../middlewares/auth-protected'; import { assertRequestBody, Resolve } from '../../utils/express'; import { IMedia, RawMediaSchema } from '../../models/media'; import { ILocation, RawLocationSchema } from '../../models/location'; @@ -15,7 +15,7 @@ interface PostBody { } export const post: Handler[] = [ - requireLogin, + authProtected, async (req, res) => { const body = assertRequestBody( req, diff --git a/server/src/routes/user/[id].ts b/server/src/routes/user/[id].ts index 16af1d09..8a3cdb99 100644 --- a/server/src/routes/user/[id].ts +++ b/server/src/routes/user/[id].ts @@ -2,14 +2,16 @@ import Joi from 'joi'; import mongoose from 'mongoose'; import { Handler } from 'express'; import { UserModel } from '../../models/user'; -import { requireLogin } from '../../middlewares/require-login'; +import { authProtected } from '../../middlewares/auth-protected'; 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'); + const user = await UserModel.findById(id) + .lean() + .select('userName bio location avatarUrl followerCount followingCount postCount createdAt'); if (!user) Resolve(res).notFound('No user found by the given ID.'); else Resolve(res).okWith(user); }; @@ -21,7 +23,7 @@ interface PatchBody { } export const patch: Handler[] = [ - requireLogin, + authProtected, async (req, res) => { const id = req.params.id; if (!mongoose.isValidObjectId(id)) return Resolve(res).badRequest('Invalid user ID provided'); diff --git a/server/src/routes/user/changeEmail.ts b/server/src/routes/user/changeEmail.ts index 70b416d8..06423434 100644 --- a/server/src/routes/user/changeEmail.ts +++ b/server/src/routes/user/changeEmail.ts @@ -1,29 +1,45 @@ import Joi from 'joi'; import { Handler } from 'express'; import { Resolve } from '../../utils/express'; -import { requireLogin } from '../../middlewares/require-login'; +import { authProtected } from '../../middlewares/auth-protected'; interface EmailBody { - newEmail: string, - confirmEmail: string + currEmail: string; + newEmail: string; + confirmEmail: string; } export const patch: Handler[] = [ - requireLogin, - async (req, res) => { - const user = req.user!; + authProtected, + async (req, res) => { + const user = req.user!; - const emailSchema = Joi.object({ - newEmail: Joi.string().trim().email().required(), - confirmEmail: Joi.string().trim().email().required() + const emailSchema = Joi.object({ + currEmail: Joi.string().trim().email().required().equal(user.email).messages({ + 'string.base': 'Email must be a string.', + 'string.email': 'The current email entered is not a valid email', + 'any.required': 'Entering the current email is required.', + 'any.only': 'The current email entered does not match.', + }), + newEmail: Joi.string().trim().email().required().messages({ + 'string.base': 'Email must be a string.', + 'string.email': 'The new email entered is not a valid email', + 'any.required': 'Entering the new email is required.', + }), + confirmEmail: Joi.string().trim().email().required().equal(Joi.ref('newEmail')).messages({ + 'string.base': 'Email must be a string.', + 'string.email': 'The confirmed email entered is not a valid email', + 'any.required': 'Confirming the email is required.', + 'any.only': 'The confirmed email does not match the new email.', + }) }); - const bodyValidationResult = emailSchema.validate(req.body); - if (bodyValidationResult.error) return res.status(400).json({ error: bodyValidationResult.error.message }); + const bodyValidationResult = emailSchema.validate(req.body); + if (bodyValidationResult.error) return Resolve(res).badRequest(bodyValidationResult.error.message); - const value = bodyValidationResult.value; + const value = bodyValidationResult.value; + const result = await user.updateOne({ email: value.newEmail }); - 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 index 8ad3a173..14dfdc9a 100644 --- a/server/src/routes/user/changeUsername.ts +++ b/server/src/routes/user/changeUsername.ts @@ -1,27 +1,33 @@ import Joi from 'joi'; import { Handler } from 'express'; import { Resolve } from '../../utils/express'; -import { requireLogin } from '../../middlewares/require-login'; +import { authProtected } from '../../middlewares/auth-protected'; interface UsernameBody { - newUsername: string + newUsername: string; } export const patch: Handler[] = [ - requireLogin, - async (req, res) => { - const user = req.user!; + authProtected, + async (req, res) => { + const user = req.user!; - const emailSchema = Joi.object({ - newUsername: Joi.string().trim().required() + const currUsername = user.userName; + + const emailSchema = Joi.object({ + newUsername: Joi.string().trim().required().invalid(currUsername).messages({ + 'string.base': 'New username must be a string.', + 'any.required': 'A username is required in order to change your username.', + 'any.invalid': 'Username entered is already the current username.', + }), }); - const bodyValidationResult = emailSchema.validate(req.body); - if (bodyValidationResult.error) return res.status(400).json({ error: bodyValidationResult.error.message }); + const validationResult = emailSchema.validate(req.body); + if (validationResult.error) return Resolve(res).badRequest(validationResult.error.message); - const value = bodyValidationResult.value; + const value = validationResult.value; - const result = await user.updateOne({ userName: value.newUsername }); + 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 index bde47f2e..89fa1302 100644 --- a/server/src/routes/user/changepassword.ts +++ b/server/src/routes/user/changepassword.ts @@ -3,7 +3,7 @@ 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'; +import { authProtected } from '../../middlewares/auth-protected'; interface PostBody { password: string; @@ -12,7 +12,7 @@ interface PostBody { } export const patch: Handler[] = [ - requireLogin, + authProtected, async (req, res) => { const user = req.user!; @@ -37,10 +37,13 @@ export const patch: Handler[] = [ const { value: body } = bodyValidationResult; - const passwordsMatch = await compareToHashed(body.password, user.password); + const password = user.password; + if (!password) return Resolve(res).forbidden('Password is incorrect.'); + + const passwordsMatch = await compareToHashed(body.password, 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/chat.ts b/server/src/routes/user/chat.ts new file mode 100644 index 00000000..2946ec1e --- /dev/null +++ b/server/src/routes/user/chat.ts @@ -0,0 +1,66 @@ +import Joi from 'joi'; +import { Handler } from 'express'; +import { assertRequestBody, Resolve } from '../../utils/express'; +import { authProtected } from '../../middlewares/auth-protected'; +import { MessageModel } from '../../models/message'; +import { ConversationModel } from '../../models/conversation'; + +interface PostBody { + receiverId: string; + content: string; +} + +export const post: Handler[] = [ + authProtected, + async (req, res) => { + const user = req.user!; + const io = req.app.get('socketio'); + + const postSchema = Joi.object({ + receiverId: Joi.string().trim().required(), + content: Joi.string().trim().required(), + }); + + const bodyValidationResult = postSchema.validate(req.body); + if (bodyValidationResult.error) return res.status(400).json({ error: bodyValidationResult.error.message }); + + const value = bodyValidationResult.value; + + const converationID = await ConversationModel.findOne({ + $or: [ + {senderId: user._id, receiverId: value.receiverId}, + {receiverId: user._id, senderId: value.receiverId}, + ] + }) + + if (converationID == null){ + const conversation = new ConversationModel({ + senderId: user._id, + receiverId: value.receiverId + }); + + const convo = await conversation.save(); + + const message = new MessageModel({ + conversationId: convo._id, + senderId: user._id, + content: value.content + }); + + const data = await message.save(); + io.emit('receiveMessage', data); + return Resolve(res).ok('Message saved successfully.'); + } + + const message = new MessageModel({ + conversationId: converationID._id, + senderId: user._id, + content: value.content + }); + + const data = await message.save(); + + io.emit('receiveMessage', data); + return Resolve(res).okWith(data, 'Message saved 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 index 22180986..800f8afe 100644 --- a/server/src/routes/user/deleteaccount/delete.ts +++ b/server/src/routes/user/deleteaccount/delete.ts @@ -1,22 +1,70 @@ import mongoose from 'mongoose'; import { Handler } from 'express'; -import { requireLogin } from '../../../middlewares/require-login'; +import { authProtected } from '../../../middlewares/auth-protected'; import { Resolve } from '../../../utils/express'; import { UserModel } from '../../../models/user'; +import Joi from 'joi'; +import { DeletedUserModel } from '../../../models/deletedUser'; + +interface deleteBody { + confirmationInput: string; +} + +const message = 'I-WANT-TO-DELETE-THIS-ACCOUNT'; export const post: Handler[] = [ - requireLogin, - async (req, res) => { + authProtected, + async (req, res) => { + const userID = req.user!.id; + const User = mongoose.model('User', UserModel.schema); + + const deleteSchema = Joi.object({ + confirmationInput: Joi.string().uppercase().trim().required().equal(message).messages({ + 'string.base': 'The phrase must not contain any numbers or special characters.', + 'string.lowercase': 'The phrase must be typed in uppcase', + 'any.required': 'The identical phrase is required to delete this account.', + 'any.only': 'The phrase entered does not match.', + }), + }); + + const validationResult = deleteSchema.validate(req.body, { convert: false }); + if (validationResult.error) return Resolve(res).badRequest(validationResult.error.message); + + const userRef = await User.findById(userID); + if (!userRef) return Resolve(res).notFound('User could not be found.'); + + const userCopy = userRef; - console.log(req.user); - - const userID = req.user?.id; - const User = mongoose.model('User', UserModel.schema); + await userRef.updateOne({ + email: null, + sso: null, + userName: 'deletedUser', + password: null, + location: null, + bio: null, + birthDate: null, + avatarUrl: null, + }); - const user = await UserModel.findById(userID); - if (!user) return res.status(404).json({ error: 'No user found by the given ID.' }); + const deletedUser = new DeletedUserModel({ + originID: userCopy._id, + email: userCopy.email, + sso: userCopy.sso, + password: userCopy.password, + userName: userCopy.userName, + bio: userCopy.bio, + location: userCopy.location, + birthDate: userCopy.birthDate, + avatarUrl: userCopy.avatarUrl, + followerCount: userCopy.followerCount, + followingCount: userCopy.followingCount, + postCount: userCopy.postCount, + savedPosts: userCopy.savedPosts, + admin: userCopy.admin, + createdAt: Date.now() + }); + await deletedUser.save(); - const result = await User.findByIdAndDelete(userID); - Resolve(res).okWith("Account Deleted!"); - } -]; \ No newline at end of file + Resolve(res).okWith(userCopy, 'Account deleted successfully.'); + }, +]; diff --git a/server/src/routes/user/forgetpassword.ts b/server/src/routes/user/forgetpassword.ts index cf0a1778..c3d2949e 100644 --- a/server/src/routes/user/forgetpassword.ts +++ b/server/src/routes/user/forgetpassword.ts @@ -4,7 +4,7 @@ import { UserModel } from '../../models/user'; import { TokenModel } from '../../models/token'; import { Resolve } from '../../utils/express'; import crypto from 'crypto'; -import {sendEmail} from '../../utils/email'; +import { sendEmail } from '../../utils/email'; interface PostBody { email: string; @@ -20,12 +20,15 @@ export const post: Handler = async (req, res) => { }); const bodyValidationResult = bodySchema.validate(req.body); - if (bodyValidationResult.error) return res.status(400).json({ error: bodyValidationResult.error.message }); - - const { value: body } = bodyValidationResult; + if (bodyValidationResult.error) return Resolve(res).badRequest(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.'); + if (!existingUser) return Resolve(res).badRequest('Sorry, no user with that email exists.'); + + const submittedEmail = existingUser.email; + if (!submittedEmail) return Resolve(res).badRequest('Sorry, no user with that email exists.'); const resetToken = crypto.randomBytes(32).toString('hex'); const passwordResetToken = crypto.createHash('sha256').update(resetToken).digest('hex'); @@ -33,7 +36,7 @@ export const post: Handler = async (req, res) => { const token = new TokenModel({ userId: existingUser._id, - email: body.email, + email: submittedEmail, passwordResetToken, passwordResetExpires: new Date(passwordResetExpires), }); @@ -47,15 +50,15 @@ export const post: Handler = async (req, res) => { try { await sendEmail({ - email: existingUser.email, + email: submittedEmail, subject: 'Password Reset Request', - text: message + text: message, }); - Resolve(res).created(post, 'Password rest link has been sent your email.'); + + return Resolve(res).badRequest('Password rest link has been sent your email.'); } catch (error) { - token.deleteOne(); + await token.deleteOne(); console.log(error); - return res.status(500).json({ error: 'There was an error sending the email. Try again later.' }); + return Resolve(res).error('There was an error sending the email. Try again later.'); } - -}; \ No newline at end of file +}; diff --git a/server/src/routes/user/getchats.ts b/server/src/routes/user/getchats.ts new file mode 100644 index 00000000..15f04e09 --- /dev/null +++ b/server/src/routes/user/getchats.ts @@ -0,0 +1,33 @@ +import Joi from 'joi'; +import { Handler } from 'express'; +import { authProtected } from '../../middlewares/auth-protected'; +import { MessageModel } from '../../models/message'; +import { ConversationModel } from '../../models/conversation'; + +interface PostBody { + receiverId: string; + content: string; +} + +export const post: Handler[] = [ + authProtected, + async (req, res) => { + const user = req.user!; + + const converationID = await ConversationModel.findOne({ + $or: [ + {senderId: user._id, receiverId: req.body.receiverId}, + {receiverId: user._id, senderId: req.body.receiverId}, + ] + }) + + if (converationID == null){ + res.json({success: false}) + return + } + + const message = await MessageModel.find({conversationId: converationID._id}); + + res.json({success: true, message}) + } +]; \ No newline at end of file diff --git a/server/src/routes/user/index.ts b/server/src/routes/user/index.ts index a94f330e..01764ccd 100644 --- a/server/src/routes/user/index.ts +++ b/server/src/routes/user/index.ts @@ -1,9 +1,9 @@ import { Handler } from 'express'; -import { requireLogin } from '../../middlewares/require-login'; +import { authProtected } from '../../middlewares/auth-protected'; import { Resolve } from '../../utils/express'; export const get: Handler[] = [ - requireLogin, + authProtected, (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 index c8db9eb9..231731b1 100644 --- a/server/src/routes/user/login.ts +++ b/server/src/routes/user/login.ts @@ -3,15 +3,24 @@ 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'; +import { authProtected } from '../../middlewares/auth-protected'; +import { JWT } from '../../utils/jwt'; interface PostBody { email: string; password: string; } -export const get: Handler[] = [requireLogin, async (_, res) => Resolve(res).ok()]; +export const get: Handler[] = [ + authProtected, + async (req, res) => { + if (!req.user) return Resolve(res).forbidden('Invalid credentials provided.'); + + const refreshToken = req.query.token === 'refresh'; + if (refreshToken) Resolve(res).okWith(JWT.signAs(req.user)); + else Resolve(res).ok(); + }, +]; export const post: Handler = async (req, res) => { const body = assertRequestBody( @@ -26,10 +35,13 @@ export const post: Handler = async (req, res) => { if (!body) return; const existingUser = await UserModel.findOne({ email: body.email }); - if (!existingUser) return Resolve(res).notFound('No user with that email exists.'); + if (!existingUser) return Resolve(res).forbidden('Invalid credentials provided.'); + + const userPassword = existingUser.password; + if (!userPassword) return Resolve(res).forbidden('Invalid credentials provided.'); - const passwordsMatch = await compareToHashed(body.password, existingUser.password); - if (!passwordsMatch) return Resolve(res).unauthorized('Password is incorrect.'); + const passwordsMatch = await compareToHashed(body.password, userPassword); + if (!passwordsMatch) return Resolve(res).forbidden('Invalid credentials provided.'); - Resolve(res).okWith(AuthToken.signAs(existingUser)); + Resolve(res).okWith(JWT.signAs(existingUser)); }; diff --git a/server/src/routes/user/oauth/google.ts b/server/src/routes/user/oauth/google.ts new file mode 100644 index 00000000..617c5e13 --- /dev/null +++ b/server/src/routes/user/oauth/google.ts @@ -0,0 +1,15 @@ +import { Handler } from 'express'; +import { Resolve } from '../../../utils/express'; +import { GoogleOAuthAdapter } from '../../../lib/auth/adapters/google-oauth'; + +export const post: Handler = async (_, res) => { + const { authClient } = GoogleOAuthAdapter.generateClients(); + + Resolve(res).okWith( + authClient.generateAuthUrl({ + access_type: 'offline', + scope: 'https://www.googleapis.com/auth/userinfo.profile openid', + prompt: 'consent', + }), + ); +}; diff --git a/server/src/routes/user/signup.ts b/server/src/routes/user/signup.ts index b1879f12..678a3df1 100644 --- a/server/src/routes/user/signup.ts +++ b/server/src/routes/user/signup.ts @@ -7,7 +7,7 @@ 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'; +import { JWT } from '../../utils/jwt'; interface PostBody { email: string; @@ -53,7 +53,7 @@ export const post: Handler = async (req, res) => { }); await user.save(); - Resolve(res).okWith(AuthToken.signAs(user)); + Resolve(res).okWith(JWT.signAs(user)); } finally { inflightEmails.delete(body.email); } diff --git a/server/src/utils/auth-token.ts b/server/src/utils/auth-token.ts deleted file mode 100644 index 88a0be75..00000000 --- a/server/src/utils/auth-token.ts +++ /dev/null @@ -1,47 +0,0 @@ -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/jwt.ts b/server/src/utils/jwt.ts new file mode 100644 index 00000000..c30e450a --- /dev/null +++ b/server/src/utils/jwt.ts @@ -0,0 +1,47 @@ +import jwt from 'jsonwebtoken'; +import { HydratedDocument } from 'mongoose'; +import { IUser } from '../models/user'; + +/** + * The interface describing the payload + * type of the JWT token. + */ +export interface JWTPayload { + userId: string; +} + +/** + * Utilities related to signing and verifying JWT payloads. + */ +export namespace JWT { + const secret = process.env.JWT_SECRET; + if (typeof secret !== 'string') throw 'JWT_SECREt is not present in environment variables!'; + + const ttlString = process.env.JWT_TTL; + if (typeof ttlString !== 'string') throw 'JWT_TTL is not present in environment variables.'; + + const ttl = parseInt(ttlString); + if (isNaN(ttl)) throw 'The JWT_TTL environment variable is not a number!'; + + /** + * The secret used to verify and sign JWT tokens. + */ + export const SECRET = secret; + + /** + * The time, in minutes, that JWT tokens expire in. + */ + export const TTL_MINS = ttl; + + /** + * 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, secret!, { + expiresIn: parseInt(ttlString!), + }); + } +}