Merge branch 'dev'

This commit is contained in:
Ceferino Patino 2022-08-02 21:35:47 -05:00
commit 32699e61f6
20 changed files with 287 additions and 242 deletions

View file

@ -19,23 +19,19 @@ import AppRegistrationIcon from '@mui/icons-material/AppRegistration';
import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode';
import SearchIcon from '@mui/icons-material/Search';
import { useState } from 'react';
import Gravatar from 'react-gravatar';
import { Link } from 'react-router-dom';
import Logo from 'assets/bdpa-logo.svg';
import { useMode } from 'contexts';
export default function MobileNavbar({ logout, userData }) {
const [open, setOpen] = useState(false);
const { mode, setMode } = useMode();
const handleClick = () => setOpen((initial) => !initial);
const onClick = () => {
setMode(() => (mode === 'light' ? 'dark' : 'light'));
};
export default function MobileNavbar({
logout,
userData,
open,
setOpen,
mode,
setMode,
}) {
function NavbarControls() {
return userData.username ? (
<>
@ -121,11 +117,11 @@ export default function MobileNavbar({ logout, userData }) {
</Typography>
<Box sx={{ flexGrow: 1 }} />
<Button color='inherit' onClick={handleClick}>
<Button color='inherit' onClick={setOpen}>
Menu
</Button>
</Toolbar>
<Drawer anchor='right' open={open} onClose={handleClick}>
<Drawer anchor='right' open={open} onClose={setOpen}>
<List>
<ListItem>
<ListItemButton component={Link} to='/questions/search'>
@ -137,7 +133,7 @@ export default function MobileNavbar({ logout, userData }) {
</ListItem>
<NavbarControls />
<ListItem>
<ListItemButton onClick={onClick}>
<ListItemButton onClick={setMode}>
<ListItemIcon>
{mode === 'light' ? (
<DarkModeIcon />

View file

@ -1,34 +0,0 @@
import { IconButton } from '@mui/material';
import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode';
import { useMode } from 'contexts';
export default function ModeToggle() {
const { mode, setMode } = useMode();
const onClick = () => {
setMode(() => (mode === 'light' ? 'dark' : 'light'));
};
if (mode === 'light') {
return (
<IconButton
color='inherit'
onClick={onClick}
sx={{ margin: '0 1vh' }}
>
<LightModeIcon />
</IconButton>
);
} else {
return (
<IconButton
color='inherit'
onClick={onClick}
sx={{ margin: '0 1vh' }}
>
<DarkModeIcon />
</IconButton>
);
}
}

View file

@ -9,10 +9,12 @@ import {
import MailIcon from '@mui/icons-material/Mail';
import { Link } from 'react-router-dom';
import Logo from 'assets/bdpa-logo.svg';
import { ModeToggle, Profile } from 'components';
import { Profile } from 'components';
import { SearchBar } from 'controllers';
import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode';
export default function Navbar({ logout, userData }) {
export default function Navbar({ logout, userData, mode, setMode }) {
function NavbarControls() {
return userData.username ? (
<>
@ -61,7 +63,23 @@ export default function Navbar({ logout, userData }) {
</Typography>
<SearchBar />
<Box sx={{ flexGrow: 1 }} />
<ModeToggle />
{mode === 'dark' ? (
<IconButton
color='inherit'
onClick={setMode}
sx={{ margin: '0 1vh' }}
>
<LightModeIcon />
</IconButton>
) : (
<IconButton
color='inherit'
onClick={setMode}
sx={{ margin: '0 1vh' }}
>
<DarkModeIcon />
</IconButton>
)}
<NavbarControls />
</Toolbar>
</AppBar>

View file

@ -1,20 +1,21 @@
import { Button, Card, CardContent, Tooltip } from '@mui/material'
import { Button, Card, CardContent, Tooltip } from '@mui/material';
import { AnswerForm } from 'controllers/FormControllers'
import MdPreview from 'components/MdPreview'
export default function CreateAnswer({
canAnswer,
show,
toggleShow,
}) {
import { AnswerForm } from 'controllers/FormControllers';
import MdPreview from 'components/MdPreview';
export default function CreateAnswer({ canAnswer, show, toggleShow }) {
return (
<div>
<Tooltip title={!canAnswer ? '' : 'You need to be authenticated to answer or level 5 if the question is protected'}>
<Tooltip
title={
canAnswer
? ''
: 'You need to be authenticated to answer or level 5 if the question is protected'
}
>
<span>
<Button
disabled={canAnswer}
disabled={!canAnswer}
variant='contained'
onClick={toggleShow}
>

View file

@ -3,7 +3,6 @@ import Form from './Form';
import ListAnswer from './ListAnswer';
import ListQuestion from './ListQuestion';
import MailUnit from './MailUnit';
import ModeToggle from './ModeToggle';
import Navbar from './Navbar';
import NoData from './NoData';
import PaginatedList from './PaginatedList';
@ -18,7 +17,6 @@ export {
ListAnswer,
ListQuestion,
MailUnit,
ModeToggle,
Navbar,
NoData,
PaginatedList,

View file

@ -1,19 +1,16 @@
import { Form } from 'controllers/FormControllers';
import { requestReset } from 'services/userServices';
import { recoverFields } from 'services/fields';
import { recoverSchema } from 'services/schemas';
export default function ForgotPasswordFormController() {
const recover = ({ username }) => {
console.log(
`Visit http://localhost:3000/users/recover/${username} to reset your password`
);
}
const recover = async ({ username }) => {
await requestReset({ username });
};
return (
Form({
fields: recoverFields,
onSubmit: recover,
validationSchema: recoverSchema
})
);
return Form({
fields: recoverFields,
onSubmit: recover,
validationSchema: recoverSchema,
});
}

View file

@ -9,7 +9,7 @@ export default function ResetFormController() {
const { id } = useParams();
const changePassword = async ({ password }) => {
const { error } = await resetPassword(id, password);
const { error } = await resetPassword(id, { password });
if (!error) {
navigate('/users/login');

View file

@ -1,4 +1,4 @@
import AnswerForm from './AnwerFormController';
import AnswerForm from './AnswerFormController';
import AskForm from './AskFormController';
import ForgotPasswordForm from './ForgotPasswordFormController';
import Form from './FormController';

View file

@ -1,14 +1,24 @@
import { useUser, useError } from 'contexts';
import { useUser, useError, useMode } from 'contexts';
import { Navbar, MobileNavbar } from 'components';
import { logout } from 'services/userServices';
import { useNavigate } from 'react-router-dom';
import { useMediaQuery } from '@mui/material';
import Cookies from 'js-cookie';
import { useState } from 'react';
export default function NavbarController() {
const { userData, setUserData } = useUser();
const { setError } = useError();
const navigate = useNavigate();
const { userData, setUserData } = useUser();
const { mode, setMode } = useMode();
const { setError } = useError();
const [open, setOpen] = useState(false);
const toggleOpen = () => setOpen((initial) => !initial);
const toggleMode = () => {
setMode(() => (mode === 'light' ? 'dark' : 'light'));
};
const sm = useMediaQuery((theme) => theme.breakpoints.only('sm'));
const md = useMediaQuery((theme) => theme.breakpoints.only('md'));
@ -24,6 +34,13 @@ export default function NavbarController() {
};
return md || sm
? Navbar({ logout: logoutUser, userData })
: MobileNavbar({ logout: logoutUser, userData });
? Navbar({ logout: logoutUser, userData, mode, setMode: toggleMode })
: MobileNavbar({
logout: logoutUser,
userData,
open,
setOpen: toggleOpen,
mode,
toggleMode,
});
}

View file

@ -10,7 +10,6 @@ export default function SearchBarController() {
const onSubmit = (e) => {
e.preventDefault();
console.log(search);
};
return <SearchBar value={search} onChange={onChange} onSubmit={onSubmit} />;

View file

@ -10,28 +10,30 @@ const throttle = new Throttle({
});
export const createEndpoint = (path) => (op, endpoint, data, auth) => {
let request = superagent[op](`${process.env.REACT_APP_API_ROOT}${path}${endpoint}`)
let request = superagent[op](
`${process.env.REACT_APP_API_ROOT}${path}${endpoint}`
)
.use(throttle.plugin)
.set('Content-Type', 'application/json');
if (auth)
request = request.set('Authorization', auth);
if (auth) request = request.set('Authorization', auth);
else if (Cookies.get('token'))
request = request.set('Authorization', `bearer ${Cookies.get('token')}`);
request = request.set(
'Authorization',
`bearer ${Cookies.get('token')}`
);
if (data)
if (op === 'get' || op === 'delete')
request = request.query(data);
else if (op === 'post' || op === 'patch')
request = request.send(data);
if (op === 'get' || op === 'delete') request = request.query(data);
else if (op === 'post' || op === 'patch') request = request.send(data);
return request
.then(({ body, status }) => {
console.log(op, path, endpoint, data, body, status);
return { ...body, status };
})
.catch(({ response, status }) => {
console.log(op, path, endpoint, data, response?.body, status);
return response ? { ...response?.body, status } : { error: 'Error' };
return response
? { ...response?.body, status }
: { error: 'Error' };
});
}
};

View file

@ -20,13 +20,15 @@ app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use('/users', userRouter);
app.use('/mail', mailRouter);
app.use('/questions', questionRouter);
app.use('/public', express.static(path.join(__dirname, 'public')));
app.use('/api/users', userRouter);
app.use('/api/mail', mailRouter);
app.use('/api/questions', questionRouter);
app.use(express.static(path.join(__dirname, 'public/build')));
app.get('*', (_, res) => {
res.sendFile(path.join(__dirname, 'public/build/index.html'));
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, './public/build/index.html'));
});
module.exports = app;

View file

@ -8,7 +8,7 @@ const Question = mongoose.Schema(
createdAt: { type: Date, required: true, default: Date.now },
creator: { type: String, required: true },
downvotes: { type: Number, required: true, default: 0 },
hasAccepted: { type: Boolean, required: true, default: false },
hasAcceptedAnswer: { type: Boolean, required: true, default: false },
lastAnswerFetch: { type: Date, default: new Date(0) },
lastCommentFetch: { type: Date, default: new Date(0) },
protect: [String],

View file

@ -3,7 +3,7 @@ servers:
- description: SwaggerHub API Auto Mocking
url: https://virtserver.swaggerhub.com/C4theBomb/qOverflow/1.0.0
- description: Localhost
url: http://localhost:3001
url: http://localhost:3001/api
info:
version: '1.0.0'
title: qOverflow API

View file

@ -1,5 +1,7 @@
const config = require('server/config.json');
const Answer = require('server/db/models/Answer');
const Question = require('server/db/models/Question');
const User = require('server/db/models/User');
const { getQuestion, refreshQuestion } = require('server/utils/question');
const createRequest = require('server/utils/api');
@ -12,10 +14,7 @@ async function EditAnswerAccepted(req, res) {
if (!question) return res.status(404).send(config.errorNotFound);
// Verify user owns question and question does not already have an accepted answer
if (
question.creator !== user.username ||
question.hasAccepted
)
if (question.creator !== user.username || question.hasAccepted)
return res.status(403).send(config.errorForbidden);
// Patch question with BDPA server
@ -30,16 +29,22 @@ async function EditAnswerAccepted(req, res) {
}
const cachedAnswer = await Answer.findByIdAndDelete(answer_id);
//update hasAcceptedAnswer
// Update hasAcceptedAnswer
await createRequest('patch', `/questions/${question_id}`, {
hasAcceptedAnswer: true
hasAcceptedAnswer: true,
});
await Question.findByIdAndUpdate(question_id, { hasAcceptedAnswer: true });
// Increment points of answer creator
await createRequest('patch', `/users/${cachedAnswer.creator}/points`, {
operation: 'increment',
amount: 15,
});
await User.findByOneAndUpdate(
{ username: cachedAnswer.creator },
{ $inc: { points: 15 } }
);
return res.sendStatus(200);
}

View file

@ -1,51 +1,11 @@
const config = require('server/config.json');
const User = require('server/db/models/User');
const Vote = require('server/db/models/Vote');
const { getQuestion, refreshQuestion } = require('server/utils/question');
const Question = require('server/db/models/Question');
const createRequest = require('server/utils/api');
const getUserLevel = require('server/utils/getUserLevel');
async function downvote(res, user) {
{
const { success } = await createRequest('patch', URL, {
operation: 'increment',
target: 'downvotes',
});
if (!success) return res.status(500).send(config.errorGeneric);
await Vote.create({
parent_id: question_id,
creator: user.username,
status: 'downvoted',
docModel: 'Question',
});
}
{
const { success } = await createRequest(
'patch',
`/users/${user.username}/points`,
{
operation: 'decrement',
amount: 1,
}
);
if (!success) return res.status(500).send(config.errorGeneric);
await User.findByIdAndUpdate(user.id, { $inc: { points: -1 } });
}
}
async function upvote(res, user) {
const { success } = await createRequest('patch', URL, {
operation: 'increment',
target: 'upvotes',
});
if (!success) return res.status(500).send(config.errorGeneric);
await Vote.create({
parent_id: question_id,
creator: user.username,
status: 'upvoted',
docModel: 'Question',
});
}
async function upvote(res, user) {}
async function EditQuestionVote(req, res) {
const { user } = req;
@ -54,19 +14,24 @@ async function EditQuestionVote(req, res) {
if (!operation) return res.status(400).send(config.errorIncomplete);
// Filter out requests that do not have the permissions
const userLevel = getUserLevel(user.points);
if (operation === 'upvote' && userLevel < 2)
if (operation === 'upvote' && userLevel < 2) {
return res.status(403).send(config.errorForbidden);
}
if (operation === 'downvote' && userLevel < 4)
if (operation === 'downvote' && userLevel < 4) {
return res.status(403).send(config.errorForbidden);
}
const URL = `/questions/${question_id}/vote/${user.username}`;
const question = await getQuestion(question_id);
// Verify that the question exists
const question = await Question.findById(question_id);
if (!question) return res.status(404).send(config.errorNotFound);
// Find the cached vote and record the status
let cachedVote = await Vote.findOneAndDelete({
parent_id: question_id,
creator: user.username,
@ -77,28 +42,54 @@ async function EditQuestionVote(req, res) {
cachedVote = !error ? { status: vote } : { status: null };
}
if (cachedVote === 'upvoted') {
{
const { success } = await createRequest('patch', URL, {
operation: 'decrement',
target: 'upvotes',
});
if (!success) return res.status(500).send(config.errorGeneric);
}
if (cachedVote.status === 'upvoted') {
// Undo the previous upvote status
const { success } = await createRequest('patch', URL, {
operation: 'decrement',
target: 'upvotes',
});
if (!success) return res.status(500).send();
if (operation === 'downvote') {
await downvote(res, user);
{
const { success } = await createRequest(
'patch',
`/users/${question.creator}/points`,
{
operation: 'decrement',
amount: 6,
}
);
if (!success) return res.status(500).send(config.errorGeneric);
// Change the vote status to downvoted
const { success } = await createRequest('patch', URL, {
operation: 'increment',
target: 'downvotes',
});
if (!success) {
return res.status(500).send(config.errorGeneric);
}
// Cache the new vote
await Vote.create({
parent_id: question_id,
creator: user.username,
status: 'downvoted',
docModel: 'Question',
});
// Decrement the question creator's points
await createRequest('patch', `/users/${question.creator}/points`, {
operation: 'decrement',
amount: 6,
});
await User.findOneAndUpdate(
{ username: question.creator },
{ $inc: { points: -6 } }
);
// Decrement the user's points
await createRequest('patch', `/users/${user.username}/points`, {
operation: 'decrement',
amount: 1,
});
await User.findByIdAndUpdate(user.id, { $inc: { points: -1 } });
return res.send({ vote: 'downvoted' });
} else {
// Decrement the question creator's points
const { success } = await createRequest(
'patch',
`/users/${question.creator}/points`,
@ -107,84 +98,138 @@ async function EditQuestionVote(req, res) {
amount: 5,
}
);
await User.findOneAndUpdate(
{ username: question.creator },
{ $inc: { points: -5 } }
);
if (!success) return res.status(500).send(config.errorGeneric);
return res.send({ vote: 'upvoted' });
}
} else if (cachedVote === 'downvoted') {
{
const { success } = await createRequest('patch', URL, {
operation: 'decrement',
target: 'downvotes',
});
if (!success) return res.status(500).send(config.errorGeneric);
return res.send({ vote: null });
}
} else if (cachedVote.status === 'downvoted') {
// Undo the previous vote status
const { success } = await createRequest('patch', URL, {
operation: 'decrement',
target: 'downvotes',
});
if (!success) return res.status(500).send(config.errorGeneric);
if (operation === 'upvote') {
await upvote(res, user);
{
const { success } = await createRequest(
'patch',
`/users/${question.creator}/points`,
{
operation: 'increment',
amount: 6,
}
);
if (!success) return res.status(500).send(config.errorGeneric);
}
// Increment upvotes
const { success } = await createRequest('patch', URL, {
operation: 'increment',
target: 'upvotes',
});
if (!success) return res.status(500).send(config.errorGeneric);
// Cache the new vote
await Vote.create({
parent_id: question_id,
creator: user.username,
status: 'upvoted',
docModel: 'Question',
});
// Increment question creator's points
await createRequest('patch', `/users/${question.creator}/points`, {
operation: 'increment',
amount: 6,
});
await User.findOneAndUpdate(
{ username: question.creator },
{ $inc: { points: 6 } }
);
return res.send({ vote: 'upvoted' });
} else {
{
const { success } = await createRequest(
'patch',
`/users/${question.creator}/points`,
{
operation: 'increment',
amount: 1,
}
);
if (!success) return res.status(500).send(config.errorGeneric);
}
{
const { success } = await createRequest(
'patch',
`/users/${user.username}/points`,
{
operation: 'increment',
amount: 1,
}
);
if (!success) return res.status(500).send(config.errorGeneric);
await User.findByIdAndUpdate(user.id, { $inc: { points: 1 } });
}
// Increment the question creator's points
await createRequest('patch', `/users/${question.creator}/points`, {
operation: 'increment',
amount: 1,
});
await User.findOneAndUpdate(
{ username: question.creator },
{ $inc: { points: 1 } }
);
// Increment the user's points
await createRequest('patch', `/users/${user.username}/points`, {
operation: 'increment',
amount: 1,
});
await User.findByIdAndUpdate(user.id, { $inc: { points: 1 } });
return res.send({ vote: null });
}
} else {
if (operation === 'upvote') {
await upvote(res, user);
{
const { success } = await createRequest(
'patch',
`/users/${question.creator}/points`,
{
operation: 'increment',
amount: 5,
}
);
if (!success) return res.status(500).send(config.errorGeneric);
}
// Increment upvotes
const { success } = await createRequest('patch', URL, {
operation: 'increment',
target: 'upvotes',
});
if (!success) return res.status(500).send(config.errorGeneric);
// Cache the new vote
await Vote.create({
parent_id: question_id,
creator: user.username,
status: 'upvoted',
docModel: 'Question',
});
// Increment question creator's points
await createRequest('patch', `/users/${question.creator}/points`, {
operation: 'increment',
amount: 5,
});
await User.findOneAndUpdate(
{ username: question.creator },
{ $inc: { points: 5 } }
);
return res.send({ vote: 'upvoted' });
} else if (operation === 'downvote') {
await downvote(res, user);
{
const { success } = await createRequest(
'patch',
`/users/${question.creator}/points`,
{
operation: 'decrement',
amount: 1,
}
);
if (!success) return res.status(500).send(config.errorGeneric);
// Change the vote status to downvoted
const { success } = await createRequest('patch', URL, {
operation: 'increment',
target: 'downvotes',
});
if (!success) {
return res.status(500).send(config.errorGeneric);
}
// Cache the new vote
await Vote.create({
parent_id: question_id,
creator: user.username,
status: 'downvoted',
docModel: 'Question',
});
// Decrement the question creator's points
await createRequest('patch', `/users/${question.creator}/points`, {
operation: 'decrement',
amount: 1,
});
await User.findOneAndUpdate(
{ username: question.creator },
{ $inc: { points: -1 } }
);
// Decrement the user's points
await createRequest('patch', `/users/${user.username}/points`, {
operation: 'decrement',
amount: 1,
});
await User.findByIdAndUpdate(user.id, { $inc: { points: -1 } });
return res.send({ vote: 'downvoted' });
}
}

View file

@ -22,7 +22,7 @@ router.get('/answers', tokenAuth, Answers);
router.post('/login', basicAuth, Login);
router.get('/remember', tokenAuth, Remember);
router.delete('/logout', tokenAuth, Logout);
router.get('/reset', RequestReset);
router.post('/reset', RequestReset);
router.post('/reset/:id', ResetPassword);
router.get('/:username', GetUser);

View file

@ -1,11 +1,12 @@
const ResetRequest = require('server/db/models/ResetRequest');
async function RequestReset(req, res) {
const { username } = req.query;
const { username } = req.body;
const request = await ResetRequest.create({ user: username });
console.log(
`Your request to reset your password has been receiver. Visit http://localhost:3000/reset/${request.id} to reset your password.`
`Your request to reset your password has been receiver. Visit http://localhost:3000/users/recover/${request.id} to reset your password.`
);
return res.sendStatus(200);

View file

@ -33,11 +33,9 @@ const createRequest = async (op, endpoint, data) => {
return request
.then((res) => {
console.log(op, endpoint, JSON.stringify(res.body).substr(0, 100));
return res.body;
})
.catch((err) => {
console.log(op, endpoint, err.response?.body);
return err.response?.body || { error: 'Error' };
});
};