From 8dffb77005884f724b036f308c38a8355b18dbc8 Mon Sep 17 00:00:00 2001 From: Enes Furkan Arslan Date: Thu, 7 Dec 2023 18:39:59 +0300 Subject: [PATCH 001/281] UI of not moderator UI of moderation page is implemented for users that are not currently moderators --- .../Pages/Moderation/Moderation.module.css | 33 ++++++++++++++++- .../frontend/src/Pages/Moderation/index.jsx | 35 +++++++++++++++---- 2 files changed, 60 insertions(+), 8 deletions(-) diff --git a/prediction-polls/frontend/src/Pages/Moderation/Moderation.module.css b/prediction-polls/frontend/src/Pages/Moderation/Moderation.module.css index a8398eb3..e68cf751 100644 --- a/prediction-polls/frontend/src/Pages/Moderation/Moderation.module.css +++ b/prediction-polls/frontend/src/Pages/Moderation/Moderation.module.css @@ -7,4 +7,35 @@ .page{ flex-direction: column; } -} \ No newline at end of file +} +.btn { + display: block; + margin: auto; + } + .questionCard { + display: flex; + flex-direction: row; + justify-content: space-between; + width: 95%; + box-shadow: 2px 4px 8px var(--neutral-shadow); + padding: 30px; + box-sizing: border-box; + border-radius: 8px; + gap: 20px; + background-color: var(--neutral-pollCard); + margin-bottom: 20px; + margin-top: 50px; + margin-left: 50px; + max-width: 800px; + max-height: 200px; + text-align: center; + } + +.text { + font-size: 20px; + font-weight: 700; + width: 75%; + color: var(--neutral-black); + text-align: center; + margin:auto; +} diff --git a/prediction-polls/frontend/src/Pages/Moderation/index.jsx b/prediction-polls/frontend/src/Pages/Moderation/index.jsx index 6066e2c9..eccab8b5 100644 --- a/prediction-polls/frontend/src/Pages/Moderation/index.jsx +++ b/prediction-polls/frontend/src/Pages/Moderation/index.jsx @@ -1,14 +1,35 @@ -import React from 'react' -import Menu from '../../Components/Menu' -import styles from './Moderation.module.css' +// Moderation.js +import React from 'react'; +import Menu from '../../Components/Menu'; +import styles from './Moderation.module.css'; +import { Button } from 'antd'; function Moderation() { + const isModerator = false; + + const handleBecomeModerator = () => { + console.log('User wants to become a moderator'); + }; + return (
- -

Moderation Page

-
- ) + + {isModerator ? ( +
You are already a moderator.
+ ) : ( + <> +
+

Would you like to apply to become a moderator?

+ +
+ + + + )} + + ); } export default Moderation; \ No newline at end of file From 0cd70b807a6f9f95219aacf599707cbf2c01965b Mon Sep 17 00:00:00 2001 From: EmreBatuhan <93476131+EmreBatuhan@users.noreply.github.com> Date: Thu, 7 Dec 2023 19:50:31 +0300 Subject: [PATCH 002/281] Add profile adaptation to google auth --- .../backend/src/services/AuthGoogleService.js | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/prediction-polls/backend/src/services/AuthGoogleService.js b/prediction-polls/backend/src/services/AuthGoogleService.js index d6a13815..7728b4f1 100644 --- a/prediction-polls/backend/src/services/AuthGoogleService.js +++ b/prediction-polls/backend/src/services/AuthGoogleService.js @@ -41,12 +41,26 @@ async function googleLogInWithCode(code,res){ const generatedPassword = generateRandomPassword(12); const { error, userId} = await db.addUser(googleUser.given_name,generatedPassword,googleUser.email,null); - const result = await profileDb.addProfile(userId,googleUser.given_name,googleUser.email); - if(!result.profileId){ - return res.status(400).send({error:errorCodes.REGISTRATION_FAILED}); + let user = {}; + + if(!error){ + console.log("Fourth") + + const result = await profileDb.addProfile(userId,googleUser.given_name,googleUser.email); + if(!result.profileId){ + return res.status(400).send({error:errorCodes.REGISTRATION_FAILED}); + } + + user = {name : googleUser.given_name, id: userId}; } + else{ + const new_user = await db.findUser({email:googleUser.email}) + console.log("Fifth",new_user) + + user = {name : new_user.username, id: new_user.id}; + } + - const user = {name : googleUser.given_name, id: userId}; const accesToken = generateAccessToken(user); const refreshToken = generateRefreshToken(user); db.addRefreshToken(refreshToken); From c67846135c0de075de1b90433fd466b63301eaca Mon Sep 17 00:00:00 2001 From: EmreBatuhan <93476131+EmreBatuhan@users.noreply.github.com> Date: Thu, 7 Dec 2023 20:02:29 +0300 Subject: [PATCH 003/281] Restructured google login for both methods --- .../backend/src/services/AuthGoogleService.js | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/prediction-polls/backend/src/services/AuthGoogleService.js b/prediction-polls/backend/src/services/AuthGoogleService.js index 7728b4f1..5decd2bc 100644 --- a/prediction-polls/backend/src/services/AuthGoogleService.js +++ b/prediction-polls/backend/src/services/AuthGoogleService.js @@ -38,13 +38,17 @@ async function googleLogInWithCode(code,res){ return res.status(403).json({error:errorCodes.GOOGLE_LOGIN_NONVERIFIED_GOOGLE_ACCOUNT}); } - const generatedPassword = generateRandomPassword(12); - const { error, userId} = await db.addUser(googleUser.given_name,generatedPassword,googleUser.email,null); + const email_exists = await db.findUser({email:googleUser.email}) let user = {}; - if(!error){ - console.log("Fourth") + if (email_exists.error){ + //This means that the given email is not in use so we will create a new user + const generatedPassword = generateRandomPassword(12); + const {error, userId} = await db.addUser(googleUser.given_name,generatedPassword,googleUser.email,null); + if(error){ + return res.status(400).send({error:errorCodes.REGISTRATION_FAILED}); + } const result = await profileDb.addProfile(userId,googleUser.given_name,googleUser.email); if(!result.profileId){ @@ -54,13 +58,10 @@ async function googleLogInWithCode(code,res){ user = {name : googleUser.given_name, id: userId}; } else{ - const new_user = await db.findUser({email:googleUser.email}) - console.log("Fifth",new_user) - - user = {name : new_user.username, id: new_user.id}; + // The given email was found so we will use that user + user = {name : email_exists.username, id: email_exists.id}; } - const accesToken = generateAccessToken(user); const refreshToken = generateRefreshToken(user); db.addRefreshToken(refreshToken); @@ -80,15 +81,31 @@ async function googleLogInWithCode(code,res){ return res.status(403).json({error:errorCodes.GOOGLE_LOGIN_NONVERIFIED_GOOGLE_ACCOUNT}); } - const generatedPassword = generateRandomPassword(12); - const {error, userId} = await db.addUser(googleUser.given_name,generatedPassword,googleUser.email,null); - const result = await profileDb.addProfile(userId,googleUser.given_name,googleUser.email); - if(!result.profileId){ - return res.status(400).send({error:errorCodes.REGISTRATION_FAILED}); - } + const email_exists = await db.findUser({email:googleUser.email}) + + let user = {}; + + if (email_exists.error){ + //This means that the given email is not in use so we will create a new user + const generatedPassword = generateRandomPassword(12); + const {error, userId} = await db.addUser(googleUser.given_name,generatedPassword,googleUser.email,null); + if(error){ + return res.status(400).send({error:errorCodes.REGISTRATION_FAILED}); + } + // Implement rollback when fail + const result = await profileDb.addProfile(userId,googleUser.given_name,googleUser.email); + if(!result.profileId){ + return res.status(400).send({error:errorCodes.REGISTRATION_FAILED}); + } - const user = {name : googleUser.given_name, id: userId}; + user = {name : googleUser.given_name, id: userId}; + } + else{ + // The given email was found so we will use that user + user = {name : email_exists.username, id: email_exists.id}; + } + const accesToken = generateAccessToken(user); const refreshToken = generateRefreshToken(user); From 6f8806d45260001474c0e355a86b17239a017c56 Mon Sep 17 00:00:00 2001 From: EmreBatuhan <93476131+EmreBatuhan@users.noreply.github.com> Date: Thu, 7 Dec 2023 20:25:41 +0300 Subject: [PATCH 004/281] Return username --- prediction-polls/backend/src/services/AuthGoogleService.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/prediction-polls/backend/src/services/AuthGoogleService.js b/prediction-polls/backend/src/services/AuthGoogleService.js index 5decd2bc..a4bb9078 100644 --- a/prediction-polls/backend/src/services/AuthGoogleService.js +++ b/prediction-polls/backend/src/services/AuthGoogleService.js @@ -65,7 +65,7 @@ async function googleLogInWithCode(code,res){ const accesToken = generateAccessToken(user); const refreshToken = generateRefreshToken(user); db.addRefreshToken(refreshToken); - res.json({accessToken: accesToken, refreshToken: refreshToken}) + res.json({accessToken: accesToken, refreshToken: refreshToken, username:user.name}) } catch (error) { return res.status(500).json({error:errorCodes.GOOGLE_LOGIN_FAILED}); @@ -105,12 +105,12 @@ async function googleLogInWithCode(code,res){ // The given email was found so we will use that user user = {name : email_exists.username, id: email_exists.id}; } - + const accesToken = generateAccessToken(user); const refreshToken = generateRefreshToken(user); db.addRefreshToken(refreshToken); - res.json({accessToken: accesToken, refreshToken: refreshToken}) + res.json({accessToken: accesToken, refreshToken: refreshToken, username:user.name}) } catch (error) { return res.status(500).json({error:errorCodes.GOOGLE_LOGIN_FAILED}); From a4566ab80a5d8cd822a4077fdb31b9eec7b5acc8 Mon Sep 17 00:00:00 2001 From: kutaysaran <74209499+kutaysaran@users.noreply.github.com> Date: Thu, 7 Dec 2023 21:03:22 +0300 Subject: [PATCH 005/281] Google auth fix --- .../frontend/src/Pages/Auth/Google/index.jsx | 33 +++++++++++-------- .../frontend/src/api/requests/googleLogin.jsx | 3 +- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/prediction-polls/frontend/src/Pages/Auth/Google/index.jsx b/prediction-polls/frontend/src/Pages/Auth/Google/index.jsx index d2b0f8a9..b225dcb8 100644 --- a/prediction-polls/frontend/src/Pages/Auth/Google/index.jsx +++ b/prediction-polls/frontend/src/Pages/Auth/Google/index.jsx @@ -7,19 +7,26 @@ function GoogleLogin() { const location = useLocation(); useEffect(() => { - const code = new URLSearchParams(location.search).get('code'); - if (code) { - googleLogin(code).then(success => { - if (success) { - // Redirect to profile if login was successful - navigate('/profile'); - } else { - // Redirect to a login error page or back to login - navigate('/auth/sign-in'); - } - }); + const code = new URLSearchParams(location.search).get('code'); + + const handleLogin = async () => { + if (code) { + try { + const result = await googleLogin(code); + if (result.success) { + navigate(`/profile/${result.username}`); + } else { + navigate('/auth/sign-in'); + } + } catch (error) { + navigate('/auth/sign-in'); + } } - }, [location.search, navigate]); + }; + + handleLogin(); +}, [location.search, navigate]); + return (
@@ -28,4 +35,4 @@ function GoogleLogin() { ); } -export default GoogleLogin; +export default GoogleLogin; \ No newline at end of file diff --git a/prediction-polls/frontend/src/api/requests/googleLogin.jsx b/prediction-polls/frontend/src/api/requests/googleLogin.jsx index a6cf6e3f..66fd73c5 100644 --- a/prediction-polls/frontend/src/api/requests/googleLogin.jsx +++ b/prediction-polls/frontend/src/api/requests/googleLogin.jsx @@ -10,7 +10,8 @@ async function googleLogin(code) { if (response.status === 200 && data.accessToken && data.refreshToken) { localStorage.setItem('accessToken', data.accessToken); localStorage.setItem('refreshToken', data.refreshToken); - return { success: true }; + localStorage.setItem('username', data.username); + return { success: true, username: data.username }; } else { return { success: false }; } From 9c08965a3195e9073b30c308e0f92aa1be9c0dad Mon Sep 17 00:00:00 2001 From: EmreBatuhan <93476131+EmreBatuhan@users.noreply.github.com> Date: Thu, 7 Dec 2023 22:26:54 +0300 Subject: [PATCH 006/281] Add new endpoint for badge selection update --- .../backend/src/routes/ProfileRouter.js | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/prediction-polls/backend/src/routes/ProfileRouter.js b/prediction-polls/backend/src/routes/ProfileRouter.js index dda8b23f..492b68ae 100644 --- a/prediction-polls/backend/src/routes/ProfileRouter.js +++ b/prediction-polls/backend/src/routes/ProfileRouter.js @@ -251,6 +251,52 @@ router.post("/profilePhoto",authenticator.authorizeAccessToken,upload.single('im */ router.patch('/', service.updateProfile); +/** + * @swagger + * /profiles/badges: + * patch: + * tags: + * - profiles + * description: Update a badge selection mode. Authorization is required. + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * badgeId: + * type: integer + * isSelected: + * type: boolean + * responses: + * 200: + * description: Badge updated successfully + * content: + * application/json: + * type: object + * properties: + * status: string + * 400: + * description: Bad Request + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * code: + * type: integer + * examples: + * INSUFFICIENT_DATA: + * value: + * error: + * code: 1007, + * message: Given data is not sufficient. Please follow guidelines. + */ +router.patch('/badges',authenticator.authorizeAccessToken,service.updateBadge) + module.exports = router; \ No newline at end of file From 9c9a71273652847ce417ee66baf4b4afc24e53b0 Mon Sep 17 00:00:00 2001 From: EmreBatuhan <93476131+EmreBatuhan@users.noreply.github.com> Date: Thu, 7 Dec 2023 22:27:16 +0300 Subject: [PATCH 007/281] Handle badge selection update logic --- .../backend/src/services/ProfileService.js | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/prediction-polls/backend/src/services/ProfileService.js b/prediction-polls/backend/src/services/ProfileService.js index b9b01fb0..7e3aacfc 100644 --- a/prediction-polls/backend/src/services/ProfileService.js +++ b/prediction-polls/backend/src/services/ProfileService.js @@ -3,6 +3,7 @@ const authDb = require("../repositories/AuthorizationDB.js"); const crypto = require('crypto'); const aws = require('aws-sdk'); const { GetObjectCommand } = require('@aws-sdk/client-s3'); +const errorCodes = require("../errorCodes.js"); const generateFileName = (bytes = 32) => crypto.randomBytes(bytes).toString('hex'); @@ -184,4 +185,32 @@ async function updateProfile(req,res){ } } -module.exports = {getProfile,getProfileWithProfileId,getMyProfile,updateProfile,uploadImagetoS3} \ No newline at end of file +async function updateBadge(req,res){ + const userId = req.user.id; + const {badgeId,isSelected} = req.body; + + if(badgeId == undefined || isSelected == undefined){ + return res.status(400).json({error:errorCodes.INSUFFICIENT_DATA}); + } + + try{ + const {badges,error} = await db.getBadges(userId); + if(error){ + throw error; + } + for (let i = 0; i < badges.length; i++) { + if(badgeId == badges[i].id){ + const badge_result = await db.updateBadge(badgeId,isSelected); + if(badge_result.error){ + throw errorCodes.DATABASE_ERROR; + } + return res.status(200).json({status:"success"}); + } + } + throw errorCodes.USER_DOES_NOT_HAVE_GIVEN_BADGE; + }catch(error){ + return res.status(400).json({error:error}); + } +} + +module.exports = {getProfile,getProfileWithProfileId,getMyProfile,updateProfile,uploadImagetoS3,updateBadge} \ No newline at end of file From 21ae3c1b133ce64b9fadf37a3a697aa8e7f1ae27 Mon Sep 17 00:00:00 2001 From: EmreBatuhan <93476131+EmreBatuhan@users.noreply.github.com> Date: Thu, 7 Dec 2023 22:27:24 +0300 Subject: [PATCH 008/281] Update ProfileDB.js --- .../backend/src/repositories/ProfileDB.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/prediction-polls/backend/src/repositories/ProfileDB.js b/prediction-polls/backend/src/repositories/ProfileDB.js index 459ab60c..4f08bd80 100644 --- a/prediction-polls/backend/src/repositories/ProfileDB.js +++ b/prediction-polls/backend/src/repositories/ProfileDB.js @@ -115,7 +115,7 @@ async function getBadges(userId){ try { const [rows] = await pool.query(sql, [userId]); - const badges = rows.map(badge => {return {topic:badge.topic,rank:badge.userRank}}) + const badges = rows.map(badge => {return {id:badge.id,topic:badge.topic,rank:badge.userRank,isSelected:badge.isSelected}}) return {badges:badges}; } catch (error) { @@ -123,6 +123,19 @@ async function getBadges(userId){ } } +async function updateBadge(badgeId,IsSelected){ + const sql = 'UPDATE badges SET isSelected = ? WHERE id = ?'; + + try { + const [resultSetHeader] = await pool.query(sql, [IsSelected, badgeId]); + + return {status:"success"}; + + } catch (error) { + return {error:errorCodes.DATABASE_ERROR}; + } +} + async function updatePoints(userId,additional_points){ const find_points_sql = 'SELECT * FROM profiles WHERE userId= ?'; @@ -146,5 +159,5 @@ async function updatePoints(userId,additional_points){ } -module.exports = {getProfileWithProfileId,getProfileWithUserId,addProfile,updateProfile,getBadges,updatePoints} +module.exports = {getProfileWithProfileId,getProfileWithUserId,addProfile,updateProfile,getBadges,updateBadge,updatePoints} \ No newline at end of file From dbd09a29bb76bcd5cb40598b6f69d180f22114e7 Mon Sep 17 00:00:00 2001 From: EmreBatuhan <93476131+EmreBatuhan@users.noreply.github.com> Date: Thu, 7 Dec 2023 22:27:30 +0300 Subject: [PATCH 009/281] Update schema.sql --- prediction-polls/backend/src/schema.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/prediction-polls/backend/src/schema.sql b/prediction-polls/backend/src/schema.sql index 32b4b3e2..b4f0a108 100644 --- a/prediction-polls/backend/src/schema.sql +++ b/prediction-polls/backend/src/schema.sql @@ -93,6 +93,7 @@ CREATE TABLE badges ( id INT AUTO_INCREMENT PRIMARY KEY, userRank INT NOT NULL, topic VARCHAR(255) NOT NULL, + isSelected BOOLEAN DEFAULT False, userId INT, FOREIGN KEY (userId) REFERENCES users(id) ON DELETE SET NULL ); From 9d70087e6307569df4b56c339939d95a93d6288a Mon Sep 17 00:00:00 2001 From: EmreBatuhan <93476131+EmreBatuhan@users.noreply.github.com> Date: Thu, 7 Dec 2023 22:27:40 +0300 Subject: [PATCH 010/281] Add new errors --- prediction-polls/backend/src/errorCodes.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/prediction-polls/backend/src/errorCodes.js b/prediction-polls/backend/src/errorCodes.js index 8a6b09a4..f3fbb890 100644 --- a/prediction-polls/backend/src/errorCodes.js +++ b/prediction-polls/backend/src/errorCodes.js @@ -22,7 +22,7 @@ const errorCodes = { ACCESS_TOKEN_NEEDED_ERROR: { code: 1004, - message: 'A access token is needed', + message: 'An access token is needed', }, REFRESH_TOKEN_NEEDED_ERROR: { @@ -35,6 +35,11 @@ const errorCodes = { message: 'Registration failed' }, + INSUFFICIENT_DATA: { + code: 1007, + message: 'Given data is not sufficient. Please follow guidelines.' + }, + USER_NOT_FOUND: { code: 2000, message: 'User not found.', @@ -150,6 +155,11 @@ const errorCodes = { message: 'User has to put points to be able to vote polls' }, + USER_DOES_NOT_HAVE_GIVEN_BADGE: { + code: 3008, + message: 'User does not have a badge with the given badge Id' + }, + BAD_DISCRETE_POLL_REQUEST_ERROR: { code: 4000, message: 'Bad request body for creating a discrete poll.' From 0e340a9d88925716ae47700d15f80001227ad791 Mon Sep 17 00:00:00 2001 From: kutaysaran <74209499+kutaysaran@users.noreply.github.com> Date: Fri, 8 Dec 2023 17:44:14 +0300 Subject: [PATCH 011/281] Token Management --- .../PrivateRoute/PrivateRoute.test.js | 44 +++++++++++++++ .../src/Components/PrivateRoute/index.jsx | 53 +++++++++++++++++-- .../frontend/src/Routes/Router.jsx | 17 +++--- 3 files changed, 101 insertions(+), 13 deletions(-) create mode 100644 prediction-polls/frontend/src/Components/PrivateRoute/PrivateRoute.test.js diff --git a/prediction-polls/frontend/src/Components/PrivateRoute/PrivateRoute.test.js b/prediction-polls/frontend/src/Components/PrivateRoute/PrivateRoute.test.js new file mode 100644 index 00000000..32424a7f --- /dev/null +++ b/prediction-polls/frontend/src/Components/PrivateRoute/PrivateRoute.test.js @@ -0,0 +1,44 @@ +import { render, screen, waitFor } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import { BrowserRouter as Router } from 'react-router-dom'; +import PrivateRoute from './PrivateRoute'; // adjust the import path as needed + +// Mock child component +const MockChildComponent = () =>
Protected Content
; + +test("redirects to sign-in page if not authenticated", async () => { + // Remove token to simulate unauthenticated user + localStorage.removeItem('accessToken'); + + render( + + + + + + ); + + // Wait for any asynchronous operations in the component + await waitFor(() => { + expect(screen.queryByText("Protected Content")).not.toBeInTheDocument(); + }); +}); +test("renders child component if authenticated", async () => { + // Set a mock token to simulate authenticated user + localStorage.setItem('accessToken', 'mock-token'); + + render( + + + + + + ); + + await waitFor(() => { + expect(screen.getByText("Protected Content")).toBeInTheDocument(); + }); + + // Cleanup: remove the mock token + localStorage.removeItem('accessToken'); +}); diff --git a/prediction-polls/frontend/src/Components/PrivateRoute/index.jsx b/prediction-polls/frontend/src/Components/PrivateRoute/index.jsx index 7c3a1144..5fb2e492 100644 --- a/prediction-polls/frontend/src/Components/PrivateRoute/index.jsx +++ b/prediction-polls/frontend/src/Components/PrivateRoute/index.jsx @@ -1,8 +1,51 @@ -import React from 'react'; -import { Route, Navigate } from 'react-router-dom'; +// components/PrivateRoute.jsx +import React, { useState, useEffect } from 'react'; +import { Navigate } from 'react-router-dom'; const PrivateRoute = ({ children }) => { - const accessToken = localStorage.getItem('accessToken'); - return accessToken ? children : ; + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [isCheckingAuth, setIsCheckingAuth] = useState(true); + + async function refreshAccessToken(refreshToken) { + try { + const response = await fetch( + process.env.REACT_APP_BACKEND_LINK + "/auth/access-token", { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refreshToken }), + }); + const data = await response.json(); + if (data.accessToken) { + localStorage.setItem('accessToken', data.accessToken); + return data.accessToken; + } + } catch (error) { + console.error('Error refreshing access token:', error); + return null; + } +} + + useEffect(() => { + async function checkAuth() { + let accessToken = localStorage.getItem('accessToken'); + const refreshToken = localStorage.getItem('refreshToken'); + + if (!accessToken && refreshToken) { + accessToken = await refreshAccessToken(refreshToken); + } + + setIsAuthenticated(!!accessToken); + setIsCheckingAuth(false); + } + + checkAuth(); + }, []); + + if (isCheckingAuth) { + return
Loading...
; + } + + return isAuthenticated ? children : ; }; -export default PrivateRoute; \ No newline at end of file + +export default PrivateRoute; diff --git a/prediction-polls/frontend/src/Routes/Router.jsx b/prediction-polls/frontend/src/Routes/Router.jsx index cf012e65..e9f38541 100644 --- a/prediction-polls/frontend/src/Routes/Router.jsx +++ b/prediction-polls/frontend/src/Routes/Router.jsx @@ -28,14 +28,15 @@ function AppRouter() { } /> } /> + + + } /> } /> - // + } /> @@ -47,18 +48,18 @@ function AppRouter() { } /> + + + } /> } /> - + } /> - + } /> From 6ea6d1fe904c1c16cc51b8a74855a2791bc6ce84 Mon Sep 17 00:00:00 2001 From: kutaysaran <74209499+kutaysaran@users.noreply.github.com> Date: Sat, 9 Dec 2023 00:38:24 +0300 Subject: [PATCH 012/281] Forgot Password Page UI --- .../Auth/ForgotPassword/ForgotPassword.jsx | 87 +++++++++++++++++ .../ForgotPassword/ForgotPassword.module.css | 94 +++++++++++++++++++ .../frontend/src/Pages/Auth/SignIn/index.jsx | 4 +- .../frontend/src/Routes/Router.jsx | 2 + 4 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 prediction-polls/frontend/src/Pages/Auth/ForgotPassword/ForgotPassword.jsx create mode 100644 prediction-polls/frontend/src/Pages/Auth/ForgotPassword/ForgotPassword.module.css diff --git a/prediction-polls/frontend/src/Pages/Auth/ForgotPassword/ForgotPassword.jsx b/prediction-polls/frontend/src/Pages/Auth/ForgotPassword/ForgotPassword.jsx new file mode 100644 index 00000000..fc4865fe --- /dev/null +++ b/prediction-polls/frontend/src/Pages/Auth/ForgotPassword/ForgotPassword.jsx @@ -0,0 +1,87 @@ +import React from "react"; +import { + Button, + Input, + Form, +} from "antd"; +import styles from "./ForgotPassword.module.css"; +import { Link, useNavigate } from "react-router-dom"; +import { ReactComponent as Logo } from "../../../Assets/Logo.svg"; +import { ReactComponent as SignPageAnimation } from "../../../Assets/SignPageAnimation.svg"; +import "../../../index.css"; + +function ForgotPassword() { + const [form] = Form.useForm(); + const navigate = useNavigate(); + const handleSubmit = async (values) => { + navigate("/auth/sign-in"); + }; + + return ( +
+
+ + + +
+
+

Reset your password

+
Enter your email address and we will send you instructions to reset your password.
+
+ + + + +
+ +
+
+ +
+ + Back To Home + +
+
+
+
+
+ {/* Our sign up image from mock-up */} + +
+
+ ); +} + +export default ForgotPassword; diff --git a/prediction-polls/frontend/src/Pages/Auth/ForgotPassword/ForgotPassword.module.css b/prediction-polls/frontend/src/Pages/Auth/ForgotPassword/ForgotPassword.module.css new file mode 100644 index 00000000..3252d293 --- /dev/null +++ b/prediction-polls/frontend/src/Pages/Auth/ForgotPassword/ForgotPassword.module.css @@ -0,0 +1,94 @@ + .splitContainerStyle { + display: flex; + width: 100%; + margin: 0 auto; + height: 100vh; + } + + .animationStyle { + max-width: 100%; + max-height: 100%; + } + + + .displayCenterStyle { + display: flex; + align-items: center; + justify-content: center; + } + + .formInputStyle { + padding: 5px 5px; + width: 100%; + margin-bottom: 25px; + } + + .formButtonStyle { + display: flex; + align-items: center; + justify-content: center; + padding: 22px 0px; + margin-bottom: 15px; + width: 100%; + opacity: 0.85; + background: var(--primary-500); + } + + .imageContainerStyle { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(var(--primary-50), var(--primary-100)); + } + + .formContainerStyle { + flex: 1; + padding: 20px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + +.logoStyle { + display: flex; + align-items: center; + justify-content: center; + color: var(--neutral-100); + margin-bottom: 20px; + max-width: 80%; + max-height: 80%; +} + +h2,h5 { + display: flex; + align-items: center; + justify-content: center; + font-family: sans-serif ; + font-weight: 500; + color: var(--neutral-700); +} + +h2{ + margin-bottom: 40px; +} + +.headerContainerStyle h5 { + max-width: 310px; + text-align: center; + margin: 0 auto; + word-wrap: break-word; + margin-bottom: 45px; +} + + @media (max-width: 768px) { + .imageContainerStyle{ + display: none; + } + .headerContainerStyle h5 { + font-size: 14px; + padding: 0 10px; + } + + } \ No newline at end of file diff --git a/prediction-polls/frontend/src/Pages/Auth/SignIn/index.jsx b/prediction-polls/frontend/src/Pages/Auth/SignIn/index.jsx index 9bdd6226..f1e79515 100644 --- a/prediction-polls/frontend/src/Pages/Auth/SignIn/index.jsx +++ b/prediction-polls/frontend/src/Pages/Auth/SignIn/index.jsx @@ -86,9 +86,9 @@ function SignIn() { }} onChange={(e) => setPassword(e.target.value)} /> - +
diff --git a/prediction-polls/frontend/src/Routes/Router.jsx b/prediction-polls/frontend/src/Routes/Router.jsx index cf012e65..369a2d4e 100644 --- a/prediction-polls/frontend/src/Routes/Router.jsx +++ b/prediction-polls/frontend/src/Routes/Router.jsx @@ -14,6 +14,7 @@ import Vote from '../Pages/Vote'; import PrivateRoute from '../Components/PrivateRoute'; import GoogleLogin from '../Pages/Auth/Google' import EditProfile from '../Pages/EditProfile'; +import ForgotPassword from '../Pages/Auth/ForgotPassword/ForgotPassword'; function AppRouter() { return ( @@ -27,6 +28,7 @@ function AppRouter() { } /> } /> } /> + } /> } /> From 40c89ba4da4f086f39f39c073ae9b0abd9219c93 Mon Sep 17 00:00:00 2001 From: EmreBatuhan <93476131+EmreBatuhan@users.noreply.github.com> Date: Sat, 9 Dec 2023 13:21:09 +0300 Subject: [PATCH 013/281] Add new poll endpoints with swagger --- .../backend/src/routes/PollRouter.js | 142 +++++++++++++++++- 1 file changed, 140 insertions(+), 2 deletions(-) diff --git a/prediction-polls/backend/src/routes/PollRouter.js b/prediction-polls/backend/src/routes/PollRouter.js index ffc136de..71992434 100644 --- a/prediction-polls/backend/src/routes/PollRouter.js +++ b/prediction-polls/backend/src/routes/PollRouter.js @@ -84,7 +84,7 @@ const router = express.Router(); * get: * tags: * - polls - * description: Get all polls + * description: Get all polls sorted by their total points spent * responses: * 200: * description: Successful response @@ -145,7 +145,145 @@ const router = express.Router(); * message: Error while accessing the database. * code: 3004 */ -router.get('/', service.getPolls); +router.get('/', service.getFamousPolls); + +/** + * @swagger + * /polls/my-opened: + * get: + * tags: + * - polls + * description: Get all polls opened by user. Has to be authorized. + * responses: + * 200: + * description: Successful response + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/objects/pollObject' + * examples: + * genericExample: + * value: + * - id: 1 + * question: "Who will become POTUS?" + * tags: ["tag1", "tag2"] + * creatorName: "user123" + * creatorUsername: "GhostDragon" + * creatorImage: null + * pollType: "discrete" + * rejectVotes: "5 min" + * closingDate: "2023-11-20T21:00:00.000Z" + * isOpen: 1 + * comments: [] + * options: + * - id: 1 + * choice_text: "Trumpo" + * poll_id: 1 + * voter_count: 0 + * - id: 2 + * choice_text: "Biden" + * poll_id: 1 + * voter_count: 1 + * - id: 2 + * question: "Test question?" + * tags: ["tag1", "tag2"] + * creatorName: "GhostDragon" + * creatorUsername: "GhostDragon" + * creatorImage: null + * pollType: "continuous" + * rejectVotes: "2 hr" + * closingDate: null + * isOpen: 1 + * cont_poll_type: "numeric" + * comments: [] + * options: + * - 7 + * - 8 + * 500: + * description: Internal Server Error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/error' + * examples: + * databaseError: + * value: + * error: + * message: Error while accessing the database. + * code: 3004 + */ +router.get('/my-opened',authenticator.authorizeAccessToken,service.getOpenedPollsOfUser); + +/** + * @swagger + * /polls/my-voted: + * get: + * tags: + * - polls + * description: Get all polls voted by user. Has to be authorized. + * responses: + * 200: + * description: Successful response + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/objects/pollObject' + * examples: + * genericExample: + * value: + * - id: 1 + * question: "Who will become POTUS?" + * tags: ["tag1", "tag2"] + * creatorName: "user123" + * creatorUsername: "GhostDragon" + * creatorImage: null + * pollType: "discrete" + * rejectVotes: "5 min" + * closingDate: "2023-11-20T21:00:00.000Z" + * isOpen: 1 + * comments: [] + * options: + * - id: 1 + * choice_text: "Trumpo" + * poll_id: 1 + * voter_count: 0 + * - id: 2 + * choice_text: "Biden" + * poll_id: 1 + * voter_count: 1 + * - id: 2 + * question: "Test question?" + * tags: ["tag1", "tag2"] + * creatorName: "GhostDragon" + * creatorUsername: "GhostDragon" + * creatorImage: null + * pollType: "continuous" + * rejectVotes: "2 hr" + * closingDate: null + * isOpen: 1 + * cont_poll_type: "numeric" + * comments: [] + * options: + * - 7 + * - 8 + * 500: + * description: Internal Server Error + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/error' + * examples: + * databaseError: + * value: + * error: + * message: Error while accessing the database. + * code: 3004 + */ +router.get('/my-voted',authenticator.authorizeAccessToken,service.getVotedPollsOfUser); /** * @swagger From 06e514ad46111ff8e76314307e2cedaa9bbdac9c Mon Sep 17 00:00:00 2001 From: EmreBatuhan <93476131+EmreBatuhan@users.noreply.github.com> Date: Sat, 9 Dec 2023 13:21:30 +0300 Subject: [PATCH 014/281] Handle get poll logic --- .../backend/src/services/PollService.js | 111 ++++++++++++------ 1 file changed, 75 insertions(+), 36 deletions(-) diff --git a/prediction-polls/backend/src/services/PollService.js b/prediction-polls/backend/src/services/PollService.js index bfd67420..e0be23d8 100644 --- a/prediction-polls/backend/src/services/PollService.js +++ b/prediction-polls/backend/src/services/PollService.js @@ -6,41 +6,30 @@ const errorCodes = require("../errorCodes.js") async function getPolls(req,res){ try { const rows = await db.getPolls(); - const pollObjects = await Promise.all(rows.map(async (pollObject) => { - const tag_rows = await db.getTagsOfPoll(pollObject.id); - - const properties = { - "id": pollObject.id, - "question": pollObject.question, - "tags": tag_rows, - "creatorName": pollObject.username, - "creatorUsername": pollObject.username, - "creatorImage": null, - "pollType": pollObject.poll_type, - "closingDate": pollObject.closingDate, - "rejectVotes": (pollObject.numericFieldValue && pollObject.selectedTimeUnit) ? `${pollObject.numericFieldValue} ${pollObject.selectedTimeUnit}` : null, - "isOpen": pollObject.isOpen ? true : false, - "comments": [] - }; - - if (properties.pollType === 'discrete') { - const choices = await db.getDiscretePollChoices(properties.id); - - const choicesWithVoteCount = await Promise.all(choices.map(async (choice) => { - const voterCount = await db.getDiscreteVoteCount(choice.id); - return { ...choice, voter_count: voterCount }; - })); - - return { ...properties, "options": choicesWithVoteCount }; - } else if (properties.pollType === 'continuous') { - const contPollRows = await db.getContinuousPollWithId(properties.id); - const choices = await db.getContinuousPollVotes(properties.id); - - const newChoices = choices.map(item => item.float_value ? item.float_value : item.date_value); - - return { ...properties, "cont_poll_type": contPollRows[0].cont_poll_type, "options": newChoices }; - } - })) + const pollObjects = await createPollsJson(rows); + res.json(pollObjects); + } catch (error) { + console.error(error); + res.status(500).json(error); + } +} + +async function getFamousPolls(req,res){ + try { + const rows = await db.getFamousPolls(); + const pollObjects = await createPollsJson(rows); + res.json(pollObjects); + } catch (error) { + console.error(error); + res.status(500).json(error); + } +} + +async function getOpenedPollsOfUser(req,res){ + const userId = req.user.id; + try { + const rows = await db.getOpenedPollsOfUser(userId); + const pollObjects = await createPollsJson(rows); res.json(pollObjects); } catch (error) { console.error(error); @@ -48,6 +37,56 @@ async function getPolls(req,res){ } } +async function getVotedPollsOfUser(req,res){ + const userId = req.user.id; + try { + const rows = await db.getVotedPollsOfUser(userId); + const pollObjects = await createPollsJson(rows); + res.json(pollObjects); + } catch (error) { + console.error(error); + res.status(500).json(error); + } +} + +async function createPollsJson(poll_rows){ + return await Promise.all(poll_rows.map(async (pollObject) => { + const tag_rows = await db.getTagsOfPoll(pollObject.id); + + const properties = { + "id": pollObject.id, + "question": pollObject.question, + "tags": tag_rows, + "creatorName": pollObject.username, + "creatorUsername": pollObject.username, + "creatorImage": null, + "pollType": pollObject.poll_type, + "closingDate": pollObject.closingDate, + "rejectVotes": (pollObject.numericFieldValue && pollObject.selectedTimeUnit) ? `${pollObject.numericFieldValue} ${pollObject.selectedTimeUnit}` : null, + "isOpen": pollObject.isOpen ? true : false, + "comments": [] + }; + + if (properties.pollType === 'discrete') { + const choices = await db.getDiscretePollChoices(properties.id); + + const choicesWithVoteCount = await Promise.all(choices.map(async (choice) => { + const voterCount = await db.getDiscreteVoteCount(choice.id); + return { ...choice, voter_count: voterCount }; + })); + + return { ...properties, "options": choicesWithVoteCount }; + } else if (properties.pollType === 'continuous') { + const contPollRows = await db.getContinuousPollWithId(properties.id); + const choices = await db.getContinuousPollVotes(properties.id); + + const newChoices = choices.map(item => item.float_value ? item.float_value : item.date_value); + + return { ...properties, "cont_poll_type": contPollRows[0].cont_poll_type, "options": newChoices }; + } + })) +} + async function getPollWithId(req, res) { try { const pollId = req.params.pollId; @@ -315,4 +354,4 @@ async function closePoll(req, res) { } } -module.exports = {getPolls, getPollWithId, addDiscretePoll, addContinuousPoll, voteDiscretePoll, voteContinuousPoll, closePoll} \ No newline at end of file +module.exports = {getPolls, getFamousPolls,getOpenedPollsOfUser,getVotedPollsOfUser, getPollWithId, addDiscretePoll, addContinuousPoll, voteDiscretePoll, voteContinuousPoll, closePoll} \ No newline at end of file From 506748390c9af3718144ca090f3a0faf1e3a56b9 Mon Sep 17 00:00:00 2001 From: EmreBatuhan <93476131+EmreBatuhan@users.noreply.github.com> Date: Sat, 9 Dec 2023 13:21:49 +0300 Subject: [PATCH 015/281] new sql queries for poll retrieval --- .../backend/src/repositories/PollDB.js | 65 ++++++++++++++++++- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/prediction-polls/backend/src/repositories/PollDB.js b/prediction-polls/backend/src/repositories/PollDB.js index f11d332b..89d49111 100644 --- a/prediction-polls/backend/src/repositories/PollDB.js +++ b/prediction-polls/backend/src/repositories/PollDB.js @@ -1,5 +1,5 @@ const mysql = require('mysql2'); -const { addRefreshToken, deleteRefreshToken } = require('./AuthorizationDB'); +const { addRefreshToken, deleteRefreshToken, findUser } = require('./AuthorizationDB'); const { updatePoints } = require('./ProfileDB'); const errorCodes = require("../errorCodes.js"); @@ -25,6 +25,67 @@ async function getPolls(){ } } +async function getFamousPolls(){ + + const sql = "SELECT polls.*, famous_polls.total_points_spent " + + "FROM polls,(SELECT poll_id, SUM(given_points) AS total_points_spent " + + "FROM discrete_polls_selections " + + "GROUP BY poll_id " + + "UNION " + + "SELECT poll_id, SUM(given_points) AS total_points_spent " + + "FROM continuous_poll_selections " + + "GROUP BY poll_id) AS famous_polls " + + "WHERE polls.id = famous_polls.poll_id " + + "ORDER BY famous_polls.total_points_spent DESC" + + try { + const [rows] = await pool.query(sql); + console.log(rows) + return rows; + } catch (error) { + throw {error: error}; + } +} + +async function getOpenedPollsOfUser(userId){ + const sql = 'SELECT * FROM polls WHERE username = ?'; + + try { + const user_result = await findUser({userId}); + if(user_result.error){ + throw user_result.error + } + const [rows, fields] = await pool.query(sql,[user_result.username]); + return rows + } catch (error) { + throw {error: error}; + } +} + +async function getVotedPollsOfUser(userId){ + + const sql = "SELECT polls.* FROM polls,( " + + "SELECT poll_id FROM discrete_polls_selections WHERE user_id = ? UNION " + + "SELECT poll_id FROM continuous_poll_selections WHERE user_id = ? ) AS voted_polls " + + "WHERE polls.id = voted_polls.poll_id"; + + const discrete_votes_sql = 'SELECT * FROM discrete_polls_selections WHERE user_id = ?'; + const continuous_votes_sql = 'SELECT * FROM continuous_poll_selections WHERE user_id = ?'; + + try { + const user_result = await findUser({userId}); + if(user_result.error){ + throw user_result.error + } + const [rows] = await pool.query(sql,[userId,userId]); + + console.log(rows) + return rows; + } catch (error) { + throw {error: error}; + } +} + async function getPollWithId(pollId){ const sql = 'SELECT * FROM polls WHERE id = ?'; @@ -348,7 +409,7 @@ async function closePoll(pollId, rewards) { } } -module.exports = {getPolls, getPollWithId, getDiscretePollWithId, getContinuousPollWithId, +module.exports = {getPolls,getFamousPolls,getOpenedPollsOfUser,getVotedPollsOfUser, getPollWithId, getDiscretePollWithId, getContinuousPollWithId, addDiscretePoll,addContinuousPoll, getDiscretePollChoices, getDiscreteVoteCount, voteDiscretePoll, voteContinuousPoll, getContinuousPollVotes,getTagsOfPoll, getUntaggedPolls, updateTagsScanned, addTopic, getDiscreteSelectionsWithPollId, closePoll} \ No newline at end of file From 8f9ba08db431eb8410d14024c0d897a837a2bba4 Mon Sep 17 00:00:00 2001 From: EmreBatuhan <93476131+EmreBatuhan@users.noreply.github.com> Date: Sat, 9 Dec 2023 13:23:38 +0300 Subject: [PATCH 016/281] Remove old getPolls function --- .../backend/src/services/PollService.js | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/prediction-polls/backend/src/services/PollService.js b/prediction-polls/backend/src/services/PollService.js index e0be23d8..926e53f6 100644 --- a/prediction-polls/backend/src/services/PollService.js +++ b/prediction-polls/backend/src/services/PollService.js @@ -3,17 +3,6 @@ const {updatePoints} = require("../repositories/ProfileDB.js"); const { findUser } = require('../repositories/AuthorizationDB.js'); const errorCodes = require("../errorCodes.js") -async function getPolls(req,res){ - try { - const rows = await db.getPolls(); - const pollObjects = await createPollsJson(rows); - res.json(pollObjects); - } catch (error) { - console.error(error); - res.status(500).json(error); - } -} - async function getFamousPolls(req,res){ try { const rows = await db.getFamousPolls(); @@ -354,4 +343,4 @@ async function closePoll(req, res) { } } -module.exports = {getPolls, getFamousPolls,getOpenedPollsOfUser,getVotedPollsOfUser, getPollWithId, addDiscretePoll, addContinuousPoll, voteDiscretePoll, voteContinuousPoll, closePoll} \ No newline at end of file +module.exports = { getFamousPolls,getOpenedPollsOfUser,getVotedPollsOfUser, getPollWithId, addDiscretePoll, addContinuousPoll, voteDiscretePoll, voteContinuousPoll, closePoll} \ No newline at end of file From 1ac05009bb2f61962a4b2428cec2f0ec33400d5c Mon Sep 17 00:00:00 2001 From: EmreBatuhan <93476131+EmreBatuhan@users.noreply.github.com> Date: Sat, 9 Dec 2023 23:30:32 +0300 Subject: [PATCH 017/281] Add new routes to app --- prediction-polls/backend/src/app.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/prediction-polls/backend/src/app.js b/prediction-polls/backend/src/app.js index c95e41ed..66c4339e 100644 --- a/prediction-polls/backend/src/app.js +++ b/prediction-polls/backend/src/app.js @@ -2,6 +2,7 @@ const express = require('express'); const authRouter = require('./routes/AuthorizationRouter.js'); const pollRouter = require('./routes/PollRouter.js'); const profileRouter = require('./routes/ProfileRouter.js'); +const moderatorRouter = require('./routes/ModeratorRouter.js'); const tagRoutine = require('./routines/tagRoutine.js'); const cors = require("cors"); @@ -22,6 +23,7 @@ app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x app.use('/polls',pollRouter); app.use('/auth', authRouter); app.use('/profiles', profileRouter); +app.use('/moderators', profileRouter); const swaggerSpec = swaggerJsDoc(swaggerOptions); app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); From 81edcd5f136ed3b3c8ef680d42cda4e6f9750187 Mon Sep 17 00:00:00 2001 From: EmreBatuhan <93476131+EmreBatuhan@users.noreply.github.com> Date: Sat, 9 Dec 2023 23:30:50 +0300 Subject: [PATCH 018/281] Add new routes to router --- .../backend/src/routes/ModeratorRouter.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 prediction-polls/backend/src/routes/ModeratorRouter.js diff --git a/prediction-polls/backend/src/routes/ModeratorRouter.js b/prediction-polls/backend/src/routes/ModeratorRouter.js new file mode 100644 index 00000000..2db245f5 --- /dev/null +++ b/prediction-polls/backend/src/routes/ModeratorRouter.js @@ -0,0 +1,14 @@ +const authenticator = require("../services/AuthorizationService.js"); +const service = require("../services/ModeratorService.js"); +const express = require('express'); +const router = express.Router(); + +router.post('/appoint',authenticator.authorizeAccessToken,service.controlModRole, service.makeMod); + +router.get('/my-tags',authenticator.authorizeAccessToken,service.controlModRole,service.getModTags); + +router.post('/my-tags',authenticator.authorizeAccessToken,service.controlModRole,service.updateTags); + +router.get('/my-requests',authenticator.authorizeAccessToken,service.controlModRole,service.getModRequests); + +router.post('/my-requests',authenticator.authorizeAccessToken,service.controlModRole,service.answerRequest); \ No newline at end of file From 8e1d2e48b9963e47be312f3f015698cb01ad09d3 Mon Sep 17 00:00:00 2001 From: EmreBatuhan <93476131+EmreBatuhan@users.noreply.github.com> Date: Sat, 9 Dec 2023 23:31:02 +0300 Subject: [PATCH 019/281] Add Moderator Logic --- .../backend/src/repositories/ModeratorDB.js | 60 +++++++ .../backend/src/services/ModeratorService.js | 157 ++++++++++++++++++ 2 files changed, 217 insertions(+) create mode 100644 prediction-polls/backend/src/repositories/ModeratorDB.js create mode 100644 prediction-polls/backend/src/services/ModeratorService.js diff --git a/prediction-polls/backend/src/repositories/ModeratorDB.js b/prediction-polls/backend/src/repositories/ModeratorDB.js new file mode 100644 index 00000000..25e341fd --- /dev/null +++ b/prediction-polls/backend/src/repositories/ModeratorDB.js @@ -0,0 +1,60 @@ +const mysql = require('mysql2'); +const { addRefreshToken, deleteRefreshToken } = require('./AuthorizationDB'); +const errorCodes = require("../errorCodes.js"); + +require('dotenv').config(); + +const pool = mysql.createPool({ + host: process.env.MYSQL_HOST, + user: process.env.MYSQL_USER, + password: process.env.MYSQL_PASSWORD, + database: process.env.MYSQL_DATABASE +}).promise() + +async function makeMod(userId){ + const sql = 'SELECT * FROM polls'; + + try { + const [rows, fields] = await pool.query(sql); + return rows + } catch (error) { + console.error('getDiscretePolls(): Database Error'); + throw {error: errorCodes.DATABASE_ERROR}; + } +} + +async function getModTags(userId){ + +} + +async function deleteModTag(userId,mod_tag_topic){ + +} + +async function addModTag(userId,mod_tag_topic){ + +} + +async function getModRequests(userId){ + +} + +async function checkRequestOfUser(requestId,userId){ + +} + +async function setDecisionOnReportRequest(requestId,ban_poll){ + +} + +async function setDecisionOnDiscreteRequest(requestId,discrete_choice){ + +} + +async function setDecisionOnContinuousRequest(requestId,continuous_choice,contPollType){ + +} + +module.exports = { makeMod, getModTags, deleteModTag, addModTag, getModRequests, checkRequestOfUser, + setDecisionOnReportRequest, setDecisionOnDiscreteRequest, setDecisionOnContinuousRequest +} \ No newline at end of file diff --git a/prediction-polls/backend/src/services/ModeratorService.js b/prediction-polls/backend/src/services/ModeratorService.js new file mode 100644 index 00000000..2110a622 --- /dev/null +++ b/prediction-polls/backend/src/services/ModeratorService.js @@ -0,0 +1,157 @@ +const db = require("../repositories/ModeratorDB.js"); +const pollDb = require("../repositories/PollDB.js"); +const { findUser } = require('../repositories/AuthorizationDB.js'); +const errorCodes = require("../errorCodes.js") +const topics = require('./topics.json'); + + +async function controlModRole(req,res,next){ + const userId = req.user.id; + try{ + const result = await findUser({userId}); + if(result.error){ + throw errorCodes.USER_NOT_FOUND + } + + if(result.isMod == undefined || result.isMod == false){ + throw errorCodes.USER_IS_NOT_MODERATOR + } + next(); + } + catch(error){ + return res.status(500).json({error:error}); + } +} + +async function makeMod(req,res){ + const userId = req.body.userId; + try{ + const appointing = await db.makeMod(userId); + if(appointing.error){ + throw errorCodes.USER_NOT_FOUND + } + return res.status(200).json({error:error}); + } + catch(error){ + return res.status(400).json({error:error}); + } +} + +async function getModTags(req,res){ + const userId = req.user.id; + try{ + const [mod_tags] = await db.getModTags(userId); + const all_tags_json = topics.topics.map((main_topic) => { + // Check whether the tag is stored in db + const isSelected = mod_tags.some(mod_tag => mod_tag.topic === main_topic) + if(isSelected) { + return {topic:main_topic,isSelected:1}; + } + else{ + return {topic:main_topic,isSelected:0}; + } + }) + return res.status(200).json(all_tags_json); + } + catch(error){ + return res.status(400).json({error:error}); + } +} + +async function updateTags(req,res){ + const userId = req.user.id; + const mod_tag_topic = req.body.topic; + const isSelected = req.body.isSelected; + + if(isSelected == undefined || mod_tag_topic == undefined){ + return res.res.status(400).json({error:errorCodes.INSUFFICIENT_DATA}); + } + try{ + if(!isSelected){ + const delete_result = await db.deleteModTag(userId,mod_tag_topic); + } + else{ + const add_result = await db.addModTag(userId,mod_tag_topic); + } + return res.status(200).json(all_tags_json); + } + catch(error){ + return res.status(400).json({error:error}); + } +} + +async function getModRequests(req,res){ + const userId = req.user.id; + + try{ + const mod_requests = await db.getModRequests(userId); + if(mod_requests.error){ + throw mod_requests.error + } + return res.status(200).json(mod_requests); + } + catch(error){ + return res.status(400).json({error:error}); + } +} + +async function answerRequest(req,res){ + const userId = req.user.id; + const requestId = req.body.requestId; + + try{ + const user_has_request = await db.checkRequestOfUser(requestId,userId); + if(user_has_request.error){ + throw errorCodes.USER_DOES_NOT_HAVE_REQUEST_ID; + } + + if(user_has_request.type == "report"){ + const ban_poll = req.body.banPoll; + if(ban_poll == undefined){ + throw errorCodes.REPORT_REQUEST_INVALID_BODY + } + const decision_set = await db.setDecisionOnReportRequest(requestId,ban_poll) + if(decision_set.error){ + throw decision_set.error + } + return res.status(200).json({status:"success"}); + } + if(user_has_request.type == "discrete"){ + const discrete_choice = req.body.choice; + if(discrete_choice == undefined){ + throw errorCodes.DISCRETE_POLL_REQUEST_INVALID_BODY + } + const decision_result = await db.setDecisionOnDiscreteRequest(requestId,discrete_choice) + if(decision_result.error){ + throw decision_result.error + } + return res.status(200).json({status:"success"}); + } + if(user_has_request.type == "continuous"){ + const continuous_choice = req.body.choice; + if(continuous_choice == undefined){ + throw errorCodes.CONTINUOUS_POLL_REQUEST_INVALID_BODY; + } + + const request_poll = pollDb.getContinuousPollWithId(user_has_request.poll_id) + if(request_poll.error){ + throw errorCodes.MOD_REQUEST_SHOWS_INVALID_POLL + } + const contPollType = request_poll.cont_poll_type + + const decision_result = await db.setDecisionOnContinuousRequest(requestId,continuous_choice,contPollType); + if(decision_result.error){ + throw decision_result.error + } + return res.status(200).json({status:"success"}); + } + + } + catch(error){ + return res.status(400).json({error:error}); + } + +} + + +module.exports = {controlModRole, makeMod, getModTags, updateTags, getModRequests, answerRequest} \ No newline at end of file From 91fb43419448777fc042169bf5e82e01951895c7 Mon Sep 17 00:00:00 2001 From: EmreBatuhan <93476131+EmreBatuhan@users.noreply.github.com> Date: Sat, 9 Dec 2023 23:31:13 +0300 Subject: [PATCH 020/281] Update error codes and schema --- prediction-polls/backend/src/errorCodes.js | 30 ++++++++++++++++ prediction-polls/backend/src/schema.sql | 42 ++++++++++++++++++++-- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/prediction-polls/backend/src/errorCodes.js b/prediction-polls/backend/src/errorCodes.js index 8a6b09a4..4028fb9a 100644 --- a/prediction-polls/backend/src/errorCodes.js +++ b/prediction-polls/backend/src/errorCodes.js @@ -35,6 +35,11 @@ const errorCodes = { message: 'Registration failed' }, + INSUFFICIENT_DATA: { + code: 1007, + message: 'Given data is not sufficient. Please follow guidelines.' + }, + USER_NOT_FOUND: { code: 2000, message: 'User not found.', @@ -173,6 +178,31 @@ const errorCodes = { CHOICE_OUT_OF_BOUNDS_ERROR: { code: 5001, message: 'Choice for the poll is out of given bounds.' + }, + + USER_IS_NOT_MODERATOR: { + code: 8000, + message: 'User is not authorized to execute this moderator activity' + }, + + REPORT_REQUEST_INVALID_BODY: { + code: 8001, + message: 'Report type moderator request should contain requestId and banPoll in body' + }, + + DISCRETE_POLL_REQUEST_INVALID_BODY:{ + code: 8002, + message: 'Discrete poll type moderator request should contain requestId and choice in body' + }, + + CONTINUOUS_POLL_REQUEST_INVALID_BODY:{ + code: 8003, + message: 'Continuous poll type moderator request should contain requestId and choice in body' + }, + + MOD_REQUEST_SHOWS_INVALID_POLL:{ + code: 8004, + message: 'Given moderator request does not show a valid poll' } // Add more error codes as needed diff --git a/prediction-polls/backend/src/schema.sql b/prediction-polls/backend/src/schema.sql index 32b4b3e2..44fc0275 100644 --- a/prediction-polls/backend/src/schema.sql +++ b/prediction-polls/backend/src/schema.sql @@ -9,6 +9,7 @@ CREATE TABLE users ( birthday DATETIME, email_verified BOOLEAN DEFAULT FALSE, email_verification_token VARCHAR(255), + isMod BOOLEAN NOT NULL DEFAULT 0, UNIQUE (username), UNIQUE (email) ); @@ -93,7 +94,7 @@ CREATE TABLE badges ( id INT AUTO_INCREMENT PRIMARY KEY, userRank INT NOT NULL, topic VARCHAR(255) NOT NULL, - userId INT, + userId INT NOT NULL, FOREIGN KEY (userId) REFERENCES users(id) ON DELETE SET NULL ); @@ -102,4 +103,41 @@ CREATE TABLE tags ( topic VARCHAR(255) NOT NULL, poll_id INT, FOREIGN KEY (poll_id) REFERENCES polls(id) ON DELETE SET NULL -); \ No newline at end of file +); + +CREATE TABLE mod_tags ( + id INT AUTO_INCREMENT PRIMARY KEY, + topic VARCHAR(255) NOT NULL, + userId INT NOT NULL, + UNIQUE(userId,topic), +) + +CREATE TABLE mod_requests ( + id INT AUTO_INCREMENT PRIMARY KEY, + userId INT NOT NULL, + poll_id INT NOT NULL, + request_type ENUM('report','discrete', 'continuous' ) NOT NULL, + FOREIGN KEY (poll_id) REFERENCES polls(id), + FOREIGN KEY (userId) REFERENCES users(id), +) + +CREATE TABLE mod_request_report ( + request_id INT, + ban_poll BOOLEAN, + FOREIGN KEY (request_id) REFERENCES mod_requests(id) ON DELETE CASCADE +) + +CREATE TABLE mod_request_discrete ( + request_id INT, + choice_id INT, + question VARCHAR(255) NOT NULL, + FOREIGN KEY (request_id) REFERENCES mod_requests(id) ON DELETE CASCADE +) + +CREATE TABLE mod_request_continuous ( + request_id INT, + float_value FLOAT, + date_value DATE, + question VARCHAR(255) NOT NULL, + FOREIGN KEY (request_id) REFERENCES mod_requests(id) ON DELETE CASCADE +) \ No newline at end of file From 6f4386456ea0bd03d64cffa24a9575e58a4c0acd Mon Sep 17 00:00:00 2001 From: EmreBatuhan <93476131+EmreBatuhan@users.noreply.github.com> Date: Sun, 10 Dec 2023 14:08:45 +0300 Subject: [PATCH 021/281] Update AuthGoogleService.js --- prediction-polls/backend/src/services/AuthGoogleService.js | 1 + 1 file changed, 1 insertion(+) diff --git a/prediction-polls/backend/src/services/AuthGoogleService.js b/prediction-polls/backend/src/services/AuthGoogleService.js index a4bb9078..f63376da 100644 --- a/prediction-polls/backend/src/services/AuthGoogleService.js +++ b/prediction-polls/backend/src/services/AuthGoogleService.js @@ -1,6 +1,7 @@ const qs = require("querystring"); const axios = require("axios"); const db = require("../repositories/AuthorizationDB.js"); +const profileDb = require("../repositories/ProfileDB.js"); const crypto = require('crypto'); const errorCodes = require("../errorCodes.js"); From d88c99c7dd36cbfb152add72bbd306a49d36509b Mon Sep 17 00:00:00 2001 From: EmreBatuhan <93476131+EmreBatuhan@users.noreply.github.com> Date: Sun, 10 Dec 2023 22:45:15 +0300 Subject: [PATCH 022/281] Fix mistakes I have done some tests and spotted some mistakes --- prediction-polls/backend/src/app.js | 2 +- .../backend/src/routes/ModeratorRouter.js | 4 ++- prediction-polls/backend/src/schema.sql | 19 ++++++------- .../backend/src/services/ModeratorService.js | 27 ++++++++++--------- 4 files changed, 28 insertions(+), 24 deletions(-) diff --git a/prediction-polls/backend/src/app.js b/prediction-polls/backend/src/app.js index 66c4339e..4fd13f56 100644 --- a/prediction-polls/backend/src/app.js +++ b/prediction-polls/backend/src/app.js @@ -23,7 +23,7 @@ app.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x app.use('/polls',pollRouter); app.use('/auth', authRouter); app.use('/profiles', profileRouter); -app.use('/moderators', profileRouter); +app.use('/moderators', moderatorRouter); const swaggerSpec = swaggerJsDoc(swaggerOptions); app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); diff --git a/prediction-polls/backend/src/routes/ModeratorRouter.js b/prediction-polls/backend/src/routes/ModeratorRouter.js index 2db245f5..b09e81bc 100644 --- a/prediction-polls/backend/src/routes/ModeratorRouter.js +++ b/prediction-polls/backend/src/routes/ModeratorRouter.js @@ -11,4 +11,6 @@ router.post('/my-tags',authenticator.authorizeAccessToken,service.controlModRole router.get('/my-requests',authenticator.authorizeAccessToken,service.controlModRole,service.getModRequests); -router.post('/my-requests',authenticator.authorizeAccessToken,service.controlModRole,service.answerRequest); \ No newline at end of file +router.post('/my-requests',authenticator.authorizeAccessToken,service.controlModRole,service.answerRequest); + +module.exports = router; \ No newline at end of file diff --git a/prediction-polls/backend/src/schema.sql b/prediction-polls/backend/src/schema.sql index 44fc0275..1d28e1e1 100644 --- a/prediction-polls/backend/src/schema.sql +++ b/prediction-polls/backend/src/schema.sql @@ -109,8 +109,8 @@ CREATE TABLE mod_tags ( id INT AUTO_INCREMENT PRIMARY KEY, topic VARCHAR(255) NOT NULL, userId INT NOT NULL, - UNIQUE(userId,topic), -) + UNIQUE(userId,topic) +); CREATE TABLE mod_requests ( id INT AUTO_INCREMENT PRIMARY KEY, @@ -118,26 +118,27 @@ CREATE TABLE mod_requests ( poll_id INT NOT NULL, request_type ENUM('report','discrete', 'continuous' ) NOT NULL, FOREIGN KEY (poll_id) REFERENCES polls(id), - FOREIGN KEY (userId) REFERENCES users(id), -) + FOREIGN KEY (userId) REFERENCES users(id) +); CREATE TABLE mod_request_report ( + id INT AUTO_INCREMENT PRIMARY KEY, request_id INT, ban_poll BOOLEAN, FOREIGN KEY (request_id) REFERENCES mod_requests(id) ON DELETE CASCADE -) +); CREATE TABLE mod_request_discrete ( + id INT AUTO_INCREMENT PRIMARY KEY, request_id INT, choice_id INT, - question VARCHAR(255) NOT NULL, FOREIGN KEY (request_id) REFERENCES mod_requests(id) ON DELETE CASCADE -) +); CREATE TABLE mod_request_continuous ( + id INT AUTO_INCREMENT PRIMARY KEY, request_id INT, float_value FLOAT, date_value DATE, - question VARCHAR(255) NOT NULL, FOREIGN KEY (request_id) REFERENCES mod_requests(id) ON DELETE CASCADE -) \ No newline at end of file +); \ No newline at end of file diff --git a/prediction-polls/backend/src/services/ModeratorService.js b/prediction-polls/backend/src/services/ModeratorService.js index 2110a622..51bb795b 100644 --- a/prediction-polls/backend/src/services/ModeratorService.js +++ b/prediction-polls/backend/src/services/ModeratorService.js @@ -2,7 +2,7 @@ const db = require("../repositories/ModeratorDB.js"); const pollDb = require("../repositories/PollDB.js"); const { findUser } = require('../repositories/AuthorizationDB.js'); const errorCodes = require("../errorCodes.js") -const topics = require('./topics.json'); +const topics = require('../routines/topics.json'); async function controlModRole(req,res,next){ @@ -30,7 +30,7 @@ async function makeMod(req,res){ if(appointing.error){ throw errorCodes.USER_NOT_FOUND } - return res.status(200).json({error:error}); + return res.status(200).json({status:"success"}); } catch(error){ return res.status(400).json({error:error}); @@ -40,15 +40,15 @@ async function makeMod(req,res){ async function getModTags(req,res){ const userId = req.user.id; try{ - const [mod_tags] = await db.getModTags(userId); + const mod_tags = await db.getModTags(userId); const all_tags_json = topics.topics.map((main_topic) => { // Check whether the tag is stored in db - const isSelected = mod_tags.some(mod_tag => mod_tag.topic === main_topic) + const isSelected = mod_tags.some(mod_tag => mod_tag.topic === main_topic.name) if(isSelected) { - return {topic:main_topic,isSelected:1}; + return {topic:main_topic.name,isSelected:1}; } else{ - return {topic:main_topic,isSelected:0}; + return {topic:main_topic.name,isSelected:0}; } }) return res.status(200).json(all_tags_json); @@ -64,7 +64,7 @@ async function updateTags(req,res){ const isSelected = req.body.isSelected; if(isSelected == undefined || mod_tag_topic == undefined){ - return res.res.status(400).json({error:errorCodes.INSUFFICIENT_DATA}); + return res.status(400).json({error:errorCodes.INSUFFICIENT_DATA}); } try{ if(!isSelected){ @@ -73,7 +73,7 @@ async function updateTags(req,res){ else{ const add_result = await db.addModTag(userId,mod_tag_topic); } - return res.status(200).json(all_tags_json); + return res.status(200).json({status:"success"}); } catch(error){ return res.status(400).json({error:error}); @@ -100,12 +100,12 @@ async function answerRequest(req,res){ const requestId = req.body.requestId; try{ - const user_has_request = await db.checkRequestOfUser(requestId,userId); + const [user_has_request] = await db.checkRequestOfUser(requestId,userId); if(user_has_request.error){ throw errorCodes.USER_DOES_NOT_HAVE_REQUEST_ID; } - if(user_has_request.type == "report"){ + if(user_has_request.request_type == "report"){ const ban_poll = req.body.banPoll; if(ban_poll == undefined){ throw errorCodes.REPORT_REQUEST_INVALID_BODY @@ -116,7 +116,7 @@ async function answerRequest(req,res){ } return res.status(200).json({status:"success"}); } - if(user_has_request.type == "discrete"){ + if(user_has_request.request_type == "discrete"){ const discrete_choice = req.body.choice; if(discrete_choice == undefined){ throw errorCodes.DISCRETE_POLL_REQUEST_INVALID_BODY @@ -127,13 +127,13 @@ async function answerRequest(req,res){ } return res.status(200).json({status:"success"}); } - if(user_has_request.type == "continuous"){ + if(user_has_request.request_type == "continuous"){ const continuous_choice = req.body.choice; if(continuous_choice == undefined){ throw errorCodes.CONTINUOUS_POLL_REQUEST_INVALID_BODY; } - const request_poll = pollDb.getContinuousPollWithId(user_has_request.poll_id) + const [request_poll] = await pollDb.getContinuousPollWithId(user_has_request.poll_id) if(request_poll.error){ throw errorCodes.MOD_REQUEST_SHOWS_INVALID_POLL } @@ -145,6 +145,7 @@ async function answerRequest(req,res){ } return res.status(200).json({status:"success"}); } + return res.status(500).json({error:errorCodes.REQUEST_HAS_INVALID_TYPE}) } catch(error){ From 29f4c6a5166aaeb3ab062cc120fec248377f56dc Mon Sep 17 00:00:00 2001 From: EmreBatuhan <93476131+EmreBatuhan@users.noreply.github.com> Date: Sun, 10 Dec 2023 22:46:05 +0300 Subject: [PATCH 023/281] Fill ModeratorDB with logic --- .../backend/src/repositories/ModeratorDB.js | 83 ++++++++++++++++++- 1 file changed, 80 insertions(+), 3 deletions(-) diff --git a/prediction-polls/backend/src/repositories/ModeratorDB.js b/prediction-polls/backend/src/repositories/ModeratorDB.js index 25e341fd..486ee768 100644 --- a/prediction-polls/backend/src/repositories/ModeratorDB.js +++ b/prediction-polls/backend/src/repositories/ModeratorDB.js @@ -12,47 +12,124 @@ const pool = mysql.createPool({ }).promise() async function makeMod(userId){ - const sql = 'SELECT * FROM polls'; + const sql = 'UPDATE users SET isMod = 1 WHERE id = ?'; try { - const [rows, fields] = await pool.query(sql); + const [rows] = await pool.query(sql,[userId]); return rows } catch (error) { - console.error('getDiscretePolls(): Database Error'); + console.error('makeMod(): Database Error'); throw {error: errorCodes.DATABASE_ERROR}; } } async function getModTags(userId){ + const sql = 'SELECT * FROM mod_tags WHERE userId = ?'; + try { + const [rows] = await pool.query(sql,[userId]); + return rows + } catch (error) { + console.error('getModTags(): Database Error'); + throw {error: errorCodes.DATABASE_ERROR}; + } } async function deleteModTag(userId,mod_tag_topic){ + const sql = 'DELETE FROM mod_tags WHERE userId = ? AND topic = ?'; + try { + const [rows] = await pool.query(sql,[userId,mod_tag_topic]); + return rows + } catch (error) { + console.error('deleteModTag(): Database Error'); + throw {error: errorCodes.DATABASE_ERROR}; + } } async function addModTag(userId,mod_tag_topic){ + const sql = 'INSERT INTO mod_tags (topic,userId) VALUES (?,?)' + try { + const [rows] = await pool.query(sql,[mod_tag_topic,userId]); + return {status:"success"} + } catch (error) { + if(error.code === 'ER_DUP_ENTRY'){ + return {status:"already exists"} // This means that tag is already added + } + console.error('addModTag(): Database Error ',error); + throw {error: errorCodes.DATABASE_ERROR}; + } } async function getModRequests(userId){ + const sql = 'SELECT * FROM mod_requests WHERE userId = ?' + try { + const [rows] = await pool.query(sql,[userId]); + return rows + } catch (error) { + console.error('getModRequests(): Database Error'); + throw {error: errorCodes.DATABASE_ERROR}; + } } async function checkRequestOfUser(requestId,userId){ + const sql = 'SELECT * FROM mod_requests WHERE id = ? AND userId = ?' + try { + const [rows] = await pool.query(sql,[requestId,userId]); + return rows + } catch (error) { + console.error('getModRequests(): Database Error'); + throw {error: errorCodes.DATABASE_ERROR}; + } } async function setDecisionOnReportRequest(requestId,ban_poll){ + const sql = 'UPDATE mod_request_report SET ban_poll = ? WHERE request_id = ?' + try { + const [rows] = await pool.query(sql,[ban_poll,requestId]); + return rows + } catch (error) { + console.error('setDecisionOnReportRequest(): Database Error'); + throw {error: errorCodes.DATABASE_ERROR}; + } } async function setDecisionOnDiscreteRequest(requestId,discrete_choice){ + const sql = 'UPDATE mod_request_discrete SET choice_id = ? WHERE request_id = ?' + try { + const [rows] = await pool.query(sql,[discrete_choice,requestId]); + return rows + } catch (error) { + console.error('setDecisionOnDiscreteRequest(): Database Error'); + throw {error: errorCodes.DATABASE_ERROR}; + } } async function setDecisionOnContinuousRequest(requestId,continuous_choice,contPollType){ + const numeric_sql = 'UPDATE mod_request_continuous SET float_value = ? WHERE request_id = ?' + const date_sql = 'UPDATE mod_request_continuous SET date_value = ? WHERE request_id = ?' + try { + let rows; + if(contPollType == "numeric"){ + [rows] = await pool.query(numeric_sql,[continuous_choice,requestId]); + } + else if(contPollType == "date"){ + [rows] = await pool.query(date_sql,[continuous_choice,requestId]); + } + else{ + throw errorCodes.DATABASE_ERROR + } + return rows + } catch (error) { + console.error('setDecisionOnContinuousRequest(): Database Error',error); + throw {error: errorCodes.DATABASE_ERROR}; + } } module.exports = { makeMod, getModTags, deleteModTag, addModTag, getModRequests, checkRequestOfUser, From 24cdf1fb26708f717e4e549970f362e20459499e Mon Sep 17 00:00:00 2001 From: EmreBatuhan <93476131+EmreBatuhan@users.noreply.github.com> Date: Sun, 10 Dec 2023 22:46:13 +0300 Subject: [PATCH 024/281] Add new error codes --- prediction-polls/backend/src/errorCodes.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/prediction-polls/backend/src/errorCodes.js b/prediction-polls/backend/src/errorCodes.js index 4028fb9a..15266aff 100644 --- a/prediction-polls/backend/src/errorCodes.js +++ b/prediction-polls/backend/src/errorCodes.js @@ -203,6 +203,11 @@ const errorCodes = { MOD_REQUEST_SHOWS_INVALID_POLL:{ code: 8004, message: 'Given moderator request does not show a valid poll' + }, + + REQUEST_HAS_INVALID_TYPE:{ + code: 8005, + message: 'Given request is corrupted and server could not handle it' } // Add more error codes as needed From 8f7abe9540e8cae6c6b51de7f254906396392f90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Serhat=20Hebun=20=C5=9Eim=C5=9Fek?= Date: Mon, 11 Dec 2023 17:49:02 +0300 Subject: [PATCH 025/281] updated email verification code --- .../src/repositories/AuthorizationDB.js | 4 ++-- .../backend/src/routes/AuthorizationRouter.js | 18 +---------------- .../src/services/AuthorizationService.js | 20 ++++++++++++++++++- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/prediction-polls/backend/src/repositories/AuthorizationDB.js b/prediction-polls/backend/src/repositories/AuthorizationDB.js index db6bb5a1..eabb56ce 100644 --- a/prediction-polls/backend/src/repositories/AuthorizationDB.js +++ b/prediction-polls/backend/src/repositories/AuthorizationDB.js @@ -155,7 +155,7 @@ async function saveEmailVerificationToken(userId, token) { return await pool.query(sql, values); } -async function verifyEmail(token) { +async function verifyEmailToken(token) { const sql = 'UPDATE users SET email_verified = TRUE WHERE email_verification_token = ?'; const values = [token]; @@ -175,4 +175,4 @@ function createTransporter() { }); } -module.exports = {pool, addRefreshToken,checkRefreshToken,deleteRefreshToken,isUsernameOrEmailInUse,checkCredentials,addUser,findUser,saveEmailVerificationToken,verifyEmail,createTransporter} +module.exports = {pool, addRefreshToken,checkRefreshToken,deleteRefreshToken,isUsernameOrEmailInUse,checkCredentials,addUser,findUser,saveEmailVerificationToken,verifyEmailToken,createTransporter} diff --git a/prediction-polls/backend/src/routes/AuthorizationRouter.js b/prediction-polls/backend/src/routes/AuthorizationRouter.js index 5531ae16..5566b3ad 100644 --- a/prediction-polls/backend/src/routes/AuthorizationRouter.js +++ b/prediction-polls/backend/src/routes/AuthorizationRouter.js @@ -183,23 +183,7 @@ router.post("/signup", service.signup) * description: Server was not able to log in user with the given data. */ router.post("/google", googleService.googleLogIn) -router.get('/verify-email', async (req, res) => { - const token = req.query.token; - - try { - // Verify the token and update the user's email_verified status in the database - const verificationSuccessful = await verifyEmailToken(token); - - if (verificationSuccessful) { - res.send('Email successfully verified'); - } else { - res.status(400).send('Invalid or expired verification token'); - } - } catch (error) { - console.error("Error during email verification:", error); - res.status(500).send('Internal server error'); - } -}); +router.get('/verify-email', service.verifyEmail); module.exports = router; \ No newline at end of file diff --git a/prediction-polls/backend/src/services/AuthorizationService.js b/prediction-polls/backend/src/services/AuthorizationService.js index 869460e4..329347d5 100644 --- a/prediction-polls/backend/src/services/AuthorizationService.js +++ b/prediction-polls/backend/src/services/AuthorizationService.js @@ -176,5 +176,23 @@ function generateRefreshToken(user) { return jwt.sign(user,process.env.REFRESH_TOKEN_SECRET); } +async function verifyEmail(req, res){ + const token = req.query.token; + + try { + // Verify the token and update the user's email_verified status in the database + const verificationSuccessful = await db.verifyEmailToken(token); + + if (verificationSuccessful) { + res.send('Email successfully verified'); + } else { + res.status(400).send('Invalid or expired verification token'); + } + } catch (error) { + console.error("Error during email verification:", error); + res.status(500).send('Internal server error'); + } +} + module.exports = {homePage, signup, createAccessTokenFromRefreshToken, logIn, - logOut, authorizeAccessToken, generateAccessToken, generateRefreshToken} \ No newline at end of file + logOut, authorizeAccessToken, generateAccessToken, generateRefreshToken,verifyEmail} \ No newline at end of file From 6ff626d6912debca961c698f6afdd483cfbe947f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Serhat=20Hebun=20=C5=9Eim=C5=9Fek?= Date: Mon, 11 Dec 2023 18:40:41 +0300 Subject: [PATCH 026/281] Implementation of forgot password --- .../src/repositories/AuthorizationDB.js | 49 ++++++++++- .../backend/src/routes/AuthorizationRouter.js | 15 +++- .../src/services/AuthorizationService.js | 87 ++++++++++++++++++- 3 files changed, 147 insertions(+), 4 deletions(-) diff --git a/prediction-polls/backend/src/repositories/AuthorizationDB.js b/prediction-polls/backend/src/repositories/AuthorizationDB.js index eabb56ce..8915206c 100644 --- a/prediction-polls/backend/src/repositories/AuthorizationDB.js +++ b/prediction-polls/backend/src/repositories/AuthorizationDB.js @@ -174,5 +174,52 @@ function createTransporter() { } }); } +async function storePasswordResetToken(userId, token, expiresIn) { + const expirationTime = new Date(new Date().getTime() + expiresIn * 60000); // expiresIn in minutes -module.exports = {pool, addRefreshToken,checkRefreshToken,deleteRefreshToken,isUsernameOrEmailInUse,checkCredentials,addUser,findUser,saveEmailVerificationToken,verifyEmailToken,createTransporter} + // SQL query to update the user's reset token and expiration + const query = ` + UPDATE users + SET reset_token = ?, reset_token_expires = ? + WHERE id = ?; + `; + + try { + const result = await pool.query(query, [token, expirationTime, userId]); + return result; + } catch (error) { + console.error('Error storing password reset token:', error); + throw error; + } +} +async function getUserByResetToken(resetToken) { + try { + const query = 'SELECT * FROM users WHERE reset_token = ? AND reset_token_expires > NOW()'; + const [rows] = await pool.query(query, [resetToken]); + return rows.length ? rows[0] : null; + } catch (error) { + console.error('Error fetching user by reset token:', error); + throw error; + } +} +async function updateUserPassword(userId, hashedPassword) { + try { + const query = 'UPDATE users SET password = ? WHERE id = ?'; + const [result] = await pool.query(query, [hashedPassword, userId]); + return result; + } catch (error) { + console.error('Error updating user password:', error); + throw error; + } +} +async function clearResetToken(userId) { + try { + const query = 'UPDATE users SET reset_token = NULL, reset_token_expires = NULL WHERE id = ?'; + const [result] = await pool.query(query, [userId]); + return result; + } catch (error) { + console.error('Error clearing reset token:', error); + throw error; + } +} +module.exports = {pool, addRefreshToken,checkRefreshToken,deleteRefreshToken,isUsernameOrEmailInUse,checkCredentials,addUser,findUser,saveEmailVerificationToken,verifyEmailToken,createTransporter,storePasswordResetToken,getUserByResetToken,updateUserPassword,clearResetToken} diff --git a/prediction-polls/backend/src/routes/AuthorizationRouter.js b/prediction-polls/backend/src/routes/AuthorizationRouter.js index 5566b3ad..beb4e449 100644 --- a/prediction-polls/backend/src/routes/AuthorizationRouter.js +++ b/prediction-polls/backend/src/routes/AuthorizationRouter.js @@ -183,7 +183,18 @@ router.post("/signup", service.signup) * description: Server was not able to log in user with the given data. */ router.post("/google", googleService.googleLogIn) -router.get('/verify-email', service.verifyEmail); - +router.get('/verify-email', service.verifyEmail) +router.post('/request-password-reset', service.requestResetPassword) +// Route to complete the password reset process +router.post('/reset-password', async (req, res) => { + try { + const { token, newPassword } = req.body; + await service.resetPassword(token, newPassword); + res.send('Your password has been successfully reset.'); + } catch (error) { + console.error('Password reset error:', error); + res.status(500).send('Error resetting password.'); + } +}); module.exports = router; \ No newline at end of file diff --git a/prediction-polls/backend/src/services/AuthorizationService.js b/prediction-polls/backend/src/services/AuthorizationService.js index 329347d5..cdea9e25 100644 --- a/prediction-polls/backend/src/services/AuthorizationService.js +++ b/prediction-polls/backend/src/services/AuthorizationService.js @@ -194,5 +194,90 @@ async function verifyEmail(req, res){ } } +function generatePasswordResetToken() { + return new Promise((resolve, reject) => { + crypto.randomBytes(20, (err, buffer) => { + if (err) { + reject(err); + } else { + resolve(buffer.toString('hex')); + } + }); + }); +} +// Function to send password reset email +async function sendPasswordResetEmail(email, token) { + const transporter = db.createTransporter(); + const resetUrl = `http://ec2-3-78-169-139.eu-central-1.compute.amazonaws.com:3000/reset-password?token=${token}`; + + try { + await transporter.sendMail({ + from: '"Your App Name" ', // Update this + to: email, + subject: 'Password Reset Request', + html: ` +

You requested a password reset for your account.

+

Click the link below to reset your password:

+ ${resetUrl} +

If you did not request a password reset, please ignore this email.

+ ` + }); + + console.log('Password reset email sent successfully.'); +} catch (error) { + console.error('Error sending password reset email:', error); + throw error; +} +} + + +const SALT_ROUNDS = 10; // For bcrypt + +async function resetPassword(token, newPassword) { + try { + // Validate the token and get user details + const user = await db.getUserByResetToken(token); + if (!user) { + throw new Error('Invalid or expired password reset token.'); + } + + // Check if the token is expired + if (new Date() > new Date(user.reset_token_expires)) { + throw new Error('Password reset token has expired.'); + } + + // Hash the new password + const hashedPassword = await bcrypt.hash(newPassword, SALT_ROUNDS); + + // Update the user's password in the database + await db.updateUserPassword(user.id, hashedPassword); + + // Clear the reset token from the database + await db.clearResetToken(user.id); + + console.log('Password reset successfully.'); + } catch (error) { + console.error('Error resetting password:', error); + throw error; + } +} +async function requestResetPassword (req, res){ + try { + const { email } = req.body; + const user = await db.findUser(email); + + if (user) { + const token = await generatePasswordResetToken(); + // Store the token in the database (functionality not shown here) + await sendPasswordResetEmail(email, token); + } + + // Respond with a generic message either way to avoid enumeration attacks + res.send('If your email is in our system, you will receive a password reset link.'); + } catch (error) { + console.error('Password reset request error:', error); + res.status(500).send('Error processing password reset request.'); + } +}; module.exports = {homePage, signup, createAccessTokenFromRefreshToken, logIn, - logOut, authorizeAccessToken, generateAccessToken, generateRefreshToken,verifyEmail} \ No newline at end of file + logOut, authorizeAccessToken, generateAccessToken, generateRefreshToken, verifyEmail, sendPasswordResetEmail, resetPassword, requestResetPassword} \ No newline at end of file From 54ca74d2e518f1c205ac036a8cb5d77b8ec9d33a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Selin=20I=C5=9F=C4=B1k?= <56879777+selinisik@users.noreply.github.com> Date: Mon, 11 Dec 2023 20:10:38 +0300 Subject: [PATCH 027/281] fixed profile bug --- .../frontend/src/Pages/EditProfile/index.jsx | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/prediction-polls/frontend/src/Pages/EditProfile/index.jsx b/prediction-polls/frontend/src/Pages/EditProfile/index.jsx index 5199cf72..02ba400a 100644 --- a/prediction-polls/frontend/src/Pages/EditProfile/index.jsx +++ b/prediction-polls/frontend/src/Pages/EditProfile/index.jsx @@ -19,7 +19,7 @@ function EditProfile() { const [form] = Form.useForm(); const [initialValues, setInitialValues] = React.useState({ username: "", - // fullname: userData.name, + // fullname: profileData.name, about: "", birthday: "", isHidden: null, @@ -29,7 +29,7 @@ function EditProfile() { const fileInputRef = React.useRef(null); const [selectedFile, setSelectedFile] = React.useState(null); - const [userData, setUserData] = React.useState({}); + const [profileData, setUserData] = React.useState({}); const navigate = useNavigate(); React.useEffect(() => { @@ -68,10 +68,10 @@ function EditProfile() { const handleEditProfile = async () => { const formUserData = form.getFieldsValue(); const profileUpdateResult = await updateProfile({ - userId: userData.id, + userId: profileData.userId, username: formUserData.username, - email: userData.email, - profile_picture: userData.profileImage, + email: profileData.email, + profile_picture: profileData.profileImage, biography: formUserData.about, birthday: formUserData.birthday ? formUserData.birthday @@ -122,9 +122,9 @@ function EditProfile() { className={styles.profileImageContainer} onClick={handlePlaceholderClick} > - {selectedFile || userData.profile_picture ? ( + {selectedFile || profileData.profile_picture ? ( Profile @@ -177,7 +177,7 @@ function EditProfile() { {/*

COVER IMAGE

thumbnailImage @@ -213,13 +213,13 @@ function EditProfile() {
- +

Badges

(You can choose at most 3)

- {userData.badges && - userData.badges.length > 0 ? - userData.badges.map((badge, index) => ( + {profileData.badges && + profileData.badges.length > 0 ? + profileData.badges.map((badge, index) => (
From 1d863a23efe8a1db80372f203cffb3278d8391c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Serhat=20Hebun=20=C5=9Eim=C5=9Fek?= Date: Mon, 11 Dec 2023 20:48:59 +0300 Subject: [PATCH 028/281] fixes for forgot password --- .../backend/src/services/AuthorizationService.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/prediction-polls/backend/src/services/AuthorizationService.js b/prediction-polls/backend/src/services/AuthorizationService.js index cdea9e25..5c0f1194 100644 --- a/prediction-polls/backend/src/services/AuthorizationService.js +++ b/prediction-polls/backend/src/services/AuthorizationService.js @@ -64,7 +64,7 @@ async function signup(req, res) { async function sendVerificationEmail(email, token) { const transporter = db.createTransporter(); - const verificationUrl = `http://ec2-3-78-169-139.eu-central-1.compute.amazonaws.com:3000/verify-email?token=${token}`; + const verificationUrl = `http://ec2-3-78-169-139.eu-central-1.compute.amazonaws.com:3000/auth/verify-email?token=${token}`; const mailOptions = { from: '"Prediction Polls" ', @@ -208,11 +208,11 @@ function generatePasswordResetToken() { // Function to send password reset email async function sendPasswordResetEmail(email, token) { const transporter = db.createTransporter(); - const resetUrl = `http://ec2-3-78-169-139.eu-central-1.compute.amazonaws.com:3000/reset-password?token=${token}`; + const resetUrl = `http://ec2-3-78-169-139.eu-central-1.compute.amazonaws.com:3000/auth/reset-password?token=${token}`; try { await transporter.sendMail({ - from: '"Your App Name" ', // Update this + from: '"Prediction Polls" ', // Update this to: email, subject: 'Password Reset Request', html: ` From cc95dcc6890ebc61c23eda64cc9c16e43c07ac7d Mon Sep 17 00:00:00 2001 From: Sefik-Palazoglu Date: Mon, 11 Dec 2023 21:15:47 +0300 Subject: [PATCH 029/281] initialize annotations --- prediction-polls/annotations/.gitignore | 4 + .../annotations/config/swaggerOptions.js | 13 + prediction-polls/annotations/package.json | 22 ++ prediction-polls/annotations/src/app.js | 28 ++ .../src/routes/AnnotationRouter.js | 240 ++++++++++++++++++ .../src/services/AddContextService.js | 7 + .../src/services/AddTimeService.js | 7 + .../src/services/AnnotationService.js | 45 ++++ .../src/services/ValidationService.js | 68 +++++ 9 files changed, 434 insertions(+) create mode 100644 prediction-polls/annotations/.gitignore create mode 100644 prediction-polls/annotations/config/swaggerOptions.js create mode 100644 prediction-polls/annotations/package.json create mode 100644 prediction-polls/annotations/src/app.js create mode 100644 prediction-polls/annotations/src/routes/AnnotationRouter.js create mode 100644 prediction-polls/annotations/src/services/AddContextService.js create mode 100644 prediction-polls/annotations/src/services/AddTimeService.js create mode 100644 prediction-polls/annotations/src/services/AnnotationService.js create mode 100644 prediction-polls/annotations/src/services/ValidationService.js diff --git a/prediction-polls/annotations/.gitignore b/prediction-polls/annotations/.gitignore new file mode 100644 index 00000000..db4cdc13 --- /dev/null +++ b/prediction-polls/annotations/.gitignore @@ -0,0 +1,4 @@ +*.env +node_modules/* +package-lock.json +.eslintrc.js diff --git a/prediction-polls/annotations/config/swaggerOptions.js b/prediction-polls/annotations/config/swaggerOptions.js new file mode 100644 index 00000000..51f61dce --- /dev/null +++ b/prediction-polls/annotations/config/swaggerOptions.js @@ -0,0 +1,13 @@ +const swaggerOptions = { + definition: { + openapi: "3.0.0", + info: { + title: 'Prediction Poll API', + version: '1.0.0', + description: 'API Documentation for Prediction Poll API', + }, + }, + apis: ["./src/routes/*.js"], // Adjust this path to match your project structure +}; + +module.exports = swaggerOptions; \ No newline at end of file diff --git a/prediction-polls/annotations/package.json b/prediction-polls/annotations/package.json new file mode 100644 index 00000000..9bab4ea6 --- /dev/null +++ b/prediction-polls/annotations/package.json @@ -0,0 +1,22 @@ +{ + "name": "annotations", + "version": "1.0.0", + "description": "", + "main": "app.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "nodemon src/app.js" + }, + "author": "", + "license": "ISC", + "dependencies": { + "body-parser": "^1.20.2", + "cors": "^2.8.5", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "joi": "^17.11.0", + "mongodb": "^6.3.0", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.0" + } +} diff --git a/prediction-polls/annotations/src/app.js b/prediction-polls/annotations/src/app.js new file mode 100644 index 00000000..1ed53bd3 --- /dev/null +++ b/prediction-polls/annotations/src/app.js @@ -0,0 +1,28 @@ +const express = require('express'); +const cors = require("cors"); +const bodyParser = require('body-parser'); + +require('dotenv').config(); + +const annotationRouter = require('./routes/AnnotationRouter.js'); + +const app = express(); +const port = 4999; + +app.use(cors()); + +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); + +const swaggerJsDoc = require('swagger-jsdoc'); +const swaggerUi = require('swagger-ui-express'); +const swaggerOptions = require('../config/swaggerOptions.js'); + +const swaggerSpec = swaggerJsDoc(swaggerOptions); +app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)); + +app.use('/annotations', annotationRouter); + +app.listen(port, () => { + console.log(`Server is running on port ${port}`); +}); diff --git a/prediction-polls/annotations/src/routes/AnnotationRouter.js b/prediction-polls/annotations/src/routes/AnnotationRouter.js new file mode 100644 index 00000000..a4a7c743 --- /dev/null +++ b/prediction-polls/annotations/src/routes/AnnotationRouter.js @@ -0,0 +1,240 @@ +const router = require('express').Router(); +const service = require("../services/annotationService.js"); +const validation = require("../services/ValidationService.js"); +const timeService = require("../services/AddTimeService.js"); +const contextService = require("../services/addContextService.js"); + +/** + * @swagger + * components: + * schemas: + * Annotation: + * type: object + * required: + * - "@context" + * - "id" + * - "type" + * - "target" + * - "created" + * - "modified" + * properties: + * "@context": + * type: string + * description: The Annotation jsonld + * id: + * type: string + * description: The identity of the Annotation, an IRI + * type: + * type: string + * description: The type of the Annotation + * target: + * oneOf: + * - type: string + * - type: object + * properties: + * source: + * type: string + * description: Source IRI of the resource + * selector: + * oneOf: + * - type: object + * properties: + * "type": + * type: string + * description: The class of the Selector + * value: + * type: string + * description: The CSS selection path to the Segment OR The xpath to the selected segment + * - type: object + * properties: + * "type": + * type: string + * description: The class of the Selector + * exact: + * type: string + * description: A copy of the text which is being selected, after normalization + * prefix: + * type: string + * description: A snippet of text that occurs immediately before the text which is being selected + * suffix: + * type: string + * description: The snippet of text that occurs immediately after the text which is being selected. + * - type: object + * properties: + * "type": + * type: string + * description: The class of the Selector + * start: + * type: integer + * description: The starting position of the segment of text. The first character in the full text is character position 0, and the character is included within the segment + * end: + * type: integer + * description: The end position of the segment of text. The character is not included within the segment + * + * description: The relationship between a Specific Resource and a Selector. + * + * description: The relationship between an Annotation and its Target + * body: + * oneOf: + * - type: string + * - type: object + * properties: + * "type": + * type: string + * description: The type of the Textual Body resource. The Body should have the TextualBody class + * value: + * type: string + * description: The character sequence of the content of the Textual Body + * format: + * type: string + * description: The format of the Web Resource's content + * description: The relationship between an Annotation and its Body + * creator: + * type: string + * description: The agent responsible for creating the resource + * created: + * type: string + * description: The time at which the resource was created. The datetime must be a xsd:dateTime with the UTC timezone expressed as "Z". + * modified: + * type: string + * description: The time at which the resource was modified, after creation. Often the same with "created". The datetime must be a xsd:dateTime with the UTC timezone expressed as "Z". + * examples: + * Basic: + * value: + * "@context": http://www.w3.org/ns/anno.jsonld + * id: http://example.org/anno1 + * type: Annotation + * target: http://example.com/page1 + * created: 2023-12-10T20:06:46.123Z + * modified: 2023-12-10T20:06:46.123Z + * WithBodyAndCreator: + * value: + * "@context": http://www.w3.org/ns/anno.jsonld + * id: http://example.org/anno1 + * type: Annotation + * target: http://example.com/page1 + * body: http://example.org/post1 + * creator: user1 + * created: 2023-12-10T20:06:46.123Z + * modified: 2023-12-10T20:06:46.123Z + * EmbeddedTextBody: + * value: + * "@context": http://www.w3.org/ns/anno.jsonld + * id: http://example.org/anno1 + * type: Annotation + * target: http://example.com/page1 + * body: + * "type": TextualBody + * value: Example annotation content + * format: text/plain + * creator: user1 + * created: 2023-12-10T20:06:46.123Z + * modified: 2023-12-10T20:06:46.123Z + * CSSSelector: + * value: + * "@context": http://www.w3.org/ns/anno.jsonld + * id: http://example.org/anno1 + * type: Annotation + * target: + * source: "http://example.org/page1.html" + * selector: + * type: "CssSelector" + * value: "#elemid > .elemclass + p" + * body: + * "type": TextualBody + * value: Example annotation content + * format: text/plain + * creator: user1 + * created: 2023-12-10T20:06:46.123Z + * modified: 2023-12-10T20:06:46.123Z + * XPathSelector: + * value: + * "@context": http://www.w3.org/ns/anno.jsonld + * id: http://example.org/anno1 + * type: Annotation + * target: + * source: "http://example.org/page1.html" + * selector: + * type: "XPathSelector" + * value: "/html/body/p[2]/table/tr[2]/td[3]/span" + * body: + * "type": TextualBody + * value: Example annotation content + * format: text/plain + * creator: user1 + * created: 2023-12-10T20:06:46.123Z + * modified: 2023-12-10T20:06:46.123Z + * TextQuoteSelector: + * value: + * "@context": http://www.w3.org/ns/anno.jsonld + * id: http://example.org/anno1 + * type: Annotation + * target: + * source: "http://example.org/page1" + * selector: + * type: "TextQuoteSelector" + * exact: "anotation" + * prefix: "this is an " + * suffix: " that has some" + * body: + * "type": TextualBody + * value: "This seems to be a typo." + * format: text/plain + * creator: user1 + * created: 2023-12-10T20:06:46.123Z + * modified: 2023-12-10T20:06:46.123Z + * TextPositionSelector: + * value: + * "@context": http://www.w3.org/ns/anno.jsonld + * id: http://example.org/anno1 + * type: Annotation + * target: + * source: "http://example.org/page1" + * selector: + * type: "TextPositionSelector" + * start: 412 + * end: 795 + * body: + * "type": TextualBody + * value: "Example annotation content." + * format: text/plain + * creator: user1 + * created: 2023-12-10T20:06:46.123Z + * modified: 2023-12-10T20:06:46.123Z + */ + +/** + * @swagger + * /annotations: + * get: + * summary: Returns the list of all Annotations + * responses: + * 200: + * description: The list of the books retrieved successfully + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Annotation" + * examples: + * Basic: + * $ref: "#/components/examples/Basic" + * WithBodyAndCreator: + * $ref: "#/components/examples/WithBodyAndCreator" + * EmbeddedTextBody: + * $ref: "#/components/examples/EmbeddedTextBody" + * CSSSelector: + * $ref: "#/components/examples/CSSSelector" + * XPathSelector: + * $ref: "#/components/examples/XPathSelector" + * TextQuoteSelector: + * $ref: "#/components/examples/TextQuoteSelector" + * TextPositionSelector: + * $ref: "#/components/examples/TextPositionSelector" + */ +router.get('/', service.getAllAnnotations); + +router.post('/', validation.validate, contextService.attachContext, timeService.attachTimestamp, service.createAnnotation); + +module.exports = router; \ No newline at end of file diff --git a/prediction-polls/annotations/src/services/AddContextService.js b/prediction-polls/annotations/src/services/AddContextService.js new file mode 100644 index 00000000..886a7570 --- /dev/null +++ b/prediction-polls/annotations/src/services/AddContextService.js @@ -0,0 +1,7 @@ +async function attachContext(req, res, next) { + req.body["@context"] = "http://www.w3.org/ns/anno.jsonld"; + req.body.type = "Annotation"; + next(); +} + +module.exports = {attachContext}; \ No newline at end of file diff --git a/prediction-polls/annotations/src/services/AddTimeService.js b/prediction-polls/annotations/src/services/AddTimeService.js new file mode 100644 index 00000000..70de6583 --- /dev/null +++ b/prediction-polls/annotations/src/services/AddTimeService.js @@ -0,0 +1,7 @@ +async function attachTimestamp(req, res, next) { + req.body.created = new Date().toISOString(); + req.body.modified = new Date().toISOString(); + next(); +} + +module.exports = { attachTimestamp }; \ No newline at end of file diff --git a/prediction-polls/annotations/src/services/AnnotationService.js b/prediction-polls/annotations/src/services/AnnotationService.js new file mode 100644 index 00000000..d0b0b2b4 --- /dev/null +++ b/prediction-polls/annotations/src/services/AnnotationService.js @@ -0,0 +1,45 @@ +const { MongoClient, ServerApiVersion } = require('mongodb'); +require('dotenv').config(); + +const client = new MongoClient(process.env.MONGO_URI, { + serverApi: { + version: ServerApiVersion.v1, + strict: true, + deprecationErrors: true, + } +}); + +async function getAllAnnotations(req, res) { + try { + await client.connect(); + + const database = client.db(process.env.MONGO_DB); + const collection = database.collection(process.env.MONGO_COLLECTION); + + const annotations = await collection.find({}, { projection: { _id: 0 } }).toArray(); + + client.close(); + + res.json(annotations); + } catch (error) { + console.error('Error fetching annotations:', error); + res.status(500).json({ error: 'Internal Server Error' }); + } +} + +async function createAnnotation(req, res) { + try { + await client.connect(); + const database = client.db(process.env.MONGO_DB); + const collection = database.collection(process.env.MONGO_COLLECTION); + + const result = await collection.insertOne(req.body); + + res.status(200).json({success: true}); + } catch (error) { + console.log(error); + res.status(500).json({error: error}) + } +} + +module.exports = { getAllAnnotations, createAnnotation }; \ No newline at end of file diff --git a/prediction-polls/annotations/src/services/ValidationService.js b/prediction-polls/annotations/src/services/ValidationService.js new file mode 100644 index 00000000..09244d05 --- /dev/null +++ b/prediction-polls/annotations/src/services/ValidationService.js @@ -0,0 +1,68 @@ +const Joi = require("joi"); + +const textPositionSelectorSchema = Joi.object({ + type: Joi.string().valid('TextPositionSelector').required(), + start: Joi.number().required(), + end: Joi.number().required(), +}); + +const textQuoteSelectorSchema = Joi.object({ + type: Joi.string().valid('TextQuoteSelector').required(), + exact: Joi.string().required(), + prefix: Joi.string().required(), + suffix: Joi.string().required(), +}); + +const xPathSelectorSchema = Joi.object({ + type: Joi.string().valid('XPathSelector').required(), + value: Joi.string().required(), +}); + +const cssSelectorSchema = Joi.object({ + type: Joi.string().valid('CssSelector').required(), + value: Joi.string().required(), +}); + +const selectorSchema = Joi.alternatives().try( + textPositionSelectorSchema, + textQuoteSelectorSchema, + xPathSelectorSchema, + cssSelectorSchema +); + +const targetUriSchema = Joi.string().uri().required(); + +const targetObjectSchema = Joi.object({ + source: Joi.string().uri().required(), + selector: selectorSchema.required(), +}); + +const targetSchema = Joi.alternatives().try(targetUriSchema, targetObjectSchema); + +const bodyUriSchema = Joi.string().uri().required(); + +const bodyObjectSchema = Joi.object({ + type: Joi.string().valid("TextualBody").required(), + value: Joi.string().required(), + format: Joi.string().valid("text/plain").required() +}); + +const bodySchema = Joi.alternatives().try(bodyUriSchema, bodyObjectSchema); + +const annotationPostSchema = Joi.object({ + target: targetSchema.required(), + body: bodySchema, + creator: Joi.string().min(1) +}); + +async function validate(req, res, next) { + const {error, value} = annotationPostSchema.validate(req.body, {abortEarly: false}); + + if (error) { + console.log(error.details); + return res.status(400).json(error.details); + } + next(); +} + +module.exports = { validate }; \ No newline at end of file From 0f0c81f7d6f4b96640d4eff93ecc027f275175cf Mon Sep 17 00:00:00 2001 From: Sefik-Palazoglu Date: Mon, 11 Dec 2023 21:33:17 +0300 Subject: [PATCH 030/281] add swagger --- .../src/routes/AnnotationRouter.js | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/prediction-polls/annotations/src/routes/AnnotationRouter.js b/prediction-polls/annotations/src/routes/AnnotationRouter.js index a4a7c743..627683c0 100644 --- a/prediction-polls/annotations/src/routes/AnnotationRouter.js +++ b/prediction-polls/annotations/src/routes/AnnotationRouter.js @@ -8,6 +8,123 @@ const contextService = require("../services/addContextService.js"); * @swagger * components: * schemas: + * TextPositionSelector: + * type: object + * properties: + * type: + * type: string + * enum: [TextPositionSelector] + * required: true + * start: + * type: number + * required: true + * end: + * type: number + * required: true + * + * TextQuoteSelector: + * type: object + * properties: + * type: + * type: string + * enum: [TextQuoteSelector] + * required: true + * exact: + * type: string + * required: true + * prefix: + * type: string + * required: true + * suffix: + * type: string + * required: true + * + * XPathSelector: + * type: object + * properties: + * type: + * type: string + * enum: [XPathSelector] + * required: true + * value: + * type: string + * required: true + * + * CssSelector: + * type: object + * properties: + * type: + * type: string + * enum: [CssSelector] + * required: true + * value: + * type: string + * required: true + * + * Selector: + * oneOf: + * - $ref: "#/components/schemas/TextPositionSelector" + * - $ref: "#/components/schemas/TextQuoteSelector" + * - $ref: "#/components/schemas/XPathSelector" + * - $ref: "#/components/schemas/CssSelector" + * + * TargetUri: + * type: string + * format: uri + * required: true + * + * TargetObject: + * type: object + * properties: + * source: + * type: string + * format: uri + * required: true + * selector: + * $ref: "#/components/schemas/Selector" + * required: true + * + * Target: + * oneOf: + * - $ref: "#/components/schemas/TargetUri" + * - $ref: "#/components/schemas/TargetObject" + * + * BodyUri: + * type: string + * format: uri + * required: true + * + * BodyObject: + * type: object + * properties: + * type: + * type: string + * enum: [TextualBody] + * required: true + * value: + * type: string + * required: true + * format: + * type: string + * enum: [text/plain] + * required: true + * + * Body: + * oneOf: + * - $ref: "#/components/schemas/BodyUri" + * - $ref: "#/components/schemas/BodyObject" + * + * AnnotationPost: + * type: object + * properties: + * target: + * $ref: "#/components/schemas/Target" + * required: true + * body: + * $ref: "#/components/schemas/Body" + * creator: + * type: string + * minLength: 1 * Annotation: * type: object * required: @@ -98,6 +215,7 @@ const contextService = require("../services/addContextService.js"); * modified: * type: string * description: The time at which the resource was modified, after creation. Often the same with "created". The datetime must be a xsd:dateTime with the UTC timezone expressed as "Z". + * * examples: * Basic: * value: @@ -235,6 +353,23 @@ const contextService = require("../services/addContextService.js"); */ router.get('/', service.getAllAnnotations); +/** + * @swagger + * /annotations: + * post: + * summary: Create a new annotation + * requestBody: + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/AnnotationPost" + * responses: + * '200': + * description: Successfully created annotation + * '400': + * description: Bad Request - Invalid input data + */ router.post('/', validation.validate, contextService.attachContext, timeService.attachTimestamp, service.createAnnotation); + module.exports = router; \ No newline at end of file From c9b8ce531282b4c6002a6d9aadfc5739450e1cb1 Mon Sep 17 00:00:00 2001 From: Sefik-Palazoglu Date: Mon, 11 Dec 2023 21:33:34 +0300 Subject: [PATCH 031/281] change start end validation --- .../annotations/src/services/ValidationService.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prediction-polls/annotations/src/services/ValidationService.js b/prediction-polls/annotations/src/services/ValidationService.js index 09244d05..8763d863 100644 --- a/prediction-polls/annotations/src/services/ValidationService.js +++ b/prediction-polls/annotations/src/services/ValidationService.js @@ -2,8 +2,8 @@ const Joi = require("joi"); const textPositionSelectorSchema = Joi.object({ type: Joi.string().valid('TextPositionSelector').required(), - start: Joi.number().required(), - end: Joi.number().required(), + start: Joi.number().integer().positive().required(), + end: Joi.number().integer().positive().min(Joi.ref('start')).required(), }); const textQuoteSelectorSchema = Joi.object({ From d8b171a560c787a23f499240e7e6ea9c458a87ff Mon Sep 17 00:00:00 2001 From: EmreBatuhan <93476131+EmreBatuhan@users.noreply.github.com> Date: Mon, 11 Dec 2023 21:37:15 +0300 Subject: [PATCH 032/281] Change endpoint names --- prediction-polls/backend/src/routes/PollRouter.js | 8 ++++---- prediction-polls/backend/src/routes/ProfileRouter.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/prediction-polls/backend/src/routes/PollRouter.js b/prediction-polls/backend/src/routes/PollRouter.js index 71992434..1bafd759 100644 --- a/prediction-polls/backend/src/routes/PollRouter.js +++ b/prediction-polls/backend/src/routes/PollRouter.js @@ -149,7 +149,7 @@ router.get('/', service.getFamousPolls); /** * @swagger - * /polls/my-opened: + * /polls/opened/me: * get: * tags: * - polls @@ -214,11 +214,11 @@ router.get('/', service.getFamousPolls); * message: Error while accessing the database. * code: 3004 */ -router.get('/my-opened',authenticator.authorizeAccessToken,service.getOpenedPollsOfUser); +router.get('/opened/me',authenticator.authorizeAccessToken,service.getOpenedPollsOfUser); /** * @swagger - * /polls/my-voted: + * /polls/voted/me: * get: * tags: * - polls @@ -283,7 +283,7 @@ router.get('/my-opened',authenticator.authorizeAccessToken,service.getOpenedPoll * message: Error while accessing the database. * code: 3004 */ -router.get('/my-voted',authenticator.authorizeAccessToken,service.getVotedPollsOfUser); +router.get('/voted/me',authenticator.authorizeAccessToken,service.getVotedPollsOfUser); /** * @swagger diff --git a/prediction-polls/backend/src/routes/ProfileRouter.js b/prediction-polls/backend/src/routes/ProfileRouter.js index 492b68ae..8fbfd33f 100644 --- a/prediction-polls/backend/src/routes/ProfileRouter.js +++ b/prediction-polls/backend/src/routes/ProfileRouter.js @@ -253,7 +253,7 @@ router.patch('/', service.updateProfile); /** * @swagger - * /profiles/badges: + * /profiles/badges/me: * patch: * tags: * - profiles @@ -295,7 +295,7 @@ router.patch('/', service.updateProfile); * code: 1007, * message: Given data is not sufficient. Please follow guidelines. */ -router.patch('/badges',authenticator.authorizeAccessToken,service.updateBadge) +router.patch('/badges/me',authenticator.authorizeAccessToken,service.updateBadge) From 75204df68ac62ef3a6d2c370f6edd9649247c08b Mon Sep 17 00:00:00 2001 From: Sefik-Palazoglu Date: Mon, 11 Dec 2023 21:43:59 +0300 Subject: [PATCH 033/281] update swagger --- .../src/routes/AnnotationRouter.js | 65 ++----------------- 1 file changed, 7 insertions(+), 58 deletions(-) diff --git a/prediction-polls/annotations/src/routes/AnnotationRouter.js b/prediction-polls/annotations/src/routes/AnnotationRouter.js index 627683c0..892cc263 100644 --- a/prediction-polls/annotations/src/routes/AnnotationRouter.js +++ b/prediction-polls/annotations/src/routes/AnnotationRouter.js @@ -120,11 +120,15 @@ const contextService = require("../services/addContextService.js"); * target: * $ref: "#/components/schemas/Target" * required: true + * description: The relationship between an Annotation and its Target * body: * $ref: "#/components/schemas/Body" + * description: The relationship between an Annotation and its Body * creator: * type: string * minLength: 1 + * description: The agent responsible for creating the resource + * * Annotation: * type: object * required: @@ -145,66 +149,11 @@ const contextService = require("../services/addContextService.js"); * type: string * description: The type of the Annotation * target: - * oneOf: - * - type: string - * - type: object - * properties: - * source: - * type: string - * description: Source IRI of the resource - * selector: - * oneOf: - * - type: object - * properties: - * "type": - * type: string - * description: The class of the Selector - * value: - * type: string - * description: The CSS selection path to the Segment OR The xpath to the selected segment - * - type: object - * properties: - * "type": - * type: string - * description: The class of the Selector - * exact: - * type: string - * description: A copy of the text which is being selected, after normalization - * prefix: - * type: string - * description: A snippet of text that occurs immediately before the text which is being selected - * suffix: - * type: string - * description: The snippet of text that occurs immediately after the text which is being selected. - * - type: object - * properties: - * "type": - * type: string - * description: The class of the Selector - * start: - * type: integer - * description: The starting position of the segment of text. The first character in the full text is character position 0, and the character is included within the segment - * end: - * type: integer - * description: The end position of the segment of text. The character is not included within the segment - * - * description: The relationship between a Specific Resource and a Selector. - * + * $ref: "#/components/schemas/Target" + * required: true * description: The relationship between an Annotation and its Target * body: - * oneOf: - * - type: string - * - type: object - * properties: - * "type": - * type: string - * description: The type of the Textual Body resource. The Body should have the TextualBody class - * value: - * type: string - * description: The character sequence of the content of the Textual Body - * format: - * type: string - * description: The format of the Web Resource's content + * $ref: "#/components/schemas/Body" * description: The relationship between an Annotation and its Body * creator: * type: string From 155eb3941840add66cff89d6f47cedeba62d9e45 Mon Sep 17 00:00:00 2001 From: Sefik-Palazoglu Date: Mon, 11 Dec 2023 21:55:44 +0300 Subject: [PATCH 034/281] change func name --- .../annotations/src/routes/AnnotationRouter.js | 2 +- .../src/services/AnnotationService.js | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/prediction-polls/annotations/src/routes/AnnotationRouter.js b/prediction-polls/annotations/src/routes/AnnotationRouter.js index 892cc263..5848349e 100644 --- a/prediction-polls/annotations/src/routes/AnnotationRouter.js +++ b/prediction-polls/annotations/src/routes/AnnotationRouter.js @@ -300,7 +300,7 @@ const contextService = require("../services/addContextService.js"); * TextPositionSelector: * $ref: "#/components/examples/TextPositionSelector" */ -router.get('/', service.getAllAnnotations); +router.get('/', service.getAnnotations); /** * @swagger diff --git a/prediction-polls/annotations/src/services/AnnotationService.js b/prediction-polls/annotations/src/services/AnnotationService.js index d0b0b2b4..31b0d2a8 100644 --- a/prediction-polls/annotations/src/services/AnnotationService.js +++ b/prediction-polls/annotations/src/services/AnnotationService.js @@ -9,14 +9,25 @@ const client = new MongoClient(process.env.MONGO_URI, { } }); -async function getAllAnnotations(req, res) { +async function getAnnotations(req, res) { + const { creator, source } = req.query; + let filter = {}; + + if (creator) { + filter.creator = creator; + } + + if (source) { + filter['target.source'] = source; + } + try { await client.connect(); const database = client.db(process.env.MONGO_DB); const collection = database.collection(process.env.MONGO_COLLECTION); - const annotations = await collection.find({}, { projection: { _id: 0 } }).toArray(); + const annotations = await collection.find(filter, { projection: { _id: 0 } }).toArray(); client.close(); @@ -42,4 +53,4 @@ async function createAnnotation(req, res) { } } -module.exports = { getAllAnnotations, createAnnotation }; \ No newline at end of file +module.exports = { getAnnotations, createAnnotation }; \ No newline at end of file From 43165651f96441d8dd55dcbec693392f4407e428 Mon Sep 17 00:00:00 2001 From: Sefik-Palazoglu Date: Mon, 11 Dec 2023 22:02:54 +0300 Subject: [PATCH 035/281] accept only relative uris --- .../annotations/src/services/ValidationService.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/prediction-polls/annotations/src/services/ValidationService.js b/prediction-polls/annotations/src/services/ValidationService.js index 8763d863..58356436 100644 --- a/prediction-polls/annotations/src/services/ValidationService.js +++ b/prediction-polls/annotations/src/services/ValidationService.js @@ -30,10 +30,10 @@ const selectorSchema = Joi.alternatives().try( cssSelectorSchema ); -const targetUriSchema = Joi.string().uri().required(); +const targetUriSchema = Joi.string().uri({relativeOnly: true}).required(); const targetObjectSchema = Joi.object({ - source: Joi.string().uri().required(), + source: Joi.string().uri({relativeOnly: true}).required(), selector: selectorSchema.required(), }); From 8cdae9634f3bb3fa36fffd9bcfd39e308e1dcaad Mon Sep 17 00:00:00 2001 From: Sefik-Palazoglu Date: Mon, 11 Dec 2023 22:03:16 +0300 Subject: [PATCH 036/281] add the frontend link --- .../annotations/src/services/AddContextService.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/prediction-polls/annotations/src/services/AddContextService.js b/prediction-polls/annotations/src/services/AddContextService.js index 886a7570..f18f97ab 100644 --- a/prediction-polls/annotations/src/services/AddContextService.js +++ b/prediction-polls/annotations/src/services/AddContextService.js @@ -1,5 +1,10 @@ async function attachContext(req, res, next) { req.body["@context"] = "http://www.w3.org/ns/anno.jsonld"; + if (typeof req.body.target === 'string') { + req.body.target = process.env.APP_URI + req.body.target; + } else if (typeof req.body.target === 'object') { + req.body.target.source = process.env.APP_URI + req.body.target.source; + } req.body.type = "Annotation"; next(); } From b9230766126862a3bc979b9d6c99b374e8c054bd Mon Sep 17 00:00:00 2001 From: Sefik-Palazoglu Date: Mon, 11 Dec 2023 22:05:57 +0300 Subject: [PATCH 037/281] concatenate url properly --- .../annotations/src/services/AddContextService.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/prediction-polls/annotations/src/services/AddContextService.js b/prediction-polls/annotations/src/services/AddContextService.js index f18f97ab..d5524a10 100644 --- a/prediction-polls/annotations/src/services/AddContextService.js +++ b/prediction-polls/annotations/src/services/AddContextService.js @@ -1,9 +1,11 @@ +const url = require('url'); + async function attachContext(req, res, next) { req.body["@context"] = "http://www.w3.org/ns/anno.jsonld"; if (typeof req.body.target === 'string') { - req.body.target = process.env.APP_URI + req.body.target; + req.body.target = url.resolve(process.env.APP_URI, req.body.target);; } else if (typeof req.body.target === 'object') { - req.body.target.source = process.env.APP_URI + req.body.target.source; + req.body.target.source = url.resolve(process.env.APP_URI, req.body.target.source); } req.body.type = "Annotation"; next(); From bdc36f32e7748421850c6e62c8e8d2ebf90b4e32 Mon Sep 17 00:00:00 2001 From: Sefik-Palazoglu Date: Mon, 11 Dec 2023 22:06:18 +0300 Subject: [PATCH 038/281] fix typo --- prediction-polls/annotations/src/services/AddContextService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prediction-polls/annotations/src/services/AddContextService.js b/prediction-polls/annotations/src/services/AddContextService.js index d5524a10..c4717399 100644 --- a/prediction-polls/annotations/src/services/AddContextService.js +++ b/prediction-polls/annotations/src/services/AddContextService.js @@ -3,7 +3,7 @@ const url = require('url'); async function attachContext(req, res, next) { req.body["@context"] = "http://www.w3.org/ns/anno.jsonld"; if (typeof req.body.target === 'string') { - req.body.target = url.resolve(process.env.APP_URI, req.body.target);; + req.body.target = url.resolve(process.env.APP_URI, req.body.target); } else if (typeof req.body.target === 'object') { req.body.target.source = url.resolve(process.env.APP_URI, req.body.target.source); } From b2de686c339d6f476bbde73d7a7be8a388f23d34 Mon Sep 17 00:00:00 2001 From: Sefik-Palazoglu Date: Mon, 11 Dec 2023 22:48:45 +0300 Subject: [PATCH 039/281] refine swagger --- .../src/routes/AnnotationRouter.js | 96 ++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/prediction-polls/annotations/src/routes/AnnotationRouter.js b/prediction-polls/annotations/src/routes/AnnotationRouter.js index 5848349e..772da002 100644 --- a/prediction-polls/annotations/src/routes/AnnotationRouter.js +++ b/prediction-polls/annotations/src/routes/AnnotationRouter.js @@ -164,7 +164,11 @@ const contextService = require("../services/addContextService.js"); * modified: * type: string * description: The time at which the resource was modified, after creation. Often the same with "created". The datetime must be a xsd:dateTime with the UTC timezone expressed as "Z". - * +*/ + +/** + * @swagger + * components: * examples: * Basic: * value: @@ -270,11 +274,84 @@ const contextService = require("../services/addContextService.js"); * modified: 2023-12-10T20:06:46.123Z */ +/** + * @swagger + * components: + * examples: + * PostBasic: + * value: + * target: /page1 + * PostWithBodyAndCreator: + * value: + * target: /page1 + * body: http://example.org/post1 + * creator: user1 + * PostEmbeddedTextBody: + * value: + * target: /page1 + * body: + * "type": TextualBody + * value: Example annotation content + * format: text/plain + * creator: user1 + * PostCSSSelector: + * value: + * target: + * source: "/page1.html" + * selector: + * type: "CssSelector" + * value: "#elemid > .elemclass + p" + * body: + * "type": TextualBody + * value: Example annotation content + * format: text/plain + * creator: user1 + * PostXPathSelector: + * value: + * target: + * source: "/page1.html" + * selector: + * type: "XPathSelector" + * value: "/html/body/p[2]/table/tr[2]/td[3]/span" + * body: + * "type": TextualBody + * value: Example annotation content + * format: text/plain + * creator: user1 + * PostTextQuoteSelector: + * value: + * target: + * source: "/page1" + * selector: + * type: "TextQuoteSelector" + * exact: "anotation" + * prefix: "this is an " + * suffix: " that has some" + * body: + * "type": TextualBody + * value: "This seems to be a typo." + * format: text/plain + * creator: user1 + * PostTextPositionSelector: + * value: + * target: + * source: "/page1" + * selector: + * type: "TextPositionSelector" + * start: 412 + * end: 795 + * body: + * "type": TextualBody + * value: "Example annotation content." + * format: text/plain + * creator: user1 + */ + /** * @swagger * /annotations: * get: - * summary: Returns the list of all Annotations + * summary: Returns the list of Annotations according to query parameters * responses: * 200: * description: The list of the books retrieved successfully @@ -312,6 +389,21 @@ router.get('/', service.getAnnotations); * application/json: * schema: * $ref: "#/components/schemas/AnnotationPost" + * examples: + * Basic: + * $ref: "#/components/examples/PostBasic" + * WithBodyAndCreator: + * $ref: "#/components/examples/PostWithBodyAndCreator" + * EmbeddedTextBody: + * $ref: "#/components/examples/PostEmbeddedTextBody" + * CSSSelector: + * $ref: "#/components/examples/PostCSSSelector" + * XPathSelector: + * $ref: "#/components/examples/PostXPathSelector" + * TextQuoteSelector: + * $ref: "#/components/examples/PostTextQuoteSelector" + * TextPositionSelector: + * $ref: "#/components/examples/PostTextPositionSelector" * responses: * '200': * description: Successfully created annotation From e02e12a9909473f626feeeacb39fd235f00a014c Mon Sep 17 00:00:00 2001 From: Sefik-Palazoglu Date: Mon, 11 Dec 2023 22:48:53 +0300 Subject: [PATCH 040/281] add "id" field --- .../annotations/src/services/AnnotationService.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/prediction-polls/annotations/src/services/AnnotationService.js b/prediction-polls/annotations/src/services/AnnotationService.js index 31b0d2a8..197dcc7e 100644 --- a/prediction-polls/annotations/src/services/AnnotationService.js +++ b/prediction-polls/annotations/src/services/AnnotationService.js @@ -1,4 +1,5 @@ -const { MongoClient, ServerApiVersion } = require('mongodb'); +const { MongoClient, ServerApiVersion, ObjectId } = require('mongodb'); +const {parse, resolve, format} = require('url'); require('dotenv').config(); const client = new MongoClient(process.env.MONGO_URI, { @@ -46,6 +47,14 @@ async function createAnnotation(req, res) { const result = await collection.insertOne(req.body); + const uniqueIdentifier = result.insertedId instanceof ObjectId ? result.insertedId.toString(): null; + + const newIRI = resolve(process.env.ANNOTATION_URI, "/annotations") + '/' + uniqueIdentifier; + await collection.updateOne( + { _id: result.insertedId }, + { $set: { id: newIRI } } + ); + res.status(200).json({success: true}); } catch (error) { console.log(error); From 0c3d836d1bf0b9d0c593b37985d1c987909ff25a Mon Sep 17 00:00:00 2001 From: Sefik-Palazoglu Date: Mon, 11 Dec 2023 23:03:12 +0300 Subject: [PATCH 041/281] add getAnnotationWithId --- .../src/services/AnnotationService.js | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/prediction-polls/annotations/src/services/AnnotationService.js b/prediction-polls/annotations/src/services/AnnotationService.js index 197dcc7e..aa3c8c8a 100644 --- a/prediction-polls/annotations/src/services/AnnotationService.js +++ b/prediction-polls/annotations/src/services/AnnotationService.js @@ -39,6 +39,30 @@ async function getAnnotations(req, res) { } } +async function getAnnotationWithId(req, res) { + const annotationId = req.params.id; + + try { + await client.connect(); + + const database = client.db(process.env.MONGO_DB); + const collection = database.collection(process.env.MONGO_COLLECTION); + + const annotation = await collection.findOne({ id: new RegExp(`.*${annotationId}$`) }, {projection: {"_id": 0}}); + + if (!annotation) { + return res.status(404).json({ error: 'Annotation not found' }); + } + + client.close(); + + res.json(annotation); + } catch (error) { + console.error('Error fetching annotations:', error); + res.status(500).json({ error: 'Internal Server Error' }); + } +} + async function createAnnotation(req, res) { try { await client.connect(); @@ -62,4 +86,4 @@ async function createAnnotation(req, res) { } } -module.exports = { getAnnotations, createAnnotation }; \ No newline at end of file +module.exports = { getAnnotations, createAnnotation, getAnnotationWithId }; \ No newline at end of file From 9e1b080e44317343df5382356af2b496b04bc59f Mon Sep 17 00:00:00 2001 From: Sefik-Palazoglu Date: Mon, 11 Dec 2023 23:03:20 +0300 Subject: [PATCH 042/281] add swagger for getAnnotationWithId --- .../src/routes/AnnotationRouter.js | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/prediction-polls/annotations/src/routes/AnnotationRouter.js b/prediction-polls/annotations/src/routes/AnnotationRouter.js index 772da002..39178192 100644 --- a/prediction-polls/annotations/src/routes/AnnotationRouter.js +++ b/prediction-polls/annotations/src/routes/AnnotationRouter.js @@ -379,6 +379,63 @@ const contextService = require("../services/addContextService.js"); */ router.get('/', service.getAnnotations); +/** + * @swagger + * /annotations/{id}: + * get: + * summary: Get an annotation by ID + * parameters: + * - in: path + * name: id + * required: true + * description: The ID of the annotation + * schema: + * type: string + * responses: + * 200: + * description: The list of the books retrieved successfully + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: "#/components/schemas/Annotation" + * examples: + * Basic: + * $ref: "#/components/examples/Basic" + * WithBodyAndCreator: + * $ref: "#/components/examples/WithBodyAndCreator" + * EmbeddedTextBody: + * $ref: "#/components/examples/EmbeddedTextBody" + * CSSSelector: + * $ref: "#/components/examples/CSSSelector" + * XPathSelector: + * $ref: "#/components/examples/XPathSelector" + * TextQuoteSelector: + * $ref: "#/components/examples/TextQuoteSelector" + * TextPositionSelector: + * $ref: "#/components/examples/TextPositionSelector" + * 404: + * description: Annotation not found + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * 500: + * description: Internal Server Error + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + */ +router.get('/:id', service.getAnnotationWithId); + /** * @swagger * /annotations: From 00704ce54bf3bf12a5067bf5fcee507de390cf92 Mon Sep 17 00:00:00 2001 From: Sefik-Palazoglu Date: Mon, 11 Dec 2023 23:08:10 +0300 Subject: [PATCH 043/281] enable abortEarly --- prediction-polls/annotations/src/services/ValidationService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/prediction-polls/annotations/src/services/ValidationService.js b/prediction-polls/annotations/src/services/ValidationService.js index 58356436..8c98caf4 100644 --- a/prediction-polls/annotations/src/services/ValidationService.js +++ b/prediction-polls/annotations/src/services/ValidationService.js @@ -56,7 +56,7 @@ const annotationPostSchema = Joi.object({ }); async function validate(req, res, next) { - const {error, value} = annotationPostSchema.validate(req.body, {abortEarly: false}); + const {error, value} = annotationPostSchema.validate(req.body); if (error) { console.log(error.details); From ad0a60141b87704d0667f70957d7b4bd22423d2f Mon Sep 17 00:00:00 2001 From: Sefik-Palazoglu Date: Mon, 11 Dec 2023 23:53:59 +0300 Subject: [PATCH 044/281] add parameters to get --- .../annotations/src/routes/AnnotationRouter.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/prediction-polls/annotations/src/routes/AnnotationRouter.js b/prediction-polls/annotations/src/routes/AnnotationRouter.js index 39178192..88b634d6 100644 --- a/prediction-polls/annotations/src/routes/AnnotationRouter.js +++ b/prediction-polls/annotations/src/routes/AnnotationRouter.js @@ -352,6 +352,17 @@ const contextService = require("../services/addContextService.js"); * /annotations: * get: * summary: Returns the list of Annotations according to query parameters + * parameters: + * - in: query + * name: creator + * schema: + * type: string + * description: The creator of the Annotation + * - in: query + * name: source + * schema: + * type: string + * description: The source of the Target of the Annotations * responses: * 200: * description: The list of the books retrieved successfully From 623819dbf7d84eb322a9d3e7af0f539c9bd196cb Mon Sep 17 00:00:00 2001 From: Sefik-Palazoglu Date: Tue, 12 Dec 2023 00:23:16 +0300 Subject: [PATCH 045/281] add patch functionality --- .../src/routes/AnnotationRouter.js | 118 ++++++++++++++++++ .../src/services/AnnotationService.js | 55 +++++++- .../src/services/ValidationService.js | 12 +- 3 files changed, 183 insertions(+), 2 deletions(-) diff --git a/prediction-polls/annotations/src/routes/AnnotationRouter.js b/prediction-polls/annotations/src/routes/AnnotationRouter.js index 88b634d6..dc0fdbc2 100644 --- a/prediction-polls/annotations/src/routes/AnnotationRouter.js +++ b/prediction-polls/annotations/src/routes/AnnotationRouter.js @@ -347,6 +347,17 @@ const contextService = require("../services/addContextService.js"); * creator: user1 */ +/** + * @swagger + * components: + * examples: + * UpdatedBodyExample: + * value: + * "type": TextualBody + * value: "Updated Annotation Body" + * format: text/plain + */ + /** * @swagger * /annotations: @@ -480,5 +491,112 @@ router.get('/:id', service.getAnnotationWithId); */ router.post('/', validation.validate, contextService.attachContext, timeService.attachTimestamp, service.createAnnotation); +/** + * @swagger + * /annotations/{id}: + * delete: + * summary: Delete an annotation by ID + * parameters: + * - in: path + * name: id + * required: true + * description: The ID of the annotation to be deleted + * schema: + * type: string + * responses: + * 200: + * description: Annotation deleted successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * 404: + * description: Annotation not found + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * 500: + * description: Internal Server Error + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + */ +router.delete('/:id', service.deleteAnnotationWithId); + +/** + * @swagger + * /annotations/{id}: + * patch: + * summary: Update the body of an annotation by ID + * parameters: + * - in: path + * name: id + * required: true + * description: The ID of the annotation to be updated + * schema: + * type: string + * requestBody: + * description: Updated body of the annotation + * required: true + * content: + * application/json: + * schema: + * $ref: "#/components/schemas/Body" + * examples: + * BodyUpdateExample: + * $ref: "#/components/examples/UpdatedBodyExample" + * responses: + * 200: + * description: Annotation updated successfully + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + * 400: + * description: Bad Request - Invalid request body format + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * properties: + * message: + * type: string + * 404: + * description: Annotation not found + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + * 500: + * description: Internal Server Error + * content: + * application/json: + * schema: + * type: object + * properties: + * error: + * type: string + */ +router.patch('/:id', validation.validatePatchBody, service.patchAnnotationWithId); + module.exports = router; \ No newline at end of file diff --git a/prediction-polls/annotations/src/services/AnnotationService.js b/prediction-polls/annotations/src/services/AnnotationService.js index aa3c8c8a..8db78c1e 100644 --- a/prediction-polls/annotations/src/services/AnnotationService.js +++ b/prediction-polls/annotations/src/services/AnnotationService.js @@ -86,4 +86,57 @@ async function createAnnotation(req, res) { } } -module.exports = { getAnnotations, createAnnotation, getAnnotationWithId }; \ No newline at end of file +async function deleteAnnotationWithId(req, res) { + const annotationId = req.params.id; + + try { + await client.connect(); + + const database = client.db(process.env.MONGO_DB); + const collection = database.collection(process.env.MONGO_COLLECTION); + + const result = await collection.deleteOne({ id: new RegExp(`.*${annotationId}$`) }); + + if (result.deletedCount === 0) { + return res.status(404).json({ error: 'Annotation not found' }); + } + + client.close(); + + res.json({ message: 'Annotation deleted successfully' }); + } catch (error) { + console.error('Error deleting annotation:', error); + res.status(500).json({ error: 'Internal Server Error' }); + } +} + +async function patchAnnotationWithId(req, res) { + const annotationId = req.params.id; + const updatedBody = req.body; + + try { + await client.connect(); + + const database = client.db(process.env.MONGO_DB); + const collection = database.collection(process.env.MONGO_COLLECTION); + + const result = await collection.updateOne( + { id: new RegExp(`.*${annotationId}$`) }, + { $set: { body: updatedBody } } + ); + + if (result.matchedCount === 0) { + return res.status(404).json({ error: 'Annotation not found' }); + } + + client.close(); + + res.json({ message: 'Annotation updated successfully' }); + } catch (error) { + console.error('Error updating annotation:', error); + res.status(500).json({ error: 'Internal Server Error' }); + } +} + + +module.exports = { getAnnotations, createAnnotation, getAnnotationWithId, deleteAnnotationWithId, patchAnnotationWithId }; \ No newline at end of file diff --git a/prediction-polls/annotations/src/services/ValidationService.js b/prediction-polls/annotations/src/services/ValidationService.js index 8c98caf4..094071c3 100644 --- a/prediction-polls/annotations/src/services/ValidationService.js +++ b/prediction-polls/annotations/src/services/ValidationService.js @@ -65,4 +65,14 @@ async function validate(req, res, next) { next(); } -module.exports = { validate }; \ No newline at end of file +async function validatePatchBody(req, res, next) { + const {error, value} = bodySchema.validate(req.body); + + if (error) { + console.log(error.details); + return res.status(400).json(error.details); + } + next(); +} + +module.exports = { validate, validatePatchBody }; \ No newline at end of file From 3175c9ab0ab19fba8d56b6a5d3ac033eb03fb280 Mon Sep 17 00:00:00 2001 From: kutaysaran <74209499+kutaysaran@users.noreply.github.com> Date: Tue, 12 Dec 2023 01:48:52 +0300 Subject: [PATCH 046/281] Reset Password Page --- .../{ForgotPassword.jsx => index.jsx} | 30 +++- .../ResetPassword/ResetPassword.module.css | 94 +++++++++++ .../src/Pages/Auth/ResetPassword/index.jsx | 147 ++++++++++++++++++ .../frontend/src/Routes/Router.jsx | 4 +- 4 files changed, 272 insertions(+), 3 deletions(-) rename prediction-polls/frontend/src/Pages/Auth/ForgotPassword/{ForgotPassword.jsx => index.jsx} (73%) create mode 100644 prediction-polls/frontend/src/Pages/Auth/ResetPassword/ResetPassword.module.css create mode 100644 prediction-polls/frontend/src/Pages/Auth/ResetPassword/index.jsx diff --git a/prediction-polls/frontend/src/Pages/Auth/ForgotPassword/ForgotPassword.jsx b/prediction-polls/frontend/src/Pages/Auth/ForgotPassword/index.jsx similarity index 73% rename from prediction-polls/frontend/src/Pages/Auth/ForgotPassword/ForgotPassword.jsx rename to prediction-polls/frontend/src/Pages/Auth/ForgotPassword/index.jsx index fc4865fe..c87b1cf5 100644 --- a/prediction-polls/frontend/src/Pages/Auth/ForgotPassword/ForgotPassword.jsx +++ b/prediction-polls/frontend/src/Pages/Auth/ForgotPassword/index.jsx @@ -13,9 +13,34 @@ import "../../../index.css"; function ForgotPassword() { const [form] = Form.useForm(); const navigate = useNavigate(); + const [email, setEmail ] = React.useState(""); + const [message, setMessage] = React.useState(""); const handleSubmit = async (values) => { + navigate("/auth/sign-in"); }; + const handleForgotPassword = async (e) => { + e.preventDefault(); + try { + const requestOptions = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: email, + }) + }; + const response = await fetch(process.env.REACT_APP_BACKEND_LINK+'/auth/request-password-reset', requestOptions); + const data = await response.json(); + console.log(data); + if (response.status === 201 && data.accessToken && data.refreshToken) { + navigate("/feed"); + } + + } + catch (error) { + setMessage("An unexpected error has occurred. Please try again!") + } + }; return (
@@ -27,7 +52,7 @@ function ForgotPassword() { labelCol={{ span: 24 }} wrapperCol={{ span: 24 }} form={form} - onFinish={handleSubmit} + onFinish={handleForgotPassword} validateTrigger="onSubmit" >
@@ -50,6 +75,7 @@ function ForgotPassword() { > setEmail(e.target.value)} type="text" className={styles.formInputStyle} placeholder="example@outlook.com" @@ -61,7 +87,7 @@ function ForgotPassword() { type="primary" htmlType="submit" className={styles.formButtonStyle} - onClick={handleSubmit} + onClick={handleForgotPassword} > Continue diff --git a/prediction-polls/frontend/src/Pages/Auth/ResetPassword/ResetPassword.module.css b/prediction-polls/frontend/src/Pages/Auth/ResetPassword/ResetPassword.module.css new file mode 100644 index 00000000..3252d293 --- /dev/null +++ b/prediction-polls/frontend/src/Pages/Auth/ResetPassword/ResetPassword.module.css @@ -0,0 +1,94 @@ + .splitContainerStyle { + display: flex; + width: 100%; + margin: 0 auto; + height: 100vh; + } + + .animationStyle { + max-width: 100%; + max-height: 100%; + } + + + .displayCenterStyle { + display: flex; + align-items: center; + justify-content: center; + } + + .formInputStyle { + padding: 5px 5px; + width: 100%; + margin-bottom: 25px; + } + + .formButtonStyle { + display: flex; + align-items: center; + justify-content: center; + padding: 22px 0px; + margin-bottom: 15px; + width: 100%; + opacity: 0.85; + background: var(--primary-500); + } + + .imageContainerStyle { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + background: linear-gradient(var(--primary-50), var(--primary-100)); + } + + .formContainerStyle { + flex: 1; + padding: 20px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + +.logoStyle { + display: flex; + align-items: center; + justify-content: center; + color: var(--neutral-100); + margin-bottom: 20px; + max-width: 80%; + max-height: 80%; +} + +h2,h5 { + display: flex; + align-items: center; + justify-content: center; + font-family: sans-serif ; + font-weight: 500; + color: var(--neutral-700); +} + +h2{ + margin-bottom: 40px; +} + +.headerContainerStyle h5 { + max-width: 310px; + text-align: center; + margin: 0 auto; + word-wrap: break-word; + margin-bottom: 45px; +} + + @media (max-width: 768px) { + .imageContainerStyle{ + display: none; + } + .headerContainerStyle h5 { + font-size: 14px; + padding: 0 10px; + } + + } \ No newline at end of file diff --git a/prediction-polls/frontend/src/Pages/Auth/ResetPassword/index.jsx b/prediction-polls/frontend/src/Pages/Auth/ResetPassword/index.jsx new file mode 100644 index 00000000..f863a235 --- /dev/null +++ b/prediction-polls/frontend/src/Pages/Auth/ResetPassword/index.jsx @@ -0,0 +1,147 @@ +import React, { useState } from "react"; +import { + Button, + Input, + Form, +} from "antd"; +import styles from "./ResetPassword.module.css"; +import { Link, useNavigate, useLocation } from "react-router-dom"; +import { ReactComponent as Logo } from "../../../Assets/Logo.svg"; +import { ReactComponent as SignPageAnimation } from "../../../Assets/SignPageAnimation.svg"; +import "../../../index.css"; + +function ResetPassword() { + const [form] = Form.useForm(); + const navigate = useNavigate(); + const [message, setMessage] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const location = useLocation(); + + const handleResetPassword = async (e) => { + e.preventDefault(); + const token = new URLSearchParams(location.search).get('token'); + try { + const requestOptions = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + token: token, + newPassword: newPassword + }) + }; + const response = await fetch(process.env.REACT_APP_BACKEND_LINK+'/auth/reset-password', requestOptions); + const data = await response.json(); + console.log(data); + // if (response.status === 201 && data.accessToken && data.refreshToken) { + // navigate("/feed"); + // } + + } + catch (error) { + setMessage("An unexpected error has occurred. Please try again!") + } + }; + + return ( +
+
+ + + +
+
+

Reset your password

+
Enter your new password below and confirm it to reset your password.
+
+ + setNewPassword(e.target.value)} + type="text" + className={styles.formInputStyle} + placeholder="New Password" + /> + + + setNewPassword(e.target.value)} + type="text" + className={styles.formInputStyle} + placeholder="Confirm Password" + /> + + +
+ +
+
+ +
+ + Back To Home + +
+
+
+
+
+ {/* Our sign up image from mock-up */} + +
+
+ ); +} + +export default ResetPassword; diff --git a/prediction-polls/frontend/src/Routes/Router.jsx b/prediction-polls/frontend/src/Routes/Router.jsx index 369a2d4e..a015423b 100644 --- a/prediction-polls/frontend/src/Routes/Router.jsx +++ b/prediction-polls/frontend/src/Routes/Router.jsx @@ -14,7 +14,8 @@ import Vote from '../Pages/Vote'; import PrivateRoute from '../Components/PrivateRoute'; import GoogleLogin from '../Pages/Auth/Google' import EditProfile from '../Pages/EditProfile'; -import ForgotPassword from '../Pages/Auth/ForgotPassword/ForgotPassword'; +import ForgotPassword from '../Pages/Auth/ForgotPassword'; +import ResetPassword from '../Pages/Auth/ResetPassword'; function AppRouter() { return ( @@ -29,6 +30,7 @@ function AppRouter() { } /> } /> } /> + } /> } /> From cdf63b4ced06569327fc08e135550a3b29043673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Selin=20I=C5=9F=C4=B1k?= <56879777+selinisik@users.noreply.github.com> Date: Tue, 12 Dec 2023 03:14:02 +0300 Subject: [PATCH 047/281] added badge select and fixed birthday isHidden --- .../frontend/src/Pages/EditProfile/index.jsx | 81 ++++++++++++++----- .../frontend/src/Pages/Profile/index.jsx | 7 +- .../frontend/src/api/requests/badgeSelect.jsx | 31 +++++++ 3 files changed, 95 insertions(+), 24 deletions(-) create mode 100644 prediction-polls/frontend/src/api/requests/badgeSelect.jsx diff --git a/prediction-polls/frontend/src/Pages/EditProfile/index.jsx b/prediction-polls/frontend/src/Pages/EditProfile/index.jsx index 5199cf72..d51f6f56 100644 --- a/prediction-polls/frontend/src/Pages/EditProfile/index.jsx +++ b/prediction-polls/frontend/src/Pages/EditProfile/index.jsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect } from "react"; import Menu from "../../Components/Menu"; import styles from "./EditProfile.module.css"; import { useParams } from "react-router-dom"; @@ -13,6 +13,7 @@ import uploadProfilePhoto from "../../api/requests/uploadProfilePhoto.jsx"; import updateProfile from "../../api/requests/editProfile.jsx"; import { useNavigate } from "react-router-dom"; import Badge from "../../Components/Badge/index.jsx"; +import badgeSelect from "../../api/requests/badgeSelect.jsx"; function EditProfile() { const { username } = useParams(); @@ -28,9 +29,11 @@ function EditProfile() { const [caption, setCaption] = React.useState(""); const fileInputRef = React.useRef(null); const [selectedFile, setSelectedFile] = React.useState(null); + const [tempBadgeSelections, setTempBadgeSelections] = React.useState({}); const [userData, setUserData] = React.useState({}); const navigate = useNavigate(); + console.log("tempBadgeSelections", tempBadgeSelections); React.useEffect(() => { const fetchData = async () => { @@ -42,8 +45,10 @@ function EditProfile() { username: response.username, // fullname: response.name, about: response.biography, - birthday: response.birthday ? moment(response.birthday) : null, - isHidden: response.isHidden, + birthday: response.birthday + ? moment(response.birthday, "YYYY-MM-DD") + : null, + isHidden: response.isHidden !== null ? !response.isHidden : true, }; setInitialValues(newInitialValues); form.setFieldsValue(newInitialValues); @@ -56,6 +61,21 @@ function EditProfile() { fetchData(); }, [username]); + React.useEffect(() => { + + if (userData.badges) { + const initialSelections = userData.badges.reduce((acc, badge) => { + acc[badge.id] = badge.isSelected === 1; + return acc; + }, {}); + setTempBadgeSelections(initialSelections); + } + }, [userData.badges]); + + const handleBadgeChange = (badgeId, isSelected) => { + setTempBadgeSelections((prev) => ({ ...prev, [badgeId]: isSelected })); + }; + const submitImage = async () => { const result = await uploadProfilePhoto(file, caption); if (result) { @@ -73,10 +93,8 @@ function EditProfile() { email: userData.email, profile_picture: userData.profileImage, biography: formUserData.about, - birthday: formUserData.birthday - ? formUserData.birthday - : null, - isHidden: formUserData.isHidden, + birthday: formUserData.birthday ? formUserData.birthday.format("YYYY-MM-DD") : null, + isHidden: formUserData.isHidden ? 0 : 1, }); if (profileUpdateResult) { @@ -86,6 +104,15 @@ function EditProfile() { console.error("Error uploading the profile image."); } } + for (const [badgeId, isSelected] of Object.entries(tempBadgeSelections)) { + if ( + isSelected !== + userData.badges.find((badge) => badge.id === badgeId)?.isSelected + ) { + console.log("Updating badge selection", badgeId, isSelected); + await badgeSelect({ badgeId, isSelected }); + } + } navigate(`/profile/${formUserData.username}`); } else { @@ -182,22 +209,25 @@ function EditProfile() { className={styles.thumbnailImage} > */} - BIRTHDAY} - name="birthday" - htmlFor="birthday" - className={styles.formItem} - > -
+
+ BIRTHDAY} + name="birthday" + htmlFor="birthday" + className={styles.formItem} + > - Show in profile -
- + + + Show in profile + +
+
- +

Badges

(You can choose at most 3)

- {userData.badges && - userData.badges.length > 0 ? + {userData.badges && userData.badges.length > 0 ? ( userData.badges.map((badge, index) => (
- + + handleBadgeChange(badge.id, e.target.checked) + } + />
- )) :

No badges yet.

} + )) + ) : ( +

No badges yet.

+ )}
diff --git a/prediction-polls/frontend/src/Pages/Profile/index.jsx b/prediction-polls/frontend/src/Pages/Profile/index.jsx index 34cdfc8e..1a316520 100644 --- a/prediction-polls/frontend/src/Pages/Profile/index.jsx +++ b/prediction-polls/frontend/src/Pages/Profile/index.jsx @@ -3,6 +3,7 @@ import Menu from "../../Components/Menu"; import styles from "./Profile.module.css"; import Users from "../../MockData/Users.json"; import EditIcon from "../../Assets/icons/EditIcon.jsx"; +import moment from "moment"; import PollCard from "../../Components/PollCard"; import { useNavigate } from "react-router-dom"; @@ -146,11 +147,13 @@ function Profile() {

About

{userData.biography}

+ {(userData.isHidden == 0 && userData.birthday!= null)&&

{moment(userData.birthday, "YYYY-MM-DD").format("MMMM Do, YYYY")}

}
{userData.badges && - userData.badges.length > 0 && - userData.badges.map((badge, index) => ( + userData.badges + .filter(badge => badge.isSelected !== 0) + .map((badge, index) => ( ))}
diff --git a/prediction-polls/frontend/src/api/requests/badgeSelect.jsx b/prediction-polls/frontend/src/api/requests/badgeSelect.jsx new file mode 100644 index 00000000..706b5743 --- /dev/null +++ b/prediction-polls/frontend/src/api/requests/badgeSelect.jsx @@ -0,0 +1,31 @@ +const baseUrl = process.env.REACT_APP_BACKEND_LINK; + +const badgeSelect = async (badgeData) => { + + const accessToken = localStorage.getItem("accessToken"); + console.log("badgeData", badgeData) + + try { + const response = await fetch(`${baseUrl}/profiles/badges/me`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(badgeData) + }); + + if (response.ok) { + const data = await response.json(); + return data; + } else { + console.error("Error in badgeSelect:", response.statusText); + return null; + } + } catch (error) { + console.error("Network error:", error); + return false; + } + }; + +export default badgeSelect; \ No newline at end of file From 137027dc1be5d895a6352ce71ae0256c68fa7b38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Selin=20I=C5=9F=C4=B1k?= <56879777+selinisik@users.noreply.github.com> Date: Tue, 12 Dec 2023 14:14:49 +0300 Subject: [PATCH 048/281] created voted polls page --- .../src/Pages/Vote/VoteList.module.css | 22 +++++++++ .../frontend/src/Pages/Vote/voteList.jsx | 48 +++++++++++++++++++ .../frontend/src/Routes/Router.jsx | 4 ++ .../src/api/requests/getPollsVotedMe.jsx | 25 ++++++++++ 4 files changed, 99 insertions(+) create mode 100644 prediction-polls/frontend/src/Pages/Vote/VoteList.module.css create mode 100644 prediction-polls/frontend/src/Pages/Vote/voteList.jsx create mode 100644 prediction-polls/frontend/src/api/requests/getPollsVotedMe.jsx diff --git a/prediction-polls/frontend/src/Pages/Vote/VoteList.module.css b/prediction-polls/frontend/src/Pages/Vote/VoteList.module.css new file mode 100644 index 00000000..16af690f --- /dev/null +++ b/prediction-polls/frontend/src/Pages/Vote/VoteList.module.css @@ -0,0 +1,22 @@ +.page{ + display: flex; + background-color: var(--neutral-white); + width: 100%; + box-sizing: border-box; +} +@media (max-width: 768px) { + .page{ + flex-direction: column; + } + .pointsButton{ + display:none; + } + .pollList{ + display: flex; + flex-direction: column; + width: 100%; + padding: 20px; + justify-content: center; + align-items: center; + } +} \ No newline at end of file diff --git a/prediction-polls/frontend/src/Pages/Vote/voteList.jsx b/prediction-polls/frontend/src/Pages/Vote/voteList.jsx new file mode 100644 index 00000000..8cfc3690 --- /dev/null +++ b/prediction-polls/frontend/src/Pages/Vote/voteList.jsx @@ -0,0 +1,48 @@ +import React, { useEffect, useState } from 'react'; +import PollCard from "../../Components/PollCard"; +import Menu from "../../Components/Menu"; +import styles from './VoteList.module.css'; +import getPollsVotedMe from '../../api/requests/getPollsVotedMe'; +import PointsButton from '../../Components/PointsButton'; +import getProfileMe from '../../api/requests/profileMe'; + +function VoteList() { + const [polls, setPolls] = useState([]); + const [userData, setUserData] = useState({}); + + React.useEffect(() => { + const data = getProfileMe(); + data.then((result) => { + setUserData(result); + }); + }, []); + + useEffect(() => { + const fetchData = async () => { + const pollData = await getPollsVotedMe(); + setPolls(pollData); + } + fetchData(); + }, []); + + return ( +
+ +
+ + {polls?.map((poll) => ( + + ))} +
+
+ +
+
+ ); +} + +export default VoteList; \ No newline at end of file diff --git a/prediction-polls/frontend/src/Routes/Router.jsx b/prediction-polls/frontend/src/Routes/Router.jsx index 9f11b931..d1bb9e2d 100644 --- a/prediction-polls/frontend/src/Routes/Router.jsx +++ b/prediction-polls/frontend/src/Routes/Router.jsx @@ -15,6 +15,7 @@ import PrivateRoute from '../Components/PrivateRoute'; import GoogleLogin from '../Pages/Auth/Google' import EditProfile from '../Pages/EditProfile'; import ForgotPassword from '../Pages/Auth/ForgotPassword/ForgotPassword'; +import VoteList from '../Pages/Vote/voteList'; function AppRouter() { return ( @@ -60,6 +61,9 @@ function AppRouter() { } /> + + } /> } /> diff --git a/prediction-polls/frontend/src/api/requests/getPollsVotedMe.jsx b/prediction-polls/frontend/src/api/requests/getPollsVotedMe.jsx new file mode 100644 index 00000000..3006ce05 --- /dev/null +++ b/prediction-polls/frontend/src/api/requests/getPollsVotedMe.jsx @@ -0,0 +1,25 @@ +const baseUrl = process.env.REACT_APP_BACKEND_LINK; // Replace with your actual base URL + +async function getPollsVotedMe() { + const accessToken = localStorage.getItem("accessToken"); + try { + const response = await fetch(`${baseUrl}/polls/voted/me`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + }); + + if (response.ok) { + const data = await response.json(); + return data; + } else { + return null; + } + } catch (error) { + return false; + } +} + +export default getPollsVotedMe; From 43f4b02aab2e89810a10d2b270feca1d86a6670c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Selin=20I=C5=9F=C4=B1k?= <56879777+selinisik@users.noreply.github.com> Date: Tue, 12 Dec 2023 14:15:24 +0300 Subject: [PATCH 049/281] added time input to date values --- .../Components/PollCard/PollCard.module.css | 1 + .../src/Components/PollCard/index.jsx | 39 +- .../src/Pages/Create/Create.module.css | 50 ++- .../frontend/src/Pages/Create/index.jsx | 347 ++++++++++-------- .../frontend/src/Pages/Profile/index.jsx | 37 +- .../src/api/requests/getPollsOpenedMe.jsx | 25 ++ 6 files changed, 302 insertions(+), 197 deletions(-) create mode 100644 prediction-polls/frontend/src/api/requests/getPollsOpenedMe.jsx diff --git a/prediction-polls/frontend/src/Components/PollCard/PollCard.module.css b/prediction-polls/frontend/src/Components/PollCard/PollCard.module.css index 5cb93904..0d5667bf 100644 --- a/prediction-polls/frontend/src/Components/PollCard/PollCard.module.css +++ b/prediction-polls/frontend/src/Components/PollCard/PollCard.module.css @@ -157,6 +157,7 @@ display: flex; flex-direction: column; width: 100%; + gap:10px; } .customOptionText{ font-size: 16px; diff --git a/prediction-polls/frontend/src/Components/PollCard/index.jsx b/prediction-polls/frontend/src/Components/PollCard/index.jsx index d6efd15c..17900a6c 100644 --- a/prediction-polls/frontend/src/Components/PollCard/index.jsx +++ b/prediction-polls/frontend/src/Components/PollCard/index.jsx @@ -6,7 +6,7 @@ import { ReactComponent as CommentIcon } from "../../Assets/icons/Comment.svg"; import { ReactComponent as ShareIcon } from "../../Assets/icons/Share.svg"; import { ReactComponent as ReportIcon } from "../../Assets/icons/Warning.svg"; import PollOption from "../PollOption"; -import { Input, DatePicker } from "antd"; +import { Input, DatePicker, TimePicker } from "antd"; import { useLocation } from "react-router-dom"; import ProfileIcon from "../../Assets/icons/ProfileIcon.jsx"; import getProfile from "../../api/requests/profile.jsx"; @@ -19,13 +19,14 @@ function PollCard({ PollData, setAnswer, onClick }) { JSON.parse(JSON.stringify(PollData)) ); const [userData, setUserData] = React.useState({}); + const [selectedDate, setSelectedDate] = React.useState(null); + const [selectedTime, setSelectedTime] = React.useState(null); const navigate = useNavigate(); const location = useLocation(); const [isVotePath, setIsVotePath] = React.useState( /^\/vote\//.test(location.pathname) ); - useEffect(() => { const data = getProfile(PollData.creatorUsername); data.then((result) => { @@ -33,7 +34,6 @@ function PollCard({ PollData, setAnswer, onClick }) { }); }, []); - React.useEffect(() => { setIsVotePath(/^\/vote\//.test(location.pathname)); }, [location.pathname]); @@ -64,6 +64,26 @@ function PollCard({ PollData, setAnswer, onClick }) { : 0; }; + const handleDateChange = (_, dateString) => { + setSelectedDate(dateString); + combineDateTime(dateString, selectedTime); + }; + + const handleTimeChange = (_, timeString) => { + setSelectedTime(timeString); + combineDateTime(selectedDate, timeString); + }; + + const combineDateTime = (date, time) => { + if (date && time) { + setAnswer(`${date}T${time}`); + } else if (date) { + setAnswer(`${date}T00:00:00`); + } else if (time) { + setAnswer(`0000-00-00T${time}`); + } + }; + return (
@@ -102,11 +122,18 @@ function PollCard({ PollData, setAnswer, onClick }) {

Enter a date

setAnswer(dateString)} + onChange={handleDateChange} onClick={() => !isVotePath && clickHandle()} > + !isVotePath && clickHandle()} + />
) : (
@@ -148,7 +175,9 @@ function PollCard({ PollData, setAnswer, onClick }) {
{userData?.profile_picture == null ? ( -
+
+ +
) : ( { + const data = getProfileMe(); + data.then((result) => { + setUserData(result); + }); + }, []); + + const choices = additionalChoices.filter((choice) => choice.trim() !== ""); + const isSubmitDisabled = + question.trim() === "" || + pollType === "" || + (pollType === "multipleChoice" && choices.length < 2) || + (setDueDate && numericFieldValue.trim() === "") || + (setDueDate && dueDatePoll === null) || + (setDueDate && isFutureDate(dueDatePoll) === false) || + (setDueDate && numericFieldValue < 0) || + (pollType === "customized" && customizedType === ""); - useEffect( () => { - const data = getProfileMe(); - data.then((result) => { - setUserData(result); - }); - },[]) - - const choices = additionalChoices.filter(choice => choice.trim() !== '') - const isSubmitDisabled = question.trim() === '' || - pollType === '' || - (pollType === 'multipleChoice' && choices.length < 2) || - (setDueDate && numericFieldValue.trim() === '') || - (setDueDate && dueDatePoll === null) || - (setDueDate && isFutureDate(dueDatePoll) === false) || - (setDueDate && numericFieldValue < 0) || - (pollType === 'customized' && customizedType === ''); - function isFutureDate(date) { const currentDate = new Date(); return date.isAfter(currentDate); @@ -56,10 +59,11 @@ function Create() { setDueDatePoll(date); }; + const handleSetDueDateChange = (e) => { setSetDueDate(e.target.checked); setDueDatePoll(null); - setNumericFieldValue(''); + setNumericFieldValue(""); }; const handleQuestionChange = (e) => { @@ -68,11 +72,11 @@ function Create() { const handlePollTypeChange = (type) => { setPollType(type); - setShowMultipleChoiceInputs(type === 'multipleChoice'); + setShowMultipleChoiceInputs(type === "multipleChoice"); }; const handleAddChoice = () => { - setAdditionalChoices([...additionalChoices, '']); + setAdditionalChoices([...additionalChoices, ""]); }; const handleDeleteChoice = (index) => { @@ -91,44 +95,55 @@ function Create() { setCustomizedType(type); }; - const handleSubmit = async () => { + const getDueDateTime = () => { + return dueDatePoll && dueTime + ? `${dueDatePoll.format("YYYY-MM-DD")}T${dueTime}:00.000Z` + : dueDatePoll + ? `${dueDatePoll.format("YYYY-MM-DD")}T00:00:00.000Z` + : null; + }; - if (pollType === 'multipleChoice' && setDueDate) { - const choicesData = additionalChoices.filter(choice => choice.trim() !== ''); // Remove empty choices + const handleSubmit = async () => { + const dueDateTime = getDueDateTime(); + console.log("dueDateTime", dueDateTime) + + + if (pollType === "multipleChoice" && setDueDate) { + const choicesData = additionalChoices.filter( + (choice) => choice.trim() !== "" + ); const multipleChoiceData = { question: question, openVisibility: openVisibility, choices: choicesData, setDueDate: setDueDate, - dueDatePoll: dueDatePoll ? dueDatePoll.format() : null, // Convert dueDatePoll to a string format if it exists + dueDatePoll: dueDateTime, numericFieldValue: numericFieldValue, selectedTimeUnit: selectedTimeUnit, }; try { - const response = await fetch(url + "/polls/discrete/", { - method: 'POST', + const response = await fetch(url + "/polls/discrete/", { + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${localStorage.getItem("accessToken")}`, - + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("accessToken")}`, }, body: JSON.stringify(multipleChoiceData), }); if (!response.ok) { - console.error('Error:', response.statusText); + console.error("Error:", response.statusText); return; } - const responseData = await response.json(); - console.log('API Response:', responseData); - // Redirect or navigate to another page after successful API request - navigate('/feed'); + + navigate("/feed"); } catch (error) { - console.error('API Request Failed:', error.message); + console.error("API Request Failed:", error.message); } - - } else if (pollType === 'multipleChoice' && !setDueDate) { - const choicesData = additionalChoices.filter(choice => choice.trim() !== ''); // Remove empty choices + } else if (pollType === "multipleChoice" && !setDueDate) { + const choicesData = additionalChoices.filter( + (choice) => choice.trim() !== "" + ); const multipleChoiceData = { question: question, openVisibility: openVisibility, @@ -137,60 +152,61 @@ function Create() { }; try { - const response = await fetch(url + "/polls/discrete/", { - method: 'POST', + const response = await fetch(url + "/polls/discrete/", { + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${localStorage.getItem("accessToken")}`, + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("accessToken")}`, }, body: JSON.stringify(multipleChoiceData), }); if (!response.ok) { - console.error('Error:', response.statusText); + console.error("Error:", response.statusText); return; } const responseData = await response.json(); - console.log('API Response:', responseData); - // Redirect or navigate to another page after successful API request - navigate('/feed'); + console.log("API Response:", responseData); + + navigate("/feed"); } catch (error) { - console.error('API Request Failed:', error.message); + console.error("API Request Failed:", error.message); } - - } else if (pollType === 'customized' && setDueDate && customizedType === 'date') { - + } else if ( + pollType === "customized" && + setDueDate && + customizedType === "date" + ) { const customizedData = { question: question, setDueDate: setDueDate, - dueDatePoll: dueDatePoll ? dueDatePoll.format() : null, // Convert dueDatePoll to a string format if it exists + dueDatePoll: dueDateTime, numericFieldValue: numericFieldValue, selectedTimeUnit: selectedTimeUnit, cont_poll_type: customizedType, }; try { - const response = await fetch(url + "/polls/continuous/", { - method: 'POST', + const response = await fetch(url + "/polls/continuous/", { + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${localStorage.getItem("accessToken")}`, + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("accessToken")}`, }, body: JSON.stringify(customizedData), }); if (!response.ok) { - console.error('Error:', response.statusText); + console.error("Error:", response.statusText); return; } - const responseData = await response.json(); - console.log('API Response:', responseData); - // Redirect or navigate to another page after successful API request - navigate('/feed'); + navigate("/feed"); } catch (error) { - console.error('API Request Failed:', error.message); + console.error("API Request Failed:", error.message); } - - } else if (pollType === 'customized' && !setDueDate && customizedType === 'date') { - + } else if ( + pollType === "customized" && + !setDueDate && + customizedType === "date" + ) { const customizedData = { question: question, setDueDate: setDueDate, @@ -198,60 +214,58 @@ function Create() { }; try { - const response = await fetch(url + "/polls/continuous/", { - method: 'POST', + const response = await fetch(url + "/polls/continuous/", { + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${localStorage.getItem("accessToken")}`, + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("accessToken")}`, }, body: JSON.stringify(customizedData), }); if (!response.ok) { - console.error('Error:', response.statusText); + console.error("Error:", response.statusText); return; } - const responseData = await response.json(); - console.log('API Response:', responseData); - // Redirect or navigate to another page after successful API request - navigate('/feed'); + navigate("/feed"); } catch (error) { - console.error('API Request Failed:', error.message); + console.error("API Request Failed:", error.message); } - - } else if (pollType === 'customized' && setDueDate && customizedType === 'numeric') { - + } else if ( + pollType === "customized" && + setDueDate && + customizedType === "numeric" + ) { const customizedData = { question: question, setDueDate: setDueDate, - dueDatePoll: dueDatePoll ? dueDatePoll.format() : null, // Convert dueDatePoll to a string format if it exists + dueDatePoll: dueDateTime, numericFieldValue: numericFieldValue, selectedTimeUnit: selectedTimeUnit, cont_poll_type: customizedType, }; try { - const response = await fetch(url + "/polls/continuous/", { - method: 'POST', + const response = await fetch(url + "/polls/continuous/", { + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${localStorage.getItem("accessToken")}`, + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("accessToken")}`, }, body: JSON.stringify(customizedData), }); if (!response.ok) { - console.error('Error:', response.statusText); + console.error("Error:", response.statusText); return; } - const responseData = await response.json(); - console.log('API Response:', responseData); - // Redirect or navigate to another page after successful API request - navigate('/feed'); + navigate("/feed"); } catch (error) { - console.error('API Request Failed:', error.message); + console.error("API Request Failed:", error.message); } - - } else if (pollType === 'customized' && !setDueDate && customizedType === 'numeric') { - + } else if ( + pollType === "customized" && + !setDueDate && + customizedType === "numeric" + ) { const customizedData = { question: question, setDueDate: setDueDate, @@ -259,35 +273,28 @@ function Create() { }; try { - const response = await fetch(url + "/polls/continuous/", { - method: 'POST', + const response = await fetch(url + "/polls/continuous/", { + method: "POST", headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${localStorage.getItem("accessToken")}`, + "Content-Type": "application/json", + Authorization: `Bearer ${localStorage.getItem("accessToken")}`, }, body: JSON.stringify(customizedData), }); if (!response.ok) { - console.error('Error:', response.statusText); + console.error("Error:", response.statusText); return; } - const responseData = await response.json(); - console.log('API Response:', responseData); - // Redirect or navigate to another page after successful API request - navigate('/feed'); + navigate("/feed"); } catch (error) { - console.error('API Request Failed:', error.message); + console.error("API Request Failed:", error.message); } } - }; - - return (
- - +
@@ -296,7 +303,7 @@ function Create() {