diff --git a/backend/src/graphql/arg/UpdateUserInfosArgs.ts b/backend/src/graphql/arg/UpdateUserInfosArgs.ts index 2f9df8dd7..e57d4ec82 100644 --- a/backend/src/graphql/arg/UpdateUserInfosArgs.ts +++ b/backend/src/graphql/arg/UpdateUserInfosArgs.ts @@ -8,6 +8,9 @@ export class UpdateUserInfosArgs { @Field({ nullable: true }) lastName?: string + @Field({ nullable: true }) + alias?: string + @Field({ nullable: true }) language?: string diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index ddfcf173a..7d71d74b1 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -1198,6 +1198,28 @@ describe('UserResolver', () => { }) }) + describe('alias', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('valid alias', () => { + it('updates the user in DB', async () => { + await mutate({ + mutation: updateUserInfos, + variables: { + alias: 'bibi_Bloxberg', + }, + }) + await expect(User.findOne()).resolves.toEqual( + expect.objectContaining({ + alias: 'bibi_Bloxberg', + }), + ) + }) + }) + }) + describe('language is not valid', () => { it('throws an error', async () => { jest.clearAllMocks() diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index ca38ae769..0afbfcc5a 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -73,6 +73,7 @@ import { getTimeDurationObject, printTimeDuration } from '@/util/time' import { FULL_CREATION_AVAILABLE } from './const/const' import { getUserCreations } from './util/creations' import { findUserByIdentifier } from './util/findUserByIdentifier' +import { validateAlias } from './util/validateAlias' // eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-commonjs const random = require('random-bigint') @@ -504,6 +505,7 @@ export class UserResolver { { firstName, lastName, + alias, language, password, passwordNew, @@ -523,6 +525,10 @@ export class UserResolver { user.lastName = lastName } + if (alias && (await validateAlias(alias))) { + user.alias = alias + } + if (language) { if (!isLanguage(language)) { throw new LogError('Given language is not a valid language', language) diff --git a/backend/src/graphql/resolver/util/validateAlias.test.ts b/backend/src/graphql/resolver/util/validateAlias.test.ts new file mode 100644 index 000000000..0cb790edb --- /dev/null +++ b/backend/src/graphql/resolver/util/validateAlias.test.ts @@ -0,0 +1,125 @@ +import { Connection } from '@dbTools/typeorm' +import { User } from '@entity/User' +import { ApolloServerTestClient } from 'apollo-server-testing' + +import { testEnvironment, cleanDB } from '@test/helpers' +import { logger, i18n as localization } from '@test/testSetup' + +import { userFactory } from '@/seeds/factory/user' +import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' + +import { validateAlias } from './validateAlias' + +let con: Connection +let testEnv: { + mutate: ApolloServerTestClient['mutate'] + query: ApolloServerTestClient['query'] + con: Connection +} + +beforeAll(async () => { + testEnv = await testEnvironment(logger, localization) + con = testEnv.con + await cleanDB() +}) + +afterAll(async () => { + await cleanDB() + await con.close() +}) + +describe('validate alias', () => { + beforeAll(() => { + jest.clearAllMocks() + }) + + describe('alias too short', () => { + it('throws and logs an error', async () => { + await expect(validateAlias('Bi')).rejects.toEqual(new Error('Given alias is too short')) + expect(logger.error).toBeCalledWith('Given alias is too short', 'Bi') + }) + }) + + describe('alias too long', () => { + it('throws and logs an error', async () => { + await expect(validateAlias('BibiBloxbergHexHexHex')).rejects.toEqual( + new Error('Given alias is too long'), + ) + expect(logger.error).toBeCalledWith('Given alias is too long', 'BibiBloxbergHexHexHex') + }) + }) + + describe('alias contains invalid characters', () => { + it('throws and logs an error', async () => { + await expect(validateAlias('Bibi.Bloxberg')).rejects.toEqual( + new Error('Invalid characters in alias'), + ) + expect(logger.error).toBeCalledWith('Invalid characters in alias', 'Bibi.Bloxberg') + }) + }) + + describe('alias is a reserved word', () => { + it('throws and logs an error', async () => { + await expect(validateAlias('admin')).rejects.toEqual(new Error('Alias is not allowed')) + expect(logger.error).toBeCalledWith('Alias is not allowed', 'admin') + }) + }) + + describe('alias is a reserved word with uppercase characters', () => { + it('throws and logs an error', async () => { + await expect(validateAlias('Admin')).rejects.toEqual(new Error('Alias is not allowed')) + expect(logger.error).toBeCalledWith('Alias is not allowed', 'Admin') + }) + }) + + describe('hyphens and underscore', () => { + describe('alias starts with underscore', () => { + it('throws and logs an error', async () => { + await expect(validateAlias('_bibi')).rejects.toEqual( + new Error('Invalid characters in alias'), + ) + expect(logger.error).toBeCalledWith('Invalid characters in alias', '_bibi') + }) + }) + + describe('alias contains two following hyphens', () => { + it('throws and logs an error', async () => { + await expect(validateAlias('bi--bi')).rejects.toEqual( + new Error('Invalid characters in alias'), + ) + expect(logger.error).toBeCalledWith('Invalid characters in alias', 'bi--bi') + }) + }) + }) + + describe('test against existing alias in database', () => { + beforeAll(async () => { + const bibi = await userFactory(testEnv, bibiBloxberg) + const user = await User.findOne({ id: bibi.id }) + if (user) { + user.alias = 'b-b' + await user.save() + } + }) + + describe('alias exists in database', () => { + it('throws and logs an error', async () => { + await expect(validateAlias('b-b')).rejects.toEqual(new Error('Alias already in use')) + expect(logger.error).toBeCalledWith('Alias already in use', 'b-b') + }) + }) + + describe('alias exists in database with in lower-case', () => { + it('throws and logs an error', async () => { + await expect(validateAlias('b-B')).rejects.toEqual(new Error('Alias already in use')) + expect(logger.error).toBeCalledWith('Alias already in use', 'b-B') + }) + }) + + describe('valid alias', () => { + it('resolves to true', async () => { + await expect(validateAlias('bibi')).resolves.toEqual(true) + }) + }) + }) +}) diff --git a/backend/src/graphql/resolver/util/validateAlias.ts b/backend/src/graphql/resolver/util/validateAlias.ts new file mode 100644 index 000000000..dcea7824c --- /dev/null +++ b/backend/src/graphql/resolver/util/validateAlias.ts @@ -0,0 +1,38 @@ +import { Raw } from '@dbTools/typeorm' +import { User as DbUser } from '@entity/User' + +import { LogError } from '@/server/LogError' + +const reservedAlias = [ + 'admin', + 'email', + 'gast', + 'gdd', + 'gradido', + 'guest', + 'home', + 'root', + 'support', + 'temp', + 'tmp', + 'tmp', + 'user', + 'usr', + 'var', +] + +export const validateAlias = async (alias: string): Promise => { + if (alias.length < 3) throw new LogError('Given alias is too short', alias) + if (alias.length > 20) throw new LogError('Given alias is too long', alias) + /* eslint-disable-next-line security/detect-unsafe-regex */ + if (!alias.match(/^[0-9A-Za-z]([_-]?[A-Za-z0-9])+$/)) + throw new LogError('Invalid characters in alias', alias) + if (reservedAlias.includes(alias.toLowerCase())) throw new LogError('Alias is not allowed', alias) + const aliasInUse = await DbUser.find({ + where: { alias: Raw((a) => `LOWER(${a}) = "${alias.toLowerCase()}"`) }, + }) + if (aliasInUse.length !== 0) { + throw new LogError('Alias already in use', alias) + } + return true +} diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index 2dfe96766..22e0b1b09 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -28,6 +28,7 @@ export const updateUserInfos = gql` mutation ( $firstName: String $lastName: String + $alias: String $password: String $passwordNew: String $locale: String @@ -37,6 +38,7 @@ export const updateUserInfos = gql` updateUserInfos( firstName: $firstName lastName: $lastName + alias: $alias password: $password passwordNew: $passwordNew language: $locale