From 99c10d49be3956e2aeb79edb037e261095002780 Mon Sep 17 00:00:00 2001 From: konrad Date: Fri, 17 Apr 2020 23:46:07 +0000 Subject: [PATCH] TOTP (#109) Fix not telling the user about invalid totp passcodes when logging in Add disabling totp authentication Add totp passcode when logging in Add totp settings Add general post method function Co-authored-by: kolaente Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/109 --- src/App.vue | 13 ++-- src/auth/index.js | 34 ++++++---- src/components/user/Login.vue | 13 +++- src/components/user/Settings.vue | 113 ++++++++++++++++++++++++++++++- src/models/totp.js | 11 +++ src/services/abstractService.js | 29 +++++--- src/services/totp.js | 38 +++++++++++ 7 files changed, 220 insertions(+), 31 deletions(-) create mode 100644 src/models/totp.js create mode 100644 src/services/totp.js diff --git a/src/App.vue b/src/App.vue index 0704479fa..321eccce5 100644 --- a/src/App.vue +++ b/src/App.vue @@ -4,7 +4,7 @@
-
+
@@ -169,7 +169,7 @@
-
+
+
+ +
+ +
+
@@ -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 d7c6d8349..acb0efcb9 100644 --- a/src/components/user/Settings.vue +++ b/src/components/user/Settings.vue @@ -1,5 +1,5 @@ @@ -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 000000000..17de13b6b --- /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 d531c605c..f2c1f87a2 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 000000000..148d51a3e --- /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