refactor(core): Move all base URLs to UrlService (no-changelog) (#8141)

This change kept coming up in #6713, #7773, and #8135. 
So this PR moves the existing code without actually changing anything,
to help get rid of some of the circular dependencies.


## Review / Merge checklist
- [x] PR title and summary are descriptive.
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2023-12-22 15:19:50 +01:00 committed by GitHub
parent 517b050d0a
commit baee47a276
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 93 additions and 85 deletions

View file

@ -1,6 +1,5 @@
import type express from 'express'; import type express from 'express';
import { validate } from 'class-validator'; import { validate } from 'class-validator';
import config from '@/config';
import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
import type { TagEntity } from '@db/entities/TagEntity'; import type { TagEntity } from '@db/entities/TagEntity';
@ -8,21 +7,6 @@ import type { User } from '@db/entities/User';
import type { UserUpdatePayload } from '@/requests'; import type { UserUpdatePayload } from '@/requests';
import { BadRequestError } from './errors/response-errors/bad-request.error'; import { BadRequestError } from './errors/response-errors/bad-request.error';
/**
* Returns the base URL n8n is reachable from
*/
export function getBaseUrl(): string {
const protocol = config.getEnv('protocol');
const host = config.getEnv('host');
const port = config.getEnv('port');
const path = config.getEnv('path');
if ((protocol === 'http' && port === 80) || (protocol === 'https' && port === 443)) {
return `${protocol}://${host}${path}`;
}
return `${protocol}://${host}:${port}${path}`;
}
/** /**
* Returns the session id if one is set * Returns the session id if one is set
*/ */

View file

@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
import { Container } from 'typedi';
import type { Router } from 'express'; import type { Router } from 'express';
import express from 'express'; import express from 'express';
import fs from 'fs/promises'; import fs from 'fs/promises';
@ -11,11 +12,11 @@ import type { OpenAPIV3 } from 'openapi-types';
import type { JsonObject } from 'swagger-ui-express'; import type { JsonObject } from 'swagger-ui-express';
import config from '@/config'; import config from '@/config';
import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { License } from '@/License'; import { License } from '@/License';
import { UserRepository } from '@db/repositories/user.repository'; import { UserRepository } from '@db/repositories/user.repository';
import { UrlService } from '@/services/url.service';
async function createApiRouter( async function createApiRouter(
version: string, version: string,
@ -29,7 +30,7 @@ async function createApiRouter(
// from the Swagger UI // from the Swagger UI
swaggerDocument.server = [ swaggerDocument.server = [
{ {
url: `${getInstanceBaseUrl()}/${publicApiEndpoint}/${version}}`, url: `${Container.get(UrlService).getInstanceBaseUrl()}/${publicApiEndpoint}/${version}}`,
}, },
]; ];
const apiController = express.Router(); const apiController = express.Router();

View file

@ -1,30 +1,15 @@
import { In } from 'typeorm'; import { In } from 'typeorm';
import { Container } from 'typedi'; import { Container } from 'typedi';
import type { Scope } from '@n8n/permissions';
import type { WhereClause } from '@/Interfaces'; import type { WhereClause } from '@/Interfaces';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import config from '@/config';
import { License } from '@/License'; import { License } from '@/License';
import { getWebhookBaseUrl } from '@/WebhookHelpers';
import type { Scope } from '@n8n/permissions';
export function isSharingEnabled(): boolean { export function isSharingEnabled(): boolean {
return Container.get(License).isSharingEnabled(); return Container.get(License).isSharingEnabled();
} }
/**
* Return the n8n instance base URL without trailing slash.
*/
export function getInstanceBaseUrl(): string {
const n8nBaseUrl = config.getEnv('editorBaseUrl') || getWebhookBaseUrl();
return n8nBaseUrl.endsWith('/') ? n8nBaseUrl.slice(0, n8nBaseUrl.length - 1) : n8nBaseUrl;
}
export function generateUserInviteUrl(inviterId: string, inviteeId: string): string {
return `${getInstanceBaseUrl()}/signup?inviterId=${inviterId}&inviteeId=${inviteeId}`;
}
// return the difference between two arrays // return the difference between two arrays
export function rightDiff<T1, T2>( export function rightDiff<T1, T2>(
[arr1, keyExtractor1]: [T1[], (item: T1) => string], [arr1, keyExtractor1]: [T1[], (item: T1) => string],

View file

@ -49,7 +49,6 @@ import type {
WebhookCORSRequest, WebhookCORSRequest,
WebhookRequest, WebhookRequest,
} from '@/Interfaces'; } from '@/Interfaces';
import * as GenericHelpers from '@/GenericHelpers';
import * as ResponseHelper from '@/ResponseHelper'; import * as ResponseHelper from '@/ResponseHelper';
import * as WorkflowHelpers from '@/WorkflowHelpers'; import * as WorkflowHelpers from '@/WorkflowHelpers';
import { WorkflowRunner } from '@/WorkflowRunner'; import { WorkflowRunner } from '@/WorkflowRunner';
@ -820,14 +819,3 @@ export async function executeWebhook(
return; return;
} }
} }
/**
* Returns the base URL of the webhooks
*/
export function getWebhookBaseUrl() {
let urlBaseWebhook = process.env.WEBHOOK_URL ?? GenericHelpers.getBaseUrl();
if (!urlBaseWebhook.endsWith('/')) {
urlBaseWebhook += '/';
}
return urlBaseWebhook;
}

View file

@ -48,7 +48,6 @@ import type {
} from '@/Interfaces'; } from '@/Interfaces';
import { NodeTypes } from '@/NodeTypes'; import { NodeTypes } from '@/NodeTypes';
import { Push } from '@/push'; import { Push } from '@/push';
import * as WebhookHelpers from '@/WebhookHelpers';
import * as WorkflowHelpers from '@/WorkflowHelpers'; import * as WorkflowHelpers from '@/WorkflowHelpers';
import { findSubworkflowStart, isWorkflowIdValid } from '@/utils'; import { findSubworkflowStart, isWorkflowIdValid } from '@/utils';
import { PermissionChecker } from './UserManagement/PermissionChecker'; import { PermissionChecker } from './UserManagement/PermissionChecker';
@ -68,6 +67,7 @@ import { Logger } from './Logger';
import { saveExecutionProgress } from './executionLifecycleHooks/saveExecutionProgress'; import { saveExecutionProgress } from './executionLifecycleHooks/saveExecutionProgress';
import { WorkflowStaticDataService } from './workflows/workflowStaticData.service'; import { WorkflowStaticDataService } from './workflows/workflowStaticData.service';
import { WorkflowRepository } from './databases/repositories/workflow.repository'; import { WorkflowRepository } from './databases/repositories/workflow.repository';
import { UrlService } from './services/url.service';
const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType'); const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');
@ -133,7 +133,7 @@ export function executeErrorWorkflow(
// Check if there was an error and if so if an errorWorkflow or a trigger is set // Check if there was an error and if so if an errorWorkflow or a trigger is set
let pastExecutionUrl: string | undefined; let pastExecutionUrl: string | undefined;
if (executionId !== undefined) { if (executionId !== undefined) {
pastExecutionUrl = `${WebhookHelpers.getWebhookBaseUrl()}workflow/${ pastExecutionUrl = `${Container.get(UrlService).getWebhookBaseUrl()}workflow/${
workflowData.id workflowData.id
}/executions/${executionId}`; }/executions/${executionId}`;
} }
@ -965,7 +965,7 @@ export async function getBase(
currentNodeParameters?: INodeParameters, currentNodeParameters?: INodeParameters,
executionTimeoutTimestamp?: number, executionTimeoutTimestamp?: number,
): Promise<IWorkflowExecuteAdditionalData> { ): Promise<IWorkflowExecuteAdditionalData> {
const urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl(); const urlBaseWebhook = Container.get(UrlService).getWebhookBaseUrl();
const formWaitingBaseUrl = urlBaseWebhook + config.getEnv('endpoints.formWaiting'); const formWaitingBaseUrl = urlBaseWebhook + config.getEnv('endpoints.formWaiting');

View file

@ -15,7 +15,6 @@ import config from '@/config';
import { ActiveExecutions } from '@/ActiveExecutions'; import { ActiveExecutions } from '@/ActiveExecutions';
import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; import { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner';
import * as GenericHelpers from '@/GenericHelpers';
import { Server } from '@/Server'; import { Server } from '@/Server';
import { EDITOR_UI_DIST_DIR, LICENSE_FEATURES } from '@/constants'; import { EDITOR_UI_DIST_DIR, LICENSE_FEATURES } from '@/constants';
import { eventBus } from '@/eventbus'; import { eventBus } from '@/eventbus';
@ -27,6 +26,7 @@ import { SingleMainSetup } from '@/services/orchestration/main/SingleMainSetup';
import { OrchestrationHandlerMainService } from '@/services/orchestration/main/orchestration.handler.main.service'; import { OrchestrationHandlerMainService } from '@/services/orchestration/main/orchestration.handler.main.service';
import { PruningService } from '@/services/pruning.service'; import { PruningService } from '@/services/pruning.service';
import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee'; import { MultiMainSetup } from '@/services/orchestration/main/MultiMainSetup.ee';
import { UrlService } from '@/services/url.service';
import { SettingsRepository } from '@db/repositories/settings.repository'; import { SettingsRepository } from '@db/repositories/settings.repository';
import { ExecutionRepository } from '@db/repositories/execution.repository'; import { ExecutionRepository } from '@db/repositories/execution.repository';
import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error'; import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error';
@ -77,7 +77,7 @@ export class Start extends BaseCommand {
* Opens the UI in browser * Opens the UI in browser
*/ */
private openBrowser() { private openBrowser() {
const editorUrl = GenericHelpers.getBaseUrl(); const editorUrl = Container.get(UrlService).baseUrl;
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
open(editorUrl, { wait: true }).catch((error: Error) => { open(editorUrl, { wait: true }).catch((error: Error) => {
@ -321,7 +321,7 @@ export class Start extends BaseCommand {
// Start to get active workflows and run their triggers // Start to get active workflows and run their triggers
await this.activeWorkflowRunner.init(); await this.activeWorkflowRunner.init();
const editorUrl = GenericHelpers.getBaseUrl(); const editorUrl = Container.get(UrlService).baseUrl;
this.log(`\nEditor is now accessible via:\n${editorUrl}`); this.log(`\nEditor is now accessible via:\n${editorUrl}`);
// Allow to open n8n editor by pressing "o" // Allow to open n8n editor by pressing "o"

View file

@ -7,13 +7,13 @@ import type { User } from '@db/entities/User';
import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { CredentialsRepository } from '@db/repositories/credentials.repository';
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository'; import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
import type { ICredentialsDb } from '@/Interfaces'; import type { ICredentialsDb } from '@/Interfaces';
import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper';
import type { OAuthRequest } from '@/requests'; import type { OAuthRequest } from '@/requests';
import { RESPONSE_ERROR_MESSAGES } from '@/constants'; import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import { CredentialsHelper } from '@/CredentialsHelper'; import { CredentialsHelper } from '@/CredentialsHelper';
import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData'; import * as WorkflowExecuteAdditionalData from '@/WorkflowExecuteAdditionalData';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
import { ExternalHooks } from '@/ExternalHooks'; import { ExternalHooks } from '@/ExternalHooks';
import { UrlService } from '@/services/url.service';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error';
@ -27,10 +27,11 @@ export abstract class AbstractOAuthController {
private readonly credentialsHelper: CredentialsHelper, private readonly credentialsHelper: CredentialsHelper,
private readonly credentialsRepository: CredentialsRepository, private readonly credentialsRepository: CredentialsRepository,
private readonly sharedCredentialsRepository: SharedCredentialsRepository, private readonly sharedCredentialsRepository: SharedCredentialsRepository,
private readonly urlService: UrlService,
) {} ) {}
get baseUrl() { get baseUrl() {
const restUrl = `${getInstanceBaseUrl()}/${config.getEnv('endpoints.rest')}`; const restUrl = `${this.urlService.getInstanceBaseUrl()}/${config.getEnv('endpoints.rest')}`;
return `${restUrl}/oauth${this.oauthVersion}-credential`; return `${restUrl}/oauth${this.oauthVersion}-credential`;
} }

View file

@ -5,7 +5,6 @@ import { IsNull, Not } from 'typeorm';
import validator from 'validator'; import validator from 'validator';
import { Get, Post, RestController } from '@/decorators'; import { Get, Post, RestController } from '@/decorators';
import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper';
import { PasswordUtility } from '@/services/password.utility'; import { PasswordUtility } from '@/services/password.utility';
import { UserManagementMailer } from '@/UserManagement/email'; import { UserManagementMailer } from '@/UserManagement/email';
import { PasswordResetRequest } from '@/requests'; import { PasswordResetRequest } from '@/requests';
@ -19,6 +18,7 @@ import { MfaService } from '@/Mfa/mfa.service';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
import { ExternalHooks } from '@/ExternalHooks'; import { ExternalHooks } from '@/ExternalHooks';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { UrlService } from '@/services/url.service';
import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error'; import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
@ -41,6 +41,7 @@ export class PasswordResetController {
private readonly mailer: UserManagementMailer, private readonly mailer: UserManagementMailer,
private readonly userService: UserService, private readonly userService: UserService,
private readonly mfaService: MfaService, private readonly mfaService: MfaService,
private readonly urlService: UrlService,
private readonly license: License, private readonly license: License,
private readonly passwordUtility: PasswordUtility, private readonly passwordUtility: PasswordUtility,
) {} ) {}
@ -130,7 +131,7 @@ export class PasswordResetController {
firstName, firstName,
lastName, lastName,
passwordResetUrl: url, passwordResetUrl: url,
domain: getInstanceBaseUrl(), domain: this.urlService.getInstanceBaseUrl(),
}); });
} catch (error) { } catch (error) {
void this.internalHooks.onEmailFailed({ void this.internalHooks.onEmailFailed({

View file

@ -2,7 +2,7 @@ import { Container, Service } from 'typedi';
import type { Variables } from '@db/entities/Variables'; import type { Variables } from '@db/entities/Variables';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { generateNanoId } from '@db/utils/generators'; import { generateNanoId } from '@db/utils/generators';
import { canCreateNewVariable } from './enviromentHelpers'; import { canCreateNewVariable } from './environmentHelpers';
import { CacheService } from '@/services/cache.service'; import { CacheService } from '@/services/cache.service';
import { VariablesRepository } from '@db/repositories/variables.repository'; import { VariablesRepository } from '@db/repositories/variables.repository';
import type { DeepPartial } from 'typeorm'; import type { DeepPartial } from 'typeorm';

View file

@ -12,18 +12,16 @@ import type {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { InstanceSettings } from 'n8n-core'; import { InstanceSettings } from 'n8n-core';
import config from '@/config';
import { LICENSE_FEATURES } from '@/constants'; import { LICENSE_FEATURES } from '@/constants';
import { CredentialsOverwrites } from '@/CredentialsOverwrites'; import { CredentialsOverwrites } from '@/CredentialsOverwrites';
import { CredentialTypes } from '@/CredentialTypes'; import { CredentialTypes } from '@/CredentialTypes';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
import { License } from '@/License'; import { License } from '@/License';
import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper';
import * as WebhookHelpers from '@/WebhookHelpers';
import config from '@/config';
import { getCurrentAuthenticationMethod } from '@/sso/ssoHelpers'; import { getCurrentAuthenticationMethod } from '@/sso/ssoHelpers';
import { getLdapLoginLabel } from '@/Ldap/helpers'; import { getLdapLoginLabel } from '@/Ldap/helpers';
import { getSamlLoginLabel } from '@/sso/saml/samlHelpers'; import { getSamlLoginLabel } from '@/sso/saml/samlHelpers';
import { getVariablesLimit } from '@/environments/variables/enviromentHelpers'; import { getVariablesLimit } from '@/environments/variables/environmentHelpers';
import { import {
getWorkflowHistoryLicensePruneTime, getWorkflowHistoryLicensePruneTime,
getWorkflowHistoryPruneTime, getWorkflowHistoryPruneTime,
@ -31,6 +29,7 @@ import {
import { UserManagementMailer } from '@/UserManagement/email'; import { UserManagementMailer } from '@/UserManagement/email';
import type { CommunityPackagesService } from '@/services/communityPackages.service'; import type { CommunityPackagesService } from '@/services/communityPackages.service';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
import { UrlService } from './url.service';
@Service() @Service()
export class FrontendService { export class FrontendService {
@ -46,6 +45,7 @@ export class FrontendService {
private readonly license: License, private readonly license: License,
private readonly mailer: UserManagementMailer, private readonly mailer: UserManagementMailer,
private readonly instanceSettings: InstanceSettings, private readonly instanceSettings: InstanceSettings,
private readonly urlService: UrlService,
) { ) {
loadNodesAndCredentials.addPostProcessor(async () => this.generateTypes()); loadNodesAndCredentials.addPostProcessor(async () => this.generateTypes());
void this.generateTypes(); void this.generateTypes();
@ -61,7 +61,7 @@ export class FrontendService {
} }
private initSettings() { private initSettings() {
const instanceBaseUrl = getInstanceBaseUrl(); const instanceBaseUrl = this.urlService.getInstanceBaseUrl();
const restEndpoint = config.getEnv('endpoints.rest'); const restEndpoint = config.getEnv('endpoints.rest');
const telemetrySettings: ITelemetrySettings = { const telemetrySettings: ITelemetrySettings = {
@ -93,7 +93,7 @@ export class FrontendService {
maxExecutionTimeout: config.getEnv('executions.maxTimeout'), maxExecutionTimeout: config.getEnv('executions.maxTimeout'),
workflowCallerPolicyDefaultOption: config.getEnv('workflows.callerPolicyDefaultOption'), workflowCallerPolicyDefaultOption: config.getEnv('workflows.callerPolicyDefaultOption'),
timezone: config.getEnv('generic.timezone'), timezone: config.getEnv('generic.timezone'),
urlBaseWebhook: WebhookHelpers.getWebhookBaseUrl(), urlBaseWebhook: this.urlService.getWebhookBaseUrl(),
urlBaseEditor: instanceBaseUrl, urlBaseEditor: instanceBaseUrl,
versionCli: '', versionCli: '',
releaseChannel: config.getEnv('generic.releaseChannel'), releaseChannel: config.getEnv('generic.releaseChannel'),
@ -222,8 +222,8 @@ export class FrontendService {
const restEndpoint = config.getEnv('endpoints.rest'); const restEndpoint = config.getEnv('endpoints.rest');
// Update all urls, in case `WEBHOOK_URL` was updated by `--tunnel` // Update all urls, in case `WEBHOOK_URL` was updated by `--tunnel`
const instanceBaseUrl = getInstanceBaseUrl(); const instanceBaseUrl = this.urlService.getInstanceBaseUrl();
this.settings.urlBaseWebhook = WebhookHelpers.getWebhookBaseUrl(); this.settings.urlBaseWebhook = this.urlService.getWebhookBaseUrl();
this.settings.urlBaseEditor = instanceBaseUrl; this.settings.urlBaseEditor = instanceBaseUrl;
this.settings.oauthCallbackUrls = { this.settings.oauthCallbackUrls = {
oauth1: `${instanceBaseUrl}/${restEndpoint}/oauth1-credential/callback`, oauth1: `${instanceBaseUrl}/${restEndpoint}/oauth1-credential/callback`,

View file

@ -0,0 +1,40 @@
import { Service } from 'typedi';
import config from '@/config';
@Service()
export class UrlService {
/** Returns the base URL n8n is reachable from */
readonly baseUrl: string;
constructor() {
this.baseUrl = this.generateBaseUrl();
}
/** Returns the base URL of the webhooks */
getWebhookBaseUrl() {
let urlBaseWebhook = process.env.WEBHOOK_URL ?? this.baseUrl;
if (!urlBaseWebhook.endsWith('/')) {
urlBaseWebhook += '/';
}
return urlBaseWebhook;
}
/** Return the n8n instance base URL without trailing slash */
getInstanceBaseUrl(): string {
const n8nBaseUrl = config.getEnv('editorBaseUrl') || this.getWebhookBaseUrl();
return n8nBaseUrl.endsWith('/') ? n8nBaseUrl.slice(0, n8nBaseUrl.length - 1) : n8nBaseUrl;
}
private generateBaseUrl(): string {
const protocol = config.getEnv('protocol');
const host = config.getEnv('host');
const port = config.getEnv('port');
const path = config.getEnv('path');
if ((protocol === 'http' && port === 80) || (protocol === 'https' && port === 443)) {
return `${protocol}://${host}${path}`;
}
return `${protocol}://${host}:${port}${path}`;
}
}

View file

@ -1,10 +1,9 @@
import Container, { Service } from 'typedi'; import { Container, Service } from 'typedi';
import type { EntityManager, FindManyOptions, FindOneOptions, FindOptionsWhere } from 'typeorm'; import type { EntityManager, FindManyOptions, FindOneOptions, FindOptionsWhere } from 'typeorm';
import { In } from 'typeorm'; import { In } from 'typeorm';
import { User } from '@db/entities/User'; import { User } from '@db/entities/User';
import type { IUserSettings } from 'n8n-workflow'; import type { IUserSettings } from 'n8n-workflow';
import { UserRepository } from '@db/repositories/user.repository'; import { UserRepository } from '@db/repositories/user.repository';
import { generateUserInviteUrl, getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper';
import type { PublicUser } from '@/Interfaces'; import type { PublicUser } from '@/Interfaces';
import type { PostHogClient } from '@/posthog'; import type { PostHogClient } from '@/posthog';
import { type JwtPayload, JwtService } from './jwt.service'; import { type JwtPayload, JwtService } from './jwt.service';
@ -14,6 +13,7 @@ import { createPasswordSha } from '@/auth/jwt';
import { UserManagementMailer } from '@/UserManagement/email'; import { UserManagementMailer } from '@/UserManagement/email';
import { InternalHooks } from '@/InternalHooks'; import { InternalHooks } from '@/InternalHooks';
import { RoleService } from '@/services/role.service'; import { RoleService } from '@/services/role.service';
import { UrlService } from '@/services/url.service';
import { ApplicationError, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow'; import { ApplicationError, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
import type { UserRequest } from '@/requests'; import type { UserRequest } from '@/requests';
import { InternalServerError } from '@/errors/response-errors/internal-server.error'; import { InternalServerError } from '@/errors/response-errors/internal-server.error';
@ -26,6 +26,7 @@ export class UserService {
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
private readonly mailer: UserManagementMailer, private readonly mailer: UserManagementMailer,
private readonly roleService: RoleService, private readonly roleService: RoleService,
private readonly urlService: UrlService,
) {} ) {}
async findOne(options: FindOneOptions<User>) { async findOne(options: FindOneOptions<User>) {
@ -78,7 +79,7 @@ export class UserService {
} }
generatePasswordResetUrl(user: User) { generatePasswordResetUrl(user: User) {
const instanceBaseUrl = getInstanceBaseUrl(); const instanceBaseUrl = this.urlService.getInstanceBaseUrl();
const url = new URL(`${instanceBaseUrl}/change-password`); const url = new URL(`${instanceBaseUrl}/change-password`);
url.searchParams.append('token', this.generatePasswordResetToken(user)); url.searchParams.append('token', this.generatePasswordResetToken(user));
@ -161,7 +162,7 @@ export class UserService {
} }
private addInviteUrl(inviterId: string, invitee: PublicUser) { private addInviteUrl(inviterId: string, invitee: PublicUser) {
const url = new URL(getInstanceBaseUrl()); const url = new URL(this.urlService.getInstanceBaseUrl());
url.pathname = '/signup'; url.pathname = '/signup';
url.searchParams.set('inviterId', inviterId); url.searchParams.set('inviterId', inviterId);
url.searchParams.set('inviteeId', invitee.id); url.searchParams.set('inviteeId', invitee.id);
@ -193,11 +194,11 @@ export class UserService {
toInviteUsers: { [key: string]: string }, toInviteUsers: { [key: string]: string },
role: 'member' | 'admin', role: 'member' | 'admin',
) { ) {
const domain = getInstanceBaseUrl(); const domain = this.urlService.getInstanceBaseUrl();
return Promise.all( return Promise.all(
Object.entries(toInviteUsers).map(async ([email, id]) => { Object.entries(toInviteUsers).map(async ([email, id]) => {
const inviteAcceptUrl = generateUserInviteUrl(owner.id, id); const inviteAcceptUrl = `${domain}/signup?inviterId=${owner.id}&inviteeId=${id}`;
const invitedUser: UserRequest.InviteResponse = { const invitedUser: UserRequest.InviteResponse = {
user: { user: {
id, id,

View file

@ -1,6 +1,5 @@
import express from 'express'; import express from 'express';
import { Container, Service } from 'typedi'; import { Container, Service } from 'typedi';
import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper';
import { import {
Authorized, Authorized,
Get, Get,
@ -35,12 +34,16 @@ import url from 'url';
import querystring from 'querystring'; import querystring from 'querystring';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { AuthError } from '@/errors/response-errors/auth.error'; import { AuthError } from '@/errors/response-errors/auth.error';
import { UrlService } from '@/services/url.service';
@Service() @Service()
@Authorized() @Authorized()
@RestController('/sso/saml') @RestController('/sso/saml')
export class SamlController { export class SamlController {
constructor(private samlService: SamlService) {} constructor(
private readonly samlService: SamlService,
private readonly urlService: UrlService,
) {}
@NoAuthRequired() @NoAuthRequired()
@Get(SamlUrls.metadata) @Get(SamlUrls.metadata)
@ -147,10 +150,10 @@ export class SamlController {
if (isSamlLicensedAndEnabled()) { if (isSamlLicensedAndEnabled()) {
await issueCookie(res, loginResult.authenticatedUser); await issueCookie(res, loginResult.authenticatedUser);
if (loginResult.onboardingRequired) { if (loginResult.onboardingRequired) {
return res.redirect(getInstanceBaseUrl() + SamlUrls.samlOnboarding); return res.redirect(this.urlService.getInstanceBaseUrl() + SamlUrls.samlOnboarding);
} else { } else {
const redirectUrl = req.body?.RelayState ?? SamlUrls.defaultRedirect; const redirectUrl = req.body?.RelayState ?? SamlUrls.defaultRedirect;
return res.redirect(getInstanceBaseUrl() + redirectUrl); return res.redirect(this.urlService.getInstanceBaseUrl() + redirectUrl);
} }
} else { } else {
return res.status(202).send(loginResult.attributes); return res.status(202).send(loginResult.attributes);

View file

@ -24,12 +24,12 @@ import axios from 'axios';
import https from 'https'; import https from 'https';
import type { SamlLoginBinding } from './types'; import type { SamlLoginBinding } from './types';
import { validateMetadata, validateResponse } from './samlValidator'; import { validateMetadata, validateResponse } from './samlValidator';
import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper';
import { Logger } from '@/Logger'; import { Logger } from '@/Logger';
import { UserRepository } from '@db/repositories/user.repository'; import { UserRepository } from '@db/repositories/user.repository';
import { SettingsRepository } from '@db/repositories/settings.repository'; import { SettingsRepository } from '@db/repositories/settings.repository';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { AuthError } from '@/errors/response-errors/auth.error'; import { AuthError } from '@/errors/response-errors/auth.error';
import { UrlService } from '@/services/url.service';
@Service() @Service()
export class SamlService { export class SamlService {
@ -55,7 +55,7 @@ export class SamlService {
loginLabel: 'SAML', loginLabel: 'SAML',
wantAssertionsSigned: true, wantAssertionsSigned: true,
wantMessageSigned: true, wantMessageSigned: true,
relayState: getInstanceBaseUrl(), relayState: this.urlService.getInstanceBaseUrl(),
signatureConfig: { signatureConfig: {
prefix: 'ds', prefix: 'ds',
location: { location: {
@ -73,7 +73,10 @@ export class SamlService {
}; };
} }
constructor(private readonly logger: Logger) {} constructor(
private readonly logger: Logger,
private readonly urlService: UrlService,
) {}
async init(): Promise<void> { async init(): Promise<void> {
// load preferences first but do not apply so as to not load samlify unnecessarily // load preferences first but do not apply so as to not load samlify unnecessarily
@ -143,14 +146,14 @@ export class SamlService {
private getRedirectLoginRequestUrl(relayState?: string): BindingContext { private getRedirectLoginRequestUrl(relayState?: string): BindingContext {
const sp = this.getServiceProviderInstance(); const sp = this.getServiceProviderInstance();
sp.entitySetting.relayState = relayState ?? getInstanceBaseUrl(); sp.entitySetting.relayState = relayState ?? this.urlService.getInstanceBaseUrl();
const loginRequest = sp.createLoginRequest(this.getIdentityProviderInstance(), 'redirect'); const loginRequest = sp.createLoginRequest(this.getIdentityProviderInstance(), 'redirect');
return loginRequest; return loginRequest;
} }
private getPostLoginRequestUrl(relayState?: string): PostBindingContext { private getPostLoginRequestUrl(relayState?: string): PostBindingContext {
const sp = this.getServiceProviderInstance(); const sp = this.getServiceProviderInstance();
sp.entitySetting.relayState = relayState ?? getInstanceBaseUrl(); sp.entitySetting.relayState = relayState ?? this.urlService.getInstanceBaseUrl();
const loginRequest = sp.createLoginRequest( const loginRequest = sp.createLoginRequest(
this.getIdentityProviderInstance(), this.getIdentityProviderInstance(),
'post', 'post',

View file

@ -1,21 +1,22 @@
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
import { getInstanceBaseUrl } from '@/UserManagement/UserManagementHelper'; import { Container } from 'typedi';
import type { ServiceProviderInstance } from 'samlify'; import type { ServiceProviderInstance } from 'samlify';
import { UrlService } from '@/services/url.service';
import { SamlUrls } from './constants'; import { SamlUrls } from './constants';
import type { SamlPreferences } from './types/samlPreferences'; import type { SamlPreferences } from './types/samlPreferences';
let serviceProviderInstance: ServiceProviderInstance | undefined; let serviceProviderInstance: ServiceProviderInstance | undefined;
export function getServiceProviderEntityId(): string { export function getServiceProviderEntityId(): string {
return getInstanceBaseUrl() + SamlUrls.restMetadata; return Container.get(UrlService).getInstanceBaseUrl() + SamlUrls.restMetadata;
} }
export function getServiceProviderReturnUrl(): string { export function getServiceProviderReturnUrl(): string {
return getInstanceBaseUrl() + SamlUrls.restAcs; return Container.get(UrlService).getInstanceBaseUrl() + SamlUrls.restAcs;
} }
export function getServiceProviderConfigTestReturnUrl(): string { export function getServiceProviderConfigTestReturnUrl(): string {
return getInstanceBaseUrl() + SamlUrls.configTestReturn; return Container.get(UrlService).getInstanceBaseUrl() + SamlUrls.configTestReturn;
} }
// TODO:SAML: make these configurable for the end user // TODO:SAML: make these configurable for the end user