├── LICENSE ├── README.md ├── backend ├── .env ├── .gitignore ├── config.js ├── controllers │ ├── comment.js │ ├── game.js │ ├── post.js │ └── user.js ├── database │ └── collections.js ├── index.js ├── middlewares │ ├── auth.js │ ├── comment.js │ ├── game.js │ ├── post.js │ ├── upload.js │ └── user.js ├── models │ ├── comment.js │ ├── game.js │ ├── post.js │ └── user.js ├── package-lock.json ├── package.json ├── routes │ ├── index.js │ ├── private.js │ ├── private │ │ ├── comment.js │ │ ├── game.js │ │ ├── post.js │ │ └── user.js │ ├── public.js │ └── public │ │ ├── comment.js │ │ ├── game.js │ │ ├── post.js │ │ └── user.js └── utils │ ├── authorize.js │ ├── token.js │ └── upload.js └── minigame-frontend ├── .editorconfig ├── .env ├── .gitignore ├── README.md ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public └── vite.svg ├── src ├── assets │ ├── ThumbsDown.jsx │ ├── ThumbsDownFilled.jsx │ ├── ThumbsUp.jsx │ ├── ThumbsUpFilled.jsx │ ├── Trash.jsx │ └── react.svg ├── components │ ├── CommentCard.jsx │ ├── CommentSection.jsx │ ├── GameDetails.jsx │ ├── GameDetailsPopup.jsx │ ├── GameRowComponent.jsx │ ├── HeroComponent.jsx │ ├── Leaderboard.jsx │ ├── LikedGames.jsx │ ├── LoginModal.jsx │ ├── PlayerRank.jsx │ ├── Popup.jsx │ ├── PostView.jsx │ ├── ProfileBioComponent.jsx │ ├── ProfileDetails.jsx │ ├── ProfilePopup.jsx │ ├── ShowcaseGameCardComponent.jsx │ ├── css │ │ ├── CommentCard.css │ │ ├── Hero.css │ │ ├── Leaderboard.css │ │ ├── ProfileBio.css │ │ ├── ProfileDetails.css │ │ └── ShowcaseGameCard.css │ └── games │ │ └── GameCard.jsx ├── contributors │ └── Contributors.jsx ├── domain │ ├── SiteContext.jsx │ └── UserContext.jsx ├── images │ ├── AdrianPFP.png │ ├── DanielPFP.jpg │ ├── JackPFP.jpg │ ├── MichaelPFP.jpeg │ ├── MinhPFP.jpg │ ├── ThaiPFP.jpg │ ├── TrentonPFP.jpg │ └── gglogo2.png ├── index.css ├── main.jsx └── page │ ├── AllGames.css │ ├── AllGames.jsx │ ├── Contributors.jsx │ ├── CreateGame.jsx │ ├── Editor.jsx │ ├── GamePage.jsx │ ├── Homepage.jsx │ ├── Layout.jsx │ ├── Post.jsx │ ├── PostViewPage.jsx │ ├── Profile.jsx │ ├── contributors.css │ └── profile.css ├── tailwind.config.js └── vite.config.js /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GameGimmel 2 | Hosted Website Link: https://gamegimmel.acmuic.org 3 | 4 | ## Running the project locally 5 | In the terminal: 6 | - move to the frontend or backend directory/folder with `cd .\directory\` 7 | 1. run `npm install` in frontend or backend directory 8 | 2. run `npm run dev` in the frontend and backend directory 9 | 10 | ## Contributors 11 | 12 | - Adrian Knight - [@Ajknight121](https://github.com/Ajknight121) 13 | - Chao Liu - [@JackLiu00331](https://github.com/JackLiu00331) 14 | - Viet Thai Nguyen - [@thai.nguyen07](https://github.com/AlgoriThai07) 15 | - Trenton Coleman - [@tdcoleman127](https://github.com/tdcoleman127) 16 | - Minh Ngo - [@hoangngo-sudo](https://github.com/hoangngo-sudo) 17 | - Daniel Barajas - [@danbarajas](https://github.com/danbarajas) 18 | - Michael Jaimes - [@mike6612](https://github.com/mike6612) 19 | - Dylan Nguyen - [@dyl4915](https://github.com/dyl4915) 20 | -------------------------------------------------------------------------------- /backend/.env: -------------------------------------------------------------------------------- 1 | PORT = 2 | MONGODB = 3 | AT_SECRET_KEY = 4 | RT_SECRET_KEY = 5 | CLOUDINARY_CONFIG = -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.local -------------------------------------------------------------------------------- /backend/config.js: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | dotenv.config({ path: ".env.local" }); 3 | 4 | const AT_SECRET_KEY = process.env.AT_SECRET_KEY; 5 | const RT_SECRET_KEY = process.env.RT_SECRET_KEY; 6 | const CLOUDINARY_CONFIG = JSON.parse(process.env.CLOUDINARY_CONFIG); 7 | const MONGODB = process.env.MONGODB; 8 | 9 | export { AT_SECRET_KEY, RT_SECRET_KEY, CLOUDINARY_CONFIG, MONGODB }; 10 | -------------------------------------------------------------------------------- /backend/controllers/comment.js: -------------------------------------------------------------------------------- 1 | import PostModel from "../models/post.js"; 2 | import CommentModel from "../models/comment.js"; 3 | import { authorizeUser } from "../utils/authorize.js"; 4 | 5 | const CommentControllers = { 6 | createComment: async (req, res) => { 7 | try { 8 | // Get postId, body, and user transferred from req 9 | const { postId } = req.params; 10 | const { body } = req.body; 11 | const { user } = req; 12 | // Find post from postId 13 | const crrPost = await PostModel.findById(postId); 14 | // If cannot find, throw an error 15 | if (!crrPost) throw new Error("Cannot not find post!"); 16 | // Create a new comment 17 | const newComment = await CommentModel.create({ 18 | author: user._id, 19 | postId, 20 | body, 21 | }); 22 | // Send out the new comment and the userName of the author 23 | res.status(201).send({ 24 | message: "Comment created successfully!", 25 | success: true, 26 | data: { 27 | ...newComment.toObject(), 28 | userName: user.userName, 29 | }, 30 | }); 31 | } catch (error) { 32 | res.status(404).send({ 33 | message: error.message, 34 | success: false, 35 | data: null, 36 | }); 37 | } 38 | }, 39 | updateComment: async (req, res) => { 40 | try { 41 | // Get commentId, body, and user transferred from req 42 | const { commentId } = req.params; 43 | const { body } = req.body; 44 | const { user } = req; 45 | // Find comment from the commentId 46 | const crrComment = await CommentModel.findById(commentId); 47 | if (!crrComment) throw new Error("Cannot find comment!"); 48 | // Authorized user 49 | const authorized = authorizeUser(user._id, crrComment.author); 50 | if (!authorized.success) throw new Error(authorized.message); 51 | // Only updated the fields that needs changing 52 | const updatedFields = {}; 53 | if (!body && String(body) !== String(crrComment.body)) { 54 | updatedFields.body = body; 55 | } 56 | // Update comment 57 | const updatedComment = await CommentModel.findByIdAndUpdate( 58 | commentId, 59 | { $set: updatedFields }, 60 | { new: true } 61 | ); 62 | // Return the updatedComment 63 | res.status(200).send({ 64 | message: "Comment updated successfully!", 65 | success: true, 66 | data: updatedComment, 67 | }); 68 | } catch (error) { 69 | res.status(404).send({ 70 | message: error.message, 71 | success: false, 72 | data: null, 73 | }); 74 | } 75 | }, 76 | getAllComment: async (req, res) => { 77 | try { 78 | const admin = req.user?.role === "Admin"; 79 | 80 | const commentFilter = admin ? {} : { isDelete: false }; 81 | 82 | const listComments = await CommentModel.find(commentFilter); 83 | if (listComments.length === 0) throw new Error("No comments found!"); 84 | 85 | res.status(201).send({ 86 | message: "Here is a list of comments!", 87 | success: true, 88 | data: listComments, 89 | }); 90 | } catch (error) { 91 | res.status(500).send({ 92 | message: error.message, 93 | success: false, 94 | data: null, 95 | }); 96 | } 97 | }, 98 | getCommentsInAPost: async (req, res) => { 99 | try { 100 | const { postId } = req.params; 101 | 102 | const admin = req.user?.role === "Admin"; 103 | 104 | const commentFilter = admin 105 | ? { postId: postId } 106 | : { postId: postId, isDelete: false }; 107 | 108 | let listComments = await CommentModel.find(commentFilter); 109 | 110 | if (listComments.length === 0) { 111 | // throw new Error("No comments in this post!"); 112 | listComments = []; 113 | } 114 | 115 | res.status(201).send({ 116 | message: "Here is a list of comments in this post", 117 | success: true, 118 | data: listComments, 119 | }); 120 | } catch (error) { 121 | res.status(500).send({ 122 | message: error.message, 123 | success: false, 124 | data: null, 125 | }); 126 | } 127 | }, 128 | deleteComment: async (req, res) => { 129 | try { 130 | const { user } = req; 131 | const { commentId } = req.params; 132 | 133 | // Get the current post 134 | const crrComment = await CommentModel.findById(commentId); 135 | if (!crrComment) throw new Error("Cannot find comment!"); 136 | 137 | // Check if the user is authorized to delete post 138 | const owner = authorizeUser(user._id, crrComment.author); 139 | const admin = user.role === "Admin"; 140 | 141 | if (!owner.success && !admin) { 142 | throw new Error("Unauthorize to delete comment!"); 143 | } 144 | // Update + Fetch in parallel 145 | const commentFilter = admin ? {} : { isDelete: false }; 146 | 147 | const [_, listComments] = await Promise.all([ 148 | CommentModel.findByIdAndUpdate(commentId, { isDelete: true }), 149 | CommentModel.find(commentFilter), 150 | ]); 151 | 152 | res.status(200).send({ 153 | message: "Comment deleted!", 154 | success: true, 155 | data: listComments, 156 | }); 157 | } catch (error) { 158 | res.status(500).send({ 159 | message: error.message, 160 | success: false, 161 | data: null, 162 | }); 163 | } 164 | }, 165 | }; 166 | 167 | export default CommentControllers; 168 | -------------------------------------------------------------------------------- /backend/controllers/game.js: -------------------------------------------------------------------------------- 1 | import GameModel from "../models/game.js"; 2 | import { handleFileUpload } from "../utils/upload.js"; 3 | 4 | const GameControllers = { 5 | createGame: async (req, res) => { 6 | try { 7 | // Get info from req 8 | const { user } = req; 9 | const { gameName, description, relatedLinks } = req.body; 10 | const coverImage = req.files?.coverImage?.[0]; // single file 11 | const listFile = req.files?.media || []; // array of files 12 | 13 | const existedGame = await GameModel.findOne({ 14 | gameName: gameName, 15 | }); 16 | 17 | if (existedGame) { 18 | throw new Error("This gameName is used. Please use another name"); 19 | } 20 | 21 | // Create a game payload 22 | const gamePayload = { 23 | gameName, 24 | description, 25 | relatedLinks, 26 | author: user._id, 27 | }; 28 | // If user upload media 29 | if (coverImage) { 30 | const response = await handleFileUpload(coverImage); 31 | if (!response.success) throw new Error(response.message); 32 | gamePayload.coverImage = response.data; 33 | } 34 | 35 | if (listFile) { 36 | const listMedia = []; 37 | for (const file of listFile) { 38 | const response = await handleFileUpload(file); 39 | if (!response.success) throw new Error(response.message); 40 | listMedia.push(response.data); 41 | } 42 | gamePayload.media = listMedia; 43 | } 44 | 45 | const newGame = await GameModel.create(gamePayload); 46 | 47 | res.status(201).send({ 48 | message: "Game created successfully", 49 | success: true, 50 | data: newGame, 51 | }); 52 | } catch (error) { 53 | res.status(500).send({ 54 | message: error.message, 55 | success: false, 56 | data: null, 57 | }); 58 | } 59 | }, 60 | getAllGames: async (req, res) => { 61 | try { 62 | const listGames = await GameModel.find(); 63 | if (listGames.length === 0) throw new Error("No games found!"); 64 | 65 | res.status(200).send({ 66 | message: "Here is a list of all the games", 67 | success: true, 68 | data: listGames, 69 | }); 70 | } catch (error) { 71 | res.status(500).send({ 72 | message: error.message, 73 | success: false, 74 | data: null, 75 | }); 76 | } 77 | }, 78 | getGameById: async (req, res) => { 79 | try { 80 | const { gameId } = req.params; 81 | 82 | const crrGame = await GameModel.findById(gameId); 83 | 84 | if (!crrGame) throw new Error("Cannot find game!"); 85 | 86 | res.status(200).send({ 87 | message: "Here is your game", 88 | success: true, 89 | data: crrGame, 90 | }); 91 | } catch (error) { 92 | res.status(500).send({ 93 | message: error.message, 94 | success: false, 95 | data: null, 96 | }); 97 | } 98 | }, 99 | }; 100 | 101 | export default GameControllers; 102 | -------------------------------------------------------------------------------- /backend/controllers/post.js: -------------------------------------------------------------------------------- 1 | import PostModel from "../models/post.js"; 2 | import { handleFileUpload } from "../utils/upload.js"; 3 | import { authorizeUser } from "../utils/authorize.js"; 4 | 5 | const PostControllers = { 6 | createPost: async (req, res) => { 7 | try { 8 | const { user } = req; 9 | const { title, body } = req.body; 10 | const listFile = req.files; 11 | 12 | const listMedia = []; 13 | 14 | // If user upload files 15 | if (listFile) { 16 | for (const file of listFile) { 17 | const response = await handleFileUpload(file); 18 | if (!response.success) throw new Error(response.message); 19 | listMedia.push(response.data); 20 | } 21 | } 22 | 23 | const newPost = await PostModel.create({ 24 | author: user._id, 25 | title, 26 | body, 27 | images: listMedia, 28 | }); 29 | 30 | res.status(201).send({ 31 | message: "Post created successfully", 32 | success: true, 33 | data: { 34 | ...newPost.toObject(), 35 | userName: user.userName, 36 | }, 37 | }); 38 | } catch (error) { 39 | res.status(500).send({ 40 | message: error.message, 41 | success: false, 42 | data: null, 43 | }); 44 | } 45 | }, 46 | updatePost: async (req, res) => { 47 | try { 48 | const { user } = req; 49 | const { title, body } = req.body; 50 | const { postId } = req.params; 51 | const listFile = req.images; 52 | 53 | // Get the current post 54 | const crrPost = await PostModel.findById(postId); 55 | if (!crrPost) throw new Error("Cannot find post!"); 56 | 57 | const authorized = authorizeUser(user._id, crrPost.author); 58 | if (!authorized.success) throw new Error(authorized.message); 59 | 60 | const updatedFields = {}; 61 | // Only update the fields that are different from the original 62 | if (title && String(title) !== String(crrPost.title)) { 63 | updatedFields.title = title; 64 | } 65 | if (body && String(body) !== String(crrPost.body)) { 66 | updatedFields.body = body; 67 | } 68 | // Update the listMedia regardless whether it has been changed 69 | // More efficient 70 | const listMedia = []; 71 | if (listFile) { 72 | for (const file of listFile) { 73 | const response = await handleFileUpload(file); 74 | if (!response.success) throw new Error(response.message); 75 | listMedia.push(response.data); 76 | } 77 | } 78 | updatedFields.images = listMedia; 79 | // Update the post 80 | const updatedPost = await PostModel.findByIdAndUpdate( 81 | postId, 82 | { $set: updatedFields }, 83 | { new: true } 84 | ); 85 | // Return the updated post 86 | res.status(200).send({ 87 | message: "Post updated successfully!", 88 | success: true, 89 | data: updatedPost, 90 | }); 91 | } catch (error) { 92 | res.status(500).send({ 93 | message: error.message, 94 | success: false, 95 | data: null, 96 | }); 97 | } 98 | }, 99 | getAllPosts: async (req, res) => { 100 | try { 101 | const admin = req.user?.role === "Admin"; 102 | 103 | const postFilter = admin ? {} : { isDelete: false }; 104 | 105 | const listPosts = await PostModel.find(postFilter); 106 | if (listPosts.length === 0) throw new Error("No posts found!"); 107 | 108 | res.status(201).send({ 109 | message: "Here is a list of posts!", 110 | success: true, 111 | data: listPosts, 112 | }); 113 | } catch (error) { 114 | res.status(500).send({ 115 | message: error.message, 116 | success: false, 117 | data: null, 118 | }); 119 | } 120 | }, 121 | getPostsByUser: async (req, res) => { 122 | try { 123 | const { userId } = req.query; 124 | const admin = req.user?.role === "Admin"; 125 | 126 | const postFilter = admin 127 | ? { 128 | author: userId, 129 | } 130 | : { 131 | author: userId, 132 | isDelete: false, 133 | }; 134 | 135 | let listPosts = await PostModel.find(postFilter); 136 | 137 | if (listPosts.length === 0) { 138 | // throw new Error("No posts by this user found!"); 139 | listPosts = [] 140 | } 141 | 142 | res.status(201).send({ 143 | message: "Here is a list of posts by this user!", 144 | success: true, 145 | data: listPosts, 146 | }); 147 | } catch (error) { 148 | res.status(500).send({ 149 | message: error.message, 150 | success: false, 151 | data: null, 152 | }); 153 | } 154 | }, 155 | getPostById: async (req, res) => { 156 | try { 157 | const { postId } = req.query; 158 | const admin = req.user?.role === "Admin"; 159 | 160 | const postFilter = admin 161 | ? { 162 | _id: postId, 163 | } 164 | : { 165 | _id: postId, 166 | isDelete: false, 167 | }; 168 | 169 | const crrPost = await PostModel.find(postFilter); 170 | 171 | if (!crrPost) throw new Error("This post doesn't exist!"); 172 | 173 | res.status(201).send({ 174 | message: "Here is your post!", 175 | success: true, 176 | data: crrPost, 177 | }); 178 | } catch (error) { 179 | res.status(500).send({ 180 | message: error.message, 181 | success: false, 182 | data: null, 183 | }); 184 | } 185 | }, 186 | deletePost: async (req, res) => { 187 | try { 188 | const { user } = req; 189 | const { postId } = req.params; 190 | 191 | // Get the current post 192 | const crrPost = await PostModel.findById(postId); 193 | if (!crrPost) throw new Error("Cannot find post!"); 194 | 195 | // Check if the user is authorized to delete post 196 | const owner = authorizeUser(user._id, crrPost.author); 197 | const admin = user.role === "Admin"; 198 | 199 | if (!owner.success && !admin) { 200 | throw new Error("Unauthorize to delete post!"); 201 | } 202 | // Update + Fetch in parallel 203 | const postFilter = admin ? {} : { isDelete: false }; 204 | 205 | const [_, listPosts] = await Promise.all([ 206 | PostModel.findByIdAndUpdate(postId, { isDelete: true }), 207 | PostModel.find(postFilter), 208 | ]); 209 | 210 | res.status(200).send({ 211 | message: "Post deleted!", 212 | success: true, 213 | data: listPosts, 214 | }); 215 | } catch (error) { 216 | res.status(500).send({ 217 | message: error.message, 218 | success: false, 219 | data: null, 220 | }); 221 | } 222 | }, 223 | }; 224 | 225 | export default PostControllers; 226 | -------------------------------------------------------------------------------- /backend/controllers/user.js: -------------------------------------------------------------------------------- 1 | import UserModel from "../models/user.js"; 2 | import bcrypt from "bcrypt"; 3 | import { CLOUDINARY_CONFIG } from "../config.js"; 4 | import { v2 as cloudinary } from "cloudinary"; 5 | import { handleFileUpload } from "../utils/upload.js"; 6 | import { generateToken } from "../utils/token.js"; 7 | 8 | cloudinary.config(CLOUDINARY_CONFIG); 9 | 10 | const UserControllers = { 11 | createUser: async (req, res) => { 12 | try { 13 | // Get the info from the user 14 | const { userName, email, password, bio, role } = req.body; 15 | const avatar = req.file; 16 | // Check if there's any email alr existed 17 | const existedUser = await UserModel.findOne({ 18 | email: email, 19 | }); 20 | 21 | // If alr exists an email, 22 | if (existedUser) 23 | throw new Error("Email existed! Please enter a different email."); 24 | // Create a hash password 25 | const saltRounds = 10; 26 | const salt = bcrypt.genSaltSync(saltRounds); 27 | const hashedPassword = bcrypt.hashSync(password, salt); 28 | // Create a payload for creating newUser 29 | const userPayload = { 30 | userName, 31 | email, 32 | password: hashedPassword, 33 | bio, 34 | role, 35 | }; 36 | // Only add avatar if the user upload avatar file 37 | if (avatar) { 38 | const response = await handleFileUpload(avatar); 39 | if (!response.success) throw new Error(response.message); 40 | userPayload.avatar = response.data; 41 | } 42 | 43 | // Create a new user 44 | const newUser = await UserModel.create(userPayload); 45 | // Hide the password when displaying 46 | newUser.password = undefined; 47 | 48 | res.status(201).send({ 49 | message: "User created successfully", 50 | success: true, 51 | data: { user: newUser }, 52 | }); 53 | } catch (error) { 54 | res.status(409).send({ 55 | message: error.message, 56 | success: false, 57 | data: null, 58 | }); 59 | } 60 | }, 61 | signinUser: async (req, res) => { 62 | try { 63 | // Get the info from the user 64 | const { email, password } = req.body; 65 | 66 | // Check if user exist 67 | const crrUser = await UserModel.findOne({ 68 | email: email, 69 | }); 70 | 71 | // If does not exist user 72 | if (!crrUser) 73 | throw new Error("Account does not exist! Please re-enter your email!"); 74 | 75 | // Check whether the password is correct or not 76 | const comparePassword = bcrypt.compareSync(password, crrUser.password); 77 | if (!comparePassword) 78 | throw new Error( 79 | "Wrong password entered! Please re-enter your password!" 80 | ); 81 | 82 | const user = { 83 | _id: crrUser._id, 84 | email: crrUser.email, 85 | userName: crrUser.userName, 86 | role: crrUser.role, 87 | }; 88 | 89 | const accessToken = generateToken( 90 | { 91 | ...user, 92 | typeToken: "AT", 93 | }, 94 | "AT" 95 | ); 96 | 97 | const refreshToken = generateToken( 98 | { 99 | ...user, 100 | typeToken: "RT", 101 | }, 102 | "RT" 103 | ); 104 | 105 | res.status(200).send({ 106 | message: "User signs in successfully", 107 | success: true, 108 | data: { 109 | user, 110 | accessToken, 111 | refreshToken, 112 | }, 113 | }); 114 | } catch (error) { 115 | res.status(400).send({ 116 | message: error.message, 117 | success: false, 118 | data: null, 119 | }); 120 | } 121 | }, 122 | updateProfile: async (req, res) => { 123 | try { 124 | const { user } = req; 125 | const { userName, email, bio, role } = req.body; 126 | const avatar = req.file; 127 | 128 | // Get the crrUser 129 | const crrUser = await UserModel.findById(user._id); 130 | 131 | const updatedFields = {}; 132 | 133 | // Check and update only changed fields 134 | if (userName && String(userName) !== String(crrUser.userName)) { 135 | updatedFields.userName = userName; 136 | } 137 | if (email && String(email) !== String(crrUser.email)) { 138 | updatedFields.email = email; 139 | } 140 | if (bio && String(bio) !== String(crrUser.bio)) { 141 | updatedFields.bio = bio; 142 | } 143 | if (role && String(role) !== String(crrUser.role)) { 144 | updatedFields.role = role; 145 | } 146 | if (avatar) { 147 | // Get the current file "fileName" 148 | const currentFileName = crrUser.avatar 149 | ? crrUser.avatar.split("/").pop().split(".")[0] 150 | : null; 151 | // Get the new file "fileName" 152 | const newFileName = avatar.originalname.split(".")[0]; 153 | 154 | // If the names are different, replace with the new file 155 | if (newFileName !== currentFileName) { 156 | // Proceed with upload 157 | const response = await handleFileUpload(avatar); 158 | if (!response.success) throw new Error(response.message); 159 | updatedFields.avatar = response.data; 160 | // Delete the old avatar from Cloudinary 161 | if (currentFileName) { 162 | await cloudinary.uploader.destroy(currentFileName); 163 | } 164 | } 165 | } 166 | // Update the only changed fields 167 | const updatedProfile = await UserModel.findByIdAndUpdate( 168 | user._id, 169 | { $set: updatedFields }, 170 | { new: true } 171 | ).select("-password"); 172 | 173 | res.status(200).send({ 174 | message: "Profile updated successfully!", 175 | success: true, 176 | data: updatedProfile, 177 | }); 178 | } catch (error) { 179 | res.status(500).send({ 180 | message: error.message, 181 | success: false, 182 | data: null, 183 | }); 184 | } 185 | }, 186 | getUserInfo: async (req, res) => { 187 | try { 188 | // Get the userId 189 | const { userId } = req.params; 190 | 191 | // Check if user exist 192 | const crrUser = await UserModel.findById(userId); 193 | 194 | // If does not exist user 195 | if (!crrUser) throw new Error("Cannot find user"); 196 | 197 | const user = { 198 | _id: crrUser._id, 199 | email: crrUser.email, 200 | userName: crrUser.userName, 201 | role: crrUser.role, 202 | bio: crrUser.bio, 203 | avatar: crrUser.avatar, 204 | likedGames: crrUser.likedGames, 205 | interests: crrUser.interests, 206 | }; 207 | 208 | res.status(201).send({ 209 | message: "Here is this user info!", 210 | success: true, 211 | data: user, 212 | }); 213 | } catch (error) { 214 | res.status(400).send({ 215 | message: error.message, 216 | success: false, 217 | data: null, 218 | }); 219 | } 220 | }, 221 | likeGame: async (req, res) => { 222 | try { 223 | // Get the userId 224 | const { game } = req.body; 225 | const { user } = req; 226 | 227 | const updatedUser = await UserModel.findByIdAndUpdate( 228 | user._id, 229 | { $addToSet: { likedGames: game } }, // prevents duplicates 230 | { new: true } 231 | ); 232 | 233 | if (!updatedUser) throw new Error("Cannot find user!"); 234 | 235 | res.status(200).send({ 236 | message: "Game liked successfully!", 237 | success: true, 238 | data: updatedUser, 239 | }); 240 | } catch (error) { 241 | res.status(400).send({ 242 | message: error.message, 243 | success: false, 244 | data: null, 245 | }); 246 | } 247 | }, 248 | unlikeGame: async (req, res) => { 249 | try { 250 | // Get the userId 251 | const { game } = req.body; 252 | const { user } = req; 253 | 254 | const updatedUser = await UserModel.findByIdAndUpdate( 255 | user._id, 256 | { $pull: { likedGames: game } }, 257 | { new: true } 258 | ); 259 | 260 | if (!updatedUser) throw new Error("Cannot find user!"); 261 | 262 | res.status(200).send({ 263 | message: "Game unliked successfully!", 264 | success: true, 265 | data: updatedUser, 266 | }); 267 | } catch (error) { 268 | res.status(400).send({ 269 | message: error.message, 270 | success: false, 271 | data: null, 272 | }); 273 | } 274 | }, 275 | addInterest: async (req, res) => { 276 | try { 277 | // Get the userId 278 | const { interest } = req.body; 279 | const { user } = req; 280 | 281 | const updatedUser = await UserModel.findByIdAndUpdate( 282 | user._id, 283 | { $addToSet: { interests: interest } }, // prevents duplicates 284 | { new: true } 285 | ); 286 | 287 | if (!updatedUser) throw new Error("Cannot find user!"); 288 | 289 | res.status(200).send({ 290 | message: "Interest added successfully!", 291 | success: true, 292 | data: updatedUser, 293 | }); 294 | } catch (error) { 295 | res.status(400).send({ 296 | message: error.message, 297 | success: false, 298 | data: null, 299 | }); 300 | } 301 | }, 302 | removeInterest: async (req, res) => { 303 | try { 304 | // Get the userId 305 | const { interest } = req.body; 306 | const { user } = req; 307 | 308 | const updatedUser = await UserModel.findByIdAndUpdate( 309 | user._id, 310 | { $pull: { interests: interest } }, // prevents duplicates 311 | { new: true } 312 | ); 313 | 314 | if (!updatedUser) throw new Error("Cannot find user!"); 315 | 316 | res.status(200).send({ 317 | message: "Interest removed successfully!", 318 | success: true, 319 | data: updatedUser, 320 | }); 321 | } catch (error) { 322 | res.status(400).send({ 323 | message: error.message, 324 | success: false, 325 | data: null, 326 | }); 327 | } 328 | }, 329 | }; 330 | 331 | export default UserControllers; 332 | -------------------------------------------------------------------------------- /backend/database/collections.js: -------------------------------------------------------------------------------- 1 | const Collections = { 2 | users: "User", 3 | posts: "Post", 4 | comments: "Comment", 5 | games: "Game", 6 | }; 7 | 8 | export default Collections; 9 | -------------------------------------------------------------------------------- /backend/index.js: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import cors from "cors"; 3 | import { MONGODB } from "./config.js"; 4 | import http from "http"; 5 | import mongoose from "mongoose"; 6 | import { RootRouteV1 } from "./routes/index.js"; 7 | 8 | await mongoose.connect(MONGODB); 9 | 10 | const app = express(); 11 | const server = http.createServer(app); 12 | 13 | app.use(express.json()); // Only used for JSON payloads (not needed for form-data) 14 | app.use(express.urlencoded({ extended: true })); // Same, for urlencoded only 15 | app.use(cors()); 16 | 17 | app.use("/api", RootRouteV1); 18 | 19 | server.listen(process.env.PORT, () => { 20 | console.log(`Server is running at port ${process.env.PORT}`); 21 | }); 22 | -------------------------------------------------------------------------------- /backend/middlewares/auth.js: -------------------------------------------------------------------------------- 1 | import { verifyToken } from "../utils/token.js"; 2 | 3 | const AuthMiddlewares = { 4 | verifyAccessToken: (req, res, next) => { 5 | try { 6 | const authHeader = req.headers["authorization"]; 7 | if (!authHeader) throw new Error("Please enter token"); 8 | // Split the access token from the bearer token 9 | // Handle both cases: with or without 'Bearer ' 10 | const token = authHeader.startsWith("Bearer ") 11 | ? authHeader.split(" ")[1] 12 | : authHeader; 13 | 14 | // Validate the access token 15 | const data = verifyToken(token, "AT"); 16 | req.user = data; 17 | return next(); 18 | } catch (error) { 19 | let type = ""; 20 | let getMessage = ""; 21 | switch (error.message) { 22 | case "invalid signature": 23 | getMessage = "Cannot verify token"; 24 | type = "INVALID_TOKEN"; 25 | break; 26 | case "jwt expired": 27 | getMessage = "Token is expired"; 28 | type = "EXP_TOKEN"; 29 | break; 30 | default: 31 | getMessage = "Cannot authenticate user"; 32 | type = "UNAUTH"; 33 | break; 34 | } 35 | res.status(401).send({ 36 | message: getMessage, 37 | type, 38 | success: false, 39 | data: null, 40 | }); 41 | } 42 | }, 43 | verifyRefreshToken: (req, res, next) => { 44 | try { 45 | const authHeader = req.headers["authorization"]; 46 | if (!authHeader) throw new Error("Please enter token"); 47 | // Split the access token from the bearer token 48 | const token = authHeader.split(" ")[1]; 49 | 50 | // Validate the access token 51 | const data = verifyToken(token, "RT"); 52 | req.user = data; 53 | return next(); 54 | } catch (error) { 55 | let type = ""; 56 | let getMessage = ""; 57 | switch (error.message) { 58 | case "invalid signature": 59 | getMessage = "Cannot verify token"; 60 | type = "INVALID_TOKEN"; 61 | break; 62 | case "jwt expired": 63 | getMessage = "Token is expired"; 64 | type = "EXP_TOKEN"; 65 | break; 66 | default: 67 | getMessage = "Cannot authenticate user"; 68 | type = "UNAUTH"; 69 | break; 70 | } 71 | res.status(401).send({ 72 | message: getMessage, 73 | type, 74 | success: false, 75 | data: null, 76 | }); 77 | } 78 | }, 79 | verifyAdmin: (req, res, next) => { 80 | try { 81 | // Get the user info from req 82 | // Assume that user is verified before 83 | const { user } = req; 84 | // If user doesn't exist or not an admin 85 | if (!user || String(user.role) !== "Admin") { 86 | throw new Error("Not_admin"); 87 | } 88 | 89 | return next(); 90 | } catch (error) { 91 | let type = ""; 92 | let getMessage = ""; 93 | switch (error.message) { 94 | case "Not_admin": 95 | getMessage = "Admin access required"; 96 | type = "Forbidden"; 97 | break; 98 | default: 99 | getMessage = "Access denied"; 100 | type = "UNAUTH"; 101 | break; 102 | } 103 | res.status(403).send({ 104 | message: getMessage, 105 | type, 106 | success: false, 107 | data: null, 108 | }); 109 | } 110 | }, 111 | }; 112 | 113 | export default AuthMiddlewares; 114 | -------------------------------------------------------------------------------- /backend/middlewares/comment.js: -------------------------------------------------------------------------------- 1 | const CommentMiddlewares = { 2 | // Middleware for creating a comment 3 | createComment: (req, res, next) => { 4 | try { 5 | // Ask for postId and comment body 6 | const { postId } = req.params; 7 | const { body } = req.body; 8 | // If lack either, throw an error 9 | if (!postId) throw new Error("Please enter postId!"); 10 | if (!body) throw new Error("Please enter your comment!"); 11 | 12 | return next(); 13 | } catch (error) { 14 | res.status(400).send({ 15 | message: error.message, 16 | success: false, 17 | data: null, 18 | }); 19 | } 20 | }, 21 | updateComment: (req, res, next) => { 22 | try { 23 | const { body } = req.body; 24 | const { commentId } = req.params; 25 | 26 | if (!commentId) throw new Error("Please enter commentId on the params!"); 27 | if (!body) throw new Error("Please enter your comment!"); 28 | 29 | return next(); 30 | } catch (error) { 31 | res.status(400).send({ 32 | message: error.message, 33 | success: false, 34 | data: null, 35 | }); 36 | } 37 | }, 38 | getCommentsInAPost: (req, res, next) => { 39 | try { 40 | const { postId } = req.params; 41 | 42 | if (!postId) throw new Error("Please enter postId!"); 43 | 44 | return next(); 45 | } catch (error) { 46 | res.status(400).send({ 47 | message: error.message, 48 | success: false, 49 | data: null, 50 | }); 51 | } 52 | }, 53 | deleteComment: (req, res, next) => { 54 | try { 55 | const { commentId } = req.params; 56 | if (!commentId) throw new Error("Please enter commentId on the params!"); 57 | 58 | return next(); 59 | } catch (error) { 60 | res.status(400).send({ 61 | message: error.message, 62 | success: false, 63 | data: null, 64 | }); 65 | } 66 | }, 67 | }; 68 | 69 | export default CommentMiddlewares; 70 | -------------------------------------------------------------------------------- /backend/middlewares/game.js: -------------------------------------------------------------------------------- 1 | const GameMiddlewares = { 2 | createGame: (req, res, next) => { 3 | try { 4 | const { gameName, description } = req.body; 5 | 6 | if (!gameName) throw new Error("Please enter gameName"); 7 | if (!description) throw new Error("Please enter game description"); 8 | 9 | return next(); 10 | } catch (error) { 11 | res.status(400).send({ 12 | message: error.message, 13 | success: false, 14 | data: null, 15 | error, 16 | }); 17 | } 18 | }, 19 | getGameById: (req, res, next) => { 20 | try { 21 | const { gameId } = req.params; 22 | 23 | if (!gameId) throw new Error("Please enter gameId!"); 24 | 25 | return next(); 26 | } catch (error) { 27 | res.status(400).send({ 28 | message: error.message, 29 | success: false, 30 | data: null, 31 | error, 32 | }); 33 | } 34 | }, 35 | }; 36 | 37 | export default GameMiddlewares; 38 | -------------------------------------------------------------------------------- /backend/middlewares/post.js: -------------------------------------------------------------------------------- 1 | const PostMiddlewares = { 2 | // A middleware for creating a new post 3 | createPost: (req, res, next) => { 4 | try { 5 | const { title, body } = req.body; 6 | // Throw error if missing title or body 7 | if (!title) throw new Error("Please enter post title!"); 8 | if (!body) throw new Error("Please enter post body!"); 9 | 10 | return next(); 11 | } catch (error) { 12 | res.status(400).send({ 13 | message: error.message, 14 | success: false, 15 | data: null, 16 | error, 17 | }); 18 | } 19 | }, 20 | // A middleware for update post 21 | updatePost: (req, res, next) => { 22 | try { 23 | const { title, body } = req.body; 24 | const { postId } = req.params; 25 | const listFile = req.files; 26 | 27 | if (!postId) throw new Error("Please enter postId!"); 28 | // Throw error if the user doesn't update anything 29 | if (!title && !body && !listFile) { 30 | throw new Error("Please enter an updated field!"); 31 | } 32 | 33 | return next(); 34 | } catch (error) { 35 | res.status(400).send({ 36 | message: error.message, 37 | success: false, 38 | data: null, 39 | error, 40 | }); 41 | } 42 | }, 43 | getPostsByUser: (req, res, next) => { 44 | try { 45 | const { userId } = req.query; 46 | if (!userId) throw new Error("Please enter userId"); 47 | 48 | return next(); 49 | } catch (error) { 50 | res.status(400).send({ 51 | message: error.message, 52 | success: false, 53 | data: null, 54 | error, 55 | }); 56 | } 57 | }, 58 | getPostById: (req, res, next) => { 59 | try { 60 | const { postId } = req.query; 61 | if (!postId) throw new Error("Please enter postId"); 62 | 63 | return next(); 64 | } catch (error) { 65 | res.status(400).send({ 66 | message: error.message, 67 | success: false, 68 | data: null, 69 | error, 70 | }); 71 | } 72 | }, 73 | deletePost: (req, res, next) => { 74 | try { 75 | const { postId } = req.params; 76 | if (!postId) throw new Error("Please enter postId"); 77 | 78 | return next(); 79 | } catch (error) { 80 | res.status(400).send({ 81 | message: error.message, 82 | success: false, 83 | data: null, 84 | error, 85 | }); 86 | } 87 | }, 88 | }; 89 | 90 | export default PostMiddlewares; 91 | -------------------------------------------------------------------------------- /backend/middlewares/upload.js: -------------------------------------------------------------------------------- 1 | import multer from "multer"; 2 | 3 | const storage = multer.memoryStorage(); 4 | const upload = multer({ storage: storage }); 5 | 6 | export default upload; 7 | -------------------------------------------------------------------------------- /backend/middlewares/user.js: -------------------------------------------------------------------------------- 1 | const UserMiddlewares = { 2 | // A middleware for the create user function 3 | createUser: (req, res, next) => { 4 | try { 5 | // Get the information from the user 6 | const { userName, email, password } = req.body; 7 | 8 | // Give an error if missing any information 9 | if (!userName) throw new Error("Please enter userName!"); 10 | if (!email) throw new Error("Please enter email!"); 11 | if (!password) throw new Error("Please enter password!"); 12 | 13 | return next(); 14 | } catch (error) { 15 | res.status(400).send({ 16 | message: error.message, 17 | success: false, 18 | data: null, 19 | }); 20 | } 21 | }, 22 | // A middleware for the signing in user function 23 | signinUser: (req, res, next) => { 24 | try { 25 | // Get the information from the user 26 | const { email, password } = req.body; 27 | 28 | // Give an error if missing any information 29 | if (!email) throw new Error("Please enter email!"); 30 | if (!password) throw new Error("Please enter password!"); 31 | 32 | return next(); 33 | } catch (error) { 34 | res.status(400).send({ 35 | message: error.message, 36 | success: false, 37 | data: null, 38 | }); 39 | } 40 | }, 41 | updateProfile: (req, res, next) => { 42 | try { 43 | const { userName, email, bio, role } = req.body; 44 | const avatar = req.file; 45 | 46 | // If the user doesn't update anything 47 | if (!(userName || email || avatar || bio || role)) { 48 | throw new Error("Please enter an updated field!"); 49 | } 50 | 51 | return next(); 52 | } catch (error) { 53 | res.status(400).send({ 54 | message: error.message, 55 | success: false, 56 | data: null, 57 | }); 58 | } 59 | }, 60 | getUserInfo: (req, res, next) => { 61 | try { 62 | const { userId } = req.params; 63 | if (!userId) throw new Error("Please enter userId!"); 64 | 65 | return next(); 66 | } catch (error) { 67 | res.status(400).send({ 68 | message: error.message, 69 | success: false, 70 | data: null, 71 | }); 72 | } 73 | }, 74 | likeGame: (req, res, next) => { 75 | try { 76 | // Get the game name 77 | const { game } = req.body; 78 | // Give an error if missing any information 79 | if (!game) throw new Error("Please enter game!"); 80 | 81 | return next(); 82 | } catch (error) { 83 | res.status(400).send({ 84 | message: error.message, 85 | success: false, 86 | data: null, 87 | }); 88 | } 89 | }, 90 | unlikeGame: (req, res, next) => { 91 | try { 92 | // Get the game name 93 | const { game } = req.body; 94 | // Give an error if missing any information 95 | if (!game) throw new Error("Please enter game!"); 96 | 97 | return next(); 98 | } catch (error) { 99 | res.status(400).send({ 100 | message: error.message, 101 | success: false, 102 | data: null, 103 | }); 104 | } 105 | }, 106 | addInterest: (req, res, next) => { 107 | try { 108 | // Get the interest 109 | const { interest } = req.body; 110 | // Give an error if missing any information 111 | if (!interest) throw new Error("Please enter interest!"); 112 | 113 | return next(); 114 | } catch (error) { 115 | res.status(400).send({ 116 | message: error.message, 117 | success: false, 118 | data: null, 119 | }); 120 | } 121 | }, 122 | removeInterest: (req, res, next) => { 123 | try { 124 | // Get the interest 125 | const { interest } = req.body; 126 | // Give an error if missing any information 127 | if (!interest) throw new Error("Please enter interest!"); 128 | 129 | return next(); 130 | } catch (error) { 131 | res.status(400).send({ 132 | message: error.message, 133 | success: false, 134 | data: null, 135 | }); 136 | } 137 | }, 138 | }; 139 | 140 | export default UserMiddlewares; 141 | -------------------------------------------------------------------------------- /backend/models/comment.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import Collections from "../database/collections.js"; 3 | 4 | const CommentSchema = mongoose.Schema( 5 | { 6 | author: { 7 | type: mongoose.Schema.Types.ObjectId, 8 | required: true, 9 | ref: Collections.users, 10 | }, 11 | postId: { 12 | type: mongoose.Schema.Types.ObjectId, 13 | required: true, 14 | ref: Collections.posts, 15 | }, 16 | body: { 17 | type: String, 18 | required: true, 19 | }, 20 | likes: { 21 | type: Number, 22 | default: 0, 23 | }, 24 | dislikes: { 25 | type: Number, 26 | default: 0, 27 | }, 28 | isDelete: { 29 | type: Boolean, 30 | default: false, 31 | }, 32 | }, 33 | { 34 | timestamps: true, 35 | } 36 | ); 37 | 38 | const CommentModel = mongoose.model(Collections.comments, CommentSchema); 39 | export default CommentModel; 40 | -------------------------------------------------------------------------------- /backend/models/game.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import Collections from "../database/collections.js"; 3 | 4 | const GameSchema = mongoose.Schema( 5 | { 6 | gameName: { 7 | type: String, 8 | required: true, 9 | unique: true, 10 | }, 11 | description: { 12 | type: String, 13 | required: true, 14 | }, 15 | coverImage: { 16 | type: String, 17 | }, 18 | media: { 19 | type: [String], 20 | }, 21 | author: { 22 | type: mongoose.Schema.Types.ObjectId, 23 | required: true, 24 | ref: Collections.users, 25 | }, 26 | relatedLinks: { 27 | type: [String], 28 | }, 29 | }, 30 | { 31 | timestamps: true, 32 | } 33 | ); 34 | 35 | const GameModel = mongoose.model(Collections.games, GameSchema); 36 | export default GameModel; 37 | -------------------------------------------------------------------------------- /backend/models/post.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import Collections from "../database/collections.js"; 3 | 4 | const PostSchema = mongoose.Schema( 5 | { 6 | author: { 7 | type: mongoose.Schema.Types.ObjectId, 8 | required: true, 9 | ref: Collections.users, 10 | }, 11 | title: { 12 | type: String, 13 | required: true, 14 | }, 15 | body: { 16 | type: String, 17 | required: true, 18 | }, 19 | images: { 20 | type: [String], 21 | }, 22 | isDelete: { 23 | type: Boolean, 24 | default: false, 25 | }, 26 | }, 27 | { 28 | timestamps: true, 29 | } 30 | ); 31 | 32 | const PostModel = mongoose.model(Collections.posts, PostSchema); 33 | export default PostModel; 34 | -------------------------------------------------------------------------------- /backend/models/user.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import Collections from "../database/collections.js"; 3 | 4 | // Create a schema for the user 5 | const UserSchema = mongoose.Schema({ 6 | userName: { 7 | type: String, 8 | required: true, 9 | }, 10 | email: { 11 | type: String, 12 | unique: true, 13 | required: true, 14 | }, 15 | password: { 16 | type: String, 17 | required: true, 18 | }, 19 | avatar: { 20 | type: String, 21 | default: 22 | "https://static.vecteezy.com/system/resources/thumbnails/009/292/244/small_2x/default-avatar-icon-of-social-media-user-vector.jpg", 23 | }, 24 | bio: { 25 | type: String, 26 | default: "", 27 | }, 28 | role: { 29 | type: String, 30 | required: true, 31 | default: "User", 32 | }, 33 | likedGames: { 34 | type: [String], 35 | default: [], 36 | }, 37 | interests: { 38 | type: [String], 39 | default: [], 40 | }, 41 | }); 42 | 43 | // Creating a user model 44 | const UserModel = mongoose.model(Collections.users, UserSchema); 45 | 46 | export default UserModel; 47 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "dev": "nodemon ./index.js" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "bcrypt": "^5.1.1", 15 | "cloudinary": "^2.6.0", 16 | "cors": "^2.8.5", 17 | "dotenv": "^16.4.7", 18 | "express": "^4.21.2", 19 | "jsonwebtoken": "^9.0.2", 20 | "mongoose": "^8.10.0", 21 | "multer": "^1.4.5-lts.1", 22 | "nodemon": "^3.1.9" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/routes/index.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import AuthMiddlewares from "../middlewares/auth.js"; 3 | import { PublicRoute } from "./public.js"; 4 | import { PrivateRoute } from "./private.js"; 5 | 6 | // Create a root route and then branching to diff routes 7 | const RootRouteV1 = Router(); 8 | 9 | // Public routes 10 | RootRouteV1.use("/public", PublicRoute); 11 | 12 | // For every router after this, need to verify user first 13 | RootRouteV1.use(AuthMiddlewares.verifyAccessToken); 14 | 15 | RootRouteV1.use("/private", PrivateRoute); 16 | 17 | export { RootRouteV1 }; 18 | -------------------------------------------------------------------------------- /backend/routes/private.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import UserPrivateRoute from "./private/user.js"; 3 | import PostPrivateRoute from "./private/post.js"; 4 | import CommentPrivateRoute from "./private/comment.js"; 5 | import GamePrivateRoute from "./private/game.js"; 6 | 7 | const PrivateRoute = Router(); 8 | 9 | // User route 10 | PrivateRoute.use("/users", UserPrivateRoute); 11 | // Post route 12 | PrivateRoute.use("/posts", PostPrivateRoute); 13 | // Comment route 14 | PrivateRoute.use("/comments", CommentPrivateRoute); 15 | // Game route 16 | PrivateRoute.use("/games", GamePrivateRoute); 17 | 18 | export { PrivateRoute }; 19 | -------------------------------------------------------------------------------- /backend/routes/private/comment.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import CommentMiddlewares from "../../middlewares/comment.js"; 3 | import CommentControllers from "../../controllers/comment.js"; 4 | import AuthMiddlewares from "../../middlewares/auth.js"; 5 | 6 | const CommentPrivateRoute = Router(); 7 | 8 | // Create comment 9 | CommentPrivateRoute.post( 10 | "/create/:postId", 11 | CommentMiddlewares.createComment, 12 | CommentControllers.createComment 13 | ); 14 | 15 | CommentPrivateRoute.put( 16 | "/updateComment/:commentId", 17 | CommentMiddlewares.updateComment, 18 | CommentControllers.updateComment 19 | ); 20 | 21 | CommentPrivateRoute.delete( 22 | "/delete/:commentId", 23 | CommentMiddlewares.deleteComment, 24 | CommentControllers.deleteComment 25 | ); 26 | 27 | CommentPrivateRoute.get( 28 | "/admin/getAll", 29 | AuthMiddlewares.verifyAdmin, 30 | CommentControllers.getAllComment 31 | ); 32 | 33 | CommentPrivateRoute.get( 34 | "/admin/get/postId", 35 | AuthMiddlewares.verifyAdmin, 36 | CommentMiddlewares.getCommentsInAPost, 37 | CommentControllers.getCommentsInAPost 38 | ); 39 | 40 | export default CommentPrivateRoute; 41 | -------------------------------------------------------------------------------- /backend/routes/private/game.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import AuthMiddlewares from "../../middlewares/auth.js"; 3 | import GameMiddlewares from "../../middlewares/game.js"; 4 | import GameControllers from "../../controllers/game.js"; 5 | import upload from "../../middlewares/upload.js"; 6 | 7 | const GamePrivateRoute = Router(); 8 | 9 | GamePrivateRoute.post( 10 | "/create", 11 | upload.fields([ 12 | { name: "coverImage", maxCount: 1 }, 13 | { name: "media", maxCount: 10 }, 14 | ]), 15 | AuthMiddlewares.verifyAdmin, 16 | GameMiddlewares.createGame, 17 | GameControllers.createGame 18 | ); 19 | 20 | export default GamePrivateRoute; 21 | -------------------------------------------------------------------------------- /backend/routes/private/post.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import PostMiddlewares from "../../middlewares/post.js"; 3 | import PostControllers from "../../controllers/post.js"; 4 | import upload from "../../middlewares/upload.js"; 5 | import AuthMiddlewares from "../../middlewares/auth.js"; 6 | 7 | const PostPrivateRoute = Router(); 8 | 9 | // Create post route 10 | PostPrivateRoute.post( 11 | "/create", 12 | upload.array("images"), 13 | PostMiddlewares.createPost, 14 | PostControllers.createPost 15 | ); 16 | 17 | PostPrivateRoute.put( 18 | "/updatePost/:postId", 19 | upload.array("images"), 20 | PostMiddlewares.updatePost, 21 | PostControllers.updatePost 22 | ); 23 | 24 | PostPrivateRoute.delete( 25 | "/delete/:postId", 26 | PostMiddlewares.deletePost, 27 | PostControllers.deletePost 28 | ); 29 | 30 | // Get all posts 31 | PostPrivateRoute.get( 32 | "/admin/getAll", 33 | AuthMiddlewares.verifyAdmin, 34 | PostControllers.getAllPosts 35 | ); 36 | 37 | PostPrivateRoute.get( 38 | "/admin/get", 39 | AuthMiddlewares.verifyAdmin, 40 | (req, res, next) => { 41 | // Get posts by userId 42 | if (req.query.userId) { 43 | return PostMiddlewares.getPostsByUser(req, res, () => 44 | PostControllers.getPostsByUser(req, res) 45 | ); 46 | } 47 | // Get post by postId 48 | if (req.query.postId) { 49 | return PostMiddlewares.getPostById(req, res, () => 50 | PostControllers.getPostById(req, res) 51 | ); 52 | } 53 | 54 | res.status(400).json({ 55 | success: false, 56 | message: "Missing query: please provide either userId or postId", 57 | }); 58 | } 59 | ); 60 | 61 | export default PostPrivateRoute; 62 | -------------------------------------------------------------------------------- /backend/routes/private/user.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import UserMiddlewares from "../../middlewares/user.js"; 3 | import UserControllers from "../../controllers/user.js"; 4 | import AuthMiddlewares from "../../middlewares/auth.js"; 5 | import upload from "../../middlewares/upload.js"; 6 | 7 | const UserPrivateRoute = Router(); 8 | 9 | UserPrivateRoute.put( 10 | "/updateProfile", 11 | upload.single("avatar"), 12 | AuthMiddlewares.verifyAccessToken, 13 | UserMiddlewares.updateProfile, 14 | UserControllers.updateProfile 15 | ); 16 | 17 | UserPrivateRoute.post( 18 | "/likeGame", 19 | AuthMiddlewares.verifyAccessToken, 20 | UserMiddlewares.likeGame, 21 | UserControllers.likeGame 22 | ); 23 | 24 | UserPrivateRoute.post( 25 | "/addInterest", 26 | AuthMiddlewares.verifyAccessToken, 27 | UserMiddlewares.addInterest, 28 | UserControllers.addInterest 29 | ); 30 | 31 | UserPrivateRoute.delete( 32 | "/unlikeGame", 33 | AuthMiddlewares.verifyAccessToken, 34 | UserMiddlewares.unlikeGame, 35 | UserControllers.unlikeGame 36 | ); 37 | 38 | UserPrivateRoute.delete( 39 | "/removeInterest", 40 | AuthMiddlewares.verifyAccessToken, 41 | UserMiddlewares.removeInterest, 42 | UserControllers.removeInterest 43 | ); 44 | 45 | export default UserPrivateRoute; 46 | -------------------------------------------------------------------------------- /backend/routes/public.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import UserPublicRoute from "./public/user.js"; 3 | import GamePublicRoute from "./public/game.js"; 4 | import CommentPublicRoute from "./public/comment.js"; 5 | import PostPublicRoute from "./public/post.js"; 6 | 7 | const PublicRoute = Router(); 8 | // User route 9 | PublicRoute.use("/users", UserPublicRoute); 10 | // Post route 11 | PublicRoute.use("/posts", PostPublicRoute); 12 | // Comment route 13 | PublicRoute.use("/comments", CommentPublicRoute); 14 | // Game route 15 | PublicRoute.use("/games", GamePublicRoute); 16 | 17 | export { PublicRoute }; 18 | -------------------------------------------------------------------------------- /backend/routes/public/comment.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import CommentControllers from "../../controllers/comment.js"; 3 | import CommentMiddlewares from "../../middlewares/comment.js"; 4 | 5 | const CommentPublicRoute = Router(); 6 | // Get all comments 7 | CommentPublicRoute.get("/getAll", CommentControllers.getAllComment); 8 | // Get all comments in a post 9 | CommentPublicRoute.get( 10 | "/get/:postId", 11 | CommentMiddlewares.getCommentsInAPost, 12 | CommentControllers.getCommentsInAPost 13 | ); 14 | export default CommentPublicRoute; 15 | -------------------------------------------------------------------------------- /backend/routes/public/game.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import GameControllers from "../../controllers/game.js"; 3 | import GameMiddlewares from "../../middlewares/game.js"; 4 | 5 | const GamePublicRoute = Router(); 6 | // Get all the games 7 | GamePublicRoute.get("/get", GameControllers.getAllGames); 8 | // Get game by its ID 9 | GamePublicRoute.get( 10 | "/get/:gameId", 11 | GameMiddlewares.getGameById, 12 | GameControllers.getGameById 13 | ); 14 | 15 | export default GamePublicRoute; 16 | -------------------------------------------------------------------------------- /backend/routes/public/post.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import PostMiddlewares from "../../middlewares/post.js"; 3 | import PostControllers from "../../controllers/post.js"; 4 | 5 | const PostPublicRoute = Router(); 6 | 7 | // Get all posts 8 | PostPublicRoute.get("/getAll", PostControllers.getAllPosts); 9 | 10 | PostPublicRoute.get("/get", (req, res, next) => { 11 | // Get posts by userId 12 | if (req.query.userId) { 13 | return PostMiddlewares.getPostsByUser(req, res, () => 14 | PostControllers.getPostsByUser(req, res) 15 | ); 16 | } 17 | // Get post by postId 18 | if (req.query.postId) { 19 | return PostMiddlewares.getPostById(req, res, () => 20 | PostControllers.getPostById(req, res) 21 | ); 22 | } 23 | 24 | res.status(400).json({ 25 | success: false, 26 | message: "Missing query: please provide either userId or postId", 27 | }); 28 | }); 29 | 30 | export default PostPublicRoute; 31 | -------------------------------------------------------------------------------- /backend/routes/public/user.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import UserMiddlewares from "../../middlewares/user.js"; 3 | import UserControllers from "../../controllers/user.js"; 4 | import upload from "../../middlewares/upload.js"; 5 | 6 | const UserPublicRoute = Router(); 7 | 8 | // Create user route 9 | UserPublicRoute.post( 10 | "/create", 11 | upload.single("avatar"), 12 | UserMiddlewares.createUser, 13 | UserControllers.createUser 14 | ); 15 | 16 | UserPublicRoute.post( 17 | "/signin", 18 | UserMiddlewares.signinUser, 19 | UserControllers.signinUser 20 | ); 21 | 22 | // Get user info 23 | UserPublicRoute.get( 24 | "/get/:userId", 25 | UserMiddlewares.getUserInfo, 26 | UserControllers.getUserInfo 27 | ); 28 | 29 | export default UserPublicRoute; 30 | -------------------------------------------------------------------------------- /backend/utils/authorize.js: -------------------------------------------------------------------------------- 1 | const authorizeUser = (userId, authorId) => { 2 | try { 3 | if (String(userId) !== String(authorId)) throw new Error("Access denied!"); 4 | return { success: true }; 5 | } catch (error) { 6 | return { success: false, message: error.message, data: null }; 7 | } 8 | }; 9 | 10 | export { authorizeUser }; 11 | -------------------------------------------------------------------------------- /backend/utils/token.js: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken"; 2 | import { AT_SECRET_KEY, RT_SECRET_KEY } from "../config.js"; 3 | 4 | const generateToken = (document, type) => { 5 | // If access token, take the first argument, 6 | // Else if refresh token, take the second argument 7 | const getSecretKey = type === "AT" ? AT_SECRET_KEY : RT_SECRET_KEY; 8 | const getExpiration = type === "AT" ? 300 : 3600 * 24 * 7; 9 | const token = jwt.sign(document, getSecretKey, { 10 | expiresIn: getExpiration, 11 | }); 12 | return token; 13 | }; 14 | 15 | const verifyToken = (token, type) => { 16 | const getSecretKey = type === "AT" ? AT_SECRET_KEY : RT_SECRET_KEY; 17 | const verifyToken = jwt.verify(token, getSecretKey); 18 | return verifyToken; 19 | }; 20 | 21 | export { generateToken, verifyToken }; 22 | -------------------------------------------------------------------------------- /backend/utils/upload.js: -------------------------------------------------------------------------------- 1 | import { v2 as cloudinary } from "cloudinary"; 2 | 3 | const handleFileUpload = async (file) => { 4 | try { 5 | const dataUrl = `data:${file.mimetype};base64,${file.buffer.toString( 6 | "base64" 7 | )}`; 8 | const fileName = file.originalname.split(".")[0]; 9 | let data; 10 | await cloudinary.uploader.upload( 11 | dataUrl, 12 | { 13 | public_id: fileName, 14 | resource_type: "auto", 15 | }, 16 | (err, result) => { 17 | if (err) { 18 | // Handle cloudinary upload error 19 | console.error("Cloudinary upload error:", err); 20 | throw new Error("Failed to upload file!"); 21 | } 22 | data = result.secure_url; 23 | } 24 | ); 25 | return { data: data, success: true }; 26 | } catch (error) { 27 | return { success: false, message: error.message, data: null }; 28 | } 29 | }; 30 | 31 | export { handleFileUpload }; 32 | -------------------------------------------------------------------------------- /minigame-frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = crlf 10 | charset = utf-8 11 | trim_trailing_whitespace = false 12 | insert_final_newline = false -------------------------------------------------------------------------------- /minigame-frontend/.env: -------------------------------------------------------------------------------- 1 | VITE_APP_SERVER_URL = 2 | -------------------------------------------------------------------------------- /minigame-frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /minigame-frontend/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/acm-uic/WebMinigames/e1e7199b4aaa16171a4388c5265242a7a5743f22/minigame-frontend/README.md -------------------------------------------------------------------------------- /minigame-frontend/eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import globals from 'globals' 3 | import react from 'eslint-plugin-react' 4 | import reactHooks from 'eslint-plugin-react-hooks' 5 | import reactRefresh from 'eslint-plugin-react-refresh' 6 | 7 | export default [ 8 | { ignores: ['dist'] }, 9 | { 10 | files: ['**/*.{js,jsx}'], 11 | languageOptions: { 12 | ecmaVersion: 2020, 13 | globals: globals.browser, 14 | parserOptions: { 15 | ecmaVersion: 'latest', 16 | ecmaFeatures: { jsx: true }, 17 | sourceType: 'module', 18 | }, 19 | }, 20 | settings: { react: { version: '18.3' } }, 21 | plugins: { 22 | react, 23 | 'react-hooks': reactHooks, 24 | 'react-refresh': reactRefresh, 25 | }, 26 | rules: { 27 | ...js.configs.recommended.rules, 28 | ...react.configs.recommended.rules, 29 | ...react.configs['jsx-runtime'].rules, 30 | ...reactHooks.configs.recommended.rules, 31 | 'react/jsx-no-target-blank': 'off', 32 | 'react/prop-types':'off', 33 | 'react-refresh/only-export-components': [ 34 | 'warn', 35 | { allowConstantExport: true }, 36 | ], 37 | }, 38 | }, 39 | ] 40 | -------------------------------------------------------------------------------- /minigame-frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 |{comment}
53 |8 | Publisher: {game.publisher} 9 |
10 |11 | Developer: {game.developer} 12 |
13 |14 | Release Date: {game.releaseDate} 15 |
16 |PLAY NOW:
20 |{game.gameName}
9 |{player.rank}
6 |{player.name}
7 |{player.points}
13 |63 | {post.body} 64 |
65 |{user.bio}
32 |{user.bio}
53 |{item}
41 |{item}
55 |Biography
7 |{bio}
8 |{props.text}
8 |Add an image/video
505 |512 | {fileError || ""} 513 |
514 |40 | {editorError || ""} 41 |
42 |