Skip to content

Commit

Permalink
feat(editor): Community+ enrollment (#10776)
Browse files Browse the repository at this point in the history
  • Loading branch information
cstuncsik authored Oct 7, 2024
1 parent 42c0733 commit 92cf860
Show file tree
Hide file tree
Showing 20 changed files with 585 additions and 22 deletions.
1 change: 1 addition & 0 deletions packages/@n8n/api-types/src/dto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { PasswordUpdateRequestDto } from './user/password-update-request.dto';
export { RoleChangeRequestDto } from './user/role-change-request.dto';
export { SettingsUpdateRequestDto } from './user/settings-update-request.dto';
export { UserUpdateRequestDto } from './user/user-update-request.dto';
export { CommunityRegisteredRequestDto } from './license/community-registered-request.dto';
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { CommunityRegisteredRequestDto } from '../community-registered-request.dto';

describe('CommunityRegisteredRequestDto', () => {
it('should fail validation for missing email', () => {
const invalidRequest = {};

const result = CommunityRegisteredRequestDto.safeParse(invalidRequest);

expect(result.success).toBe(false);
expect(result.error?.issues[0]).toEqual(
expect.objectContaining({ message: 'Required', path: ['email'] }),
);
});

it('should fail validation for an invalid email', () => {
const invalidRequest = {
email: 'invalid-email',
};

const result = CommunityRegisteredRequestDto.safeParse(invalidRequest);

expect(result.success).toBe(false);
expect(result.error?.issues[0]).toEqual(
expect.objectContaining({ message: 'Invalid email', path: ['email'] }),
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { z } from 'zod';
import { Z } from 'zod-class';

export class CommunityRegisteredRequestDto extends Z.class({ email: z.string().email() }) {}
5 changes: 5 additions & 0 deletions packages/cli/src/events/maps/relay.event-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,11 @@ export type RelayEventMap = {
success: boolean;
};

'license-community-plus-registered': {
email: string;
licenseKey: string;
};

// #endregion

// #region Variable
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/events/relays/telemetry.event-relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export class TelemetryEventRelay extends EventRelay {
'source-control-user-finished-push-ui': (event) =>
this.sourceControlUserFinishedPushUi(event),
'license-renewal-attempted': (event) => this.licenseRenewalAttempted(event),
'license-community-plus-registered': (event) => this.licenseCommunityPlusRegistered(event),
'variable-created': () => this.variableCreated(),
'external-secrets-provider-settings-saved': (event) =>
this.externalSecretsProviderSettingsSaved(event),
Expand Down Expand Up @@ -234,6 +235,16 @@ export class TelemetryEventRelay extends EventRelay {
});
}

private licenseCommunityPlusRegistered({
email,
licenseKey,
}: RelayEventMap['license-community-plus-registered']) {
this.telemetry.track('User registered for license community plus', {
email,
licenseKey,
});
}

// #endregion

// #region Variable
Expand Down
36 changes: 36 additions & 0 deletions packages/cli/src/license/__tests__/license.service.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { TEntitlement } from '@n8n_io/license-sdk';
import axios, { AxiosError } from 'axios';
import { mock } from 'jest-mock-extended';

import type { WorkflowRepository } from '@/databases/repositories/workflow.repository';
Expand All @@ -7,6 +8,8 @@ import type { EventService } from '@/events/event.service';
import type { License } from '@/license';
import { LicenseErrors, LicenseService } from '@/license/license.service';

jest.mock('axios');

describe('LicenseService', () => {
const license = mock<License>();
const workflowRepository = mock<WorkflowRepository>();
Expand Down Expand Up @@ -84,4 +87,37 @@ describe('LicenseService', () => {
});
});
});

describe('registerCommunityEdition', () => {
test('on success', async () => {
jest
.spyOn(axios, 'post')
.mockResolvedValueOnce({ data: { title: 'Title', text: 'Text', licenseKey: 'abc-123' } });
const data = await licenseService.registerCommunityEdition({
email: '[email protected]',
instanceId: '123',
instanceUrl: 'http://localhost',
licenseType: 'community-registered',
});

expect(data).toEqual({ title: 'Title', text: 'Text' });
expect(eventService.emit).toHaveBeenCalledWith('license-community-plus-registered', {
email: '[email protected]',
licenseKey: 'abc-123',
});
});

test('on failure', async () => {
jest.spyOn(axios, 'post').mockRejectedValueOnce(new AxiosError('Failed'));
await expect(
licenseService.registerCommunityEdition({
email: '[email protected]',
instanceId: '123',
instanceUrl: 'http://localhost',
licenseType: 'community-registered',
}),
).rejects.toThrowError('Failed');
expect(eventService.emit).not.toHaveBeenCalled();
});
});
});
27 changes: 24 additions & 3 deletions packages/cli/src/license/license.controller.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { CommunityRegisteredRequestDto } from '@n8n/api-types';
import type { AxiosError } from 'axios';
import { InstanceSettings } from 'n8n-core';

import { Get, Post, RestController, GlobalScope } from '@/decorators';
import { Get, Post, RestController, GlobalScope, Body } from '@/decorators';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { AuthenticatedRequest, LicenseRequest } from '@/requests';
import { AuthenticatedRequest, AuthlessRequest, LicenseRequest } from '@/requests';
import { UrlService } from '@/services/url.service';

import { LicenseService } from './license.service';

@RestController('/license')
export class LicenseController {
constructor(private readonly licenseService: LicenseService) {}
constructor(
private readonly licenseService: LicenseService,
private readonly instanceSettings: InstanceSettings,
private readonly urlService: UrlService,
) {}

@Get('/')
async getLicenseData() {
Expand All @@ -32,6 +39,20 @@ export class LicenseController {
}
}

@Post('/enterprise/community-registered')
async registerCommunityEdition(
_req: AuthlessRequest,
_res: Response,
@Body payload: CommunityRegisteredRequestDto,
) {
return await this.licenseService.registerCommunityEdition({
email: payload.email,
instanceId: this.instanceSettings.instanceId,
instanceUrl: this.urlService.getInstanceBaseUrl(),
licenseType: 'community-registered',
});
}

@Post('/activate')
@GlobalScope('license:manage')
async activateLicense(req: LicenseRequest.Activate) {
Expand Down
40 changes: 39 additions & 1 deletion packages/cli/src/license/license.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import axios from 'axios';
import axios, { AxiosError } from 'axios';
import { ensureError } from 'n8n-workflow';
import { Service } from 'typedi';

import type { User } from '@/databases/entities/user';
Expand Down Expand Up @@ -60,6 +61,43 @@ export class LicenseService {
});
}

async registerCommunityEdition({
email,
instanceId,
instanceUrl,
licenseType,
}: {
email: string;
instanceId: string;
instanceUrl: string;
licenseType: string;
}): Promise<{ title: string; text: string }> {
try {
const {
data: { licenseKey, ...rest },
} = await axios.post<{ title: string; text: string; licenseKey: string }>(
'https://enterprise.n8n.io/community-registered',
{
email,
instanceId,
instanceUrl,
licenseType,
},
);
this.eventService.emit('license-community-plus-registered', { email, licenseKey });
return rest;
} catch (e: unknown) {
if (e instanceof AxiosError) {
const error = e as AxiosError<{ message: string }>;
const errorMsg = error.response?.data?.message ?? e.message;
throw new BadRequestError('Failed to register community edition: ' + errorMsg);
} else {
this.logger.error('Failed to register community edition', { error: ensureError(e) });
throw new BadRequestError('Failed to register community edition');
}
}
}

getManagementJwt(): string {
return this.license.getManagementJwt();
}
Expand Down
13 changes: 13 additions & 0 deletions packages/editor-ui/src/api/usage.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CommunityRegisteredRequestDto } from '@n8n/api-types';
import { makeRestApiRequest } from '@/utils/apiUtils';
import type { IRestApiContext, UsageState } from '@/Interface';

Expand All @@ -21,3 +22,15 @@ export const requestLicenseTrial = async (
): Promise<UsageState['data']> => {
return await makeRestApiRequest(context, 'POST', '/license/enterprise/request_trial');
};

export const registerCommunityEdition = async (
context: IRestApiContext,
params: CommunityRegisteredRequestDto,
): Promise<{ title: string; text: string }> => {
return await makeRestApiRequest(
context,
'POST',
'/license/enterprise/community-registered',
params,
);
};
Loading

0 comments on commit 92cf860

Please sign in to comment.