mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 04:04:06 -08:00
fix(core): Make email for UM case insensitive (#3078)
* 🚧 lowercasing email * ✅ add tests for case insensitive email * 🐘 add migration to lowercase email * 🚚 rename migration * 🐛 fix package.lock * 🐛 fix double import * 📋 add todo
This commit is contained in:
parent
d3fecb9f6d
commit
8532b0030d
|
@ -12,6 +12,7 @@ import {
|
|||
} from './Interfaces';
|
||||
import { NodeMailer } from './NodeMailer';
|
||||
|
||||
// TODO: make function fully async (remove sync functions)
|
||||
async function getTemplate(configKeyName: string, defaultFilename: string) {
|
||||
const templateOverride = (await GenericHelpers.getConfigValue(
|
||||
`userManagement.emails.templates.${configKeyName}`,
|
||||
|
|
|
@ -104,7 +104,7 @@ export function usersNamespace(this: N8nApp): void {
|
|||
400,
|
||||
);
|
||||
}
|
||||
createUsers[invite.email] = null;
|
||||
createUsers[invite.email.toLowerCase()] = null;
|
||||
});
|
||||
|
||||
const role = await Db.collections.Role.findOne({ scope: 'global', name: 'member' });
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
BeforeInsert,
|
||||
} from 'typeorm';
|
||||
import { IsEmail, IsString, Length } from 'class-validator';
|
||||
import * as config from '../../../config';
|
||||
|
@ -20,7 +21,7 @@ import { Role } from './Role';
|
|||
import { SharedWorkflow } from './SharedWorkflow';
|
||||
import { SharedCredentials } from './SharedCredentials';
|
||||
import { NoXss } from '../utils/customValidators';
|
||||
import { answersFormatter } from '../utils/transformers';
|
||||
import { answersFormatter, lowerCaser } from '../utils/transformers';
|
||||
|
||||
export const MIN_PASSWORD_LENGTH = 8;
|
||||
|
||||
|
@ -62,7 +63,11 @@ export class User {
|
|||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ length: 254, nullable: true })
|
||||
@Column({
|
||||
length: 254,
|
||||
nullable: true,
|
||||
transformer: lowerCaser,
|
||||
})
|
||||
@Index({ unique: true })
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
@ -119,8 +124,10 @@ export class User {
|
|||
})
|
||||
updatedAt: Date;
|
||||
|
||||
@BeforeInsert()
|
||||
@BeforeUpdate()
|
||||
setUpdateDate(): void {
|
||||
preUpsertHook(): void {
|
||||
this.email = this.email?.toLowerCase();
|
||||
this.updatedAt = new Date();
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import config = require('../../../../config');
|
||||
|
||||
export class LowerCaseUserEmail1648740597343 implements MigrationInterface {
|
||||
name = 'LowerCaseUserEmail1648740597343';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const tablePrefix = config.get('database.tablePrefix');
|
||||
|
||||
await queryRunner.query(`
|
||||
UPDATE ${tablePrefix}user
|
||||
SET email = LOWER(email);
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||
}
|
|
@ -12,6 +12,7 @@ import { AddWaitColumnId1626183952959 } from './1626183952959-AddWaitColumn';
|
|||
import { UpdateWorkflowCredentials1630451444017 } from './1630451444017-UpdateWorkflowCredentials';
|
||||
import { AddExecutionEntityIndexes1644424784709 } from './1644424784709-AddExecutionEntityIndexes';
|
||||
import { CreateUserManagement1646992772331 } from './1646992772331-CreateUserManagement';
|
||||
import { LowerCaseUserEmail1648740597343 } from './1648740597343-LowerCaseUserEmail';
|
||||
|
||||
export const mysqlMigrations = [
|
||||
InitialMigration1588157391238,
|
||||
|
@ -28,4 +29,5 @@ export const mysqlMigrations = [
|
|||
UpdateWorkflowCredentials1630451444017,
|
||||
AddExecutionEntityIndexes1644424784709,
|
||||
CreateUserManagement1646992772331,
|
||||
LowerCaseUserEmail1648740597343,
|
||||
];
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import config = require('../../../../config');
|
||||
|
||||
export class LowerCaseUserEmail1648740597343 implements MigrationInterface {
|
||||
name = 'LowerCaseUserEmail1648740597343';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
let tablePrefix = config.get('database.tablePrefix');
|
||||
const schema = config.get('database.postgresdb.schema');
|
||||
if (schema) {
|
||||
tablePrefix = schema + '.' + tablePrefix;
|
||||
}
|
||||
|
||||
await queryRunner.query(`
|
||||
UPDATE ${tablePrefix}user
|
||||
SET email = LOWER(email);
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||
}
|
|
@ -10,6 +10,7 @@ import { UpdateWorkflowCredentials1630419189837 } from './1630419189837-UpdateWo
|
|||
import { AddExecutionEntityIndexes1644422880309 } from './1644422880309-AddExecutionEntityIndexes';
|
||||
import { IncreaseTypeVarcharLimit1646834195327 } from './1646834195327-IncreaseTypeVarcharLimit';
|
||||
import { CreateUserManagement1646992772331 } from './1646992772331-CreateUserManagement';
|
||||
import { LowerCaseUserEmail1648740597343 } from './1648740597343-LowerCaseUserEmail';
|
||||
|
||||
export const postgresMigrations = [
|
||||
InitialMigration1587669153312,
|
||||
|
@ -24,4 +25,5 @@ export const postgresMigrations = [
|
|||
AddExecutionEntityIndexes1644422880309,
|
||||
IncreaseTypeVarcharLimit1646834195327,
|
||||
CreateUserManagement1646992772331,
|
||||
LowerCaseUserEmail1648740597343,
|
||||
];
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
import config = require('../../../../config');
|
||||
import { logMigrationEnd, logMigrationStart } from '../../utils/migrationHelpers';
|
||||
|
||||
export class LowerCaseUserEmail1648740597343 implements MigrationInterface {
|
||||
name = 'LowerCaseUserEmail1648740597343';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
logMigrationStart(this.name);
|
||||
|
||||
const tablePrefix = config.get('database.tablePrefix');
|
||||
|
||||
await queryRunner.query(`
|
||||
UPDATE "${tablePrefix}user"
|
||||
SET email = LOWER(email);
|
||||
`);
|
||||
|
||||
logMigrationEnd(this.name);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||
}
|
|
@ -11,6 +11,7 @@ import { AddWaitColumn1621707690587 } from './1621707690587-AddWaitColumn';
|
|||
import { UpdateWorkflowCredentials1630330987096 } from './1630330987096-UpdateWorkflowCredentials';
|
||||
import { AddExecutionEntityIndexes1644421939510 } from './1644421939510-AddExecutionEntityIndexes';
|
||||
import { CreateUserManagement1646992772331 } from './1646992772331-CreateUserManagement';
|
||||
import { LowerCaseUserEmail1648740597343 } from './1648740597343-LowerCaseUserEmail';
|
||||
|
||||
const sqliteMigrations = [
|
||||
InitialMigration1588102412422,
|
||||
|
@ -24,6 +25,7 @@ const sqliteMigrations = [
|
|||
UpdateWorkflowCredentials1630330987096,
|
||||
AddExecutionEntityIndexes1644421939510,
|
||||
CreateUserManagement1646992772331,
|
||||
LowerCaseUserEmail1648740597343,
|
||||
];
|
||||
|
||||
export { sqliteMigrations };
|
||||
|
|
|
@ -2,8 +2,13 @@
|
|||
import { IPersonalizationSurveyAnswers } from '../../Interfaces';
|
||||
|
||||
export const idStringifier = {
|
||||
from: (value: number): string | number => (value ? value.toString() : value),
|
||||
to: (value: string): number | string => (value ? Number(value) : value),
|
||||
from: (value: number): string | number => (typeof value === 'number' ? value.toString() : value),
|
||||
to: (value: string): number | string => (typeof value === 'string' ? Number(value) : value),
|
||||
};
|
||||
|
||||
export const lowerCaser = {
|
||||
from: (value: string): string => value,
|
||||
to: (value: string): string => (typeof value === 'string' ? value.toLowerCase() : value),
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { hashSync, genSaltSync } from 'bcryptjs';
|
||||
import express from 'express';
|
||||
import validator from 'validator';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
@ -60,10 +59,18 @@ afterAll(async () => {
|
|||
test('POST /login should log user in', async () => {
|
||||
const authlessAgent = utils.createAgent(app);
|
||||
|
||||
const response = await authlessAgent.post('/login').send({
|
||||
await Promise.all(
|
||||
[
|
||||
{
|
||||
email: TEST_USER.email,
|
||||
password: TEST_USER.password,
|
||||
});
|
||||
},
|
||||
{
|
||||
email: TEST_USER.email.toUpperCase(),
|
||||
password: TEST_USER.password,
|
||||
},
|
||||
].map(async (payload) => {
|
||||
const response = await authlessAgent.post('/login').send(payload);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
|
@ -84,7 +91,6 @@ test('POST /login should log user in', async () => {
|
|||
expect(lastName).toBe(TEST_USER.lastName);
|
||||
expect(password).toBeUndefined();
|
||||
expect(personalizationAnswers).toBeNull();
|
||||
expect(password).toBeUndefined();
|
||||
expect(resetPasswordToken).toBeUndefined();
|
||||
expect(globalRole).toBeDefined();
|
||||
expect(globalRole.name).toBe('owner');
|
||||
|
@ -92,6 +98,8 @@ test('POST /login should log user in', async () => {
|
|||
|
||||
const authToken = utils.getAuthToken(response);
|
||||
expect(authToken).toBeDefined();
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('GET /login should receive logged in user', async () => {
|
||||
|
|
|
@ -91,7 +91,7 @@ describe('Owner shell', () => {
|
|||
} = response.body.data;
|
||||
|
||||
expect(validator.isUUID(id)).toBe(true);
|
||||
expect(email).toBe(validPayload.email);
|
||||
expect(email).toBe(validPayload.email.toLowerCase());
|
||||
expect(firstName).toBe(validPayload.firstName);
|
||||
expect(lastName).toBe(validPayload.lastName);
|
||||
expect(personalizationAnswers).toBeNull();
|
||||
|
@ -103,7 +103,7 @@ describe('Owner shell', () => {
|
|||
|
||||
const storedOwnerShell = await Db.collections.User!.findOneOrFail(id);
|
||||
|
||||
expect(storedOwnerShell.email).toBe(validPayload.email);
|
||||
expect(storedOwnerShell.email).toBe(validPayload.email.toLowerCase());
|
||||
expect(storedOwnerShell.firstName).toBe(validPayload.firstName);
|
||||
expect(storedOwnerShell.lastName).toBe(validPayload.lastName);
|
||||
}
|
||||
|
@ -245,7 +245,7 @@ describe('Member', () => {
|
|||
} = response.body.data;
|
||||
|
||||
expect(validator.isUUID(id)).toBe(true);
|
||||
expect(email).toBe(validPayload.email);
|
||||
expect(email).toBe(validPayload.email.toLowerCase());
|
||||
expect(firstName).toBe(validPayload.firstName);
|
||||
expect(lastName).toBe(validPayload.lastName);
|
||||
expect(personalizationAnswers).toBeNull();
|
||||
|
@ -257,7 +257,7 @@ describe('Member', () => {
|
|||
|
||||
const storedMember = await Db.collections.User!.findOneOrFail(id);
|
||||
|
||||
expect(storedMember.email).toBe(validPayload.email);
|
||||
expect(storedMember.email).toBe(validPayload.email.toLowerCase());
|
||||
expect(storedMember.firstName).toBe(validPayload.firstName);
|
||||
expect(storedMember.lastName).toBe(validPayload.lastName);
|
||||
}
|
||||
|
@ -400,7 +400,7 @@ describe('Owner', () => {
|
|||
} = response.body.data;
|
||||
|
||||
expect(validator.isUUID(id)).toBe(true);
|
||||
expect(email).toBe(validPayload.email);
|
||||
expect(email).toBe(validPayload.email.toLowerCase());
|
||||
expect(firstName).toBe(validPayload.firstName);
|
||||
expect(lastName).toBe(validPayload.lastName);
|
||||
expect(personalizationAnswers).toBeNull();
|
||||
|
@ -412,19 +412,13 @@ describe('Owner', () => {
|
|||
|
||||
const storedOwner = await Db.collections.User!.findOneOrFail(id);
|
||||
|
||||
expect(storedOwner.email).toBe(validPayload.email);
|
||||
expect(storedOwner.email).toBe(validPayload.email.toLowerCase());
|
||||
expect(storedOwner.firstName).toBe(validPayload.firstName);
|
||||
expect(storedOwner.lastName).toBe(validPayload.lastName);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const TEST_USER = {
|
||||
email: randomEmail(),
|
||||
firstName: randomName(),
|
||||
lastName: randomName(),
|
||||
};
|
||||
|
||||
const SURVEY = [
|
||||
'codingSkill',
|
||||
'companyIndustry',
|
||||
|
@ -444,7 +438,7 @@ const VALID_PATCH_ME_PAYLOADS = [
|
|||
password: randomValidPassword(),
|
||||
},
|
||||
{
|
||||
email: randomEmail(),
|
||||
email: randomEmail().toUpperCase(),
|
||||
firstName: randomName(),
|
||||
lastName: randomName(),
|
||||
password: randomValidPassword(),
|
||||
|
|
|
@ -30,6 +30,12 @@ beforeAll(async () => {
|
|||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.mock('../../config');
|
||||
|
||||
config.set('userManagement.isInstanceOwnerSetUp', false);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await testDb.truncate(['User'], testDbName);
|
||||
});
|
||||
|
||||
|
@ -88,6 +94,29 @@ test('POST /owner should create owner and enable isInstanceOwnerSetUp', async ()
|
|||
expect(isInstanceOwnerSetUpSetting).toBe(true);
|
||||
});
|
||||
|
||||
test('POST /owner should create owner with lowercased email', async () => {
|
||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||
|
||||
const newOwnerData = {
|
||||
email: randomEmail().toUpperCase(),
|
||||
firstName: randomName(),
|
||||
lastName: randomName(),
|
||||
password: randomValidPassword(),
|
||||
};
|
||||
|
||||
const response = await authOwnerAgent.post('/owner').send(newOwnerData);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
const { id, email } = response.body.data;
|
||||
|
||||
expect(email).toBe(newOwnerData.email.toLowerCase());
|
||||
|
||||
const storedOwner = await Db.collections.User!.findOneOrFail(id);
|
||||
expect(storedOwner.email).toBe(newOwnerData.email.toLowerCase());
|
||||
});
|
||||
|
||||
test('POST /owner should fail with invalid inputs', async () => {
|
||||
const ownerShell = await testDb.createUserShell(globalOwnerRole);
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: ownerShell });
|
||||
|
|
|
@ -19,6 +19,7 @@ jest.mock('../../src/telemetry');
|
|||
let app: express.Application;
|
||||
let testDbName = '';
|
||||
let globalOwnerRole: Role;
|
||||
let globalMemberRole: Role;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = utils.initTestServer({ endpointGroups: ['passwordReset'], applyAuth: true });
|
||||
|
@ -26,6 +27,7 @@ beforeAll(async () => {
|
|||
testDbName = initResult.testDbName;
|
||||
|
||||
globalOwnerRole = await testDb.getGlobalOwnerRole();
|
||||
globalMemberRole = await testDb.getGlobalMemberRole();
|
||||
|
||||
utils.initTestTelemetry();
|
||||
utils.initTestLogger();
|
||||
|
@ -50,17 +52,22 @@ test('POST /forgot-password should send password reset email', async () => {
|
|||
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
||||
|
||||
const authlessAgent = utils.createAgent(app);
|
||||
const member = await testDb.createUser({ email: 'test@test.com', globalRole: globalMemberRole });
|
||||
|
||||
await utils.configureSmtp();
|
||||
|
||||
const response = await authlessAgent.post('/forgot-password').send({ email: owner.email });
|
||||
await Promise.all(
|
||||
[{ email: owner.email }, { email: member.email.toUpperCase() }].map(async (payload) => {
|
||||
const response = await authlessAgent.post('/forgot-password').send(payload);
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body).toEqual({});
|
||||
|
||||
const storedOwner = await Db.collections.User!.findOneOrFail({ email: owner.email });
|
||||
expect(storedOwner.resetPasswordToken).toBeDefined();
|
||||
expect(storedOwner.resetPasswordTokenExpiration).toBeGreaterThan(Math.ceil(Date.now() / 1000));
|
||||
const user = await Db.collections.User!.findOneOrFail({ email: payload.email });
|
||||
expect(user.resetPasswordToken).toBeDefined();
|
||||
expect(user.resetPasswordTokenExpiration).toBeGreaterThan(Math.ceil(Date.now() / 1000));
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('POST /forgot-password should fail if emailing is not set up', async () => {
|
||||
|
|
|
@ -481,13 +481,15 @@ test('POST /users should fail if user management is disabled', async () => {
|
|||
expect(response.statusCode).toBe(500);
|
||||
});
|
||||
|
||||
test('POST /users should email invites and create user shells', async () => {
|
||||
test('POST /users should email invites and create user shells but ignore existing', async () => {
|
||||
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
|
||||
const member = await testDb.createUser({ globalRole: globalMemberRole });
|
||||
const memberShell = await testDb.createUserShell(globalMemberRole);
|
||||
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
|
||||
|
||||
await utils.configureSmtp();
|
||||
|
||||
const testEmails = [randomEmail(), randomEmail(), randomEmail()];
|
||||
const testEmails = [randomEmail(), randomEmail().toUpperCase(), memberShell.email, member.email];
|
||||
|
||||
const payload = testEmails.map((e) => ({ email: e }));
|
||||
|
||||
|
@ -495,12 +497,17 @@ test('POST /users should email invites and create user shells', async () => {
|
|||
|
||||
expect(response.statusCode).toBe(200);
|
||||
|
||||
await Promise.all(
|
||||
response.body.data.map(async ({ user, error }: { user: User; error: Error }) => {
|
||||
const { id, email: receivedEmail } = user;
|
||||
|
||||
for (const {
|
||||
user: { id, email: receivedEmail },
|
||||
error,
|
||||
} of response.body.data) {
|
||||
expect(validator.isUUID(id)).toBe(true);
|
||||
expect(testEmails.some((e) => e === receivedEmail)).toBe(true);
|
||||
expect(id).not.toBe(member.id);
|
||||
|
||||
const lowerCasedEmail = receivedEmail.toLowerCase();
|
||||
expect(receivedEmail).toBe(lowerCasedEmail);
|
||||
expect(payload.some(({ email }) => email.toLowerCase() === lowerCasedEmail)).toBe(true);
|
||||
|
||||
if (error) {
|
||||
expect(error).toBe('Email could not be sent');
|
||||
}
|
||||
|
@ -514,8 +521,7 @@ test('POST /users should email invites and create user shells', async () => {
|
|||
expect(personalizationAnswers).toBeNull();
|
||||
expect(password).toBeNull();
|
||||
expect(resetPasswordToken).toBeNull();
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('POST /users should fail with invalid inputs', async () => {
|
||||
|
|
Loading…
Reference in a new issue