diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c1d5222 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +.DS_Store +node_modules +/dist + +# local env files +.env.local +.env.*.local +.env + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/README.md b/README.md index 94a19de..b36cbc7 100644 --- a/README.md +++ b/README.md @@ -1 +1,31 @@ -# hacktivoverflow \ No newline at end of file +# hacktivoverflow + + +# User Routes + +Routes| Method | Request Body | Response Description | Response Success | Response Error +------|--------|--------------|----------------------|--------------|--------------- +/users/register | POST | {
  "firstName": String (**required**),
  "lastName": String (**required**),
  "username": String (**required**),
  "email": String (**required**),
  "password": String (**required**)
} | Registers a new user| res status: 201, *user created* | res.status: 400, *Email has already been registered* +/users/login | POST | `{
  "email": String (**required**),
  "password": String (**required**)
} | login with an existing user*| res status: 200,
{
  "accessToken": (token value)
} | res.status: 400, *Username/password incorrect +/users/decode | POST | {
  "access_token" : access_token
} | decodes jsonwebtoken to retrieve user data | {
  "id": user._id,
  "firstName": user.firstName,
  "lastName": user.lastName,
  "email": user.email
} | res.status: 404, *resource not found* + +# Question Routes + +Routes| Method | Request Body | Response Description | Response Success | Response Error +------|--------|--------------|----------------------|--------------|--------------- +/questions/all | GET | none | Retrieve array of all posted questions | res.status: 200, [
  {
  "_id": question._id,
  "question": question title,
  "description": question.description,
  "user" : ObjectId user,
  "upvotes": [ ObjectId upvotes ],
  "downvotes": [ ObjectId downvotes],
  "answers": [ Object Id answers ]
  }
] | res.status: 500, *internal server error* +/questions | GET | none | Retrieve array of all posted questions based on logged in user | res.status: 200,
[
  {
  "_id": question._id,
  "question": question title,
  "description": question.description,
  "user" : ObjectId user,
  "upvotes": [ ObjectId upvotes ],
  "downvotes": [ ObjectId downvotes],
  "answers": [ Object Id answers ]
  }
] | res.status: 401, *Unauthorized* +/questions/:id | GET | none | Retrieve question object based on id | res.status: 200,
{
  "_id": question._id,
  "question": question title,
  "description": question.description,
  "user" : ObjectId user,
  "upvotes": [ ObjectId upvotes ],
  "downvotes": [ ObjectId downvotes],
  "answers": [ Object Id answers ]
  } | res.status: 404, *Resource not found* +/questions/:id | PATCH | {
  "question": String (**required**),
  "description": String (**required**)
} | Edits an existing question | res.status: 200,
{
  "_id": question._id,
  "question": question title,
  "description": question.description,
  "user" : ObjectId user,
  "upvotes": [ ObjectId upvotes ],
  "downvotes": [ ObjectId downvotes],
  "answers": [ Object Id answers ]
  } | res.status: 401, *Unauthorized*.
res.status: 400, *Resource not found* +/questions/:id | DELETE | none | Deletes an existing question based on its id | res.status: 200, *question deleted* | res.status: 401, *Unauthorized*,
res.status: 400: *Resource not found* +/questions | POST | {
  "question": String (**required**),
  "description" (**required**)
} | Submits a question | res.status: 201,
{
  "_id": question._id,
  "question": question title,
  "description": question.description,
  "user" : ObjectId user,
  "upvotes": [ ],
  "downvotes": [ ],
  "answers": []
  } | res.status: 401, *Unauthorized*
res.status: 400, *missing required parameteres* +/questions/vote/:id | PATCH | Upvotes or downvotes question based on client feedback | res.status: 200, *question upvote/downvote arrays updated* | res.status: 401, *Unauthorized*,
res.status: 400, *Resource not found* + +# Answer routes + +Routes| Method | Request Body | Response Description | Response Success | Response Error +------|--------|--------------|----------------------|--------------|--------------- +/answers | POST | {
  "title": String (**required**),
  "description": String (**required**)
} | res.status: 201, {
  "_id": ObjectId,
  "title": answer.title,
  "description": answer.description,
  "upvotes": [ ObjectId users ],
  "downvotes": [ ObjectId users ]
} | res.status: 401, *Unauthorized* +/answers/:id | PATCH | {
  "title": String (**required**),
  "description": String (**required**)
} | Edits an existing answer | {
  "_id": ObjectId,
  "title": answer.title,
  "description": answer.description,
  "upvotes": [ ObjectId users ],
  "downvotes": [ ObjectId users ]
} | res.status: 401, *Unauthorized*,
res.status: 400, *Missing required parameters* +/answers/:id | DELETE | none | Deletes an existing answer based on its id | res.status: 200, *answer deleted successfully* | res.status: 401, *Unauthorized*, res.status: 400, *Resource not found* +/answers/vote/:id | PATCH | {
  "title": String (**required**),
  "description": String (**required**)
} | Upvotes or downvotes an answer based on client feedback | res.status: 200, *answer upvote/downvote arrays updated* | res.status: 401, *Unauthorized*, res.status: 400, *Resource not found* \ No newline at end of file diff --git a/client/.browserslistrc b/client/.browserslistrc new file mode 100644 index 0000000..d6471a3 --- /dev/null +++ b/client/.browserslistrc @@ -0,0 +1,2 @@ +> 1% +last 2 versions diff --git a/client/.editorconfig b/client/.editorconfig new file mode 100644 index 0000000..7053c49 --- /dev/null +++ b/client/.editorconfig @@ -0,0 +1,5 @@ +[*.{js,jsx,ts,tsx,vue}] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/client/.eslintrc.js b/client/.eslintrc.js new file mode 100644 index 0000000..98d0431 --- /dev/null +++ b/client/.eslintrc.js @@ -0,0 +1,17 @@ +module.exports = { + root: true, + env: { + node: true + }, + 'extends': [ + 'plugin:vue/essential', + '@vue/standard' + ], + rules: { + 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', + 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off' + }, + parserOptions: { + parser: 'babel-eslint' + } +} diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..0297c85 --- /dev/null +++ b/client/README.md @@ -0,0 +1,29 @@ +# client + +## Project setup +``` +npm install +``` + +### Compiles and hot-reloads for development +``` +npm run serve +``` + +### Compiles and minifies for production +``` +npm run build +``` + +### Run your tests +``` +npm run test +``` + +### Lints and fixes files +``` +npm run lint +``` + +### Customize configuration +See [Configuration Reference](https://cli.vuejs.org/config/). diff --git a/client/babel.config.js b/client/babel.config.js new file mode 100644 index 0000000..ba17966 --- /dev/null +++ b/client/babel.config.js @@ -0,0 +1,5 @@ +module.exports = { + presets: [ + '@vue/app' + ] +} diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..4e36963 --- /dev/null +++ b/client/package.json @@ -0,0 +1,32 @@ +{ + "name": "client", + "version": "0.1.0", + "private": true, + "scripts": { + "serve": "vue-cli-service serve", + "build": "vue-cli-service build", + "lint": "vue-cli-service lint" + }, + "dependencies": { + "@ckeditor/ckeditor5-build-classic": "^12.2.0", + "@ckeditor/ckeditor5-vue": "^1.0.0-beta.2", + "axios": "^0.19.0", + "core-js": "^2.6.5", + "moment": "^2.24.0", + "vue": "^2.6.10", + "vue-router": "^3.0.3", + "vuelidate": "^0.7.4", + "vuetify": "^1.5.16", + "vuex": "^3.0.1" + }, + "devDependencies": { + "@vue/cli-plugin-babel": "^3.8.0", + "@vue/cli-plugin-eslint": "^3.8.0", + "@vue/cli-service": "^3.8.0", + "@vue/eslint-config-standard": "^4.0.0", + "babel-eslint": "^10.0.1", + "eslint": "^5.16.0", + "eslint-plugin-vue": "^5.0.0", + "vue-template-compiler": "^2.6.10" + } +} diff --git a/client/postcss.config.js b/client/postcss.config.js new file mode 100644 index 0000000..961986e --- /dev/null +++ b/client/postcss.config.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: { + autoprefixer: {} + } +} diff --git a/client/public/favicon.ico b/client/public/favicon.ico new file mode 100644 index 0000000..df36fcf Binary files /dev/null and b/client/public/favicon.ico differ diff --git a/client/public/hacktiv8.png b/client/public/hacktiv8.png new file mode 100644 index 0000000..27a0b95 Binary files /dev/null and b/client/public/hacktiv8.png differ diff --git a/client/public/index.html b/client/public/index.html new file mode 100644 index 0000000..f3f9fb4 --- /dev/null +++ b/client/public/index.html @@ -0,0 +1,18 @@ + + + + + + + + + HacktivOverflow + + + +
+ + + diff --git a/client/src/App.vue b/client/src/App.vue new file mode 100644 index 0000000..3428675 --- /dev/null +++ b/client/src/App.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/client/src/components/answers.vue b/client/src/components/answers.vue new file mode 100644 index 0000000..a94e3b7 --- /dev/null +++ b/client/src/components/answers.vue @@ -0,0 +1,195 @@ + + + + + diff --git a/client/src/components/editanswer.vue b/client/src/components/editanswer.vue new file mode 100644 index 0000000..04d72ea --- /dev/null +++ b/client/src/components/editanswer.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/client/src/components/editquestion.vue b/client/src/components/editquestion.vue new file mode 100644 index 0000000..1631341 --- /dev/null +++ b/client/src/components/editquestion.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/client/src/components/navbar.vue b/client/src/components/navbar.vue new file mode 100644 index 0000000..28df9d0 --- /dev/null +++ b/client/src/components/navbar.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/client/src/components/postdetail.vue b/client/src/components/postdetail.vue new file mode 100644 index 0000000..c4d284e --- /dev/null +++ b/client/src/components/postdetail.vue @@ -0,0 +1,194 @@ + + + + + diff --git a/client/src/components/posts.vue b/client/src/components/posts.vue new file mode 100644 index 0000000..797f359 --- /dev/null +++ b/client/src/components/posts.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/client/src/components/postsuccess.vue b/client/src/components/postsuccess.vue new file mode 100644 index 0000000..2a5e6d2 --- /dev/null +++ b/client/src/components/postsuccess.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/client/src/components/profile.vue b/client/src/components/profile.vue new file mode 100644 index 0000000..bf1739d --- /dev/null +++ b/client/src/components/profile.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/client/src/components/signin.vue b/client/src/components/signin.vue new file mode 100644 index 0000000..66216c8 --- /dev/null +++ b/client/src/components/signin.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/client/src/components/signinsuccess.vue b/client/src/components/signinsuccess.vue new file mode 100644 index 0000000..5f8dd33 --- /dev/null +++ b/client/src/components/signinsuccess.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/client/src/components/signinwarning.vue b/client/src/components/signinwarning.vue new file mode 100644 index 0000000..cb9e8f5 --- /dev/null +++ b/client/src/components/signinwarning.vue @@ -0,0 +1,47 @@ + + + + + diff --git a/client/src/components/signup.vue b/client/src/components/signup.vue new file mode 100644 index 0000000..726758b --- /dev/null +++ b/client/src/components/signup.vue @@ -0,0 +1,182 @@ + + + + + diff --git a/client/src/main.js b/client/src/main.js new file mode 100644 index 0000000..c1e0c2d --- /dev/null +++ b/client/src/main.js @@ -0,0 +1,23 @@ +import Vue from 'vue' +import App from './App.vue' +import router from './router' +import store from './store' +import Vuetify from 'vuetify' +import 'vuetify/dist/vuetify.min.css' +import Vuelidate from 'vuelidate' +import CKEditor from '@ckeditor/ckeditor5-vue' + +Vue.use(Vuelidate) +Vue.use(Vuetify, { + options: { + customProperties: true + } +}) +Vue.use(CKEditor) +Vue.config.productionTip = false + +new Vue({ + router, + store, + render: h => h(App) +}).$mount('#app') diff --git a/client/src/router.js b/client/src/router.js new file mode 100644 index 0000000..428ddeb --- /dev/null +++ b/client/src/router.js @@ -0,0 +1,41 @@ +import Vue from 'vue' +import Router from 'vue-router' +import SignIn from '@/components/signin.vue' +import Home from '@/views/home.vue' +import SignUp from '@/components/signup.vue' +import AskQuestion from '@/views/askquestion.vue' +import ViewPost from '@/views/viewpost.vue' + +Vue.use(Router) + +export default new Router({ + mode: 'history', + base: process.env.BASE_URL, + routes: [ + { + path: '/', + name: 'Home', + component: Home + }, + { + path: '/signin', + name: 'SignIn', + component: SignIn + }, + { + path: '/signup', + name: 'SignUp', + component: SignUp + }, + { + path: '/askquestion', + name: 'AskQuestion', + component: AskQuestion + }, + { + path: '/post/:id', + name: 'ViewPost', + component: ViewPost + } + ] +}) diff --git a/client/src/store.js b/client/src/store.js new file mode 100644 index 0000000..1a76539 --- /dev/null +++ b/client/src/store.js @@ -0,0 +1,130 @@ +import Vue from 'vue' +import Vuex from 'vuex' +import axios from 'axios' + +Vue.use(Vuex) + +export default new Vuex.Store({ + state: { + baseURL: 'http://localhost:3000', + isLoggedIn: false, + loggedUser: {}, + publicQuestions: [], + myQuestions: [] + }, + getters: { + + }, + mutations: { + SET_LOGIN (state, user) { + state.isLoggedIn = true + state.loggedUser = { + id: user.id, + firstName: user.firstName, + lastName: user.lastName + } + }, + + SET_LOGOUT (state, user) { + state.isLoggedIn = false + state.loggedUser = {} + }, + + SET_PUBLICQUESTIONS (state, questions) { + state.publicQuestions = questions + }, + + SET_MYQUESTIONS (state, questions) { + state.myQuestions = questions + } + }, + actions: { + decodeToken (context) { + axios({ + method: 'POST', + url: `${this.state.baseURL}/users/decode`, + data: { + access_token: localStorage.access_token + } + }) + .then(({ data }) => { + context.commit('SET_LOGIN', data) + }) + .catch(({ response }) => { + console.log(response) + }) + }, + + getPublicQuestions (context) { + axios({ + method: 'GET', + url: `${this.state.baseURL}/questions/all` + }) + .then(({ data }) => { + context.commit('SET_PUBLICQUESTIONS', data) + }) + .catch(({ response }) => { + console.log(response) + }) + }, + + getMyQuestions (context) { + axios({ + method: 'GET', + url: `${this.state.baseURL}/questions`, + headers: { + access_token: localStorage.access_token + } + }) + .then(({ data }) => { + context.commit('SET_MYQUESTIONS', data) + }) + .catch(({ response }) => { + console.log(response) + }) + }, + + voteQuestion (context, question) { + axios({ + method: 'PATCH', + url: `${this.state.baseURL}/questions/vote/${question._id}`, + data: { + upvotes: question.upvotes, + downvotes: question.downvotes + }, + headers: { + access_token: localStorage.access_token + } + }) + .then(({ data }) => { + context.dispatch('getPublicQuestions') + }) + .catch(({ response }) => { + console.log(response) + }) + }, + + voteAnswer (context, answer) { + console.log('MASUK JURAGAN') + axios({ + method: 'PATCH', + url: `${this.state.baseURL}/answers/vote/${answer._id}`, + data: { + upvotes: answer.upvotes, + downvotes: answer.downvotes + }, + headers: { + access_token: localStorage.access_token + } + }) + .then(({ data }) => { + console.log(data) + // context.dispatch('getPublicanswers') + }) + .catch(({ response }) => { + console.log(response) + }) + } + + } +}) diff --git a/client/src/views/askquestion.vue b/client/src/views/askquestion.vue new file mode 100644 index 0000000..3f7364b --- /dev/null +++ b/client/src/views/askquestion.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/client/src/views/home.vue b/client/src/views/home.vue new file mode 100644 index 0000000..112ee39 --- /dev/null +++ b/client/src/views/home.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/client/src/views/viewpost.vue b/client/src/views/viewpost.vue new file mode 100644 index 0000000..44e3fee --- /dev/null +++ b/client/src/views/viewpost.vue @@ -0,0 +1,215 @@ + + + + + diff --git a/server/app.js b/server/app.js new file mode 100644 index 0000000..5a5c1b1 --- /dev/null +++ b/server/app.js @@ -0,0 +1,43 @@ +const express = require('express'); +const app = express(); +const routes = require('./routes'); +const port = 3000; +const cors = require('cors'); + +if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') { + require('dotenv').config() +} + +app.use(express.urlencoded({extended: false})); +app.use(express.json()); + +app.use(cors()); +app.use('/', routes) + +const mongoose = require('mongoose'); +const url = 'mongodb://localhost:27017/hacktivoverflow2' +mongoose.connect(url, {useNewUrlParser: true}, (err) => { + if(err) { + console.log(err) + } + else { + console.log('mongoose connected') + } +}) + +app.use(function(err,req,res,next){ + console.log(err) + if(err.code === 404) { + res.status(404).json({ message: 'Resource not found' }) + } else if(err.name === 'ValidationError') { + res.status(500).json({ message: err.message }) + } else { + const message = err.message + const status = err.code || 500 + res.status(status).json({ message: message }) + } +}); + +app.listen(port, () => console.log(`listening on port ${port}`)) + +//install axios, bcrypt, cors, dotenv, express, jwt, mongoose \ No newline at end of file diff --git a/server/controllers/answercontroller.js b/server/controllers/answercontroller.js new file mode 100644 index 0000000..b57cb66 --- /dev/null +++ b/server/controllers/answercontroller.js @@ -0,0 +1,93 @@ +const Answer = require('../models/answer') +const Question = require('../models/question') +const User = require('../models/user') + +class AnswerController { + static getAnswers(req,res,next) { + Answer.find({ + // question: + }) + } + + static addAnswer(req,res,next) { + const { title, description, questionId } = req.body + + let answerId; + Answer.create({ + user: req.decode.id, + title: title, + description: description, + question: questionId + }) + .then(answer => { + answerId = answer._id + // res.status(201).json(answer) + return Question.findOne({ _id: questionId }) + }) + .then(question => { + question.answers.push(answerId) + return question.save() + }) + .then(edited => { + return Answer.populate(edited, { path: 'answers', options: { sort: { createdAt: -1 } }, populate: { path: 'user'}}) + }) + .then(edited2 => { + return User.populate(edited2, { path: 'user'}) + }) + .then(pop => { + // pop.answers.sort({ "createdAt": -1 }) + res.status(200).json(pop) + }) + .catch(next) + } + + static editAnswer(req,res,next) { + + const { title, description } = req.body + + Answer.findOne({ + _id: req.params.id + }) + .then(answer => { + answer.title = title + answer.description = description + return answer.save() + }) + .then(edited => { + res.status(200).json(edited) + }) + .catch(next) + } + + static voteAnswer(req, res, next) { + // console.log(req.body) + Answer.findOne({ + _id: req.params.id + }) + .then(answer => { + answer.upvotes = req.body.upvotes + answer.downvotes = req.body.downvotes + return answer.save() + }) + .then(edited => { + res.status(200).json(edited) + }) + .catch(err => { + console.log(err.message) + }) + } + + static deleteAnswer(req,res,next) { + // console.log('masukkk')? + // console.log(req.params.id) + Answer.deleteOne({ + _id: req.params.id + }) + .then(deleted => { + res.status(200).json(deleted) + }) + .catch(next) + } +} + +module.exports = AnswerController \ No newline at end of file diff --git a/server/controllers/questioncontroller.js b/server/controllers/questioncontroller.js new file mode 100644 index 0000000..ab88f81 --- /dev/null +++ b/server/controllers/questioncontroller.js @@ -0,0 +1,107 @@ +const Question = require('../models/question') +const Answer = require('../models/answer') + +class QuestionController { + static getQuestion(req,res,next) { + Question.findOne({ + _id: req.params.id + }) + .populate({ + path: 'answers', + options: { sort: {"createdAt" : -1}}, + populate: { + path: 'user' + } + }) + .populate('user') + .then(question => { + res.status(200).json(question) + }) + .catch(next) + } + + static getPublicQuestions(req,res,next) { + Question.find() + .populate('answers') + .populate('user') + .sort({'createdAt': -1}) + .then(questions => { + res.status(200).json(questions) + }) + .catch(next) + } + + static getMyQuestions(req,res,next) { + Question.find({ + user: req.decode.id + }) + .populate('answers') + .populate('user') + .sort({'createdAt': -1}) + .then(questions => { + res.status(200).json(questions) + }) + .catch(next) + + } + static voteQuestion(req,res,next) { + Question.findOne({ + _id: req.params.id + }) + .then(question => { + question.upvotes = req.body.upvotes + question.downvotes = req.body.downvotes + return question.save() + }) + .then(edited => { + res.status(200).json(edited) + }) + .catch(next) + } + + static addQuestion(req,res,next) { + const { question, description } = req.body + + Question.create({ + user: req.decode.id, + question: question, + description: description, + }) + .then(question => { + res.status(201).json(question) + }) + .catch(next) + } + + static editQuestion(req,res,next) { + const { title, description } = req.body + + Question.findOne({ + _id: req.params.id + }) + .then(question => { + question.question = title + question.description = description + return question.save() + }) + .then(edited => { + res.status(200).json(edited) + }) + .catch(next) + } + + static deleteQuestion(req,res,next) { + + Question.deleteOne({ + _id: req.params.id + }) + .then(deleted => { + res.status(200).json(deleted) + }) + .catch(next) + } + + +} + +module.exports = QuestionController \ No newline at end of file diff --git a/server/controllers/usercontroller.js b/server/controllers/usercontroller.js new file mode 100644 index 0000000..ce2d33d --- /dev/null +++ b/server/controllers/usercontroller.js @@ -0,0 +1,56 @@ +const User = require('../models/user') +const { verifyPassword } = require('../helpers/bcrypt') +const { generateToken } = require('../helpers/jwt') +const { verifyToken } = require('../helpers/jwt') + +class UserController { + static register(req, res, next) { + const { firstName, lastName, username, email, password } = req.body + const input = { firstName, lastName, username, email, password } + User.create(input) + .then(newUser => { + res.status(201).json(newUser) + }) + .catch(next) + } + + static decodeToken(req,res,next) { + const decode = verifyToken(req.body.access_token) + + res.status(200).json(decode) + } + + static login(req, res, next) { + User.findOne({ + email: req.body.email + }) + .then(user => { + if (user) { + if (verifyPassword(req.body.password, user.password)) { + const payload = { + firstName: user.firstName, + lastName: user.lastName, + username: user.username, + email: user.email, + id: user._id + } + const token = generateToken(payload) + res.status(200).json({ + firstName: user.firstName, + lastName: user.lastName, + access_token: token + }) + } + else { + next({ code: 400, message: 'username/password invalid'}) + } + } + else { + next({ code: 400, message: 'username/password invalid' }) + } + }) + .catch(next) + } +} + +module.exports = UserController \ No newline at end of file diff --git a/server/helpers/bcrypt.js b/server/helpers/bcrypt.js new file mode 100644 index 0000000..cff7c9a --- /dev/null +++ b/server/helpers/bcrypt.js @@ -0,0 +1,12 @@ +const bcrypt = require('bcrypt') +const salt = bcrypt.genSaltSync(10) + +const hashPassword = (input) => { + return bcrypt.hashSync(input, salt) +} + +const verifyPassword = (input, password) => { + return bcrypt.compareSync(input, password) +} + +module.exports = {hashPassword, verifyPassword} \ No newline at end of file diff --git a/server/helpers/jwt.js b/server/helpers/jwt.js new file mode 100644 index 0000000..8611dd8 --- /dev/null +++ b/server/helpers/jwt.js @@ -0,0 +1,10 @@ +const jwt = require('jsonwebtoken') + +module.exports = { + generateToken(payload) { + return jwt.sign(payload, process.env.JWT_SECRET) + }, + verifyToken(token) { + return jwt.verify(token, process.env.JWT_SECRET) +} +} \ No newline at end of file diff --git a/server/middlewares/authentication.js b/server/middlewares/authentication.js new file mode 100644 index 0000000..5828a70 --- /dev/null +++ b/server/middlewares/authentication.js @@ -0,0 +1,19 @@ +const {verifyToken} = require('../helpers/jwt') + +module.exports = { + authentication(req,res,next) { + if(req.headers.hasOwnProperty('access_token')) { + try { + const decode = verifyToken(req.headers.access_token) + req.decode = decode + next() + } + catch(err) { + next({status: 401, message: "unauthorized"}) + } + } + else{ + next({status: 401, message: "unauthorized"}) + } + } +} \ No newline at end of file diff --git a/server/middlewares/authorization.js b/server/middlewares/authorization.js new file mode 100644 index 0000000..0176ec3 --- /dev/null +++ b/server/middlewares/authorization.js @@ -0,0 +1,59 @@ +const Question = require('../models/question') +const Answer = require('../models/answer') + +module.exports = { + questionAuthorization(req,res,next) { + Question.findOne({ + _id: req.params.id + }) + .then(question => { + if(question) { + const {id} = req.decode + let strObj = question.user + '' + + if(strObj === id) { + next() + } + else { + next({status: 401, message: "unauthorized"}) + } + } + else { + next({code: 404}) + } + }) + .catch(next) + }, + + answerAuthorization(req,res,next){ + Answer.findOne({ + _id: req.params.id + }) + .then(answer => { + if(answer) { + const {id} = req.decode + let strObj = answer.user + '' + + if(strObj === id) { + next() + } + else{ + next({status: 401, message: "unauthorized"}) + } + } + else{ + next({code: 404}) + } + }) + .catch(next) + }, + + adminAuthorization(req,res,next) { + if(req.decode.email === "admin@admin.com") { + next() + } + else{ + next({code: 401, message: "unauthorized"}) + } + } +} \ No newline at end of file diff --git a/server/models/answer.js b/server/models/answer.js new file mode 100644 index 0000000..f809600 --- /dev/null +++ b/server/models/answer.js @@ -0,0 +1,27 @@ +const mongoose = require('mongoose') +const Schema = mongoose.Schema + +const AnswerSchema = new Schema({ + user: { + type: Schema.Types.ObjectId, + ref: 'User' + }, + title: { + type: String, + required: [true, 'Answer titel is required'] + }, + description: { + type: String, + required: [true, 'Answer description is required'] + }, + upvotes: [ { type: Schema.Types.ObjectId, ref: 'User'} ], + downvotes: [{ type: Schema.Types.ObjectId, ref: 'User' }], + question: { + type: Schema.Types.ObjectId, + ref: 'Question' + } +}, { timestamps: true }) + +const Answer = mongoose.model('Answer', AnswerSchema) + +module.exports = Answer \ No newline at end of file diff --git a/server/models/question.js b/server/models/question.js new file mode 100644 index 0000000..953cae2 --- /dev/null +++ b/server/models/question.js @@ -0,0 +1,25 @@ +const mongoose = require('mongoose') +const Schema = mongoose.Schema + +const QuestionSchema = new Schema({ + user: { + type: Schema.Types.ObjectId, + ref: 'User' + }, + question: { + type: String, + required: [true, 'Question is required'] + }, + description: { + type: String, + require: [true, 'Description is required'] + }, + upvotes: [{ type: Schema.Types.ObjectId, ref: 'User' }], + downvotes: [{ type: Schema.Types.ObjectId, ref: 'User' }], + answers: [{ type: Schema.Types.ObjectId, ref: 'Answer' }] + // comments: [{ type: Schema.Types.ObjectId, ref: 'Comment'}] +}, { timestamps: true }) + +const Question = mongoose.model('Question', QuestionSchema) + +module.exports = Question \ No newline at end of file diff --git a/server/models/user.js b/server/models/user.js new file mode 100644 index 0000000..06a9721 --- /dev/null +++ b/server/models/user.js @@ -0,0 +1,70 @@ +const mongoose = require('mongoose') +const Schema = mongoose.Schema +const { hashPassword } = require('../helpers/bcrypt') + +let uniqueCheck = function(value) { + return User.findOne({ + email: value + }) + .then(user => { + if (user) { + return false + } + }) +} + +let formatCheck = function(value) { + let re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + return re.test(value); +} + +const UserSchema = new Schema({ + firstName : { + type: String, + required: [true, 'first name is required'] + }, + lastName : { + type: String, + required: [true, 'last name is required'] + }, + username: { + type: String, + required: [true, 'Username is required'], + validate: { + validator: function(value) { + User.findOne({ + username : value + }) + .then(user => { + if(user) { + return false + } + }) + }, + message: props => `Username is already registered` + } + }, + email: { + type: String, + required: [true, 'email is required'], + validate: [ + { validator: uniqueCheck, msg: 'Email is already registered' }, + { validator: formatCheck, msg: 'Incorrect email format'} + ], + }, + password: { + type: String, + required: [true, 'password is required'] + } +}) + +UserSchema.pre('save', function (next) { + let hash = hashPassword(this.password) + this.password = hash + next() +}) + + +const User = mongoose.model('User', UserSchema) + +module.exports = User \ No newline at end of file diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..880205f --- /dev/null +++ b/server/package.json @@ -0,0 +1,30 @@ +{ + "name": "hacktivOverflow-server", + "version": "1.0.0", + "description": "", + "main": "app.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "dev": "NODE_ENV=development nodemon app.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/shandiyuwono/hacktivOverflow-server.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/shandiyuwono/hacktivOverflow-server/issues" + }, + "homepage": "https://github.com/shandiyuwono/hacktivOverflow-server#readme", + "dependencies": { + "axios": "^0.19.0", + "bcrypt": "^3.0.6", + "cors": "^2.8.5", + "dotenv": "^8.0.0", + "express": "^4.17.1", + "jsonwebtoken": "^8.5.1", + "mongoose": "^5.6.2" + } +} diff --git a/server/routes/answerrouter.js b/server/routes/answerrouter.js new file mode 100644 index 0000000..41893ec --- /dev/null +++ b/server/routes/answerrouter.js @@ -0,0 +1,12 @@ +const router = require('express').Router() +const AnswerController = require('../controllers/answercontroller') +const { authentication } = require('../middlewares/authentication') +const { answerAuthorization } = require('../middlewares/authorization') + +router.use(authentication) +router.post('/', AnswerController.addAnswer) +router.patch('/vote/:id', AnswerController.voteAnswer) +router.patch('/:id', answerAuthorization, AnswerController.editAnswer) +router.delete('/:id', answerAuthorization, AnswerController.deleteAnswer) + +module.exports = router \ No newline at end of file diff --git a/server/routes/index.js b/server/routes/index.js new file mode 100644 index 0000000..ea223ab --- /dev/null +++ b/server/routes/index.js @@ -0,0 +1,10 @@ +const router = require('express').Router() +const userRouter = require('./userrouter') +const questionRouter = require('./questionrouter') +const answerRouter = require('./answerrouter') + +router.use('/users', userRouter) +router.use('/questions', questionRouter) +router.use('/answers', answerRouter) + +module.exports = router \ No newline at end of file diff --git a/server/routes/questionrouter.js b/server/routes/questionrouter.js new file mode 100644 index 0000000..8d92ad9 --- /dev/null +++ b/server/routes/questionrouter.js @@ -0,0 +1,17 @@ +const router = require('express').Router() +const QuestionController = require('../controllers/questioncontroller') +const { questionAuthorization } = require('../middlewares/authorization') +const { authentication } = require('../middlewares/authentication') + +router.get('/all', QuestionController.getPublicQuestions) +router.get('/:id', QuestionController.getQuestion ) + +router.use(authentication) +router.post('/', QuestionController.addQuestion) +router.get('/', QuestionController.getMyQuestions) + +router.patch('/:id', questionAuthorization, QuestionController.editQuestion) +router.delete('/:id', questionAuthorization, QuestionController.deleteQuestion) +router.patch('/vote/:id', QuestionController.voteQuestion) + +module.exports = router \ No newline at end of file diff --git a/server/routes/userrouter.js b/server/routes/userrouter.js new file mode 100644 index 0000000..fde1068 --- /dev/null +++ b/server/routes/userrouter.js @@ -0,0 +1,11 @@ +const router = require('express').Router() +const UserController = require('../controllers/usercontroller') + + + +router.post('/signup', UserController.register) +router.post('/signin', UserController.login) +router.post('/decode', UserController.decodeToken) + + +module.exports = router \ No newline at end of file