From 9533c1a21f82ba38d0878f8f7c0adf084a129429 Mon Sep 17 00:00:00 2001 From: "Marcus V. S. Lages" <65512084+MarcusLages@users.noreply.github.com> Date: Fri, 24 May 2024 15:50:01 -0700 Subject: [PATCH] Merging dev into main - Sprint 4 (#3) * planet route added * Planets Homepage Init * displaying rotating sun with css animation * planets page css colors added * 1. Displaying all the planets in order 2. Positioned sun in the middle 3. Added planets orbits css 4. Animations added for all the planets * added google authentication dependencies * added auth worker for multi-type authentication * enabled selective properties for get-user-by-id * renamed authentication middleware * fixed errors with new auth implementation * added refresh to token on confirm login route * added google authenticated message to info * Added conversationId to schema * Created new converation schema to store common ID between two users * created POST /user/chat API to store messages between two users * API for retrieving all the messages between two users * added route for messages * new messages page module created * added google auth buttons to frontend * removed unnecessary redirect url param * added base feed route to get most popular posts * added specific feed route to get latest posts from planet or user * bootstrap navbar added for temporary header * fixed settings page width * seperated danger zone * updates modal component docs * updated docs in Header and Modal confirmation * Custom mouse cursor image added * displaying the custom cursor globally * Cursor component created, still in progress... * added full screen loader * added small text loader * revised scrambling animation * added full screen coverage to loader * adjusted the settings page style * Added UIBox component for common textboxes * added cards to each setting sections and experimented with new colors * Added main layout of the Post page. (TODO) Change text tag to a dynamic text area or dynamic text tag so the user can type more * style of settings page looks more like signup and feeds page * added box shadow to danger zone * styled confirmation modal component * finsihed the modal styling in the settings modals * added field clearing when clicking btns on the change pass modal * added input clearing to the delete account modal aswell * fixed resposnse toasts for change password * Finish the posting feature and post feature. Integrated post page to backend and database Added the post link to the post button on the hotbar * fixed the delete acc API so that input is joi validated * removed old comments * Finished a dummy profile component Finished a dummy profile page (TODO) Add settings and edit profile button to profile component * finished privacy policy and terms of use pages * Added a profile and settings page * fixed some styling issues with the hotbar * Implemented a profile page that displays the user data (TODO) Query user's posts and display them in them in the profile page * made a 'sky.net' header option and updated all of the pages * updated header documentation * updated docs in the modal component * Implemented the posts from the user to the database using the backend route Updated the post component to accept the data from the database Update the documentatioin of the post component * added canvas dependencies * added background stars * added visual properties to planet model * added react ref helper function * implemented center logo * implemented orbiting planets * combined planets, bg and logo on homepage * changed naming and added as home page * Create post detail page to retrive post from db * Modify the post date and ttime area * Create comment component and modified routes * fetching messages onload between two users setup * Basic design structure setup for messaging * saving messages in db with dummy receiver id * displaying user messages from the db * renamed file * fixed routing with invalid authentication and added planet map to resulting route * fixed home planet icon not appearing * removed old comments * fetching the receiver id from the url * Added userID to profile page Fixed bug on profile page for infinite request Modified the Profile component to accept a version for outside users Added profile page and user page routes to hotbar and app.tsx * diplaying sender message on right and receiver message on left * id query param added to route * added frontend change username functionality * Integrated change email functionality to frontend * added surpise challenge easter egg * Fixed a bug in which the name wouldn't appear on posts in the profile page * Changed the name in user page and profile page * defined isChat State for conversations between two users * if else condition for whether chat conversations is present or not * experimented with hotbar no major changes * wrong opacity value fixed * Added user page Fixed some bugs related to appearing unwanted posts on the profile and user page * added deleteduser model and delete user API is now storing original data in col * added basic soft delete feature * Added a placeholder for when the user has no posts in the profile or user page * added use-image utility to load images easily for Konva * Modify Comment Module CSS * Connected the general feed page to the backend to receive posts from database (rough code, should check it later) * added planet information card * connected saving post API to the post component * fixed clickability of planet info card when it is inactive * Added integration of liking/disliking posts backend routes to frontend, now posts can be liked/disliked through post buttons * Added visual feedback so if a post is already liked or disliked will appear as so even if the user refreshes the page * 1. using socket.io to display real time messages 2. listening socket event for chat between two users 3. emitting socket event upon message submit * passed key to the parent element * Added Socket.IO module * Added a feed page for each planet * Modified the general page to redirect to planet pages (HARD CODED) (TODO) Automate the process of listing the planets. Waiting for the true finished planet home page, so not a priority * Finished connecting the home page to planet feed and general feed * updated the packages used on both client and server side * Added socket.io package * Socket.IO setup with multiple events listening back and forth from frontend * 1. Validating req fields with joi Socket event being emitted to frontend * minor changes * added planet images for visualization * added zoom from scrolling on desktop * removed unnecessary group wrapper from center visual * sorted imports * integrated new auth context into existing components * integrated list home view with planet map home view * Modify the post detail page * Add .gitignore back * Apply like and save function on the detail page * Apply Like, Bookmark to comments * Fixed Comments like issues. Modify to retrieve all comments * Add back .gitignore * fixed local gitignore * added environment helpers and moved dependencies around correctly * removed environment files from repo * removed all node_modules again * prefixed .env variables with VITE * fixed port for vite server * Test if git ignore works * fixed like interaction not returning correct result * fetching the server host dynamically * styled backdrop filter of hotbar, easier to see * Fixed the like, save isssue on feed and post detail page * added media querries for header font size * Changed link to the general feed * font size tweaks * Remove the console log * Fixed user profile link issue * Fixed merge conflicts with planet-feed Added general feed and planet feed pages Added link from home and home planet pages to the feeds Fixed like/saved counts and icons * deletedusers now expire after 30 days * Added a hotbar link to the posting page Redirected the user after posting to the profile page * removed expiry time for now as it is not working * link to private messages * Font styling * 1. Displaying User logged in 2. Back button redirects to window previous history * sending socket emit upon first time save * hotbar hotfix on icon size * Hotfixed username issue * fixed google oauth taking priority over JWT --------- Co-authored-by: Kamal Dolikay Co-authored-by: Zyrakia Co-authored-by: Samarjit Bhogal Co-authored-by: Tianyou-Xie --- .gitignore | 3 + README.md | 11 + client/package-lock.json | 294 +++++++--- client/package.json | 11 +- client/src/app.tsx | 79 ++- client/src/assets/images/amongus-black.webp | Bin 0 -> 5864 bytes client/src/assets/images/amongus-blue.webp | Bin 0 -> 5508 bytes client/src/assets/images/amongus-green.webp | Bin 0 -> 8078 bytes client/src/assets/images/amongus-pink.webp | Bin 0 -> 7472 bytes client/src/assets/images/amongus-red.webp | Bin 0 -> 7296 bytes client/src/assets/images/amongus-white.webp | Bin 0 -> 7938 bytes client/src/assets/images/amongus-yellow.webp | Bin 0 -> 7308 bytes client/src/assets/images/icons8-cursor-38.png | Bin 0 -> 851 bytes .../src/components/Comment/Comment.module.css | 102 ++++ client/src/components/Comment/Comment.tsx | 240 ++++++++ .../src/components/Header/Header.module.css | 19 + client/src/components/Header/Header.tsx | 42 +- .../src/components/Hotbar/Hotbar.module.css | 44 +- client/src/components/Hotbar/Hotbar.tsx | 40 +- .../ModalConfirmation.module.css | 19 +- .../ModalConfirmation/ModalConfirmation.tsx | 32 +- client/src/components/Page/Page.tsx | 4 +- client/src/components/Post/Post.module.css | 9 - client/src/components/Post/Post.tsx | 185 ++++-- .../src/components/Profile/Profile.module.css | 49 ++ client/src/components/Profile/Profile.tsx | 72 +++ client/src/components/UIBox/UIBox.module.css | 28 + client/src/components/UIBox/UIBox.tsx | 38 ++ .../src/components/cursor/cursor.module.css | 59 ++ client/src/components/cursor/cursor.tsx | 21 + .../google-auth-btn/google-auth-btn.tsx | 43 ++ .../src/components/loader/loader.module.css | 56 ++ client/src/components/loader/loader.tsx | 19 + .../components/loader/small-loader.module.css | 28 + client/src/components/loader/small-loader.tsx | 16 + client/src/components/scrambler/scrambler.tsx | 33 ++ client/src/environment.ts | 86 +++ client/src/index.css | 4 + client/src/lib/auth.ts | 42 +- client/src/lib/axios.ts | 10 +- client/src/lib/create-slug.ts | 10 + client/src/lib/isUser.ts | 13 + client/src/lib/with-ref.ts | 13 + client/src/pages/about/about.module.css | 31 +- client/src/pages/about/about.tsx | 44 +- client/src/pages/about/options/policy.tsx | 160 ++++++ client/src/pages/about/options/terms.tsx | 127 +++++ .../src/pages/general-feed/general-feed.tsx | 76 +-- client/src/pages/home/home.tsx | 58 +- client/src/pages/login/login-component.tsx | 21 +- client/src/pages/login/login-html.tsx | 34 +- .../src/pages/messages/messages-component.tsx | 96 ++++ client/src/pages/messages/messages-html.tsx | 107 ++++ client/src/pages/messages/messages.module.css | 11 + client/src/pages/my-feed/my-feed.tsx | 13 +- .../pages/planet-feed/planet-feed.module.css | 0 client/src/pages/planet-feed/planet-feed.tsx | 57 ++ client/src/pages/planet-map/center-visual.tsx | 46 ++ .../src/pages/planet-map/decorative-star.tsx | 20 + .../src/pages/planet-map/planet-info-card.tsx | 100 ++++ client/src/pages/planet-map/planet-map.tsx | 248 +++++++++ client/src/pages/planet-map/planet-visual.tsx | 162 ++++++ .../src/pages/planet-map/space-traveller.tsx | 73 +++ .../src/pages/planet-map/star-background.tsx | 48 ++ .../src/pages/planets/planets-component.tsx | 7 + client/src/pages/planets/planets-html.tsx | 38 ++ client/src/pages/planets/planets.module.css | 233 ++++++++ .../src/pages/post-page/post-page.module.css | 52 ++ client/src/pages/post-page/post-page.tsx | 128 +++++ client/src/pages/post/post.module.css | 0 client/src/pages/post/post.tsx | 94 ++++ .../profile-page/profile-page.module.css | 1 + .../src/pages/profile-page/profile-page.tsx | 87 +++ client/src/pages/signup/signup-html.tsx | 8 + .../src/pages/test-page/test-page.module.css | 6 +- client/src/pages/test-page/test-page.tsx | 6 +- .../src/pages/user-page/user-page.module.css | 1 + client/src/pages/user-page/user-page.tsx | 121 ++++ .../user-settings/options/change-email.tsx | 119 ++++ .../options/change-password-modal.tsx | 145 +++++ .../user-settings/options/change-password.tsx | 124 ----- .../user-settings/options/change-username.tsx | 95 ++++ ...e-account.tsx => delete-account-modal.tsx} | 53 +- .../user-settings/options/manage-account.tsx | 229 ++++++++ .../pages/user-settings/options/your-info.tsx | 51 +- .../user-settings/user-settings.module.css | 60 +- .../src/pages/user-settings/user-settings.tsx | 206 +++---- server/index.ts | 33 +- server/package-lock.json | 527 ++++++++++++++++-- server/package.json | 9 +- server/src/environment.ts | 82 +++ server/src/lib/auth/adapters/basic-jwt.ts | 31 ++ server/src/lib/auth/adapters/google-oauth.ts | 112 ++++ server/src/lib/auth/auth-adapter.ts | 23 + server/src/lib/auth/auth-worker.ts | 52 ++ server/src/load-env.ts | 5 - server/src/middlewares/auth-protected.ts | 16 + server/src/middlewares/require-login.ts | 21 - server/src/models/conversation.ts | 22 + server/src/models/deletedUser.ts | 44 ++ server/src/models/message.ts | 10 +- server/src/models/planet.ts | 17 + server/src/models/user.ts | 24 +- server/src/routes/feed/[planetOrUserId].ts | 28 + server/src/routes/feed/index.ts | 13 + server/src/routes/post/[id]/comment.ts | 53 +- server/src/routes/post/[id]/index.ts | 16 +- server/src/routes/post/[id]/like.ts | 15 +- server/src/routes/post/[id]/save.ts | 8 +- server/src/routes/post/index.ts | 4 +- server/src/routes/user/[id].ts | 8 +- server/src/routes/user/changeEmail.ts | 46 +- server/src/routes/user/changeUsername.ts | 32 +- server/src/routes/user/changepassword.ts | 11 +- server/src/routes/user/chat.ts | 66 +++ .../src/routes/user/deleteaccount/delete.ts | 74 ++- server/src/routes/user/forgetpassword.ts | 29 +- server/src/routes/user/getchats.ts | 33 ++ server/src/routes/user/index.ts | 4 +- server/src/routes/user/login.ts | 26 +- server/src/routes/user/oauth/google.ts | 15 + server/src/routes/user/signup.ts | 4 +- server/src/utils/auth-token.ts | 47 -- server/src/utils/jwt.ts | 47 ++ 124 files changed, 5920 insertions(+), 857 deletions(-) create mode 100644 client/src/assets/images/amongus-black.webp create mode 100644 client/src/assets/images/amongus-blue.webp create mode 100644 client/src/assets/images/amongus-green.webp create mode 100644 client/src/assets/images/amongus-pink.webp create mode 100644 client/src/assets/images/amongus-red.webp create mode 100644 client/src/assets/images/amongus-white.webp create mode 100644 client/src/assets/images/amongus-yellow.webp create mode 100644 client/src/assets/images/icons8-cursor-38.png create mode 100644 client/src/components/Comment/Comment.module.css create mode 100644 client/src/components/Comment/Comment.tsx create mode 100644 client/src/components/Profile/Profile.module.css create mode 100644 client/src/components/Profile/Profile.tsx create mode 100644 client/src/components/UIBox/UIBox.module.css create mode 100644 client/src/components/UIBox/UIBox.tsx create mode 100644 client/src/components/cursor/cursor.module.css create mode 100644 client/src/components/cursor/cursor.tsx create mode 100644 client/src/components/google-auth-btn/google-auth-btn.tsx create mode 100644 client/src/components/loader/loader.module.css create mode 100644 client/src/components/loader/loader.tsx create mode 100644 client/src/components/loader/small-loader.module.css create mode 100644 client/src/components/loader/small-loader.tsx create mode 100644 client/src/components/scrambler/scrambler.tsx create mode 100644 client/src/environment.ts create mode 100644 client/src/lib/create-slug.ts create mode 100644 client/src/lib/isUser.ts create mode 100644 client/src/lib/with-ref.ts create mode 100644 client/src/pages/about/options/policy.tsx create mode 100644 client/src/pages/about/options/terms.tsx create mode 100644 client/src/pages/messages/messages-component.tsx create mode 100644 client/src/pages/messages/messages-html.tsx create mode 100644 client/src/pages/messages/messages.module.css create mode 100644 client/src/pages/planet-feed/planet-feed.module.css create mode 100644 client/src/pages/planet-feed/planet-feed.tsx create mode 100644 client/src/pages/planet-map/center-visual.tsx create mode 100644 client/src/pages/planet-map/decorative-star.tsx create mode 100644 client/src/pages/planet-map/planet-info-card.tsx create mode 100644 client/src/pages/planet-map/planet-map.tsx create mode 100644 client/src/pages/planet-map/planet-visual.tsx create mode 100644 client/src/pages/planet-map/space-traveller.tsx create mode 100644 client/src/pages/planet-map/star-background.tsx create mode 100644 client/src/pages/planets/planets-component.tsx create mode 100644 client/src/pages/planets/planets-html.tsx create mode 100644 client/src/pages/planets/planets.module.css create mode 100644 client/src/pages/post-page/post-page.module.css create mode 100644 client/src/pages/post-page/post-page.tsx create mode 100644 client/src/pages/post/post.module.css create mode 100644 client/src/pages/post/post.tsx create mode 100644 client/src/pages/profile-page/profile-page.module.css create mode 100644 client/src/pages/profile-page/profile-page.tsx create mode 100644 client/src/pages/user-page/user-page.module.css create mode 100644 client/src/pages/user-page/user-page.tsx create mode 100644 client/src/pages/user-settings/options/change-email.tsx create mode 100644 client/src/pages/user-settings/options/change-password-modal.tsx delete mode 100644 client/src/pages/user-settings/options/change-password.tsx create mode 100644 client/src/pages/user-settings/options/change-username.tsx rename client/src/pages/user-settings/options/{delete-account.tsx => delete-account-modal.tsx} (64%) create mode 100644 client/src/pages/user-settings/options/manage-account.tsx create mode 100644 server/src/environment.ts create mode 100644 server/src/lib/auth/adapters/basic-jwt.ts create mode 100644 server/src/lib/auth/adapters/google-oauth.ts create mode 100644 server/src/lib/auth/auth-adapter.ts create mode 100644 server/src/lib/auth/auth-worker.ts delete mode 100644 server/src/load-env.ts create mode 100644 server/src/middlewares/auth-protected.ts delete mode 100644 server/src/middlewares/require-login.ts create mode 100644 server/src/models/conversation.ts create mode 100644 server/src/models/deletedUser.ts create mode 100644 server/src/routes/feed/[planetOrUserId].ts create mode 100644 server/src/routes/feed/index.ts create mode 100644 server/src/routes/user/chat.ts create mode 100644 server/src/routes/user/getchats.ts create mode 100644 server/src/routes/user/oauth/google.ts delete mode 100644 server/src/utils/auth-token.ts create mode 100644 server/src/utils/jwt.ts 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 0000000000000000000000000000000000000000..3332c523226726aff54909139f9329980b7bdb09 GIT binary patch literal 5864 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 literal 0 HcmV?d00001 diff --git a/client/src/assets/images/amongus-green.webp b/client/src/assets/images/amongus-green.webp new file mode 100644 index 0000000000000000000000000000000000000000..d4323ffb61778031a1a06e464abf688b95391b57 GIT binary patch literal 8078 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 literal 0 HcmV?d00001 diff --git a/client/src/assets/images/amongus-white.webp b/client/src/assets/images/amongus-white.webp new file mode 100644 index 0000000000000000000000000000000000000000..e694791f2c997626a9789d15718154cf5e07a8dc GIT binary patch literal 7938 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⁣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 I|EKzY088BQ-T(jq literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..7715a6907311a96b7f5f753913e56162e421fe33 GIT binary patch literal 851 zcmV-Z1FZasP)$DCR z4sn6dhOlALI_$w=9rh%p99oo!qE*TTxglF@$T=&uNwJY~C5gf}uikfOwuHsZ?99mi z>$(5U^Stl-J)=}_{>KD^!2xM$X@gZNMMMfg`2fm$l8aQTgyczjnLhH0eNsXs*Qito z$!2Db^aH!vHO9;|Fio;C*)9d(42Ng?fyUQ5Q2ShC{DKJXpi-(&(qJS{!^`S~nW}%O zF?pGRnL1TeRMao2XDkqbSzL$4jIs!>IX5@Af5Kq^j??u*-@!~ZywaFmWpMy6r>Cb6 zN+^uvF^c;mEL_fly`4Cb8zM3^k+37A{1nxNTXINKBt&r^dY(5dAus@kO-Ib=y2UL& zp&@Pv;DOjV0?RoacFpH~b)ePqN6!#;^;<&>A z)LGcB_P>JJs=TFU#>n$5%)k)?5i0jROCnkUi0 zOx3h$s14e^3)v#=eHqlcM__u53V%D!XUAH@cXtWD$QM{oyXTAKiDu^rob zFLs!zx)++0O2Qw1)W_1XjkaUeccaIcr>3S&2deMrz{YDjuE!ydjwmB;{CVMn>C<@=p~`r61@x!^d?K$Yl_TxL7A=~~xy$HcDd-lUZ3tCA4_ zB^FkGwe$5Zn3WoTdPy#}qg-`XBX5ei{svaAk{#D|$J!3L86l8gL}Z~#l>k^DKI9XU dZRyQFlW*0%FzpJy{#O71002ovPDHLkV1jz~pB4ZB literal 0 HcmV?d00001 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!), + }); + } +}