diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/boot/after_store.js | 2 | ||||
| -rw-r--r-- | src/components/registration/registration.js | 84 | ||||
| -rw-r--r-- | src/components/registration/registration.vue | 146 | ||||
| -rw-r--r-- | src/i18n/en.json | 10 | ||||
| -rw-r--r-- | src/i18n/ru.json | 10 | ||||
| -rw-r--r-- | src/modules/errors.js | 12 | ||||
| -rw-r--r-- | src/modules/users.js | 47 |
7 files changed, 238 insertions, 73 deletions
diff --git a/src/boot/after_store.js b/src/boot/after_store.js index ea5d4ecd..a80baaf5 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -58,6 +58,7 @@ const afterStoreSetup = ({store, i18n}) => { var loginMethod = (config.loginMethod) var scopeCopy = (config.scopeCopy) var subjectLineBehavior = (config.subjectLineBehavior) + var alwaysShowSubjectInput = (config.alwaysShowSubjectInput) store.dispatch('setInstanceOption', { name: 'theme', value: theme }) store.dispatch('setInstanceOption', { name: 'background', value: background }) @@ -75,6 +76,7 @@ const afterStoreSetup = ({store, i18n}) => { store.dispatch('setInstanceOption', { name: 'loginMethod', value: loginMethod }) store.dispatch('setInstanceOption', { name: 'scopeCopy', value: scopeCopy }) store.dispatch('setInstanceOption', { name: 'subjectLineBehavior', value: subjectLineBehavior }) + store.dispatch('setInstanceOption', { name: 'alwaysShowSubjectInput', value: alwaysShowSubjectInput }) if (chatDisabled) { store.dispatch('disableChat') } diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js index f7f8a720..e5ead8bc 100644 --- a/src/components/registration/registration.js +++ b/src/components/registration/registration.js @@ -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 = { + mixins: [validationMixin], data: () => ({ - user: {}, - error: false, - registering: false + user: { + email: '', + fullname: '', + username: '', + password: '', + confirm: '' + } }), + validations: { + user: { + email: { required }, + username: { required }, + fullname: { required }, + password: { required }, + confirm: { + required, + sameAsPassword: sameAs('password') + } + } + }, created () { - if ((!this.$store.state.instance.registrationOpen && !this.token) || !!this.$store.state.users.currentUser) { + if ((!this.registrationOpen && !this.token) || this.signedIn) { 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) { - this.$router.push('/registration') - } }, 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: { - submit () { - this.registering = true + ...mapActions(['signUp']), + async submit () { this.user.nickname = this.user.username this.user.token = this.token - this.$store.state.api.backendInteractor.register(this.user).then( - (response) => { - if (response.ok) { - const data = { - oauth: this.$store.state.oauth, - 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.error = data.error - }) - } + + this.$v.$touch() + + if (!this.$v.$invalid) { + try { + await this.signUp(this.user) + this.$router.push('/main/friends') + } catch (error) { + console.warn('Registration failed: ' + error) } - ) + } } } } diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue index 087cab6b..8cb1392b 100644 --- a/src/components/registration/registration.vue +++ b/src/components/registration/registration.vue @@ -7,50 +7,90 @@ <form v-on:submit.prevent='submit(user)' class='registration-form'> <div class='container'> <div class='text-fields'> - <div class='form-group'> - <label for='username'>{{$t('login.username')}}</label> - <input :disabled="registering" v-model='user.username' class='form-control' id='username' placeholder='e.g. lain'> + <div class='form-group' :class="{ 'form-group--error': $v.user.username.$error }"> + <label class='form--label' for='sign-up-username'>{{$t('login.username')}}</label> + <input :disabled="isPending" v-model.trim='$v.user.username.$model' class='form-control' id='sign-up-username' placeholder='e.g. lain'> </div> - <div class='form-group'> - <label for='fullname'>{{$t('registration.fullname')}}</label> - <input :disabled="registering" v-model='user.fullname' class='form-control' id='fullname' placeholder='e.g. Lain Iwakura'> + <div class="form-error" v-if="$v.user.username.$dirty"> + <ul> + <li v-if="!$v.user.username.required"> + <span>{{$t('registration.validations.username_required')}}</span> + </li> + </ul> </div> - <div class='form-group'> - <label for='email'>{{$t('registration.email')}}</label> - <input :disabled="registering" v-model='user.email' class='form-control' id='email' type="email"> + + <div class='form-group' :class="{ 'form-group--error': $v.user.fullname.$error }"> + <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 class='form-group'> - <label for='bio'>{{$t('registration.bio')}}</label> - <input :disabled="registering" v-model='user.bio' class='form-control' id='bio'> + <div class="form-error" v-if="$v.user.fullname.$dirty"> + <ul> + <li v-if="!$v.user.fullname.required"> + <span>{{$t('registration.validations.fullname_required')}}</span> + </li> + </ul> </div> - <div class='form-group'> - <label for='password'>{{$t('login.password')}}</label> - <input :disabled="registering" v-model='user.password' class='form-control' id='password' type='password'> + + <div class='form-group' :class="{ 'form-group--error': $v.user.email.$error }"> + <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 class='form-group'> - <label for='password_confirmation'>{{$t('registration.password_confirm')}}</label> - <input :disabled="registering" v-model='user.confirm' class='form-control' id='password_confirmation' type='password'> + <div class="form-error" v-if="$v.user.email.$dirty"> + <ul> + <li v-if="!$v.user.email.required"> + <span>{{$t('registration.validations.email_required')}}</span> + </li> + </ul> </div> - <!-- + <div class='form-group'> - <label for='captcha'>Captcha</label> - <img src='/qvittersimplesecurity/captcha.jpg' alt='captcha' class='captcha'> - <input :disabled="registering" v-model='user.captcha' placeholder='Enter captcha' type='test' class='form-control' id='captcha'> + <label class='form--label' for='bio'>{{$t('registration.bio')}}</label> + <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"> + <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' > <label for='token'>{{$t('registration.token')}}</label> <input disabled='true' v-model='token' class='form-control' id='token' type='text'> </div> <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 class='terms-of-service' v-html="termsofservice"> + + <div class='terms-of-service' v-html="termsOfService"> </div> </div> - <div v-if="error" class='form-group'> - <div class='alert error'>{{error}}</div> + <div v-if="serverValidationErrors.length" class='form-group'> + <div class='alert error'> + <span v-for="error in serverValidationErrors">{{error}}</span> + </div> </div> </form> </div> @@ -60,6 +100,7 @@ <script src="./registration.js"></script> <style lang="scss"> @import '../../_variables.scss'; +$validations-cRed: #f04124; .registration-form { display: flex; @@ -89,6 +130,55 @@ flex-direction: column; padding: 0.3em 0.0em 0.3em; 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 { @@ -102,8 +192,6 @@ } .btn { - //align-self: flex-start; - //width: 10em; margin-top: 0.6em; height: 28px; } diff --git a/src/i18n/en.json b/src/i18n/en.json index 24282cf5..44291ad0 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -72,7 +72,15 @@ "fullname": "Display name", "password_confirm": "Password confirmation", "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": { "attachmentRadius": "Attachments", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index ceec9f7b..0f48e050 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -55,7 +55,15 @@ "fullname": "Отображаемое имя", "password_confirm": "Подтверждение пароля", "registration": "Регистрация", - "token": "Код приглашения" + "token": "Код приглашения", + "validations": { + "username_required": "не должно быть пустым", + "fullname_required": "не должно быть пустым", + "email_required": "не должен быть пустым", + "password_required": "не должен быть пустым", + "password_confirmation_required": "не должно быть пустым", + "password_confirmation_match": "должно совпадать с паролем" + } }, "settings": { "attachmentRadius": "Прикреплённые файлы", diff --git a/src/modules/errors.js b/src/modules/errors.js new file mode 100644 index 00000000..c809e1b5 --- /dev/null +++ b/src/modules/errors.js @@ -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] + }, []) +} + diff --git a/src/modules/users.js b/src/modules/users.js index 8630ee0d..6d966c3b 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -1,6 +1,8 @@ import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js' import { compact, map, each, merge } from 'lodash' import { set } from 'vue' +import oauthApi from '../services/new_api/oauth' +import {humanizeErrors} from './errors' // TODO: Unify with mergeOrAdd in statuses.js export const mergeOrAdd = (arr, obj, item) => { @@ -46,15 +48,28 @@ export const mutations = { setColor (state, { user: {id}, highlighted }) { const user = state.usersObject[id] 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 = { + loggingIn: false, lastLoginName: false, currentUser: false, - loggingIn: false, users: [], - usersObject: {} + usersObject: {}, + signUpPending: false, + signUpErrors: [] } const users = { @@ -80,6 +95,34 @@ const users = { 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) { store.commit('clearCurrentUser') store.commit('setToken', false) |
