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