rewrite "password reset" with React

This commit is contained in:
Pig Fang 2020-03-30 10:12:35 +08:00
parent b7ac9bbfa1
commit fd2e3f2c1b
6 changed files with 174 additions and 153 deletions

View File

@ -106,8 +106,8 @@ export default [
},
{
path: 'auth/reset/(\\d+)',
component: () => import('../views/auth/Reset.vue'),
el: 'form',
react: () => import('../views/auth/Reset'),
el: 'main',
},
{
path: 'skinlib',

View File

@ -0,0 +1,105 @@
import React, { useState } from 'react'
import { hot } from 'react-hot-loader/root'
import { t } from '@/scripts/i18n'
import * as fetch from '@/scripts/net'
import { toast } from '@/scripts/notify'
import Alert from '@/components/Alert'
const Reset: React.FC = () => {
const [password, setPassword] = useState('')
const [confirmation, setConfirmation] = useState('')
const [warningMessage, setWarningMessage] = useState('')
const [isPending, setIsPending] = useState(false)
const handlePasswordChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setPassword(event.target.value)
}
const handleConfirmationChange = (
event: React.ChangeEvent<HTMLInputElement>,
) => {
setConfirmation(event.target.value)
}
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
if (password !== confirmation) {
setWarningMessage(t('auth.invalidConfirmPwd'))
return
}
setIsPending(true)
const { code, message } = await fetch.post<fetch.ResponseBody>(
location.href.replace(blessing.base_url, ''),
{ password },
)
if (code === 0) {
toast.success(message)
setTimeout(() => {
window.location.href = `${blessing.base_url}/auth/login`
}, 2000)
} else {
setWarningMessage(message)
setIsPending(false)
}
}
return (
<form onSubmit={handleSubmit}>
<div className="input-group mb-3">
<input
type="password"
required
minLength={8}
maxLength={32}
className="form-control"
placeholder={t('auth.password')}
value={password}
onChange={handlePasswordChange}
/>
<div className="input-group-append">
<div className="input-group-text">
<i className="fas fa-lock"></i>
</div>
</div>
</div>
<div className="input-group mb-3">
<input
type="password"
required
minLength={8}
maxLength={32}
className="form-control"
placeholder={t('auth.repeat-pwd')}
value={confirmation}
onChange={handleConfirmationChange}
/>
<div className="input-group-append">
<div className="input-group-text">
<i className="fas fa-sign-in-alt"></i>
</div>
</div>
</div>
<Alert type="warning">{warningMessage}</Alert>
<button
className="btn btn-primary float-right"
type="submit"
disabled={isPending}
>
{isPending ? (
<>
<i className="fas fa-spinner fa-spin mr-1"></i>
{t('auth.resetting')}
</>
) : (
t('auth.reset-button')
)}
</button>
</form>
)
}
export default hot(Reset)

View File

@ -1,106 +0,0 @@
<template>
<form @submit.prevent="reset">
<div class="input-group mb-3">
<input
v-model="password"
type="password"
class="form-control"
:placeholder="$t('auth.password')"
required
minlength="8"
maxlength="32"
>
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-lock" />
</div>
</div>
</div>
<div class="input-group mb-3">
<input
ref="confirm"
v-model="confirm"
type="password"
class="form-control"
:placeholder="$t('auth.repeat-pwd')"
required
minlength="8"
maxlength="32"
>
<div class="input-group-append">
<div class="input-group-text">
<span class="fas fa-sign-in-alt" />
</div>
</div>
</div>
<div class="alert alert-info" :class="{ 'd-none': !infoMsg }">
<i class="icon fas fa-info" />
{{ infoMsg }}
</div>
<div class="alert alert-warning" :class="{ 'd-none': !warningMsg }">
<i class="icon fas fa-exclamation-triangle" />
{{ warningMsg }}
</div>
<button
class="btn btn-primary float-right"
type="submit"
:disabled="pending"
>
<template v-if="pending">
<i class="fa fa-spinner fa-spin" /> {{ $t('auth.resetting') }}
</template>
<span v-else>{{ $t('auth.reset-button') }}</span>
</button>
</form>
</template>
<script>
import emitMounted from '../../components/mixins/emitMounted'
import { toast } from '../../scripts/notify'
export default {
name: 'Reset',
mixins: [
emitMounted,
],
data() {
return {
uid: +this.$route[1],
password: '',
confirm: '',
infoMsg: '',
warningMsg: '',
pending: false,
}
},
methods: {
async reset() {
const { password, confirm } = this
if (password !== confirm) {
this.infoMsg = this.$t('auth.invalidConfirmPwd')
this.$refs.confirm.focus()
return
}
this.pending = true
const { code, message } = await this.$http.post(
`/auth/reset/${this.uid}${location.search}`,
{ password },
)
if (code === 0) {
toast.success(message)
setTimeout(() => {
window.location = `${blessing.base_url}/auth/login`
}, 2000)
} else {
this.infoMsg = ''
this.warningMsg = message
this.pending = false
}
},
},
}
</script>

View File

@ -1,44 +0,0 @@
import Vue from 'vue'
import { mount } from '@vue/test-utils'
import { flushPromises } from '../../utils'
import { toast } from '@/scripts/notify'
import Reset from '@/views/auth/Reset.vue'
jest.mock('@/scripts/notify')
test('reset password', async () => {
Vue.prototype.$http.post
.mockResolvedValueOnce({ code: 1, message: 'fail' })
.mockResolvedValueOnce({ code: 0, message: 'ok' })
const wrapper = mount(Reset, {
mocks: {
$route: ['/auth/reset/1', '1'],
},
})
const form = wrapper.find('form')
const info = wrapper.find('.alert-info')
const warning = wrapper.find('.alert-warning')
wrapper.findAll('[type="password"]').at(0)
.setValue('12345678')
wrapper.findAll('[type="password"]').at(1)
.setValue('123456')
form.trigger('submit')
expect(Vue.prototype.$http.post).not.toBeCalled()
expect(info.text()).toBe('auth.invalidConfirmPwd')
wrapper.findAll('[type="password"]').at(1)
.setValue('12345678')
form.trigger('submit')
expect(Vue.prototype.$http.post).toBeCalledWith(
'/auth/reset/1', // Ignore `location.search`
{ password: '12345678' },
)
await flushPromises()
expect(warning.text()).toBe('fail')
form.trigger('submit')
await flushPromises()
expect(toast.success).toBeCalledWith('ok')
jest.runAllTimers()
})

View File

@ -0,0 +1,66 @@
import React from 'react'
import { render, waitFor, fireEvent } from '@testing-library/react'
import { t } from '@/scripts/i18n'
import * as fetch from '@/scripts/net'
import Reset from '@/views/auth/Reset'
jest.mock('@/scripts/net')
test('confirmation is not matched', () => {
const { getByText, getByPlaceholderText, queryByText } = render(<Reset />)
fireEvent.input(getByPlaceholderText(t('auth.password')), {
target: { value: 'password' },
})
fireEvent.input(getByPlaceholderText(t('auth.repeat-pwd')), {
target: { value: 'password1' },
})
fireEvent.click(getByText(t('auth.reset-button')))
expect(queryByText(t('auth.invalidConfirmPwd'))).toBeInTheDocument()
expect(fetch.post).not.toBeCalled()
})
test('succeeded', async () => {
fetch.post.mockResolvedValue({ code: 0, message: 'ok' })
const { getByText, getByPlaceholderText, getByRole, queryByText } = render(
<Reset />,
)
fireEvent.input(getByPlaceholderText(t('auth.password')), {
target: { value: 'password' },
})
fireEvent.input(getByPlaceholderText(t('auth.repeat-pwd')), {
target: { value: 'password' },
})
fireEvent.click(getByText(t('auth.reset-button')))
await waitFor(() =>
expect(fetch.post).toBeCalledWith(
location.href.replace(blessing.base_url, ''),
{ password: 'password' },
),
)
expect(queryByText('ok')).toBeInTheDocument()
expect(getByRole('status')).toHaveClass('alert-success')
jest.runAllTimers()
})
test('failed', async () => {
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
const { getByText, getByPlaceholderText, queryByText } = render(<Reset />)
fireEvent.input(getByPlaceholderText(t('auth.password')), {
target: { value: 'password' },
})
fireEvent.input(getByPlaceholderText(t('auth.repeat-pwd')), {
target: { value: 'password' },
})
fireEvent.click(getByText(t('auth.reset-button')))
await waitFor(() =>
expect(fetch.post).toBeCalledWith(
location.href.replace(blessing.base_url, ''),
{ password: 'password' },
),
)
expect(queryByText('failed')).toBeInTheDocument()
})

View File

@ -6,5 +6,5 @@
<p class="login-box-msg">
{{ trans('auth.reset.message', {username: user.nickname}) }}
</p>
<form></form>
<main></main>
{% endblock %}