mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
Merge pull request #3021 from n8n-io/n8n-3173-add-get-users-endpoint
⚡ Add GET /users route - public api
This commit is contained in:
commit
8829684d5b
|
@ -4,7 +4,7 @@ module.exports = {
|
||||||
'^.+\\.ts?$': 'ts-jest',
|
'^.+\\.ts?$': 'ts-jest',
|
||||||
},
|
},
|
||||||
testURL: 'http://localhost/',
|
testURL: 'http://localhost/',
|
||||||
testRegex: '(/__tests__/.*|(\\.|/)(test))\\.ts$',
|
testRegex: '(/__tests__/.*|(\\.|/)(test-api))\\.ts$',
|
||||||
testPathIgnorePatterns: ['/dist/', '/node_modules/'],
|
testPathIgnorePatterns: ['/dist/', '/node_modules/'],
|
||||||
moduleFileExtensions: ['ts', 'js', 'json'],
|
moduleFileExtensions: ['ts', 'js', 'json'],
|
||||||
globals: {
|
globals: {
|
||||||
|
|
47
packages/cli/src/PublicApi/helpers.ts
Normal file
47
packages/cli/src/PublicApi/helpers.ts
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import * as querystring from 'querystring';
|
||||||
|
|
||||||
|
interface IPaginationOffsetDecoded {
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const decodeCursor = (cursor: string): IPaginationOffsetDecoded => {
|
||||||
|
const data = JSON.parse(Buffer.from(cursor, 'base64').toString()) as string;
|
||||||
|
const unserializedData = querystring.decode(data) as { offset: string; limit: string };
|
||||||
|
return {
|
||||||
|
offset: parseInt(unserializedData.offset, 10),
|
||||||
|
limit: parseInt(unserializedData.limit, 10),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getNextCursor = (
|
||||||
|
offset: number,
|
||||||
|
limit: number,
|
||||||
|
numberOfRecords: number,
|
||||||
|
): string | null => {
|
||||||
|
const retrieveRecordsLength = offset + limit;
|
||||||
|
|
||||||
|
if (retrieveRecordsLength < numberOfRecords) {
|
||||||
|
return Buffer.from(
|
||||||
|
JSON.stringify(
|
||||||
|
querystring.encode({
|
||||||
|
limit,
|
||||||
|
offset: offset + limit,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
).toString('base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSelectableProperties = (table: 'user' | 'role'): string[] => {
|
||||||
|
return {
|
||||||
|
user: ['id', 'email', 'firstName', 'lastName', 'createdAt', 'updatedAt'],
|
||||||
|
role: ['id', 'name', 'scope', 'createdAt', 'updatedAt'],
|
||||||
|
}[table];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const connectionName = (): string => {
|
||||||
|
return 'default';
|
||||||
|
};
|
|
@ -1,7 +1,4 @@
|
||||||
|
import { Application, Response } from 'express';
|
||||||
import {
|
|
||||||
Application,
|
|
||||||
} from 'express';
|
|
||||||
|
|
||||||
import * as OpenApiValidator from 'express-openapi-validator';
|
import * as OpenApiValidator from 'express-openapi-validator';
|
||||||
|
|
||||||
|
@ -9,57 +6,70 @@ import path = require('path');
|
||||||
|
|
||||||
import express = require('express');
|
import express = require('express');
|
||||||
|
|
||||||
import { OpenAPIV3 } from 'express-openapi-validator/dist/framework/types';
|
import { HttpError, OpenAPIV3 } from 'express-openapi-validator/dist/framework/types';
|
||||||
|
// eslint-disable-next-line import/no-cycle
|
||||||
import { Db } from '../..';
|
import { Db } from '../..';
|
||||||
|
import config = require('../../../config');
|
||||||
|
|
||||||
export interface N8nApp {
|
export interface N8nApp {
|
||||||
app: Application;
|
app: Application;
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicApiController = express.Router();
|
export const publicApiController = express.Router();
|
||||||
|
|
||||||
export const getRoutes = (): express.Router => {
|
publicApiController.use(
|
||||||
|
`/v1`,
|
||||||
|
OpenApiValidator.middleware({
|
||||||
|
apiSpec: path.join(__dirname, 'openapi.yml'),
|
||||||
|
operationHandlers: path.join(__dirname),
|
||||||
|
validateRequests: true,
|
||||||
|
validateApiSpec: true,
|
||||||
|
validateSecurity: {
|
||||||
|
handlers: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
ApiKeyAuth: async (req, scopes, schema: OpenAPIV3.ApiKeySecurityScheme) => {
|
||||||
|
const apiKey = req.headers[schema.name.toLowerCase()];
|
||||||
|
|
||||||
publicApiController.use(`/v1`,
|
const user = await Db.collections.User?.find({
|
||||||
OpenApiValidator.middleware({
|
where: {
|
||||||
apiSpec: path.join(__dirname, 'openapi.yml'),
|
apiKey,
|
||||||
operationHandlers: path.join(__dirname),
|
},
|
||||||
validateRequests: true, // (default)
|
relations: ['globalRole'],
|
||||||
validateApiSpec: true,
|
});
|
||||||
validateSecurity: {
|
|
||||||
handlers: {
|
|
||||||
ApiKeyAuth: async (req, scopes, schema: OpenAPIV3.ApiKeySecurityScheme) => {
|
|
||||||
|
|
||||||
const apiKey = req.headers[schema.name.toLowerCase()];
|
if (!user?.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const user = await Db.collections.User!.find({
|
if (!config.get('userManagement.isInstanceOwnerSetUp')) {
|
||||||
where: {
|
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
||||||
apiKey,
|
throw {
|
||||||
},
|
message: 'asasasas',
|
||||||
relations: ['globalRole'],
|
status: 400,
|
||||||
});
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!user.length) {
|
if (user[0].globalRole.name !== 'owner') {
|
||||||
return false;
|
// eslint-disable-next-line @typescript-eslint/no-throw-literal
|
||||||
}
|
throw {
|
||||||
|
status: 403,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
req.user = user[0];
|
[req.user] = user;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
},
|
||||||
|
}),
|
||||||
//add error handler
|
);
|
||||||
//@ts-ignore
|
|
||||||
publicApiController.use((err, req, res, next) => {
|
|
||||||
res.status(err.status || 500).json({
|
|
||||||
message: err.message,
|
|
||||||
errors: err.errors,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// add error handler
|
||||||
|
// @ts-ignore
|
||||||
|
publicApiController.use((error: HttpError, req, res: Response) => {
|
||||||
|
res.status(error.status || 500).json({
|
||||||
|
message: error.message,
|
||||||
|
errors: error.errors,
|
||||||
});
|
});
|
||||||
|
});
|
||||||
return publicApiController;
|
|
||||||
};
|
|
||||||
|
|
|
@ -59,6 +59,14 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
example: MTIzZTQ1NjctZTg5Yi0xMmQzLWE0NTYtNDI2NjE0MTc0MDA
|
example: MTIzZTQ1NjctZTg5Yi0xMmQzLWE0NTYtNDI2NjE0MTc0MDA
|
||||||
|
- name: includeRole
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
style: form
|
||||||
|
explode: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: true
|
||||||
responses:
|
responses:
|
||||||
"200":
|
"200":
|
||||||
description: Successful operation
|
description: Successful operation
|
||||||
|
|
|
@ -1,18 +1,53 @@
|
||||||
import express = require('express');
|
import express = require('express');
|
||||||
|
import { getConnection } from 'typeorm';
|
||||||
|
|
||||||
import { UserRequest } from '../../../../requests';
|
import { UserRequest } from '../../../../requests';
|
||||||
|
|
||||||
|
import { User } from '../../../../databases/entities/User';
|
||||||
|
import {
|
||||||
|
connectionName,
|
||||||
|
decodeCursor,
|
||||||
|
getNextCursor,
|
||||||
|
getSelectableProperties,
|
||||||
|
} from '../../../helpers';
|
||||||
|
|
||||||
export = {
|
export = {
|
||||||
createUsers: async (req: UserRequest.Invite, res: express.Response) => {
|
createUsers: async (req: UserRequest.Invite, res: express.Response): Promise<void> => {
|
||||||
res.json({ success: true});
|
|
||||||
},
|
|
||||||
deleteUser: async (req: UserRequest.Delete, res: express.Response) => {
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
},
|
},
|
||||||
getUser: async (req: UserRequest.Get, res: express.Response) => {
|
deleteUser: async (req: UserRequest.Delete, res: express.Response): Promise<void> => {
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
},
|
},
|
||||||
getUsers: async (req: UserRequest.Get, res: express.Response) => {
|
getUser: async (req: UserRequest.Get, res: express.Response): Promise<void> => {
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
},
|
},
|
||||||
};
|
getUsers: async (req: UserRequest.Get, res: express.Response): Promise<void> => {
|
||||||
|
let offset = 0;
|
||||||
|
let limit = parseInt(req.query.limit, 10) || 10;
|
||||||
|
const includeRole = req.query?.includeRole?.toLowerCase() === 'true' || false;
|
||||||
|
|
||||||
|
if (req.query.cursor) {
|
||||||
|
const { cursor } = req.query;
|
||||||
|
({ offset, limit } = decodeCursor(cursor));
|
||||||
|
}
|
||||||
|
|
||||||
|
const query = getConnection(connectionName())
|
||||||
|
.getRepository(User)
|
||||||
|
.createQueryBuilder()
|
||||||
|
.leftJoinAndSelect('User.globalRole', 'Role')
|
||||||
|
.select(getSelectableProperties('user')?.map((property) => `User.${property}`))
|
||||||
|
.limit(limit)
|
||||||
|
.offset(offset);
|
||||||
|
|
||||||
|
if (includeRole) {
|
||||||
|
query.addSelect(getSelectableProperties('role')?.map((property) => `Role.${property}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
const [users, count] = await query.getManyAndCount();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
users,
|
||||||
|
nextCursor: getNextCursor(offset, limit, count),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -172,7 +172,7 @@ import { SharedWorkflow } from './databases/entities/SharedWorkflow';
|
||||||
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from './constants';
|
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from './constants';
|
||||||
import { credentialsController } from './api/credentials.api';
|
import { credentialsController } from './api/credentials.api';
|
||||||
import { getInstanceBaseUrl, isEmailSetUp } from './UserManagement/UserManagementHelper';
|
import { getInstanceBaseUrl, isEmailSetUp } from './UserManagement/UserManagementHelper';
|
||||||
import * as publicApiv1Routes from './PublicApi/v1';
|
import { publicApiController } from './PublicApi/v1';
|
||||||
|
|
||||||
require('body-parser-xml')(bodyParser);
|
require('body-parser-xml')(bodyParser);
|
||||||
|
|
||||||
|
@ -564,25 +564,23 @@ class App {
|
||||||
// Public API
|
// Public API
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
|
||||||
|
// test routes to create/regenerate/delete token
|
||||||
//test routes to create/regenerate/delete token
|
// NOTE: Only works with admin role
|
||||||
//NOTE: Only works with admin role
|
// This should be within the user's management user scope
|
||||||
//This should be within the user's management user scope
|
|
||||||
this.app.post('/token', async (req: express.Request, res: express.Response) => {
|
this.app.post('/token', async (req: express.Request, res: express.Response) => {
|
||||||
const ramdonToken = randomBytes(20).toString('hex');
|
const ramdonToken = randomBytes(20).toString('hex');
|
||||||
//@ts-ignore
|
// @ts-ignore
|
||||||
await Db.collections.User!.update({ globalRole: 1 }, { apiKey: ramdonToken });
|
await Db.collections.User!.update({ globalRole: 1 }, { apiKey: ramdonToken });
|
||||||
return ResponseHelper.sendSuccessResponse(res, { token: ramdonToken }, true, 200);
|
return ResponseHelper.sendSuccessResponse(res, { token: ramdonToken }, true, 200);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.app.delete('/token', async (req: express.Request, res: express.Response) => {
|
this.app.delete('/token', async (req: express.Request, res: express.Response) => {
|
||||||
//@ts-ignore
|
// @ts-ignore
|
||||||
await Db.collections.User!.update({ globalRole: 1 }, { apiKey: null });
|
await Db.collections.User!.update({ globalRole: 1 }, { apiKey: null });
|
||||||
return ResponseHelper.sendSuccessResponse(res, {}, true, 204);
|
return ResponseHelper.sendSuccessResponse(res, {}, true, 204);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.app.use(`/${this.publicApiEndpoint}/`, publicApiController);
|
||||||
this.app.use(`/${this.publicApiEndpoint}`, publicApiv1Routes.getRoutes());
|
|
||||||
|
|
||||||
// Parse cookies for easier access
|
// Parse cookies for easier access
|
||||||
this.app.use(cookieParser());
|
this.app.use(cookieParser());
|
||||||
|
@ -3111,7 +3109,7 @@ async function getExecutionsCount(
|
||||||
try {
|
try {
|
||||||
// Get an estimate of rows count.
|
// Get an estimate of rows count.
|
||||||
const estimateRowsNumberSql =
|
const estimateRowsNumberSql =
|
||||||
'SELECT n_live_tup FROM pg_stat_all_tables WHERE relname = \'execution_entity\';';
|
"SELECT n_live_tup FROM pg_stat_all_tables WHERE relname = 'execution_entity';";
|
||||||
const rows: Array<{ n_live_tup: string }> = await Db.collections.Execution!.query(
|
const rows: Array<{ n_live_tup: string }> = await Db.collections.Execution!.query(
|
||||||
estimateRowsNumberSql,
|
estimateRowsNumberSql,
|
||||||
);
|
);
|
||||||
|
|
14
packages/cli/src/requests.d.ts
vendored
14
packages/cli/src/requests.d.ts
vendored
|
@ -196,9 +196,19 @@ export declare namespace UserRequest {
|
||||||
{ inviterId?: string; inviteeId?: string }
|
{ inviterId?: string; inviteeId?: string }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type Delete = AuthenticatedRequest<{ id: string, email: string }, {}, {}, { transferId?: string }>;
|
export type Delete = AuthenticatedRequest<
|
||||||
|
{ id: string; email: string },
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{ transferId?: string }
|
||||||
|
>;
|
||||||
|
|
||||||
export type Get = AuthenticatedRequest<{ id: string, email: string }, {}, {}, { limit: string, cursor: string }>;
|
export type Get = AuthenticatedRequest<
|
||||||
|
{ id: string; email: string },
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{ limit: string; cursor: string; includeRole: string }
|
||||||
|
>;
|
||||||
|
|
||||||
export type Reinvite = AuthenticatedRequest<{ id: string }>;
|
export type Reinvite = AuthenticatedRequest<{ id: string }>;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,183 @@
|
||||||
|
import express = require('express');
|
||||||
|
import validator from 'validator';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
import { compare } from 'bcryptjs';
|
||||||
|
|
||||||
|
import { Db } from '../../../src';
|
||||||
|
import config = require('../../../config');
|
||||||
|
import { SUCCESS_RESPONSE_BODY } from './../shared/constants';
|
||||||
|
import { Role } from '../../../src/databases/entities/Role';
|
||||||
|
import {
|
||||||
|
randomApiKey,
|
||||||
|
randomEmail,
|
||||||
|
randomInvalidPassword,
|
||||||
|
randomName,
|
||||||
|
randomValidPassword,
|
||||||
|
} from './../shared/random';
|
||||||
|
|
||||||
|
import * as utils from './../shared/utils';
|
||||||
|
import * as testDb from './../shared/testDb';
|
||||||
|
|
||||||
|
// import * from './../../../src/PublicApi/helpers'
|
||||||
|
|
||||||
|
let app: express.Application;
|
||||||
|
let testDbName = '';
|
||||||
|
let globalOwnerRole: Role;
|
||||||
|
let globalMemberRole: Role;
|
||||||
|
let workflowOwnerRole: Role;
|
||||||
|
let credentialOwnerRole: Role;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
app = utils.initTestServer({ endpointGroups: ['publicApi'], applyAuth: false });
|
||||||
|
const initResult = await testDb.init();
|
||||||
|
testDbName = initResult.testDbName;
|
||||||
|
|
||||||
|
const [
|
||||||
|
fetchedGlobalOwnerRole,
|
||||||
|
fetchedGlobalMemberRole,
|
||||||
|
fetchedWorkflowOwnerRole,
|
||||||
|
fetchedCredentialOwnerRole,
|
||||||
|
] = await testDb.getAllRoles();
|
||||||
|
|
||||||
|
globalOwnerRole = fetchedGlobalOwnerRole;
|
||||||
|
globalMemberRole = fetchedGlobalMemberRole;
|
||||||
|
workflowOwnerRole = fetchedWorkflowOwnerRole;
|
||||||
|
credentialOwnerRole = fetchedCredentialOwnerRole;
|
||||||
|
|
||||||
|
utils.initTestTelemetry();
|
||||||
|
utils.initTestLogger();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// do not combine calls - shared tables must be cleared first and separately
|
||||||
|
await testDb.truncate(['SharedCredentials', 'SharedWorkflow'], testDbName);
|
||||||
|
await testDb.truncate(['User', 'Workflow', 'Credentials'], testDbName);
|
||||||
|
|
||||||
|
jest.isolateModules(() => {
|
||||||
|
jest.mock('../../../config');
|
||||||
|
jest.mock('./../../../src/PublicApi/helpers', () => ({
|
||||||
|
...jest.requireActual('./../../../src/PublicApi/helpers'),
|
||||||
|
connectionName: jest.fn(() => testDbName),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
await testDb.createUser({
|
||||||
|
id: INITIAL_TEST_USER.id,
|
||||||
|
email: INITIAL_TEST_USER.email,
|
||||||
|
password: INITIAL_TEST_USER.password,
|
||||||
|
firstName: INITIAL_TEST_USER.firstName,
|
||||||
|
lastName: INITIAL_TEST_USER.lastName,
|
||||||
|
globalRole: globalOwnerRole,
|
||||||
|
apiKey: INITIAL_TEST_USER.apiKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
config.set('userManagement.disabled', false);
|
||||||
|
config.set('userManagement.isInstanceOwnerSetUp', true);
|
||||||
|
config.set('userManagement.emails.mode', '');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await testDb.terminate(testDbName);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET /users should fail due to missing API Key', async () => {
|
||||||
|
const owner = await Db.collections.User!.findOneOrFail();
|
||||||
|
|
||||||
|
const authOwnerAgent = utils.createAgent(app, { apiPath: 'public', auth: false, user: owner });
|
||||||
|
|
||||||
|
await testDb.createUser();
|
||||||
|
|
||||||
|
const response = await authOwnerAgent.get('/v1/users');
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(401);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET /users should fail due to invalid API Key', async () => {
|
||||||
|
const owner = await Db.collections.User!.findOneOrFail();
|
||||||
|
|
||||||
|
owner.apiKey = null;
|
||||||
|
|
||||||
|
const authOwnerAgent = utils.createAgent(app, { apiPath: 'public', auth: false, user: owner });
|
||||||
|
|
||||||
|
const response = await authOwnerAgent.get('/v1/users');
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET /users should fail due to member trying to access owner only endpoint', async () => {
|
||||||
|
const member = await testDb.createUser();
|
||||||
|
|
||||||
|
const authOwnerAgent = utils.createAgent(app, { apiPath: 'public', auth: true, user: member });
|
||||||
|
|
||||||
|
const response = await authOwnerAgent.get('/v1/users');
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET /users should fail due no instance owner not setup', async () => {
|
||||||
|
|
||||||
|
config.set('userManagement.isInstanceOwnerSetUp', false);
|
||||||
|
|
||||||
|
const owner = await Db.collections.User!.findOneOrFail();
|
||||||
|
|
||||||
|
const authOwnerAgent = utils.createAgent(app, { apiPath: 'public', auth: true, user: owner });
|
||||||
|
|
||||||
|
const response = await authOwnerAgent.get('/v1/users');
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(400);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET /users should return all users', async () => {
|
||||||
|
|
||||||
|
const owner = await Db.collections.User!.findOneOrFail();
|
||||||
|
|
||||||
|
const authOwnerAgent = utils.createAgent(app, { apiPath: 'public', auth: true, user: owner });
|
||||||
|
|
||||||
|
await testDb.createUser();
|
||||||
|
|
||||||
|
const response = await authOwnerAgent.get('/v1/users');
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
expect(response.body.users.length).toBe(2);
|
||||||
|
expect(response.body.nextCursor).toBeNull();
|
||||||
|
|
||||||
|
for (const user of response.body.users) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
personalizationAnswers,
|
||||||
|
globalRole,
|
||||||
|
password,
|
||||||
|
resetPasswordToken,
|
||||||
|
isPending,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
} = user;
|
||||||
|
|
||||||
|
expect(validator.isUUID(id)).toBe(true);
|
||||||
|
expect(email).toBeDefined();
|
||||||
|
expect(firstName).toBeDefined();
|
||||||
|
expect(lastName).toBeDefined();
|
||||||
|
expect(personalizationAnswers).toBeUndefined();
|
||||||
|
expect(password).toBeUndefined();
|
||||||
|
expect(resetPasswordToken).toBeUndefined();
|
||||||
|
//virtual method not working
|
||||||
|
//expect(isPending).toBe(false);
|
||||||
|
expect(globalRole).toBeUndefined();
|
||||||
|
expect(createdAt).toBeDefined();
|
||||||
|
expect(updatedAt).toBeDefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const INITIAL_TEST_USER = {
|
||||||
|
id: uuid(),
|
||||||
|
email: randomEmail(),
|
||||||
|
firstName: randomName(),
|
||||||
|
lastName: randomName(),
|
||||||
|
password: randomValidPassword(),
|
||||||
|
apiKey: randomApiKey(),
|
||||||
|
};
|
|
@ -2,6 +2,8 @@ import config = require('../../../config');
|
||||||
|
|
||||||
export const REST_PATH_SEGMENT = config.get('endpoints.rest') as Readonly<string>;
|
export const REST_PATH_SEGMENT = config.get('endpoints.rest') as Readonly<string>;
|
||||||
|
|
||||||
|
export const PUBLIC_API_REST_PATH_SEGMENT = config.get('publicApiEndpoints.path') as Readonly<string>;
|
||||||
|
|
||||||
export const AUTHLESS_ENDPOINTS: Readonly<string[]> = [
|
export const AUTHLESS_ENDPOINTS: Readonly<string[]> = [
|
||||||
'healthz',
|
'healthz',
|
||||||
'metrics',
|
'metrics',
|
||||||
|
|
|
@ -10,6 +10,10 @@ export function randomString(min: number, max: number) {
|
||||||
return randomBytes(randomInteger / 2).toString('hex');
|
return randomBytes(randomInteger / 2).toString('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function randomApiKey() {
|
||||||
|
return randomBytes(20).toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
const chooseRandomly = <T>(array: T[]) => array[Math.floor(Math.random() * array.length)];
|
const chooseRandomly = <T>(array: T[]) => array[Math.floor(Math.random() * array.length)];
|
||||||
|
|
||||||
const randomDigit = () => Math.floor(Math.random() * 10);
|
const randomDigit = () => Math.floor(Math.random() * 10);
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Credentials, UserSettings } from 'n8n-core';
|
||||||
import config = require('../../../config');
|
import config = require('../../../config');
|
||||||
import { BOOTSTRAP_MYSQL_CONNECTION_NAME, BOOTSTRAP_POSTGRES_CONNECTION_NAME } from './constants';
|
import { BOOTSTRAP_MYSQL_CONNECTION_NAME, BOOTSTRAP_POSTGRES_CONNECTION_NAME } from './constants';
|
||||||
import { DatabaseType, Db, ICredentialsDb, IDatabaseCollections } from '../../../src';
|
import { DatabaseType, Db, ICredentialsDb, IDatabaseCollections } from '../../../src';
|
||||||
import { randomEmail, randomName, randomString, randomValidPassword } from './random';
|
import { randomApiKey, randomEmail, randomName, randomString, randomValidPassword } from './random';
|
||||||
import { CredentialsEntity } from '../../../src/databases/entities/CredentialsEntity';
|
import { CredentialsEntity } from '../../../src/databases/entities/CredentialsEntity';
|
||||||
|
|
||||||
import { RESPONSE_ERROR_MESSAGES } from '../../../src/constants';
|
import { RESPONSE_ERROR_MESSAGES } from '../../../src/constants';
|
||||||
|
@ -185,13 +185,14 @@ export async function saveCredential(
|
||||||
* Store a user in the DB, defaulting to a `member`.
|
* Store a user in the DB, defaulting to a `member`.
|
||||||
*/
|
*/
|
||||||
export async function createUser(attributes: Partial<User> = {}): Promise<User> {
|
export async function createUser(attributes: Partial<User> = {}): Promise<User> {
|
||||||
const { email, password, firstName, lastName, globalRole, ...rest } = attributes;
|
const { email, password, firstName, lastName, globalRole, apiKey, ...rest } = attributes;
|
||||||
const user = {
|
const user = {
|
||||||
email: email ?? randomEmail(),
|
email: email ?? randomEmail(),
|
||||||
password: password ?? randomValidPassword(),
|
password: password ?? randomValidPassword(),
|
||||||
firstName: firstName ?? randomName(),
|
firstName: firstName ?? randomName(),
|
||||||
lastName: lastName ?? randomName(),
|
lastName: lastName ?? randomName(),
|
||||||
globalRole: globalRole ?? (await getGlobalMemberRole()),
|
globalRole: globalRole ?? (await getGlobalMemberRole()),
|
||||||
|
apiKey: apiKey?? randomApiKey(),
|
||||||
...rest,
|
...rest,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,9 @@ export type SmtpTestAccount = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type EndpointGroup = 'me' | 'users' | 'auth' | 'owner' | 'passwordReset' | 'credentials';
|
export type ApiPath = 'internal' | 'public';
|
||||||
|
|
||||||
|
type EndpointGroup = 'me' | 'users' | 'auth' | 'owner' | 'passwordReset' | 'credentials' | 'publicApi';
|
||||||
|
|
||||||
export type CredentialPayload = {
|
export type CredentialPayload = {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { INodeTypes, LoggerProxy } from 'n8n-workflow';
|
||||||
import { UserSettings } from 'n8n-core';
|
import { UserSettings } from 'n8n-core';
|
||||||
|
|
||||||
import config = require('../../../config');
|
import config = require('../../../config');
|
||||||
import { AUTHLESS_ENDPOINTS, REST_PATH_SEGMENT } from './constants';
|
import { AUTHLESS_ENDPOINTS, PUBLIC_API_REST_PATH_SEGMENT, REST_PATH_SEGMENT } from './constants';
|
||||||
import { AUTH_COOKIE_NAME } from '../../../src/constants';
|
import { AUTH_COOKIE_NAME } from '../../../src/constants';
|
||||||
import { addRoutes as authMiddleware } from '../../../src/UserManagement/routes';
|
import { addRoutes as authMiddleware } from '../../../src/UserManagement/routes';
|
||||||
import { Db, ExternalHooks, InternalHooksManager } from '../../../src';
|
import { Db, ExternalHooks, InternalHooksManager } from '../../../src';
|
||||||
|
@ -23,10 +23,10 @@ import { passwordResetNamespace as passwordResetEndpoints } from '../../../src/U
|
||||||
import { issueJWT } from '../../../src/UserManagement/auth/jwt';
|
import { issueJWT } from '../../../src/UserManagement/auth/jwt';
|
||||||
import { getLogger } from '../../../src/Logger';
|
import { getLogger } from '../../../src/Logger';
|
||||||
import { credentialsController } from '../../../src/api/credentials.api';
|
import { credentialsController } from '../../../src/api/credentials.api';
|
||||||
|
import { publicApiController } from '../../../src/PublicApi/v1/';
|
||||||
import type { User } from '../../../src/databases/entities/User';
|
import type { User } from '../../../src/databases/entities/User';
|
||||||
import { Telemetry } from '../../../src/telemetry';
|
import { Telemetry } from '../../../src/telemetry';
|
||||||
import type { EndpointGroup, SmtpTestAccount } from './types';
|
import type { ApiPath, EndpointGroup, SmtpTestAccount } from './types';
|
||||||
import type { N8nApp } from '../../../src/UserManagement/Interfaces';
|
import type { N8nApp } from '../../../src/UserManagement/Interfaces';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -45,6 +45,7 @@ export function initTestServer({
|
||||||
const testServer = {
|
const testServer = {
|
||||||
app: express(),
|
app: express(),
|
||||||
restEndpoint: REST_PATH_SEGMENT,
|
restEndpoint: REST_PATH_SEGMENT,
|
||||||
|
publicApiEndpoint: PUBLIC_API_REST_PATH_SEGMENT,
|
||||||
...(endpointGroups?.includes('credentials') ? { externalHooks: ExternalHooks() } : {}),
|
...(endpointGroups?.includes('credentials') ? { externalHooks: ExternalHooks() } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -65,10 +66,15 @@ export function initTestServer({
|
||||||
if (routerEndpoints.length) {
|
if (routerEndpoints.length) {
|
||||||
const map: Record<string, express.Router> = {
|
const map: Record<string, express.Router> = {
|
||||||
credentials: credentialsController,
|
credentials: credentialsController,
|
||||||
|
publicApi: publicApiController,
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const group of routerEndpoints) {
|
for (const group of routerEndpoints) {
|
||||||
testServer.app.use(`/${testServer.restEndpoint}/${group}`, map[group]);
|
if (group === 'publicApi') {
|
||||||
|
testServer.app.use(`/${testServer.publicApiEndpoint}`, map[group]);
|
||||||
|
} else {
|
||||||
|
testServer.app.use(`/${testServer.restEndpoint}/${group}`, map[group]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,7 +112,7 @@ const classifyEndpointGroups = (endpointGroups: string[]) => {
|
||||||
const functionEndpoints: string[] = [];
|
const functionEndpoints: string[] = [];
|
||||||
|
|
||||||
endpointGroups.forEach((group) =>
|
endpointGroups.forEach((group) =>
|
||||||
(group === 'credentials' ? routerEndpoints : functionEndpoints).push(group),
|
(group === 'credentials' || group === 'publicApi' ? routerEndpoints : functionEndpoints).push(group),
|
||||||
);
|
);
|
||||||
|
|
||||||
return [routerEndpoints, functionEndpoints];
|
return [routerEndpoints, functionEndpoints];
|
||||||
|
@ -143,13 +149,24 @@ export function initConfigFile() {
|
||||||
/**
|
/**
|
||||||
* Create a request agent, optionally with an auth cookie.
|
* Create a request agent, optionally with an auth cookie.
|
||||||
*/
|
*/
|
||||||
export function createAgent(app: express.Application, options?: { auth: true; user: User }) {
|
export function createAgent(app: express.Application, options?: { apiPath?: ApiPath, auth: boolean; user: User }) {
|
||||||
const agent = request.agent(app);
|
const agent = request.agent(app);
|
||||||
agent.use(prefix(REST_PATH_SEGMENT));
|
|
||||||
|
|
||||||
if (options?.auth && options?.user) {
|
if (options?.apiPath === undefined || options?.apiPath === 'internal') {
|
||||||
const { token } = issueJWT(options.user);
|
agent.use(prefix(REST_PATH_SEGMENT));
|
||||||
agent.jar.setCookie(`${AUTH_COOKIE_NAME}=${token}`);
|
|
||||||
|
if (options?.auth && options?.user) {
|
||||||
|
const { token } = issueJWT(options.user);
|
||||||
|
agent.jar.setCookie(`${AUTH_COOKIE_NAME}=${token}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.apiPath === 'public') {
|
||||||
|
agent.use(prefix(PUBLIC_API_REST_PATH_SEGMENT));
|
||||||
|
|
||||||
|
if (options?.auth && options?.user.apiKey) {
|
||||||
|
agent.set({ 'X-N8N-API-KEY': options.user.apiKey });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return agent;
|
return agent;
|
||||||
|
@ -171,7 +188,6 @@ export function prefix(pathSegment: string) {
|
||||||
|
|
||||||
url.pathname = pathSegment + url.pathname;
|
url.pathname = pathSegment + url.pathname;
|
||||||
request.url = url.toString();
|
request.url = url.toString();
|
||||||
|
|
||||||
return request;
|
return request;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue