mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-12 23:35:58 +00:00
Merge pull request #551 from Human-Connection/2019/kw15/change_password_strength
2019/kw15/change password strength
This commit is contained in:
commit
0712b3c0dc
@ -1,83 +0,0 @@
|
||||
<template>
|
||||
<ds-form
|
||||
v-model="formData"
|
||||
:schema="formSchema"
|
||||
@submit="handleSubmit"
|
||||
>
|
||||
<template>
|
||||
<ds-input
|
||||
id="oldPassword"
|
||||
model="oldPassword"
|
||||
type="password"
|
||||
label="Your old password"
|
||||
/>
|
||||
<ds-input
|
||||
id="newPassword"
|
||||
model="newPassword"
|
||||
type="password"
|
||||
label="Your new password"
|
||||
/>
|
||||
<ds-input
|
||||
id="confirmPassword"
|
||||
model="confirmPassword"
|
||||
type="password"
|
||||
label="Confirm new password"
|
||||
/>
|
||||
<ds-space margin-top="base">
|
||||
<ds-button
|
||||
:loading="loading"
|
||||
primary
|
||||
>
|
||||
{{ $t('settings.security.change-password.button') }}
|
||||
</ds-button>
|
||||
</ds-space>
|
||||
</template>
|
||||
</ds-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export default {
|
||||
name: 'ChangePassword',
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
},
|
||||
formSchema: {
|
||||
oldPassword: { required: true },
|
||||
newPassword: { required: true },
|
||||
confirmPassword: { required: true }
|
||||
},
|
||||
loading: false,
|
||||
disabled: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleSubmit(data) {
|
||||
this.loading = true
|
||||
const mutation = gql`
|
||||
mutation($oldPassword: String!, $newPassword: String!) {
|
||||
changePassword(oldPassword: $oldPassword, newPassword: $newPassword)
|
||||
}
|
||||
`
|
||||
const variables = this.formData
|
||||
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate({ mutation, variables })
|
||||
this.$store.commit('auth/SET_TOKEN', data.changePassword)
|
||||
this.$toast.success(
|
||||
this.$t('settings.security.change-password.success')
|
||||
)
|
||||
} catch (err) {
|
||||
this.$toast.error(err.message)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -1,5 +1,5 @@
|
||||
import { mount, createLocalVue } from '@vue/test-utils'
|
||||
import ChangePassword from './ChangePassword.vue'
|
||||
import ChangePassword from './Change.vue'
|
||||
import Vue from 'vue'
|
||||
import Styleguide from '@human-connection/styleguide'
|
||||
|
||||
@ -97,8 +97,9 @@ describe('ChangePassword.vue', () => {
|
||||
})
|
||||
|
||||
describe('submit form', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.find('form').trigger('submit')
|
||||
beforeEach(async done => {
|
||||
await wrapper.find('form').trigger('submit')
|
||||
done()
|
||||
})
|
||||
|
||||
it('calls changePassword mutation', () => {
|
||||
@ -119,8 +120,7 @@ describe('ChangePassword.vue', () => {
|
||||
|
||||
describe('mutation resolves', () => {
|
||||
beforeEach(() => {
|
||||
mocks.$apollo.mutate = jest.fn().mockResolvedValue()
|
||||
wrapper = Wrapper()
|
||||
wrapper.find('form').trigger('submit')
|
||||
})
|
||||
|
||||
it('calls auth/SET_TOKEN with response', () => {
|
||||
@ -138,16 +138,21 @@ describe('ChangePassword.vue', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('mutation rejects', () => {
|
||||
beforeEach(() => {
|
||||
// second call will reject
|
||||
wrapper.find('form').trigger('submit')
|
||||
// TODO This is not a valid testcase - we have to decide if we catch the same password on clientside
|
||||
/* describe('mutation rejects', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('input#oldPassword').setValue('supersecret')
|
||||
await wrapper.find('input#newPassword').setValue('supersecret')
|
||||
await wrapper.find('input#confirmPassword').setValue('supersecret')
|
||||
})
|
||||
|
||||
it('displays error message', () => {
|
||||
it('displays error message', async () => {
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await mocks.$apollo.mutate
|
||||
|
||||
expect(mocks.$toast.error).toHaveBeenCalledWith('Ouch!')
|
||||
})
|
||||
})
|
||||
}) */
|
||||
})
|
||||
})
|
||||
})
|
||||
136
webapp/components/Password/Change.vue
Normal file
136
webapp/components/Password/Change.vue
Normal file
@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<ds-form
|
||||
v-model="formData"
|
||||
:schema="formSchema"
|
||||
@submit="handleSubmit"
|
||||
@input="handleInput"
|
||||
@input-valid="handleInputValid"
|
||||
>
|
||||
<template>
|
||||
<ds-input
|
||||
id="oldPassword"
|
||||
model="oldPassword"
|
||||
type="password"
|
||||
:label="$t('settings.security.change-password.label-old-password')"
|
||||
/>
|
||||
<ds-input
|
||||
id="newPassword"
|
||||
model="newPassword"
|
||||
type="password"
|
||||
:label="$t('settings.security.change-password.label-new-password')"
|
||||
/>
|
||||
<ds-input
|
||||
id="confirmPassword"
|
||||
model="confirmPassword"
|
||||
type="password"
|
||||
:label="$t('settings.security.change-password.label-new-password-confirm')"
|
||||
/>
|
||||
<password-strength :password="formData.newPassword" />
|
||||
<ds-space margin-top="base">
|
||||
<ds-button
|
||||
:loading="loading"
|
||||
:disabled="disabled"
|
||||
primary
|
||||
>
|
||||
{{ $t('settings.security.change-password.button') }}
|
||||
</ds-button>
|
||||
</ds-space>
|
||||
</template>
|
||||
</ds-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import gql from 'graphql-tag'
|
||||
import PasswordStrength from './Strength'
|
||||
|
||||
export default {
|
||||
name: 'ChangePassword',
|
||||
components: {
|
||||
PasswordStrength
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formData: {
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
},
|
||||
formSchema: {
|
||||
oldPassword: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
message: this.$t(
|
||||
'settings.security.change-password.message-old-password-required'
|
||||
)
|
||||
},
|
||||
newPassword: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
message: this.$t(
|
||||
'settings.security.change-password.message-new-password-required'
|
||||
)
|
||||
},
|
||||
confirmPassword: [
|
||||
{ validator: this.matchPassword },
|
||||
{
|
||||
type: 'string',
|
||||
required: true,
|
||||
message: this.$t(
|
||||
'settings.security.change-password.message-new-password-confirm-required'
|
||||
)
|
||||
}
|
||||
]
|
||||
},
|
||||
loading: false,
|
||||
disabled: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleInput(data) {
|
||||
this.disabled = true
|
||||
},
|
||||
async handleInputValid(data) {
|
||||
this.disabled = false
|
||||
},
|
||||
async handleSubmit(data) {
|
||||
this.loading = true
|
||||
const mutation = gql`
|
||||
mutation($oldPassword: String!, $newPassword: String!) {
|
||||
changePassword(oldPassword: $oldPassword, newPassword: $newPassword)
|
||||
}
|
||||
`
|
||||
const variables = this.formData
|
||||
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate({ mutation, variables })
|
||||
this.$store.commit('auth/SET_TOKEN', data.changePassword)
|
||||
this.$toast.success(
|
||||
this.$t('settings.security.change-password.success')
|
||||
)
|
||||
this.formData = {
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
}
|
||||
} catch (err) {
|
||||
this.$toast.error(err.message)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
matchPassword(rule, value, callback, source, options) {
|
||||
var errors = []
|
||||
if (this.formData.newPassword !== value) {
|
||||
errors.push(
|
||||
new Error(
|
||||
this.$t(
|
||||
'settings.security.change-password.message-new-password-missmatch'
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
callback(errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
131
webapp/components/Password/Strength.vue
Normal file
131
webapp/components/Password/Strength.vue
Normal file
@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div class="field">
|
||||
<div class="password-strength-meter">
|
||||
<div
|
||||
class="password-strength-meter-inner"
|
||||
:class="'strength-' + strength"
|
||||
/>
|
||||
</div>
|
||||
<p class="help">
|
||||
<span
|
||||
v-if="pass"
|
||||
:class="{ insecure: !isSecure }"
|
||||
>
|
||||
{{ $t('settings.security.change-password.passwordSecurity') }}:
|
||||
<strong>{{ $t(`settings.security.change-password.passwordStrength${strength}`) }}</strong>
|
||||
</span>
|
||||
<span v-else> </span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import zxcvbn from 'zxcvbn'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
export default {
|
||||
name: 'PasswordMeter',
|
||||
props: {
|
||||
password: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
strength: null,
|
||||
isSecure: false,
|
||||
pass: this.password || null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
password(pass) {
|
||||
// update password when prop is changing
|
||||
this.pass = pass || null
|
||||
|
||||
// strength is the score calculated by zxcvbn
|
||||
const strength = !isEmpty(this.pass) ? zxcvbn(this.pass).score : null
|
||||
if (strength !== this.strength) {
|
||||
this.strength = strength
|
||||
this.isSecure = Boolean(strength >= 3)
|
||||
this.$emit('change', {
|
||||
strength,
|
||||
isSecure: this.isSecure
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.password-strength-meter {
|
||||
position: relative;
|
||||
height: 3px;
|
||||
background: $color-neutral-85;
|
||||
margin: 10px auto 6px;
|
||||
border-radius: 3px;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
height: inherit;
|
||||
background: transparent;
|
||||
display: block;
|
||||
border-color: #fff;
|
||||
border-style: solid;
|
||||
border-width: 0 6px 0 6px;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
width: calc(20% + 6px);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
&:before {
|
||||
left: calc(20% - 6px);
|
||||
}
|
||||
&:after {
|
||||
right: calc(20% - 6px);
|
||||
}
|
||||
}
|
||||
|
||||
.help {
|
||||
.insecure {
|
||||
strong {
|
||||
color: $color-danger;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.password-strength-meter-inner {
|
||||
background: transparent;
|
||||
height: inherit;
|
||||
position: absolute;
|
||||
width: 0;
|
||||
border-radius: inherit;
|
||||
transition: width 0.5s ease-in-out, background 0.25s;
|
||||
}
|
||||
|
||||
.password-strength-meter-inner {
|
||||
&.strength-0 {
|
||||
background: darken($color-warning, 40%);
|
||||
width: 20%;
|
||||
}
|
||||
&.strength-1 {
|
||||
background: darken(mix($color-warning, $color-yellow, 50%), 30%);
|
||||
width: 40%;
|
||||
}
|
||||
&.strength-2 {
|
||||
background: darken($color-yellow, 20%);
|
||||
width: 60%;
|
||||
}
|
||||
&.strength-3 {
|
||||
background: darken($color-success, 10%);
|
||||
width: 80%;
|
||||
}
|
||||
&.strength-4 {
|
||||
background: $color-success;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -26,7 +26,7 @@
|
||||
},
|
||||
"notifications": {
|
||||
"menu": {
|
||||
"mentioned": "hat dich in einem Beitrag erwähnt"
|
||||
"mentioned": "hat dich in einem Beitrag erwähnt"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
@ -48,7 +48,20 @@
|
||||
"name": "Sicherheit",
|
||||
"change-password": {
|
||||
"button": "Passwort ändern",
|
||||
"success": "Passwort erfolgreich geändert!"
|
||||
"success": "Passwort erfolgreich geändert!",
|
||||
"label-old-password": "Dein altes Passwort",
|
||||
"label-new-password": "Dein neues Passwort",
|
||||
"label-new-password-confirm": "Bestätige Dein neues Passwort",
|
||||
"message-old-password-required": "Gebe dein altes Passwort ein",
|
||||
"message-new-password-required": "Gebe ein neues Passwort ein",
|
||||
"message-new-password-confirm-required": "Bestätige dein neues Passwort",
|
||||
"message-new-password-missmatch": "Gebe das gleiche Passwort nochmals ein",
|
||||
"passwordSecurity": "Passwortsicherheit",
|
||||
"passwordStrength0": "Sehr unsicheres Passwort",
|
||||
"passwordStrength1": "Unsicheres Passwort",
|
||||
"passwordStrength2": "Mittelmäßiges Passwort",
|
||||
"passwordStrength3": "Sicheres Passwort",
|
||||
"passwordStrength4": "Sehr sicheres Passwort"
|
||||
}
|
||||
},
|
||||
"invites": {
|
||||
|
||||
@ -48,7 +48,20 @@
|
||||
"name": "Security",
|
||||
"change-password": {
|
||||
"button": "Change password",
|
||||
"success": "Password successfully changed!"
|
||||
"success": "Password successfully changed!",
|
||||
"label-old-password": "Your old password",
|
||||
"label-new-password": "Your new password",
|
||||
"label-new-password-confirm": "Confirm new password",
|
||||
"message-old-password-required": "Enter your old password",
|
||||
"message-new-password-required": "Enter a new password",
|
||||
"message-new-password-confirm-required": "Confirm your new password",
|
||||
"message-new-password-missmatch": "Type the same password again",
|
||||
"passwordSecurity": "Password security",
|
||||
"passwordStrength0": "Very insecure password",
|
||||
"passwordStrength1": "Insecure password",
|
||||
"passwordStrength2": "Mediocre password",
|
||||
"passwordStrength3": "Strong password",
|
||||
"passwordStrength4": "Very strong password"
|
||||
}
|
||||
},
|
||||
"invites": {
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
{
|
||||
"name": "hc-webapp-next",
|
||||
"version": "1.0.0",
|
||||
"description": "Human Connection GraphQL UI Prototype",
|
||||
"author": "Grzegorz Leoniec",
|
||||
"private": true,
|
||||
"description": "Human Connection Frontend",
|
||||
"authors": [
|
||||
"Grzegorz Leoniec (appinteractive)",
|
||||
"ulfgebhardt"
|
||||
],
|
||||
"author": "",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
|
||||
"dev:styleguide": "cross-env STYLEGUIDE_DEV=true yarn dev",
|
||||
@ -71,7 +75,8 @@
|
||||
"vue-count-to": "~1.0.13",
|
||||
"vue-izitoast": "1.1.2",
|
||||
"vue-sweetalert-icons": "~3.2.0",
|
||||
"vuex-i18n": "~1.11.0"
|
||||
"vuex-i18n": "~1.11.0",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "~7.4.4",
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
|
||||
<script>
|
||||
import HcEmpty from '~/components/Empty.vue'
|
||||
import ChangePassword from '~/components/ChangePassword.vue'
|
||||
import ChangePassword from '~/components/Password/Change'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
||||
@ -11410,3 +11410,8 @@ zen-observable@^0.8.0:
|
||||
version "0.8.14"
|
||||
resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.14.tgz#d33058359d335bc0db1f0af66158b32872af3bf7"
|
||||
integrity sha512-kQz39uonEjEESwh+qCi83kcC3rZJGh4mrZW7xjkSQYXkq//JZHTtKo+6yuVloTgMtzsIWOJrjIrKvk/dqm0L5g==
|
||||
|
||||
zxcvbn@^4.4.2:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/zxcvbn/-/zxcvbn-4.4.2.tgz#28ec17cf09743edcab056ddd8b1b06262cc73c30"
|
||||
integrity sha1-KOwXzwl0PtyrBW3dixsGJizHPDA=
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user