refactor(core): Port user management config (#18205)

This commit is contained in:
Iván Ovejero
2025-08-11 16:10:58 +02:00
committed by GitHub
parent 58df26c70b
commit f69d8efa04
13 changed files with 142 additions and 72 deletions

View File

@@ -0,0 +1,67 @@
import { Container } from '@n8n/di';
import { UserManagementConfig } from '../user-management.config';
describe('UserManagementConfig', () => {
beforeEach(() => {
Container.reset();
jest.clearAllMocks();
});
const originalEnv = process.env;
afterEach(() => {
process.env = originalEnv;
});
test('with refresh timout > session, sets refresh timout to `0`', () => {
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
process.env = {
N8N_USER_MANAGEMENT_JWT_DURATION_HOURS: '1',
N8N_USER_MANAGEMENT_JWT_REFRESH_TIMEOUT_HOURS: '2',
};
const config = Container.get(UserManagementConfig);
expect(config.jwtRefreshTimeoutHours).toBe(0);
expect(consoleWarnSpy).toHaveBeenCalledWith(
'N8N_USER_MANAGEMENT_JWT_REFRESH_TIMEOUT_HOURS needs to be smaller than N8N_USER_MANAGEMENT_JWT_DURATION_HOURS. Setting N8N_USER_MANAGEMENT_JWT_REFRESH_TIMEOUT_HOURS to 0.',
);
consoleWarnSpy.mockRestore();
});
test('with refresh timout == session, sets refresh timout to `0`', () => {
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
process.env = {
N8N_USER_MANAGEMENT_JWT_DURATION_HOURS: '1',
N8N_USER_MANAGEMENT_JWT_REFRESH_TIMEOUT_HOURS: '1',
};
const config = Container.get(UserManagementConfig);
expect(config.jwtRefreshTimeoutHours).toBe(0);
expect(consoleWarnSpy).toHaveBeenCalledWith(
'N8N_USER_MANAGEMENT_JWT_REFRESH_TIMEOUT_HOURS needs to be smaller than N8N_USER_MANAGEMENT_JWT_DURATION_HOURS. Setting N8N_USER_MANAGEMENT_JWT_REFRESH_TIMEOUT_HOURS to 0.',
);
consoleWarnSpy.mockRestore();
});
test('with refresh timout < session, keeps refresh timout intact', () => {
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
process.env = {
N8N_USER_MANAGEMENT_JWT_DURATION_HOURS: '10',
N8N_USER_MANAGEMENT_JWT_REFRESH_TIMEOUT_HOURS: '5',
};
const config = Container.get(UserManagementConfig);
expect(config.jwtRefreshTimeoutHours).toBe(5);
expect(consoleWarnSpy).not.toHaveBeenCalled();
consoleWarnSpy.mockRestore();
});
});

View File

@@ -86,8 +86,34 @@ class EmailConfig {
template: TemplateConfig;
}
const INVALID_JWT_REFRESH_TIMEOUT_WARNING =
'N8N_USER_MANAGEMENT_JWT_REFRESH_TIMEOUT_HOURS needs to be smaller than N8N_USER_MANAGEMENT_JWT_DURATION_HOURS. Setting N8N_USER_MANAGEMENT_JWT_REFRESH_TIMEOUT_HOURS to 0.';
@Config
export class UserManagementConfig {
@Nested
emails: EmailConfig;
/** JWT secret to use. If unset, n8n will generate its own. */
@Env('N8N_USER_MANAGEMENT_JWT_SECRET')
jwtSecret: string = '';
/** How long (in hours) before the JWT expires. */
@Env('N8N_USER_MANAGEMENT_JWT_DURATION_HOURS')
jwtSessionDurationHours: number = 168;
/**
* How long (in hours) before expiration to automatically refresh it.
* - `0` means 25% of `N8N_USER_MANAGEMENT_JWT_DURATION_HOURS`.
* - `-1` means it will never refresh. This forces users to log back in after expiration.
*/
@Env('N8N_USER_MANAGEMENT_JWT_REFRESH_TIMEOUT_HOURS')
jwtRefreshTimeoutHours: number = 0;
sanitize() {
if (this.jwtRefreshTimeoutHours >= this.jwtSessionDurationHours) {
console.warn(INVALID_JWT_REFRESH_TIMEOUT_WARNING);
this.jwtRefreshTimeoutHours = 0;
}
}
}

View File

@@ -81,6 +81,9 @@ export const Config: ClassDecorator = (ConfigClass: Class) => {
}
}
}
if (typeof config.sanitize === 'function') config.sanitize();
return config;
};
// eslint-disable-next-line @typescript-eslint/no-unsafe-return

View File

@@ -2,6 +2,7 @@ import { Container } from '@n8n/di';
import fs from 'fs';
import { mock } from 'jest-mock-extended';
import type { UserManagementConfig } from '../src/configs/user-management.config';
import { GlobalConfig } from '../src/index';
jest.mock('fs');
@@ -101,6 +102,9 @@ describe('GlobalConfig', () => {
},
},
userManagement: {
jwtSecret: '',
jwtSessionDurationHours: 168,
jwtRefreshTimeoutHours: 0,
emails: {
mode: 'smtp',
smtp: {
@@ -124,7 +128,7 @@ describe('GlobalConfig', () => {
'project-shared': '',
},
},
},
} as UserManagementConfig,
eventBus: {
checkUnsentInterval: 0,
crashRecoveryMode: 'extensive',