Merged dev into main

This commit is contained in:
Ceferino Patino 2022-08-02 00:58:59 -05:00
commit b813ddb3a8
26 changed files with 380 additions and 162 deletions

View file

@ -3,6 +3,7 @@
## BDPA NHSCC 2022 Problem:
- [Problem Statement](https://github.com/nhscc/problem-statements/tree/main/2022/qoverflow)
- [Backend Documentation](https://hscc8udvc7gs.docs.apiary.io/)
## Solution

View file

@ -17,6 +17,7 @@
"js-cookie": "^3.0.1",
"nvm": "^0.0.4",
"react": "^18.2.0",
"react-gravatar": "^2.6.3",
"react-helmet": "^6.1.0",
"react-markdown": "^8.0.3",
"react-router-dom": "^6.3.0",

View file

@ -1,5 +1,7 @@
import { Card, CardContent } from '@mui/material';
import React, { useEffect } from 'react';
import { Inbox, SendMail } from 'controllers';
import { MdPreview } from 'components';
import { useNavigate } from 'react-router-dom';
import { useUser } from 'contexts';

View file

@ -3,19 +3,33 @@ import { Form } from 'controllers/FormControllers';
import { composeMailFields } from 'services/fields';
import { postMail } from 'services/mailServices';
import { mailSchema } from 'services/schemas';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
export default function SendMailController() {
const navigate = useNavigate();
const { userData } = useUser();
const sendMail = async (data) => {
const { status } = await postMail(data);
if (status === 200)
navigate('/');
const navigate = useNavigate();
useEffect(() => {
if (checkAuth()) {
navigate('/users/login', {
state: {
name: 'ask',
msg: 'you need to be authenticated to access this feature',
prevPath: '/ask',
},
});
}
}, []);
function checkAuth() {
if (!userData.username) {
return true;
}
}
return Form({
fields: composeMailFields,
onSubmit: sendMail,
validationSchema: mailSchema
onSubmit: postMail,
validationSchema: mailSchema,
});
}

View file

@ -8,7 +8,7 @@ import ResetForm from './ResetFormController';
import SendMail from './SendMailController';
import SearchForm from './SearchFormController';
import UpdateForm from './UpdateFormController';
import AnswerForm from './AnwerFormController';
export {
AnswerForm,
AskForm,
@ -19,5 +19,6 @@ export {
ResetForm,
SendMail,
SearchForm,
UpdateForm
UpdateForm,
AnswerForm,
};

View file

@ -8,28 +8,28 @@ const answerFields = [
{
id: 'text',
label: 'Answer',
multiline: true
}
multiline: true,
},
];
const askQuestionFields = [
{
id: 'title',
label: 'Title'
label: 'Title',
},
{
id: 'text',
label: 'Text',
multiline: true
}
multiline: true,
},
];
const commentFields = [
{
id: 'text',
label: 'Comment',
multiline: true
}
multiline: true,
},
];
const composeMailFields = [
@ -39,38 +39,38 @@ const composeMailFields = [
},
{
id: 'subject',
label: 'Subject:',
label: 'Subject',
},
{
id: 'text',
label: 'Body',
multiline: true,
}
},
];
const loginFields = [
{
id: 'username',
title: 'Username'
title: 'Username',
},
{
id: 'password',
title: 'Password',
type: 'password'
}
type: 'password',
},
];
const recoverFields = [
{
id: 'username',
title: 'Username'
}
title: 'Username',
},
];
const registerFields = [
{
id: 'username',
title: 'Username'
title: 'Username',
},
{
id: 'email',
@ -85,7 +85,7 @@ const registerFields = [
{
id: 'captcha',
title: 'CAPTCHA',
}
},
];
const resetFields = [
@ -94,7 +94,7 @@ const resetFields = [
helperText: (value) => `Password strength: ${passwordStrength(value)}`,
title: 'Password',
type: 'password',
}
},
];
const patchFields = [
@ -107,8 +107,8 @@ const patchFields = [
helperText: (value) => `Password strength: ${passwordStrength(value)}`,
title: 'Password',
type: 'password',
}
]
},
];
const searchFields = [
{
@ -120,13 +120,13 @@ const searchFields = [
label: 'Text',
},
{
id: 'createdAt',
id: 'createdAt',
type: 'date',
},
{
id: 'creator',
label: 'Creator',
}
},
];
export {
@ -139,5 +139,5 @@ export {
registerFields,
recoverFields,
resetFields,
searchFields
searchFields,
};

View file

@ -2,17 +2,8 @@ import { createEndpoint } from './api';
const callMailAPI = createEndpoint('/mail');
const postMail = async (data) =>
callMailAPI(
'post',
``,
data
);
const postMail = async (data) => callMailAPI('post', ``, data);
const getMail = async () =>
callMailAPI(
'get',
``
);
const getMail = async () => callMailAPI('get', ``);
export { getMail, postMail };

117
package-lock.json generated
View file

@ -30,6 +30,7 @@
"js-cookie": "^3.0.1",
"nvm": "^0.0.4",
"react": "^18.2.0",
"react-gravatar": "^2.6.3",
"react-helmet": "^6.1.0",
"react-markdown": "^8.0.3",
"react-router-dom": "^6.3.0",
@ -5549,6 +5550,14 @@
"version": "1.2.1",
"license": "MIT"
},
"node_modules/charenc": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==",
"engines": {
"node": "*"
}
},
"node_modules/charm": {
"version": "0.1.2",
"license": "MIT/X11"
@ -6020,6 +6029,14 @@
"node": ">= 8"
}
},
"node_modules/crypt": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
"integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==",
"engines": {
"node": "*"
}
},
"node_modules/crypto-js": {
"version": "4.1.1",
"license": "MIT"
@ -9472,6 +9489,11 @@
"node": ">=0.10.0"
}
},
"node_modules/is-retina": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-retina/-/is-retina-1.0.3.tgz",
"integrity": "sha512-/tCmbIETZwCd8uHWO+GvbRa7jxwHFHdfetHfiwoP0aN9UDf3prUJMtKn7iBFYipYhqY1bSTjur8hC/Dakt8eyw=="
},
"node_modules/is-root": {
"version": "2.1.0",
"license": "MIT",
@ -12491,6 +12513,16 @@
"tmpl": "1.0.5"
}
},
"node_modules/md5": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
"integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
"dependencies": {
"charenc": "0.0.2",
"crypt": "0.0.2",
"is-buffer": "~1.1.6"
}
},
"node_modules/mdast-util-definitions": {
"version": "5.1.1",
"license": "MIT",
@ -15575,6 +15607,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/query-string": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz",
"integrity": "sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==",
"dependencies": {
"object-assign": "^4.1.0",
"strict-uri-encode": "^1.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"funding": [
@ -15856,6 +15900,19 @@
"version": "2.0.4",
"license": "MIT"
},
"node_modules/react-gravatar": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/react-gravatar/-/react-gravatar-2.6.3.tgz",
"integrity": "sha512-yITonigS2LmG7Fw0gWfZfcVwy1mpiBHNVmoFyetitQjXu7JCYoE6jtub0GIfq+ydpnQSYyJT3kwpX6zj1wXR4w==",
"dependencies": {
"is-retina": "^1.0.3",
"md5": "^2.1.0",
"query-string": "^4.2.2"
},
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-helmet": {
"version": "6.1.0",
"license": "MIT",
@ -18020,6 +18077,14 @@
"node": ">= 0.8"
}
},
"node_modules/strict-uri-encode": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
"integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"license": "MIT",
@ -23572,6 +23637,11 @@
"character-parser": {
"version": "1.2.1"
},
"charenc": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz",
"integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA=="
},
"charm": {
"version": "0.1.2"
},
@ -23865,6 +23935,11 @@
"which": "^2.0.1"
}
},
"crypt": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz",
"integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow=="
},
"crypto-js": {
"version": "4.1.1"
},
@ -25907,6 +25982,11 @@
"is-regexp": {
"version": "1.0.0"
},
"is-retina": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/is-retina/-/is-retina-1.0.3.tgz",
"integrity": "sha512-/tCmbIETZwCd8uHWO+GvbRa7jxwHFHdfetHfiwoP0aN9UDf3prUJMtKn7iBFYipYhqY1bSTjur8hC/Dakt8eyw=="
},
"is-root": {
"version": "2.1.0"
},
@ -27809,6 +27889,16 @@
"tmpl": "1.0.5"
}
},
"md5": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz",
"integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==",
"requires": {
"charenc": "0.0.2",
"crypt": "0.0.2",
"is-buffer": "~1.1.6"
}
},
"mdast-util-definitions": {
"version": "5.1.1",
"requires": {
@ -29461,7 +29551,7 @@
"@emotion/styled": "^11.9.3",
"@mui/icons-material": "^5.8.4",
"@mui/material": "^5.9.1",
"acorn": "*",
"acorn": "^8.8.0",
"crypto-js": "^4.1.1",
"dotenv": "^16.0.1",
"formik": "^2.2.9",
@ -29470,6 +29560,7 @@
"js-cookie": "^3.0.1",
"nvm": "^0.0.4",
"react": "^18.2.0",
"react-gravatar": "*",
"react-helmet": "^6.1.0",
"react-markdown": "^8.0.3",
"react-router-dom": "^6.3.0",
@ -29495,6 +29586,15 @@
"side-channel": "^1.0.4"
}
},
"query-string": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.4.tgz",
"integrity": "sha512-O2XLNDBIg1DnTOa+2XrIwSiXEV8h2KImXUnjhhn2+UsvZ+Es2uyd5CCRTNQlDGbzUQOW3aYCBx9rVA6dzsiY7Q==",
"requires": {
"object-assign": "^4.1.0",
"strict-uri-encode": "^1.0.0"
}
},
"queue-microtask": {
"version": "1.2.3"
},
@ -29665,6 +29765,16 @@
"react-fast-compare": {
"version": "2.0.4"
},
"react-gravatar": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/react-gravatar/-/react-gravatar-2.6.3.tgz",
"integrity": "sha512-yITonigS2LmG7Fw0gWfZfcVwy1mpiBHNVmoFyetitQjXu7JCYoE6jtub0GIfq+ydpnQSYyJT3kwpX6zj1wXR4w==",
"requires": {
"is-retina": "^1.0.3",
"md5": "^2.1.0",
"query-string": "^4.2.2"
}
},
"react-helmet": {
"version": "6.1.0",
"requires": {
@ -31087,6 +31197,11 @@
"statuses": {
"version": "2.0.1"
},
"strict-uri-encode": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz",
"integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ=="
},
"string_decoder": {
"version": "1.3.0",
"requires": {

View file

@ -8,8 +8,9 @@
"start": "npm start -w",
"start:server": "npm start -w server",
"start:client": "npm start -w client",
"start:prod": "npm run start:server",
"prestart:prod": "npm run build -w client",
"start:prod": "npm run prod -w server",
"prestart:prod": "npm run build",
"dev": "npm start -workspaces",
"build": "npm run build -w client"
},
"repository": {
@ -25,4 +26,4 @@
"client",
"server"
]
}
}

View file

@ -19,7 +19,6 @@ 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('/users', userRouter);
app.use('/mail', mailRouter);

View file

@ -34,10 +34,16 @@ const server = http.createServer(app);
mongoose.connect(process.env.DB_CONN_STRING, {
useNewUrlParser: true,
useUnifiedTopology: true,
dbName: process.env.DB_NAME,
});
mongoose.connection.once('open', () => console.log('MongoDB connected'));
mongoose.connection.on('error', console.error.bind(console, 'MongoDB connection error:'));
mongoose.connection.once('open', () =>
console.log('[INFO]: MongoDB connected')
);
mongoose.connection.on(
'error',
console.error.bind(console, '[ERROR]: MongoDB connection error - ')
);
cron.schedule('*/30 * * * * *', refreshQuestions);
cron.schedule('*/2 * * * *', refreshUsers);
@ -84,6 +90,7 @@ function onError(error) {
function onListening() {
const addr = server.address();
const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port;
const bind =
typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port;
debug('Listening on ' + bind);
}

View file

@ -4,7 +4,7 @@ const User = mongoose.Schema(
{
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
points: { type: Number, required: true, default: 1 },
points: { type: Number, required: true, default: 0 },
salt: { type: String, required: true },
lastMailFetch: { type: Date, default: new Date(0) },
lastAnswerFetch: { type: Date, default: new Date(0) },

View file

@ -1,6 +1,5 @@
const Token = require('server/db/models/Token');
const User = require('server/db/models/User');
const jwt = require('jsonwebtoken');
const config = require('server/config.json');
async function tokenAuth(req, res, next) {
@ -9,10 +8,9 @@ async function tokenAuth(req, res, next) {
const token = authHeader?.split(' ')[1];
// Verify token is included in request
if (!token)
return res.status(401).send(config.errorUnauthed);
if (!token) return res.status(401).send(config.errorUnauthed);
const result = await Token.findOne({ token: token });
const result = await Token.findOne({ token });
// Verify token expiration
const date = new Date();

View file

@ -45,13 +45,11 @@ paths:
schema:
type: object
properties:
username:
type: string
minLength: 4
maxLength: 16
email:
type: string
format: email
password:
type: string
security:
- BearerAuth: []
responses:
@ -159,6 +157,27 @@ paths:
$ref: '#/components/responses/ServerError'
tags:
- users
/users/remember:
get:
summary: Retrieve authenticated data of the user
security:
- BearerAuth: []
responses:
'200':
description: OK
content:
'application/json':
schema:
type: object
properties:
user:
$ref: '#/components/schemas/User'
'400':
$ref: '#/components/responses/BadRequest'
'500':
$ref: '#/components/responses/ServerError'
tags:
- users
/users/logout:
delete:
summary: Login a user and receiver an authentication token
@ -344,11 +363,11 @@ paths:
$ref: '#/components/responses/ServerError'
tags:
- questions
/questions/{question_id}:
/questions/{questionID}:
parameters:
- $ref: '#/components/parameters/question_id'
- $ref: '#/components/parameters/questionID'
get:
summary: Get a question by its question_id
summary: Get a question by its questionID
responses:
'200':
description: OK
@ -364,7 +383,7 @@ paths:
tags:
- questions
patch:
summary: Modify a question by its question_id
summary: Modify a question by its questionID
security:
- BearerAuth: []
requestBody:
@ -386,9 +405,9 @@ paths:
$ref: '#/components/responses/BadRequest'
tags:
- questions
/questions/{question_id}/close:
/questions/{questionID}/close:
parameters:
- $ref: '#/components/parameters/question_id'
- $ref: '#/components/parameters/questionID'
patch:
summary: Toggle vote to close a question
security:
@ -404,9 +423,9 @@ paths:
$ref: '#/components/responses/BadRequest'
tags:
- questions
/questions/{question_id}/protect:
/questions/{questionID}/protect:
parameters:
- $ref: '#/components/parameters/question_id'
- $ref: '#/components/parameters/questionID'
patch:
summary: Toggle vote to protect a question
security:
@ -422,9 +441,9 @@ paths:
$ref: '#/components/responses/BadRequest'
tags:
- questions
/questions/{question_id}/reopen:
/questions/{questionID}/reopen:
parameters:
- $ref: '#/components/parameters/question_id'
- $ref: '#/components/parameters/questionID'
patch:
summary: Toggle vote to reopen a question
security:
@ -440,9 +459,9 @@ paths:
$ref: '#/components/responses/BadRequest'
tags:
- questions
/questions/{question_id}/vote:
/questions/{questionID}/vote:
parameters:
- $ref: '#/components/parameters/question_id'
- $ref: '#/components/parameters/questionID'
get:
summary: Get a user's vote on a question
security:
@ -491,9 +510,9 @@ paths:
$ref: '#/components/responses/BadRequest'
tags:
- questions
/questions/{question_id}/comments:
/questions/{questionID}/comments:
parameters:
- $ref: '#/components/parameters/question_id'
- $ref: '#/components/parameters/questionID'
get:
summary: Get all comments of a question
responses:
@ -536,10 +555,10 @@ paths:
$ref: '#/components/responses/BadRequest'
tags:
- comments
/questions/{question_id}/comments/{comment_id}:
/questions/{questionID}/comments/{commentID}:
parameters:
- $ref: '#/components/parameters/question_id'
- $ref: '#/components/parameters/comment_id'
- $ref: '#/components/parameters/questionID'
- $ref: '#/components/parameters/commentID'
delete:
summary: Delete a question comment
security:
@ -555,10 +574,10 @@ paths:
$ref: '#/components/responses/ServerError'
tags:
- comments
/questions/{question_id}/comments/{comment_id}/vote:
/questions/{questionID}/comments/{commentID}/vote:
parameters:
- $ref: '#/components/parameters/question_id'
- $ref: '#/components/parameters/comment_id'
- $ref: '#/components/parameters/questionID'
- $ref: '#/components/parameters/commentID'
get:
summary: Get a user's vote on a question comment
security:
@ -609,9 +628,9 @@ paths:
tags:
- comments
- votes
/questions/{question_id}/answers:
/questions/{questionID}/answers:
parameters:
- $ref: '#/components/parameters/question_id'
- $ref: '#/components/parameters/questionID'
get:
summary: Get all answers of a question
responses:
@ -654,12 +673,12 @@ paths:
$ref: '#/components/responses/BadRequest'
tags:
- answers
/questions/{question_id}/answers/{answer_id}:
/questions/{questionID}/answers/{answerID}:
parameters:
- $ref: '#/components/parameters/question_id'
- $ref: '#/components/parameters/answer_id'
- $ref: '#/components/parameters/questionID'
- $ref: '#/components/parameters/answerID'
patch:
summary: Modify an answer by its answer_id
summary: Modify an answer by its answerID
security:
- BearerAuth: []
requestBody:
@ -688,10 +707,10 @@ paths:
$ref: '#/components/responses/BadRequest'
tags:
- answers
/questions/{question_id}/answers/{answer_id}/accept:
/questions/{questionID}/answers/{answerID}/accept:
parameters:
- $ref: '#/components/parameters/question_id'
- $ref: '#/components/parameters/answer_id'
- $ref: '#/components/parameters/questionID'
- $ref: '#/components/parameters/answerID'
patch:
summary: Accept an answer as correct
security:
@ -707,10 +726,10 @@ paths:
$ref: '#/components/responses/BadRequest'
tags:
- answers
/questions/{question_id}/answers/{answer_id}/vote:
/questions/{questionID}/answers/{answerID}/vote:
parameters:
- $ref: '#/components/parameters/question_id'
- $ref: '#/components/parameters/answer_id'
- $ref: '#/components/parameters/questionID'
- $ref: '#/components/parameters/answerID'
get:
summary: Get a user's vote on an answer
security:
@ -761,10 +780,10 @@ paths:
tags:
- answers
- votes
/questions/{question_id}/answers/{answer_id}/comments:
/questions/{questionID}/answers/{answerID}/comments:
parameters:
- $ref: '#/components/parameters/question_id'
- $ref: '#/components/parameters/answer_id'
- $ref: '#/components/parameters/questionID'
- $ref: '#/components/parameters/answerID'
get:
summary: Get all comments of a answer
responses:
@ -807,11 +826,11 @@ paths:
$ref: '#/components/responses/BadRequest'
tags:
- comments
/questions/{question_id}/answers/{answer_id}/comments/{comment_id}:
/questions/{questionID}/answers/{answerID}/comments/{commentID}:
parameters:
- $ref: '#/components/parameters/question_id'
- $ref: '#/components/parameters/answer_id'
- $ref: '#/components/parameters/comment_id'
- $ref: '#/components/parameters/questionID'
- $ref: '#/components/parameters/answerID'
- $ref: '#/components/parameters/commentID'
delete:
summary: Delete an answer comment
security:
@ -827,11 +846,11 @@ paths:
$ref: '#/components/responses/ServerError'
tags:
- comments
/questions/{question_id}/answers/{answer_id}/comments/{comment_id}/vote:
/questions/{questionID}/answers/{answerID}/comments/{commentID}/vote:
parameters:
- $ref: '#/components/parameters/question_id'
- $ref: '#/components/parameters/answer_id'
- $ref: '#/components/parameters/comment_id'
- $ref: '#/components/parameters/questionID'
- $ref: '#/components/parameters/answerID'
- $ref: '#/components/parameters/commentID'
get:
summary: Get a user's vote on an answer comment
security:
@ -903,7 +922,7 @@ components:
id:
type: string
example: '62d8727d6c584b3b12eb23be'
question_id:
questionID:
type: string
example: '62d5c7ad1b8e0da8804bde22'
creator:
@ -926,7 +945,7 @@ components:
id:
type: string
example: '62d87292afa7f2e7e81f1470'
parent_id:
parentID:
type: string
example: '62d5c7ad1b8e0da8804bde22'
creator:
@ -1021,6 +1040,8 @@ components:
format: email
points:
type: integer
level:
type: integer
Vote:
type: string
nullable: true
@ -1077,8 +1098,8 @@ components:
type: string
example: 'That resource was not found. Please check your request and try again.'
parameters:
question_id:
name: question_id
questionID:
name: questionID
in: path
required: true
schema:
@ -1086,8 +1107,8 @@ components:
example: '62d61d428849dda1c15a8b64'
description: >-
This should be the object id of the desired question.
answer_id:
name: answer_id
answerID:
name: answerID
in: path
required: true
schema:
@ -1095,8 +1116,8 @@ components:
example: '62d61ffd5f20e06f3cb350c7'
description: >-
This should be the object id of the desired answer.
comment_id:
name: comment_id
commentID:
name: commentID
in: path
required: true
schema:

View file

@ -19,8 +19,8 @@
"superagent-throttle": "^1.0.1"
},
"scripts": {
"start": "pm2 start ./bin/www",
"dev": "nodemon ./bin/www"
"start": "nodemon ./bin/www",
"prod": "pm2 start ./bin/www"
},
"devDependencies": {
"jest": "^28.1.3",

View file

@ -22,16 +22,12 @@ async function CreateAnswer(req, res) {
if (question.status === 'closed')
return res.status(403).send(config.errorForbidden);
if (
question.status === 'protected' &&
getUserLevel(user.points) < 5
) {
if (question.status === 'protected' && getUserLevel(user.points) < 5) {
return res.status(403).send(config.errorForbidden);
}
// Verify request includes required information
if (!text)
return res.status(400).send(config.errorIncomplete);
if (!text) return res.status(400).send(config.errorIncomplete);
// Create question with BDPA server
const { success, answer } = await createRequest(

View file

@ -4,11 +4,12 @@ const User = require('server/db/models/User');
const createRequest = require('server/utils/api');
async function CreateQuestion(req, res) {
const { user: {username, user_id} } = req;
const {
user: { username, user_id },
} = req;
const { title, text } = req.body;
if (!title || !text)
return res.status(400).send(config.errorIncomplete);
if (!title || !text) return res.status(400).send(config.errorIncomplete);
const { success, question } = await createRequest('post', `/questions`, {
creator: username,
@ -26,7 +27,7 @@ async function CreateQuestion(req, res) {
amount: 1,
});
await User.findByIdAndUpdate(user_id, { $inc: { points: 1 } });
await User.findByIdAndUpdate(user.id, { points: { $inc: 1 } });
return res.sendStatus(200);
}

View file

@ -8,27 +8,32 @@ async function GetUser(req, res) {
const cachedUser = await User.findOne({ username });
if (cachedUser) return res.send({
user: {
username: cachedUser.username,
email: cachedUser.email,
points: cachedUser.points,
level: getUserLevel(cachedUser.points),
}
});
if (cachedUser)
return res.send({
user: {
username: cachedUser.username,
email: cachedUser.email,
points: cachedUser.points,
level: getUserLevel(cachedUser.points),
},
});
const { success, user } = await createRequest('get', `/users/${username}`);
if (!success) return res.status(500).send(config.errorGeneric);
const newUser = await User.create({ ...user, id: user.user_id });
const newUser = await User.findByIdAndUpdate(user.user_id, user, {
upsert: true,
new: true,
});
return res.send({
user: {
username: newUser.username,
email: newUser.email,
points: newUser.points,
level: getUserLevel(newUser.points),
}
},
});
}

View file

@ -12,7 +12,7 @@ async function Register(req, res) {
const { salt, key } = await deriveKeyFromPassword(password);
const { success, error } = await createRequest('post', '/users', {
const { success, error, user } = await createRequest('post', '/users', {
username,
email,
salt,
@ -29,7 +29,7 @@ async function Register(req, res) {
return res.status(500).send(config.errorGeneric);
}
await User.create({ username, email, salt });
await User.create(user);
return res.sendStatus(201);
}

View file

@ -11,5 +11,5 @@ async function refreshUsers(_job, done) {
module.exports = {
refreshQuestions,
refreshUsers
}
refreshUsers,
};

View file

@ -12,38 +12,75 @@ async function getAllData(Model, url, id, dataName) {
...item,
id: item[id],
}));
for (const item of data)
await Model.findByIdAndUpdate(item[id], item, { upsert: true });
return data;
}
async function getAllAnswers({ question_id, username }) {
async function getAllAnswers(question_id, username) {
if (question_id)
return getAllData(Answer, `/questions/${question_id}/answers`, 'answer_id', 'answers')
return getAllData(
Answer,
`/questions/${question_id}/answers`,
'answer_id',
'answers'
);
else if (username)
return getAllData(Answer, `/users/${username}/answers`, 'answer_id', 'answers')
return getAllData(
Answer,
`/users/${username}/answers`,
'answer_id',
'answers'
);
}
async function getAllComments({ answer_id, question_id }) {
async function getAllComments(answer_id, question_id) {
if (answer_id)
return getAllData(Comment, `/questions/${question_id}/answers/${answer_id}/comments`, 'comment_id', 'comments')
return getAllData(
Comment,
`/questions/${question_id}/answers/${answer_id}/comments`,
'comment_id',
'comments'
);
else
return getAllData(Comment, `/questions/${question_id}/comments`, 'comment_id', 'comments')
return getAllData(
Comment,
`/questions/${question_id}/comments`,
'comment_id',
'comments'
);
}
async function getAllMail({ username }) {
return getAllData(Mail, `/mail/${username}`, 'mail_id', 'messages')
async function getAllMail({ question_id }) {
return getAllData(
Mail,
`/questions/${question_id}/comments`,
'mail_id',
'mail'
);
}
async function getAllQuestions({ username }) {
async function getAllQuestions(username) {
if (username)
return getAllData(Question, `/users/${username}/questions`, 'question_id', 'questions')
else
return getAllData(Question, `/questions/search`, 'question_id', 'questions')
return getAllData(
Question,
`/users/${username}/questions`,
'question_id',
'questions'
);
else {
return getAllData(
Question,
`/questions/search`,
'question_id',
'questions'
);
}
}
async function getAllUsers() {
return getAllData(User, `/users`, 'user_id', 'users')
return getAllData(User, `/users`, 'user_id', 'users');
}
module.exports = {
@ -51,5 +88,6 @@ module.exports = {
getAllComments,
getAllMail,
getAllQuestions,
getAllUsers
getAllUsers,
getAllMail,
};

View file

@ -4,8 +4,12 @@ const Answer = require('server/db/models/Answer');
async function getAnswer(question_id, answer_id) {
let cachedAnswer;
try { cachedAnswer = await Answer.findById(answer_id); }
catch { return; }
try {
cachedAnswer = await Answer.findById(answer_id);
} catch {
return;
}
// Retrieve uncached answer and patch to cache
if (!cachedAnswer) {
@ -14,7 +18,14 @@ async function getAnswer(question_id, answer_id) {
`/questions/${question_id}/answers/${answer_id}`
);
if (!success) return;
return Answer.create({ ...answer, id: answer.answer_id, question_id });
return Answer.findByIdAndUpdate(
answer.answer_id,
{ ...answer, id: answer.answer_id },
{
upsert: true,
new: true,
}
);
} else return cachedAnswer;
}

View file

@ -4,8 +4,11 @@ const Question = require('server/db/models/Question');
async function getQuestion(question_id) {
let cachedQuestion;
try { cachedQuestion = await Question.findById(question_id); }
catch { return; }
try {
cachedQuestion = await Question.findById(question_id);
} catch {
return;
}
// Retrieve uncached question and patch to cache
if (!cachedQuestion) {
@ -13,8 +16,17 @@ async function getQuestion(question_id) {
'get',
`/questions/${question_id}`
);
if (!success) return;
return Question.create({ ...question, id: question.question_id });
return Question.findByIdAndUpdate(
question.question_id,
{ ...question, id: question.question_id },
{
upsert: true,
new: true,
}
);
} else return cachedQuestion;
}

View file

@ -1,11 +1,9 @@
const getAnswer = require('./getAnswer');
const getComment = require('./getComment');
const getQuestion = require('./getQuestion');
const refreshQuestion = require('./refreshQuestion');
module.exports = {
getAnswer,
getComment,
getQuestion,
refreshQuestion
};
refreshQuestion,
};

View file

@ -2,7 +2,10 @@ const Question = require('server/db/models/Question');
const createRequest = require('server/utils/api');
async function refreshQuestion(question_id) {
const { question, success } = await createRequest('get', `/questions/${question_id}`);
const { question, success } = await createRequest(
'get',
`/questions/${question_id}`
);
if (!success) return false;
// Refresh cache
@ -10,7 +13,10 @@ async function refreshQuestion(question_id) {
...question,
id: question.question_id,
};
return Question.findByIdAndUpdate(question.id, newQuestion, { upsert: true });
return Question.findByIdAndUpdate(question.id, newQuestion, {
upsert: true,
});
}
module.exports = refreshQuestion;