Preliminary API without caching (getVotes doesn't work)

This commit is contained in:
Ceferino Patino 2022-07-15 21:48:11 -05:00
commit 7c00e79271
115 changed files with 25704 additions and 19236 deletions

49
client/package.json Normal file
View file

@ -0,0 +1,49 @@
{
"name": "qoverflow-client",
"version": "0.1.0",
"private": true,
"homepage": "",
"dependencies": {
"@emotion/react": "^11.9.3",
"@emotion/styled": "^11.9.3",
"@mui/icons-material": "^5.8.4",
"@mui/material": "^5.8.7",
"crypto-js": "^4.1.1",
"dotenv": "^16.0.1",
"formik": "^2.2.9",
"gh-pages": "^4.0.0",
"marked": "^4.0.18",
"react": "^18.2.0",
"react-gravatar": "^2.6.3",
"react-router-dom": "^6.3.0",
"react-scripts": "^5.0.1",
"remove-markdown": "^0.5.0",
"superagent": "^8.0.0",
"superagent-throttle": "^1.0.1",
"universal-cookie": "^4.0.4",
"yup": "^0.32.11"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"predeploy": "npm run build",
"deploy": "gh-pages -d build"
},
"eslintConfig": {
"extends": [
"react-app"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View file

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before After
Before After

View file

@ -3,6 +3,7 @@ import { Link } from 'react-router-dom';
import { getUser, getLevel } from '../services/userServices';
import { useState } from 'react';
import Gravatar from 'react-gravatar';
import removeMd from 'remove-markdown';
export default function ListQuestion({ question, summaryLimit }) {
const [level, setLevel] = useState();
@ -60,8 +61,9 @@ export default function ListQuestion({ question, summaryLimit }) {
[{status}] {title}
</Typography>
<Typography variant='body1'>
{text.split(' ').slice(0, summaryLimit).join(' ') +
'...'}
{removeMd(
text.split(' ').slice(0, summaryLimit).join(' ')
) + '...'}
</Typography>
<Typography variant='body1' textAlign='right'>

View file

@ -1,23 +1,24 @@
import {
Accordion,
AccordionSummary,
Typography,
AccordionDetails,
} from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
export default function MailUnit(sender, createdAt, subject, text) {
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Subject: {subject}</Typography>
<Typography>
From {sender} at {new Date(createdAt * 1000)}
</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography variant='body1'>{text}</Typography>
</AccordionDetails>
</Accordion>
);
}
import {
Accordion,
AccordionSummary,
Typography,
AccordionDetails,
} from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { marked } from 'marked';
export default function MailUnit(sender, createdAt, subject, text) {
return (
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
<Typography>Subject: {subject}</Typography>
<Typography>
From {sender} at {new Date(createdAt * 1000)}
</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography variant='body1'>{marked.parse(text)}</Typography>
</AccordionDetails>
</Accordion>
);
}

View file

@ -1,58 +1,58 @@
import { Link } from 'react-router-dom';
import {
AppBar,
Button,
IconButton,
Toolbar,
Typography,
Box,
} from '@mui/material';
import Logo from '../assets/bdpa-logo.svg';
import { SearchBar } from '.';
import Gravatar from 'react-gravatar';
export default function Navbar({ logout, userData }) {
function ButtonGroup() {
return userData.username ? (
<>
<Button color='inherit' component={Link} to='/mail'>
Mail
</Button>
<Button color='inherit' component={Link} to='/dashboard'>
Account
</Button>
<Button color='inherit' onClick={logout}>
Logout
</Button>
<IconButton component={Link} to='/'>
<Gravatar size={40} email={userData.email} />
</IconButton>
</>
) : (
<>
<Button color='inherit' component={Link} to='/login'>
Login
</Button>
<Button color='inherit' component={Link} to='/register'>
Register
</Button>
</>
);
}
return (
<AppBar position='static'>
<Toolbar>
<IconButton component={Link} to='/'>
<img src={Logo} alt='bdpa logo' width='40' height='40' />
</IconButton>
<Typography variant='h6' component='div'>
qOverflow
</Typography>
<SearchBar />
<Box sx={{ flexGrow: 1 }} />
<ButtonGroup />
</Toolbar>
</AppBar>
);
}
import { Link } from 'react-router-dom';
import {
AppBar,
Button,
IconButton,
Toolbar,
Typography,
Box,
} from '@mui/material';
import Logo from '../assets/bdpa-logo.svg';
import { SearchBar } from '.';
import Gravatar from 'react-gravatar';
export default function Navbar({ logout, userData }) {
function ButtonGroup() {
return userData.username ? (
<>
<Button color='inherit' component={Link} to='/mail'>
Mail
</Button>
<Button color='inherit' component={Link} to='/dashboard'>
Account
</Button>
<Button color='inherit' onClick={logout}>
Logout
</Button>
<IconButton component={Link} to='/'>
<Gravatar size={40} email={userData.email} />
</IconButton>
</>
) : (
<>
<Button color='inherit' component={Link} to='/login'>
Login
</Button>
<Button color='inherit' component={Link} to='/register'>
Register
</Button>
</>
);
}
return (
<AppBar position='static'>
<Toolbar>
<IconButton component={Link} to='/'>
<img src={Logo} alt='bdpa logo' width='40' height='40' />
</IconButton>
<Typography variant='h6' component='div'>
qOverflow
</Typography>
<SearchBar />
<Box sx={{ flexGrow: 1 }} />
<ButtonGroup />
</Toolbar>
</AppBar>
);
}

View file

@ -1,33 +1,33 @@
import { useState } from 'react';
import { Outlet } from 'react-router-dom';
import { Box, CssBaseline } from '@mui/material';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { Navbar } from '../controllers';
export default function Layout() {
const [mode, setMode] = useState('light');
const theme = createTheme({
palette: { mode },
breakpoints: {
values: {
xs: 0,
sm: 768,
md: 1024,
},
},
});
return (
<ThemeProvider theme={theme}>
<CssBaseline enableColorScheme />
<main>
<Navbar />
<Box sx={{ height: '93vh' }}>
<Outlet />
</Box>
</main>
</ThemeProvider>
);
}
import { useState } from 'react';
import { Outlet } from 'react-router-dom';
import { Box, CssBaseline } from '@mui/material';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { Navbar } from '../controllers';
export default function Layout() {
const [mode, setMode] = useState('light');
const theme = createTheme({
palette: { mode },
breakpoints: {
values: {
xs: 0,
sm: 768,
md: 1024,
},
},
});
return (
<ThemeProvider theme={theme}>
<CssBaseline enableColorScheme />
<main>
<Navbar />
<Box sx={{ height: '93vh' }}>
<Outlet />
</Box>
</main>
</ThemeProvider>
);
}

View file

@ -1,11 +1,11 @@
import { QuestionProvider, UserProvider } from '.';
export default function ContextProvider({ children }) {
return (
<UserProvider>
<QuestionProvider>
{children}
</QuestionProvider>
</UserProvider>
);
}
import { QuestionProvider, UserProvider } from '.';
export default function ContextProvider({ children }) {
return (
<UserProvider>
<QuestionProvider>
{children}
</QuestionProvider>
</UserProvider>
);
}

View file

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

View file

@ -1,28 +1,28 @@
import React, { useState, useEffect } from 'react';
import { Box } from '@mui/material';
import { useUser } from '../contexts';
import { getMail } from '../services/mailServices';
import { MailUnit } from '../components';
export default function InboxController() {
const { userData } = useUser();
const [mail, setMail] = useState([]);
useEffect(() => {
const fetchMail = async () => {
const { success, messages } = await getMail(userData?.username);
if (success) {
setMail(() => messages);
}
}
fetchMail();
}, []);
return (
<Box>
{mail.map((message) => (
<MailUnit {...message} />
))}
</Box>
);
}
import React, { useState, useEffect } from 'react';
import { Box } from '@mui/material';
import { useUser } from '../contexts';
import { getMail } from '../services/mailServices';
import { MailUnit } from '../components';
export default function InboxController() {
const { userData } = useUser();
const [mail, setMail] = useState([]);
useEffect(() => {
const fetchMail = async () => {
const { success, messages } = await getMail(userData?.username);
if (success) {
setMail(() => messages);
}
}
fetchMail();
}, []);
return (
<Box>
{mail.map((message) => (
<MailUnit {...message} />
))}
</Box>
);
}

View file

@ -1,32 +1,32 @@
import { useNavigate, useParams } from 'react-router-dom';
import { Form } from '.';
import { useUser } from '../contexts';
import { resetSchema } from '../services/schemas';
import { resetFields } from '../services/fields';
import { updateUser } from '../services/userServices';
import { deriveKeyFromPassword } from '../services/auth';
export default function ResetFormController() {
const navigate = useNavigate();
const { username } = useParams();
const { setUserData } = useUser();
const changePassword = async ({ password }) => {
const body = await deriveKeyFromPassword(password);
const { user, success } = await updateUser(username, body);
if (success) {
setUserData(() => user);
navigate('/login');
}
}
return (
<Form
fields={resetFields}
onSubmit={changePassword}
validationSchema={resetSchema}
/>
);
}
import { useNavigate, useParams } from 'react-router-dom';
import { Form } from '.';
import { useUser } from '../contexts';
import { resetSchema } from '../services/schemas';
import { resetFields } from '../services/fields';
import { updateUser } from '../services/userServices';
import { deriveKeyFromPassword } from '../services/auth';
export default function ResetFormController() {
const navigate = useNavigate();
const { username } = useParams();
const { setUserData } = useUser();
const changePassword = async ({ password }) => {
const body = await deriveKeyFromPassword(password);
const { user, success } = await updateUser(username, body);
if (success) {
setUserData(() => user);
navigate('/login');
}
}
return (
<Form
fields={resetFields}
onSubmit={changePassword}
validationSchema={resetSchema}
/>
);
}

View file

@ -1,20 +1,20 @@
import { Form } from '.';
import { useUser } from '../contexts';
import { mailSchema } from '../services/schemas';
import { composeMailFields } from '../services/fields';
import { postMail } from '../services/mailServices';
export default function SendMailController() {
const { userData } = useUser();
const sendMail = ({ reciever, subject, text }) =>
postMail(userData.username, reciever, subject, text);
return (
<Form
fields={composeMailFields}
onSubmit={sendMail}
validationSchema={mailSchema}
/>
);
}
import { Form } from '.';
import { useUser } from '../contexts';
import { mailSchema } from '../services/schemas';
import { composeMailFields } from '../services/fields';
import { postMail } from '../services/mailServices';
export default function SendMailController() {
const { userData } = useUser();
const sendMail = ({ reciever, subject, text }) =>
postMail(userData.username, reciever, subject, text);
return (
<Form
fields={composeMailFields}
onSubmit={sendMail}
validationSchema={mailSchema}
/>
);
}

5681
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,48 +1,23 @@
{
"name": "qoverflow",
"version": "0.1.0",
"private": true,
"homepage": "",
"dependencies": {
"@emotion/react": "^11.9.3",
"@emotion/styled": "^11.9.3",
"@mui/icons-material": "^5.8.4",
"@mui/material": "^5.8.7",
"crypto-js": "^4.1.1",
"dotenv": "^16.0.1",
"formik": "^2.2.9",
"gh-pages": "^4.0.0",
"marked": "^4.0.18",
"react": "^18.2.0",
"react-gravatar": "^2.6.3",
"react-router-dom": "^6.3.0",
"react-scripts": "^5.0.1",
"superagent": "^8.0.0",
"superagent-throttle": "^1.0.1",
"universal-cookie": "^4.0.4",
"yup": "^0.32.11"
},
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"predeploy": "npm run build",
"deploy": "gh-pages -d build"
"test": "echo \"Error: no test specified\" && exit 1"
},
"eslintConfig": {
"extends": [
"react-app"
]
"repository": {
"type": "git",
"url": "git+https://github.com/spicecat/qOverflow.git"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/spicecat/qOverflow/issues"
},
"workspaces": [
"client",
"server"
],
"homepage": "https://github.com/spicecat/qOverflow#readme"
}

36
server/app.js Normal file
View file

@ -0,0 +1,36 @@
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var userRouter = require('./routes/user');
var mailRouter = require('./routes/mail');
var questionRouter = require('./routes/question');
var app = express();
app.disable('x-powered-by');
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/user', userRouter);
app.use('/mail', mailRouter);
app.use('/question', questionRouter);
// catch 404 and forward to error handler
app.use(function (req, res, next) {
return res
.status(404)
.send({ success: false, error: 'This URL does not exist.' });
});
module.exports = app;

98
server/bin/www Normal file
View file

@ -0,0 +1,98 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('server:server');
var http = require('http');
const mongoose = require('mongoose');
require('dotenv').config();
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
mongoose.connect(process.env.DB_CONN_STRING, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
mongoose.connection.on(
'error',
console.error.bind(console, 'MongoDB connection error:')
);
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port;
debug('Listening on ' + bind);
}

15
server/config.json Normal file
View file

@ -0,0 +1,15 @@
{
"askQuestion": 1,
"answerQuestion": 2,
"upvoteQuestion": 5,
"downvoteQuestion": 1,
"upvoteAnswer": 10,
"downvoteAnswer": 5,
"userDownvotes": 1,
"acceptAnswer": 15,
"mailExpires": 10000,
"userExpires": 100000,
"errorGeneric": "Oops! Something went wrong! Please try again."
}

View file

@ -0,0 +1,16 @@
const mongoose = require('mongoose');
const Answer = mongoose.Schema(
{
questionID: { type: mongoose.Schema.Types.ObjectId, ref: 'Question' },
creator: { type: String, required: true, ref: 'User' },
createdAt: { type: Date, required: true },
text: { type: String, required: true },
upvotes: { type: Number, required: true, default: 0 },
downvotes: { type: Number, required: true, default: 0 },
accepted: { type: Boolean, require: true, default: false },
},
{ timestamps: { createdAt: false, updatedAt: true } }
);
module.exports = mongoose.model('Answer', Answer);

View file

@ -0,0 +1,20 @@
const mongoose = require('mongoose');
const Comment = mongoose.Schema(
{
parentID: { type: mongoose.Schema.Types.ObjectId, ref: 'docModel' },
creator: { type: String, required: true },
createdAt: { type: Date, required: true },
text: { type: String, required: true },
upvotes: { type: Number, required: true, default: 0 },
downvotes: { type: Number, required: true, default: 0 },
docModel: {
type: String,
required: true,
enum: ['Question', 'Answer'],
},
},
{ timestamps: { createdAt: false, updatedAt: true } }
);
module.exports = mongoose.model('Comment', Comment);

14
server/db/models/Mail.js Normal file
View file

@ -0,0 +1,14 @@
const mongoose = require('mongoose');
const Mail = mongoose.Schema(
{
sender: { type: mongoose.Schema.Types.ObjectId, ref: 'Question' },
reciever: { type: String, required: true },
subject: { type: String, required: true },
text: { type: String, required: true },
createdAt: { type: Date, required: true },
},
{ timestamps: false }
);
module.exports = mongoose.model('Mail', Mail);

View file

@ -0,0 +1,19 @@
const mongoose = require('mongoose');
const Question = mongoose.Schema(
{
title: { type: String, required: true },
text: { type: String, required: true },
creator: { type: String, required: true },
status: { type: String, required: true },
createdAt: { type: Date, required: true },
views: { type: Number, required: true },
answers: { type: Number, required: true },
comments: { type: Number, required: true },
upvotes: { type: Number, required: true },
downvotes: { type: Number, required: true },
},
{ timestamps: { createdAt: false, updatedAt: true } }
);
module.exports = mongoose.model('Question', Question);

View file

@ -0,0 +1,7 @@
const mongoose = require('mongoose');
const ResetRequestSchema = mongoose.Schema({
user: { type: mongoose.Schema.Types.ObjectId, required: true, ref: 'User' },
});
module.exports = mongoose.model('ResetRequest', ResetRequestSchema);

15
server/db/models/Token.js Normal file
View file

@ -0,0 +1,15 @@
const mongoose = require('mongoose');
const TokenSchema = mongoose.Schema(
{
user: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'User',
},
expires: { type: Boolean, required: true, default: true },
},
{ timestamps: { createdAt: true, updatedAt: false } }
);
module.exports = mongoose.model('Tokens', TokenSchema);

12
server/db/models/User.js Normal file
View file

@ -0,0 +1,12 @@
const mongoose = require('mongoose');
const User = mongoose.Schema(
{
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
points: { type: String, required: true, default: 0 },
},
{ timestamps: { createdAt: false, updatedAt: true } }
);
module.exports = mongoose.model('User', User);

25
server/db/models/Vote.js Normal file
View file

@ -0,0 +1,25 @@
const mongoose = require('mongoose');
const Comment = mongoose.Schema(
{
parentID: { type: mongoose.Schema.Types.ObjectId, ref: 'docModel' },
creator: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: 'User',
},
status: {
type: String,
required: true,
enum: ['upvoted', 'downvoted'],
},
docModel: {
type: String,
required: true,
enum: ['Question', 'Answer', 'Comment'],
},
},
{ timestamps: { createdAt: false, updatedAt: true } }
);
module.exports = mongoose.model('Comment', Comment);

View file

@ -0,0 +1,56 @@
const deriveKeyFromPassword = require('../utils/auth');
const createRequest = require('../utils/api');
const config = require('../config.json');
const User = require('../db/models/User');
async function basicAuth(req, res, next) {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).send('Your request is malformed.');
}
const cacheUser = await User.findOne({ username });
if (cacheUser) {
const { key } = await deriveKeyFromPassword(password, cacheUser.salt);
const checkCacheAuth = await createRequest(
'post',
`/users/${username}/auth`,
{
key,
}
);
if (checkCacheAuth.success) {
req.user = cacheUser.username;
return next();
}
}
const { user, success } = await createRequest('get', `/users/${username}`);
await User.create(user);
if (!success) {
return res.status(500).send({
success: false,
error: config.errorGeneric,
});
}
const { key } = await deriveKeyFromPassword(password, user.salt);
const checkAuth = await createRequest('post', `/users/${username}/auth`, {
key,
});
if (checkAuth.success) {
req.user = user;
return next();
} else {
return res.status(401).send({
success: false,
error: 'Login failed. Please check your username and password and try again.',
});
}
}
module.exports = basicAuth;

View file

@ -0,0 +1,29 @@
const Token = require('../db/models/Token');
async function tokenAuth(req, res, next) {
const token = req.query.token || req.body.token;
if (!token) {
return res.status(401).send('Your request is missing session token.');
}
const result = await Token.findByIdAndDelete(token);
const date = new Date();
if (!result) {
return res.status(401).send('The user is unauthenticated.');
} else if (
result.expires &&
date.setDate(date.getDate + 1) > result.createdAt
) {
await Token.findByIdAndDelete(token);
return res.status(401).send('Session token has expired.');
}
req.user = result.username;
return next();
}
module.exports = tokenAuth;

8
server/openapi.json Normal file
View file

@ -0,0 +1,8 @@
{
"openapi": "3.1.0",
"info": {
"title": "qOverflow API BDPA-STL",
"version": "0.1.0"
},
"paths": {}
}

26
server/package.json Normal file
View file

@ -0,0 +1,26 @@
{
"name": "server",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "nodemon ./bin/www"
},
"dependencies": {
"bull": "^4.8.4",
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"dotenv": "^16.0.1",
"express": "~4.16.1",
"http-errors": "~1.6.3",
"jade": "~1.11.0",
"mongoose": "^6.4.4",
"morgan": "~1.9.1",
"superagent": "^8.0.0",
"superagent-throttle": "^1.0.1"
},
"devDependencies": {
"jest": "^28.1.2",
"nodemon": "^2.0.19",
"supertest": "^6.2.4"
}
}

12
server/routes/mail.js Normal file
View file

@ -0,0 +1,12 @@
var express = require('express');
var router = express.Router();
const tokenAuth = require('../middleware/tokenAuth');
const GetMail = require('./mail/Get');
const SendMail = require('./mail/Send');
router.post('/', tokenAuth, SendMail);
router.get('/', tokenAuth, GetMail);
module.exports = router;

33
server/routes/mail/Get.js Normal file
View file

@ -0,0 +1,33 @@
const createRequest = require('../../utils/api');
const config = require('../../config.json');
const { checkOutdated } = require('../../utils/checkOutdated');
const getAllMail = require('../../utils/getAllMail');
const Mail = require('../../db/models/Mail');
async function Get(req, res, next) {
const { username } = req.params;
const messages = Mail.find({ reciever: username }).sort({
createdAt: 'descending',
});
if (mail.length === 0 && checkOutdated(mail[0], config.mailExpires)) {
try {
await getAllMail();
} catch {
return res
.status(500)
.send({ success: false, error: config.errorGeneric });
}
} else {
return res.send({ success: true, messages });
}
const newMessages = Mail.find({ reciever, username }).sort({
createdAt: 'descending',
});
return res.send({ success: true, messages: newMessages });
}
module.exports = Get;

View file

@ -0,0 +1,27 @@
const Mail = require('../../db/models/Mail');
const createRequest = require('../../utils/api');
async function Send(req, res, next) {
const user = req.user;
const { reciever, subject, text } = req.body;
if (!reciever | !subject | !text) {
return res.status(400).send('Your request is missing information.');
}
const { success, message } = await createRequest(
'post',
`/mail/${username}`,
{
sender: user,
}
);
Mail.create({ ...message, id: message.mail_id });
return success
? res.send({ success: true })
: res.status(500).send('Something went wrong.');
}
module.exports = Send;

40
server/routes/question.js Normal file
View file

@ -0,0 +1,40 @@
var express = require('express');
var router = express.Router();
const tokenAuth = require('../middleware/tokenAuth');
const CreateAnswer = require('./question/CreateAnswer');
const CreateAnswerComment = require('./question/CreateAnswerComment');
const CreateComment = require('./question/CreateComment');
const CreateQuestion = require('./question/CreateQuestion');
const DeleteAnswerComment = require('./question/DeleteAnswerComment');
const DeleteComment = require('./question/DeleteComment');
const EditAnswer = require('./question/EditAnswer');
const EditQuestion = require('./question/EditQuestion');
const GetQuestion = require('./question/GetQuestion');
const Search = require('./question/Search');
const UpdateQuestionVote = require('./question/UpdateQuestionVote');
const UpdateQuestionCommentVote = require('./question/UpdateQuestionCommentVote');
const UpdateQuestionAnswerVote = require('./question/UpdateQuestionAnswerVote');
const UpdateQuestionAnswerCommentVote = require('./question/UpdateQuestionAnswerCommentVote');
router.get('/search', Search);
router.get('/:questionID', GetQuestion);
router.patch('/:questionID', tokenAuth, EditQuestion);
router.post('/', tokenAuth, CreateQuestion);
router.post('/:questionID/comment', tokenAuth, CreateComment);
router.post('/:questionID/answer', tokenAuth, CreateAnswer);
router.post('/:questionID/:answerID/comment', tokenAuth, CreateAnswerComment);
router.patch('/:questionID/:answerID', tokenAuth, EditAnswer);
router.patch('/:questionID/vote');
router.patch('/:questionID/comments/:commentID/vote');
router.patch('/:questionID/answers/:answerID/vote');
router.patch('/:questionID/answers/:answerID/comments/:commentID/vote');
router.delete(
'/:questionID/:answerID/:commentID',
tokenAuth,
DeleteAnswerComment
);
router.delete('/:questionID/:commentID', tokenAuth, DeleteComment);
module.exports = router;

View file

@ -0,0 +1,18 @@
async function CreateAnswer(req, res, next) {
const user = req.user;
const { title, text } = req.body;
if (!title || !text) {
return res.status(400).send('Your request is missing information.');
}
const { success } = await createRequest('post', `/questions`, {
creator: user,
title,
text,
});
return success ? res.send() : res.status(500).send('Something went wrong.');
}
module.exports = CreateAnswer;

View file

@ -0,0 +1,22 @@
async function CreateAnswerComment(req, res, next) {
const user = req.user;
const { questionID, answerID } = req.params;
const { text } = req.body;
if (!text) {
return res.status(400).send('Your request is missing information.');
}
const { success } = await createRequest(
'post',
`/questions/${questionID}/answers/${answerID}/comments`,
{
creator: user,
text,
}
);
return success ? res.send() : res.status(500).send('Something went wrong.');
}
module.exports = CreateAnswerComment;

View file

@ -0,0 +1,22 @@
async function CreateComment(req, res, next) {
const user = req.user;
const { questionID } = req.params;
const { text } = req.body;
if (!text) {
return res.status(400).send('Your request is missing information.');
}
const { success } = await createRequest(
'post',
`/questions/${questionID}/comments`,
{
creator: user,
text,
}
);
return success ? res.send() : res.status(500).send('Something went wrong.');
}
module.exports = CreateComment;

View file

@ -0,0 +1,18 @@
async function CreateQuestion(req, res, next) {
const user = req.user;
const { title, text } = req.body;
if (!title || !text) {
return res.status(400).send('Your request is missing information.');
}
const { success } = await createRequest('post', `/questions`, {
creator: user,
title,
text,
});
return success ? res.send() : res.status(500).send('Something went wrong.');
}
module.exports = CreateQuestion;

View file

@ -0,0 +1,13 @@
async function DeleteAnswerComment(req, res, next) {
const user = req.user;
const { questionID, answerID, commentID } = req.params;
const { success } = await createRequest(
'delete',
`/questions/${questionID}/answers/${answerID}/comments/${commentID}`
);
return success ? res.send() : res.status(500).send('Something went wrong.');
}
module.exports = DeleteAnswerComment;

View file

@ -0,0 +1,13 @@
async function DeleteComment(req, res, next) {
const user = req.user;
const { questionID, commentID } = req.params;
const { success } = await createRequest(
'delete',
`/questions/${questionID}/comments/${commentID}`
);
return success ? res.send() : res.status(500).send('Something went wrong.');
}
module.exports = DeleteComment;

View file

@ -0,0 +1,15 @@
async function EditAnswer(req, res, next) {
const user = req.user;
const { token, ...body } = req.body;
const { questionID, answerID } = req.params;
const { success } = await createRequest(
'patch',
`/questions/${questionID}/answers/${answerID}`,
body
);
return success ? res.send() : res.status(500).send('Something went wrong.');
}
module.exports = EditAnswer;

View file

@ -0,0 +1,15 @@
async function EditQuestion(req, res, next) {
const user = req.user;
const { token, ...body } = req.body;
const { questionID } = req.params;
const { success } = await createRequest(
'patch',
`/questions/${questionID}`,
body
);
return success ? res.send() : res.status(500).send('Something went wrong.');
}
module.exports = EditQuestion;

View file

@ -0,0 +1,16 @@
const createRequest = require('../../utils/api');
async function GetQuestion(req, res, next) {
const { questionID } = req.params;
const { success, question } = createRequest(
'get',
`/questions/${questionID}`
);
return success
? res.send(question)
: res.status(500).send('Something went wrong.');
}
module.exports = GetQuestion;

View file

@ -0,0 +1,25 @@
async function GetVote(req, res, next) {
const user = req.user;
const { token, questionID, answerID, commentID, ...body } = req.query;
if (!questionID) {
return res.status(400).send('Your request is missing something.');
}
var requestURL = '';
if (answerID && commentID) {
requestURL = `/questions/${questionID}/answers/${answerID}/comments/${commentID}/vote/${user}`;
} else if (answerID) {
requestURL = `/questions/${questionID}/answers/${answerID}/vote/${user}`;
} else if (commentID) {
requestURL = `/questions/${questionID}/comments/${commentID}/vote/${user}`;
} else {
requestURL = `/questions/${questionID}/vote/${user}`;
}
const { success } = await createRequest('get', requestURL, body);
return success ? res.send() : res.status(500).send('Something went wrong.');
}
module.exports = GetVote;

View file

@ -0,0 +1,24 @@
const createRequest = require('../../utils/api');
const Question = require('../../db/models/Question');
async function Seach(req, res, next) {
const { success, questions } = await createRequest(
'get',
`/questions/search`,
req.params
);
const questionSet = questions.map((question) => ({
id: question.question_id,
...question,
}));
await Question.create(questionSet);
return success
? res.send(questionSet)
: res.status(500).send('Something went wrong.');
}
module.exports = Seach;

View file

@ -0,0 +1,64 @@
const createRequest = require('../../utils/api');
async function UpdateQuestionAnswerCommentVote(req, res, next) {
const user = req.user;
const { questionID, answerID, commentID } = req.params;
const { operation } = req.body;
if (!operation) {
return res.status(400).send('Your request is missing something.');
}
const currentVote = await createRequest(
'get',
`/questions/${questionID}/answers/${answerID}/comments/${commentID}/vote/${user}`
);
if (!currentVote.success) {
return res.status(500).send('Something went wrong.');
}
if (operation === 'upvote') {
if (currentVote.vote === 'upvoted') {
res.status(400).send('You have already upvoted this question.');
} else {
if (currentVote.vote === 'downvoted') {
await createRequest(
'patch',
`/questions/${questionID}/answers/${answerID}/comments/${commentID}/vote/${user}`,
{ operation: 'decrement', target: 'downvotes' }
);
await createRequest('patch', `/users/${user}/points`, {
operation: 'increment',
amount: 1,
});
}
await createRequest(
'patch',
`/questions/${questionID}/answers/${answerID}/comments/${commentID}/vote/${user}`,
{ operation: 'increment', target: 'upvotes' }
);
}
} else {
if (currentVote.vote === 'downvoted') {
res.status(400).send('You have already downvoted this question.');
} else {
if (currentVote.vote === 'upvoted') {
await createRequest(
'patch',
`/questions/${questionID}/answers/${answerID}/comments/${commentID}/vote/${user}`,
{ operation: 'decrement', target: 'upvotes' }
);
}
await createRequest(
'patch',
`/questions/${questionID}/answers/${answerID}/comments/${commentID}/vote/${user}`,
{ operation: 'increment', target: 'downvotes' }
);
}
}
}
module.exports = UpdateQuestionAnswerCommentVote;

View file

@ -0,0 +1,60 @@
const createRequest = require('../../utils/api');
async function UpdateQuestionAnswerCommentVote(req, res, next) {
const user = req.user;
const { questionID, answerID, commentID } = req.params;
const { operation } = req.body;
if (!operation) {
return res.status(400).send('Your request is missing something.');
}
const currentVote = await createRequest(
'get',
`/questions/${questionID}/answers/${answerID}/vote/${user}`
);
if (!currentVote.success) {
return res.status(500).send('Something went wrong.');
}
if (operation === 'upvote') {
if (currentVote.vote === 'upvoted') {
res.status(400).send('You have already upvoted this question.');
} else {
if (currentVote.vote === 'downvoted') {
await createRequest(
'patch',
`/questions/${questionID}/answers/${answerID}/vote/${user}`,
{ operation: 'decrement', target: 'downvotes' }
);
}
await createRequest(
'patch',
`/questions/${questionID}/answers/${answerID}/vote/${user}`,
{ operation: 'increment', target: 'upvotes' }
);
}
} else {
if (currentVote.vote === 'downvoted') {
res.status(400).send('You have already downvoted this question.');
} else {
if (currentVote.vote === 'upvoted') {
await createRequest(
'patch',
`/questions/${questionID}/answers/${answerID}/vote/${user}`,
{ operation: 'decrement', target: 'upvotes' }
);
}
await createRequest(
'patch',
`/questions/${questionID}/answers/${answerID}/vote/${user}`,
{ operation: 'increment', target: 'downvotes' }
);
}
}
}
module.exports = UpdateQuestionAnswerCommentVote;

View file

@ -0,0 +1,60 @@
const createRequest = require('../../utils/api');
async function UpdateQuestionAnswerCommentVote(req, res, next) {
const user = req.user;
const { questionID, answerID, commentID } = req.params;
const { operation } = req.body;
if (!operation) {
return res.status(400).send('Your request is missing something.');
}
const currentVote = await createRequest(
'get',
`/questions/${questionID}/comments/${commentID}/vote/${user}`
);
if (!currentVote.success) {
return res.status(500).send('Something went wrong.');
}
if (operation === 'upvote') {
if (currentVote.vote === 'upvoted') {
res.status(400).send('You have already upvoted this question.');
} else {
if (currentVote.vote === 'downvoted') {
await createRequest(
'patch',
`/questions/${questionID}/comments/${commentID}/vote/${user}`,
{ operation: 'decrement', target: 'downvotes' }
);
}
await createRequest(
'patch',
`/questions/${questionID}/comments/${commentID}/vote/${user}`,
{ operation: 'increment', target: 'upvotes' }
);
}
} else {
if (currentVote.vote === 'downvoted') {
res.status(400).send('You have already downvoted this question.');
} else {
if (currentVote.vote === 'upvoted') {
await createRequest(
'patch',
`/questions/${questionID}/comments/${commentID}/vote/${user}`,
{ operation: 'decrement', target: 'upvotes' }
);
}
await createRequest(
'patch',
`/questions/${questionID}/comments/${commentID}/vote/${user}`,
{ operation: 'increment', target: 'downvotes' }
);
}
}
}
module.exports = UpdateQuestionAnswerCommentVote;

View file

@ -0,0 +1,60 @@
const createRequest = require('../../utils/api');
async function UpdateQuestionAnswerCommentVote(req, res, next) {
const user = req.user;
const { questionID, answerID, commentID } = req.params;
const { operation } = req.body;
if (!operation) {
return res.status(400).send('Your request is missing something.');
}
const currentVote = await createRequest(
'get',
`/questions/${questionID}/vote/${user}`
);
if (!currentVote.success) {
return res.status(500).send('Something went wrong.');
}
if (operation === 'upvote') {
if (currentVote.vote === 'upvoted') {
res.status(400).send('You have already upvoted this question.');
} else {
if (currentVote.vote === 'downvoted') {
await createRequest(
'patch',
`/questions/${questionID}/vote/${user}`,
{ operation: 'decrement', target: 'downvotes' }
);
}
await createRequest(
'patch',
`/questions/${questionID}/vote/${user}`,
{ operation: 'increment', target: 'upvotes' }
);
}
} else {
if (currentVote.vote === 'downvoted') {
res.status(400).send('You have already downvoted this question.');
} else {
if (currentVote.vote === 'upvoted') {
await createRequest(
'patch',
`/questions/${questionID}/vote/${user}`,
{ operation: 'decrement', target: 'upvotes' }
);
}
await createRequest(
'patch',
`/questions/${questionID}/vote/${user}`,
{ operation: 'increment', target: 'downvotes' }
);
}
}
}
module.exports = UpdateQuestionAnswerCommentVote;

27
server/routes/user.js Normal file
View file

@ -0,0 +1,27 @@
var express = require('express');
var router = express.Router();
const basicAuth = require('../middleware/basicAuth');
const tokenAuth = require('../middleware/tokenAuth');
const Questions = require('./user/Questions');
const Answers = require('./user/Answers');
const GetUser = require('./user/GetUser');
const Login = require('./user/Login');
const Logout = require('./user/Logout');
const Register = require('./user/Register');
const RequestReset = require('./user/RequestReset');
const ResetPassword = require('./user/ResetPassword');
const Update = require('./user/Update');
router.get('/', GetUser);
router.get('/questions', tokenAuth, Questions);
router.get('/answers', tokenAuth, Answers);
router.get('/login', basicAuth, Login);
router.get('/logout', tokenAuth, Logout);
router.post('/register', Register);
router.get('/reset', RequestReset);
router.post('/reset/:id', ResetPassword);
router.patch('/update', tokenAuth, Update);
module.exports = router;

Some files were not shown because too many files have changed in this diff Show more