From ae733392f32b3c3079d22376c903067992a7cfe1 Mon Sep 17 00:00:00 2001 From: NGPixel Date: Sun, 30 Aug 2020 21:46:55 -0400 Subject: [PATCH] feat: password reset --- client/components/login.vue | 126 ++++++-- server/controllers/auth.js | 22 ++ server/core/mail.js | 2 +- server/graph/resolvers/authentication.js | 13 + server/graph/schemas/authentication.graphql | 4 + server/models/users.js | 32 +++ server/templates/account-reset-pwd.html | 304 ++++++++++++++++++++ server/views/login.pug | 1 + 8 files changed, 478 insertions(+), 26 deletions(-) create mode 100644 server/templates/account-reset-pwd.html diff --git a/client/components/login.vue b/client/components/login.vue index 340134cc..5c216df2 100644 --- a/client/components/login.vue +++ b/client/components/login.vue @@ -253,6 +253,10 @@ export default { hideLocal: { type: Boolean, default: false + }, + changePwdContinuationToken: { + type: String, + default: null } }, data () { @@ -309,6 +313,9 @@ export default { }, selectedStrategyKey (newValue, oldValue) { this.selectedStrategy = _.find(this.strategies, ['key', newValue]) + if (this.screen === 'changePwd') { + return + } this.screen = 'login' if (!this.selectedStrategy.strategy.useForm) { this.isLoading = true @@ -322,6 +329,10 @@ export default { }, mounted () { this.isShown = true + if (this.changePwdContinuationToken) { + this.screen = 'changePwd' + this.continuationToken = this.changePwdContinuationToken + } }, methods: { /** @@ -475,32 +486,51 @@ export default { this.loaderColor = 'grey darken-4' this.loaderTitle = this.$t('auth:changePwd.loading') this.isLoading = true - const resp = await this.$apollo.mutate({ - mutation: gql` - { - authentication { - activeStrategies { - key + try { + const resp = await this.$apollo.mutate({ + mutation: gql` + mutation ( + $continuationToken: String! + $newPassword: String! + ) { + authentication { + loginChangePassword ( + continuationToken: $continuationToken + newPassword: $newPassword + ) { + responseResult { + succeeded + errorCode + slug + message + } + jwt + continuationToken + redirect + } } } + `, + variables: { + continuationToken: this.continuationToken, + newPassword: this.newPassword } - `, - variables: { - continuationToken: this.continuationToken, - newPassword: this.newPassword + }) + if (_.has(resp, 'data.authentication.loginChangePassword')) { + let respObj = _.get(resp, 'data.authentication.loginChangePassword', {}) + if (respObj.responseResult.succeeded === true) { + this.handleLoginResponse(respObj) + } else { + throw new Error(respObj.responseResult.message) + } + } else { + throw new Error(this.$t('auth:genericError')) } - }) - if (_.get(resp, 'data.authentication.loginChangePassword.responseResult.succeeded', false) === true) { - this.loaderColor = 'green darken-1' - this.loaderTitle = this.$t('auth:loginSuccess') - Cookies.set('jwt', _.get(resp, 'data.authentication.loginChangePassword.jwt', ''), { expires: 365 }) - _.delay(() => { - window.location.replace('/') // TEMPORARY - USE RETURNURL - }, 1000) - } else { + } catch (err) { + console.error(err) this.$store.commit('showNotification', { style: 'red', - message: _.get(resp, 'data.authentication.loginChangePassword.responseResult.message', false), + message: err.message, icon: 'alert' }) this.isLoading = false @@ -519,11 +549,57 @@ export default { * FORGOT PASSWORD SUBMIT */ async forgotPasswordSubmit () { - this.$store.commit('showNotification', { - style: 'pink', - message: 'Coming soon!', - icon: 'ferry' - }) + this.loaderColor = 'grey darken-4' + this.loaderTitle = this.$t('auth:forgotPasswordLoading') + this.isLoading = true + try { + const resp = await this.$apollo.mutate({ + mutation: gql` + mutation ( + $email: String! + ) { + authentication { + forgotPassword ( + email: $email + ) { + responseResult { + succeeded + errorCode + slug + message + } + } + } + } + `, + variables: { + email: this.username + } + }) + if (_.has(resp, 'data.authentication.forgotPassword.responseResult')) { + let respObj = _.get(resp, 'data.authentication.forgotPassword.responseResult', {}) + if (respObj.succeeded === true) { + this.$store.commit('showNotification', { + style: 'success', + message: this.$t('auth:forgotPasswordSuccess'), + icon: 'email' + }) + this.screen = 'login' + } else { + throw new Error(respObj.message) + } + } else { + throw new Error(this.$t('auth:genericError')) + } + } catch (err) { + console.error(err) + this.$store.commit('showNotification', { + style: 'red', + message: err.message, + icon: 'alert' + }) + } + this.isLoading = false }, handleLoginResponse (respObj) { this.continuationToken = respObj.continuationToken diff --git a/server/controllers/auth.js b/server/controllers/auth.js index 7be1b491..df204e7e 100644 --- a/server/controllers/auth.js +++ b/server/controllers/auth.js @@ -148,6 +148,28 @@ router.get('/verify/:token', bruteforce.prevent, async (req, res, next) => { } }) +/** + * Reset Password + */ +router.get('/login-reset/:token', bruteforce.prevent, async (req, res, next) => { + try { + const usr = await WIKI.models.userKeys.validateToken({ kind: 'resetPwd', token: req.params.token }) + if (!usr) { + throw new Error('Invalid Token') + } + req.brute.reset() + + const changePwdContinuationToken = await WIKI.models.userKeys.generateToken({ + userId: usr.id, + kind: 'changePwd' + }) + const bgUrl = !_.isEmpty(WIKI.config.auth.loginBgUrl) ? WIKI.config.auth.loginBgUrl : '/_assets/img/splash/1.jpg' + res.render('login', { bgUrl, hideLocal: WIKI.config.auth.hideLocal, changePwdContinuationToken }) + } catch (err) { + next(err) + } +}) + /** * JWT Public Endpoints */ diff --git a/server/core/mail.js b/server/core/mail.js index 9362c61d..43cf0141 100644 --- a/server/core/mail.js +++ b/server/core/mail.js @@ -56,7 +56,7 @@ module.exports = { subject: `${opts.subject} - ${WIKI.config.title}`, text: opts.text, html: _.get(this.templates, opts.template)({ - logo: '', + logo: WIKI.config.logoUrl, siteTitle: WIKI.config.title, copyright: WIKI.config.company.length > 0 ? WIKI.config.company : 'Powered by Wiki.js', ...opts.data diff --git a/server/graph/resolvers/authentication.js b/server/graph/resolvers/authentication.js index ae72f715..d2ed5d9d 100644 --- a/server/graph/resolvers/authentication.js +++ b/server/graph/resolvers/authentication.js @@ -137,6 +137,19 @@ module.exports = { return graphHelper.generateError(err) } }, + /** + * Perform Mandatory Password Change after Login + */ + async forgotPassword (obj, args, context) { + try { + await WIKI.models.users.loginForgotPassword(args, context) + return { + responseResult: graphHelper.generateSuccess('Password reset request processed.') + } + } catch (err) { + return graphHelper.generateError(err) + } + }, /** * Register a new account */ diff --git a/server/graph/schemas/authentication.graphql b/server/graph/schemas/authentication.graphql index b079cf23..202e82a5 100644 --- a/server/graph/schemas/authentication.graphql +++ b/server/graph/schemas/authentication.graphql @@ -52,6 +52,10 @@ type AuthenticationMutation { newPassword: String! ): AuthenticationLoginResponse @rateLimit(limit: 5, duration: 60) + forgotPassword( + email: String! + ): DefaultResponse @rateLimit(limit: 3, duration: 60) + register( email: String! password: String! diff --git a/server/models/users.js b/server/models/users.js index 89c335c7..411389f6 100644 --- a/server/models/users.js +++ b/server/models/users.js @@ -478,6 +478,38 @@ module.exports = class User extends Model { } } + /** + * Send a password reset request + */ + static async loginForgotPassword ({ email }, context) { + const usr = await WIKI.models.users.query().where({ + email, + providerKey: 'local' + }).first() + if (!usr) { + WIKI.logger.debug(`Password reset attempt on nonexistant local account ${email}: [DISCARDED]`) + return + } + const resetToken = await WIKI.models.userKeys.generateToken({ + userId: usr.id, + kind: 'resetPwd' + }) + + await WIKI.mail.send({ + template: 'accountResetPwd', + to: email, + subject: `Password Reset Request`, + data: { + preheadertext: `A password reset was requested for ${WIKI.config.title}`, + title: `A password reset was requested for ${WIKI.config.title}`, + content: `Click the button below to reset your password. If you didn't request this password reset, simply discard this email.`, + buttonLink: `${WIKI.config.host}/login-reset/${resetToken}`, + buttonText: 'Reset Password' + }, + text: `A password reset was requested for wiki ${WIKI.config.title}. Open the following link to proceed: ${WIKI.config.host}/login-reset/${resetToken}` + }) + } + /** * Create a new user * diff --git a/server/templates/account-reset-pwd.html b/server/templates/account-reset-pwd.html new file mode 100644 index 00000000..b3d55cc5 --- /dev/null +++ b/server/templates/account-reset-pwd.html @@ -0,0 +1,304 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ <%= preheadertext %> +
+ + + + +
+ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌  +
+ + + + + + +
+ + diff --git a/server/views/login.pug b/server/views/login.pug index 4536a921..c0408cd9 100644 --- a/server/views/login.pug +++ b/server/views/login.pug @@ -5,4 +5,5 @@ block body login( bg-url=bgUrl hide-local=hideLocal + change-pwd-continuation-token=changePwdContinuationToken )