added a lot of auth code and resolved merge conflicht

This commit is contained in:
2019-03-21 16:23:04 +01:00
11 changed files with 585 additions and 16 deletions

21
package-lock.json generated
View File

@@ -2392,6 +2392,15 @@
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
}, },
"axios": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz",
"integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=",
"requires": {
"follow-redirects": "^1.3.0",
"is-buffer": "^1.1.5"
}
},
"babel-code-frame": { "babel-code-frame": {
"version": "6.26.0", "version": "6.26.0",
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
@@ -5515,7 +5524,6 @@
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.7.0.tgz",
"integrity": "sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==", "integrity": "sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ==",
"dev": true,
"requires": { "requires": {
"debug": "^3.2.6" "debug": "^3.2.6"
}, },
@@ -5524,7 +5532,6 @@
"version": "3.2.6", "version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"dev": true,
"requires": { "requires": {
"ms": "^2.1.1" "ms": "^2.1.1"
} }
@@ -12830,6 +12837,11 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.8.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.8.tgz",
"integrity": "sha512-+vp9lEC2Kt3yom673pzg1J7T1NVGuGzO9j8Wxno+rQN2WYVBX2pyo/RGQ3fXCLh2Pk76Skw/laAPCuBuEQ4diw==" "integrity": "sha512-+vp9lEC2Kt3yom673pzg1J7T1NVGuGzO9j8Wxno+rQN2WYVBX2pyo/RGQ3fXCLh2Pk76Skw/laAPCuBuEQ4diw=="
}, },
"vue-axios": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/vue-axios/-/vue-axios-2.1.4.tgz",
"integrity": "sha512-DS8Q+WFT3i7nS0aZ/NMmTPf2yhbtlXhj4QEZmY69au/BshsGzGjC6dXaniZaPQlErP3J3Sv1HtQ4RVrXaUTkxA=="
},
"vue-class-component": { "vue-class-component": {
"version": "6.3.2", "version": "6.3.2",
"resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-6.3.2.tgz", "resolved": "https://registry.npmjs.org/vue-class-component/-/vue-class-component-6.3.2.tgz",
@@ -12950,6 +12962,11 @@
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.1.0.tgz", "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.1.0.tgz",
"integrity": "sha512-mdHeHT/7u4BncpUZMlxNaIdcN/HIt1GsGG5LKByArvYG/v6DvHcOxvDCts+7SRdCoIRGllK8IMZvQtQXLppDYg==" "integrity": "sha512-mdHeHT/7u4BncpUZMlxNaIdcN/HIt1GsGG5LKByArvYG/v6DvHcOxvDCts+7SRdCoIRGllK8IMZvQtQXLppDYg=="
}, },
"vuex-typex": {
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/vuex-typex/-/vuex-typex-3.1.4.tgz",
"integrity": "sha512-MXW5QBk8Cq01zP5/5cKLp8QQmC5Zp2m/mSZlWLWmmeF6gGT7d1RpQ2zxc0rT4JQkLOYLdZees+yvgjivtBZDLg=="
},
"w3c-hr-time": { "w3c-hr-time": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz",

View File

@@ -10,14 +10,17 @@
}, },
"dependencies": { "dependencies": {
"@vue/cli": "^3.4.1", "@vue/cli": "^3.4.1",
"axios": "^0.18.0",
"bootstrap-vue": "^2.0.0-rc.13", "bootstrap-vue": "^2.0.0-rc.13",
"vue": "^2.6.6", "vue": "^2.6.6",
"vue-axios": "^2.1.4",
"vue-class-component": "^6.0.0", "vue-class-component": "^6.0.0",
"vue-flag-icon": "^1.0.6", "vue-flag-icon": "^1.0.6",
"vue-i18n": "^8.9.0", "vue-i18n": "^8.9.0",
"vue-property-decorator": "^7.0.0", "vue-property-decorator": "^7.0.0",
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vuex": "^3.0.1" "vuex": "^3.0.1",
"vuex-typex": "^3.1.4"
}, },
"devDependencies": { "devDependencies": {
"@types/chai": "^4.1.0", "@types/chai": "^4.1.0",

31
src/api/index.ts Normal file
View File

@@ -0,0 +1,31 @@
import axios from 'axios';
const API_URL = 'http://localhost:5443/api';
export function fetchSurveys () {
return axios.get(`${API_URL}/surveys/`)
}
export function fetchSurvey (surveyId: string) {
return axios.get(`${API_URL}/surveys/${surveyId}/`)
}
export function saveSurveyResponse (surveyResponse: any) {
return axios.put(`${API_URL}/surveys/${surveyResponse.id}/`, surveyResponse)
}
export function postNewSurvey (survey: any, jwt: any) {
return axios.post(`${API_URL}/surveys/`, survey, { headers: { Authorization: `Bearer: ${jwt}` } })
}
export function authenticate (userData: any) {
return axios.post(`${API_URL}/auth/login`, userData)
}
export function register (userData: any) {
return axios.post(`${API_URL}/auth/register`, userData)
}
export function getProviders (providers: any) {
return axios.get(`${API_URL}/auth/providers`, providers)
}

79
src/components/Login.vue Normal file
View File

@@ -0,0 +1,79 @@
<!-- components/Login.vue -->
<template>
<div>
<section class="hero is-primary">
<div class="hero-body">
<div class="container has-text-centered">
<h2 class="title">Login or Register</h2>
<p class="subtitle error-msg">{{ errorMsg }}</p>
</div>
</div>
</section>
<section class="section">
<div class="container">
<div class="field">
<label class="label is-large" for="email">Email:</label>
<div class="control">
<input type="email" class="input is-large" id="email" v-model="email">
</div>
</div>
<div class="field">
<label class="label is-large" for="password">Password:</label>
<div class="control">
<input type="password" class="input is-large" id="password" v-model="password">
</div>
</div>
<div class="control">
<a class="button is-large is-primary" @click="authenticate">Login</a>
<a class="button is-large is-success" @click="register">Register</a>
</div>
</div>
</section>
</div>
</template>
<script>
import { EventBus } from '@/utils'
export default {
data () {
return {
email: '',
password: '',
errorMsg: ''
}
},
methods: {
authenticate () {
this.$store.dispatch('login', { email: this.email, password: this.password })
.then(() => this.$router.push('/'))
},
register () {
this.$store.dispatch('register', { email: this.email, password: this.password })
.then(() => this.$router.push('/'))
}
},
mounted () {
EventBus.$on('failedRegistering', (msg) => {
this.errorMsg = msg
})
EventBus.$on('failedAuthentication', (msg) => {
this.errorMsg = msg
})
},
beforeDestroy () {
EventBus.$off('failedRegistering')
EventBus.$off('failedAuthentication')
}
}
</script>
<style lang="scss">
.error-msg {
color: red;
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,75 @@
<template>
<div>
<div class="field">
<label class="label is-large">Question</label>
<div class="control">
<input type="text" class="input is-large" v-model="question">
</div>
</div>
<div class="field">
<div class="control">
<a class="button is-large is-info" @click="addChoice">
<span class="icon is-small">
<i class="fa fa-plus-square-o fa-align-left" aria-hidden="true"></i>
</span>
<span>Add choice</span>
</a>
<a class="button is-large is-primary" @click="saveQuestion">
<span class="icon is-small">
<i class="fa fa-check"></i>
</span>
<span>Save</span>
</a>
</div>
</div>
<h2 class="label is-large" v-show="choices.length > 0">Question Choices</h2>
<div class="field has-addons" v-for="(choice, idx) in choices" v-bind:key="idx">
<div class="control choice">
<input type="text" class="input is-large" v-model="choices[idx]">
</div>
<div class="control">
<a class="button is-large">
<span class="icon is-small" @click.stop="removeChoice(choice)">
<i class="fa fa-times" aria-hidden="true"></i>
</span>
</a>
</div>
</div>
</div>
</template>
<script>
export default {
data () {
return {
question: '',
choices: []
}
},
methods: {
removeChoice (choice) {
const idx = this.choices.findIndex(c => c === choice)
this.choices.splice(idx, 1)
},
saveQuestion () {
this.$emit('questionComplete', {
question: this.question,
choices: this.choices.filter(c => !!c)
})
this.question = ''
this.choices = ['']
},
addChoice () {
this.choices.push('')
}
}
}
</script>
<style>
.choice {
width: 90%;
}
</style>

View File

@@ -0,0 +1,122 @@
<template>
<div>
<section class="hero is-primary">
<div class="hero-body">
<div class="container has-text-centered">
<h2 class="title">{{ name }}</h2>
</div>
</div>
</section>
<section class="section">
<div class="container">
<div class="tabs is-centered is-fullwidth is-large">
<ul>
<li :class="{'is-active': step == 'name'}" @click="step = 'name'">
<a>Name</a>
</li>
<li :class="{'is-active': step == 'questions'}" @click="step = 'questions'">
<a>Questions</a>
</li>
<li :class="{'is-active': step == 'review'}" @click="step = 'review'">
<a>Review</a>
</li>
</ul>
</div>
<div class="columns">
<div class="column is-half is-offset-one-quarter">
<div class="name" v-show="step === 'name'">
<div class="field">
<label class="label is-large" for="name">Survey name:</label>
<div class="control">
<input type="text" class="input is-large" id="name" v-model="name">
</div>
</div>
</div>
<div class="questions" v-show="step === 'questions'">
<new-question v-on:questionComplete="appendQuestion"/>
</div>
<div class="review" v-show="step === 'review'">
<ul>
<li class="question" v-for="(question, qIdx) in questions" :key="`question-${qIdx}`">
<div class="title">
{{ question.question }}
<span class="icon is-medium is-pulled-right delete-question"
@click.stop="removeQuestion(question)">
<i class="fa fa-times" aria-hidden="true"></i>
</span>
</div>
<ul>
<li v-for="(choice , cIdx) in question.choices" :key="`choice-${cIdx}`">
{{ cIdx + 1 }}. {{ choice }}
</li>
</ul>
</li>
</ul>
<div class="control">
<a class="button is-large is-primary" @click="submitSurvey">Submit</a>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</template>
<script>
import NewQuestion from '@/components/NewQuestion'
export default {
components: { NewQuestion },
data () {
return {
step: 'name',
name: '',
questions: []
}
},
methods: {
appendQuestion (newQuestion) {
this.questions.push(newQuestion)
},
removeQuestion (question) {
const idx = this.questions.findIndex(q => q.question === question.question)
this.questions.splice(idx, 1)
},
submitSurvey () {
this.$store.dispatch('submitNewSurvey', {
name: this.name,
questions: this.questions
})
.then(() => this.$router.push('/'))
.catch((error) => {
console.log('Error creating survey', error)
this.$router.push('/')
})
}
}
}
</script>
<style>
.question {
margin: 10px 20px 25px 10px;
}
.delete-question {
cursor: pointer;
padding: 10px;
}
.delete-question:hover {
background-color: lightgray;
border-radius: 50%;
}
</style>

115
src/components/Survey.vue Normal file
View File

@@ -0,0 +1,115 @@
<template>
<div>
<section class="hero is-primary">
<div class="hero-body">
<div class="container has-text-centered">
<h2 class="title">{{ survey.name }}</h2>
</div>
</div>
</section>
<section class="section">
<div class="container">
<div class="columns">
<div class="column is-10 is-offset-1">
<div
v-for="(question, idx) in survey.questions"
v-bind:key="question.id"
v-show="currentQuestion === idx">
<div class="column is-offset-3 is-6">
<h4 class='title has-text-centered'>{{ question.text }}</h4>
</div>
<div class="column is-offset-4 is-4">
<div class="control">
<div v-for="choice in question.choices" v-bind:key="choice.id">
<label class="radio">
<input type="radio" v-model="question.choice" name="choice" :value="choice.id">
{{ choice.text }}
</label>
</div>
</div>
</div>
</div>
<div class="column is-offset-one-quarter is-half">
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
<a class="pagination-previous" @click.stop="goToPreviousQuestion"><i class="fa fa-chevron-left" aria-hidden="true"></i> &nbsp;&nbsp; Back</a>
<a class="pagination-next" @click.stop="goToNextQuestion">Next &nbsp;&nbsp; <i class="fa fa-chevron-right" aria-hidden="true"></i></a>
</nav>
</div>
<div class="has-text-centered">
<a v-show="surveyComplete" class='button is-large is-focused is-primary' @click="handleSubmit">Submit</a>
</div>
</div>
</div>
</div>
</section>
</div>
</template>
<script>
export default {
data () {
return {
currentQuestion: 0
}
},
beforeMount () {
this.$store.dispatch('loadSurvey', { id: parseInt(this.$route.params.id) })
},
methods: {
goToNextQuestion () {
if (this.currentQuestion === this.survey.questions.length - 1) {
this.currentQuestion = 0
} else {
this.currentQuestion++
}
},
goToPreviousQuestion () {
if (this.currentQuestion === 0) {
this.currentQuestion = this.survey.questions.lenth - 1
} else {
this.currentQuestion--
}
},
handleSubmit () {
this.$store.dispatch('addSurveyResponse')
.then(() => this.$router.push('/'))
}
},
computed: {
surveyComplete () {
if (this.survey.questions) {
const numQuestions = this.survey.questions.length
const numCompleted = this.survey.questions.filter(q => q.choice).length
return numQuestions === numCompleted
}
return false
},
survey () {
return this.$store.state.currentSurvey
},
selectedChoice: {
get () {
const question = this.survey.questions[this.currentQuestion]
return question.choice
},
set (value) {
const question = this.survey.questions[this.currentQuestion]
this.$store.commit('setChoice', { questionId: question.id, choice: value })
}
}
}
}
</script>
<style>
</style>

View File

@@ -1,4 +1,6 @@
import Vue from 'vue'; import Vue from 'vue';
import axios from 'axios'
import VueAxios from 'vue-axios'
import App from './App.vue'; import App from './App.vue';
import router from './router'; import router from './router';
import store from './store'; import store from './store';
@@ -8,6 +10,7 @@ import FlagIcon from 'vue-flag-icon';
// following is to avoid missing type definitions // following is to avoid missing type definitions
// const FlagIcon = require('vue-flag-icon'); // const FlagIcon = require('vue-flag-icon');
Vue.use(VueAxios, axios)
Vue.use(FlagIcon); Vue.use(FlagIcon);
// setup fake backend // setup fake backend

View File

@@ -5,6 +5,11 @@ import NotFound from './views/NotFound.vue';
import LoginPage from './views/LoginPage.vue'; import LoginPage from './views/LoginPage.vue';
import Survey from '@/components/Survey.vue';
import NewSurvey from '@/components/NewSurvey.vue';
import Login from '@/components/Login.vue';
import store from '@/store'
Vue.use(Router); Vue.use(Router);
export const router = new Router({ export const router = new Router({
@@ -28,6 +33,25 @@ export const router = new Router({
// this generates a separate chunk (about.[hash].js) for this route // this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited. // which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ './views/About.vue'), component: () => import(/* webpackChunkName: "about" */ './views/About.vue'),
}, {
path: '/surveys/:id',
name: 'Survey',
component: Survey
}, {
path: '/surveys',
name: 'NewSurvey',
component: NewSurvey,
beforeEnter (to, from, next) {
if (!store.getters.isAuthenticated) {
next('/login')
} else {
next()
}
}
}, {
path: '/login',
name: 'Login',
component: Login
}, },
{ {
path: '*', path: '*',

View File

@@ -1,25 +1,110 @@
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { alert } from './store/alert.module';
import { account } from './store/account.module'; // imports of AJAX functions will go here
import { users } from './store/users.module'; import { fetchSurveys, fetchSurvey, saveSurveyResponse, postNewSurvey, authenticate, register } from '@/api'
import { isValidJwt, EventBus } from '@/utils'
Vue.use(Vuex); Vue.use(Vuex);
export default new Vuex.Store({ const state = {
modules: { // single source of data
alert, surveys: [],
account, currentSurvey: {},
users, user: {},
}, jwt: ''
state: { }
const actions = {
// asynchronous operations
loadSurveys (context: any) {
return fetchSurveys()
.then((response) => {
context.commit('setSurveys', { surveys: response.data })
})
},
// @ts-ignore
loadSurvey (context: any, { id }) {
return fetchSurvey(id)
.then((response) => {
context.commit('setSurvey', { survey: response.data })
})
},
addSurveyResponse (context: any) {
return saveSurveyResponse(context.state.currentSurvey)
}, },
mutations: {
login (context: any, userData: any) {
context.commit('setUserData', { userData })
return authenticate(userData)
.then(response => context.commit('setJwtToken', { jwt: response.data }))
.catch(error => {
console.log('Error Authenticating: ', error)
// @ts-ignore
EventBus.emit('failedAuthentication', error)
})
}, },
actions: { register (context: any, userData: any) {
context.commit('setUserData', { userData })
return register(userData)
.then(context.dispatch('login', userData))
.catch(error => {
console.log('Error Registering: ', error)
// @ts-ignore
EventBus.emit('failedRegistering: ', error)
})
},
submitNewSurvey (context: any, survey: any) {
return postNewSurvey(survey, context.state.jwt.token)
}
}
const mutations = {
// isolated data mutations
setSurveys (state: any, payload: any) {
state.surveys = payload.surveys
}, },
}); setSurvey (state: any, payload: any) {
const nQuestions = payload.survey.questions.length
for (let i = 0; i < nQuestions; i++) {
payload.survey.questions[i].choice = null
}
state.currentSurvey = payload.survey
},
setChoice (state: any, payload: any) {
const { questionId, choice } = payload
const nQuestions = state.currentSurvey.questions.length
for (let i = 0; i < nQuestions; i++) {
if (state.currentSurvey.questions[i].id === questionId) {
state.currentSurvey.questions[i].choice = choice
break
}
}
},
setUserData (state: any, payload: any) {
console.log('setUserData payload = ', payload)
state.userData = payload.userData
},
setJwtToken (state: any, payload: any) {
console.log('setJwtToken payload = ', payload)
localStorage.token = payload.jwt.token
state.jwt = payload.jwt
}
}
const getters = {
// reusable data accessors
isAuthenticated (state: any) {
return isValidJwt(state.jwt.token)
}
}
const store = new Vuex.Store({
state,
actions,
mutations,
getters
})
export default store

15
src/utils/index.ts Normal file
View File

@@ -0,0 +1,15 @@
// utils/index.js
import Vue from 'vue'
export const EventBus = new Vue();
export function isValidJwt (jwt: any) {
if (!jwt || jwt.split('.').length < 3) {
return false
}
const data = JSON.parse(atob(jwt.split('.')[1]));
const exp = new Date(data.exp * 1000);
const now = new Date();
return now < exp;
}