From afdd476bff784d5a0c6579f61d0b270488b58d32 Mon Sep 17 00:00:00 2001 From: Nuno Das Neves Date: Tue, 9 Oct 2018 17:59:01 +1100 Subject: [PATCH 01/25] signup now in two stages; create firebase account, and create profile --- backend/src/controllers/user.js | 14 ++++------ backend/src/models/db/tables.js | 7 ++--- backend/src/models/user.js | 4 +-- backend/src/routes/user.js | 14 +++++----- backend/src/utils/helpers.js | 13 +++++++++- frontend/src/components/AppNavBar.vue | 6 ++--- frontend/src/router.js | 5 ++++ frontend/src/store/auth/index.js | 30 ++++++++++++---------- frontend/src/store/root.js | 12 ++++++--- frontend/src/utils/api/auth.js | 4 +-- frontend/src/views/SignUp.vue | 37 +++++++++++++++++++++------ 11 files changed, 95 insertions(+), 51 deletions(-) diff --git a/backend/src/controllers/user.js b/backend/src/controllers/user.js index 03f6fb3f..3faeaa55 100644 --- a/backend/src/controllers/user.js +++ b/backend/src/controllers/user.js @@ -3,13 +3,9 @@ const userModel = require('../models/user')() const { responseHandler } = require('../utils/helpers') /* Get data for a specific user */ -exports.getUser = function(_, res) { - return res.json({ - userID: 1, - firstName: 'Walker', - lastName: 'Francis', - email: 'alnuno-das-hinds@gmail.com' - }) +exports.getUser = function({id}, res) { + return responseHandler(userModel.getPublicProfile(id), res) + .catch(errorHandler(res)) } /** @@ -25,7 +21,7 @@ exports.getSelf = function(req, res) { return res.json(req.user) } -exports.createUser = function({ authorized }, res) { - return responseHandler(userModel.createUser(authorized), res) +exports.createUser = function({ authorized: { email, uid }, body: { displayName } }, res) { + return responseHandler(userModel.createUser({ email, uid, displayName }), res) .catch(errorHandler(res)) } diff --git a/backend/src/models/db/tables.js b/backend/src/models/db/tables.js index 22c015de..81ade4d7 100644 --- a/backend/src/models/db/tables.js +++ b/backend/src/models/db/tables.js @@ -6,14 +6,15 @@ function createUserTable (db) { return new Promise((resolve, reject) => { db.run(`CREATE TABLE user ( id INTEGER PRIMARY KEY AUTOINCREMENT, - uid TEXT NOT NULL, - displayName TEXT DEFAULT 'ANON', + uid TEXT UNIQUE NOT NULL, + displayName TEXT UNIQUE NOT NULL, email TEXT UNIQUE NOT NULL, joined TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, reputation INTEGER DEFAULT '0', degree TEXT, gradYear TIMESTAMP DEFAULT '2018', - description TEXT + description TEXT, + picture TEXT )`, (err) => err ? reject(err) : resolve('Created User Table')) }) diff --git a/backend/src/models/user.js b/backend/src/models/user.js index 499dd220..17d46c05 100644 --- a/backend/src/models/user.js +++ b/backend/src/models/user.js @@ -20,9 +20,9 @@ class User { * @param {number} id Required id param. * @returns {object} */ - getUser(id) { + getPublicProfile(id) { return this.db - .query('SELECT * FROM user WHERE id=?', [id]) + .query('SELECT id, displayName, joined, degree, gradYear, description, picture FROM user WHERE id=?', [id]) } /** diff --git a/backend/src/routes/user.js b/backend/src/routes/user.js index e17bccc2..44db6214 100644 --- a/backend/src/routes/user.js +++ b/backend/src/routes/user.js @@ -1,20 +1,20 @@ const express = require('express') const router = express.Router() -const { isAuthorized } = require('../utils/helpers') +const { isAuthorized, isFirebaseAuthorized } = require('../utils/helpers') const { getSelf, getUser, createUser } = require('../controllers/user') -/* Get data for a specific user */ +/* Get public data for a specific user */ router.get('/:id', getUser) /* no valid jwt then don't allow user to be created */ +router.use(isFirebaseAuthorized) +router.post('/', createUser) + +/* full auth check */ router.use(isAuthorized) -/** - * Both of these require an authorization header - * provide frontend with any user specific data - */ +/* provide frontend with any user specific data */ router.get('/', getSelf) -router.post('/', createUser) module.exports = router diff --git a/backend/src/utils/helpers.js b/backend/src/utils/helpers.js index 4233bb9c..24214ebe 100644 --- a/backend/src/utils/helpers.js +++ b/backend/src/utils/helpers.js @@ -11,12 +11,23 @@ exports.responseHandler = function(fn, response) { exports.toLowerCase = str => str.toLowerCase() -exports.isAuthorized = function(req, res, next) { +exports.isFirebaseAuthorized = function(req, res, next) { + console.log(req.authorized) + console.log(req.body) if (!req.authorized) { return res.status(401).json({ code: 401, message: 'Unauthorized' }) } next() } +exports.isAuthorized = function(req, res, next) { + console.log(req.authorized) + console.log(req.user) + if (!req.user) { + return res.status(401).json({ code: 401, message: 'Unauthorized' }) + } + next() +} + /** * Convert a string to utf8 diff --git a/frontend/src/components/AppNavBar.vue b/frontend/src/components/AppNavBar.vue index d17374c8..8962f9d5 100644 --- a/frontend/src/components/AppNavBar.vue +++ b/frontend/src/components/AppNavBar.vue @@ -9,6 +9,7 @@

Login

Sign Up

+

Complete Signup

@@ -16,13 +17,12 @@ diff --git a/frontend/src/router.js b/frontend/src/router.js index a5472d13..ece4dc26 100644 --- a/frontend/src/router.js +++ b/frontend/src/router.js @@ -115,6 +115,11 @@ export default new Router({ name: 'Login', component: () => import('./views/Login') }, + { + path: '/profile', + name: 'Profile', + component: () => import('./views/Profile') + }, { path: '/password-reset', name: 'Forgot Password', diff --git a/frontend/src/store/auth/index.js b/frontend/src/store/auth/index.js index 1f2a591a..01657b31 100644 --- a/frontend/src/store/auth/index.js +++ b/frontend/src/store/auth/index.js @@ -1,7 +1,7 @@ // firebase authentication class import auth from './config' -import { createUser } from '@/utils/api/auth' +import { createProfile } from '@/utils/api/auth' const state = { loading: false, @@ -43,7 +43,7 @@ const actions = { */ logout({ commit }) { return auth.signOut() - .then(() => commit('SET_USER', null, { root: true })) + .then(() => commit('SET_USER', {}, { root: true })) .catch(error => commit('ERROR', error.message)) }, @@ -52,19 +52,23 @@ const actions = { * UserAuth obj has field user which is what we're interested in * Currently, successful signUp will automatically sign in user. **/ - signUp({ commit }, { email, password }) { + createAccount({ commit }, { email, password }) { commit('SET_LOADING', true) return auth.createUserWithEmailAndPassword(email, password) - .then(({ user }) => { - /* TODO implement retry if failure occurs on server - Simultaneously: - 1. Send off new user creds for backend creation, - 2. Set user state in store. - */ - return Promise.all([ - createUser(user), - commit('SET_USER', user, { root: true })]) + .then(({ user }) => commit('SET_USER', user, { root: true })) + .catch(error => { + commit('ERROR', error.message) + throw error }) + .finally(() => commit('SET_LOADING', false)) + }, + /** + * Create user profile in the backend + **/ + createProfile({ commit }, { user, displayName }) { + commit('SET_LOADING', true) + return createProfile(user, { displayName }) + .then((profile) => commit('SET_PROFILE', profile, { root: true })) .catch(error => { commit('ERROR', error.message) throw error @@ -82,7 +86,7 @@ const actions = { resolve(user) }, reject) }) - .then(user => commit('SET_USER', user, { root: true })) + .then(user => commit('SET_USER', user || {}, { root: true })) .catch(error => { commit('ERROR', error) }) diff --git a/frontend/src/store/root.js b/frontend/src/store/root.js index 50d67537..37d90716 100644 --- a/frontend/src/store/root.js +++ b/frontend/src/store/root.js @@ -5,15 +5,18 @@ const state = { loading: false, // cached courses courses: [], - // fire authObject + // firebase authObject userAuthObject: {}, // our own user data profile: {} } const getters = { - isLoggedIn: ({ userAuthObject }) => !!userAuthObject, - authObject: ({ userAuthObject }) => userAuthObject + authObject: ({ userAuthObject }) => userAuthObject, + // logged into firebase (authenticated account) + isLoggedIn: ({ userAuthObject }) => !!Object.keys(userAuthObject).length, + // logged into backend (existing profile) + hasProfile: ({ profile }) => !!Object.keys(profile).length } const actions = { @@ -42,6 +45,9 @@ const mutations = { */ SET_USER(state, user) { state.userAuthObject = user + }, + SET_PROFILE(state, profile) { + state.profile = profile } } diff --git a/frontend/src/utils/api/auth.js b/frontend/src/utils/api/auth.js index afbbdfe4..acf2a24c 100644 --- a/frontend/src/utils/api/auth.js +++ b/frontend/src/utils/api/auth.js @@ -33,9 +33,9 @@ export function getSelf(user) { * entry for the user * @param {*} user The user object provided by firebase */ -export function createUser(user) { +export function createProfile(user, data) { return getAuthHeaders(user) - .then(options => post('/user', options)) + .then(options => post('/user', { ...options, data })) } /** diff --git a/frontend/src/views/SignUp.vue b/frontend/src/views/SignUp.vue index 6b6456b4..6f5f162b 100644 --- a/frontend/src/views/SignUp.vue +++ b/frontend/src/views/SignUp.vue @@ -1,19 +1,33 @@ @@ -28,18 +42,25 @@ export default { data() { return { email: '', - password: '' + password: '', + displayName: '' } }, components: { AppAuthForm, AuthInput }, computed: { - ...mapGetters('auth', [ 'error', 'loading' ]) + ...mapGetters('auth', [ 'error', 'loading' ]), + ...mapGetters([ 'isLoggedIn', 'hasProfile', 'authObject' ]) }, methods: { - clickHandler() { + createAccount() { const { email, password } = this - this.$store.dispatch('auth/signUp', { email, password }) - .then(() => this.$router.push('/')) + this.$store.dispatch('auth/createAccount', { email, password }) + .catch(e => {}) + }, + createProfile() { + const { displayName } = this + this.$store.dispatch('auth/createProfile', { user: this.authObject, displayName }) + .then(this.$router.push('/')) .catch(e => {}) } }, From 787eb209aed895946c0fedc740cac7cc5f33773f Mon Sep 17 00:00:00 2001 From: Nuno Das Neves Date: Wed, 10 Oct 2018 02:07:56 +1100 Subject: [PATCH 02/25] signup flow more logical now ... handles most edge cases pretty well --- backend/src/utils/helpers.js | 6 +--- frontend/src/components/AppNavBar.vue | 5 ++-- frontend/src/store/auth/index.js | 41 ++++++++++++++++++++++----- frontend/src/store/root.js | 4 +-- frontend/src/utils/api/auth.js | 4 +-- frontend/src/views/Login.vue | 19 ++++++++++--- frontend/src/views/SignUp.vue | 2 +- 7 files changed, 57 insertions(+), 24 deletions(-) diff --git a/backend/src/utils/helpers.js b/backend/src/utils/helpers.js index 24214ebe..06d1455c 100644 --- a/backend/src/utils/helpers.js +++ b/backend/src/utils/helpers.js @@ -12,18 +12,14 @@ exports.responseHandler = function(fn, response) { exports.toLowerCase = str => str.toLowerCase() exports.isFirebaseAuthorized = function(req, res, next) { - console.log(req.authorized) - console.log(req.body) if (!req.authorized) { return res.status(401).json({ code: 401, message: 'Unauthorized' }) } next() } exports.isAuthorized = function(req, res, next) { - console.log(req.authorized) - console.log(req.user) if (!req.user) { - return res.status(401).json({ code: 401, message: 'Unauthorized' }) + return res.status(403).json({ code: 403, message: 'No user profile' }) } next() } diff --git a/frontend/src/components/AppNavBar.vue b/frontend/src/components/AppNavBar.vue index 8962f9d5..78ca0242 100644 --- a/frontend/src/components/AppNavBar.vue +++ b/frontend/src/components/AppNavBar.vue @@ -9,8 +9,9 @@

Login

Sign Up

-

Complete Signup

- +

Profile

+

Profile

+
diff --git a/frontend/src/store/auth/index.js b/frontend/src/store/auth/index.js index 01657b31..27f83251 100644 --- a/frontend/src/store/auth/index.js +++ b/frontend/src/store/auth/index.js @@ -1,7 +1,7 @@ // firebase authentication class import auth from './config' -import { createProfile } from '@/utils/api/auth' +import { createProfile, getSelf } from '@/utils/api/auth' const state = { loading: false, @@ -15,7 +15,7 @@ const getters = { const mutations = { ERROR(state, message) { - if (message) console.log('AUTH ERROR', message) + if (message) console.warn('AUTH ERROR', message) state.error = message }, SET_LOADING(state, isLoading) { @@ -29,10 +29,20 @@ const actions = { signIn({ commit }, { email, password }) { commit('SET_LOADING', true) return auth.signInWithEmailAndPassword(email, password) - .then(({ user }) => commit('SET_USER', user, { root: true })) + .then(({ user }) => { + commit('SET_USER', user, { root: true }) + return getSelf(user) + }) + .then(profile => { + commit('SET_PROFILE', profile, { root: true }) + }) .catch(error => { commit('ERROR', error.message) - throw error + commit('SET_PROFILE', {}, { root: true }) + if (!error.code || error.code != 403) { + auth.signOut() + commit('SET_USER', {}, { root: true }) + } }) .finally(() => commit('SET_LOADING', false)) }, @@ -43,7 +53,10 @@ const actions = { */ logout({ commit }) { return auth.signOut() - .then(() => commit('SET_USER', {}, { root: true })) + .then(() => { + commit('SET_USER', {}, { root: true }) + commit('SET_PROFILE', {}, { root: true }) + }) .catch(error => commit('ERROR', error.message)) }, @@ -62,6 +75,7 @@ const actions = { }) .finally(() => commit('SET_LOADING', false)) }, + /** * Create user profile in the backend **/ @@ -86,10 +100,23 @@ const actions = { resolve(user) }, reject) }) - .then(user => commit('SET_USER', user || {}, { root: true })) + .then((user) => { + if (user === null) throw Error('Not logged in') + commit('SET_USER', user, { root: true }) + return getSelf(user) + }) + .then(profile => { + commit('SET_PROFILE', profile, { root: true }) + }) .catch(error => { - commit('ERROR', error) + commit('ERROR', error.message) + commit('SET_PROFILE', {}, { root: true }) + if (!error.code || error.code != 403) { + auth.signOut() + commit('SET_USER', {}, { root: true }) + } }) + .finally(() => commit('SET_LOADING', false)) } } diff --git a/frontend/src/store/root.js b/frontend/src/store/root.js index 37d90716..3ebec010 100644 --- a/frontend/src/store/root.js +++ b/frontend/src/store/root.js @@ -44,10 +44,10 @@ const mutations = { * @param {*} user The logged in user object or null */ SET_USER(state, user) { - state.userAuthObject = user + state.userAuthObject = user || {} }, SET_PROFILE(state, profile) { - state.profile = profile + state.profile = profile || {} } } diff --git a/frontend/src/utils/api/auth.js b/frontend/src/utils/api/auth.js index acf2a24c..4ceb2843 100644 --- a/frontend/src/utils/api/auth.js +++ b/frontend/src/utils/api/auth.js @@ -23,9 +23,7 @@ export function getAuthHeaders(user) { */ export function getSelf(user) { return getAuthHeaders(user) - .then(options => - get('/user', options)) - .catch(err => console.warn(err)) + .then(options => get('/user', options)) } /** diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue index f495c0af..acbb27f2 100644 --- a/frontend/src/views/Login.vue +++ b/frontend/src/views/Login.vue @@ -1,6 +1,6 @@ @@ -33,14 +36,22 @@ export default { }, components: { AppAuthForm, AuthInput }, computed: { - ...mapGetters('auth', [ 'error', 'loading' ]) + ...mapGetters('auth', [ 'error', 'loading' ]), + ...mapGetters([ 'isLoggedIn', 'hasProfile', 'authObject' ]) }, methods: { clickHandler() { const { email, password } = this this.$store.dispatch('auth/signIn', { email, password }) - .then(() => this.$router.push('/')) - .catch(e => {}) + .then(() => { + if (this.isLoggedIn) { + if (this.hasProfile) { + this.$router.push('/') + } else { + this.$router.push('/signup') + } + } + }) } }, created() { diff --git a/frontend/src/views/SignUp.vue b/frontend/src/views/SignUp.vue index 6f5f162b..d5352902 100644 --- a/frontend/src/views/SignUp.vue +++ b/frontend/src/views/SignUp.vue @@ -14,7 +14,7 @@ Date: Wed, 10 Oct 2018 12:12:45 +1100 Subject: [PATCH 03/25] basic profile page - still buggy, non functional update profile button --- frontend/src/components/AppInput.vue | 13 +++ .../questions-answers/QuestionForm.vue | 4 +- .../components/reviews-replies/ReviewForm.vue | 4 +- frontend/src/store/auth/index.js | 5 +- frontend/src/store/root.js | 6 +- frontend/src/views/Profile.vue | 105 +++++++++++++++++- 6 files changed, 128 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/AppInput.vue b/frontend/src/components/AppInput.vue index 46908f08..dc767341 100644 --- a/frontend/src/components/AppInput.vue +++ b/frontend/src/components/AppInput.vue @@ -21,6 +21,19 @@ input[type=text] { transition: 0.2s border ease-in-out; } +input[type=textarea] { + border: var(--border); + border-radius: 2px; + font: inherit; + resize: none; + padding: 10px; + outline: none; + margin: 10px 0px; + width: calc(100% - 20px); + height: 100px; + transition: 0.2s border ease-in-out; +} + input:active, input:focus { border: 1px solid #acc; } diff --git a/frontend/src/components/questions-answers/QuestionForm.vue b/frontend/src/components/questions-answers/QuestionForm.vue index 8259cc8b..99218cd1 100644 --- a/frontend/src/components/questions-answers/QuestionForm.vue +++ b/frontend/src/components/questions-answers/QuestionForm.vue @@ -3,8 +3,8 @@
Submit Your Question
- -
+ +
Ask diff --git a/frontend/src/components/reviews-replies/ReviewForm.vue b/frontend/src/components/reviews-replies/ReviewForm.vue index e0764d1b..02956fb1 100644 --- a/frontend/src/components/reviews-replies/ReviewForm.vue +++ b/frontend/src/components/reviews-replies/ReviewForm.vue @@ -9,9 +9,9 @@ Please think carefully about your feedback before you submit it.

* -
+
* -
+
Would you recommend this course to a friend? * diff --git a/frontend/src/store/auth/index.js b/frontend/src/store/auth/index.js index 27f83251..9240fac5 100644 --- a/frontend/src/store/auth/index.js +++ b/frontend/src/store/auth/index.js @@ -39,7 +39,7 @@ const actions = { .catch(error => { commit('ERROR', error.message) commit('SET_PROFILE', {}, { root: true }) - if (!error.code || error.code != 403) { + if (!error.code || error.code !== 403) { auth.signOut() commit('SET_USER', {}, { root: true }) } @@ -94,6 +94,7 @@ const actions = { * Called on application boot once firebase has been initialised */ checkAuth({ commit }) { + commit('SET_LOADING', true) return new Promise((resolve, reject) => { const unsubscribe = auth.onAuthStateChanged(user => { unsubscribe() @@ -111,7 +112,7 @@ const actions = { .catch(error => { commit('ERROR', error.message) commit('SET_PROFILE', {}, { root: true }) - if (!error.code || error.code != 403) { + if (!error.code || error.code !== 403) { auth.signOut() commit('SET_USER', {}, { root: true }) } diff --git a/frontend/src/store/root.js b/frontend/src/store/root.js index 3ebec010..d3e51004 100644 --- a/frontend/src/store/root.js +++ b/frontend/src/store/root.js @@ -3,6 +3,7 @@ import { get } from '../utils/api' /* root application state */ const state = { loading: false, + error: '', // cached courses courses: [], // firebase authObject @@ -16,7 +17,10 @@ const getters = { // logged into firebase (authenticated account) isLoggedIn: ({ userAuthObject }) => !!Object.keys(userAuthObject).length, // logged into backend (existing profile) - hasProfile: ({ profile }) => !!Object.keys(profile).length + hasProfile: ({ profile }) => !!Object.keys(profile).length, + profile: ({ profile }) => profile, + loading: ({ loading }) => loading, + error: ({ error }) => error } const actions = { diff --git a/frontend/src/views/Profile.vue b/frontend/src/views/Profile.vue index a8604d54..40bf66d9 100644 --- a/frontend/src/views/Profile.vue +++ b/frontend/src/views/Profile.vue @@ -1,4 +1,105 @@ + + + + From cc0e33dbbb6d3f4c97d8e5e291eb872bae2833a0 Mon Sep 17 00:00:00 2001 From: Nuno Das Neves Date: Wed, 10 Oct 2018 12:29:42 +1100 Subject: [PATCH 04/25] local default picture --- frontend/public/defaultpicture.png | Bin 0 -> 4957 bytes frontend/src/views/Profile.vue | 11 +++++++---- 2 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 frontend/public/defaultpicture.png diff --git a/frontend/public/defaultpicture.png b/frontend/public/defaultpicture.png new file mode 100644 index 0000000000000000000000000000000000000000..eda26dd39cf136dd561d2b773517f356f846cdae GIT binary patch literal 4957 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 KLk|z0z4&hd$f$_` literal 0 HcmV?d00001 diff --git a/frontend/src/views/Profile.vue b/frontend/src/views/Profile.vue index 40bf66d9..8080c941 100644 --- a/frontend/src/views/Profile.vue +++ b/frontend/src/views/Profile.vue @@ -6,7 +6,7 @@
- +

{{ profile.displayName }}

@@ -40,6 +40,7 @@ export default { components: { AppInput, AppButton, Card }, data() { return { + picture: '', degree: '', gradYear: '', description: '' @@ -50,7 +51,9 @@ export default { ...mapGetters('auth', [ 'loading', 'error' ]) }, mounted() { + // TODO this doesn't work if page reloaded because auth gets checked after mounting if (this.hasProfile) { + this.picture = this.profile.picture this.degree = this.profile.degree this.gradYear = this.profile.gradYear this.description = this.profile.description @@ -75,7 +78,6 @@ p { } figure { - border: var(--border); border-radius: 100%; margin: auto; width: 120px; @@ -84,9 +86,10 @@ figure { } .avatar { - margin: 8px; - width: 100px; + margin: 0; + width: inherit; height: auto; + border-radius: 100%; } .form { From bfb209fb1d200714110fec7733d3d1d95baf2758 Mon Sep 17 00:00:00 2001 From: Nuno Das Neves Date: Wed, 10 Oct 2018 12:54:33 +1100 Subject: [PATCH 05/25] update user backend added... no tests --- backend/src/controllers/user.js | 5 +++++ backend/src/models/user.js | 10 ++++++++-- backend/src/routes/user.js | 3 ++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/backend/src/controllers/user.js b/backend/src/controllers/user.js index 3faeaa55..71e572ea 100644 --- a/backend/src/controllers/user.js +++ b/backend/src/controllers/user.js @@ -25,3 +25,8 @@ exports.createUser = function({ authorized: { email, uid }, body: { displayName return responseHandler(userModel.createUser({ email, uid, displayName }), res) .catch(errorHandler(res)) } + +exports.updateUser = function({ user: { id }, body: { degree, gradYear, description, picture } }, res) { + return responseHandler(userModel.updateUser(id, { degree, gradYear, description, picture }), res) + .catch(errorHandler(res)) +} diff --git a/backend/src/models/user.js b/backend/src/models/user.js index 17d46c05..feaef3f3 100644 --- a/backend/src/models/user.js +++ b/backend/src/models/user.js @@ -6,12 +6,12 @@ class User { } /** - * TODO Return specialsed information for auth'd user + * TODO Return specialised information for auth'd user * @param {string} id The id of the auth'd user */ getProfile(id) { return this.db - .query('SELECT * FROM user WHERE id=?', [id]) + .query('SELECT id, email, displayName, joined, degree, gradYear, description, picture, joined FROM user WHERE id=?', [id]) } /** @@ -44,6 +44,12 @@ class User { .insert('user', { displayName, email, uid }) .then(id => this.getProfile(id)) } + + updateUser(id, data) { + return this.db + .update('user', data, { id }) + .then(() => this.getProfile(id)) + } } let Singleton = null diff --git a/backend/src/routes/user.js b/backend/src/routes/user.js index 44db6214..428b7d5f 100644 --- a/backend/src/routes/user.js +++ b/backend/src/routes/user.js @@ -1,7 +1,7 @@ const express = require('express') const router = express.Router() const { isAuthorized, isFirebaseAuthorized } = require('../utils/helpers') -const { getSelf, getUser, createUser } = require('../controllers/user') +const { getSelf, getUser, createUser, updateUser } = require('../controllers/user') /* Get public data for a specific user */ router.get('/:id', getUser) @@ -15,6 +15,7 @@ router.use(isAuthorized) /* provide frontend with any user specific data */ router.get('/', getSelf) +router.put('/', updateUser) module.exports = router From e5d0168e0196fddfbc2a3b8ef153e052d5848ede Mon Sep 17 00:00:00 2001 From: Nuno Das Neves Date: Wed, 10 Oct 2018 12:58:21 +1100 Subject: [PATCH 06/25] lint errors fixed --- frontend/src/views/Profile.vue | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/frontend/src/views/Profile.vue b/frontend/src/views/Profile.vue index 8080c941..9fc57589 100644 --- a/frontend/src/views/Profile.vue +++ b/frontend/src/views/Profile.vue @@ -14,7 +14,7 @@ Degree: Graduation Year: Description: - + Update Profile @@ -39,24 +39,28 @@ import { mapGetters } from 'vuex' export default { components: { AppInput, AppButton, Card }, data() { - return { - picture: '', - degree: '', - gradYear: '', - description: '' - } + return { + picture: '', + degree: '', + gradYear: '', + description: '' + } }, computed: { ...mapGetters([ 'isLoggedIn', 'hasProfile', 'profile' ]), ...mapGetters('auth', [ 'loading', 'error' ]) }, + methods: { + updateProfile() { + } + }, mounted() { // TODO this doesn't work if page reloaded because auth gets checked after mounting if (this.hasProfile) { - this.picture = this.profile.picture - this.degree = this.profile.degree - this.gradYear = this.profile.gradYear - this.description = this.profile.description + this.picture = this.profile.picture + this.degree = this.profile.degree + this.gradYear = this.profile.gradYear + this.description = this.profile.description } } } From 4054dd33fcc033da7cdf5756905fdced69bd40b4 Mon Sep 17 00:00:00 2001 From: Nuno Das Neves Date: Wed, 10 Oct 2018 16:12:02 +1100 Subject: [PATCH 07/25] working profile update --- backend/src/controllers/user.js | 8 ++---- backend/src/models/db/tables.js | 2 +- backend/src/models/user.js | 6 ++--- backend/tests/models/user.js | 45 ++++++++++++++++++++++++++++++++ frontend/src/store/auth/index.js | 16 +++++++++++- frontend/src/utils/api/auth.js | 7 ++++- frontend/src/views/Profile.vue | 10 ++++--- 7 files changed, 78 insertions(+), 16 deletions(-) create mode 100644 backend/tests/models/user.js diff --git a/backend/src/controllers/user.js b/backend/src/controllers/user.js index 71e572ea..f9062976 100644 --- a/backend/src/controllers/user.js +++ b/backend/src/controllers/user.js @@ -13,12 +13,8 @@ exports.getUser = function({id}, res) { * provide frontend with any user specific data */ exports.getSelf = function(req, res) { - const errorResponse = errorHandler(res) - if (!req.user) { - return errorResponse({ message: 'Invalid Credentials' }) - } - // fine for now, should include profile data - return res.json(req.user) + return responseHandler(userModel.getProfile(req.user.id), res) + .catch(errorHandler(res)) } exports.createUser = function({ authorized: { email, uid }, body: { displayName } }, res) { diff --git a/backend/src/models/db/tables.js b/backend/src/models/db/tables.js index 81ade4d7..d9240c03 100644 --- a/backend/src/models/db/tables.js +++ b/backend/src/models/db/tables.js @@ -12,7 +12,7 @@ function createUserTable (db) { joined TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, reputation INTEGER DEFAULT '0', degree TEXT, - gradYear TIMESTAMP DEFAULT '2018', + gradYear TIMESTAMP, description TEXT, picture TEXT )`, diff --git a/backend/src/models/user.js b/backend/src/models/user.js index feaef3f3..917725c7 100644 --- a/backend/src/models/user.js +++ b/backend/src/models/user.js @@ -11,7 +11,7 @@ class User { */ getProfile(id) { return this.db - .query('SELECT id, email, displayName, joined, degree, gradYear, description, picture, joined FROM user WHERE id=?', [id]) + .query('SELECT id, email, displayName, degree, gradYear, description, picture, joined FROM user WHERE id=?', [id]) } /** @@ -22,11 +22,11 @@ class User { */ getPublicProfile(id) { return this.db - .query('SELECT id, displayName, joined, degree, gradYear, description, picture FROM user WHERE id=?', [id]) + .query('SELECT id, displayName, degree, gradYear, description, picture, joined FROM user WHERE id=?', [id]) } /** - * Alternative getter to get the relevant user's details + * Get all users details by UID. Used by authentication system * @param {uid} uid uid string * @returns {object} user object */ diff --git a/backend/tests/models/user.js b/backend/tests/models/user.js new file mode 100644 index 00000000..43ea77fb --- /dev/null +++ b/backend/tests/models/user.js @@ -0,0 +1,45 @@ +const { expect } = require('chai') +// use an actual database +const userModel = require('../../src/models/user')(require('../../src/models/db')) + +describe('User Model', () => { + + it('compiles', function () { + expect(userModel) + }) + + describe('User creation and retrieval', () => { + const addUserObj = { displayName: 'mickey', email: 'potato@potatoman.potato', uid: '1234' } + const retrieveUserObj = { email: addUserObj.email, displayName: addUserObj.displayName, degree:null, gradYear:null, description:null, picture:null } + it('it creates a user and retrieves the profile', () => { + return userModel + .createUser(addUserObj) + .then(({ displayName, email, id }) => { + expect(displayName).to.equal(addUserObj.displayName) + expect(email).to.equal(addUserObj.email) + return userModel.getProfile(id) + }) + .then(({ email, displayName, degree, gradYear, description, picture }) => { + const profile = { email, displayName, degree, gradYear, description, picture } + expect(profile).to.deep.equal(retrieveUserObj) + }) + }) + }) + + describe('User update', () => { + const credsObj = { displayName: 'mickeyMoo', email: 'tomatoboy@hotmail.zomg' } + const addUserObj = { ...credsObj, uid: '4321' } + const updateObj = { degree: "BS in CS", gradYear: 2026, description: "testu", picture: "baaaaaa" } + it('it updates the profile correctly', () => { + return userModel + .createUser(addUserObj) + .then(({ id, description }) => { + return userModel.updateUser(id, updateObj) + }) + .then(({ email, displayName, degree, gradYear, description, picture }) => { + const profile = { email, displayName, degree, gradYear, description, picture } + expect(profile).to.deep.equal({ ...credsObj, ...updateObj }) + }) + }) + }) +}) diff --git a/frontend/src/store/auth/index.js b/frontend/src/store/auth/index.js index 9240fac5..91fa8a1d 100644 --- a/frontend/src/store/auth/index.js +++ b/frontend/src/store/auth/index.js @@ -1,7 +1,7 @@ // firebase authentication class import auth from './config' -import { createProfile, getSelf } from '@/utils/api/auth' +import { createProfile, updateProfile, getSelf } from '@/utils/api/auth' const state = { loading: false, @@ -90,6 +90,20 @@ const actions = { .finally(() => commit('SET_LOADING', false)) }, + /** + * Update user profile + **/ + updateProfile({ commit }, { user, data }) { + commit('SET_LOADING', true) + return updateProfile(user, data) + .then((profile) => commit('SET_PROFILE', profile, { root: true })) + .catch(error => { + commit('ERROR', error.message) + throw error + }) + .finally(() => commit('SET_LOADING', false)) + }, + /** * Called on application boot once firebase has been initialised */ diff --git a/frontend/src/utils/api/auth.js b/frontend/src/utils/api/auth.js index 4ceb2843..559226dd 100644 --- a/frontend/src/utils/api/auth.js +++ b/frontend/src/utils/api/auth.js @@ -1,4 +1,4 @@ -import { get, post } from '.' +import { get, post, put } from '.' /** * Add authtoken to request @@ -36,6 +36,11 @@ export function createProfile(user, data) { .then(options => post('/user', { ...options, data })) } +export function updateProfile(user, data) { + return getAuthHeaders(user) + .then(options => put('/user', { ...options, data })) +} + /** * Return public information for a user * @param {*} id The public user id diff --git a/frontend/src/views/Profile.vue b/frontend/src/views/Profile.vue index 9fc57589..ecaea5ce 100644 --- a/frontend/src/views/Profile.vue +++ b/frontend/src/views/Profile.vue @@ -21,7 +21,7 @@
- Click here to create a profile. + Click here to create a profile.
@@ -31,9 +31,9 @@ diff --git a/frontend/src/store/auth/index.js b/frontend/src/store/auth/index.js index 85a23f06..30256099 100644 --- a/frontend/src/store/auth/index.js +++ b/frontend/src/store/auth/index.js @@ -116,16 +116,23 @@ const actions = { }, reject) }) .then((user) => { - if (user === null) throw Error('Not logged in') commit('SET_USER', user, { root: true }) - return getSelf(user) + }) + .finally(() => { + commit('SET_LOADING', false) }) + }, + + getProfile({ commit, rootState }) { + commit('SET_LOADING', true) + return getSelf(rootState.userAuthObject) .then(profile => { commit('SET_PROFILE', profile, { root: true }) }) .catch(error => { commit('ERROR', error.message) commit('SET_PROFILE', {}, { root: true }) + // idk if this can happen but meh if (!error.code || error.code !== 403) { auth.signOut() commit('SET_USER', {}, { root: true }) diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue index acbb27f2..b2c58261 100644 --- a/frontend/src/views/Login.vue +++ b/frontend/src/views/Login.vue @@ -9,8 +9,7 @@ :link="{ text: 'Forgot your password?', name: 'Forgot Password' - }" - > + }"> From 300d3a71f635b9c9b50518d86204c0c10a2ef2e4 Mon Sep 17 00:00:00 2001 From: Nuno Das Neves Date: Thu, 11 Oct 2018 23:07:20 +1100 Subject: [PATCH 16/25] reverted some stuff ... working again --- frontend/src/App.vue | 1 - frontend/src/store/auth/index.js | 11 ++--------- frontend/src/views/Login.vue | 1 + 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 3753b9b8..5b86d104 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -22,7 +22,6 @@ export default { }, created() { this.$store.dispatch('auth/checkAuth') - .then(() => this.$store.dispatch('auth/getProfile')) this.$store.dispatch('subject/getSubjects') } } diff --git a/frontend/src/store/auth/index.js b/frontend/src/store/auth/index.js index 30256099..85a23f06 100644 --- a/frontend/src/store/auth/index.js +++ b/frontend/src/store/auth/index.js @@ -116,23 +116,16 @@ const actions = { }, reject) }) .then((user) => { + if (user === null) throw Error('Not logged in') commit('SET_USER', user, { root: true }) - }) - .finally(() => { - commit('SET_LOADING', false) + return getSelf(user) }) - }, - - getProfile({ commit, rootState }) { - commit('SET_LOADING', true) - return getSelf(rootState.userAuthObject) .then(profile => { commit('SET_PROFILE', profile, { root: true }) }) .catch(error => { commit('ERROR', error.message) commit('SET_PROFILE', {}, { root: true }) - // idk if this can happen but meh if (!error.code || error.code !== 403) { auth.signOut() commit('SET_USER', {}, { root: true }) diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue index b2c58261..a7f885b5 100644 --- a/frontend/src/views/Login.vue +++ b/frontend/src/views/Login.vue @@ -42,6 +42,7 @@ export default { clickHandler() { const { email, password } = this this.$store.dispatch('auth/signIn', { email, password }) + .catch(()=>{}) .then(() => { if (this.isLoggedIn) { if (this.hasProfile) { From 08b0a3550abce3421116d62b04b7c7f82fd8cfac Mon Sep 17 00:00:00 2001 From: Nuno Das Neves Date: Fri, 12 Oct 2018 14:45:51 +1100 Subject: [PATCH 17/25] better auth code, profile cache, global loading spinner in nav, reroutes --- frontend/src/components/AppNavBar.vue | 40 +++-- frontend/src/store/auth/index.js | 201 ++++++++++++++++++-------- frontend/src/store/root.js | 26 +--- frontend/src/views/Login.vue | 30 ++-- frontend/src/views/Profile.vue | 22 ++- frontend/src/views/SignUp.vue | 24 ++- 6 files changed, 206 insertions(+), 137 deletions(-) diff --git a/frontend/src/components/AppNavBar.vue b/frontend/src/components/AppNavBar.vue index 4f3faa4e..5cbbd273 100644 --- a/frontend/src/components/AppNavBar.vue +++ b/frontend/src/components/AppNavBar.vue @@ -1,22 +1,22 @@ @@ -28,8 +28,14 @@ import { mapGetters } from 'vuex' export default { components: { Search }, computed: { - ...mapGetters([ 'isLoggedIn', 'hasProfile' ]), - ...mapGetters('auth', [ 'loading' ]) + ...mapGetters([ 'loading' ]), + ...mapGetters('auth', [ 'isFirebaseAuthorised', 'isLoggedIn', 'hasProfile' ]) + }, + methods: { + logout() { + this.$store.dispatch('auth/logout') + //.then(() => this.$router.push("/")) + } } } @@ -47,6 +53,12 @@ export default { font-size: var(--font-small); } +.logo-span { + display: flex; + flex-direction: row; + justify-content: start; +} + .links { /* should be flex will require less hacks */ display: flex; diff --git a/frontend/src/store/auth/index.js b/frontend/src/store/auth/index.js index 85a23f06..38868186 100644 --- a/frontend/src/store/auth/index.js +++ b/frontend/src/store/auth/index.js @@ -5,10 +5,23 @@ import { createProfile, updateProfile, getSelf } from '@/utils/api/auth' const state = { loading: false, - error: '' + error: '', + // firebase authObject + userAuthObject: null, + // our own user data + profile: null } const getters = { +// logged into firebase (authenticated account) + isFirebaseAuthorised: ({ userAuthObject }) => !!userAuthObject, + // TODO not sure if this is useful... + hasProfile: ({ profile, userAuthObject }) => !!profile, + // logged into backend (existing profile) and authed with firebase + isLoggedIn: ({ profile, userAuthObject }) => !!profile && !!userAuthObject, + + profile: ({ profile }) => profile, + userAuthObject: ({ userAuthObject }) => userAuthObject, loading: ({ loading }) => loading, error: ({ error }) => error } @@ -20,31 +33,51 @@ const mutations = { }, SET_LOADING(state, isLoading) { state.loading = isLoading + }, + /** + * @param {*} state The root state + * @param {*} user The logged in user object or null + */ + SET_USER(state, user) { + state.userAuthObject = user + }, + /** + * @param {*} state The root state + * @param {*} profile The user's profile object + */ + SET_PROFILE(state, profile) { + // anytime we want to set the profile, we want to update the local storage too + if (profile) { + localStorage.setItem('PROFILE_KEY', JSON.stringify(profile)) + } else { + localStorage.removeItem('PROFILE_KEY') + } + state.profile = profile } } /* TODO CHANGE THESE TO ASYNC */ /* successful signIn returns an UserAuth object which has field user */ const actions = { - signIn({ commit }, { email, password }) { + async signIn({ commit, dispatch, state }, { email, password }) { + commit('SET_LOADING', true) - return auth.signInWithEmailAndPassword(email, password) - .then(({ user }) => { - commit('SET_USER', user, { root: true }) - return getSelf(user) - }) - .then(profile => { - commit('SET_PROFILE', profile, { root: true }) - }) - .catch(error => { - commit('ERROR', error.message) - commit('SET_PROFILE', {}, { root: true }) - if (!error.code || error.code !== 403) { - auth.signOut() - commit('SET_USER', {}, { root: true }) - } - }) - .finally(() => commit('SET_LOADING', false)) + try { + // this returns the 'usercredential' which is not the user object -.- + const { user } = await auth.signInWithEmailAndPassword(email, password) + commit('SET_USER', user) + } catch (error) { + commit('ERROR', error.message) + } + + // no firebase auth, just get outta here + if (!state.userAuthObject) { + commit('SET_LOADING', false) + return + } + + await dispatch('getProfile') + commit('SET_LOADING', false) }, /** @@ -52,12 +85,13 @@ const actions = { * firebase for this. */ logout({ commit }) { + commit('SET_LOADING', true) return auth.signOut() .then(() => { - commit('SET_USER', {}, { root: true }) - commit('SET_PROFILE', {}, { root: true }) + commit('SET_USER', null) + commit('SET_PROFILE', null) }) - .catch(error => commit('ERROR', error.message)) + .finally(() => commit('SET_LOADING', false)) }, /** @@ -68,72 +102,111 @@ const actions = { createAccount({ commit }, { email, password }) { commit('SET_LOADING', true) return auth.createUserWithEmailAndPassword(email, password) - .then(({ user }) => commit('SET_USER', user, { root: true })) - .catch(error => { - commit('ERROR', error.message) - throw error - }) + .then(({ user }) => commit('SET_USER', user)) + .catch(error => commit('ERROR', error.message)) .finally(() => commit('SET_LOADING', false)) }, /** * Create user profile in the backend + * Assume existing user **/ - createProfile({ commit }, { user, displayName }) { + createProfile({ commit, state }, { displayName }) { commit('SET_LOADING', true) - return createProfile(user, { displayName }) - .then((profile) => commit('SET_PROFILE', profile, { root: true })) - .catch(error => { - commit('ERROR', error.message) - throw error - }) + return createProfile(state.userAuthObject, { displayName }) + .then((profile) => commit('SET_PROFILE', profile)) + .catch(error => commit('ERROR', error.message)) .finally(() => commit('SET_LOADING', false)) }, /** * Update user profile + * Assume existing user **/ - updateProfile({ commit }, { user, data }) { + updateProfile({ commit }, { data }) { commit('SET_LOADING', true) - return updateProfile(user, data) - .then((profile) => commit('SET_PROFILE', profile, { root: true })) - .catch(error => { - commit('ERROR', error.message) - throw error - }) + return updateProfile(state.userAuthObject, data) + .then((profile) => commit('SET_PROFILE', profile)) + .catch(error => commit('ERROR', error.message)) .finally(() => commit('SET_LOADING', false)) }, + /* + * Get profile from backend and put it in the store. + * Assumes valid firebase token in the store + */ + async getProfile({ commit, state }) { + const oldLoading = state.loading + commit('SET_LOADING', true) + + try { + const profile = await getSelf(state.userAuthObject) + // success! + commit('SET_PROFILE', profile) + } catch (error) { + // abort! abort! + commit('ERROR', error.message) + commit('SET_PROFILE', null) + // if there's a 403 error code, it means it's a valid account but no profile exists yet + // otherwise, completely abort auth + if (!error.code || error.code !== 403) { + commit('SET_USER', null) + await auth.signOut() + } + } finally { + // restore loading state + commit('SET_LOADING', oldLoading) + } + }, + /** * Called on application boot once firebase has been initialised + * Logs into firebase and retrieves the profile + * If anything fails it clears everything */ - checkAuth({ commit }) { + async checkAuth({ commit, dispatch, state }) { commit('SET_LOADING', true) - return new Promise((resolve, reject) => { - const unsubscribe = auth.onAuthStateChanged(user => { - unsubscribe() - resolve(user) - }, reject) - }) - .then((user) => { - if (user === null) throw Error('Not logged in') - commit('SET_USER', user, { root: true }) - return getSelf(user) + try { + // returns user object + const user = await new Promise((resolve, reject) => { + // add a handler for change in signin state + const unsubscribe = auth.onAuthStateChanged(user => { + // unsubscribe this function, so it no longer handles the state change + unsubscribe() + // return the user auth object, or null if not authorized + resolve(user) + }, reject) }) - .then(profile => { - commit('SET_PROFILE', profile, { root: true }) - }) - .catch(error => { - commit('ERROR', error.message) - commit('SET_PROFILE', {}, { root: true }) - if (!error.code || error.code !== 403) { - auth.signOut() - commit('SET_USER', {}, { root: true }) - } - }) - .finally(() => commit('SET_LOADING', false)) + // put it in the store + commit('SET_USER', user) + } catch (error) { + commit('ERROR', error.message) + } + + // no firebase auth, just get outta here + if (!state.userAuthObject) { + commit('SET_LOADING', false) + return + } + + // get existing profile information if cached + const existing = localStorage.getItem('PROFILE_KEY') + if (existing) { + const parsed = JSON.parse(existing) + // existing profile data means the user didn't log out + commit('SET_PROFILE', parsed) + commit('SET_LOADING', false) + return + } + + // we're authorized with firebase but there's no valid profile info yet + await dispatch('getProfile') + commit('SET_LOADING', false) }, + /** + * simply send a password reset email... no need to be authorised or anything + */ sendPasswordResetEmail({ commit }, { email }) { commit('SET_LOADING', true) return auth.sendPasswordResetEmail(email) diff --git a/frontend/src/store/root.js b/frontend/src/store/root.js index 06a86f18..3247d800 100644 --- a/frontend/src/store/root.js +++ b/frontend/src/store/root.js @@ -2,24 +2,14 @@ import { get } from '../utils/api' /* root application state */ const state = { - loading: false, error: '', // cached courses - courses: [], - // firebase authObject - userAuthObject: {}, - // our own user data - profile: {} + courses: [] } const getters = { - authObject: ({ userAuthObject }) => userAuthObject, - // logged into firebase (authenticated account) - isLoggedIn: ({ userAuthObject }) => !!Object.keys(userAuthObject).length, - // logged into backend (existing profile) - hasProfile: ({ profile, userAuthObject }) => !!Object.keys(profile).length && !!Object.keys(userAuthObject).length, - profile: ({ profile }) => profile, - loading: ({ loading }) => loading, + loading: ({ auth, course, questions, reviews, subject }) => + [auth.loading, course.loading, questions.loading, reviews.loading, subject.loading].reduce((acc, curr) => acc || curr, false), error: ({ error }) => error } @@ -42,16 +32,6 @@ const mutations = { }, POPULATE_SEARCH(state, data) { state.courses = data - }, - /** - * @param {*} state The root state - * @param {*} user The logged in user object or null - */ - SET_USER(state, user) { - state.userAuthObject = user || {} - }, - SET_PROFILE(state, profile) { - state.profile = profile || {} } } diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue index a7f885b5..970368b1 100644 --- a/frontend/src/views/Login.vue +++ b/frontend/src/views/Login.vue @@ -13,9 +13,6 @@ -
- You are already signed in. -
@@ -35,23 +32,26 @@ export default { }, components: { AppAuthForm, AuthInput }, computed: { - ...mapGetters('auth', [ 'error', 'loading' ]), - ...mapGetters([ 'isLoggedIn', 'hasProfile', 'authObject' ]) + ...mapGetters('auth', [ 'loading', 'error', 'isFirebaseAuthorised', 'isLoggedIn', 'hasProfile' ]) + }, + // reroute whenever auth loading state changes + watch: { + loading() { this.reroute() } }, methods: { clickHandler() { const { email, password } = this this.$store.dispatch('auth/signIn', { email, password }) - .catch(()=>{}) - .then(() => { - if (this.isLoggedIn) { - if (this.hasProfile) { - this.$router.push('/') - } else { - this.$router.push('/signup') - } - } - }) + }, + reroute() { + // we need to check the store to determine state + if (this.isFirebaseAuthorised) { + if (this.hasProfile) { + this.$router.push('/') + } else { + this.$router.push('/signup') + } + } } }, created() { diff --git a/frontend/src/views/Profile.vue b/frontend/src/views/Profile.vue index e58c6231..9dd1b960 100644 --- a/frontend/src/views/Profile.vue +++ b/frontend/src/views/Profile.vue @@ -3,7 +3,7 @@
-
+
@@ -21,9 +21,6 @@
-
- Click here to create a profile. -
@@ -48,8 +45,7 @@ export default { } }, computed: { - ...mapGetters([ 'isLoggedIn', 'hasProfile', 'profile', 'authObject' ]), - ...mapGetters('auth', [ 'loading', 'error' ]) + ...mapGetters('auth', [ 'loading', 'error', 'isFirebaseAuthorised', 'isLoggedIn', 'hasProfile', 'profile' ]) }, watch: { hasProfile (oldState, newState) { @@ -59,12 +55,22 @@ export default { this.gradYear = this.profile.gradYear this.description = this.profile.description } - } + }, + loading() { this.reroute() } }, methods: { updateProfile() { const data = { picture: this.picture, degree: this.degree, gradYear: this.gradYear, description: this.description } - this.$store.dispatch('auth/updateProfile', { user: this.authObject, data }) + this.$store.dispatch('auth/updateProfile', { data }) + }, + reroute() { + if (this.isFirebaseAuthorised) { + if (!this.hasProfile) { + this.$router.push('/signup') + } + } else { + this.$router.push('/login') + } } }, mounted() { diff --git a/frontend/src/views/SignUp.vue b/frontend/src/views/SignUp.vue index 63ce335f..8da305bb 100644 --- a/frontend/src/views/SignUp.vue +++ b/frontend/src/views/SignUp.vue @@ -1,6 +1,6 @@ @@ -48,22 +45,23 @@ export default { }, components: { AppAuthForm, AuthInput }, computed: { - ...mapGetters('auth', [ 'error', 'loading' ]), - ...mapGetters([ 'isLoggedIn', 'hasProfile', 'authObject' ]) + ...mapGetters('auth', [ 'loading', 'error', 'isFirebaseAuthorised', 'isLoggedIn', 'hasProfile' ]) + }, + // reroute whenever auth loading state changes + watch: { + loading() { this.reroute() } }, methods: { createAccount() { const { email, password } = this this.$store.dispatch('auth/createAccount', { email, password }) - .catch(e => {}) }, createProfile() { const { displayName } = this - this.$store.dispatch('auth/createProfile', { user: this.authObject, displayName }) - .then(() => { - if (this.isLoggedIn && this.hasProfile) this.$router.push('/') - }) - .catch(e => {}) + this.$store.dispatch('auth/createProfile', { displayName }) + }, + reroute() { + if (this.isLoggedIn) this.$router.push('/') } }, created() { From 0ebe595d6b1dce27b3255bee2f02e4baec09dadd Mon Sep 17 00:00:00 2001 From: Nuno Das Neves Date: Fri, 12 Oct 2018 14:49:40 +1100 Subject: [PATCH 18/25] lint --- frontend/src/components/AppNavBar.vue | 4 ++-- frontend/src/store/auth/index.js | 1 - frontend/src/views/Login.vue | 2 +- frontend/src/views/SignUp.vue | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/AppNavBar.vue b/frontend/src/components/AppNavBar.vue index 5cbbd273..9cfa49d8 100644 --- a/frontend/src/components/AppNavBar.vue +++ b/frontend/src/components/AppNavBar.vue @@ -33,8 +33,8 @@ export default { }, methods: { logout() { - this.$store.dispatch('auth/logout') - //.then(() => this.$router.push("/")) + this.$store.dispatch('auth/logout') + // .then(() => this.$router.push("/")) } } } diff --git a/frontend/src/store/auth/index.js b/frontend/src/store/auth/index.js index 38868186..748f7114 100644 --- a/frontend/src/store/auth/index.js +++ b/frontend/src/store/auth/index.js @@ -60,7 +60,6 @@ const mutations = { /* successful signIn returns an UserAuth object which has field user */ const actions = { async signIn({ commit, dispatch, state }, { email, password }) { - commit('SET_LOADING', true) try { // this returns the 'usercredential' which is not the user object -.- diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue index 970368b1..e1776d0b 100644 --- a/frontend/src/views/Login.vue +++ b/frontend/src/views/Login.vue @@ -36,7 +36,7 @@ export default { }, // reroute whenever auth loading state changes watch: { - loading() { this.reroute() } + loading() { this.reroute() } }, methods: { clickHandler() { diff --git a/frontend/src/views/SignUp.vue b/frontend/src/views/SignUp.vue index 8da305bb..00cc4261 100644 --- a/frontend/src/views/SignUp.vue +++ b/frontend/src/views/SignUp.vue @@ -49,7 +49,7 @@ export default { }, // reroute whenever auth loading state changes watch: { - loading() { this.reroute() } + loading() { this.reroute() } }, methods: { createAccount() { From eab32304c89bdd792409746fb5895e48a712e353 Mon Sep 17 00:00:00 2001 From: Nuno Das Neves Date: Fri, 12 Oct 2018 15:56:17 +1100 Subject: [PATCH 19/25] page size fixaroo --- backend/src/controllers/course.js | 22 ++++++++++++---------- backend/src/models/review.js | 6 +++--- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/backend/src/controllers/course.js b/backend/src/controllers/course.js index 87275986..d90abe89 100644 --- a/backend/src/controllers/course.js +++ b/backend/src/controllers/course.js @@ -19,20 +19,21 @@ exports.getCourse = function ({ params }, res) { /* Get all questions for a course */ exports.getCourseQuestions = function ({ params, query }, res) { - let p = parseInt(query.p) - const pageNumber = p || 1 + const pageNumber = parseInt(query.p) || 1 + // TODO get page size from query + const pageSize = PAGE_SIZE const getCourseQuestions = Promise.all([ - questionModel.getQuestions(params.code, pageNumber, PAGE_SIZE), + questionModel.getQuestions(params.code, pageNumber, pageSize), questionModel.getQuestionCount(params.code) ]) .then(function(values) { - const lastPage = Math.trunc((values[1][0]['COUNT()'] + PAGE_SIZE) / PAGE_SIZE) + const lastPage = Math.trunc((values[1][0]['COUNT()'] + pageSize) / pageSize) return { 'meta': { 'curr': pageNumber, 'last': lastPage, - 'PAGE_SIZE': PAGE_SIZE + 'pageSize': pageSize }, 'data': values[0] } @@ -44,20 +45,21 @@ exports.getCourseQuestions = function ({ params, query }, res) { /* Get all reviews for a course */ exports.getCourseReviews = function ({ params, query }, res) { - let p = parseInt(query.p) - const pageNumber = p || 1 + const pageNumber = parseInt(query.p) || 1 + // TODO get page size from query + const pageSize = PAGE_SIZE const getCourseReviews = new Promise((resolve, reject) => { Promise.all([ - reviewModel.getReviews(params.code, pageNumber, PAGE_SIZE), + reviewModel.getReviews(params.code, pageNumber, pageSize), reviewModel.getReviewCount(params.code) ]).then(function(values) { - const lastPage = Math.trunc((values[1][0]['COUNT()'] + PAGE_SIZE) / PAGE_SIZE) + const lastPage = Math.trunc((values[1][0]['COUNT()'] + pageSize) / pageSize) resolve({ 'meta': { 'curr': pageNumber, 'last': lastPage, - 'PAGE_SIZE': PAGE_SIZE + 'pageSize':pageSize }, 'data': values[0] }) diff --git a/backend/src/models/review.js b/backend/src/models/review.js index 154f6496..84004e27 100644 --- a/backend/src/models/review.js +++ b/backend/src/models/review.js @@ -23,11 +23,11 @@ class Review { * @param {number} pageNumber The page number for which we want to get questions. * @returns {Array} */ - getReviews(code, pageNumber, PAGE_SIZE) { - const offset = (PAGE_SIZE * pageNumber) - PAGE_SIZE + getReviews(code, pageNumber, pageSize) { + const offset = (pageSize * pageNumber) - pageSize return this.db .queryAll('SELECT * FROM review WHERE code=? ORDER BY timestamp DESC LIMIT ?, ?', - [code, offset, PAGE_SIZE]) + [code, offset, pageSize]) } /** From 0621fe2094fb7bef648ddac6c8aa48bb012d3a88 Mon Sep 17 00:00:00 2001 From: Alex Hinds Date: Fri, 12 Oct 2018 17:12:25 +1100 Subject: [PATCH 20/25] I'v messed something up --- frontend/src/router/index.js | 31 ++++++++++++++++++++ frontend/src/{router.js => router/routes.js} | 9 ++---- 2 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 frontend/src/router/index.js rename frontend/src/{router.js => router/routes.js} (97%) diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 00000000..53a6bf03 --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,31 @@ +import routes from '.' +import store from '../store' +import Vue from 'vue' +import Router from 'vue-router' + +Vue.use(Router) + +// requires meta field to be be added to routes object +const router = new Router(routes) + +router.beforeEach((to, from, next) => { + const authState = store.getters['auth/isLoggedIn'] + + if (to.matched.some(record => record.meta.requiresAuth)) { + if (authState) { + next() + } else { + next('/login') + } + } else if (to.matched.some(record => record.meta.preAuth)) { + if (!authState) { + next() + } else { + next('/subjects') + } + } else { + next() + } +}) + +export default router diff --git a/frontend/src/router.js b/frontend/src/router/routes.js similarity index 97% rename from frontend/src/router.js rename to frontend/src/router/routes.js index ece4dc26..d5c759c4 100644 --- a/frontend/src/router.js +++ b/frontend/src/router/routes.js @@ -1,8 +1,5 @@ -import Vue from 'vue' -import Router from 'vue-router' -import Home from './views/Home' -Vue.use(Router) +import Home from './views/Home' const questionView = () => import('./views/Question') const reviewView = () => import('./views/Review') @@ -14,7 +11,7 @@ const subjectList = () => import('./views/SubjectList') const subjectCourses = () => import('./views/SubjectCourses') const ErrorPage = () => import('./views/404') -export default new Router({ +export default { mode: 'history', scrollBehavior (to, from, savedPosition) { // scroll to top of page on following a route, unless history state used @@ -135,4 +132,4 @@ export default new Router({ component: ErrorPage } ] -}) +} From 2678b5aaaf77a61fd5116bf879ea02a3cf471076 Mon Sep 17 00:00:00 2001 From: Alex Hinds Date: Fri, 12 Oct 2018 17:14:36 +1100 Subject: [PATCH 21/25] protecting routes with guards --- frontend/src/router/index.js | 9 +------- frontend/src/router/routes.js | 39 +++++++++++++++++++---------------- 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js index 53a6bf03..d3aa4b65 100644 --- a/frontend/src/router/index.js +++ b/frontend/src/router/index.js @@ -1,12 +1,5 @@ -import routes from '.' +import router from './routes' import store from '../store' -import Vue from 'vue' -import Router from 'vue-router' - -Vue.use(Router) - -// requires meta field to be be added to routes object -const router = new Router(routes) router.beforeEach((to, from, next) => { const authState = store.getters['auth/isLoggedIn'] diff --git a/frontend/src/router/routes.js b/frontend/src/router/routes.js index d5c759c4..d7f668ac 100644 --- a/frontend/src/router/routes.js +++ b/frontend/src/router/routes.js @@ -1,17 +1,20 @@ +import Vue from 'vue' +import Router from 'vue-router' +import Home from '../views/Home' -import Home from './views/Home' +Vue.use(Router) -const questionView = () => import('./views/Question') -const reviewView = () => import('./views/Review') -const newQuestionView = () => import('./views/NewQuestion') -const newReviewView = () => import('./views/NewReview') -const courseQuestions = () => import('./views/course/CourseQuestions') -const courseReviews = () => import('./views/course/CourseReviews') -const subjectList = () => import('./views/SubjectList') -const subjectCourses = () => import('./views/SubjectCourses') -const ErrorPage = () => import('./views/404') +const questionView = () => import('../views/Question') +const reviewView = () => import('../views/Review') +const newQuestionView = () => import('../views/NewQuestion') +const newReviewView = () => import('../views/NewReview') +const courseQuestions = () => import('../views/course/CourseQuestions') +const courseReviews = () => import('../views/course/CourseReviews') +const subjectList = () => import('../views/SubjectList') +const subjectCourses = () => import('../views/SubjectCourses') +const ErrorPage = () => import('../views/404') -export default { +export default new Router({ mode: 'history', scrollBehavior (to, from, savedPosition) { // scroll to top of page on following a route, unless history state used @@ -66,7 +69,7 @@ export default { // route level code-splitting // this generates a separate chunk (course.[hash].js) for this route // which is lazy-loaded when the route is visited. - component: () => import('./views/course') + component: () => import('../views/course') }, { path: '/course/:code([\\w]{8})/question/new', @@ -105,26 +108,26 @@ export default { { path: '/signup', name: 'Sign Up', - component: () => import('./views/SignUp') + component: () => import('../views/SignUp') }, { path: '/login', name: 'Login', - component: () => import('./views/Login') + component: () => import('../views/Login') }, { path: '/profile', name: 'Profile', - component: () => import('./views/Profile') + component: () => import('../views/Profile') }, { path: '/password-reset', name: 'Forgot Password', - component: () => import('./views/ForgotPassword') + component: () => import('../views/ForgotPassword') }, { path: '/fonts', - component: () => import('./views/Design') + component: () => import('../views/Design') }, { // match any other route @@ -132,4 +135,4 @@ export default { component: ErrorPage } ] -} +}) From 1b0ae62b264354fb99f98b77611b5b6c35091e48 Mon Sep 17 00:00:00 2001 From: Nuno Das Neves Date: Fri, 12 Oct 2018 17:31:49 +1100 Subject: [PATCH 22/25] fixed some PR things --- frontend/src/components/AppNavBar.vue | 4 +- frontend/src/router.js | 5 +++ frontend/src/views/CreateProfile.vue | 53 +++++++++++++++++++++++++++ frontend/src/views/ForgotPassword.vue | 1 - frontend/src/views/Login.vue | 4 +- frontend/src/views/Onboarding.vue | 4 -- frontend/src/views/Profile.vue | 2 +- frontend/src/views/SignUp.vue | 22 ++--------- 8 files changed, 66 insertions(+), 29 deletions(-) create mode 100644 frontend/src/views/CreateProfile.vue delete mode 100644 frontend/src/views/Onboarding.vue diff --git a/frontend/src/components/AppNavBar.vue b/frontend/src/components/AppNavBar.vue index 9cfa49d8..1fb83573 100644 --- a/frontend/src/components/AppNavBar.vue +++ b/frontend/src/components/AppNavBar.vue @@ -5,7 +5,7 @@ - +