Skip to content

Commit

Permalink
Merge branch 'master' into ask-assistant
Browse files Browse the repository at this point in the history
* master:
  docs: Update login url for OpenAI node (no-changelog) (#10222)
  fix(LinkedIn Node): Fix issue with some characters cutting off posts early (#10185)
  fix(Google BigQuery Node): Send timeoutMs in query, pagination support (#10205)
  feat(core): Show Public API key value only once (no-changelog) (#10126)
  refactor(core): Display stack trace in error reporter (no-changelog) (#10225)
  fix(core): Fix missing successful items on continueErrorOutput with multiple outputs (#10218)
  fix(n8n Form Trigger Node): Remove custom attribution option (no-changelog) (#10229)
  feat(n8n Form Trigger Node): Improvements (#10092)
  refactor(core): Port path, host, port, listen_address and protocol config (no-changelog) (#10223)
  docs: Update add options text (no-changelog) (#10049)
  fix(Postgres Node): Option to treat query parameters enclosed in single quotas as text (#10214)
  refactor(core): Decouple server started event from internal hooks (no-changelog) (#10221)
  feat(Shopify Node): Update Shopify API version (#10155)
  fix(editor): Defer `User saved credentials` telemetry event for OAuth credentials (#10215)
  fix(editor): Fix parameter input glitch when there was an error loading remote options (#10209)
  • Loading branch information
MiloradFilipovic committed Jul 30, 2024
2 parents bde612e + 47c7904 commit b20f5a6
Show file tree
Hide file tree
Showing 339 changed files with 2,328 additions and 998 deletions.
22 changes: 21 additions & 1 deletion packages/@n8n/config/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Config, Nested } from './decorators';
import { Config, Env, Nested } from './decorators';
import { CredentialsConfig } from './configs/credentials';
import { DatabaseConfig } from './configs/database';
import { EmailConfig } from './configs/email';
Expand Down Expand Up @@ -51,4 +51,24 @@ export class GlobalConfig {

@Nested
readonly workflows: WorkflowsConfig;

/** Path n8n is deployed to */
@Env('N8N_PATH')
readonly path: string = '/';

/** Host name n8n can be reached */
@Env('N8N_HOST')
readonly host: string = 'localhost';

/** HTTP port n8n can be reached */
@Env('N8N_PORT')
readonly port: number = 5678;

/** IP address n8n should listen on */
@Env('N8N_LISTEN_ADDRESS')
readonly listen_address: string = '0.0.0.0';

/** HTTP Protocol via which n8n can be reached */
@Env('N8N_PROTOCOL')
readonly protocol: 'http' | 'https' = 'http';
}
5 changes: 5 additions & 0 deletions packages/@n8n/config/test/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ describe('GlobalConfig', () => {
});

const defaultConfig: GlobalConfig = {
path: '/',
host: 'localhost',
port: 5678,
listen_address: '0.0.0.0',
protocol: 'http',
database: {
logging: {
enabled: false,
Expand Down
12 changes: 6 additions & 6 deletions packages/cli/src/AbstractServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { Logger } from '@/Logger';
import { ServiceUnavailableError } from './errors/response-errors/service-unavailable.error';
import { OnShutdown } from '@/decorators/OnShutdown';
import { ActiveWebhooks } from '@/ActiveWebhooks';
import { GlobalConfig } from '@n8n/config';

@Service()
export abstract class AbstractServer {
Expand All @@ -33,7 +34,7 @@ export abstract class AbstractServer {

protected externalHooks: ExternalHooks;

protected protocol = config.getEnv('protocol');
protected protocol = Container.get(GlobalConfig).protocol;

protected sslKey: string;

Expand Down Expand Up @@ -153,25 +154,24 @@ export abstract class AbstractServer {
this.server = http.createServer(app);
}

const PORT = config.getEnv('port');
const ADDRESS = config.getEnv('listen_address');
const { port, listen_address: address } = Container.get(GlobalConfig);

this.server.on('error', (error: Error & { code: string }) => {
if (error.code === 'EADDRINUSE') {
this.logger.info(
`n8n's port ${PORT} is already in use. Do you have another instance of n8n running already?`,
`n8n's port ${port} is already in use. Do you have another instance of n8n running already?`,
);
process.exit(1);
}
});

await new Promise<void>((resolve) => this.server.listen(PORT, ADDRESS, () => resolve()));
await new Promise<void>((resolve) => this.server.listen(port, address, () => resolve()));

this.externalHooks = Container.get(ExternalHooks);

await this.setupHealthCheck();

this.logger.info(`n8n ready on ${ADDRESS}, port ${PORT}`);
this.logger.info(`n8n ready on ${address}, port ${port}`);
}

async start(): Promise<void> {
Expand Down
78 changes: 0 additions & 78 deletions packages/cli/src/InternalHooks.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { Service } from 'typedi';
import { snakeCase } from 'change-case';
import os from 'node:os';
import { get as pslGet } from 'psl';
import { GlobalConfig } from '@n8n/config';
import type {
ExecutionStatus,
INodesGraphResult,
Expand All @@ -11,27 +9,23 @@ import type {
IWorkflowBase,
} from 'n8n-workflow';
import { TelemetryHelpers } from 'n8n-workflow';
import { InstanceSettings } from 'n8n-core';

import config from '@/config';
import { N8N_VERSION } from '@/constants';
import type { AuthProviderType } from '@db/entities/AuthIdentity';
import type { User } from '@db/entities/User';
import { SharedWorkflowRepository } from '@db/repositories/sharedWorkflow.repository';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { determineFinalExecutionStatus } from '@/executionLifecycleHooks/shared/sharedHookFunctions';
import type {
ITelemetryUserDeletionData,
IWorkflowDb,
IExecutionTrackProperties,
} from '@/Interfaces';
import { License } from '@/License';
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
import { NodeTypes } from '@/NodeTypes';
import { Telemetry } from '@/telemetry';
import type { Project } from '@db/entities/Project';
import { ProjectRelationRepository } from './databases/repositories/projectRelation.repository';
import { SharedCredentialsRepository } from './databases/repositories/sharedCredentials.repository';
import { MessageEventBus } from './eventbus/MessageEventBus/MessageEventBus';

/**
Expand All @@ -42,16 +36,11 @@ import { MessageEventBus } from './eventbus/MessageEventBus/MessageEventBus';
@Service()
export class InternalHooks {
constructor(
private readonly globalConfig: GlobalConfig,
private readonly telemetry: Telemetry,
private readonly nodeTypes: NodeTypes,
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
private readonly workflowRepository: WorkflowRepository,
workflowStatisticsService: WorkflowStatisticsService,
private readonly instanceSettings: InstanceSettings,
private readonly license: License,
private readonly projectRelationRepository: ProjectRelationRepository,
private readonly sharedCredentialsRepository: SharedCredentialsRepository,
private readonly _eventBus: MessageEventBus, // needed until we decouple telemetry
) {
workflowStatisticsService.on(
Expand All @@ -68,73 +57,6 @@ export class InternalHooks {
await this.telemetry.init();
}

async onServerStarted(): Promise<unknown[]> {
const cpus = os.cpus();
const binaryDataConfig = config.getEnv('binaryDataManager');

const isS3Selected = config.getEnv('binaryDataManager.mode') === 's3';
const isS3Available = config.getEnv('binaryDataManager.availableModes').includes('s3');
const isS3Licensed = this.license.isBinaryDataS3Licensed();
const authenticationMethod = config.getEnv('userManagement.authenticationMethod');

const info = {
version_cli: N8N_VERSION,
db_type: this.globalConfig.database.type,
n8n_version_notifications_enabled: this.globalConfig.versionNotifications.enabled,
n8n_disable_production_main_process: config.getEnv(
'endpoints.disableProductionWebhooksOnMainProcess',
),
system_info: {
os: {
type: os.type(),
version: os.version(),
},
memory: os.totalmem() / 1024,
cpus: {
count: cpus.length,
model: cpus[0].model,
speed: cpus[0].speed,
},
},
execution_variables: {
executions_mode: config.getEnv('executions.mode'),
executions_timeout: config.getEnv('executions.timeout'),
executions_timeout_max: config.getEnv('executions.maxTimeout'),
executions_data_save_on_error: config.getEnv('executions.saveDataOnError'),
executions_data_save_on_success: config.getEnv('executions.saveDataOnSuccess'),
executions_data_save_on_progress: config.getEnv('executions.saveExecutionProgress'),
executions_data_save_manual_executions: config.getEnv(
'executions.saveDataManualExecutions',
),
executions_data_prune: config.getEnv('executions.pruneData'),
executions_data_max_age: config.getEnv('executions.pruneDataMaxAge'),
},
n8n_deployment_type: config.getEnv('deployment.type'),
n8n_binary_data_mode: binaryDataConfig.mode,
smtp_set_up: this.globalConfig.userManagement.emails.mode === 'smtp',
ldap_allowed: authenticationMethod === 'ldap',
saml_enabled: authenticationMethod === 'saml',
license_plan_name: this.license.getPlanName(),
license_tenant_id: config.getEnv('license.tenantId'),
binary_data_s3: isS3Available && isS3Selected && isS3Licensed,
multi_main_setup_enabled: config.getEnv('multiMainSetup.enabled'),
};

const firstWorkflow = await this.workflowRepository.findOne({
select: ['createdAt'],
order: { createdAt: 'ASC' },
where: {},
});

return await Promise.all([
this.telemetry.identify(info),
this.telemetry.track('Instance started', {
...info,
earliest_workflow_created: firstWorkflow?.createdAt,
}),
]);
}

async onFrontendSettingsAPI(pushRef?: string): Promise<void> {
return await this.telemetry.track('Session started', { session_id: pushRef });
}
Expand Down
4 changes: 1 addition & 3 deletions packages/cli/src/PublicApi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import type { HttpError } from 'express-openapi-validator/dist/framework/types';
import type { OpenAPIV3 } from 'openapi-types';
import type { JsonObject } from 'swagger-ui-express';

import config from '@/config';

import { License } from '@/License';
import { UserRepository } from '@db/repositories/user.repository';
import { UrlService } from '@/services/url.service';
Expand All @@ -25,7 +23,7 @@ async function createApiRouter(
handlersDirectory: string,
publicApiEndpoint: string,
): Promise<Router> {
const n8nPath = config.getEnv('path');
const n8nPath = Container.get(GlobalConfig).path;
const swaggerDocument = YAML.load(openApiSpecPath) as JsonObject;
// add the server depending on the config so the user can interact with the API
// from the Swagger UI
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import { isLdapEnabled } from '@/Ldap/helpers.ee';
import { AbstractServer } from '@/AbstractServer';
import { PostHogClient } from '@/posthog';
import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus';
import { InternalHooks } from '@/InternalHooks';
import { handleMfaDisable, isMfaFeatureEnabled } from '@/Mfa/helpers';
import type { FrontendService } from '@/services/frontend.service';
import { OrchestrationService } from '@/services/orchestration.service';
Expand Down Expand Up @@ -69,6 +68,7 @@ import '@/ExternalSecrets/ExternalSecrets.controller.ee';
import '@/license/license.controller';
import '@/workflows/workflowHistory/workflowHistory.controller.ee';
import '@/workflows/workflows.controller';
import { EventService } from './eventbus/event.service';

const exec = promisify(callbackExec);

Expand All @@ -85,6 +85,7 @@ export class Server extends AbstractServer {
private readonly orchestrationService: OrchestrationService,
private readonly postHogClient: PostHogClient,
private readonly globalConfig: GlobalConfig,
private readonly eventService: EventService,
) {
super('main');

Expand All @@ -109,7 +110,7 @@ export class Server extends AbstractServer {
void this.loadNodesAndCredentials.setupHotReload();
}

void Container.get(InternalHooks).onServerStarted();
this.eventService.emit('server-started');
}

private async registerAdditionalControllers() {
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export class Start extends BaseCommand {

private async generateStaticAssets() {
// Read the index file and replace the path placeholder
const n8nPath = config.getEnv('path');
const n8nPath = Container.get(GlobalConfig).path;
const restEndpoint = config.getEnv('endpoints.rest');
const hooksUrls = config.getEnv('externalFrontendHooksUrls');

Expand Down Expand Up @@ -283,7 +283,7 @@ export class Start extends BaseCommand {
}

const { default: localtunnel } = await import('@n8n/localtunnel');
const port = config.getEnv('port');
const { port } = Container.get(GlobalConfig);

const webhookTunnel = await localtunnel(port, {
host: 'https://hooks.n8n.cloud',
Expand Down
34 changes: 0 additions & 34 deletions packages/cli/src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,40 +307,6 @@ export const schema = {
},
},

// How n8n can be reached (Editor & REST-API)
path: {
format: String,
default: '/',
arg: 'path',
env: 'N8N_PATH',
doc: 'Path n8n is deployed to',
},
host: {
format: String,
default: 'localhost',
arg: 'host',
env: 'N8N_HOST',
doc: 'Host name n8n can be reached',
},
port: {
format: Number,
default: 5678,
arg: 'port',
env: 'N8N_PORT',
doc: 'HTTP port n8n can be reached',
},
listen_address: {
format: String,
default: '0.0.0.0',
env: 'N8N_LISTEN_ADDRESS',
doc: 'IP address n8n should listen on',
},
protocol: {
format: ['http', 'https'] as const,
default: 'http',
env: 'N8N_PROTOCOL',
doc: 'HTTP Protocol via which n8n can be reached',
},
secure_cookie: {
doc: 'This sets the `Secure` flag on n8n auth cookie',
format: Boolean,
Expand Down
15 changes: 14 additions & 1 deletion packages/cli/src/controllers/me.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { UserRepository } from '@/databases/repositories/user.repository';
import { isApiEnabled } from '@/PublicApi';
import { EventService } from '@/eventbus/event.service';

export const API_KEY_PREFIX = 'n8n_api_';

export const isApiEnabledMiddleware: RequestHandler = (_, res, next) => {
if (isApiEnabled()) {
next();
Expand Down Expand Up @@ -208,7 +210,8 @@ export class MeController {
*/
@Get('/api-key', { middlewares: [isApiEnabledMiddleware] })
async getAPIKey(req: AuthenticatedRequest) {
return { apiKey: req.user.apiKey };
const apiKey = this.redactApiKey(req.user.apiKey);
return { apiKey };
}

/**
Expand Down Expand Up @@ -242,4 +245,14 @@ export class MeController {

return user.settings;
}

private redactApiKey(apiKey: string | null) {
if (!apiKey) return;
const keepLength = 5;
return (
API_KEY_PREFIX +
apiKey.slice(API_KEY_PREFIX.length, API_KEY_PREFIX.length + keepLength) +
'*'.repeat(apiKey.length - API_KEY_PREFIX.length - keepLength)
);
}
}
2 changes: 1 addition & 1 deletion packages/cli/src/databases/entities/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export class User extends WithTimestamps implements IUser {

@Column({ type: String, nullable: true })
@Index({ unique: true })
apiKey?: string | null;
apiKey: string | null;

@Column({ type: Boolean, default: false })
mfaEnabled: boolean;
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/eventbus/event.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export type UserLike = {
* Events sent by `EventService` and forwarded by relays, e.g. `AuditEventRelay` and `TelemetryEventRelay`.
*/
export type Event = {
'server-started': {};

'workflow-created': {
user: UserLike;
workflow: IWorkflowBase;
Expand Down
Loading

0 comments on commit b20f5a6

Please sign in to comment.