mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 17:46:45 +00:00
ci: Include THIRD_PARTY_LICENSES.md file with release (#18739)
This commit is contained in:
5
packages/cli/THIRD_PARTY_LICENSES.md
Normal file
5
packages/cli/THIRD_PARTY_LICENSES.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Third-Party Licenses
|
||||
|
||||
**Note**: This is a placeholder file used during local development. The complete third-party licenses file is generated during production builds and contains detailed licensing information for all dependencies.
|
||||
|
||||
For the complete list of third-party licenses, please refer to the production build.
|
||||
@@ -0,0 +1,25 @@
|
||||
import { CLI_DIR } from '@/constants';
|
||||
import { Get, RestController } from '@n8n/decorators';
|
||||
import { Request, Response } from 'express';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { resolve } from 'path';
|
||||
|
||||
@RestController('/third-party-licenses')
|
||||
export class ThirdPartyLicensesController {
|
||||
/**
|
||||
* Get third-party licenses content
|
||||
* Requires authentication to access
|
||||
*/
|
||||
@Get('/')
|
||||
async getThirdPartyLicenses(_: Request, res: Response) {
|
||||
const licenseFile = resolve(CLI_DIR, 'THIRD_PARTY_LICENSES.md');
|
||||
|
||||
try {
|
||||
const content = await readFile(licenseFile, 'utf-8');
|
||||
res.setHeader('Content-Type', 'text/markdown; charset=utf-8');
|
||||
res.send(content);
|
||||
} catch {
|
||||
res.status(404).send('Third-party licenses file not found');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,6 +91,7 @@ export class Server extends AbstractServer {
|
||||
const { FrontendService } = await import('@/services/frontend.service');
|
||||
this.frontendService = Container.get(FrontendService);
|
||||
await import('@/controllers/module-settings.controller');
|
||||
await import('@/controllers/third-party-licenses.controller');
|
||||
}
|
||||
|
||||
this.presetCredentialsLoaded = false;
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { createMember, createOwner } from '../shared/db/users';
|
||||
import type { SuperAgentTest } from '../shared/types';
|
||||
import { setupTestServer } from '../shared/utils';
|
||||
|
||||
jest.mock('fs/promises', () => ({
|
||||
readFile: jest.fn(),
|
||||
}));
|
||||
|
||||
import { readFile } from 'fs/promises';
|
||||
const mockReadFile = readFile as jest.MockedFunction<typeof readFile>;
|
||||
|
||||
describe('ThirdPartyLicensesController', () => {
|
||||
const testServer = setupTestServer({ endpointGroups: ['third-party-licenses'] });
|
||||
let ownerAgent: SuperAgentTest;
|
||||
let memberAgent: SuperAgentTest;
|
||||
|
||||
beforeAll(async () => {
|
||||
const owner = await createOwner();
|
||||
const member = await createMember();
|
||||
ownerAgent = testServer.authAgentFor(owner);
|
||||
memberAgent = testServer.authAgentFor(member);
|
||||
});
|
||||
|
||||
describe('GET /third-party-licenses', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
await testServer.authlessAgent.get('/third-party-licenses').expect(401);
|
||||
});
|
||||
|
||||
describe('when license file exists', () => {
|
||||
beforeEach(() => {
|
||||
mockReadFile.mockResolvedValue('# Third Party Licenses\n\nSome license content...');
|
||||
});
|
||||
|
||||
it('should allow authenticated owner to get third-party licenses', async () => {
|
||||
const response = await ownerAgent.get('/third-party-licenses');
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers['content-type']).toMatch(/text\/markdown/);
|
||||
expect(response.text).toBe('# Third Party Licenses\n\nSome license content...');
|
||||
});
|
||||
|
||||
it('should allow authenticated member to get third-party licenses', async () => {
|
||||
const response = await memberAgent.get('/third-party-licenses');
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers['content-type']).toMatch(/text\/markdown/);
|
||||
expect(response.text).toBe('# Third Party Licenses\n\nSome license content...');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when license file does not exist', () => {
|
||||
beforeEach(() => {
|
||||
mockReadFile.mockRejectedValue(new Error('ENOENT: no such file or directory'));
|
||||
});
|
||||
|
||||
it('should return 404 for authenticated owner', async () => {
|
||||
const response = await ownerAgent.get('/third-party-licenses');
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.text).toBe('Third-party licenses file not found');
|
||||
});
|
||||
|
||||
it('should return 404 for authenticated member', async () => {
|
||||
const response = await memberAgent.get('/third-party-licenses');
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.text).toBe('Third-party licenses file not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when file read fails with other errors', () => {
|
||||
beforeEach(() => {
|
||||
mockReadFile.mockRejectedValue(new Error('EACCES: permission denied'));
|
||||
});
|
||||
|
||||
it('should return 404 for permission errors', async () => {
|
||||
const response = await ownerAgent.get('/third-party-licenses');
|
||||
expect(response.status).toBe(404);
|
||||
expect(response.text).toBe('Third-party licenses file not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('file path resolution', () => {
|
||||
it('should request the correct file path', async () => {
|
||||
mockReadFile.mockResolvedValue('test content');
|
||||
|
||||
await ownerAgent.get('/third-party-licenses');
|
||||
|
||||
expect(mockReadFile).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/THIRD_PARTY_LICENSES\.md$/),
|
||||
'utf-8',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -43,8 +43,10 @@ type EndpointGroup =
|
||||
| 'ai'
|
||||
| 'folder'
|
||||
| 'insights'
|
||||
| 'data-store'
|
||||
| 'module-settings'
|
||||
| 'data-table'
|
||||
| 'module-settings';
|
||||
| 'third-party-licenses';
|
||||
|
||||
type ModuleName = 'insights' | 'external-secrets' | 'community-packages' | 'data-table';
|
||||
|
||||
|
||||
@@ -313,6 +313,10 @@ export const setupTestServer = ({
|
||||
case 'module-settings':
|
||||
await import('@/controllers/module-settings.controller');
|
||||
break;
|
||||
|
||||
case 'third-party-licenses':
|
||||
await import('@/controllers/third-party-licenses.controller');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -123,6 +123,9 @@
|
||||
"about.debug.message": "Copy debug information",
|
||||
"about.debug.toast.title": "Debug info",
|
||||
"about.debug.toast.message": "Copied debug info to clipboard",
|
||||
"about.thirdPartyLicenses": "Third-Party Licenses",
|
||||
"about.thirdPartyLicensesLink": "View all third-party licenses",
|
||||
"about.thirdPartyLicenses.downloadError": "Failed to download third-party licenses file",
|
||||
"askAi.dialog.title": "'Ask AI' is almost ready",
|
||||
"askAi.dialog.body": "We’re still applying the finishing touches. Soon, you will be able to <strong>automatically generate code from simple text prompts</strong>. Join the waitlist to get early access to this feature.",
|
||||
"askAi.dialog.signup": "Join Waitlist",
|
||||
|
||||
@@ -16,6 +16,7 @@ export * from './module-settings';
|
||||
export * from './sso';
|
||||
export type * from './tags';
|
||||
export * from './templates';
|
||||
export * from './third-party-licenses';
|
||||
export * from './ui';
|
||||
export * from './users';
|
||||
export * from './versions';
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { request } from '../utils';
|
||||
|
||||
export async function getThirdPartyLicenses(context: IRestApiContext): Promise<string> {
|
||||
return await request({
|
||||
method: 'GET',
|
||||
baseURL: context.baseUrl,
|
||||
endpoint: '/third-party-licenses',
|
||||
});
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { useToast } from '@/composables/useToast';
|
||||
import { useClipboard } from '@/composables/useClipboard';
|
||||
import { useDebugInfo } from '@/composables/useDebugInfo';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { getThirdPartyLicenses } from '@n8n/rest-api-client';
|
||||
|
||||
const modalBus = createEventBus();
|
||||
const toast = useToast();
|
||||
@@ -19,6 +20,23 @@ const closeDialog = () => {
|
||||
modalBus.emit('close');
|
||||
};
|
||||
|
||||
const downloadThirdPartyLicenses = async () => {
|
||||
try {
|
||||
const thirdPartyLicenses = await getThirdPartyLicenses(rootStore.restApiContext);
|
||||
|
||||
const blob = new File([thirdPartyLicenses], 'THIRD_PARTY_LICENSES.md', {
|
||||
type: 'text/markdown',
|
||||
});
|
||||
window.open(URL.createObjectURL(blob));
|
||||
} catch (error) {
|
||||
toast.showToast({
|
||||
title: i18n.baseText('about.thirdPartyLicenses.downloadError'),
|
||||
message: error.message,
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const copyDebugInfoToClipboard = async () => {
|
||||
toast.showToast({
|
||||
title: i18n.baseText('about.debug.toast.title'),
|
||||
@@ -66,6 +84,16 @@ const copyDebugInfoToClipboard = async () => {
|
||||
</n8n-link>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="8" class="info-name">
|
||||
<n8n-text>{{ i18n.baseText('about.thirdPartyLicenses') }}</n8n-text>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<n8n-link @click="downloadThirdPartyLicenses">
|
||||
{{ i18n.baseText('about.thirdPartyLicensesLink') }}
|
||||
</n8n-link>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="8" class="info-name">
|
||||
<n8n-text>{{ i18n.baseText('about.instanceID') }}</n8n-text>
|
||||
|
||||
Reference in New Issue
Block a user