Add client validation for registration form

* also extract registration logic to users.js module
This commit is contained in:
raeno 2018-12-05 13:43:01 +04:00
parent 02e000b53e
commit 0029313775
6 changed files with 147 additions and 1035 deletions

View File

@ -30,6 +30,7 @@
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vue-template-compiler": "^2.3.4", "vue-template-compiler": "^2.3.4",
"vue-timeago": "^3.1.2", "vue-timeago": "^3.1.2",
"vuelidate": "^0.7.4",
"vuex": "^3.0.1", "vuex": "^3.0.1",
"whatwg-fetch": "^2.0.3" "whatwg-fetch": "^2.0.3"
}, },

View File

@ -1,12 +1,27 @@
import oauthApi from '../../services/new_api/oauth.js' import { validationMixin } from 'vuelidate'
import { humanizeErrors } from '../../modules/errors' import { required } from 'vuelidate/lib/validators'
import { mapActions, mapState } from 'vuex'
import { SIGN_UP } from "../../mutation_types"
const registration = { const registration = {
mixins: [validationMixin],
data: () => ({ data: () => ({
user: {}, user: {
errors: [], email: '',
registering: false username: '',
password: '',
confirm: ''
},
clientValidationFailed: false
}), }),
validations: {
user: {
email: { required },
username: { required },
password: { required },
confirm: { required }
}
},
created () { created () {
if ((!this.$store.state.instance.registrationOpen && !this.token) || !!this.$store.state.users.currentUser) { if ((!this.$store.state.instance.registrationOpen && !this.token) || !!this.$store.state.users.currentUser) {
this.$router.push('/main/all') this.$router.push('/main/all')
@ -17,43 +32,24 @@ const registration = {
} }
}, },
computed: { computed: {
termsofservice () { return this.$store.state.instance.tos }, token () { return this.$route.params.token },
token () { return this.$route.params.token } ...mapState({
isPending: (state) => state.users[SIGN_UP.isPending],
serverValidationErrors: (state) => state.users[SIGN_UP.errors],
termsofservice: 'instance.tos',
})
}, },
methods: { methods: {
...mapActions(['signUp']),
submit () { submit () {
this.registering = true
this.user.nickname = this.user.username this.user.nickname = this.user.username
this.user.token = this.token this.user.token = this.token
this.$store.state.api.backendInteractor.register(this.user).then(
(response) => { this.$v.$touch()
if (response.ok) {
const data = { if (!this.$v.$invalid) {
oauth: this.$store.state.oauth, this.signUp(this.user)
instance: this.$store.state.instance.server
} }
oauthApi.getOrCreateApp(data).then((app) => {
oauthApi.getTokenWithCredentials(
{
app,
instance: data.instance,
username: this.user.username,
password: this.user.password
})
.then((result) => {
this.$store.commit('setToken', result.access_token)
this.$store.dispatch('loginUser', result.access_token)
this.$router.push('/main/friends')
})
})
} else {
this.registering = false
response.json().then((data) => {
this.errors = humanizeErrors(JSON.parse(data.error))
})
}
},
)
} }
} }
} }

View File

@ -7,29 +7,46 @@
<form v-on:submit.prevent='submit(user)' class='registration-form'> <form v-on:submit.prevent='submit(user)' class='registration-form'>
<div class='container'> <div class='container'>
<div class='text-fields'> <div class='text-fields'>
<div class='form-group'> <div class='form-group' :class="{ 'form-group--error': $v.user.username.$error }">
<label for='username'>{{$t('login.username')}}</label> <label class='form--label' for='sign-up-username'>{{$t('login.username')}}</label>
<input :disabled="registering" v-model='user.username' class='form-control' id='username' placeholder='e.g. lain'> <input :disabled="isPending" v-model.trim='$v.user.username.$model' class='form-control' id='sign-up-username' placeholder='e.g. lain'>
</div> </div>
<div class='form-group'> <div class="form-error" v-if="$v.user.username.$dirty">
<label for='fullname'>{{$t('registration.fullname')}}</label> <span class="error-required" v-if="!$v.user.username.required">Username is required.</span>
<input :disabled="registering" v-model='user.fullname' class='form-control' id='fullname' placeholder='e.g. Lain Iwakura'>
</div> </div>
<div class='form-group'> <div class='form-group'>
<label for='email'>{{$t('registration.email')}}</label> <label class='form--label' for='sign-up-fullname'>{{$t('registration.fullname')}}</label>
<input :disabled="registering" v-model='user.email' class='form-control' id='email' type="email"> <input :disabled="isPending" v-model='user.fullname' class='form-control' id='sign-up-fullname' placeholder='e.g. Lain Iwakura'>
</div> </div>
<div class='form-group'>
<label for='bio'>{{$t('registration.bio')}}</label> <div class='form-group' :class="{ 'form-group--error': $v.user.email.$error }">
<input :disabled="registering" v-model='user.bio' class='form-control' id='bio'> <label class='form--label' for='email'>{{$t('registration.email')}}</label>
<input :disabled="isPending" v-model='$v.user.email.$model' class='form-control' id='email' type="email">
</div> </div>
<div class='form-group'> <div class="form-error" v-if="$v.user.email.$dirty">
<label for='password'>{{$t('login.password')}}</label> <span class="error-required" v-if="!$v.user.email.required">Email is required.</span>
<input :disabled="registering" v-model='user.password' class='form-control' id='password' type='password'>
</div> </div>
<div class='form-group'> <div class='form-group'>
<label for='password_confirmation'>{{$t('registration.password_confirm')}}</label> <label class='form--label' for='bio'>{{$t('registration.bio')}}</label>
<input :disabled="registering" v-model='user.confirm' class='form-control' id='password_confirmation' type='password'> <input :disabled="isPending" v-model='user.bio' class='form-control' id='bio'>
</div>
<div class='form-group' :class="{ 'form-group--error': $v.user.password.$error }">
<label class='form--label' for='sign-up-password'>{{$t('login.password')}}</label>
<input :disabled="isPending" v-model='user.password' class='form-control' id='sign-up-password' type='password'>
</div>
<div class="form-error" v-if="$v.user.password.$dirty">
<span class="error-required" v-if="!$v.user.password.required">Password is required.</span>
</div>
<div class='form-group' :class="{ 'form-group--error': $v.user.confirm.$error }">
<label class='form--label' for='sign-up-password-confirmation'>{{$t('registration.password_confirm')}}</label>
<input :disabled="isPending" v-model='user.confirm' class='form-control' id='sign-up-password-confirmation' type='password'>
</div>
<div class="form-error" v-if="$v.user.confirm.$dirty">
<span class="error-required" v-if="!$v.user.confirm.required">Password confirmation is required.</span>
</div> </div>
<!-- <!--
<div class='form-group'> <div class='form-group'>
@ -43,15 +60,18 @@
<input disabled='true' v-model='token' class='form-control' id='token' type='text'> <input disabled='true' v-model='token' class='form-control' id='token' type='text'>
</div> </div>
<div class='form-group'> <div class='form-group'>
<button :disabled="registering" type='submit' class='btn btn-default'>{{$t('general.submit')}}</button> <button :disabled="isPending" type='submit' class='btn btn-default'>{{$t('general.submit')}}</button>
</div> </div>
</div> </div>
<div class='terms-of-service' v-html="termsofservice"> <div class='terms-of-service' v-html="termsofservice">
</div> </div>
</div> </div>
<div v-if="errors.length" class='form-group'> <div v-if="clientValidationFailed">
<span>Form is invalid</span>
</div>
<div v-if="serverValidationErrors.length" class='form-group'>
<div class='alert error'> <div class='alert error'>
<span v-for="error in errors">{{error}}</span> <span v-for="error in serverValidationErrors">{{error}}</span>
</div> </div>
</div> </div>
</form> </form>
@ -91,6 +111,27 @@
flex-direction: column; flex-direction: column;
padding: 0.3em 0.0em 0.3em; padding: 0.3em 0.0em 0.3em;
line-height:24px; line-height:24px;
margin-bottom: 1em;
}
.form-group--error {
animation-name: shakeError;
animation-duration: .6s;
animation-timing-function: ease-in-out;
}
.form-group--error .form--label {
color: #f04124;
}
.form-error {
margin-top: -0.7em;
margin-bottom: 0.5em;
text-align: left;
span {
font-size: 12px;
}
} }
form textarea { form textarea {

View File

@ -1,6 +1,9 @@
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
import { compact, map, each, merge } from 'lodash' import { compact, map, each, merge } from 'lodash'
import { set } from 'vue' import { set } from 'vue'
import { SIGN_UP } from '../mutation_types'
import oauthApi from '../services/new_api/oauth'
import {humanizeErrors} from './errors'
// TODO: Unify with mergeOrAdd in statuses.js // TODO: Unify with mergeOrAdd in statuses.js
export const mergeOrAdd = (arr, obj, item) => { export const mergeOrAdd = (arr, obj, item) => {
@ -46,15 +49,26 @@ export const mutations = {
setColor (state, { user: {id}, highlighted }) { setColor (state, { user: {id}, highlighted }) {
const user = state.usersObject[id] const user = state.usersObject[id]
set(user, 'highlight', highlighted) set(user, 'highlight', highlighted)
},
[SIGN_UP.PENDING] (state) {
state[SIGN_UP.isPending] = true
},
[SIGN_UP.SUCCESS] (state) {
state[SIGN_UP.isPending] = false
},
[SIGN_UP.FAILURE] (state, errors) {
state[SIGN_UP.isPending] = false
state[SIGN_UP.errors] = [...state[SIGN_UP.errors], ...errors]
} }
} }
export const defaultState = { export const defaultState = {
lastLoginName: false, lastLoginName: false,
currentUser: false, currentUser: false,
loggingIn: false,
users: [], users: [],
usersObject: {} usersObject: {},
sign_up_pending: false,
sign_up_errors: []
} }
const users = { const users = {
@ -80,6 +94,32 @@ const users = {
store.commit('setUserForStatus', status) store.commit('setUserForStatus', status)
}) })
}, },
async signUp (store, userInfo) {
store.commit(SIGN_UP.PENDING)
let response = await store.rootState.api.backendInteractor.register(userInfo)
if (response.ok) {
const data = {
oauth: store.state.oauth,
instance: store.state.instance.server
}
let app = await oauthApi.getOrCreateApp(data)
let result = await oauthApi.getTokenWithCredentials({
app,
instance: data.instance,
username: this.user.username,
password: this.user.password
})
store.commit(SIGN_UP.SUCCESS)
store.commit('setToken', result.access_token)
store.dispatch('loginUser', result.access_token)
this.$router.push('/main/friends')
} else {
let data = await response.json()
let errors = humanizeErrors(JSON.parse(data.error))
store.commit(SIGN_UP.FAILURE, errors)
}
},
logout (store) { logout (store) {
store.commit('clearCurrentUser') store.commit('clearCurrentUser')
store.commit('setToken', false) store.commit('setToken', false)

7
src/mutation_types.js Normal file
View File

@ -0,0 +1,7 @@
export const SIGN_UP = {
SUCCESS: 'SIGN_UP_SUCCESS',
FAILURE: 'SIGN_UP_FAILURE',
PENDING: 'SIGN_UP_PENDING',
isPending: 'sign_up_pending',
errors: 'sign_up_errors'
}

981
yarn.lock

File diff suppressed because it is too large Load Diff