@@ -298,6 +298,11 @@
// call the method again if the route changes
'$route': 'doStuffAfterRoute'
},
+ computed: {
+ userInfo() {
+ return auth.getUserInfos()
+ }
+ },
methods: {
logout() {
auth.logout()
@@ -313,7 +318,7 @@
})
},
loadNamespacesIfNeeded(e) {
- if (auth.user.authenticated && auth.user.infos.type === authTypes.USER && (e.name === 'home' || this.namespaces.length === 0)) {
+ if (auth.user.authenticated && (this.userInfo && this.userInfo.type === authTypes.USER) && (e.name === 'home' || this.namespaces.length === 0)) {
this.loadNamespaces()
}
},
diff --git a/src/auth/index.js b/src/auth/index.js
index f1ed34bc..9300f1fb 100644
--- a/src/auth/index.js
+++ b/src/auth/index.js
@@ -11,13 +11,19 @@ export default {
infos: {},
},
- login(context, creds, redirect) {
+ login(context, credentials, redirect = '') {
localStorage.removeItem('token') // Delete an eventually preexisting old token
- HTTP.post('login', {
- username: creds.username,
- password: creds.password
- })
+ const data = {
+ username: credentials.username,
+ password: credentials.password
+ }
+
+ if(credentials.totpPasscode) {
+ data.totp_passcode = credentials.totpPasscode
+ }
+
+ HTTP.post('login', data)
.then(response => {
// Save the token to local storage for later use
localStorage.setItem('token', response.data.token)
@@ -25,28 +31,28 @@ export default {
// Tell others the user is autheticated
this.user.authenticated = true
this.user.isLinkShareAuth = false
- const inf = this.getUserInfos()
- // eslint-disable-next-line
- console.log(inf)
-
- // Hide the loader
- context.loading = false
// Redirect if nessecary
- if (redirect) {
+ if (redirect !== '') {
router.push({name: redirect})
}
})
.catch(e => {
- // Hide the loader
- context.loading = false
if (e.response) {
+ if (e.response.data.code === 1017 && !credentials.totpPasscode) {
+ context.needsTotpPasscode = true
+ return
+ }
+
context.errorMsg = e.response.data.message
if (e.response.status === 401) {
context.errorMsg = 'Wrong username or password.'
}
}
})
+ .finally(() => {
+ context.loading = false
+ })
},
register(context, creds, redirect) {
diff --git a/src/components/user/Login.vue b/src/components/user/Login.vue
index 13607c43..157dc4c2 100644
--- a/src/components/user/Login.vue
+++ b/src/components/user/Login.vue
@@ -18,6 +18,12 @@
@@ -45,7 +51,8 @@
return {
errorMsg: '',
confirmedEmailSuccess: false,
- loading: false
+ loading: false,
+ needsTotpPasscode: false,
}
},
beforeMount() {
@@ -83,6 +90,10 @@
password: this.$refs.password.value,
}
+ if(this.needsTotpPasscode) {
+ credentials.totpPasscode = this.$refs.totpPasscode.value
+ }
+
auth.login(this, credentials, 'home')
}
}
diff --git a/src/components/user/Settings.vue b/src/components/user/Settings.vue
index d7c6d834..acb0efcb 100644
--- a/src/components/user/Settings.vue
+++ b/src/components/user/Settings.vue
@@ -1,5 +1,5 @@
-
+
+
+
+
Enroll
+
+
+ To finish your setup, use this secret in your totp app (Google Authenticator or similar): {{ totp.secret }}
+ After that, enter a code from your app below.
+
+
+ Alternatively you can scan this QR code:
+
+
+
+
Confirm
+
+
+
+ You've sucessfully set up two factor authentication!
+
+
+ Disable
+
+
+
+
+
@@ -83,6 +129,8 @@
import PasswordUpdateService from '../../services/passwordUpdateService'
import EmailUpdateService from '../../services/emailUpdate'
import EmailUpdateModel from '../../models/emailUpdate'
+ import TotpModel from '../../models/totp'
+ import TotpService from '../../services/totp'
export default {
name: 'Settings',
@@ -94,14 +142,40 @@
emailUpdateService: EmailUpdateService,
emailUpdate: EmailUpdateModel,
+
+ totpService: TotpService,
+ totp: TotpModel,
+ totpQR: '',
+ totpEnrolled: false,
+ totpConfirmPasscode: '',
+ totpDisableForm: false,
+ totpDisablePassword: '',
}
},
created() {
- this.passwordUpdate = new PasswordUpdateModel()
this.passwordUpdateService = new PasswordUpdateService()
+ this.passwordUpdate = new PasswordUpdateModel()
- this.emailUpdate = new EmailUpdateModel()
this.emailUpdateService = new EmailUpdateService()
+ this.emailUpdate = new EmailUpdateModel()
+
+ this.totpService = new TotpService()
+ this.totp = new TotpModel()
+
+ this.totpService.get()
+ .then(r => {
+ this.$set(this, 'totp', r)
+ this.totpSetQrCode()
+ })
+ .catch(e => {
+ // Error code 1016 means totp is not enabled, we don't need an error in that case.
+ if (e.response && e.response.data && e.response.data.code && e.response.data.code === 1016) {
+ this.totpEnrolled = false
+ return
+ }
+
+ this.error(e, this)
+ })
},
methods: {
updatePassword() {
@@ -123,6 +197,39 @@
})
.catch(e => this.error(e, this))
},
+ totpSetQrCode() {
+ this.totpService.qrcode()
+ .then(qr => {
+ const urlCreator = window.URL || window.webkitURL
+ this.totpQR = urlCreator.createObjectURL(qr)
+ })
+ },
+ totpEnroll() {
+ this.totpService.enroll()
+ .then(r => {
+ this.totpEnrolled = true
+ this.$set(this, 'totp', r)
+ this.totpSetQrCode()
+ })
+ .catch(e => this.error(e, this))
+ },
+ totpConfirm() {
+ this.totpService.enable({passcode: this.totpConfirmPasscode})
+ .then(() => {
+ this.$set(this.totp, 'enabled', true)
+ this.success({message: 'You\'ve successfully confirmed your totp setup and can use it from now on!'}, this)
+ })
+ .catch(e => this.error(e, this))
+ },
+ totpDisable() {
+ this.totpService.disable({password: this.totpDisablePassword})
+ .then(() => {
+ this.totpEnrolled = false
+ this.$set(this, 'totp', new TotpModel())
+ this.success({message: 'Two factor authentication was sucessfully disabled.'}, this)
+ })
+ .catch(e => this.error(e, this))
+ },
},
}
diff --git a/src/models/totp.js b/src/models/totp.js
new file mode 100644
index 00000000..17de13b6
--- /dev/null
+++ b/src/models/totp.js
@@ -0,0 +1,11 @@
+import AbstractModel from './abstractModel'
+
+export default class TotpModel extends AbstractModel {
+ defaults() {
+ return {
+ secret: '',
+ enabled: false,
+ url: '',
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/services/abstractService.js b/src/services/abstractService.js
index d531c605..f2c1f87a 100644
--- a/src/services/abstractService.js
+++ b/src/services/abstractService.js
@@ -386,19 +386,16 @@ export default class AbstractService {
}
/**
- * Performs a post request to the update url
+ * An abstract implementation to send post requests.
+ * Services can use this to implement functions to do post requests other than using the update method.
+ * @param url
* @param model
- * @returns {Q.Promise
}
+ * @returns {Q.Promise}
*/
- update(model) {
- if (this.paths.update === '') {
- return Promise.reject({message: 'This model is not able to update data.'})
- }
-
+ post(url, model) {
const cancel = this.setLoading()
- const finalUrl = this.getReplacedRoute(this.paths.update, model)
- return this.http.post(finalUrl, model)
+ return this.http.post(url, model)
.catch(error => {
return this.errorHandler(error)
})
@@ -410,6 +407,20 @@ export default class AbstractService {
})
}
+ /**
+ * Performs a post request to the update url
+ * @param model
+ * @returns {Q.Promise}
+ */
+ update(model) {
+ if (this.paths.update === '') {
+ return Promise.reject({message: 'This model is not able to update data.'})
+ }
+
+ const finalUrl = this.getReplacedRoute(this.paths.update, model)
+ return this.post(finalUrl, model)
+ }
+
/**
* Performs a delete request to the update url
* @param model
diff --git a/src/services/totp.js b/src/services/totp.js
new file mode 100644
index 00000000..148d51a3
--- /dev/null
+++ b/src/services/totp.js
@@ -0,0 +1,38 @@
+import AbstractService from './abstractService'
+import TotpModel from "../models/totp";
+
+export default class TotpService extends AbstractService {
+ urlPrefix = '/user/settings/totp'
+
+ constructor() {
+ super({})
+
+ this.paths.get = this.urlPrefix
+ }
+
+ modelFactory(data) {
+ return new TotpModel(data)
+ }
+
+ enroll() {
+ return this.post(`${this.urlPrefix}/enroll`, {})
+ }
+
+ enable(model) {
+ return this.post(`${this.urlPrefix}/enable`, model)
+ }
+
+ disable(model) {
+ return this.post(`${this.urlPrefix}/disable`, model)
+ }
+
+ qrcode() {
+ return this.http({
+ url: `${this.urlPrefix}/qrcode`,
+ method: 'GET',
+ responseType: 'blob',
+ }).then(response => {
+ return Promise.resolve(new Blob([response.data]))
+ })
+ }
+}
\ No newline at end of file