Implement 'inviteCode' and 'nonce' lengths via 'registration.js' in backend and webapp

This commit is contained in:
Wolfgang Huß 2022-06-23 17:44:59 +02:00
parent 5cd1b95907
commit 5abbf22b43
17 changed files with 72 additions and 32 deletions

View File

@ -0,0 +1,5 @@
// this file is duplicated in `backend/src/config/metadata.js` and `webapp/constants/metadata.js`
export default {
NONCE_LENGTH: 5,
INVITE_CODE_LENGTH: 6,
}

View File

@ -158,7 +158,7 @@ describe('VerifyEmailAddress', () => {
` `
beforeEach(() => { beforeEach(() => {
variables = { ...variables, email: 'to-be-verified@example.org', nonce: '123456' } variables = { ...variables, email: 'to-be-verified@example.org', nonce: '12345' }
}) })
describe('unauthenticated', () => { describe('unauthenticated', () => {

View File

@ -1,8 +1,13 @@
import CONSTANTS_REGISTRATION from './../../../constants/registration'
export default function generateInviteCode() { export default function generateInviteCode() {
// 6 random numbers in [ 0, 35 ] are 36 possible numbers (10 [0-9] + 26 [A-Z]) // 6 random numbers in [ 0, 35 ] are 36 possible numbers (10 [0-9] + 26 [A-Z])
return Array.from({ length: 6 }, (n = Math.floor(Math.random() * 36)) => { return Array.from(
// n > 9: it is a letter (ASCII 65 is A) -> 10 + 55 = 65 { length: CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH },
// else: it is a number (ASCII 48 is 0) -> 0 + 48 = 48 (n = Math.floor(Math.random() * 36)) => {
return String.fromCharCode(n > 9 ? n + 55 : n + 48) // n > 9: it is a letter (ASCII 65 is A) -> 10 + 55 = 65
}).join('') // else: it is a number (ASCII 48 is 0) -> 0 + 48 = 48
return String.fromCharCode(n > 9 ? n + 55 : n + 48)
},
).join('')
} }

View File

@ -1,5 +1,11 @@
import CONSTANTS_REGISTRATION from './../../../constants/registration'
// TODO: why this is not used in resolver 'requestPasswordReset'?
export default function generateNonce() { export default function generateNonce() {
return Array.from({ length: 5 }, (n = Math.floor(Math.random() * 10)) => { return Array.from(
return String.fromCharCode(n + 48) { length: CONSTANTS_REGISTRATION.NONCE_LENGTH },
}).join('') (n = Math.floor(Math.random() * 10)) => {
return String.fromCharCode(n + 48)
},
).join('')
} }

View File

@ -3,6 +3,7 @@ import { getDriver } from '../../db/neo4j'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import createServer from '../../server' import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import CONSTANTS_REGISTRATION from './../../constants/registration'
let user let user
let query let query
@ -107,7 +108,11 @@ describe('inviteCodes', () => {
errors: undefined, errors: undefined,
data: { data: {
GenerateInviteCode: { GenerateInviteCode: {
code: expect.stringMatching(/^[0-9A-Z]{6,6}$/), code: expect.stringMatching(
new RegExp(
`^[0-9A-Z]{${CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH},${CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH}}$`,
),
),
expiresAt: null, expiresAt: null,
createdAt: expect.any(String), createdAt: expect.any(String),
}, },
@ -129,7 +134,11 @@ describe('inviteCodes', () => {
errors: undefined, errors: undefined,
data: { data: {
GenerateInviteCode: { GenerateInviteCode: {
code: expect.stringMatching(/^[0-9A-Z]{6,6}$/), code: expect.stringMatching(
new RegExp(
`^[0-9A-Z]{${CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH},${CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH}}$`,
),
),
expiresAt: nextWeek.toISOString(), expiresAt: nextWeek.toISOString(),
createdAt: expect.any(String), createdAt: expect.any(String),
}, },

View File

@ -1,11 +1,13 @@
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import bcrypt from 'bcryptjs' import bcrypt from 'bcryptjs'
import CONSTANTS_REGISTRATION from './../../constants/registration'
import createPasswordReset from './helpers/createPasswordReset' import createPasswordReset from './helpers/createPasswordReset'
export default { export default {
Mutation: { Mutation: {
requestPasswordReset: async (_parent, { email }, { driver }) => { requestPasswordReset: async (_parent, { email }, { driver }) => {
const nonce = uuid().substring(0, 6) // TODO: why this is generated differntly from 'backend/src/schema/resolvers/helpers/generateNonce.js'?
const nonce = uuid().substring(0, CONSTANTS_REGISTRATION.NONCE_LENGTH)
return createPasswordReset({ driver, nonce, email }) return createPasswordReset({ driver, nonce, email })
}, },
resetPassword: async (_parent, { email, nonce, newPassword }, { driver }) => { resetPassword: async (_parent, { email, nonce, newPassword }, { driver }) => {

View File

@ -1,6 +1,7 @@
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest' import { gql } from '../../helpers/jest'
import { getNeode, getDriver } from '../../db/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import CONSTANTS_REGISTRATION from './../../constants/registration'
import createPasswordReset from './helpers/createPasswordReset' import createPasswordReset from './helpers/createPasswordReset'
import createServer from '../../server' import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
@ -109,7 +110,7 @@ describe('passwordReset', () => {
const resets = await getAllPasswordResets() const resets = await getAllPasswordResets()
const [reset] = resets const [reset] = resets
const { nonce } = reset.properties const { nonce } = reset.properties
expect(nonce).toHaveLength(6) expect(nonce).toHaveLength(CONSTANTS_REGISTRATION.NONCE_LENGTH)
}) })
}) })
}) })
@ -118,7 +119,7 @@ describe('passwordReset', () => {
describe('resetPassword', () => { describe('resetPassword', () => {
const setup = async (options = {}) => { const setup = async (options = {}) => {
const { email = 'user@example.org', issuedAt = new Date(), nonce = 'abcde' } = options const { email = 'user@example.org', issuedAt = new Date(), nonce = '12345' } = options
await createPasswordReset({ driver, email, issuedAt, nonce }) await createPasswordReset({ driver, email, issuedAt, nonce })
} }
@ -148,7 +149,7 @@ describe('resetPassword', () => {
describe('invalid email', () => { describe('invalid email', () => {
it('resolves to false', async () => { it('resolves to false', async () => {
await setup() await setup()
variables = { ...variables, email: 'non-existent@example.org', nonce: 'abcde' } variables = { ...variables, email: 'non-existent@example.org', nonce: '12345' }
await expect(mutate({ mutation, variables })).resolves.toMatchObject({ await expect(mutate({ mutation, variables })).resolves.toMatchObject({
data: { resetPassword: false }, data: { resetPassword: false },
}) })
@ -162,7 +163,7 @@ describe('resetPassword', () => {
describe('but invalid nonce', () => { describe('but invalid nonce', () => {
beforeEach(() => { beforeEach(() => {
variables = { ...variables, nonce: 'slkdjf' } variables = { ...variables, nonce: 'slkdj' }
}) })
it('resolves to false', async () => { it('resolves to false', async () => {
@ -177,7 +178,7 @@ describe('resetPassword', () => {
beforeEach(() => { beforeEach(() => {
variables = { variables = {
...variables, ...variables,
nonce: 'abcde', nonce: '12345',
} }
}) })

View File

@ -22,6 +22,7 @@
<script> <script>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import CONSTANTS_REGISTRATION from './../../constants/registration'
export const isValidInviteCodeQuery = gql` export const isValidInviteCodeQuery = gql`
query($code: ID!) { query($code: ID!) {
@ -41,10 +42,12 @@ export default {
formSchema: { formSchema: {
inviteCode: { inviteCode: {
type: 'string', type: 'string',
min: 6, min: CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH,
max: 6, max: CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH,
required: true, required: true,
message: this.$t('components.registration.invite-code.form.validations.length'), message: this.$t('components.registration.invite-code.form.validations.length', {
inviteCodeLength: CONSTANTS_REGISTRATION.INVITE_CODE_LENGTH,
}),
}, },
}, },
dbRequestInProgress: false, dbRequestInProgress: false,

View File

@ -25,6 +25,8 @@
<script> <script>
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { isEmail } from 'validator' import { isEmail } from 'validator'
import CONSTANTS_REGISTRATION from './../../constants/registration'
import EmailDisplayAndVerify from './EmailDisplayAndVerify' import EmailDisplayAndVerify from './EmailDisplayAndVerify'
export const verifyNonceQuery = gql` export const verifyNonceQuery = gql`
@ -48,10 +50,12 @@ export default {
formSchema: { formSchema: {
nonce: { nonce: {
type: 'string', type: 'string',
min: 5, min: CONSTANTS_REGISTRATION.NONCE_LENGTH,
max: 5, max: CONSTANTS_REGISTRATION.NONCE_LENGTH,
required: true, required: true,
message: this.$t('components.registration.email-nonce.form.validations.length'), message: this.$t('components.registration.email-nonce.form.validations.length', {
nonceLength: CONSTANTS_REGISTRATION.NONCE_LENGTH,
}),
}, },
}, },
dbRequestInProgress: false, dbRequestInProgress: false,

View File

@ -0,0 +1,5 @@
// this file is duplicated in `backend/src/config/metadata.js` and `webapp/constants/metadata.js`
export default {
NONCE_LENGTH: 5,
INVITE_CODE_LENGTH: 6,
}

View File

@ -170,7 +170,7 @@
"nonce": "E-Mail-Code: 32143", "nonce": "E-Mail-Code: 32143",
"validations": { "validations": {
"error": "Ungültiger Bestätigungs-Code <b>{nonce}</b> für E-Mail <b>{email}</b>!", "error": "Ungültiger Bestätigungs-Code <b>{nonce}</b> für E-Mail <b>{email}</b>!",
"length": "muss genau 5 Buchstaben lang sein", "length": "muss genau {nonceLength} Buchstaben lang sein",
"success": "Gültiger Bestätigungs-Code <b>{nonce}</b> für E-Mail <b>{email}</b>!" "success": "Gültiger Bestätigungs-Code <b>{nonce}</b> für E-Mail <b>{email}</b>!"
} }
}, },
@ -184,7 +184,7 @@
"next": "Weiter", "next": "Weiter",
"validations": { "validations": {
"error": "Ungültiger Einladungs-Code <b>{inviteCode}</b>!", "error": "Ungültiger Einladungs-Code <b>{inviteCode}</b>!",
"length": "muss genau 6 Buchstaben lang sein", "length": "muss genau {inviteCodeLength} Buchstaben lang sein",
"success": "Gültiger Einladungs-Code <b>{inviteCode}</b>!" "success": "Gültiger Einladungs-Code <b>{inviteCode}</b>!"
} }
} }

View File

@ -170,7 +170,7 @@
"nonce": "E-mail code: 32143", "nonce": "E-mail code: 32143",
"validations": { "validations": {
"error": "Invalid verification code <b>{nonce}</b> for e-mail <b>{email}</b>!", "error": "Invalid verification code <b>{nonce}</b> for e-mail <b>{email}</b>!",
"length": "must be 5 characters long", "length": "must be {nonceLength} characters long",
"success": "Valid verification code <b>{nonce}</b> for e-mail <b>{email}</b>!" "success": "Valid verification code <b>{nonce}</b> for e-mail <b>{email}</b>!"
} }
}, },
@ -184,7 +184,7 @@
"next": "Continue", "next": "Continue",
"validations": { "validations": {
"error": "Invalid invite code <b>{inviteCode}</b>!", "error": "Invalid invite code <b>{inviteCode}</b>!",
"length": "must be 6 characters long", "length": "must be {inviteCodeLength} characters long",
"success": "Valid invite code <b>{inviteCode}</b>!" "success": "Valid invite code <b>{inviteCode}</b>!"
} }
} }

View File

@ -143,7 +143,7 @@
"next": "Continuar", "next": "Continuar",
"nonce": "Introduzca el código", "nonce": "Introduzca el código",
"validations": { "validations": {
"length": "debe tener exactamente 5 letras" "length": "debe tener exactamente {nonceLength} letras"
} }
} }
}, },

View File

@ -143,7 +143,7 @@
"next": "Continuer", "next": "Continuer",
"nonce": "Entrez votre code", "nonce": "Entrez votre code",
"validations": { "validations": {
"length": "doit comporter 5 caractères" "length": "doit comporter {nonceLength} caractères"
} }
} }
}, },

View File

@ -104,7 +104,7 @@
"next": "Kontynuuj", "next": "Kontynuuj",
"nonce": "Wprowadź swój kod", "nonce": "Wprowadź swój kod",
"validations": { "validations": {
"length": "musi mieć długość 5 znaków." "length": "musi mieć długość {nonceLength} znaków."
} }
} }
} }

View File

@ -190,7 +190,7 @@
"next": "Continue", "next": "Continue",
"nonce": "Digite seu código", "nonce": "Digite seu código",
"validations": { "validations": {
"length": "deve ter 5 caracteres" "length": "deve ter {nonceLength} caracteres"
} }
} }
}, },

View File

@ -143,7 +143,7 @@
"next": "Продолжить", "next": "Продолжить",
"nonce": "Введите код", "nonce": "Введите код",
"validations": { "validations": {
"length": "длина должна быть 5 символов" "length": "длина должна быть {nonceLength} символов"
} }
} }
}, },