diff --git a/backend/src/middleware/email/emailMiddleware.js b/backend/src/middleware/email/emailMiddleware.js index 1143e3d0f..ea69dcd95 100644 --- a/backend/src/middleware/email/emailMiddleware.js +++ b/backend/src/middleware/email/emailMiddleware.js @@ -71,6 +71,5 @@ export default { AddEmailAddress: sendEmailVerificationMail, requestPasswordReset: sendPasswordResetMail, Signup: sendSignupMail, - SignupByInvitation: sendSignupMail, }, } diff --git a/backend/src/middleware/email/templateBuilder.js b/backend/src/middleware/email/templateBuilder.js index 6e147d752..28d75b3eb 100644 --- a/backend/src/middleware/email/templateBuilder.js +++ b/backend/src/middleware/email/templateBuilder.js @@ -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, diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index e1b2feebe..b10389f50 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -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, diff --git a/backend/src/middleware/permissionsMiddleware.spec.js b/backend/src/middleware/permissionsMiddleware.spec.js index 775533867..8f311e8c2 100644 --- a/backend/src/middleware/permissionsMiddleware.spec.js +++ b/backend/src/middleware/permissionsMiddleware.spec.js @@ -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 }, + }) + }) + }) + }) + }) }) }) diff --git a/backend/src/schema/resolvers/emails.js b/backend/src/schema/resolvers/emails.js index 7986f2613..8f6b1c651 100644 --- a/backend/src/schema/resolvers/emails.js +++ b/backend/src/schema/resolvers/emails.js @@ -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 diff --git a/backend/src/schema/resolvers/emails.spec.js b/backend/src/schema/resolvers/emails.spec.js index 94e7ede31..6b1e24d2b 100644 --- a/backend/src/schema/resolvers/emails.spec.js +++ b/backend/src/schema/resolvers/emails.spec.js @@ -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 }, + }) + }) +}) diff --git a/backend/src/schema/resolvers/helpers/generateNonce.js b/backend/src/schema/resolvers/helpers/generateNonce.js index e9b758774..6da40b5c2 100644 --- a/backend/src/schema/resolvers/helpers/generateNonce.js +++ b/backend/src/schema/resolvers/helpers/generateNonce.js @@ -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('') } diff --git a/backend/src/schema/resolvers/inviteCodes.js b/backend/src/schema/resolvers/inviteCodes.js index 2a0269b54..442ff17b1 100644 --- a/backend/src/schema/resolvers/inviteCodes.js +++ b/backend/src/schema/resolvers/inviteCodes.js @@ -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: { diff --git a/backend/src/schema/resolvers/registration.js b/backend/src/schema/resolvers/registration.js index f1c43be21..075f65554 100644 --- a/backend/src/schema/resolvers/registration.js +++ b/backend/src/schema/resolvers/registration.js @@ -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 +} diff --git a/backend/src/schema/resolvers/registration.spec.js b/backend/src/schema/resolvers/registration.spec.js index 63dc35519..c8b71deb9 100644 --- a/backend/src/schema/resolvers/registration.spec.js +++ b/backend/src/schema/resolvers/registration.spec.js @@ -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!' }], }) diff --git a/backend/src/schema/resolvers/transactions/inviteCodes.js b/backend/src/schema/resolvers/transactions/inviteCodes.js new file mode 100644 index 000000000..554b15f86 --- /dev/null +++ b/backend/src/schema/resolvers/transactions/inviteCodes.js @@ -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() + } +} diff --git a/backend/src/schema/types/type/EmailAddress.gql b/backend/src/schema/types/type/EmailAddress.gql index e09ec9e63..b2e65eafa 100644 --- a/backend/src/schema/types/type/EmailAddress.gql +++ b/backend/src/schema/types/type/EmailAddress.gql @@ -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 diff --git a/webapp/components/ComponentSlider/ComponentSlider.vue b/webapp/components/ComponentSlider/ComponentSlider.vue index 9ecaa5711..19e82417f 100644 --- a/webapp/components/ComponentSlider/ComponentSlider.vue +++ b/webapp/components/ComponentSlider/ComponentSlider.vue @@ -2,18 +2,38 @@
- - {{ sliderData.sliders[sliderIndex].title }} + + {{ + (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, + )) + }} - +
- {{ sliderData.sliders[sliderIndex].button.title }} + {{ $t(sliderData.sliders[sliderIndex].button.titleIdent) }} @@ -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; } } diff --git a/webapp/components/EnterNonce/EnterNonce.spec.js b/webapp/components/EnterNonce/EnterNonce.spec.js index 3e05e45d0..a1f2e6b94 100644 --- a/webapp/components/EnterNonce/EnterNonce.spec.js +++ b/webapp/components/EnterNonce/EnterNonce.spec.js @@ -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) }) }) diff --git a/webapp/components/EnterNonce/EnterNonce.vue b/webapp/components/EnterNonce/EnterNonce.vue index eb44c3235..4785b2c71 100644 --- a/webapp/components/EnterNonce/EnterNonce.vue +++ b/webapp/components/EnterNonce/EnterNonce.vue @@ -8,17 +8,17 @@ @input-valid="handleInputValid" > - {{ $t('components.enter-nonce.form.description') }} + {{ $t('components.registration.email-nonce.form.description') }} - {{ $t('components.enter-nonce.form.next') }} + {{ $t('components.registration.email-nonce.form.next') }} @@ -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, diff --git a/webapp/components/LoginForm/LoginForm.vue b/webapp/components/LoginForm/LoginForm.vue index 28998d3fc..01c3c8661 100644 --- a/webapp/components/LoginForm/LoginForm.vue +++ b/webapp/components/LoginForm/LoginForm.vue @@ -45,7 +45,7 @@

{{ $t('login.no-account') }} - {{ $t('login.register') }} + {{ $t('login.register') }}

@@ -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' } }, diff --git a/webapp/components/Registration/RegistrationSlideEmail.vue b/webapp/components/Registration/RegistrationSlideEmail.vue new file mode 100644 index 000000000..5289248cc --- /dev/null +++ b/webapp/components/Registration/RegistrationSlideEmail.vue @@ -0,0 +1,208 @@ + + + + diff --git a/webapp/components/Registration/RegistrationItemEnterInvite.vue b/webapp/components/Registration/RegistrationSlideInvite.vue similarity index 54% rename from webapp/components/Registration/RegistrationItemEnterInvite.vue rename to webapp/components/Registration/RegistrationSlideInvite.vue index b52704224..5a3aee777 100644 --- a/webapp/components/Registration/RegistrationItemEnterInvite.vue +++ b/webapp/components/Registration/RegistrationSlideInvite.vue @@ -7,14 +7,14 @@ @input-valid="handleInputValid" > - {{ $t('components.enter-invite.form.description') }} + {{ $t('components.registration.invite-code.form.description') }} @@ -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 } } }, diff --git a/webapp/components/Registration/RegistrationSlideNoPublic.vue b/webapp/components/Registration/RegistrationSlideNoPublic.vue new file mode 100644 index 000000000..1d26cc8e2 --- /dev/null +++ b/webapp/components/Registration/RegistrationSlideNoPublic.vue @@ -0,0 +1,35 @@ + + + diff --git a/webapp/components/Registration/RegistrationSlideNonce.vue b/webapp/components/Registration/RegistrationSlideNonce.vue new file mode 100644 index 000000000..40ccd10ad --- /dev/null +++ b/webapp/components/Registration/RegistrationSlideNonce.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/webapp/components/Registration/RegistrationSlider.story.js b/webapp/components/Registration/RegistrationSlider.story.js index fc6dd3e32..9fb1c347f 100644 --- a/webapp/components/Registration/RegistrationSlider.story.js +++ b/webapp/components/Registration/RegistrationSlider.story.js @@ -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: ` @@ -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: ` @@ -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) `, })) + .add('no-public-registration', () => ({ + components: { RegistrationSlider }, + store: createStore({ loginSuccess: true }), + data: () => ({}), + template: ` + + `, + })) diff --git a/webapp/components/Registration/RegistrationSlider.vue b/webapp/components/Registration/RegistrationSlider.vue index e69231c78..b4a180ecf 100644 --- a/webapp/components/Registration/RegistrationSlider.vue +++ b/webapp/components/Registration/RegistrationSlider.vue @@ -8,37 +8,27 @@ -