Merge branch 'better_errors_on_registration' into 'develop'
Registration form: Client side validation + better display of server validation errors See merge request pleroma/pleroma-fe!399
This commit is contained in:
commit
0cb3c4e056
8 changed files with 243 additions and 1052 deletions
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,57 +1,61 @@
|
||||||
import oauthApi from '../../services/new_api/oauth.js'
|
import { validationMixin } from 'vuelidate'
|
||||||
|
import { required, sameAs } from 'vuelidate/lib/validators'
|
||||||
|
import { mapActions, mapState } from 'vuex'
|
||||||
|
|
||||||
const registration = {
|
const registration = {
|
||||||
|
mixins: [validationMixin],
|
||||||
data: () => ({
|
data: () => ({
|
||||||
user: {},
|
user: {
|
||||||
error: false,
|
email: '',
|
||||||
registering: false
|
fullname: '',
|
||||||
}),
|
username: '',
|
||||||
created () {
|
password: '',
|
||||||
if ((!this.$store.state.instance.registrationOpen && !this.token) || !!this.$store.state.users.currentUser) {
|
confirm: ''
|
||||||
this.$router.push('/main/all')
|
|
||||||
}
|
}
|
||||||
// Seems like this doesn't work at first page open for some reason
|
}),
|
||||||
if (this.$store.state.instance.registrationOpen && this.token) {
|
validations: {
|
||||||
this.$router.push('/registration')
|
user: {
|
||||||
|
email: { required },
|
||||||
|
username: { required },
|
||||||
|
fullname: { required },
|
||||||
|
password: { required },
|
||||||
|
confirm: {
|
||||||
|
required,
|
||||||
|
sameAsPassword: sameAs('password')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created () {
|
||||||
|
if ((!this.registrationOpen && !this.token) || this.signedIn) {
|
||||||
|
this.$router.push('/main/all')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
termsofservice () { return this.$store.state.instance.tos },
|
token () { return this.$route.params.token },
|
||||||
token () { return this.$route.params.token }
|
...mapState({
|
||||||
|
registrationOpen: (state) => state.instance.registrationOpen,
|
||||||
|
signedIn: (state) => !!state.users.currentUser,
|
||||||
|
isPending: (state) => state.users.signUpPending,
|
||||||
|
serverValidationErrors: (state) => state.users.signUpErrors,
|
||||||
|
termsOfService: (state) => state.instance.tos
|
||||||
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
submit () {
|
...mapActions(['signUp']),
|
||||||
this.registering = true
|
async submit () {
|
||||||
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,
|
try {
|
||||||
instance: this.$store.state.instance.server
|
await this.signUp(this.user)
|
||||||
}
|
this.$router.push('/main/friends')
|
||||||
oauthApi.getOrCreateApp(data).then((app) => {
|
} catch (error) {
|
||||||
oauthApi.getTokenWithCredentials(
|
console.warn('Registration failed: ' + error)
|
||||||
{
|
|
||||||
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.error = data.error
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,50 +7,90 @@
|
||||||
<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>
|
<ul>
|
||||||
<input :disabled="registering" v-model='user.fullname' class='form-control' id='fullname' placeholder='e.g. Lain Iwakura'>
|
<li v-if="!$v.user.username.required">
|
||||||
|
<span>{{$t('registration.validations.username_required')}}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class='form-group'>
|
|
||||||
<label for='email'>{{$t('registration.email')}}</label>
|
<div class='form-group' :class="{ 'form-group--error': $v.user.fullname.$error }">
|
||||||
<input :disabled="registering" v-model='user.email' class='form-control' id='email' type="email">
|
<label class='form--label' for='sign-up-fullname'>{{$t('registration.fullname')}}</label>
|
||||||
|
<input :disabled="isPending" v-model.trim='$v.user.fullname.$model' class='form-control' id='sign-up-fullname' placeholder='e.g. Lain Iwakura'>
|
||||||
</div>
|
</div>
|
||||||
<div class='form-group'>
|
<div class="form-error" v-if="$v.user.fullname.$dirty">
|
||||||
<label for='bio'>{{$t('registration.bio')}}</label>
|
<ul>
|
||||||
<input :disabled="registering" v-model='user.bio' class='form-control' id='bio'>
|
<li v-if="!$v.user.fullname.required">
|
||||||
|
<span>{{$t('registration.validations.fullname_required')}}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class='form-group'>
|
|
||||||
<label for='password'>{{$t('login.password')}}</label>
|
<div class='form-group' :class="{ 'form-group--error': $v.user.email.$error }">
|
||||||
<input :disabled="registering" v-model='user.password' class='form-control' id='password' type='password'>
|
<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_confirmation'>{{$t('registration.password_confirm')}}</label>
|
<ul>
|
||||||
<input :disabled="registering" v-model='user.confirm' class='form-control' id='password_confirmation' type='password'>
|
<li v-if="!$v.user.email.required">
|
||||||
|
<span>{{$t('registration.validations.email_required')}}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<!--
|
|
||||||
<div class='form-group'>
|
<div class='form-group'>
|
||||||
<label for='captcha'>Captcha</label>
|
<label class='form--label' for='bio'>{{$t('registration.bio')}}</label>
|
||||||
<img src='/qvittersimplesecurity/captcha.jpg' alt='captcha' class='captcha'>
|
<input :disabled="isPending" v-model='user.bio' class='form-control' id='bio'>
|
||||||
<input :disabled="registering" v-model='user.captcha' placeholder='Enter captcha' type='test' class='form-control' id='captcha'>
|
|
||||||
</div>
|
</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">
|
||||||
|
<ul>
|
||||||
|
<li v-if="!$v.user.password.required">
|
||||||
|
<span>{{$t('registration.validations.password_required')}}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</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">
|
||||||
|
<ul>
|
||||||
|
<li v-if="!$v.user.confirm.required">
|
||||||
|
<span>{{$t('registration.validations.password_confirmation_required')}}</span>
|
||||||
|
</li>
|
||||||
|
<li v-if="!$v.user.confirm.sameAsPassword">
|
||||||
|
<span>{{$t('registration.validations.password_confirmation_match')}}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class='form-group' v-if='token' >
|
<div class='form-group' v-if='token' >
|
||||||
<label for='token'>{{$t('registration.token')}}</label>
|
<label for='token'>{{$t('registration.token')}}</label>
|
||||||
<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="error" class='form-group'>
|
<div v-if="serverValidationErrors.length" class='form-group'>
|
||||||
<div class='alert error'>{{error}}</div>
|
<div class='alert error'>
|
||||||
|
<span v-for="error in serverValidationErrors">{{error}}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -60,6 +100,7 @@
|
||||||
<script src="./registration.js"></script>
|
<script src="./registration.js"></script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../../_variables.scss';
|
@import '../../_variables.scss';
|
||||||
|
$validations-cRed: #f04124;
|
||||||
|
|
||||||
.registration-form {
|
.registration-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -89,6 +130,55 @@
|
||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shakeError {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0); }
|
||||||
|
15% {
|
||||||
|
transform: translateX(0.375rem); }
|
||||||
|
30% {
|
||||||
|
transform: translateX(-0.375rem); }
|
||||||
|
45% {
|
||||||
|
transform: translateX(0.375rem); }
|
||||||
|
60% {
|
||||||
|
transform: translateX(-0.375rem); }
|
||||||
|
75% {
|
||||||
|
transform: translateX(0.375rem); }
|
||||||
|
90% {
|
||||||
|
transform: translateX(-0.375rem); }
|
||||||
|
100% {
|
||||||
|
transform: translateX(0); } }
|
||||||
|
|
||||||
|
.form-group--error {
|
||||||
|
animation-name: shakeError;
|
||||||
|
animation-duration: .6s;
|
||||||
|
animation-timing-function: ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group--error .form--label {
|
||||||
|
color: $validations-cRed;
|
||||||
|
color: var(--cRed, $validations-cRed);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-error {
|
||||||
|
margin-top: -0.7em;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-error ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0 0 0 5px;
|
||||||
|
margin-top: 0;
|
||||||
|
|
||||||
|
li::before {
|
||||||
|
content: "• ";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
form textarea {
|
form textarea {
|
||||||
|
@ -102,8 +192,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
//align-self: flex-start;
|
|
||||||
//width: 10em;
|
|
||||||
margin-top: 0.6em;
|
margin-top: 0.6em;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,15 @@
|
||||||
"fullname": "Display name",
|
"fullname": "Display name",
|
||||||
"password_confirm": "Password confirmation",
|
"password_confirm": "Password confirmation",
|
||||||
"registration": "Registration",
|
"registration": "Registration",
|
||||||
"token": "Invite token"
|
"token": "Invite token",
|
||||||
|
"validations": {
|
||||||
|
"username_required": "cannot be left blank",
|
||||||
|
"fullname_required": "cannot be left blank",
|
||||||
|
"email_required": "cannot be left blank",
|
||||||
|
"password_required": "cannot be left blank",
|
||||||
|
"password_confirmation_required": "cannot be left blank",
|
||||||
|
"password_confirmation_match": "should be the same as password"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"attachmentRadius": "Attachments",
|
"attachmentRadius": "Attachments",
|
||||||
|
|
|
@ -55,7 +55,15 @@
|
||||||
"fullname": "Отображаемое имя",
|
"fullname": "Отображаемое имя",
|
||||||
"password_confirm": "Подтверждение пароля",
|
"password_confirm": "Подтверждение пароля",
|
||||||
"registration": "Регистрация",
|
"registration": "Регистрация",
|
||||||
"token": "Код приглашения"
|
"token": "Код приглашения",
|
||||||
|
"validations": {
|
||||||
|
"username_required": "не должно быть пустым",
|
||||||
|
"fullname_required": "не должно быть пустым",
|
||||||
|
"email_required": "не должен быть пустым",
|
||||||
|
"password_required": "не должен быть пустым",
|
||||||
|
"password_confirmation_required": "не должно быть пустым",
|
||||||
|
"password_confirmation_match": "должно совпадать с паролем"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"attachmentRadius": "Прикреплённые файлы",
|
"attachmentRadius": "Прикреплённые файлы",
|
||||||
|
|
12
src/modules/errors.js
Normal file
12
src/modules/errors.js
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { capitalize } from 'lodash'
|
||||||
|
|
||||||
|
export function humanizeErrors (errors) {
|
||||||
|
return Object.entries(errors).reduce((errs, [k, val]) => {
|
||||||
|
let message = val.reduce((acc, message) => {
|
||||||
|
let key = capitalize(k.replace(/_/g, ' '))
|
||||||
|
return acc + [key, message].join(' ') + '. '
|
||||||
|
}, '')
|
||||||
|
return [...errs, message]
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
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 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 +48,28 @@ 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)
|
||||||
|
},
|
||||||
|
signUpPending (state) {
|
||||||
|
state.signUpPending = true
|
||||||
|
state.signUpErrors = []
|
||||||
|
},
|
||||||
|
signUpSuccess (state) {
|
||||||
|
state.signUpPending = false
|
||||||
|
},
|
||||||
|
signUpFailure (state, errors) {
|
||||||
|
state.signUpPending = false
|
||||||
|
state.signUpErrors = errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultState = {
|
export const defaultState = {
|
||||||
|
loggingIn: false,
|
||||||
lastLoginName: false,
|
lastLoginName: false,
|
||||||
currentUser: false,
|
currentUser: false,
|
||||||
loggingIn: false,
|
|
||||||
users: [],
|
users: [],
|
||||||
usersObject: {}
|
usersObject: {},
|
||||||
|
signUpPending: false,
|
||||||
|
signUpErrors: []
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = {
|
const users = {
|
||||||
|
@ -80,6 +95,34 @@ const users = {
|
||||||
store.commit('setUserForStatus', status)
|
store.commit('setUserForStatus', status)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
async signUp (store, userInfo) {
|
||||||
|
store.commit('signUpPending')
|
||||||
|
|
||||||
|
let rootState = store.rootState
|
||||||
|
|
||||||
|
let response = await rootState.api.backendInteractor.register(userInfo)
|
||||||
|
if (response.ok) {
|
||||||
|
const data = {
|
||||||
|
oauth: rootState.oauth,
|
||||||
|
instance: rootState.instance.server
|
||||||
|
}
|
||||||
|
let app = await oauthApi.getOrCreateApp(data)
|
||||||
|
let result = await oauthApi.getTokenWithCredentials({
|
||||||
|
app,
|
||||||
|
instance: data.instance,
|
||||||
|
username: userInfo.username,
|
||||||
|
password: userInfo.password
|
||||||
|
})
|
||||||
|
store.commit('signUpSuccess')
|
||||||
|
store.commit('setToken', result.access_token)
|
||||||
|
store.dispatch('loginUser', result.access_token)
|
||||||
|
} else {
|
||||||
|
let data = await response.json()
|
||||||
|
let errors = humanizeErrors(JSON.parse(data.error))
|
||||||
|
store.commit('signUpFailure', errors)
|
||||||
|
throw Error(errors)
|
||||||
|
}
|
||||||
|
},
|
||||||
logout (store) {
|
logout (store) {
|
||||||
store.commit('clearCurrentUser')
|
store.commit('clearCurrentUser')
|
||||||
store.commit('setToken', false)
|
store.commit('setToken', false)
|
||||||
|
|
Loading…
Reference in a new issue