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 @@
+
+
+
+
+
+ keyboard_arrow_up
+ {{ answer.upvotes.length - answer.downvotes.length}}
+ keyboard_arrow_down
+
+
+
+
+
+ {{ answer.title }}
+
+
+
+
+
+ Answered {{ getTime }} by {{ answer.user.username }}
+
+
+ You must login before voting.
+
+ edit
+ delete
+
+
+
+
+
+
+
+
+ Are you sure you want to delete this answer?
+
+
+ Once deleted your answer cannot be recovered.
+
+
+
+
+
+
+ Nevermind
+
+
+
+ Yes, i'm sure
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Nevermind
+
+
+
+ Edit
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+ Nevermind
+
+
+
+ Edit
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+ Have something in mind?
+
+
+
+
+
+ add
+
+
+
+ Ask a question
+
+
+
+
+ You must log in to use this feature.
+
+
+
+
+
+
+ {{ item.icon }}
+
+
+
+ {{ item.text }}
+
+
+
+
+
+
+
+ keyboard_return
+
+
+
+ Sign out
+
+
+
+
+
+
+
+ Hacktiv Overflow
+
+
+ Welcome {{ $store.state.loggedUser.firstName[0].toUpperCase() + $store.state.loggedUser.firstName.slice(1)}} {{ $store.state.loggedUser.lastName[0].toUpperCase() + $store.state.loggedUser.lastName.slice(1)}}
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+ keyboard_arrow_up
+ {{ question.upvotes.length - question.downvotes.length }}
+ keyboard_arrow_down
+
+
+
+
+
+ {{ question.question }}
+
+
+ edit
+ delete
+
+
+
+
+ {{question.answers.length}} Answers
+
+ Posted by: {{ question.user.username }}
+ {{ getTime }}
+
+
+
+
+
+
+
+
+ Are you sure you want to delete this post?
+
+
+ Once deleted your post cannot be recovered.
+
+
+
+
+
+
+ Nevermind
+
+
+
+ Yes, i'm sure
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+ You must login before voting.
+
+
+ Public questions
+ Your questions
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+ Question posted!
+
+
+ Your question is now out for everyone to see.
+
+
+
+
+
+
+ Close
+
+
+
+ Back to home
+
+
+
+ See your post
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+ {{ errMessage }}
+
+
+ Sign In
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+ Successfully registered
+
+
+ You can now sign in and use HacktivOverflow's features!
+
+
+
+
+
+
+ Close
+
+
+
+ Procceed to sign in
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
+
+
+ Nevermind
+
+
+
+ Edit
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+ {{ errMessage }}
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+ Ask a question
+
+
+
+
+
+ Post your question
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+ You must login before voting.
+
+
+
+
+ keyboard_arrow_up
+ {{ question.upvotes.length - question.downvotes.length }}
+ keyboard_arrow_down
+
+
+
+
+ {{ question.question }}
+
+ Asked {{ getTime }} by {{question.user.username}}
+
+
+
+
+
+
+
+
+ Have relevant insight? Answer below.
+
+
+
+
+
+
+ You must log in first before answering.
+
+ Submit
+
+
+
+
+
+
+ -
+
+
+
+
+
+
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