Merge pull request #4270 from Ocelot-Social-Community/4092-implement-new-registration

feat: 🍰Implement Registration Slider
This commit is contained in:
Ulf Gebhardt 2021-03-30 05:04:32 +02:00 committed by GitHub
commit 9ad7dab918
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1736 additions and 1520 deletions

View File

@ -71,6 +71,5 @@ export default {
AddEmailAddress: sendEmailVerificationMail,
requestPasswordReset: sendPasswordResetMail,
Signup: sendSignupMail,
SignupByInvitation: sendSignupMail,
},
}

View File

@ -15,9 +15,11 @@ const defaultParams = {
export const signupTemplate = ({ email, nonce }) => {
const subject = `Willkommen, Bienvenue, Welcome to ${CONFIG.APPLICATION_NAME}!`
const actionUrl = new URL('/registration/create-user-account', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('nonce', nonce)
// dev format example: http://localhost:3000/registration?method=invite-mail&email=wolle.huss%40pjannto.com&nonce=64853
const actionUrl = new URL('/registration', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('method', 'invite-mail')
actionUrl.searchParams.set('email', email)
actionUrl.searchParams.set('nonce', nonce)
return {
from,
@ -34,8 +36,8 @@ export const signupTemplate = ({ email, nonce }) => {
export const emailVerificationTemplate = ({ email, nonce, name }) => {
const subject = 'Neue E-Mail Adresse | New E-Mail Address'
const actionUrl = new URL('/settings/my-email-address/verify', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('nonce', nonce)
actionUrl.searchParams.set('email', email)
actionUrl.searchParams.set('nonce', nonce)
return {
from,

View File

@ -1,6 +1,7 @@
import { rule, shield, deny, allow, or } from 'graphql-shield'
import { getNeode } from '../db/neo4j'
import CONFIG from '../config'
import { validateInviteCode } from '../schema/resolvers/transactions/inviteCodes'
const debug = !!CONFIG.DEBUG
const allowExternalErrors = true
@ -89,6 +90,13 @@ const noEmailFilter = rule({
const publicRegistration = rule()(() => CONFIG.PUBLIC_REGISTRATION)
const inviteRegistration = rule()(async (_parent, args, { user, driver }) => {
if (!CONFIG.INVITE_REGISTRATION) return false
const { inviteCode } = args
const session = driver.session()
return validateInviteCode(session, inviteCode)
})
// Permissions
export default shield(
{
@ -121,6 +129,7 @@ export default shield(
userData: isAuthenticated,
MyInviteCodes: isAuthenticated,
isValidInviteCode: allow,
VerifyNonce: allow,
queryLocations: isAuthenticated,
availableRoles: isAdmin,
getInviteCode: isAuthenticated, // and inviteRegistration
@ -128,8 +137,7 @@ export default shield(
Mutation: {
'*': deny,
login: allow,
SignupByInvitation: allow,
Signup: or(publicRegistration, isAdmin),
Signup: or(publicRegistration, inviteRegistration, isAdmin),
SignupVerification: allow,
UpdateUser: onlyYourself,
CreatePost: isAuthenticated,

View File

@ -3,11 +3,13 @@ import createServer from '../server'
import Factory, { cleanDatabase } from '../db/factories'
import { gql } from '../helpers/jest'
import { getDriver, getNeode } from '../db/neo4j'
import CONFIG from '../config'
const instance = getNeode()
const driver = getDriver()
let query, authenticatedUser, owner, anotherRegularUser, administrator, variables, moderator
let query, mutate, variables
let authenticatedUser, owner, anotherRegularUser, administrator, moderator
describe('authorization', () => {
beforeAll(async () => {
@ -20,6 +22,7 @@ describe('authorization', () => {
}),
})
query = createTestClient(server).query
mutate = createTestClient(server).mutate
})
afterEach(async () => {
@ -159,5 +162,132 @@ describe('authorization', () => {
})
})
})
describe('access Signup', () => {
const signupMutation = gql`
mutation($email: String!, $inviteCode: String) {
Signup(email: $email, inviteCode: $inviteCode) {
email
}
}
`
describe('admin invite only', () => {
beforeEach(async () => {
variables = {
email: 'some@email.org',
inviteCode: 'AAAAAA',
}
CONFIG.INVITE_REGISTRATION = false
CONFIG.PUBLIC_REGISTRATION = false
await Factory.build('inviteCode', {
code: 'AAAAAA',
})
})
describe('as user', () => {
beforeEach(async () => {
authenticatedUser = await anotherRegularUser.toJson()
})
it('denies permission', async () => {
await expect(mutate({ mutation: signupMutation, variables })).resolves.toMatchObject({
errors: [{ message: 'Not Authorised!' }],
data: { Signup: null },
})
})
})
describe('as admin', () => {
beforeEach(async () => {
authenticatedUser = await administrator.toJson()
})
it('returns an email', async () => {
await expect(mutate({ mutation: signupMutation, variables })).resolves.toMatchObject({
errors: undefined,
data: {
Signup: { email: 'some@email.org' },
},
})
})
})
})
describe('public registration', () => {
beforeEach(async () => {
variables = {
email: 'some@email.org',
inviteCode: 'AAAAAA',
}
CONFIG.INVITE_REGISTRATION = false
CONFIG.PUBLIC_REGISTRATION = true
await Factory.build('inviteCode', {
code: 'AAAAAA',
})
})
describe('as anyone', () => {
beforeEach(async () => {
authenticatedUser = null
})
it('returns an email', async () => {
await expect(mutate({ mutation: signupMutation, variables })).resolves.toMatchObject({
errors: undefined,
data: {
Signup: { email: 'some@email.org' },
},
})
})
})
})
describe('invite registration', () => {
beforeEach(async () => {
CONFIG.INVITE_REGISTRATION = true
CONFIG.PUBLIC_REGISTRATION = false
await Factory.build('inviteCode', {
code: 'AAAAAA',
})
})
describe('as anyone with valid invite code', () => {
beforeEach(async () => {
variables = {
email: 'some@email.org',
inviteCode: 'AAAAAA',
}
authenticatedUser = null
})
it('returns an email', async () => {
await expect(mutate({ mutation: signupMutation, variables })).resolves.toMatchObject({
errors: undefined,
data: {
Signup: { email: 'some@email.org' },
},
})
})
})
describe('as anyone without valid invite', () => {
beforeEach(async () => {
variables = {
email: 'some@email.org',
inviteCode: 'no valid invite code',
}
authenticatedUser = null
})
it('denies permission', async () => {
await expect(mutate({ mutation: signupMutation, variables })).resolves.toMatchObject({
errors: [{ message: 'Not Authorised!' }],
data: { Signup: null },
})
})
})
})
})
})
})

View File

@ -6,6 +6,27 @@ import Validator from 'neode/build/Services/Validator.js'
import normalizeEmail from './helpers/normalizeEmail'
export default {
Query: {
VerifyNonce: async (_parent, args, context, _resolveInfo) => {
const session = context.driver.session()
const readTxResultPromise = session.readTransaction(async (txc) => {
const result = await txc.run(
`
MATCH (email:EmailAddress {email: $email, nonce: $nonce})
RETURN count(email) > 0 AS result
`,
{ email: args.email, nonce: args.nonce },
)
return result
})
try {
const txResult = await readTxResultPromise
return txResult.records[0].get('result')
} finally {
session.close()
}
},
},
Mutation: {
AddEmailAddress: async (_parent, args, context, _resolveInfo) => {
let response

View File

@ -6,7 +6,7 @@ import { createTestClient } from 'apollo-server-testing'
const neode = getNeode()
let mutate
let mutate, query
let authenticatedUser
let user
let variables
@ -16,7 +16,8 @@ beforeEach(async () => {
variables = {}
})
beforeAll(() => {
beforeAll(async () => {
await cleanDatabase()
const { server } = createServer({
context: () => {
return {
@ -27,6 +28,7 @@ beforeAll(() => {
},
})
mutate = createTestClient(server).mutate
query = createTestClient(server).query
})
afterEach(async () => {
@ -185,7 +187,7 @@ describe('VerifyEmailAddress', () => {
let emailAddress
beforeEach(async () => {
emailAddress = await Factory.build('unverifiedEmailAddress', {
nonce: 'abcdef',
nonce: '12345',
verifiedAt: null,
createdAt: new Date().toISOString(),
email: 'to-be-verified@example.org',
@ -204,7 +206,7 @@ describe('VerifyEmailAddress', () => {
describe('given valid nonce for `UnverifiedEmailAddress` node', () => {
beforeEach(() => {
variables = { ...variables, nonce: 'abcdef' }
variables = { ...variables, nonce: '12345' }
})
describe('but the address does not belong to the authenticated user', () => {
@ -295,3 +297,40 @@ describe('VerifyEmailAddress', () => {
})
})
})
describe('VerifyNonce', () => {
beforeEach(async () => {
await Factory.build('emailAddress', {
nonce: '12345',
verifiedAt: null,
createdAt: new Date().toISOString(),
email: 'to-be-verified@example.org',
})
})
const verifyNonceQuery = gql`
query($email: String!, $nonce: String!) {
VerifyNonce(email: $email, nonce: $nonce)
}
`
it('returns true when nonce and email match', async () => {
variables = {
email: 'to-be-verified@example.org',
nonce: '12345',
}
await expect(query({ query: verifyNonceQuery, variables })).resolves.toMatchObject({
data: { VerifyNonce: true },
})
})
it('returns false when nonce and email do not match', async () => {
variables = {
email: 'to-be-verified@example.org',
nonce: '---',
}
await expect(query({ query: verifyNonceQuery, variables })).resolves.toMatchObject({
data: { VerifyNonce: false },
})
})
})

View File

@ -1,4 +1,5 @@
import { v4 as uuid } from 'uuid'
export default function generateNonce() {
return uuid().substring(0, 6)
return Array.from({ length: 5 }, (n = Math.floor(Math.random() * 10)) => {
return String.fromCharCode(n + 48)
}).join('')
}

View File

@ -1,5 +1,6 @@
import generateInviteCode from './helpers/generateInviteCode'
import Resolver from './helpers/Resolver'
import { validateInviteCode } from './transactions/inviteCodes'
const uniqueInviteCode = async (session, code) => {
return session.readTransaction(async (txc) => {
@ -82,28 +83,9 @@ export default {
},
isValidInviteCode: async (_parent, args, context, _resolveInfo) => {
const { code } = args
if (!code) return false
const session = context.driver.session()
const readTxResultPromise = session.readTransaction(async (txc) => {
const result = await txc.run(
`MATCH (ic:InviteCode { code: toUpper($code) })
RETURN
CASE
WHEN ic.expiresAt IS NULL THEN true
WHEN datetime(ic.expiresAt) >= datetime() THEN true
ELSE false END AS result`,
{
code,
},
)
return result.records.map((record) => record.get('result'))
})
try {
const txResult = await readTxResultPromise
return !!txResult[0]
} finally {
session.close()
}
if (!code) return false
return validateInviteCode(session, code)
},
},
Mutation: {

View File

@ -29,34 +29,22 @@ export default {
}
args.termsAndConditionsAgreedAt = new Date().toISOString()
let { nonce, email } = args
let { nonce, email, inviteCode } = args
email = normalizeEmail(email)
delete args.nonce
delete args.email
delete args.inviteCode
args = encryptPassword(args)
const { driver } = context
const session = driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const createUserTransactionResponse = await transaction.run(
`
MATCH(email:EmailAddress {nonce: $nonce, email: $email})
WHERE NOT (email)-[:BELONGS_TO]->()
CREATE (user:User)
MERGE(user)-[:PRIMARY_EMAIL]->(email)
MERGE(user)<-[:BELONGS_TO]-(email)
SET user += $args
SET user.id = randomUUID()
SET user.role = 'user'
SET user.createdAt = toString(datetime())
SET user.updatedAt = toString(datetime())
SET user.allowEmbedIframes = FALSE
SET user.showShoutsPublicly = FALSE
SET email.verifiedAt = toString(datetime())
RETURN user {.*}
`,
{ args, nonce, email },
)
const createUserTransactionResponse = await transaction.run(signupCypher(inviteCode), {
args,
nonce,
email,
inviteCode,
})
const [user] = createUserTransactionResponse.records.map((record) => record.get('user'))
if (!user) throw new UserInputError('Invalid email or nonce')
return user
@ -74,3 +62,39 @@ export default {
},
},
}
const signupCypher = (inviteCode) => {
let optionalMatch = ''
let optionalMerge = ''
if (inviteCode) {
optionalMatch = `
OPTIONAL MATCH
(inviteCode:InviteCode {code: $inviteCode})<-[:GENERATED]-(host:User)
`
optionalMerge = `
MERGE(user)-[:REDEEMED]->(inviteCode)
MERGE(host)-[:INVITED]->(user)
MERGE(user)-[:FOLLOWS]->(host)
MERGE(host)-[:FOLLOWS]->(user)
`
}
const cypher = `
MATCH(email:EmailAddress {nonce: $nonce, email: $email})
WHERE NOT (email)-[:BELONGS_TO]->()
${optionalMatch}
CREATE (user:User)
MERGE(user)-[:PRIMARY_EMAIL]->(email)
MERGE(user)<-[:BELONGS_TO]-(email)
${optionalMerge}
SET user += $args
SET user.id = randomUUID()
SET user.role = 'user'
SET user.createdAt = toString(datetime())
SET user.updatedAt = toString(datetime())
SET user.allowEmbedIframes = FALSE
SET user.showShoutsPublicly = FALSE
SET email.verifiedAt = toString(datetime())
RETURN user {.*}
`
return cypher
}

View File

@ -3,6 +3,7 @@ import { gql } from '../../helpers/jest'
import { getDriver, getNeode } from '../../db/neo4j'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'
import CONFIG from '../../config'
const neode = getNeode()
@ -15,7 +16,8 @@ beforeEach(async () => {
variables = {}
})
beforeAll(() => {
beforeAll(async () => {
await cleanDatabase()
const { server } = createServer({
context: () => {
return {
@ -34,8 +36,8 @@ afterEach(async () => {
describe('Signup', () => {
const mutation = gql`
mutation($email: String!) {
Signup(email: $email) {
mutation($email: String!, $inviteCode: String) {
Signup(email: $email, inviteCode: $inviteCode) {
email
}
}
@ -50,6 +52,8 @@ describe('Signup', () => {
})
it('throws AuthorizationError', async () => {
CONFIG.INVITE_REGISTRATION = false
CONFIG.PUBLIC_REGISTRATION = false
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
errors: [{ message: 'Not Authorised!' }],
})

View File

@ -0,0 +1,22 @@
export async function validateInviteCode(session, inviteCode) {
const readTxResultPromise = session.readTransaction(async (txc) => {
const result = await txc.run(
`MATCH (ic:InviteCode { code: toUpper($inviteCode) })
RETURN
CASE
WHEN ic.expiresAt IS NULL THEN true
WHEN datetime(ic.expiresAt) >= datetime() THEN true
ELSE false END AS result`,
{
inviteCode,
},
)
return result.records.map((record) => record.get('result'))
})
try {
const txResult = await readTxResultPromise
return !!txResult[0]
} finally {
session.close()
}
}

View File

@ -4,12 +4,16 @@ type EmailAddress {
createdAt: String
}
type Query {
VerifyNonce(email: String!, nonce: String!): Boolean!
}
type Mutation {
Signup(email: String!): EmailAddress
SignupByInvitation(email: String!, token: String!): EmailAddress
Signup(email: String!, inviteCode: String = null): EmailAddress
SignupVerification(
nonce: String!
email: String!
inviteCode: String = null
name: String!
password: String!
slug: String

View File

@ -2,18 +2,38 @@
<div class="Sliders">
<slot :name="'header'" />
<ds-heading v-if="sliderData.sliders[sliderIndex].title" size="h3">
{{ sliderData.sliders[sliderIndex].title }}
<ds-heading
v-if="
sliderData.sliders[sliderIndex].titleIdent &&
((typeof sliderData.sliders[sliderIndex].titleIdent === 'string' &&
$t(sliderData.sliders[sliderIndex].titleIdent).length > 0) ||
(typeof sliderData.sliders[sliderIndex].titleIdent === 'object' &&
$t(
sliderData.sliders[sliderIndex].titleIdent.id,
sliderData.sliders[sliderIndex].titleIdent.data,
).length > 0))
"
size="h3"
>
{{
(typeof sliderData.sliders[sliderIndex].titleIdent === 'string' &&
$t(sliderData.sliders[sliderIndex].titleIdent)) ||
(typeof sliderData.sliders[sliderIndex].titleIdent === 'object' &&
$t(
sliderData.sliders[sliderIndex].titleIdent.id,
sliderData.sliders[sliderIndex].titleIdent.data,
))
}}
</ds-heading>
<slot :name="sliderData.sliders[sliderIndex].name" />
<ds-flex>
<ds-flex-item :centered="true">
<ds-flex-item v-if="multipleSliders" :centered="true">
<div
v-for="(slider, index) in sliderData.sliders"
:key="slider.name"
:class="['Sliders__slider-selection', index < sliderIndex && '--confirmed']"
:class="['Sliders__slider-selection', index === sliderIndex && '--unconfirmed']"
>
<base-button
:class="['selection-dot']"
@ -30,15 +50,20 @@
</ds-flex-item>
<ds-flex-item>
<base-button
style="float: right"
:style="multipleSliders && 'float: right'"
:icon="sliderData.sliders[sliderIndex].button.icon"
type="submit"
filled
:loading="false"
:loading="
sliderData.sliders[sliderIndex].button.loading !== undefined
? sliderData.sliders[sliderIndex].button.loading
: false
"
:disabled="!sliderData.sliders[sliderIndex].validated"
@click="onNextClick"
data-test="next-button"
>
{{ sliderData.sliders[sliderIndex].button.title }}
{{ $t(sliderData.sliders[sliderIndex].button.titleIdent) }}
</base-button>
</ds-flex-item>
</ds-flex>
@ -57,6 +82,9 @@ export default {
sliderIndex() {
return this.sliderData.sliderIndex // to have a shorter notation
},
multipleSliders() {
return this.sliderData.sliders.length > 1
},
},
methods: {
async onNextClick() {
@ -79,7 +107,7 @@ export default {
.selection-dot {
margin-right: 2px;
}
&.--confirmed {
&.--unconfirmed {
opacity: $opacity-disabled;
}
}

View File

@ -37,12 +37,12 @@ describe('EnterNonce ', () => {
describe('after nonce entered', () => {
beforeEach(() => {
wrapper = Wrapper()
wrapper.find('input#nonce').setValue('123456')
wrapper.find('input#nonce').setValue('12345')
wrapper.find('form').trigger('submit')
})
it('emits `nonceEntered`', () => {
const expected = [[{ nonce: '123456', email: 'mail@example.org' }]]
const expected = [[{ nonce: '12345', email: 'mail@example.org' }]]
expect(wrapper.emitted('nonceEntered')).toEqual(expected)
})
})

View File

@ -8,17 +8,17 @@
@input-valid="handleInputValid"
>
<ds-input
:placeholder="$t('components.enter-nonce.form.nonce')"
:placeholder="$t('components.registration.email-nonce.form.nonce')"
model="nonce"
name="nonce"
id="nonce"
icon="question-circle"
/>
<ds-text>
{{ $t('components.enter-nonce.form.description') }}
{{ $t('components.registration.email-nonce.form.description') }}
</ds-text>
<base-button :disabled="disabled" filled name="submit" type="submit">
{{ $t('components.enter-nonce.form.next') }}
{{ $t('components.registration.email-nonce.form.next') }}
</base-button>
<slot></slot>
</ds-form>
@ -37,10 +37,10 @@ export default {
formSchema: {
nonce: {
type: 'string',
min: 6,
max: 6,
min: 5,
max: 5,
required: true,
message: this.$t('components.enter-nonce.form.validations.length'),
message: this.$t('components.registration.email-nonce.form.validations.length'),
},
},
disabled: true,

View File

@ -45,7 +45,7 @@
</base-button>
<p>
{{ $t('login.no-account') }}
<nuxt-link to="/registration/signup">{{ $t('login.register') }}</nuxt-link>
<nuxt-link to="/registration">{{ $t('login.register') }}</nuxt-link>
</p>
</form>
<template #topMenu>

View File

@ -1,153 +0,0 @@
import { config, mount } from '@vue/test-utils'
import Vue from 'vue'
import { VERSION } from '~/constants/terms-and-conditions-version.js'
import CreateUserAccount from './CreateUserAccount'
import { SignupVerificationMutation } from '~/graphql/Registration.js'
const localVue = global.localVue
config.stubs['sweetalert-icon'] = '<span><slot /></span>'
config.stubs['client-only'] = '<span><slot /></span>'
config.stubs['nuxt-link'] = '<span><slot /></span>'
describe('CreateUserAccount', () => {
let wrapper, Wrapper, mocks, propsData, stubs
beforeEach(() => {
mocks = {
$toast: {
success: jest.fn(),
error: jest.fn(),
},
$t: jest.fn(),
$apollo: {
loading: false,
mutate: jest.fn(),
},
$i18n: {
locale: () => 'en',
},
}
propsData = {}
stubs = {
LocaleSwitch: "<div class='stub'></div>",
}
})
describe('mount', () => {
Wrapper = () => {
return mount(CreateUserAccount, {
mocks,
propsData,
localVue,
stubs,
})
}
describe('given email and nonce', () => {
beforeEach(() => {
propsData.nonce = '666777'
propsData.email = 'sixseven@example.org'
})
it('renders a form to create a new user', () => {
wrapper = Wrapper()
expect(wrapper.find('.create-user-account').exists()).toBe(true)
})
describe('submit', () => {
let action
beforeEach(() => {
action = async () => {
wrapper = Wrapper()
wrapper.find('input#name').setValue('John Doe')
wrapper.find('input#password').setValue('hellopassword')
wrapper.find('textarea#about').setValue('Hello I am the `about` attribute')
wrapper.find('input#passwordConfirmation').setValue('hellopassword')
wrapper.find('input#checkbox0').setChecked()
wrapper.find('input#checkbox1').setChecked()
wrapper.find('input#checkbox2').setChecked()
wrapper.find('input#checkbox3').setChecked()
wrapper.find('input#checkbox4').setChecked()
await wrapper.find('form').trigger('submit')
await wrapper.html()
}
})
it('delivers data to backend', async () => {
await action()
const expected = expect.objectContaining({
variables: {
about: 'Hello I am the `about` attribute',
name: 'John Doe',
email: 'sixseven@example.org',
nonce: '666777',
password: 'hellopassword',
termsAndConditionsAgreedVersion: VERSION,
locale: 'en',
},
})
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
it('calls CreateUserAccount graphql mutation', async () => {
await action()
const expected = expect.objectContaining({ mutation: SignupVerificationMutation })
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
describe('in case mutation resolves', () => {
beforeEach(() => {
mocks.$apollo.mutate = jest.fn().mockResolvedValue({
data: {
SignupVerification: {
id: 'u1',
name: 'John Doe',
slug: 'john-doe',
},
},
})
})
it('displays success', async () => {
await action()
await Vue.nextTick()
expect(mocks.$t).toHaveBeenCalledWith(
'components.registration.create-user-account.success',
)
})
describe('after timeout', () => {
beforeEach(jest.useFakeTimers)
it('emits `userCreated` with { password, email }', async () => {
await action()
jest.runAllTimers()
expect(wrapper.emitted('userCreated')).toEqual([
[
{
email: 'sixseven@example.org',
password: 'hellopassword',
},
],
])
})
})
})
describe('in case mutation rejects', () => {
beforeEach(() => {
mocks.$apollo.mutate = jest.fn().mockRejectedValue(new Error('Invalid nonce'))
})
it('displays form errors', async () => {
await action()
await Vue.nextTick()
expect(mocks.$t).toHaveBeenCalledWith(
'components.registration.create-user-account.error',
)
})
})
})
})
})
})

View File

@ -1,84 +0,0 @@
import { storiesOf } from '@storybook/vue'
import { withA11y } from '@storybook/addon-a11y'
import { action } from '@storybook/addon-actions'
import Vuex from 'vuex'
import helpers from '~/storybook/helpers'
import links from '~/constants/links.js'
import metadata from '~/constants/metadata.js'
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
import CreateUserAccount from './CreateUserAccount.vue'
helpers.init()
const createStore = ({ loginSuccess }) => {
return new Vuex.Store({
modules: {
auth: {
namespaced: true,
state: () => ({
pending: false,
}),
mutations: {
SET_PENDING(state, pending) {
state.pending = pending
},
},
getters: {
pending(state) {
return !!state.pending
},
},
actions: {
async login({ commit, dispatch }, args) {
action('Vuex action `auth/login`')(args)
return new Promise((resolve, reject) => {
commit('SET_PENDING', true)
setTimeout(() => {
commit('SET_PENDING', false)
if (loginSuccess) {
resolve(loginSuccess)
} else {
reject(new Error('Login unsuccessful'))
}
}, 1000)
})
},
},
},
},
})
}
storiesOf('CreateUserAccount', module)
.addDecorator(withA11y)
.addDecorator(helpers.layout)
.add('standard', () => ({
components: { LocaleSwitch, CreateUserAccount },
store: createStore({ loginSuccess: true }),
data: () => ({
links,
metadata,
nonce: 'A34RB56',
email: 'user@example.org',
}),
methods: {
handleSuccess() {
action('You are logged in!')()
},
},
template: `
<ds-container width="small">
<base-card>
<template #imageColumn>
<a :href="links.ORGANIZATION" :title="$t('login.moreInfo', metadata)" target="_blank">
<img class="image" alt="Sign up" src="/img/custom/sign-up.svg" />
</a>
</template>
<create-user-account @userCreated="handleSuccess" :email="email" :nonce="nonce" />
<template #topMenu>
<locale-switch offset="5" />
</template>
</base-card>
</ds-container>
`,
}))

View File

@ -1,222 +0,0 @@
<template>
<div v-if="response === 'success'">
<transition name="ds-transition-fade">
<sweetalert-icon icon="success" />
</transition>
<ds-text align="center" bold color="success">
{{ $t('components.registration.create-user-account.success') }}
</ds-text>
</div>
<div v-else-if="response === 'error'">
<transition name="ds-transition-fade">
<sweetalert-icon icon="error" />
</transition>
<ds-text align="center" bold color="danger">
{{ $t('components.registration.create-user-account.error') }}
</ds-text>
<ds-text align="center">
{{ $t('components.registration.create-user-account.help') }}
<a :href="'mailto:' + supportEmail">{{ supportEmail }}</a>
</ds-text>
<ds-space centered>
<nuxt-link to="/login">{{ $t('site.back-to-login') }}</nuxt-link>
</ds-space>
</div>
<div v-else class="create-account-card">
<ds-space margin-top="large">
<ds-heading size="h3">
{{ $t('components.registration.create-user-account.title') }}
</ds-heading>
</ds-space>
<ds-form class="create-user-account" v-model="formData" :schema="formSchema" @submit="submit">
<template v-slot="{ errors }">
<ds-input
id="name"
model="name"
icon="user"
:label="$t('settings.data.labelName')"
:placeholder="$t('settings.data.namePlaceholder')"
/>
<ds-input
id="about"
model="about"
type="textarea"
rows="3"
:label="$t('settings.data.labelBio')"
:placeholder="$t('settings.data.labelBio')"
/>
<ds-input
id="password"
model="password"
type="password"
autocomplete="off"
:label="$t('settings.security.change-password.label-new-password')"
/>
<ds-input
id="passwordConfirmation"
model="passwordConfirmation"
type="password"
autocomplete="off"
:label="$t('settings.security.change-password.label-new-password-confirm')"
/>
<password-strength :password="formData.password" />
<ds-text>
<input
id="checkbox0"
type="checkbox"
v-model="termsAndConditionsConfirmed"
:checked="termsAndConditionsConfirmed"
/>
<label for="checkbox0">
{{ $t('termsAndConditions.termsAndConditionsConfirmed') }}
<br />
<nuxt-link to="/terms-and-conditions">{{ $t('site.termsAndConditions') }}</nuxt-link>
</label>
</ds-text>
<ds-text>
<input id="checkbox1" type="checkbox" v-model="dataPrivacy" :checked="dataPrivacy" />
<label for="checkbox1">
{{ $t('components.registration.signup.form.data-privacy') }}
<br />
<nuxt-link to="/data-privacy">
{{ $t('site.data-privacy') }}
</nuxt-link>
</label>
</ds-text>
<ds-text>
<input id="checkbox2" type="checkbox" v-model="minimumAge" :checked="minimumAge" />
<label
for="checkbox2"
v-html="$t('components.registration.signup.form.minimum-age')"
></label>
</ds-text>
<ds-text>
<input id="checkbox3" type="checkbox" v-model="noCommercial" :checked="noCommercial" />
<label
for="checkbox3"
v-html="$t('components.registration.signup.form.no-commercial')"
></label>
</ds-text>
<ds-text>
<input id="checkbox4" type="checkbox" v-model="noPolitical" :checked="noPolitical" />
<label
for="checkbox4"
v-html="$t('components.registration.signup.form.no-political')"
></label>
</ds-text>
<base-button
style="float: right"
icon="check"
type="submit"
filled
:loading="$apollo.loading"
:disabled="
errors ||
!termsAndConditionsConfirmed ||
!dataPrivacy ||
!minimumAge ||
!noCommercial ||
!noPolitical
"
>
{{ $t('actions.save') }}
</base-button>
</template>
</ds-form>
</div>
</template>
<script>
import links from '~/constants/links'
import PasswordStrength from '../Password/Strength'
import { SweetalertIcon } from 'vue-sweetalert-icons'
import PasswordForm from '~/components/utils/PasswordFormHelper'
import { VERSION } from '~/constants/terms-and-conditions-version.js'
import { SignupVerificationMutation } from '~/graphql/Registration.js'
import emails from '~/constants/emails'
export default {
components: {
PasswordStrength,
SweetalertIcon,
},
data() {
const passwordForm = PasswordForm({ translate: this.$t })
return {
links,
supportEmail: emails.SUPPORT,
formData: {
name: '',
about: '',
...passwordForm.formData,
},
formSchema: {
name: {
type: 'string',
required: true,
min: 3,
},
about: {
type: 'string',
required: false,
},
...passwordForm.formSchema,
},
disabled: true,
response: null,
// TODO: Our styleguide does not support checkmarks.
// Integrate termsAndConditionsConfirmed into `this.formData` once we
// have checkmarks available.
termsAndConditionsConfirmed: false,
dataPrivacy: false,
minimumAge: false,
noCommercial: false,
noPolitical: false,
}
},
props: {
nonce: { type: String, required: true },
email: { type: String, required: true },
},
methods: {
async submit() {
const { name, password, about } = this.formData
const { email, nonce } = this
const termsAndConditionsAgreedVersion = VERSION
const locale = this.$i18n.locale()
try {
await this.$apollo.mutate({
mutation: SignupVerificationMutation,
variables: {
name,
password,
about,
email,
nonce,
termsAndConditionsAgreedVersion,
locale,
},
})
this.response = 'success'
setTimeout(() => {
this.$emit('userCreated', {
email,
password,
})
}, 3000)
} catch (err) {
this.response = 'error'
}
},
},
}
</script>
<style lang="scss" scoped>
.create-account-image {
width: 50%;
max-width: 200px;
}
</style>

View File

@ -0,0 +1,39 @@
<template>
<ds-text>
{{ $t('components.registration.email-display.yourEmail') }}
<b v-if="emailAsString.length > 0">
{{ emailAsString }}
<b v-if="!isEmailFormat" class="email-warning">
{{ $t('components.registration.email-display.warningFormat') }}
</b>
</b>
<b v-else class="email-warning">
{{ $t('components.registration.email-display.warningUndef') }}
</b>
</ds-text>
</template>
<script>
import { isEmail } from 'validator'
export default {
name: 'EmailDisplayAndVerify',
props: {
email: { type: String, default: () => '' },
},
computed: {
isEmailFormat() {
return isEmail(this.email)
},
emailAsString() {
return !this.email ? '' : this.email
},
},
}
</script>
<style lang="scss">
.email-warning {
color: $text-color-danger;
}
</style>

View File

@ -1,262 +0,0 @@
<template>
<!-- Wolle <ds-space v-if="!data && !error" margin="large"> -->
<ds-form
v-model="formData"
:schema="formSchema"
@input="handleInput"
@input-valid="handleInputValid"
>
<!-- Wolle <h1>
{{
invitation
? $t('profile.invites.title', metadata)
: $t('components.registration.signup.title', metadata)
}}
</h1> -->
<ds-text
v-if="token"
v-html="$t('registration.signup.form.invitation-code', { code: token })"
/>
<ds-text>
{{
invitation
? $t('profile.invites.description')
: $t('components.registration.signup.form.description')
}}
</ds-text>
<ds-input
:placeholder="invitation ? $t('profile.invites.emailPlaceholder') : $t('login.email')"
type="email"
id="email"
model="email"
name="email"
icon="envelope"
/>
<slot></slot>
<ds-text v-if="sliderData.collectedInputData.emailSend">
<input id="checkbox" type="checkbox" v-model="sendEmailAgain" :checked="sendEmailAgain" />
<label for="checkbox0">
<!-- Wolle {{ $t('termsAndConditions.termsAndConditionsConfirmed') }} -->
{{ 'Send e-mail again' }}
</label>
</ds-text>
</ds-form>
<!-- Wolle </ds-space>
<div v-else margin="large">
<template v-if="!error">
<transition name="ds-transition-fade">
<sweetalert-icon icon="info" />
</transition>
<ds-text align="center" v-html="submitMessage" />
</template>
<template v-else>
<transition name="ds-transition-fade">
<sweetalert-icon icon="error" />
</transition>
<ds-text align="center">{{ error.message }}</ds-text>
<ds-space centered class="space-top">
<nuxt-link to="/login">{{ $t('site.back-to-login') }}</nuxt-link>
</ds-space>
</template>
</div> -->
</template>
<script>
import gql from 'graphql-tag'
import metadata from '~/constants/metadata'
import { isEmail } from 'validator'
import normalizeEmail from '~/components/utils/NormalizeEmail'
// Wolle import { SweetalertIcon } from 'vue-sweetalert-icons'
export const SignupMutation = gql`
mutation($email: String!) {
Signup(email: $email) {
email
}
}
`
export const SignupByInvitationMutation = gql`
mutation($email: String!, $token: String!) {
SignupByInvitation(email: $email, token: $token) {
email
}
}
`
export default {
name: 'RegistrationItemEnterEmail',
components: {
// Wolle SweetalertIcon,
},
props: {
sliderData: { type: Object, required: true },
token: { type: String, default: null }, // Wolle not used???
invitation: { type: Boolean, default: false },
},
data() {
return {
metadata,
formData: {
email: '',
},
formSchema: {
email: {
type: 'email',
required: true,
message: this.$t('common.validations.email'),
},
},
// TODO: Our styleguide does not support checkmarks.
// Integrate termsAndConditionsConfirmed into `this.formData` once we
// have checkmarks available.
sendEmailAgain: false,
error: null, // Wolle
}
},
mounted: function () {
this.$nextTick(function () {
// Code that will run only after the entire view has been rendered
this.formData.email = this.sliderData.collectedInputData.email
? this.sliderData.collectedInputData.email
: ''
this.sendValidation()
this.sliderData.setSliderValuesCallback(this.validInput, {
sliderSettings: {
...this.buttonValues().sliderSettings,
buttonSliderCallback: this.onNextClick,
},
})
})
},
watch: {
sendEmailAgain() {
this.setButtonValues()
},
},
computed: {
sliderIndex() {
return this.sliderData.sliderIndex // to have a shorter notation
},
// Wolle submitMessage() {
// const { email } = this.data.Signup
// return this.$t('components.registration.signup.form.success', { email })
// },
validInput() {
return isEmail(this.formData.email)
},
},
methods: {
async sendValidation() {
if (this.formData.email && isEmail(this.formData.email)) {
this.formData.email = normalizeEmail(this.formData.email)
}
const { email } = this.formData
this.sliderData.setSliderValuesCallback(this.validInput, { collectedInputData: { email } })
},
async handleInput() {
this.sendValidation()
},
async handleInputValid() {
this.sendValidation()
},
buttonValues() {
return {
sliderSettings: {
buttonTitle: this.sliderData.collectedInputData.emailSend
? this.sendEmailAgain
? 'Resend e-mail'
: 'Skip resend'
: 'Send e-mail', // Wolle
buttonIcon: this.sliderData.collectedInputData.emailSend
? this.sendEmailAgain
? 'envelope'
: 'arrow-right'
: 'envelope', // Wolle
},
}
},
setButtonValues() {
this.sliderData.setSliderValuesCallback(this.validInput, this.buttonValues())
},
async onNextClick() {
const mutation = this.token ? SignupByInvitationMutation : SignupMutation
const { token } = this
const { email } = this.formData
const variables = { email, token }
if (!this.sendEmailAgain && this.sliderData.collectedInputData.emailSend) {
return true
}
if (
this.sendEmailAgain ||
!this.sliderData.sliders[this.sliderIndex].data.request ||
(this.sliderData.sliders[this.sliderIndex].data.request &&
(!this.sliderData.sliders[this.sliderIndex].data.request.variables ||
(this.sliderData.sliders[this.sliderIndex].data.request.variables &&
!this.sliderData.sliders[this.sliderIndex].data.request.variables === variables)))
) {
this.sliderData.setSliderValuesCallback(
this.sliderData.sliders[this.sliderIndex].validated,
{ sliderData: { request: { variables }, response: null } },
)
try {
const response = await this.$apollo.mutate({ mutation, variables }) // e-mail is send in emailMiddleware of backend
this.sliderData.setSliderValuesCallback(
this.sliderData.sliders[this.sliderIndex].validated,
{ sliderData: { response: response.data } },
)
if (this.sliderData.sliders[this.sliderIndex].data.response) {
this.sliderData.setSliderValuesCallback(this.validInput, {
collectedInputData: { emailSend: true },
})
this.setButtonValues()
const { email: respnseEmail } =
this.sliderData.sliders[this.sliderIndex].data.response.Signup ||
this.sliderData.sliders[this.sliderIndex].data.response.SignupByInvitation
this.$toast.success(
this.$t('components.registration.email.form.success', { email: respnseEmail }),
)
}
return true
} catch (err) {
this.sliderData.setSliderValuesCallback(
this.sliderData.sliders[this.sliderIndex].validated,
{ sliderData: { request: null, response: null } },
)
this.sliderData.setSliderValuesCallback(this.validInput, {
collectedInputData: { emailSend: false },
})
this.setButtonValues()
const { message } = err
const mapping = {
'A user account with this email already exists': 'email-exists',
'Invitation code already used or does not exist': 'invalid-invitation-token',
}
for (const [pattern, key] of Object.entries(mapping)) {
if (message.includes(pattern))
this.error = {
key,
message: this.$t(`components.registration.signup.form.errors.${key}`),
}
}
if (!this.error) {
this.$toast.error(message)
}
return false
}
}
},
},
}
</script>
<style>
.space-top {
margin-top: 6ex;
}
</style>

View File

@ -1,107 +0,0 @@
<template>
<ds-form
class="enter-nonce"
v-model="formData"
:schema="formSchema"
@submit="handleSubmitVerify"
@input="handleInput"
@input-valid="handleInputValid"
>
<ds-text>
<!-- Wolle {{ $t('components.enter-nonce.form.description') }} -->
Your e-mail address:
<b>{{ this.sliderData.collectedInputData.email }}</b>
</ds-text>
<ds-input
:placeholder="$t('components.enter-nonce.form.nonce')"
model="nonce"
name="nonce"
id="nonce"
icon="question-circle"
/>
<ds-text>
{{ $t('components.enter-nonce.form.description') }}
</ds-text>
<slot></slot>
</ds-form>
</template>
<script>
export default {
name: 'RegistrationItemEnterNonce',
props: {
sliderData: { type: Object, required: true },
},
data() {
return {
formData: {
nonce: '',
},
formSchema: {
nonce: {
type: 'string',
// Wolle min: 6,
// max: 6,
required: true,
message: this.$t('components.enter-nonce.form.validations.length'),
},
},
}
},
mounted: function () {
this.$nextTick(function () {
// Code that will run only after the entire view has been rendered
// console.log('mounted !!! ')
this.formData.nonce = this.sliderData.collectedInputData.nonce
? this.sliderData.collectedInputData.nonce
: ''
this.sendValidation()
this.sliderData.setSliderValuesCallback(this.validInput, {
sliderSettings: { buttonSliderCallback: this.onNextClick },
})
})
},
computed: {
validInput() {
return this.formData.nonce.length === 6
},
},
methods: {
sendValidation() {
const { nonce } = this.formData
// Wolle shall the nonce be validated in the database?
// let dbValidated = false
// if (this.validInput) {
// await this.handleSubmitVerify()
// dbValidated = this.sliderData.sliders[this.sliderIndex].data.response.isValidInviteCode
// }
// this.sliderData.setSliderValuesCallback(dbValidated, {
this.sliderData.setSliderValuesCallback(this.validInput, {
collectedInputData: {
nonce,
},
})
},
async handleInput() {
this.sendValidation()
},
async handleInputValid() {
this.sendValidation()
},
handleSubmitVerify() {},
onNextClick() {
return true
},
},
}
</script>
<style lang="scss">
.enter-nonce {
display: flex;
flex-direction: column;
margin: $space-large 0 $space-xxx-small 0;
}
</style>

View File

@ -23,17 +23,10 @@
</ds-space>
</div>
<div v-else class="create-account-card">
<!-- Wolle <ds-space margin-top="large">
<ds-heading size="h3">
{{ $t('components.registration.create-user-account.title') }}
</ds-heading>
</ds-space> -->
<ds-form
class="create-user-account"
v-model="formData"
:schema="formSchema"
@submit="submit"
@input="handleInput"
@input-valid="handleInputValid"
>
@ -47,14 +40,6 @@
:label="$t('settings.data.labelName')"
:placeholder="$t('settings.data.namePlaceholder')"
/>
<ds-input
id="about"
model="about"
type="textarea"
rows="3"
:label="$t('settings.data.labelBio')"
:placeholder="$t('settings.data.labelBio')"
/>
<ds-input
id="password"
model="password"
@ -71,11 +56,7 @@
/>
<password-strength class="password-strength" :password="formData.password" />
<ds-text>
<!-- Wolle {{ $t('components.enter-nonce.form.description') }} -->
Your e-mail address:
<b>{{ this.sliderData.collectedInputData.email }}</b>
</ds-text>
<email-display-and-verify :email="sliderData.collectedInputData.email" />
<ds-text>
<input
@ -85,41 +66,31 @@
:checked="termsAndConditionsConfirmed"
/>
<label for="checkbox0">
{{ $t('termsAndConditions.termsAndConditionsConfirmed') }}
{{ $t('components.registration.create-user-account.termsAndCondsEtcConfirmed') }}
<br />
<nuxt-link to="/terms-and-conditions">{{ $t('site.termsAndConditions') }}</nuxt-link>
</label>
</ds-text>
<ds-text>
<input id="checkbox1" type="checkbox" v-model="dataPrivacy" :checked="dataPrivacy" />
<label for="checkbox1">
{{ $t('components.registration.signup.form.data-privacy') }}
<a :href="'/terms-and-conditions'" target="_blank">
{{ $t('site.termsAndConditions') }}
</a>
<br />
<nuxt-link to="/data-privacy">
<a :href="'/data-privacy'" target="_blank">
{{ $t('site.data-privacy') }}
</nuxt-link>
</a>
</label>
</ds-text>
<ds-text>
<input id="checkbox2" type="checkbox" v-model="minimumAge" :checked="minimumAge" />
<label
for="checkbox2"
v-html="$t('components.registration.signup.form.minimum-age')"
></label>
</ds-text>
<ds-text>
<input id="checkbox3" type="checkbox" v-model="noCommercial" :checked="noCommercial" />
<label
for="checkbox3"
v-html="$t('components.registration.signup.form.no-commercial')"
></label>
</ds-text>
<ds-text>
<input id="checkbox4" type="checkbox" v-model="noPolitical" :checked="noPolitical" />
<label
for="checkbox4"
v-html="$t('components.registration.signup.form.no-political')"
></label>
<input
id="checkbox1"
type="checkbox"
v-model="recieveCommunicationAsEmailsEtcConfirmed"
:checked="recieveCommunicationAsEmailsEtcConfirmed"
/>
<label for="checkbox1">
{{
$t(
'components.registration.create-user-account.recieveCommunicationAsEmailsEtcConfirmed',
)
}}
</label>
</ds-text>
</template>
</ds-form>
@ -130,15 +101,17 @@
import { VERSION } from '~/constants/terms-and-conditions-version.js'
import links from '~/constants/links'
import emails from '~/constants/emails'
import PasswordStrength from '../Password/Strength'
import PasswordStrength from '~/components/Password/Strength'
import EmailDisplayAndVerify from './EmailDisplayAndVerify'
import { SweetalertIcon } from 'vue-sweetalert-icons'
import PasswordForm from '~/components/utils/PasswordFormHelper'
import { SignupVerificationMutation } from '~/graphql/Registration.js'
export default {
name: 'RegistrationItemCreateUserAccount',
name: 'RegistrationSlideCreate',
components: {
PasswordStrength,
EmailDisplayAndVerify,
SweetalertIcon,
},
props: {
@ -151,7 +124,6 @@ export default {
supportEmail: emails.SUPPORT,
formData: {
name: '',
about: '',
...passwordForm.formData,
},
formSchema: {
@ -160,32 +132,23 @@ export default {
required: true,
min: 3,
},
about: {
type: 'string',
required: false,
},
...passwordForm.formSchema,
},
response: null, // Wolle
response: null,
// TODO: Our styleguide does not support checkmarks.
// Integrate termsAndConditionsConfirmed into `this.formData` once we
// have checkmarks available.
termsAndConditionsConfirmed: false,
dataPrivacy: false,
minimumAge: false,
noCommercial: false,
noPolitical: false,
recieveCommunicationAsEmailsEtcConfirmed: false,
}
},
mounted: function () {
this.$nextTick(function () {
// Code that will run only after the entire view has been rendered
this.formData.name = this.sliderData.collectedInputData.name
? this.sliderData.collectedInputData.name
: ''
this.formData.about = this.sliderData.collectedInputData.about
? this.sliderData.collectedInputData.about
: ''
this.formData.password = this.sliderData.collectedInputData.password
? this.sliderData.collectedInputData.password
: ''
@ -196,17 +159,9 @@ export default {
.termsAndConditionsConfirmed
? this.sliderData.collectedInputData.termsAndConditionsConfirmed
: false
this.dataPrivacy = this.sliderData.collectedInputData.dataPrivacy
? this.sliderData.collectedInputData.dataPrivacy
: false
this.minimumAge = this.sliderData.collectedInputData.minimumAge
? this.sliderData.collectedInputData.minimumAge
: false
this.noCommercial = this.sliderData.collectedInputData.noCommercial
? this.sliderData.collectedInputData.noCommercial
: false
this.noPolitical = this.sliderData.collectedInputData.noPolitical
? this.sliderData.collectedInputData.noPolitical
this.recieveCommunicationAsEmailsEtcConfirmed = this.sliderData.collectedInputData
.recieveCommunicationAsEmailsEtcConfirmed
? this.sliderData.collectedInputData.recieveCommunicationAsEmailsEtcConfirmed
: false
this.sendValidation()
@ -222,10 +177,7 @@ export default {
this.formData.password.length >= 1 &&
this.formData.password === this.formData.passwordConfirmation &&
this.termsAndConditionsConfirmed &&
this.dataPrivacy &&
this.minimumAge &&
this.noCommercial &&
this.noPolitical
this.recieveCommunicationAsEmailsEtcConfirmed
)
},
},
@ -233,41 +185,22 @@ export default {
termsAndConditionsConfirmed() {
this.sendValidation()
},
dataPrivacy() {
this.sendValidation()
},
minimumAge() {
this.sendValidation()
},
noCommercial() {
this.sendValidation()
},
noPolitical() {
recieveCommunicationAsEmailsEtcConfirmed() {
this.sendValidation()
},
},
methods: {
sendValidation() {
const { name, about, password, passwordConfirmation } = this.formData
const {
termsAndConditionsConfirmed,
dataPrivacy,
minimumAge,
noCommercial,
noPolitical,
} = this
const { name, password, passwordConfirmation } = this.formData
const { termsAndConditionsConfirmed, recieveCommunicationAsEmailsEtcConfirmed } = this
this.sliderData.setSliderValuesCallback(this.validInput, {
collectedInputData: {
name,
about,
password,
passwordConfirmation,
termsAndConditionsConfirmed,
dataPrivacy,
minimumAge,
noCommercial,
noPolitical,
recieveCommunicationAsEmailsEtcConfirmed,
},
})
},
@ -278,31 +211,39 @@ export default {
this.sendValidation()
},
async submit() {
const { name, password, about } = this.formData
const { email, nonce } = this.sliderData.collectedInputData
const { name, password } = this.formData
const { email, inviteCode = null, nonce } = this.sliderData.collectedInputData
const termsAndConditionsAgreedVersion = VERSION
const locale = this.$i18n.locale()
try {
this.sliderData.setSliderValuesCallback(null, {
sliderSettings: { buttonLoading: true },
})
await this.$apollo.mutate({
mutation: SignupVerificationMutation,
variables: {
name,
password,
about,
email,
inviteCode,
nonce,
termsAndConditionsAgreedVersion,
locale,
},
})
this.response = 'success'
// Wolle setTimeout(() => {
// this.$emit('userCreated', {
// email,
// password,
// })
// }, 3000)
setTimeout(async () => {
await this.$store.dispatch('auth/login', { email, password })
this.$toast.success(this.$t('login.success'))
this.$router.push('/')
this.sliderData.setSliderValuesCallback(null, {
sliderSettings: { buttonLoading: false },
})
}, 3000)
} catch (err) {
this.sliderData.setSliderValuesCallback(null, {
sliderSettings: { buttonLoading: false },
})
this.response = 'error'
}
},

View File

@ -0,0 +1,208 @@
<template>
<ds-form
class="enter-email"
v-model="formData"
:schema="formSchema"
@input="handleInput"
@input-valid="handleInputValid"
>
<ds-text>
{{ $t('components.registration.signup.form.description') }}
</ds-text>
<ds-input
:placeholder="$t('login.email')"
type="email"
id="email"
model="email"
name="email"
icon="envelope"
/>
<slot></slot>
<ds-text v-if="sliderData.collectedInputData.emailSend">
<input id="checkbox" type="checkbox" v-model="sendEmailAgain" :checked="sendEmailAgain" />
<label for="checkbox0">
{{ $t('components.registration.email.form.sendEmailAgain') }}
</label>
</ds-text>
</ds-form>
</template>
<script>
import gql from 'graphql-tag'
import metadata from '~/constants/metadata'
import { isEmail } from 'validator'
import normalizeEmail from '~/components/utils/NormalizeEmail'
import translateErrorMessage from '~/components/utils/TranslateErrorMessage'
export const SignupMutation = gql`
mutation($email: String!, $inviteCode: String) {
Signup(email: $email, inviteCode: $inviteCode) {
email
}
}
`
export default {
name: 'RegistrationSlideEmail',
props: {
sliderData: { type: Object, required: true },
},
data() {
return {
metadata,
formData: {
email: '',
},
formSchema: {
email: {
type: 'email',
required: true,
message: this.$t('common.validations.email'),
},
},
// TODO: Our styleguide does not support checkmarks.
// Integrate termsAndConditionsConfirmed into `this.formData` once we
// have checkmarks available.
sendEmailAgain: false,
}
},
mounted: function () {
this.$nextTick(function () {
// Code that will run only after the entire view has been rendered
this.formData.email = this.sliderData.collectedInputData.email
? this.sliderData.collectedInputData.email
: ''
this.sendValidation()
this.sliderData.setSliderValuesCallback(this.validInput, {
sliderSettings: {
...this.buttonValues().sliderSettings,
buttonSliderCallback: this.onNextClick,
},
})
})
},
watch: {
sendEmailAgain() {
this.setButtonValues()
},
},
computed: {
sliderIndex() {
return this.sliderData.sliderIndex // to have a shorter notation
},
validInput() {
return isEmail(this.formData.email)
},
},
methods: {
async sendValidation() {
if (this.formData.email && isEmail(this.formData.email)) {
this.formData.email = normalizeEmail(this.formData.email)
}
const { email } = this.formData
this.sliderData.setSliderValuesCallback(this.validInput, { collectedInputData: { email } })
},
async handleInput() {
this.sendValidation()
},
async handleInputValid() {
this.sendValidation()
},
buttonValues() {
return {
sliderSettings: {
buttonTitleIdent: this.sliderData.collectedInputData.emailSend
? this.sendEmailAgain
? 'components.registration.email.buttonTitle.resend'
: 'components.registration.email.buttonTitle.skipResend'
: 'components.registration.email.buttonTitle.send',
buttonIcon: this.sliderData.collectedInputData.emailSend
? this.sendEmailAgain
? 'envelope'
: 'arrow-right'
: 'envelope',
},
}
},
setButtonValues() {
this.sliderData.setSliderValuesCallback(this.validInput, this.buttonValues())
},
isVariablesRequested(variables) {
return (
this.sliderData.sliders[this.sliderIndex].data.request &&
this.sliderData.sliders[this.sliderIndex].data.request.variables &&
this.sliderData.sliders[this.sliderIndex].data.request.variables.email === variables.email
)
},
async onNextClick() {
const { email } = this.formData
const { inviteCode = null } = this.sliderData.collectedInputData
const variables = { email, inviteCode }
if (this.sliderData.collectedInputData.emailSend && !this.sendEmailAgain) {
return true
}
if (
!this.sliderData.collectedInputData.emailSend ||
this.sendEmailAgain ||
!this.isVariablesRequested(variables)
) {
try {
this.sliderData.setSliderValuesCallback(null, {
sliderSettings: { buttonLoading: true },
})
const response = await this.$apollo.mutate({ mutation: SignupMutation, variables }) // e-mail is send in emailMiddleware of backend
this.sliderData.setSliderValuesCallback(null, {
sliderData: { request: { variables }, response: response.data },
})
if (this.sliderData.sliders[this.sliderIndex].data.response) {
this.sliderData.setSliderValuesCallback(this.validInput, {
collectedInputData: { emailSend: true },
})
this.setButtonValues()
const { email: responseEmail } = this.sliderData.sliders[
this.sliderIndex
].data.response.Signup
this.$toast.success(
this.$t('components.registration.email.form.success', { email: responseEmail }),
)
}
this.sliderData.setSliderValuesCallback(null, {
sliderSettings: { buttonLoading: false },
})
return true
} catch (err) {
this.sliderData.setSliderValuesCallback(this.validInput, {
sliderData: { request: null, response: null },
collectedInputData: { emailSend: false },
sliderSettings: { buttonLoading: false },
})
this.setButtonValues()
this.$toast.error(
translateErrorMessage(
err.message,
{
'A user account with this email already exists':
'components.registration.signup.form.errors.email-exists',
},
this.$t,
),
)
return false
}
}
},
},
}
</script>
<style>
.space-top {
margin-top: 6ex;
}
</style>

View File

@ -7,14 +7,14 @@
@input-valid="handleInputValid"
>
<ds-input
:placeholder="$t('components.enter-invite.form.invite-code')"
:placeholder="$t('components.registration.invite-code.form.invite-code')"
model="inviteCode"
name="inviteCode"
id="inviteCode"
icon="question-circle"
/>
<ds-text>
{{ $t('components.enter-invite.form.description') }}
{{ $t('components.registration.invite-code.form.description') }}
</ds-text>
<slot></slot>
</ds-form>
@ -29,7 +29,7 @@ export const isValidInviteCodeQuery = gql`
}
`
export default {
name: 'RegistrationItemEnterInvite',
name: 'RegistrationSlideInvite',
props: {
sliderData: { type: Object, required: true },
},
@ -41,17 +41,19 @@ export default {
formSchema: {
inviteCode: {
type: 'string',
// Wolle min: 6,
// max: 6,
min: 6,
max: 6,
required: true,
message: this.$t('components.enter-invite.form.validations.length'),
message: this.$t('components.registration.invite-code.form.validations.length'),
},
},
dbRequestInProgress: false,
}
},
mounted: function () {
this.$nextTick(function () {
// Code that will run only after the entire view has been rendered
this.formData.inviteCode = this.sliderData.collectedInputData.inviteCode
? this.sliderData.collectedInputData.inviteCode
: ''
@ -74,12 +76,14 @@ export default {
async sendValidation() {
const { inviteCode } = this.formData
this.sliderData.setSliderValuesCallback(null, { collectedInputData: { inviteCode } })
let dbValidated = false
if (this.validInput) {
await this.handleSubmitVerify()
dbValidated = this.sliderData.sliders[this.sliderIndex].data.response.isValidInviteCode
}
this.sliderData.setSliderValuesCallback(dbValidated, { collectedInputData: { inviteCode } })
this.sliderData.setSliderValuesCallback(dbValidated)
},
async handleInput() {
this.sendValidation()
@ -87,45 +91,53 @@ export default {
async handleInputValid() {
this.sendValidation()
},
isVariablesRequested(variables) {
return (
this.sliderData.sliders[this.sliderIndex].data.request &&
this.sliderData.sliders[this.sliderIndex].data.request.variables &&
this.sliderData.sliders[this.sliderIndex].data.request.variables.code === variables.code
)
},
async handleSubmitVerify() {
const { inviteCode } = this.formData
const { inviteCode } = this.sliderData.collectedInputData
const variables = { code: inviteCode }
if (
!this.sliderData.sliders[this.sliderIndex].data.request ||
(this.sliderData.sliders[this.sliderIndex].data.request &&
(!this.sliderData.sliders[this.sliderIndex].data.request.variables ||
(this.sliderData.sliders[this.sliderIndex].data.request.variables &&
!this.sliderData.sliders[this.sliderIndex].data.request.variables === variables)))
) {
this.sliderData.setSliderValuesCallback(
this.sliderData.sliders[this.sliderIndex].validated,
{ sliderData: { request: { variables }, response: null } },
)
if (!this.isVariablesRequested(variables) && !this.dbRequestInProgress) {
try {
const response = await this.$apollo.query({ query: isValidInviteCodeQuery, variables })
this.sliderData.setSliderValuesCallback(
this.sliderData.sliders[this.sliderIndex].validated,
{ sliderData: { response: response.data } },
)
this.dbRequestInProgress = true
if (
this.sliderData.sliders[this.sliderIndex].data.response &&
this.sliderData.sliders[this.sliderIndex].data.response.isValidInviteCode
) {
this.$toast.success(
this.$t('components.registration.invite-code.form.success', { inviteCode }),
)
const response = await this.$apollo.query({ query: isValidInviteCodeQuery, variables })
this.sliderData.setSliderValuesCallback(null, {
sliderData: {
request: { variables },
response: response.data,
},
})
if (this.sliderData.sliders[this.sliderIndex].data.response) {
if (this.sliderData.sliders[this.sliderIndex].data.response.isValidInviteCode) {
this.$toast.success(
this.$t('components.registration.invite-code.form.validations.success', {
inviteCode,
}),
)
} else {
this.$toast.error(
this.$t('components.registration.invite-code.form.validations.error', {
inviteCode,
}),
)
}
}
} catch (err) {
this.sliderData.setSliderValuesCallback(
this.sliderData.sliders[this.sliderIndex].validated,
{ sliderData: { response: { isValidInviteCode: false } } },
)
this.sliderData.setSliderValuesCallback(false, {
sliderData: { response: { isValidInviteCode: false } },
})
const { message } = err
this.$toast.error(message)
} finally {
this.dbRequestInProgress = false
}
}
},

View File

@ -0,0 +1,35 @@
<template>
<ds-space centered>
<hc-empty icon="events" :message="$t('components.registration.signup.unavailable')" />
<slot></slot>
</ds-space>
</template>
<script>
import HcEmpty from '~/components/Empty/Empty'
export default {
name: 'RegistrationSlideNoPublic',
components: {
HcEmpty,
},
props: {
sliderData: { type: Object, required: true },
},
mounted: function () {
this.$nextTick(function () {
// Code that will run only after the entire view has been rendered
this.sliderData.setSliderValuesCallback(true, {
sliderSettings: { buttonSliderCallback: this.onNextClick },
})
})
},
methods: {
onNextClick() {
this.$router.history.push('/login')
return true
},
},
}
</script>

View File

@ -0,0 +1,168 @@
<template>
<ds-form
class="enter-nonce"
v-model="formData"
:schema="formSchema"
@submit="handleSubmitVerify"
@input="handleInput"
@input-valid="handleInputValid"
>
<email-display-and-verify :email="sliderData.collectedInputData.email" />
<ds-input
:placeholder="$t('components.registration.email-nonce.form.nonce')"
model="nonce"
name="nonce"
id="nonce"
icon="question-circle"
/>
<ds-text>
{{ $t('components.registration.email-nonce.form.description') }}
</ds-text>
<slot></slot>
</ds-form>
</template>
<script>
import gql from 'graphql-tag'
import { isEmail } from 'validator'
import EmailDisplayAndVerify from './EmailDisplayAndVerify'
export const verifyNonceQuery = gql`
query($email: String!, $nonce: String!) {
VerifyNonce(email: $email, nonce: $nonce)
}
`
export default {
name: 'RegistrationSlideNonce',
components: {
EmailDisplayAndVerify,
},
props: {
sliderData: { type: Object, required: true },
},
data() {
return {
formData: {
nonce: '',
},
formSchema: {
nonce: {
type: 'string',
min: 5,
max: 5,
required: true,
message: this.$t('components.registration.email-nonce.form.validations.length'),
},
},
dbRequestInProgress: false,
}
},
mounted: function () {
this.$nextTick(function () {
// Code that will run only after the entire view has been rendered
this.formData.nonce = this.sliderData.collectedInputData.nonce
? this.sliderData.collectedInputData.nonce
: ''
this.sendValidation()
this.sliderData.setSliderValuesCallback(this.validInput, {
sliderSettings: { buttonSliderCallback: this.onNextClick },
})
})
},
computed: {
sliderIndex() {
return this.sliderData.sliderIndex // to have a shorter notation
},
validInput() {
return this.formData.nonce.length === 5
},
isEmailFormat() {
return isEmail(this.sliderData.collectedInputData.email)
},
},
methods: {
async sendValidation() {
const { nonce } = this.formData
this.sliderData.setSliderValuesCallback(null, { collectedInputData: { nonce } })
let dbValidated = false
if (this.validInput) {
await this.handleSubmitVerify()
dbValidated = this.sliderData.sliders[this.sliderIndex].data.response.VerifyNonce
}
this.sliderData.setSliderValuesCallback(dbValidated)
},
async handleInput() {
this.sendValidation()
},
async handleInputValid() {
this.sendValidation()
},
isVariablesRequested(variables) {
return (
this.sliderData.sliders[this.sliderIndex].data.request &&
this.sliderData.sliders[this.sliderIndex].data.request.variables &&
this.sliderData.sliders[this.sliderIndex].data.request.variables.email ===
variables.email &&
this.sliderData.sliders[this.sliderIndex].data.request.variables.nonce === variables.nonce
)
},
async handleSubmitVerify() {
const { email, nonce } = this.sliderData.collectedInputData
const variables = { email, nonce }
if (!this.isVariablesRequested(variables) && !this.dbRequestInProgress) {
try {
this.dbRequestInProgress = true
const response = await this.$apollo.query({ query: verifyNonceQuery, variables })
this.sliderData.setSliderValuesCallback(null, {
sliderData: { request: { variables }, response: response.data },
})
if (this.sliderData.sliders[this.sliderIndex].data.response) {
if (this.sliderData.sliders[this.sliderIndex].data.response.VerifyNonce) {
this.$toast.success(
this.$t('components.registration.email-nonce.form.validations.success', {
email,
nonce,
}),
)
} else {
this.$toast.error(
this.$t('components.registration.email-nonce.form.validations.error', {
email,
nonce,
}),
)
}
}
} catch (err) {
this.sliderData.setSliderValuesCallback(false, {
sliderData: { response: { VerifyNonce: false } },
})
const { message } = err
this.$toast.error(message)
} finally {
this.dbRequestInProgress = false
}
}
},
onNextClick() {
return true
},
},
}
</script>
<style lang="scss">
.enter-nonce {
display: flex;
flex-direction: column;
margin: $space-large 0 $space-xxx-small 0;
}
</style>

View File

@ -1,8 +1,10 @@
import { storiesOf } from '@storybook/vue'
import { withA11y } from '@storybook/addon-a11y'
import RegistrationSlider from './RegistrationSlider.vue'
import { action } from '@storybook/addon-actions'
import Vuex from 'vuex'
import helpers from '~/storybook/helpers'
import Vue from 'vue'
import RegistrationSlider from './RegistrationSlider.vue'
const plugins = [
(app = {}) => {
@ -14,11 +16,8 @@ const plugins = [
if (JSON.stringify(data).includes('Signup')) {
return { data: { Signup: { email: data.variables.email } } }
}
if (JSON.stringify(data).includes('SignupByInvitation')) {
return { data: { SignupByInvitation: { email: data.variables.email } } }
}
if (JSON.stringify(data).includes('SignupVerification')) {
return { data: { SignupByInvitation: { ...data.variables } } }
return { data: { SignupVerification: { ...data.variables } } }
}
throw new Error(`Mutation name not found!`)
},
@ -26,6 +25,9 @@ const plugins = [
if (JSON.stringify(data).includes('isValidInviteCode')) {
return { data: { isValidInviteCode: true } }
}
if (JSON.stringify(data).includes('VerifyNonce')) {
return { data: { VerifyNonce: true } }
}
throw new Error(`Query name not found!`)
},
}
@ -35,12 +37,51 @@ const plugins = [
]
helpers.init({ plugins })
const createStore = ({ loginSuccess }) => {
return new Vuex.Store({
modules: {
auth: {
namespaced: true,
state: () => ({
pending: false,
}),
mutations: {
SET_PENDING(state, pending) {
state.pending = pending
},
},
getters: {
pending(state) {
return !!state.pending
},
},
actions: {
async login({ commit, dispatch }, args) {
action('Vuex action `auth/login`')(args)
return new Promise((resolve, reject) => {
commit('SET_PENDING', true)
setTimeout(() => {
commit('SET_PENDING', false)
if (loginSuccess) {
resolve(loginSuccess)
} else {
reject(new Error('Login unsuccessful'))
}
}, 1000)
})
},
},
},
},
})
}
storiesOf('RegistrationSlider', module)
.addDecorator(withA11y)
.addDecorator(helpers.layout)
.add('invite-code empty', () => ({
components: { RegistrationSlider },
store: helpers.store,
store: createStore({ loginSuccess: true }),
data: () => ({}),
template: `
<registration-slider registrationType="invite-code" />
@ -48,23 +89,19 @@ storiesOf('RegistrationSlider', module)
}))
.add('invite-code with data', () => ({
components: { RegistrationSlider },
store: helpers.store,
store: createStore({ loginSuccess: true }),
data: () => ({
overwriteSliderData: {
collectedInputData: {
inviteCode: 'IN1T6Y',
inviteCode: 'INZTBY',
email: 'wolle.huss@pjannto.com',
emailSend: false,
nonce: 'NTRSCZ',
nonce: '47539',
name: 'Wolle',
password: 'Hello',
passwordConfirmation: 'Hello',
about: `Hey`,
termsAndConditionsConfirmed: true,
dataPrivacy: true,
minimumAge: true,
noCommercial: true,
noPolitical: true,
recieveCommunicationAsEmailsEtcConfirmed: true,
},
},
}),
@ -74,7 +111,7 @@ storiesOf('RegistrationSlider', module)
}))
.add('public-registration empty', () => ({
components: { RegistrationSlider },
store: helpers.store,
store: createStore({ loginSuccess: true }),
data: () => ({}),
template: `
<registration-slider registrationType="public-registration" />
@ -82,23 +119,19 @@ storiesOf('RegistrationSlider', module)
}))
.add('public-registration with data', () => ({
components: { RegistrationSlider },
store: helpers.store,
store: createStore({ loginSuccess: true }),
data: () => ({
overwriteSliderData: {
collectedInputData: {
inviteCode: null,
email: 'wolle.huss@pjannto.com',
emailSend: false,
nonce: 'NTRSCZ',
nonce: '47539',
name: 'Wolle',
password: 'Hello',
passwordConfirmation: 'Hello',
about: `Hey`,
termsAndConditionsConfirmed: true,
dataPrivacy: true,
minimumAge: true,
noCommercial: true,
noPolitical: true,
recieveCommunicationAsEmailsEtcConfirmed: true,
},
},
}),
@ -108,7 +141,7 @@ storiesOf('RegistrationSlider', module)
}))
.add('invite-mail empty', () => ({
components: { RegistrationSlider },
store: helpers.store,
store: createStore({ loginSuccess: true }),
data: () => ({
overwriteSliderData: {
collectedInputData: {
@ -119,12 +152,8 @@ storiesOf('RegistrationSlider', module)
name: null,
password: null,
passwordConfirmation: null,
about: null,
termsAndConditionsConfirmed: null,
dataPrivacy: null,
minimumAge: null,
noCommercial: null,
noPolitical: null,
recieveCommunicationAsEmailsEtcConfirmed: null,
},
},
}),
@ -134,23 +163,19 @@ storiesOf('RegistrationSlider', module)
}))
.add('invite-mail with data', () => ({
components: { RegistrationSlider },
store: helpers.store,
store: createStore({ loginSuccess: true }),
data: () => ({
overwriteSliderData: {
collectedInputData: {
inviteCode: null,
email: 'wolle.huss@pjannto.com',
emailSend: true,
nonce: 'NTRSCZ',
nonce: '47539',
name: 'Wolle',
password: 'Hello',
passwordConfirmation: 'Hello',
about: `Hey`,
termsAndConditionsConfirmed: true,
dataPrivacy: true,
minimumAge: true,
noCommercial: true,
noPolitical: true,
recieveCommunicationAsEmailsEtcConfirmed: true,
},
},
}),
@ -158,3 +183,11 @@ storiesOf('RegistrationSlider', module)
<registration-slider registrationType="invite-mail" :overwriteSliderData="overwriteSliderData" />
`,
}))
.add('no-public-registration', () => ({
components: { RegistrationSlider },
store: createStore({ loginSuccess: true }),
data: () => ({}),
template: `
<registration-slider registrationType="no-public-registration" />
`,
}))

View File

@ -8,37 +8,27 @@
</template>
<component-slider :sliderData="sliderData">
<template #header>
<ds-heading size="h2">
{{ $t('components.registration.signup.title', metadata) }}
</ds-heading>
<template #no-public-registration>
<registration-slide-no-public :sliderData="sliderData" />
</template>
<template v-if="['invite-code'].includes(registrationType)" #enter-invite>
<registration-item-enter-invite :sliderData="sliderData" />
<template #enter-invite>
<registration-slide-invite :sliderData="sliderData" />
</template>
<template
v-if="['invite-code', 'public-registration'].includes(registrationType)"
#enter-email
>
<!-- Wolle !!! may create same source with 'webapp/pages/registration/signup.vue' -->
<!-- <signup v-if="publicRegistration" :invitation="false" @submit="handleSubmitted"> -->
<registration-item-enter-email :sliderData="sliderData" :invitation="false" />
<template #enter-email>
<registration-slide-email :sliderData="sliderData" :invitation="false" />
</template>
<template
v-if="['invite-code', 'public-registration', 'invite-mail'].includes(registrationType)"
#enter-nonce
>
<registration-item-enter-nonce :sliderData="sliderData" />
<template #enter-nonce>
<registration-slide-nonce :sliderData="sliderData" />
</template>
<template #create-user-account>
<registration-item-create-user-account :sliderData="sliderData" />
<registration-slide-create :sliderData="sliderData" />
</template>
<template #footer>
<template v-if="registrationType !== 'no-public-registration'" #footer>
<ds-space margin-bottom="xxx-small" margin-top="small" centered>
<nuxt-link to="/login">{{ $t('site.back-to-login') }}</nuxt-link>
</ds-space>
@ -57,93 +47,113 @@ import links from '~/constants/links.js'
import metadata from '~/constants/metadata.js'
import ComponentSlider from '~/components/ComponentSlider/ComponentSlider'
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
import RegistrationItemCreateUserAccount from './RegistrationItemCreateUserAccount'
import RegistrationItemEnterEmail from '~/components/Registration/RegistrationItemEnterEmail'
import RegistrationItemEnterInvite from './RegistrationItemEnterInvite'
import RegistrationItemEnterNonce from './RegistrationItemEnterNonce'
import RegistrationSlideCreate from './RegistrationSlideCreate'
import RegistrationSlideEmail from './RegistrationSlideEmail'
import RegistrationSlideInvite from './RegistrationSlideInvite'
import RegistrationSlideNonce from './RegistrationSlideNonce'
import RegistrationSlideNoPublic from './RegistrationSlideNoPublic'
export default {
name: 'RegistrationSlider',
components: {
ComponentSlider,
LocaleSwitch,
RegistrationItemCreateUserAccount,
RegistrationItemEnterEmail,
RegistrationItemEnterInvite,
RegistrationItemEnterNonce,
RegistrationSlideCreate,
RegistrationSlideEmail,
RegistrationSlideInvite,
RegistrationSlideNonce,
RegistrationSlideNoPublic,
},
props: {
registrationType: { type: String, required: true },
overwriteSliderData: { type: Object, default: () => {} },
},
data() {
const slidersPortfolio = [
{
const slidersPortfolio = {
noPublicRegistration: {
name: 'no-public-registration',
titleIdent: 'components.registration.no-public-registrstion.title',
validated: false,
data: { request: null, response: null },
button: {
titleIdent: 'site.back-to-login',
icon: null,
callback: this.buttonCallback,
sliderCallback: null, // optional set by slot
},
},
enterInvite: {
name: 'enter-invite',
// title: this.$t('components.registration.create-user-account.title'),
title: 'Invitation', // Wolle
titleIdent: { id: 'components.registration.signup.title', data: metadata },
validated: false,
data: { request: null, response: { isValidInviteCode: false } },
button: {
title: 'Next', // Wolle
titleIdent: 'components.registration.invite-code.buttonTitle',
icon: 'arrow-right',
callback: this.buttonCallback,
sliderCallback: null, // optional set by slot
},
},
{
enterEmail: {
name: 'enter-email',
title: 'E-Mail', // Wolle
titleIdent: 'components.registration.email.title',
validated: false,
data: { request: null, response: null },
button: {
title: '', // set by slider component
icon: '', // set by slider component
titleIdent: 'components.registration.email.buttonTitle.send', // changed by slider component
icon: 'envelope', // changed by slider component
callback: this.buttonCallback,
sliderCallback: null, // optional set by slot
},
},
{
enterNonce: {
name: 'enter-nonce',
title: 'E-Mail Confirmation', // Wolle
titleIdent: 'components.registration.email-nonce.title',
validated: false,
data: { request: null, response: null },
data: { request: null, response: { VerifyNonce: false } },
button: {
title: 'Confirm', // Wolle
titleIdent: 'components.registration.email-nonce.buttonTitle',
icon: 'arrow-right',
callback: this.buttonCallback,
sliderCallback: null, // optional set by slot
},
},
{
createUserAccount: {
name: 'create-user-account',
title: this.$t('components.registration.create-user-account.title'),
titleIdent: 'components.registration.create-user-account.title',
validated: false,
data: { request: null, response: null },
button: {
// title: this.$t('actions.save'), // Wolle
title: 'Create', // Wolle
titleIdent: 'components.registration.create-user-account.buttonTitle',
icon: 'check',
loading: false,
callback: this.buttonCallback,
sliderCallback: null, // optional set by slot
},
},
]
}
let sliders = []
switch (this.registrationType) {
case 'no-public-registration':
sliders = [slidersPortfolio.noPublicRegistration]
break
case 'invite-code':
sliders = [
slidersPortfolio[0],
slidersPortfolio[1],
slidersPortfolio[2],
slidersPortfolio[3],
slidersPortfolio.enterInvite,
slidersPortfolio.enterEmail,
slidersPortfolio.enterNonce,
slidersPortfolio.createUserAccount,
]
break
case 'public-registration':
sliders = [slidersPortfolio[1], slidersPortfolio[2], slidersPortfolio[3]]
sliders = [
slidersPortfolio.enterEmail,
slidersPortfolio.enterNonce,
slidersPortfolio.createUserAccount,
]
break
case 'invite-mail':
sliders = [slidersPortfolio[2], slidersPortfolio[3]]
sliders = [slidersPortfolio.enterNonce, slidersPortfolio.createUserAccount]
break
}
@ -159,12 +169,8 @@ export default {
name: null,
password: null,
passwordConfirmation: null,
about: null,
termsAndConditionsConfirmed: null,
dataPrivacy: null,
minimumAge: null,
noCommercial: null,
noPolitical: null,
recieveCommunicationAsEmailsEtcConfirmed: null,
},
sliderIndex: 0,
sliders: sliders,
@ -180,11 +186,15 @@ export default {
},
},
methods: {
setSliderValuesCallback(isValid, { collectedInputData, sliderData, sliderSettings }) {
setSliderValuesCallback(
isValid = null,
{ collectedInputData, sliderData, sliderSettings } = {},
) {
// all changes of 'this.sliders' has to be filled in from the top to be spread to the component slider and all slider components in the slot
this.sliderData.sliders[this.sliderIndex].validated = isValid
if (isValid !== null) {
this.sliderData.sliders[this.sliderIndex].validated = isValid
}
if (collectedInputData) {
this.sliderData.collectedInputData = {
...this.sliderData.collectedInputData,
@ -204,14 +214,17 @@ export default {
}
}
if (sliderSettings) {
const { buttonTitle, buttonIcon, buttonSliderCallback } = sliderSettings
if (buttonTitle) {
this.sliderData.sliders[this.sliderIndex].button.title = buttonTitle
const { buttonTitleIdent, buttonIcon, buttonLoading, buttonSliderCallback } = sliderSettings
if (buttonTitleIdent !== undefined) {
this.sliderData.sliders[this.sliderIndex].button.titleIdent = buttonTitleIdent
}
if (buttonIcon) {
if (buttonIcon !== undefined) {
this.sliderData.sliders[this.sliderIndex].button.icon = buttonIcon
}
if (buttonSliderCallback) {
if (buttonLoading !== undefined) {
this.sliderData.sliders[this.sliderIndex].button.loading = buttonLoading
}
if (buttonSliderCallback !== undefined) {
this.sliderData.sliders[this.sliderIndex].button.sliderCallback = buttonSliderCallback
}
}
@ -221,6 +234,10 @@ export default {
if (selectedIndex <= this.sliderIndex + 1 && selectedIndex < this.sliderData.sliders.length) {
this.sliderData.sliderIndex = selectedIndex
if (this.sliderData.sliders[this.sliderIndex].button.loading !== undefined) {
this.sliderData.sliders[this.sliderIndex].button.loading = false
}
}
},
buttonCallback(success) {

View File

@ -1,5 +1,5 @@
import { config, mount } from '@vue/test-utils'
import Signup, { SignupMutation, SignupByInvitationMutation } from './Signup'
import Signup, { SignupMutation } from './Signup'
const localVue = global.localVue
@ -58,7 +58,8 @@ describe('Signup', () => {
it('delivers email to backend', () => {
const expected = expect.objectContaining({
variables: { email: 'mAIL@exAMPLE.org', token: null },
mutation: SignupMutation,
variables: { email: 'mAIL@exAMPLE.org', inviteCode: null },
})
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
@ -84,68 +85,5 @@ describe('Signup', () => {
})
})
})
describe('with invitation code', () => {
let action
beforeEach(() => {
propsData.token = '666777'
action = async () => {
wrapper = Wrapper()
wrapper.find('input#email').setValue('mail@example.org')
await wrapper.find('form').trigger('submit')
await wrapper.html()
}
})
describe('submit', () => {
it('calls SignupByInvitation graphql mutation', async () => {
await action()
const expected = expect.objectContaining({ mutation: SignupByInvitationMutation })
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
it('delivers invitation token to backend', async () => {
await action()
const expected = expect.objectContaining({
variables: { email: 'mail@example.org', token: '666777' },
})
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
})
describe('in case a user account with the email already exists', () => {
beforeEach(() => {
mocks.$apollo.mutate = jest
.fn()
.mockRejectedValue(
new Error('UserInputError: A user account with this email already exists.'),
)
})
it('explains the error', async () => {
await action()
expect(mocks.$t).toHaveBeenCalledWith(
'components.registration.signup.form.errors.email-exists',
)
})
})
describe('in case the invitation code was incorrect', () => {
beforeEach(() => {
mocks.$apollo.mutate = jest
.fn()
.mockRejectedValue(
new Error('UserInputError: Invitation code already used or does not exist.'),
)
})
it('explains the error', async () => {
await action()
expect(mocks.$t).toHaveBeenCalledWith(
'components.registration.signup.form.errors.invalid-invitation-token',
)
})
})
})
})
})
})

View File

@ -14,9 +14,6 @@
: $t('components.registration.signup.title', metadata)
}}
</h1>
<ds-space v-if="token" margin-botton="large">
<ds-text v-html="$t('registration.signup.form.invitation-code', { code: token })" />
</ds-space>
<ds-space margin-botton="large">
<ds-text>
{{
@ -70,17 +67,11 @@
import gql from 'graphql-tag'
import metadata from '~/constants/metadata'
import { SweetalertIcon } from 'vue-sweetalert-icons'
import translateErrorMessage from '~/components/utils/TranslateErrorMessage'
export const SignupMutation = gql`
mutation($email: String!) {
Signup(email: $email) {
email
}
}
`
export const SignupByInvitationMutation = gql`
mutation($email: String!, $token: String!) {
SignupByInvitation(email: $email, token: $token) {
mutation($email: String!, $inviteCode: String) {
Signup(email: $email, inviteCode: $inviteCode) {
email
}
}
@ -91,7 +82,6 @@ export default {
SweetalertIcon,
},
props: {
token: { type: String, default: null },
invitation: { type: Boolean, default: false },
},
data() {
@ -126,32 +116,28 @@ export default {
this.disabled = false
},
async handleSubmit() {
const mutation = this.token ? SignupByInvitationMutation : SignupMutation
const { token } = this
const { email } = this.formData
try {
const response = await this.$apollo.mutate({ mutation, variables: { email, token } })
const response = await this.$apollo.mutate({
mutation: SignupMutation,
variables: { email, inviteCode: null },
})
this.data = response.data
setTimeout(() => {
this.$emit('submit', { email: this.data.Signup.email })
}, 3000)
} catch (err) {
const { message } = err
const mapping = {
'A user account with this email already exists': 'email-exists',
'Invitation code already used or does not exist': 'invalid-invitation-token',
}
for (const [pattern, key] of Object.entries(mapping)) {
if (message.includes(pattern))
this.error = {
key,
message: this.$t(`components.registration.signup.form.errors.${key}`),
}
}
if (!this.error) {
this.$toast.error(message)
}
this.$toast.error(
translateErrorMessage(
err.message,
{
'A user account with this email already exists':
'components.registration.signup.form.errors.email-exists',
},
this.$t,
),
)
}
},
},

View File

@ -0,0 +1,12 @@
export default (message, mapping, translate) => {
let translatedMessage = null
for (const [pattern, ident] of Object.entries(mapping)) {
if (message.includes(pattern)) {
translatedMessage = translate(ident)
}
}
if (!translatedMessage) {
translatedMessage = message
}
return translatedMessage
}

View File

@ -4,6 +4,7 @@ export const SignupVerificationMutation = gql`
$nonce: String!
$name: String!
$email: String!
$inviteCode: String
$password: String!
$about: String
$termsAndConditionsAgreedVersion: String!
@ -12,6 +13,7 @@ export const SignupVerificationMutation = gql`
SignupVerification(
nonce: $nonce
email: $email
inviteCode: $inviteCode
name: $name
password: $password
about: $about

View File

@ -120,26 +120,6 @@
"versus": "Versus"
},
"components": {
"enter-invite": {
"form": {
"description": "Gib den Einladungs-Code ein, den du bekommen hast.",
"invite-code": "Einladungs-Code eingeben",
"next": "Weiter",
"validations": {
"length": "muss genau 6 Buchstaben lang sein"
}
}
},
"enter-nonce": {
"form": {
"description": "Öffne Dein E-Mail Postfach und gib den Code ein, den wir geschickt haben.",
"next": "Weiter",
"nonce": "Code eingeben",
"validations": {
"length": "muss genau 6 Buchstaben lang sein"
}
}
},
"password-reset": {
"change-password": {
"error": "Passwort Änderung fehlgeschlagen. Möglicherweise falscher Sicherheitscode?",
@ -157,33 +137,68 @@
},
"registration": {
"create-user-account": {
"buttonTitle": "Erstellen",
"error": "Es konnte kein Benutzerkonto erstellt werden!",
"help": "Vielleicht war der Bestätigungscode falsch oder abgelaufen? Wenn das Problem weiterhin besteht, schicke uns gerne eine E-Mail an:",
"recieveCommunicationAsEmailsEtcConfirmed": "Ich stimme auch dem Erhalt von E-Mails und anderen Formen der Kommunikation (z.B. Push-Benachrichtigungen) zu.",
"success": "Dein Benutzerkonto wurde erstellt!",
"termsAndCondsEtcConfirmed": "Ich habe folgendes gelesen, verstanden und stimme zu:",
"title": "Benutzerkonto anlegen"
},
"email": {
"buttonTitle": {
"resend": "Erneut senden",
"send": "Sende E-Mail",
"skipResend": "Nicht senden"
},
"form": {
"sendEmailAgain": "E-Mail erneut senden",
"success": "Verifikations-E-Mail gesendet an <b>{email}</b>!"
}
},
"title": "E-Mail"
},
"email-display": {
"warningFormat": "⚠️ E-Mail hat ein ungültiges Format!",
"warningUndef": "⚠️ Keine E-Mail definiert!",
"yourEmail": "Deine E-Mail-Adresse:"
},
"email-nonce": {
"buttonTitle": "Bestätigen",
"form": {
"description": "Öffne Dein E-Mail Postfach und gib den Code ein, den wir geschickt haben.",
"next": "Weiter",
"nonce": "E-Mail-Code: 32143",
"validations": {
"error": "Ungültiger Bestätigungs-Code <b>{nonce}</b> für E-Mail <b>{email}</b>!",
"length": "muss genau 5 Buchstaben lang sein",
"success": "Gültiger Bestätigungs-Code <b>{nonce}</b> für E-Mail <b>{email}</b>!"
}
},
"title": "E-Mail Bestätigung"
},
"invite-code": {
"buttonTitle": "Weiter",
"form": {
"success": "Gültiger Einladungs-Code <b>{inviteCode}</b>!"
"description": "Gib den Einladungs-Code ein, den du bekommen hast.",
"invite-code": "Einladungs-Code: ACJERB",
"next": "Weiter",
"validations": {
"error": "Ungültiger Einladungs-Code <b>{inviteCode}</b>!",
"length": "muss genau 6 Buchstaben lang sein",
"success": "Gültiger Einladungs-Code <b>{inviteCode}</b>!"
}
}
},
"no-public-registrstion": {
"title": "Keine öffentliche Registrierung möglich"
},
"signup": {
"form": {
"data-privacy": "Ich habe die Datenschutzerklärung gelesen und verstanden.",
"description": "Um loszulegen, kannst Du Dich hier kostenfrei registrieren:",
"errors": {
"email-exists": "Es gibt schon ein Benutzerkonto mit dieser E-Mail-Adresse!",
"invalid-invitation-token": "Es sieht so aus, als ob der Einladungscode schon eingelöst wurde. Jeder Code kann nur einmalig benutzt werden."
"email-exists": "Es gibt schon ein Benutzerkonto mit dieser E-Mail-Adresse!"
},
"invitation-code": "Dein Einladungscode lautet: <b>{code}</b>",
"minimum-age": "Ich bin 18 Jahre oder älter.",
"no-commercial": "Ich habe keine kommerziellen Absichten und ich repräsentiere kein kommerzielles Unternehmen oder Organisation.",
"no-political": "Ich bin nicht im Auftrag einer Partei oder politischen Organisation im Netzwerk.",
"submit": "Konto erstellen",
"success": "Eine E-Mail mit einem Link zum Abschließen Deiner Registrierung wurde an <b>{email}</b> geschickt",
"terms-and-condition": "Ich stimme den <a href=\"/terms-and-conditions\" target=\"_blank\"><ds-text bold color=\"primary\">Nutzungsbedingungen</ds-text></a> zu."
@ -786,9 +801,7 @@
}
},
"termsAndConditions": {
"agree": "Ich stimme zu!",
"newTermsAndConditions": "Neue Nutzungsbedingungen",
"termsAndConditionsConfirmed": "Ich habe die Nutzungsbedingungen durchgelesen und stimme ihnen zu.",
"termsAndConditionsNewConfirm": "Ich habe die neuen Nutzungsbedingungen durchgelesen und stimme zu.",
"termsAndConditionsNewConfirmText": "Bitte lies Dir die neuen Nutzungsbedingungen jetzt durch!"
},

View File

@ -120,26 +120,6 @@
"versus": "Versus"
},
"components": {
"enter-invite": {
"form": {
"description": "Enter the invitation code you received.",
"invite-code": "Enter your invite code",
"next": "Continue",
"validations": {
"length": "must be 6 characters long"
}
}
},
"enter-nonce": {
"form": {
"description": "Open your inbox and enter the code that we've sent to you.",
"next": "Continue",
"nonce": "Enter your code",
"validations": {
"length": "must be 6 characters long"
}
}
},
"password-reset": {
"change-password": {
"error": "Changing your password failed. Maybe the security code was not correct?",
@ -157,33 +137,68 @@
},
"registration": {
"create-user-account": {
"buttonTitle": "Create",
"error": "No user account could be created!",
"help": " Maybe the confirmation was invalid? In case of problems, feel free to ask for help by sending us a mail to:",
"recieveCommunicationAsEmailsEtcConfirmed": "I also agree to receive e-mails and other forms of communication (e.g. push notifications).",
"success": "Your account has been created!",
"termsAndCondsEtcConfirmed": "I have read, understand and agree to the following:",
"title": "Create user account"
},
"email": {
"buttonTitle": {
"resend": "Resend e-mail",
"send": "Send e-mail",
"skipResend": "Skip resend"
},
"form": {
"sendEmailAgain": "Send e-mail again",
"success": "Verification e-mail send to <b>{email}</b>!"
}
},
"title": "E-Mail"
},
"email-display": {
"warningFormat": "⚠️ E-mail has wrong format!",
"warningUndef": "⚠️ No e-mail defined!",
"yourEmail": "Your e-mail address:"
},
"email-nonce": {
"buttonTitle": "Confirm",
"form": {
"description": "Open your inbox and enter the code that we've sent to you.",
"next": "Continue",
"nonce": "E-mail code: 32143",
"validations": {
"error": "Invalid verification code <b>{nonce}</b> for e-mail <b>{email}</b>!",
"length": "must be 5 characters long",
"success": "Valid verification code <b>{nonce}</b> for e-mail <b>{email}</b>!"
}
},
"title": "E-Mail Confirmation"
},
"invite-code": {
"buttonTitle": "Next",
"form": {
"success": "Valid invite code <b>{inviteCode}</b>!"
"description": "Enter the invitation code you received.",
"invite-code": "Invite code: ACJERB",
"next": "Continue",
"validations": {
"error": "Invalid invite code <b>{inviteCode}</b>!",
"length": "must be 6 characters long",
"success": "Valid invite code <b>{inviteCode}</b>!"
}
}
},
"no-public-registrstion": {
"title": "No Public Registration"
},
"signup": {
"form": {
"data-privacy": "I have read and understood the privacy statement.",
"description": "To get started, you can register here for free:",
"errors": {
"email-exists": "There is already a user account with this e-mail address!",
"invalid-invitation-token": "It looks like as if the invitation has been used already. Invitation links can only be used once."
"email-exists": "There is already a user account with this e-mail address!"
},
"invitation-code": "Your invitation code is: <b>{code}</b>",
"minimum-age": "I'm 18 years or older.",
"no-commercial": "I have no commercial interests and I am not representing a company or any other commercial organisation on the network.",
"no-political": "I am not on behalf of a party or political organization in the network.",
"submit": "Create an account",
"success": "A mail with a link to complete your registration has been sent to <b>{email}</b>",
"terms-and-condition": "I confirm to the <a href=\"/terms-and-conditions\" target=\"_blank\"><ds-text bold color=\"primary\">Terms and conditions</ds-text></a>."
@ -786,9 +801,7 @@
}
},
"termsAndConditions": {
"agree": "I agree!",
"newTermsAndConditions": "New Terms and Conditions",
"termsAndConditionsConfirmed": "I have read and confirmed the terms and conditions.",
"termsAndConditionsNewConfirm": "I have read and agree to the new terms of conditions.",
"termsAndConditionsNewConfirmText": "Please read the new terms of use now!"
},

View File

@ -116,16 +116,6 @@
"versus": "Versus"
},
"components": {
"enter-nonce": {
"form": {
"description": "Abra su buzón de correo e introduzca el código que le enviamos.",
"next": "Continuar",
"nonce": "Introduzca el código",
"validations": {
"length": "debe tener exactamente 6 letras"
}
}
},
"password-reset": {
"change-password": {
"error": "Error al cambiar la contraseña. ¿Posiblemente un código de seguridad incorrecto?",
@ -148,18 +138,23 @@
"success": "¡Su cuenta de usuario ha sido creada!",
"title": "Crear una cuenta de usuario"
},
"email-nonce": {
"form": {
"description": "Abra su buzón de correo e introduzca el código que le enviamos.",
"next": "Continuar",
"nonce": "Introduzca el código",
"validations": {
"length": "debe tener exactamente 5 letras"
}
}
},
"signup": {
"form": {
"data-privacy": "He leido y entendido la declaración de protección de datos.",
"description": "Para empezar, introduzca su dirección de correo electrónico:",
"errors": {
"email-exists": "¡Ya hay una cuenta de usuario con esta dirección de correo electrónico!",
"invalid-invitation-token": "Parece que el código de invitación ya ha sido canjeado. Cada código sólo se puede utilizar una vez."
"email-exists": "¡Ya hay una cuenta de usuario con esta dirección de correo electrónico!"
},
"invitation-code": "Su código de invitación es: <b>{code}</b>",
"minimum-age": "Tengo 18 años o más.",
"no-commercial": "No tengo intensiones comerciales y no represento una empresa u organización comercial.",
"no-political": "No estoy en la red en nombre de un partido o una organización política.",
"submit": "Crear una cuenta",
"success": "Se ha enviado un correo electrónico con un enlace de confirmación para el registro a <b>{email}</b>.",
"terms-and-condition": "Estoy de acuerdo con los términos de uso."
@ -729,9 +724,7 @@
}
},
"termsAndConditions": {
"agree": "¡Estoy de acuerdo!",
"newTermsAndConditions": "Nuevos términos de uso",
"termsAndConditionsConfirmed": "He leído y acepto los términos de uso.",
"termsAndConditionsNewConfirm": "He leído y acepto los nuevos términos de uso.",
"termsAndConditionsNewConfirmText": "¡Por favor, lea los nuevos términos de uso ahora!"
},

View File

@ -116,16 +116,6 @@
"versus": "Versus"
},
"components": {
"enter-nonce": {
"form": {
"description": "Ouvrez votre boîte de réception et entrez le code que nous vous avons envoyé.",
"next": "Continuer",
"nonce": "Entrez votre code",
"validations": {
"length": "doit comporter 6 caractères"
}
}
},
"password-reset": {
"change-password": {
"error": "La modification de votre mot de passe a échoué. Peut-être que le code de sécurité n'était pas correct ?",
@ -148,18 +138,23 @@
"success": "Votre compte a été créé!",
"title": "Créer un compte utilisateur"
},
"email-nonce": {
"form": {
"description": "Ouvrez votre boîte de réception et entrez le code que nous vous avons envoyé.",
"next": "Continuer",
"nonce": "Entrez votre code",
"validations": {
"length": "doit comporter 5 caractères"
}
}
},
"signup": {
"form": {
"data-privacy": "J'ai lu et compris la Déclaration de confidentialité.",
"description": "Pour commencer, entrez votre adresse mail :",
"errors": {
"email-exists": "Il existe déjà un compte utilisateur avec cette adresse mail!",
"invalid-invitation-token": "On dirait que l'invitation a déjà été utilisée. Les liens d'invitation ne peuvent être utilisés qu'une seule fois."
"email-exists": "Il existe déjà un compte utilisateur avec cette adresse mail!"
},
"invitation-code": "Votre code d'invitation est: <b> {code} </b>",
"minimum-age": "J'ai 18 ans ou plus.",
"no-commercial": "Je n'ai aucun intérêt commercial et je ne représente pas d'entreprise ou toute autre organisation commerciale sur le réseau.",
"no-political": "Je ne parle pas au nom d'un parti ou d'une organisation politique sur le réseau.",
"submit": "Créer un compte",
"success": "Un mail avec un lien pour compléter votre inscription a été envoyé à <b>{email}</b>",
"terms-and-condition": "Je confirme les Conditions générales."
@ -697,9 +692,7 @@
}
},
"termsAndConditions": {
"agree": "J'accepte!",
"newTermsAndConditions": "Nouvelles conditions générales",
"termsAndConditionsConfirmed": "J'ai lu et accepte les conditions générales.",
"termsAndConditionsNewConfirm": "J'ai lu et accepté les nouvelles conditions générales.",
"termsAndConditionsNewConfirmText": "Veuillez lire les nouvelles conditions d'utilisation dès maintenant !"
},

View File

@ -123,16 +123,6 @@
"versus": "Verso"
},
"components": {
"enter-nonce": {
"form": {
"description": null,
"next": null,
"nonce": null,
"validations": {
"length": null
}
}
},
"password-reset": {
"change-password": {
"error": "Modifica della password non riuscita. Forse il codice di sicurezza non era corretto?",
@ -155,16 +145,23 @@
"success": null,
"title": null
},
"email-nonce": {
"form": {
"description": null,
"next": null,
"nonce": null,
"validations": {
"length": null
}
}
},
"signup": {
"form": {
"data-privacy": null,
"description": null,
"errors": {
"email-exists": null,
"invalid-invitation-token": "Sembra che l'invito sia già stato utilizzato. I link di invito possono essere utilizzati una sola volta."
"email-exists": null
},
"invitation-code": null,
"minimum-age": null,
"submit": null,
"success": null,
"terms-and-condition": null
@ -645,9 +642,7 @@
}
},
"termsAndConditions": {
"agree": "Sono d'accordo!",
"newTermsAndConditions": "Nuovi Termini e Condizioni",
"termsAndConditionsConfirmed": "Ho letto e confermato i Termini e condizioni.",
"termsAndConditionsNewConfirm": "Ho letto e accetto le nuove condizioni generali di contratto.",
"termsAndConditionsNewConfirmText": "Si prega di leggere le nuove condizioni d'uso ora!"
},

View File

@ -83,16 +83,6 @@
"versus": "werset"
},
"components": {
"enter-nonce": {
"form": {
"description": "Otwórz swoją skrzynkę odbiorczą i wpisz kod, który do Ciebie wysłaliśmy.",
"next": "Kontynuuj",
"nonce": "Wprowadź swój kod",
"validations": {
"length": "musi mieć długość 6 znaków."
}
}
},
"password-reset": {
"change-password": {
"error": "Zmiana hasła nie powiodła się. Może kod bezpieczeństwa nie był poprawny?",
@ -107,6 +97,18 @@
},
"title": "Zresetuj hasło"
}
},
"registration": {
"email-nonce": {
"form": {
"description": "Otwórz swoją skrzynkę odbiorczą i wpisz kod, który do Ciebie wysłaliśmy.",
"next": "Kontynuuj",
"nonce": "Wprowadź swój kod",
"validations": {
"length": "musi mieć długość 5 znaków."
}
}
}
}
},
"contribution": {

View File

@ -163,16 +163,6 @@
"versus": "Contra"
},
"components": {
"enter-nonce": {
"form": {
"description": "Abra a sua caixa de entrada e digite o código que lhe enviamos.",
"next": "Continue",
"nonce": "Digite seu código",
"validations": {
"length": "deve ter 6 caracteres"
}
}
},
"password-reset": {
"change-password": {
"error": "A alteração da sua senha falhou. Talvez o código de segurança não estava correto?",
@ -195,16 +185,23 @@
"success": "A sua conta foi criada!",
"title": "Criar uma conta de usuário"
},
"email-nonce": {
"form": {
"description": "Abra a sua caixa de entrada e digite o código que lhe enviamos.",
"next": "Continue",
"nonce": "Digite seu código",
"validations": {
"length": "deve ter 5 caracteres"
}
}
},
"signup": {
"form": {
"data-privacy": "Eu li e entendi o Política de Privacidade.",
"description": "Para começar, digite seu endereço de e-mail:",
"errors": {
"email-exists": "Já existe uma conta de usuário com este endereço de e-mail!",
"invalid-invitation-token": "Parece que o convite já foi usado. Os links para convites só podem ser usados uma vez."
"email-exists": "Já existe uma conta de usuário com este endereço de e-mail!"
},
"invitation-code": "O seu código de convite é: <b>{code}</b>",
"minimum-age": "Tenho 18 anos ou mais.",
"submit": "Criar uma conta",
"success": "Um e-mail com um link para completar o seu registo foi enviado para <b>{email}</b>",
"terms-and-condition": "Eu concordo com os Termos e condições."
@ -680,9 +677,7 @@
}
},
"termsAndConditions": {
"agree": "Eu concordo!",
"newTermsAndConditions": "Novos Termos e Condições",
"termsAndConditionsConfirmed": "Eu li e confirmei os Terms and Conditions.",
"termsAndConditionsNewConfirm": "Eu li e concordo com os novos termos de condições.",
"termsAndConditionsNewConfirmText": "Por favor, leia os novos termos de uso agora!"
},

View File

@ -116,16 +116,6 @@
"versus": "Против"
},
"components": {
"enter-nonce": {
"form": {
"description": "Откройте папку \\\"Входящие\\\" и введите код из сообщения.",
"next": "Продолжить",
"nonce": "Введите код",
"validations": {
"length": "длина должна быть 6 символов"
}
}
},
"password-reset": {
"change-password": {
"error": "Смена пароля не удалась. Может быть, код безопасности был неправильным?",
@ -148,18 +138,23 @@
"success": "Учетная запись успешно создана!",
"title": "Создать учетную запись"
},
"email-nonce": {
"form": {
"description": "Откройте папку \\\"Входящие\\\" и введите код из сообщения.",
"next": "Продолжить",
"nonce": "Введите код",
"validations": {
"length": "длина должна быть 5 символов"
}
}
},
"signup": {
"form": {
"data-privacy": "Я прочитал и понял Заявление о конфиденциальности",
"description": "Для начала работы введите свой адрес электронной почты:",
"errors": {
"email-exists": "Уже есть учетная запись пользователя с этим адресом электронной почты!",
"invalid-invitation-token": "Похоже, что приглашение уже было использовано. Ссылку из приглашения можно использовать только один раз."
"email-exists": "Уже есть учетная запись пользователя с этим адресом электронной почты!"
},
"invitation-code": "Код приглашения: <b>{code}</b>",
"minimum-age": "Мне 18 лет или более",
"no-commercial": "У меня нет коммерческих намерений, и я не представляю коммерческое предприятие или организацию.",
"no-political": "Я не от имени какой-либо партии или политической организации в сети.",
"submit": "Создать учетную запись",
"success": "Письмо со ссылкой для завершения регистрации было отправлено на <b> {email} </b>",
"terms-and-condition": "Принимаю Условия и положения."
@ -729,9 +724,7 @@
}
},
"termsAndConditions": {
"agree": "Я согласен(на)!",
"newTermsAndConditions": "Новые условия и положения",
"termsAndConditionsConfirmed": "Я прочитал(а) и подтверждаю Условия и положения.",
"termsAndConditionsNewConfirm": "Я прочитал(а) и согласен(на) с новыми условиями.",
"termsAndConditionsNewConfirmText": "Пожалуйста, ознакомьтесь с новыми условиями использования!"
},

View File

@ -22,5 +22,5 @@ export default async ({ store, env, route, redirect }) => {
params.path = route.path
}
return redirect('/registration/signup', params)
return redirect('/registration', params)
}

View File

@ -35,9 +35,7 @@ export default {
'password-reset-request',
'password-reset-enter-nonce',
'password-reset-change-password',
'registration-signup',
'registration-enter-nonce',
'registration-create-user-account',
'registration',
'pages-slug',
'terms-and-conditions',
'code-of-conduct',

View File

@ -0,0 +1,467 @@
import { config, mount } from '@vue/test-utils'
import Registration from './registration.vue'
import Vue from 'vue'
const localVue = global.localVue
config.stubs['client-only'] = '<span><slot /></span>'
config.stubs['router-link'] = '<span><slot /></span>'
config.stubs['nuxt-link'] = '<span><slot /></span>'
config.stubs['infinite-loading'] = '<span><slot /></span>'
describe('Registration', () => {
let wrapper
let Wrapper
let mocks
beforeEach(() => {
mocks = {
$t: (key) => key,
$i18n: {
locale: () => 'de',
},
$route: {
query: {},
},
$env: {},
}
})
describe('mount', () => {
Wrapper = () => {
return mount(Registration, {
mocks,
localVue,
})
}
describe('no "PUBLIC_REGISTRATION" and no "INVITE_REGISTRATION"', () => {
beforeEach(() => {
mocks.$env = {
PUBLIC_REGISTRATION: false,
INVITE_REGISTRATION: false,
}
})
it('no "method" query in URI show "RegistrationSlideNoPublic"', () => {
mocks.$route.query = {}
wrapper = Wrapper()
expect(wrapper.find('.hc-empty').exists()).toBe(true)
expect(wrapper.find('.enter-invite').exists()).toBe(false)
expect(wrapper.find('.enter-email').exists()).toBe(false)
})
describe('"method=invite-mail" in URI show "RegistrationSlideNonce"', () => {
it('no "email" query in URI', () => {
mocks.$route.query = { method: 'invite-mail' }
wrapper = Wrapper()
expect(wrapper.find('.enter-nonce').exists()).toBe(true)
})
describe('"email=user%40example.org" query in URI', () => {
it('have email displayed', () => {
mocks.$route.query = { method: 'invite-mail', email: 'user@example.org' }
wrapper = Wrapper()
expect(wrapper.find('.enter-nonce').text()).toContain('user@example.org')
})
it('"nonce=64835" query in URI have nonce in input', async () => {
mocks.$route.query = {
method: 'invite-mail',
email: 'user@example.org',
nonce: '64835',
}
wrapper = Wrapper()
await Vue.nextTick()
const form = wrapper.find('.enter-nonce')
expect(form.vm.formData.nonce).toEqual('64835')
})
})
})
describe('"method=invite-code" in URI show "RegistrationSlideNoPublic"', () => {
it('no "inviteCode" query in URI', () => {
mocks.$route.query = { method: 'invite-code' }
wrapper = Wrapper()
expect(wrapper.find('.hc-empty').exists()).toBe(true)
})
it('"inviteCode=AAAAAA" query in URI', () => {
mocks.$route.query = { method: 'invite-code', inviteCode: 'AAAAAA' }
wrapper = Wrapper()
expect(wrapper.find('.hc-empty').exists()).toBe(true)
})
})
})
describe('no "PUBLIC_REGISTRATION" but "INVITE_REGISTRATION"', () => {
beforeEach(() => {
mocks.$env = {
PUBLIC_REGISTRATION: false,
INVITE_REGISTRATION: true,
}
})
it('no "method" query in URI show "RegistrationSlideInvite"', () => {
mocks.$route.query = {}
wrapper = Wrapper()
expect(wrapper.find('.enter-invite').exists()).toBe(true)
expect(wrapper.find('.enter-email').exists()).toBe(false)
})
describe('"method=invite-mail" in URI show "RegistrationSlideNonce"', () => {
it('no "inviteCode" query in URI', () => {
mocks.$route.query = { method: 'invite-mail' }
wrapper = Wrapper()
expect(wrapper.find('.enter-nonce').exists()).toBe(true)
})
describe('"email=user%40example.org" query in URI', () => {
it('have email displayed', () => {
mocks.$route.query = { method: 'invite-mail', email: 'user@example.org' }
wrapper = Wrapper()
expect(wrapper.find('.enter-nonce').text()).toContain('user@example.org')
})
it('"nonce=64835" query in URI have nonce in input', async () => {
mocks.$route.query = {
method: 'invite-mail',
email: 'user@example.org',
nonce: '64835',
}
wrapper = Wrapper()
await Vue.nextTick()
const form = wrapper.find('.enter-nonce')
expect(form.vm.formData.nonce).toEqual('64835')
})
})
})
describe('"method=invite-code" in URI show "RegistrationSlideInvite"', () => {
it('no "inviteCode" query in URI', () => {
mocks.$route.query = { method: 'invite-code' }
wrapper = Wrapper()
expect(wrapper.find('.enter-invite').exists()).toBe(true)
})
it('"inviteCode=AAAAAA" query in URI have invite code in input', async () => {
mocks.$route.query = { method: 'invite-code', inviteCode: 'AAAAAA' }
wrapper = Wrapper()
await Vue.nextTick()
const form = wrapper.find('.enter-invite')
expect(form.vm.formData.inviteCode).toEqual('AAAAAA')
})
})
})
describe('"PUBLIC_REGISTRATION" but no "INVITE_REGISTRATION"', () => {
beforeEach(() => {
mocks.$env = {
PUBLIC_REGISTRATION: true,
INVITE_REGISTRATION: false,
}
})
it('no "method" query in URI show "RegistrationSlideEmail"', () => {
mocks.$route.query = {}
wrapper = Wrapper()
expect(wrapper.find('.enter-email').exists()).toBe(true)
expect(wrapper.find('.enter-invite').exists()).toBe(false)
})
describe('"method=invite-mail" in URI show "RegistrationSlideNonce"', () => {
it('no "inviteCode" query in URI', () => {
mocks.$route.query = { method: 'invite-mail' }
wrapper = Wrapper()
expect(wrapper.find('.enter-nonce').exists()).toBe(true)
})
describe('"email=user%40example.org" query in URI', () => {
it('have email displayed', () => {
mocks.$route.query = { method: 'invite-mail', email: 'user@example.org' }
wrapper = Wrapper()
expect(wrapper.find('.enter-nonce').text()).toContain('user@example.org')
})
it('"nonce=64835" query in URI have nonce in input', async () => {
mocks.$route.query = {
method: 'invite-mail',
email: 'user@example.org',
nonce: '64835',
}
wrapper = Wrapper()
await Vue.nextTick()
const form = wrapper.find('.enter-nonce')
expect(form.vm.formData.nonce).toEqual('64835')
})
})
})
describe('"method=invite-code" in URI show "RegistrationSlideEmail"', () => {
it('no "inviteCode" query in URI', () => {
mocks.$route.query = { method: 'invite-code' }
wrapper = Wrapper()
expect(wrapper.find('.enter-email').exists()).toBe(true)
expect(wrapper.find('.enter-invite').exists()).toBe(false)
})
})
})
describe('"PUBLIC_REGISTRATION" and "INVITE_REGISTRATION"', () => {
beforeEach(() => {
mocks.$env = {
PUBLIC_REGISTRATION: true,
INVITE_REGISTRATION: true,
}
})
it('no "method" query in URI show "RegistrationSlideEmail"', () => {
mocks.$route.query = {}
wrapper = Wrapper()
expect(wrapper.find('.enter-email').exists()).toBe(true)
expect(wrapper.find('.enter-invite').exists()).toBe(false)
})
describe('"method=invite-mail" in URI show "RegistrationSlideNonce"', () => {
it('no "inviteCode" query in URI', () => {
mocks.$route.query = { method: 'invite-mail' }
wrapper = Wrapper()
expect(wrapper.find('.enter-nonce').exists()).toBe(true)
})
describe('"email=user%40example.org" query in URI', () => {
it('have email displayed', () => {
mocks.$route.query = { method: 'invite-mail', email: 'user@example.org' }
wrapper = Wrapper()
expect(wrapper.find('.enter-nonce').text()).toContain('user@example.org')
})
it('"nonce=64835" query in URI have nonce in input', async () => {
mocks.$route.query = {
method: 'invite-mail',
email: 'user@example.org',
nonce: '64835',
}
wrapper = Wrapper()
await Vue.nextTick()
const form = wrapper.find('.enter-nonce')
expect(form.vm.formData.nonce).toEqual('64835')
})
})
})
describe('"method=invite-code" in URI show "RegistrationSlideInvite"', () => {
it('no "inviteCode" query in URI', () => {
mocks.$route.query = { method: 'invite-code' }
wrapper = Wrapper()
expect(wrapper.find('.enter-invite').exists()).toBe(true)
})
it('"inviteCode=AAAAAA" query in URI have invite code in input', async () => {
mocks.$route.query = { method: 'invite-code', inviteCode: 'AAAAAA' }
wrapper = Wrapper()
await Vue.nextTick()
const form = wrapper.find('.enter-invite')
expect(form.vm.formData.inviteCode).toEqual('AAAAAA')
})
})
})
// copied from webapp/components/Registration/Signup.spec.js as testing template
// describe('with invitation code', () => {
// let action
// beforeEach(() => {
// propsData.token = '12345'
// action = async () => {
// wrapper = Wrapper()
// wrapper.find('input#email').setValue('mail@example.org')
// await wrapper.find('form').trigger('submit')
// await wrapper.html()
// }
// })
// describe('submit', () => {
// it('delivers invitation code to backend', async () => {
// await action()
// const expected = expect.objectContaining({
// mutation: SignupMutation,
// variables: { email: 'mail@example.org', inviteCode: '12345' },
// })
// expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
// })
// describe('in case a user account with the email already exists', () => {
// beforeEach(() => {
// mocks.$apollo.mutate = jest
// .fn()
// .mockRejectedValue(
// new Error('UserInputError: A user account with this email already exists.'),
// )
// })
// it('explains the error', async () => {
// await action()
// expect(mocks.$t).toHaveBeenCalledWith(
// 'components.registration.signup.form.errors.email-exists',
// )
// })
// })
// })
// })
})
})
// template from deleted webapp/components/Registration/CreateUserAccount.spec.js
// import { config, mount } from '@vue/test-utils'
// import Vue from 'vue'
// import { VERSION } from '~/constants/terms-and-conditions-version.js'
// import CreateUserAccount from './CreateUserAccount'
// import { SignupVerificationMutation } from '~/graphql/Registration.js'
// const localVue = global.localVue
// config.stubs['sweetalert-icon'] = '<span><slot /></span>'
// config.stubs['client-only'] = '<span><slot /></span>'
// config.stubs['nuxt-link'] = '<span><slot /></span>'
// describe('CreateUserAccount', () => {
// let wrapper, Wrapper, mocks, propsData, stubs
// beforeEach(() => {
// mocks = {
// $toast: {
// success: jest.fn(),
// error: jest.fn(),
// },
// $t: jest.fn(),
// $apollo: {
// loading: false,
// mutate: jest.fn(),
// },
// $i18n: {
// locale: () => 'en',
// },
// }
// propsData = {}
// stubs = {
// LocaleSwitch: "<div class='stub'></div>",
// }
// })
// describe('mount', () => {
// Wrapper = () => {
// return mount(CreateUserAccount, {
// mocks,
// propsData,
// localVue,
// stubs,
// })
// }
// describe('given email and nonce', () => {
// beforeEach(() => {
// propsData.nonce = '666777'
// propsData.email = 'sixseven@example.org'
// })
// it('renders a form to create a new user', () => {
// wrapper = Wrapper()
// expect(wrapper.find('.create-user-account').exists()).toBe(true)
// })
// describe('submit', () => {
// let action
// beforeEach(() => {
// action = async () => {
// wrapper = Wrapper()
// wrapper.find('input#name').setValue('John Doe')
// wrapper.find('input#password').setValue('hellopassword')
// wrapper.find('textarea#about').setValue('Hello I am the `about` attribute')
// wrapper.find('input#passwordConfirmation').setValue('hellopassword')
// wrapper.find('input#checkbox0').setChecked()
// wrapper.find('input#checkbox1').setChecked()
// wrapper.find('input#checkbox2').setChecked()
// wrapper.find('input#checkbox3').setChecked()
// wrapper.find('input#checkbox4').setChecked()
// await wrapper.find('form').trigger('submit')
// await wrapper.html()
// }
// })
// it('delivers data to backend', async () => {
// await action()
// const expected = expect.objectContaining({
// variables: {
// about: 'Hello I am the `about` attribute',
// name: 'John Doe',
// email: 'sixseven@example.org',
// nonce: '666777',
// password: 'hellopassword',
// termsAndConditionsAgreedVersion: VERSION,
// locale: 'en',
// },
// })
// expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
// })
// it('calls CreateUserAccount graphql mutation', async () => {
// await action()
// const expected = expect.objectContaining({ mutation: SignupVerificationMutation })
// expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected)
// })
// describe('in case mutation resolves', () => {
// beforeEach(() => {
// mocks.$apollo.mutate = jest.fn().mockResolvedValue({
// data: {
// SignupVerification: {
// id: 'u1',
// name: 'John Doe',
// slug: 'john-doe',
// },
// },
// })
// })
// it('displays success', async () => {
// await action()
// await Vue.nextTick()
// expect(mocks.$t).toHaveBeenCalledWith(
// 'components.registration.create-user-account.success',
// )
// })
// describe('after timeout', () => {
// beforeEach(jest.useFakeTimers)
// it('emits `userCreated` with { password, email }', async () => {
// await action()
// jest.runAllTimers()
// expect(wrapper.emitted('userCreated')).toEqual([
// [
// {
// email: 'sixseven@example.org',
// password: 'hellopassword',
// },
// ],
// ])
// })
// })
// })
// describe('in case mutation rejects', () => {
// beforeEach(() => {
// mocks.$apollo.mutate = jest.fn().mockRejectedValue(new Error('Invalid nonce'))
// })
// it('displays form errors', async () => {
// await action()
// await Vue.nextTick()
// expect(mocks.$t).toHaveBeenCalledWith(
// 'components.registration.create-user-account.error',
// )
// })
// })
// })
// })
// })
// })

View File

@ -1,33 +1,33 @@
<template>
<ds-container width="small">
<base-card>
<template #imageColumn>
<a :href="links.ORGANIZATION" :title="$t('login.moreInfo', metadata)" target="_blank">
<img class="image" alt="Sign up" src="/img/custom/sign-up.svg" />
</a>
</template>
<nuxt-child />
<template #topMenu>
<locale-switch offset="5" />
</template>
</base-card>
</ds-container>
<registration-slider
:registrationType="registrationType"
:overwriteSliderData="overwriteSliderData"
/>
</template>
<script>
import links from '~/constants/links.js'
import metadata from '~/constants/metadata.js'
import LocaleSwitch from '~/components/LocaleSwitch/LocaleSwitch'
import RegistrationSlider from '~/components/Registration/RegistrationSlider'
export default {
components: {
LocaleSwitch,
},
layout: 'no-header',
name: 'Registration',
components: {
RegistrationSlider,
},
data() {
const { method = null, email = null, inviteCode = null, nonce = null } = this.$route.query
return {
metadata,
links,
method,
overwriteSliderData: {
collectedInputData: {
inviteCode,
email,
emailSend: !!email,
nonce,
},
},
publicRegistration: this.$env.PUBLIC_REGISTRATION === true, // for 'false' in .env PUBLIC_REGISTRATION is of type undefined and not(!) boolean false, because of internal handling
inviteRegistration: this.$env.INVITE_REGISTRATION === true, // for 'false' in .env INVITE_REGISTRATION is of type undefined and not(!) boolean false, because of internal handling
}
},
asyncData({ store, redirect }) {
@ -35,11 +35,24 @@ export default {
redirect('/')
}
},
computed: {
registrationType() {
if (!this.method) {
return (
(this.publicRegistration && 'public-registration') ||
(this.inviteRegistration && 'invite-code') ||
'no-public-registration'
)
} else {
if (
this.method === 'invite-mail' ||
(this.method === 'invite-code' && this.inviteRegistration)
) {
return this.method
}
return this.publicRegistration ? 'public-registration' : 'no-public-registration'
}
},
},
}
</script>
<style lang="scss">
.image {
width: 100%;
}
</style>

View File

@ -1,27 +0,0 @@
<template>
<create-user-account @userCreated="handleUserCreated" :email="email" :nonce="nonce" />
</template>
<script>
import CreateUserAccount from '~/components/Registration/CreateUserAccount'
export default {
data() {
const { nonce = '', email = '' } = this.$route.query
return { nonce, email }
},
components: {
CreateUserAccount,
},
methods: {
async handleUserCreated({ email, password }) {
try {
await this.$store.dispatch('auth/login', { email, password })
this.$toast.success('You are logged in!')
this.$router.push('/')
} catch (err) {
this.$toast.error(err.message)
}
},
},
}
</script>

View File

@ -1,25 +0,0 @@
<template>
<enter-nonce :email="email" @nonceEntered="nonceEntered">
<ds-space margin-bottom="xxx-small" margin-top="large" centered>
<nuxt-link to="/login">{{ $t('site.back-to-login') }}</nuxt-link>
</ds-space>
</enter-nonce>
</template>
<script>
import EnterNonce from '~/components/EnterNonce/EnterNonce.vue'
export default {
components: {
EnterNonce,
},
data() {
const { email = '' } = this.$route.query
return { email }
},
methods: {
nonceEntered({ email, nonce }) {
this.$router.push({ path: 'create-user-account', query: { email, nonce } })
},
},
}
</script>

View File

@ -1,34 +0,0 @@
<template>
<signup v-if="publicRegistration" :invitation="false" @submit="handleSubmitted">
<ds-space centered margin-top="large">
<nuxt-link to="/login">{{ $t('site.back-to-login') }}</nuxt-link>
</ds-space>
</signup>
<ds-space v-else centered>
<hc-empty icon="events" :message="$t('components.registration.signup.unavailable')" />
<nuxt-link to="/login">{{ $t('site.back-to-login') }}</nuxt-link>
</ds-space>
</template>
<script>
import Signup from '~/components/Registration/Signup'
import HcEmpty from '~/components/Empty/Empty'
export default {
layout: 'no-header',
components: {
HcEmpty,
Signup,
},
asyncData({ app }) {
return {
publicRegistration: app.$env.PUBLIC_REGISTRATION,
}
},
methods: {
handleSubmitted({ email }) {
this.$router.push({ path: 'enter-nonce', query: { email } })
},
},
}
</script>