diff --git a/api/main_endpoints/routes/LeetCodeLeaderboard.js b/api/main_endpoints/routes/LeetCodeLeaderboard.js new file mode 100644 index 000000000..e8075dcb0 --- /dev/null +++ b/api/main_endpoints/routes/LeetCodeLeaderboard.js @@ -0,0 +1,108 @@ +const express = require('express'); +const router = express.Router(); +const { + OK, + SERVER_ERROR, + BAD_REQUEST +} = require('../../util/constants.js').STATUS_CODES; +const { decodeToken } = require('../util/token-functions.js'); +const logger = require('../../util/logger.js'); +const { + getAllUsers, + addUserToLeaderboard, + deleteUserFromLeaderboard, + checkIfUserExists, +} = require('../util/LeetCodeLeaderboard.js'); +const AuditLogActions = require('../util/auditLogActions.js'); +const AuditLog = require('../models/AuditLog.js'); +const membershipState = require('../../util/constants').MEMBERSHIP_STATE; + +router.get('/getAllUsers', async (req, res) => { + const decoded = await decodeToken(req, membershipState.OFFICER); + if (decoded.status !== OK) { + return res.sendStatus(decoded.status); + } + + const users = await getAllUsers(); + if (!users) { + return res.status(SERVER_ERROR); + } + return res.status(OK).send({ users }); +}); + +router.post('/addUser', async (req, res) => { + const decoded = await decodeToken(req, membershipState.OFFICER); + if (decoded.status !== OK) { + return res.sendStatus(decoded.status); + } + + const { username, firstName, lastName } = req.body; + const required = [ + { value: username, title: 'LeetCode username', }, + { value: firstName, title: 'User\'s first name', }, + { value: lastName, title: 'User\'s last name', } + ]; + + const missingValue = required.find(({ value }) => !value); + + if (missingValue) { + return res.status(BAD_REQUEST).send(`${missingValue.title} missing from request`); + } + + if (!await addUserToLeaderboard({ + username, + firstName, + lastName + })) { + return res.sendStatus(SERVER_ERROR); + } + + AuditLog.create({ + userId: decoded.token._id, + action: AuditLogActions.ADD_LEETCODE_USER, + details: { username }, + }); + return res.sendStatus(OK); +}); + +router.post('/deleteUser', async (req, res) => { + const decoded = await decodeToken(req, membershipState.OFFICER); + if (decoded.status !== OK) { + return res.sendStatus(decoded.status); + } + + const { username } = req.body; + if (!username) { + return res.status(BAD_REQUEST).send('Username field missing'); + } + + if (!await deleteUserFromLeaderboard(username)) { + return res.sendStatus(SERVER_ERROR); + } + + AuditLog.create({ + userId: decoded.token._id, + action: AuditLogActions.DELETE_LEETCODE_USER, + details: { username }, + }); + return res.sendStatus(OK); +}); + +router.post('/checkIfUserExists', async (req, res) => { + const decoded = await decodeToken(req, membershipState.OFFICER); + if (decoded.status !== OK) { + return res.sendStatus(decoded.status); + } + + const { username } = req.body; + if (!username) { + return res.status(BAD_REQUEST).send('Username field missing'); + } + const response = await checkIfUserExists(username); + if (response.error) { + return res.status(response.status).send(response.message); + } + return res.status(OK).send(response.exists); +}); + +module.exports = router; diff --git a/api/main_endpoints/util/LeetCodeLeaderboard.js b/api/main_endpoints/util/LeetCodeLeaderboard.js new file mode 100644 index 000000000..ada93cf7a --- /dev/null +++ b/api/main_endpoints/util/LeetCodeLeaderboard.js @@ -0,0 +1,103 @@ +const logger = require('../../util/logger'); +const LEETCODE_URL = 'http://192.168.69.180:8080'; + +async function getAllUsers() { + try { + const url = new URL('/getAllUsers', LEETCODE_URL); + const res = await fetch(url.href); + const data = await res.json(); + if ('error' in data) { + logger.error('Error from LeetCode Leaderboard: ', data.error); + return null; + } + return data.users; + } catch (err) { + logger.error('getAllUsers encountered an error: ', err); + return null; + } +} + +async function addUserToLeaderboard(userData) { + try { + const url = new URL('/user/add', LEETCODE_URL); + const res = await fetch(url.href, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(userData), + }); + const data = await res.json(); + if ('error' in data) { + logger.error('Error from LeetCode Leaderboard: ', data.error); + return false; + } + return true; + } catch (err) { + logger.error('addUserToLeaderboard encountered an error: ', err); + return false; + } +} + +async function deleteUserFromLeaderboard(username) { + try { + const url = new URL('/user/remove', LEETCODE_URL); + const res = await fetch(url.href, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username }), + }); + const data = await res.json(); + if ('error' in data) { + logger.error('Error from LeetCode Leaderboard: ', data.error); + return false; + } + return true; + } catch (err) { + logger.error('deleteUserFromLeaderboard encountered an error: ', err); + return false; + } +} + +async function checkIfUserExists(username) { + try { + const url = new URL('/checkIfUserExists', LEETCODE_URL); + const res = await fetch(url.href, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username }), + }); + const data = await res.json(); + if ('error' in data) { + logger.error('Error from LeetCode Leaderboard: ', data.error); + return { + error: true, + message: data.error, + status: data.status_code, + }; + } + return { + error: false, + exists: data.exists, + }; + + } catch (err) { + logger.error('checkIfUserExists encountered an error: ', err); + return { + error: true, + message: err, + }; + } +} + +module.exports = { + getAllUsers, + addUserToLeaderboard, + deleteUserFromLeaderboard, + checkIfUserExists, +}; + diff --git a/api/main_endpoints/util/auditLogActions.js b/api/main_endpoints/util/auditLogActions.js index 13e7a3d5b..55d885c06 100644 --- a/api/main_endpoints/util/auditLogActions.js +++ b/api/main_endpoints/util/auditLogActions.js @@ -15,6 +15,8 @@ const AuditLogActions = { ADD_CARD: 'ADD_CARD', DELETE_CARD: 'DELETE_CARD', EDIT_CARD: 'EDIT_CARD', + ADD_LEETCODE_USER: 'ADD_LEETCODE_USER', + DELETE_LEETCODE_USER: 'DELETE_LEETCODE_USER', }; module.exports = AuditLogActions; diff --git a/src/APIFunctions/LeetCodeLeaderboard.js b/src/APIFunctions/LeetCodeLeaderboard.js new file mode 100644 index 000000000..b666daf1d --- /dev/null +++ b/src/APIFunctions/LeetCodeLeaderboard.js @@ -0,0 +1,100 @@ +import { ApiResponse } from './ApiResponses'; +import { BASE_API_URL } from '../Enums'; + +export async function getAllUsers(token) { + let status = new ApiResponse(); + try { + const url = new URL('/api/LeetCodeLeaderboard/getAllUsers', BASE_API_URL); + const res = await fetch(url.href, { + headers: { + 'Authorization': `Bearer ${token}`, + } + }); + if (res.ok) { + const result = await res.json(); + status.responseData = result; + } else { + status.error = true; + } + } catch (err) { + status.error = true; + status.responseData = err; + } + return status; +} + +export async function addUser(userData, token) { + let status = new ApiResponse(); + try { + const url = new URL('/api/LeetCodeLeaderboard/addUser', BASE_API_URL); + const res = await fetch(url.href, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(userData), + }); + if (res.ok) { + const result = await res.json(); + status.responseData = result; + } else { + status.error = true; + } + } catch (err) { + status.error = true; + status.responseData = err; + } + return status; +} + +export async function deleteUser(username, token) { + let status = new ApiResponse(); + try { + const url = new URL('/api/LeetCodeLeaderboard/deleteUser', BASE_API_URL); + const res = await fetch(url.href, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username }), + }); + if (res.ok) { + const result = await res.json(); + status.responseData = result; + } else { + status.error = true; + } + } catch (err) { + status.error = true; + status.responseData = err; + } + return status; +} + +export async function checkIfUserExists(username, token) { + let status = new ApiResponse(); + try { + const url = new URL('/api/LeetCodeLeaderboard/checkIfUserExists', BASE_API_URL); + const res = await fetch(url.href, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, + }, + body: JSON.stringify({ username }), + }); + if (res.ok) { + const result = await res.json(); + status.responseData = result; + } else { + status.error = true; + } + } catch (err) { + status.error = true; + status.responseData = err; + } + return status; +} + diff --git a/src/Components/Navbar/AdminNavbar.js b/src/Components/Navbar/AdminNavbar.js index 3221d9eb4..2e74272d1 100644 --- a/src/Components/Navbar/AdminNavbar.js +++ b/src/Components/Navbar/AdminNavbar.js @@ -100,6 +100,15 @@ export default function UserNavBar(props) { ) }, + { + title: 'LeetCode Leaderboard', + route: '/leetcode-leaderboard', + icon: ( + + + + ), + }, ]; const renderRoutesForNavbar = (navbarLinks) => { diff --git a/src/Pages/LeetCodeLeaderboard/LeetCodeLeaderboard.js b/src/Pages/LeetCodeLeaderboard/LeetCodeLeaderboard.js new file mode 100644 index 000000000..281bd70b8 --- /dev/null +++ b/src/Pages/LeetCodeLeaderboard/LeetCodeLeaderboard.js @@ -0,0 +1,226 @@ +import React, { useState, useEffect } from 'react'; +import { + getAllUsers, + deleteUser, + addUser, + checkIfUserExists +} from '../../APIFunctions/LeetCodeLeaderboard.js'; +import { trashcanSymbol } from '../Overview/SVG.js'; +import ConfirmationModal from '../../Components/DecisionModal/ConfirmationModal.js'; +import { useSCE } from '../../Components/context/SceContext.js'; + +export default function LeetCodeLeaderboard() { + const { user } = useSCE(); + const token = user.token; + + const [registeredUsers, setRegisteredUsers] = useState([]); + const [toggleDelete, setToggleDelete] = useState(false); + const [userToDelete, setUserToDelete] = useState({}); + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + const [leetcodeUsername, setLeetcodeUsername] = useState(''); + const [confirmLeetcodeUsername, setConfirmLeetcodeUsername] = useState(''); + const [message, setMessage] = useState(''); + const [isError, setIsError] = useState(false); + const [isDisabled, setIsDisabled] = useState(true); + + async function getAllRegisteredUsers() { + const apiResponse = await getAllUsers(token); + if (!apiResponse.error) { + setRegisteredUsers(apiResponse.responseData.users); + } + } + + function handleDeleteClick(user) { + setToggleDelete(!toggleDelete); + setUserToDelete(user); + } + + function resetInputFields() { + setFirstName(''); + setLastName(''); + setLeetcodeUsername(''); + setConfirmLeetcodeUsername(''); + } + + async function handleRegisterUser(e) { + e.preventDefault(); + const doesUserExist = await checkIfUserExists(leetcodeUsername, token); + if (doesUserExist.responseData) { + setMessage('This username is already registered, please try again'); + return; + } + const newUser = { + username: leetcodeUsername, + firstName, + lastName + }; + if (!await addUser(newUser, token)) { + setMessage('Error registering user, please try again later'); + } else { + getAllRegisteredUsers(); + } + resetInputFields(); + } + + useEffect(() => { + getAllRegisteredUsers(); + }, []); + + useEffect(() => { + setIsError(() => { + if (leetcodeUsername.length === 0 || confirmLeetcodeUsername.length === 0) { + setMessage(''); + setIsDisabled(true); + return false; + } + if (leetcodeUsername !== confirmLeetcodeUsername) { + setMessage('Usernames do not match.'); + setIsDisabled(true); + return true; + } + setMessage(''); + setIsDisabled(false); + return false; + }); + }, [leetcodeUsername, confirmLeetcodeUsername]); + + return ( +
+

+ SCE LeetCode Leaderboard +

+
This page manages the functions of the LeetCode Leaderboard
+
+

+ LeetCode Leaderboard +

+ { + await deleteUser(userToDelete.username, token); + await getAllRegisteredUsers(); + setToggleDelete(!toggleDelete); + }, + handleCancel: () => setToggleDelete(!toggleDelete), + open: toggleDelete + } + } /> +
+
+