Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into typed-emitter
Browse files Browse the repository at this point in the history
  • Loading branch information
netroy committed Jul 30, 2024
2 parents b8f7c48 + 99dc56c commit 48210a1
Show file tree
Hide file tree
Showing 25 changed files with 154 additions and 93 deletions.
2 changes: 1 addition & 1 deletion packages/cli/src/License.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class License {
* This ensures the mains do not cause a 429 (too many requests) on license init.
*/
if (config.getEnv('multiMainSetup.enabled')) {
return autoRenewEnabled && config.getEnv('multiMainSetup.instanceType') === 'leader';
return autoRenewEnabled && config.getEnv('instanceRole') === 'leader';
}

return autoRenewEnabled;
Expand Down
7 changes: 2 additions & 5 deletions packages/cli/src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,7 @@ export class Start extends BaseCommand {
await this.initOrchestration();
this.logger.debug('Orchestration init complete');

if (
!config.getEnv('license.autoRenewEnabled') &&
config.getEnv('multiMainSetup.instanceType') === 'leader'
) {
if (!config.getEnv('license.autoRenewEnabled') && config.getEnv('instanceRole') === 'leader') {
this.logger.warn(
'Automatic license renewal is disabled. The license will not renew automatically, and access to licensed features may be lost!',
);
Expand All @@ -211,7 +208,7 @@ export class Start extends BaseCommand {

async initOrchestration() {
if (config.getEnv('executions.mode') === 'regular') {
config.set('multiMainSetup.instanceType', 'leader');
config.set('instanceRole', 'leader');
return;
}

Expand Down
11 changes: 6 additions & 5 deletions packages/cli/src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -883,12 +883,13 @@ export const schema = {
},
},

instanceRole: {
doc: 'Always `leader` in single-main setup. `leader` or `follower` in multi-main setup.',
format: ['unset', 'leader', 'follower'] as const,
default: 'unset', // only until Start.initOrchestration
},

multiMainSetup: {
instanceType: {
doc: 'Type of instance in multi-main setup',
format: ['unset', 'leader', 'follower'] as const,
default: 'unset', // only until first leader key check
},
enabled: {
doc: 'Whether to enable multi-main setup for queue mode (license required)',
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
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe('ExecutionRecoveryService', () => {
});

beforeEach(() => {
config.set('multiMainSetup.instanceType', 'leader');
config.set('instanceRole', 'leader');
});

afterEach(async () => {
Expand Down Expand Up @@ -130,7 +130,7 @@ describe('ExecutionRecoveryService', () => {
/**
* Arrange
*/
config.set('multiMainSetup.instanceType', 'follower');
config.set('instanceRole', 'follower');
// @ts-expect-error Private method
const amendSpy = jest.spyOn(executionRecoveryService, 'amend');
const messages = setupMessages('123', 'Some workflow');
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/executions/execution-recovery.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ export class ExecutionRecoveryService {
private shouldScheduleQueueRecovery() {
return (
config.getEnv('executions.mode') === 'queue' &&
config.getEnv('multiMainSetup.instanceType') === 'leader' &&
config.getEnv('instanceRole') === 'leader' &&
!this.isShuttingDown
);
}
Expand Down
9 changes: 3 additions & 6 deletions packages/cli/src/services/orchestration.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,12 @@ export class OrchestrationService {
return config.getEnv('redis.queueModeId');
}

/**
* Whether this instance is the leader in a multi-main setup. Always `false` in single-main setup.
*/
get isLeader() {
return config.getEnv('multiMainSetup.instanceType') === 'leader';
return config.getEnv('instanceRole') === 'leader';
}

get isFollower() {
return config.getEnv('multiMainSetup.instanceType') !== 'leader';
return config.getEnv('instanceRole') !== 'leader';
}

sanityCheck() {
Expand All @@ -66,7 +63,7 @@ export class OrchestrationService {
if (this.isMultiMainSetupEnabled) {
await this.multiMainSetup.init();
} else {
config.set('multiMainSetup.instanceType', 'leader');
config.set('instanceRole', 'leader');
}

this.isInitialized = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class MultiMainSetup extends TypedEmitter<MultiMainEvents> {
async shutdown() {
clearInterval(this.leaderCheckInterval);

const isLeader = config.getEnv('multiMainSetup.instanceType') === 'leader';
const isLeader = config.getEnv('instanceRole') === 'leader';

if (isLeader) await this.redisPublisher.clear(this.leaderKey);
}
Expand All @@ -69,8 +69,8 @@ export class MultiMainSetup extends TypedEmitter<MultiMainEvents> {
if (leaderId && leaderId !== this.instanceId) {
this.logger.debug(`[Instance ID ${this.instanceId}] Leader is other instance "${leaderId}"`);

if (config.getEnv('multiMainSetup.instanceType') === 'leader') {
config.set('multiMainSetup.instanceType', 'follower');
if (config.getEnv('instanceRole') === 'leader') {
config.set('instanceRole', 'follower');

this.emit('leader-stepdown'); // lost leadership - stop triggers, pollers, pruning, wait-tracking, queue recovery

Expand All @@ -85,7 +85,7 @@ export class MultiMainSetup extends TypedEmitter<MultiMainEvents> {
`[Instance ID ${this.instanceId}] Leadership vacant, attempting to become leader...`,
);

config.set('multiMainSetup.instanceType', 'follower');
config.set('instanceRole', 'follower');

/**
* Lost leadership - stop triggers, pollers, pruning, wait tracking, license renewal, queue recovery
Expand All @@ -106,7 +106,7 @@ export class MultiMainSetup extends TypedEmitter<MultiMainEvents> {
if (keySetSuccessfully) {
this.logger.debug(`[Instance ID ${this.instanceId}] Leader is now this instance`);

config.set('multiMainSetup.instanceType', 'leader');
config.set('instanceRole', 'leader');

await this.redisPublisher.setExpiration(this.leaderKey, this.leaderKeyTtl);

Expand All @@ -115,7 +115,7 @@ export class MultiMainSetup extends TypedEmitter<MultiMainEvents> {
*/
this.emit('leader-takeover');
} else {
config.set('multiMainSetup.instanceType', 'follower');
config.set('instanceRole', 'follower');
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/services/pruning.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class PruningService {
if (
config.getEnv('multiMainSetup.enabled') &&
config.getEnv('generic.instanceType') === 'main' &&
config.getEnv('multiMainSetup.instanceType') === 'follower'
config.getEnv('instanceRole') === 'follower'
) {
return false;
}
Expand Down
12 changes: 6 additions & 6 deletions packages/cli/test/integration/me.api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,14 +175,14 @@ describe('Owner shell', () => {
expect(storedShellOwner.apiKey).toEqual(response.body.data.apiKey);
});

test('GET /me/api-key should fetch the api key', async () => {
test('GET /me/api-key should fetch the api key redacted', async () => {
const response = await authOwnerShellAgent.get('/me/api-key');

expect(response.statusCode).toBe(200);
expect(response.body.data.apiKey).toEqual(ownerShell.apiKey);
expect(response.body.data.apiKey).not.toEqual(ownerShell.apiKey);
});

test('DELETE /me/api-key should fetch the api key', async () => {
test('DELETE /me/api-key should delete the api key', async () => {
const response = await authOwnerShellAgent.delete('/me/api-key');

expect(response.statusCode).toBe(200);
Expand Down Expand Up @@ -327,14 +327,14 @@ describe('Member', () => {
expect(storedMember.apiKey).toEqual(response.body.data.apiKey);
});

test('GET /me/api-key should fetch the api key', async () => {
test('GET /me/api-key should fetch the api key redacted', async () => {
const response = await testServer.authAgentFor(member).get('/me/api-key');

expect(response.statusCode).toBe(200);
expect(response.body.data.apiKey).toEqual(member.apiKey);
expect(response.body.data.apiKey).not.toEqual(member.apiKey);
});

test('DELETE /me/api-key should fetch the api key', async () => {
test('DELETE /me/api-key should delete the api key', async () => {
const response = await testServer.authAgentFor(member).delete('/me/api-key');

expect(response.statusCode).toBe(200);
Expand Down
8 changes: 4 additions & 4 deletions packages/cli/test/unit/controllers/me.controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import jwt from 'jsonwebtoken';
import { mock, anyObject } from 'jest-mock-extended';
import type { PublicUser } from '@/Interfaces';
import type { User } from '@db/entities/User';
import { MeController } from '@/controllers/me.controller';
import { API_KEY_PREFIX, MeController } from '@/controllers/me.controller';
import { AUTH_COOKIE_NAME } from '@/constants';
import type { AuthenticatedRequest, MeRequest } from '@/requests';
import { UserService } from '@/services/user.service';
Expand Down Expand Up @@ -223,7 +223,7 @@ describe('MeController', () => {
describe('API Key methods', () => {
let req: AuthenticatedRequest;
beforeAll(() => {
req = mock({ user: mock<Partial<User>>({ id: '123', apiKey: 'test-key' }) });
req = mock({ user: mock<Partial<User>>({ id: '123', apiKey: `${API_KEY_PREFIX}test-key` }) });
});

describe('createAPIKey', () => {
Expand All @@ -234,9 +234,9 @@ describe('MeController', () => {
});

describe('getAPIKey', () => {
it('should return the users api key', async () => {
it('should return the users api key redacted', async () => {
const { apiKey } = await controller.getAPIKey(req);
expect(apiKey).toEqual(req.user.apiKey);
expect(apiKey).not.toEqual(req.user.apiKey);
});
});

Expand Down
4 changes: 2 additions & 2 deletions packages/cli/test/unit/services/orchestration.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ describe('Orchestration Service', () => {

describe('shouldAddWebhooks', () => {
beforeEach(() => {
config.set('multiMainSetup.instanceType', 'leader');
config.set('instanceRole', 'leader');
});
test('should return true for init', () => {
// We want to ensure that webhooks are populated on init
Expand All @@ -169,7 +169,7 @@ describe('Orchestration Service', () => {
});

test('should return false for update or activate when not leader', () => {
config.set('multiMainSetup.instanceType', 'follower');
config.set('instanceRole', 'follower');
const modes = ['update', 'activate'] as WorkflowActivateMode[];
for (const mode of modes) {
const result = os.shouldAddWebhooks(mode);
Expand Down
11 changes: 10 additions & 1 deletion packages/editor-ui/src/components/CopyInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
[$style.copyText]: true,
[$style[size]]: true,
[$style.collapsed]: collapse,
[$style.noHover]: disableCopy,
'ph-no-capture': redactValue,
}"
data-test-id="copy-input"
@click="copy"
>
<span ref="copyInputValue">{{ value }}</span>
<div :class="$style.copyButton">
<div v-if="!disableCopy" :class="$style.copyButton">
<span>{{ copyButtonText }}</span>
</div>
</div>
Expand All @@ -36,6 +37,7 @@ type Props = {
size?: 'medium' | 'large';
collapse?: boolean;
redactValue?: boolean;
disableCopy: boolean;
};
const props = withDefaults(defineProps<Props>(), {
Expand All @@ -46,6 +48,7 @@ const props = withDefaults(defineProps<Props>(), {
size: 'medium',
copyButtonText: useI18n().baseText('generic.copy'),
toastTitle: useI18n().baseText('generic.copiedToClipboard'),
disableCopy: false,
});
const emit = defineEmits<{
copy: [];
Expand All @@ -55,6 +58,8 @@ const clipboard = useClipboard();
const { showMessage } = useToast();
function copy() {
if (props.disableCopy) return;
emit('copy');
void clipboard.copy(props.value ?? '');
Expand Down Expand Up @@ -88,6 +93,10 @@ function copy() {
}
}
.noHover {
cursor: default;
}
.large {
span {
font-size: var(--font-size-s);
Expand Down
3 changes: 2 additions & 1 deletion packages/editor-ui/src/plugins/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@
"credentialEdit.credentialSharing.role.user": "User",
"credentialEdit.docs.aws": "Configure this credential:\n\n- Select your AWS **Region**.\n- Log in to your <a href=\"https://aws.amazon.com/\" target=\"_blank\">AWS</a> account.\n- Generate your access key pair:\n - Open the AWS <a href=\"https://console.aws.amazon.com/iam\" target=\"_blank\">IAM console</a> and open your user menu.\n - Select **Security credentials**.\n - Create a new access key pair in the **Access Keys** section.\n - Reveal the **Access Key ID** and **Secret Access Key** and enter them in n8n.\n- To use a **temporary security credential**, turn this option on and add a **Session token**.\n- If you use Amazon Virtual Private Cloud <a href=\"https://aws.amazon.com/vpc/\" target=\"_blank\">VPC</a> to host n8n, you can establish a connection between your VPC and some apps. Use **Custom Endpoints** to enter relevant custom endpoint(s) for this connection.\n\nClick the docs link above for more detailed instructions.",
"credentialEdit.docs.gmailOAuth2": "Configure this credential:\n\n- Log in to your <a href=\"https://cloud.google.com/\" target=\"_blank\">Google Cloud</a> account.\n- Go to <a href=\"https://console.cloud.google.com/apis/credentials\" target=\"_blank\">Google Cloud Console / APIs and services</a> and choose the project you want to use from the select at the top left (or create a new one and select it).\n- If you haven't used OAuth in this Google Cloud project before, <a href=\"https://developers.google.com/workspace/guides/configure-oauth-consent\" target=\"_blank\">configure the OAuth consent screen</a>.\n- In Credentials, select **+ CREATE CREDENTIALS > OAuth client ID**.\n- In the **Application type** dropdown, select **Web application**.\n- Under **Authorized redirect URIs**, select **+ ADD URI**. Paste in the OAuth redirect URL from n8n.\n- Select **Create**.\n- In Enabled APIs and services, select **+ ENABLE APIS AND SERVICES**.\n- Select and enable the Gmail API.\n- Back to Credentials, click on the credential in OAuth 2.0 Client IDs, and on the credential page, you will find the Client ID and Client Secret.\n\nClick the docs link above for more detailed instructions.",
"credentialEdit.docs.openAiApi": "Configure this credential:\n\n- Log in to your <a href=\"https://openai.com/\" target=\"_blank\">OpenAI</a> account.\n- Open your OpenAI <a href=\"https://platform.openai.com/api-keys\" target=\"_blank\">API keys</a> page to create an **API key**.\n- Enter an **Organization ID** if you belong to multiple organizations; otherwise, leave blank. Open your OpenAI <a href=\"https://platform.openai.com/account/organization\" target=\"_blank\">Organization Settings</a> page to get your Organization ID.\n\nClick the docs link above for more detailed instructions.",
"credentialEdit.docs.openAiApi": "Configure this credential:\n\n- Log in to your <a href=\"https://platform.openai.com/login\" target=\"_blank\">OpenAI</a> account.\n- Open your OpenAI <a href=\"https://platform.openai.com/api-keys\" target=\"_blank\">API keys</a> page to create an **API key**.\n- Enter an **Organization ID** if you belong to multiple organizations; otherwise, leave blank. Open your OpenAI <a href=\"https://platform.openai.com/account/organization\" target=\"_blank\">Organization Settings</a> page to get your Organization ID.\n\nClick the docs link above for more detailed instructions.",
"credentialSelectModal.addNewCredential": "Add new credential",
"credentialSelectModal.continue": "Continue",
"credentialSelectModal.searchForApp": "Search for app...",
Expand Down Expand Up @@ -1724,6 +1724,7 @@
"settings.api.view.copy.toast": "API Key copied to clipboard",
"settings.api.view.apiPlayground": "API Playground",
"settings.api.view.info": "Use your API Key to control n8n programmatically using the {apiAction}. But if you only want to trigger workflows, consider using the {webhookAction} instead.",
"settings.api.view.copy": "Make sure to copy your API key now as you will not be able to see this again.",
"settings.api.view.info.api": "n8n API",
"settings.api.view.info.webhook": "webhook node",
"settings.api.view.myKey": "My API Key",
Expand Down
7 changes: 6 additions & 1 deletion packages/editor-ui/src/views/SettingsApiView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,13 @@
:copy-button-text="$locale.baseText('generic.clickToCopy')"
:toast-title="$locale.baseText('settings.api.view.copy.toast')"
:redact-value="true"
:disable-copy="isRedactedApiKey"
:hint="!isRedactedApiKey ? $locale.baseText('settings.api.view.copy') : ''"
@copy="onCopy"
/>
</div>
</n8n-card>
<div :class="$style.hint">
<div v-if="!isRedactedApiKey" :class="$style.hint">
<n8n-text size="small">
{{
$locale.baseText(`settings.api.view.${swaggerUIEnabled ? 'tryapi' : 'more-details'}`)
Expand Down Expand Up @@ -146,6 +148,9 @@ export default defineComponent({
isPublicApiEnabled(): boolean {
return this.settingsStore.isPublicApiEnabled;
},
isRedactedApiKey(): boolean {
return this.apiKey.includes('*');
},
},
methods: {
onUpgrade() {
Expand Down
Loading

0 comments on commit 48210a1

Please sign in to comment.