mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
refactor: Move api-keys
endpoints to their own controller (#11000)
This commit is contained in:
parent
77fec195d9
commit
e54a396088
|
@ -0,0 +1,79 @@
|
||||||
|
import { mock } from 'jest-mock-extended';
|
||||||
|
import { randomString } from 'n8n-workflow';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
|
||||||
|
import type { ApiKey } from '@/databases/entities/api-key';
|
||||||
|
import type { User } from '@/databases/entities/user';
|
||||||
|
import { ApiKeyRepository } from '@/databases/repositories/api-key.repository';
|
||||||
|
import type { ApiKeysRequest, AuthenticatedRequest } from '@/requests';
|
||||||
|
import { API_KEY_PREFIX } from '@/services/public-api-key.service';
|
||||||
|
import { mockInstance } from '@test/mocking';
|
||||||
|
|
||||||
|
import { ApiKeysController } from '../api-keys.controller';
|
||||||
|
|
||||||
|
describe('ApiKeysController', () => {
|
||||||
|
const apiKeysRepository = mockInstance(ApiKeyRepository);
|
||||||
|
const controller = Container.get(ApiKeysController);
|
||||||
|
|
||||||
|
let req: AuthenticatedRequest;
|
||||||
|
beforeAll(() => {
|
||||||
|
req = mock<AuthenticatedRequest>({ user: mock<User>({ id: '123' }) });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createAPIKey', () => {
|
||||||
|
it('should create and save an API key', async () => {
|
||||||
|
const apiKeyData = {
|
||||||
|
id: '123',
|
||||||
|
userId: '123',
|
||||||
|
label: 'My API Key',
|
||||||
|
apiKey: `${API_KEY_PREFIX}${randomString(42)}`,
|
||||||
|
createdAt: new Date(),
|
||||||
|
} as ApiKey;
|
||||||
|
|
||||||
|
apiKeysRepository.upsert.mockImplementation();
|
||||||
|
|
||||||
|
apiKeysRepository.findOneByOrFail.mockResolvedValue(apiKeyData);
|
||||||
|
|
||||||
|
const newApiKey = await controller.createAPIKey(req);
|
||||||
|
|
||||||
|
expect(apiKeysRepository.upsert).toHaveBeenCalled();
|
||||||
|
expect(apiKeyData).toEqual(newApiKey);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAPIKeys', () => {
|
||||||
|
it('should return the users api keys redacted', async () => {
|
||||||
|
const apiKeyData = {
|
||||||
|
id: '123',
|
||||||
|
userId: '123',
|
||||||
|
label: 'My API Key',
|
||||||
|
apiKey: `${API_KEY_PREFIX}${randomString(42)}`,
|
||||||
|
createdAt: new Date(),
|
||||||
|
} as ApiKey;
|
||||||
|
|
||||||
|
apiKeysRepository.findBy.mockResolvedValue([apiKeyData]);
|
||||||
|
|
||||||
|
const apiKeys = await controller.getAPIKeys(req);
|
||||||
|
expect(apiKeys[0].apiKey).not.toEqual(apiKeyData.apiKey);
|
||||||
|
expect(apiKeysRepository.findBy).toHaveBeenCalledWith({ userId: req.user.id });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteAPIKey', () => {
|
||||||
|
it('should delete the API key', async () => {
|
||||||
|
const user = mock<User>({
|
||||||
|
id: '123',
|
||||||
|
password: 'password',
|
||||||
|
authIdentities: [],
|
||||||
|
role: 'global:member',
|
||||||
|
mfaEnabled: false,
|
||||||
|
});
|
||||||
|
const req = mock<ApiKeysRequest.DeleteAPIKey>({ user, params: { id: user.id } });
|
||||||
|
await controller.deleteAPIKey(req);
|
||||||
|
expect(apiKeysRepository.delete).toHaveBeenCalledWith({
|
||||||
|
userId: req.user.id,
|
||||||
|
id: req.params.id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -2,14 +2,11 @@ import { UserUpdateRequestDto } from '@n8n/api-types';
|
||||||
import type { Response } from 'express';
|
import type { Response } from 'express';
|
||||||
import { mock, anyObject } from 'jest-mock-extended';
|
import { mock, anyObject } from 'jest-mock-extended';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import { randomString } from 'n8n-workflow';
|
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
|
|
||||||
import { AUTH_COOKIE_NAME } from '@/constants';
|
import { AUTH_COOKIE_NAME } from '@/constants';
|
||||||
import { MeController } from '@/controllers/me.controller';
|
import { MeController } from '@/controllers/me.controller';
|
||||||
import type { ApiKey } from '@/databases/entities/api-key';
|
|
||||||
import type { User } from '@/databases/entities/user';
|
import type { User } from '@/databases/entities/user';
|
||||||
import { ApiKeyRepository } from '@/databases/repositories/api-key.repository';
|
|
||||||
import { AuthUserRepository } from '@/databases/repositories/auth-user.repository';
|
import { AuthUserRepository } from '@/databases/repositories/auth-user.repository';
|
||||||
import { InvalidAuthTokenRepository } from '@/databases/repositories/invalid-auth-token.repository';
|
import { InvalidAuthTokenRepository } from '@/databases/repositories/invalid-auth-token.repository';
|
||||||
import { UserRepository } from '@/databases/repositories/user.repository';
|
import { UserRepository } from '@/databases/repositories/user.repository';
|
||||||
|
@ -21,7 +18,6 @@ import type { PublicUser } from '@/interfaces';
|
||||||
import { License } from '@/license';
|
import { License } from '@/license';
|
||||||
import { MfaService } from '@/mfa/mfa.service';
|
import { MfaService } from '@/mfa/mfa.service';
|
||||||
import type { AuthenticatedRequest, MeRequest } from '@/requests';
|
import type { AuthenticatedRequest, MeRequest } from '@/requests';
|
||||||
import { API_KEY_PREFIX } from '@/services/public-api-key.service';
|
|
||||||
import { UserService } from '@/services/user.service';
|
import { UserService } from '@/services/user.service';
|
||||||
import { mockInstance } from '@test/mocking';
|
import { mockInstance } from '@test/mocking';
|
||||||
import { badPasswords } from '@test/test-data';
|
import { badPasswords } from '@test/test-data';
|
||||||
|
@ -34,7 +30,6 @@ describe('MeController', () => {
|
||||||
const userService = mockInstance(UserService);
|
const userService = mockInstance(UserService);
|
||||||
const userRepository = mockInstance(UserRepository);
|
const userRepository = mockInstance(UserRepository);
|
||||||
const mockMfaService = mockInstance(MfaService);
|
const mockMfaService = mockInstance(MfaService);
|
||||||
const apiKeysRepository = mockInstance(ApiKeyRepository);
|
|
||||||
mockInstance(AuthUserRepository);
|
mockInstance(AuthUserRepository);
|
||||||
mockInstance(InvalidAuthTokenRepository);
|
mockInstance(InvalidAuthTokenRepository);
|
||||||
mockInstance(License).isWithinUsersLimit.mockReturnValue(true);
|
mockInstance(License).isWithinUsersLimit.mockReturnValue(true);
|
||||||
|
@ -413,68 +408,4 @@ describe('MeController', () => {
|
||||||
await expect(controller.storeSurveyAnswers(req)).rejects.toThrowError(BadRequestError);
|
await expect(controller.storeSurveyAnswers(req)).rejects.toThrowError(BadRequestError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('API Key methods', () => {
|
|
||||||
let req: AuthenticatedRequest;
|
|
||||||
beforeAll(() => {
|
|
||||||
req = mock<AuthenticatedRequest>({ user: mock<User>({ id: '123' }) });
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('createAPIKey', () => {
|
|
||||||
it('should create and save an API key', async () => {
|
|
||||||
const apiKeyData = {
|
|
||||||
id: '123',
|
|
||||||
userId: '123',
|
|
||||||
label: 'My API Key',
|
|
||||||
apiKey: `${API_KEY_PREFIX}${randomString(42)}`,
|
|
||||||
createdAt: new Date(),
|
|
||||||
} as ApiKey;
|
|
||||||
|
|
||||||
apiKeysRepository.upsert.mockImplementation();
|
|
||||||
|
|
||||||
apiKeysRepository.findOneByOrFail.mockResolvedValue(apiKeyData);
|
|
||||||
|
|
||||||
const newApiKey = await controller.createAPIKey(req);
|
|
||||||
|
|
||||||
expect(apiKeysRepository.upsert).toHaveBeenCalled();
|
|
||||||
expect(apiKeyData).toEqual(newApiKey);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getAPIKeys', () => {
|
|
||||||
it('should return the users api keys redacted', async () => {
|
|
||||||
const apiKeyData = {
|
|
||||||
id: '123',
|
|
||||||
userId: '123',
|
|
||||||
label: 'My API Key',
|
|
||||||
apiKey: `${API_KEY_PREFIX}${randomString(42)}`,
|
|
||||||
createdAt: new Date(),
|
|
||||||
} as ApiKey;
|
|
||||||
|
|
||||||
apiKeysRepository.findBy.mockResolvedValue([apiKeyData]);
|
|
||||||
|
|
||||||
const apiKeys = await controller.getAPIKeys(req);
|
|
||||||
expect(apiKeys[0].apiKey).not.toEqual(apiKeyData.apiKey);
|
|
||||||
expect(apiKeysRepository.findBy).toHaveBeenCalledWith({ userId: req.user.id });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('deleteAPIKey', () => {
|
|
||||||
it('should delete the API key', async () => {
|
|
||||||
const user = mock<User>({
|
|
||||||
id: '123',
|
|
||||||
password: 'password',
|
|
||||||
authIdentities: [],
|
|
||||||
role: 'global:member',
|
|
||||||
mfaEnabled: false,
|
|
||||||
});
|
|
||||||
const req = mock<MeRequest.DeleteAPIKey>({ user, params: { id: user.id } });
|
|
||||||
await controller.deleteAPIKey(req);
|
|
||||||
expect(apiKeysRepository.delete).toHaveBeenCalledWith({
|
|
||||||
userId: req.user.id,
|
|
||||||
id: req.params.id,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
56
packages/cli/src/controllers/api-keys.controller.ts
Normal file
56
packages/cli/src/controllers/api-keys.controller.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import { type RequestHandler } from 'express';
|
||||||
|
|
||||||
|
import { Delete, Get, Post, RestController } from '@/decorators';
|
||||||
|
import { EventService } from '@/events/event.service';
|
||||||
|
import { isApiEnabled } from '@/public-api';
|
||||||
|
import { ApiKeysRequest, AuthenticatedRequest } from '@/requests';
|
||||||
|
import { PublicApiKeyService } from '@/services/public-api-key.service';
|
||||||
|
|
||||||
|
export const isApiEnabledMiddleware: RequestHandler = (_, res, next) => {
|
||||||
|
if (isApiEnabled()) {
|
||||||
|
next();
|
||||||
|
} else {
|
||||||
|
res.status(404).end();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@RestController('/api-keys')
|
||||||
|
export class ApiKeysController {
|
||||||
|
constructor(
|
||||||
|
private readonly eventService: EventService,
|
||||||
|
private readonly publicApiKeyService: PublicApiKeyService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an API Key
|
||||||
|
*/
|
||||||
|
@Post('/', { middlewares: [isApiEnabledMiddleware] })
|
||||||
|
async createAPIKey(req: AuthenticatedRequest) {
|
||||||
|
const newApiKey = await this.publicApiKeyService.createPublicApiKeyForUser(req.user);
|
||||||
|
|
||||||
|
this.eventService.emit('public-api-key-created', { user: req.user, publicApi: false });
|
||||||
|
|
||||||
|
return newApiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get API keys
|
||||||
|
*/
|
||||||
|
@Get('/', { middlewares: [isApiEnabledMiddleware] })
|
||||||
|
async getAPIKeys(req: AuthenticatedRequest) {
|
||||||
|
const apiKeys = await this.publicApiKeyService.getRedactedApiKeysForUser(req.user);
|
||||||
|
return apiKeys;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an API Key
|
||||||
|
*/
|
||||||
|
@Delete('/:id', { middlewares: [isApiEnabledMiddleware] })
|
||||||
|
async deleteAPIKey(req: ApiKeysRequest.DeleteAPIKey) {
|
||||||
|
await this.publicApiKeyService.deleteApiKeyForUser(req.user, req.params.id);
|
||||||
|
|
||||||
|
this.eventService.emit('public-api-key-deleted', { user: req.user, publicApi: false });
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,12 +4,12 @@ import {
|
||||||
UserUpdateRequestDto,
|
UserUpdateRequestDto,
|
||||||
} from '@n8n/api-types';
|
} from '@n8n/api-types';
|
||||||
import { plainToInstance } from 'class-transformer';
|
import { plainToInstance } from 'class-transformer';
|
||||||
import { type RequestHandler, Response } from 'express';
|
import { Response } from 'express';
|
||||||
|
|
||||||
import { AuthService } from '@/auth/auth.service';
|
import { AuthService } from '@/auth/auth.service';
|
||||||
import type { User } from '@/databases/entities/user';
|
import type { User } from '@/databases/entities/user';
|
||||||
import { UserRepository } from '@/databases/repositories/user.repository';
|
import { UserRepository } from '@/databases/repositories/user.repository';
|
||||||
import { Body, Delete, Get, Patch, Post, RestController } from '@/decorators';
|
import { Body, Patch, Post, RestController } from '@/decorators';
|
||||||
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
|
||||||
import { InvalidMfaCodeError } from '@/errors/response-errors/invalid-mfa-code.error';
|
import { InvalidMfaCodeError } from '@/errors/response-errors/invalid-mfa-code.error';
|
||||||
import { EventService } from '@/events/event.service';
|
import { EventService } from '@/events/event.service';
|
||||||
|
@ -18,23 +18,12 @@ import { validateEntity } from '@/generic-helpers';
|
||||||
import type { PublicUser } from '@/interfaces';
|
import type { PublicUser } from '@/interfaces';
|
||||||
import { Logger } from '@/logger';
|
import { Logger } from '@/logger';
|
||||||
import { MfaService } from '@/mfa/mfa.service';
|
import { MfaService } from '@/mfa/mfa.service';
|
||||||
import { isApiEnabled } from '@/public-api';
|
|
||||||
import { AuthenticatedRequest, MeRequest } from '@/requests';
|
import { AuthenticatedRequest, MeRequest } from '@/requests';
|
||||||
import { PasswordUtility } from '@/services/password.utility';
|
import { PasswordUtility } from '@/services/password.utility';
|
||||||
import { PublicApiKeyService } from '@/services/public-api-key.service';
|
|
||||||
import { UserService } from '@/services/user.service';
|
import { UserService } from '@/services/user.service';
|
||||||
import { isSamlLicensedAndEnabled } from '@/sso/saml/saml-helpers';
|
import { isSamlLicensedAndEnabled } from '@/sso/saml/saml-helpers';
|
||||||
|
|
||||||
import { PersonalizationSurveyAnswersV4 } from './survey-answers.dto';
|
import { PersonalizationSurveyAnswersV4 } from './survey-answers.dto';
|
||||||
|
|
||||||
export const isApiEnabledMiddleware: RequestHandler = (_, res, next) => {
|
|
||||||
if (isApiEnabled()) {
|
|
||||||
next();
|
|
||||||
} else {
|
|
||||||
res.status(404).end();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@RestController('/me')
|
@RestController('/me')
|
||||||
export class MeController {
|
export class MeController {
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -46,7 +35,6 @@ export class MeController {
|
||||||
private readonly userRepository: UserRepository,
|
private readonly userRepository: UserRepository,
|
||||||
private readonly eventService: EventService,
|
private readonly eventService: EventService,
|
||||||
private readonly mfaService: MfaService,
|
private readonly mfaService: MfaService,
|
||||||
private readonly publicApiKeyService: PublicApiKeyService,
|
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -217,39 +205,6 @@ export class MeController {
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an API Key
|
|
||||||
*/
|
|
||||||
@Post('/api-keys', { middlewares: [isApiEnabledMiddleware] })
|
|
||||||
async createAPIKey(req: AuthenticatedRequest) {
|
|
||||||
const newApiKey = await this.publicApiKeyService.createPublicApiKeyForUser(req.user);
|
|
||||||
|
|
||||||
this.eventService.emit('public-api-key-created', { user: req.user, publicApi: false });
|
|
||||||
|
|
||||||
return newApiKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get API keys
|
|
||||||
*/
|
|
||||||
@Get('/api-keys', { middlewares: [isApiEnabledMiddleware] })
|
|
||||||
async getAPIKeys(req: AuthenticatedRequest) {
|
|
||||||
const apiKeys = await this.publicApiKeyService.getRedactedApiKeysForUser(req.user);
|
|
||||||
return apiKeys;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete an API Key
|
|
||||||
*/
|
|
||||||
@Delete('/api-keys/:id', { middlewares: [isApiEnabledMiddleware] })
|
|
||||||
async deleteAPIKey(req: MeRequest.DeleteAPIKey) {
|
|
||||||
await this.publicApiKeyService.deleteApiKeyForUser(req.user, req.params.id);
|
|
||||||
|
|
||||||
this.eventService.emit('public-api-key-deleted', { user: req.user, publicApi: false });
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the logged-in user's settings.
|
* Update the logged-in user's settings.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -180,13 +180,20 @@ export declare namespace CredentialRequest {
|
||||||
>;
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// /api-keys
|
||||||
|
// ----------------------------------
|
||||||
|
|
||||||
|
export declare namespace ApiKeysRequest {
|
||||||
|
export type DeleteAPIKey = AuthenticatedRequest<{ id: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
// /me
|
// /me
|
||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
|
||||||
export declare namespace MeRequest {
|
export declare namespace MeRequest {
|
||||||
export type SurveyAnswers = AuthenticatedRequest<{}, {}, IPersonalizationSurveyAnswersV4>;
|
export type SurveyAnswers = AuthenticatedRequest<{}, {}, IPersonalizationSurveyAnswersV4>;
|
||||||
export type DeleteAPIKey = AuthenticatedRequest<{ id: string }>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserSetupPayload {
|
export interface UserSetupPayload {
|
||||||
|
|
|
@ -56,6 +56,7 @@ import '@/controllers/translation.controller';
|
||||||
import '@/controllers/users.controller';
|
import '@/controllers/users.controller';
|
||||||
import '@/controllers/user-settings.controller';
|
import '@/controllers/user-settings.controller';
|
||||||
import '@/controllers/workflow-statistics.controller';
|
import '@/controllers/workflow-statistics.controller';
|
||||||
|
import '@/controllers/api-keys.controller';
|
||||||
import '@/credentials/credentials.controller';
|
import '@/credentials/credentials.controller';
|
||||||
import '@/eventbus/event-bus.controller';
|
import '@/eventbus/event-bus.controller';
|
||||||
import '@/events/events.controller';
|
import '@/events/events.controller';
|
||||||
|
|
178
packages/cli/test/integration/api-keys.api.test.ts
Normal file
178
packages/cli/test/integration/api-keys.api.test.ts
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
import { GlobalConfig } from '@n8n/config';
|
||||||
|
import { Container } from 'typedi';
|
||||||
|
|
||||||
|
import type { ApiKey } from '@/databases/entities/api-key';
|
||||||
|
import type { User } from '@/databases/entities/user';
|
||||||
|
import { ApiKeyRepository } from '@/databases/repositories/api-key.repository';
|
||||||
|
import { PublicApiKeyService } from '@/services/public-api-key.service';
|
||||||
|
import { mockInstance } from '@test/mocking';
|
||||||
|
|
||||||
|
import { createOwnerWithApiKey, createUser, createUserShell } from './shared/db/users';
|
||||||
|
import { randomValidPassword } from './shared/random';
|
||||||
|
import * as testDb from './shared/test-db';
|
||||||
|
import type { SuperAgentTest } from './shared/types';
|
||||||
|
import * as utils from './shared/utils/';
|
||||||
|
|
||||||
|
const testServer = utils.setupTestServer({ endpointGroups: ['apiKeys'] });
|
||||||
|
let publicApiKeyService: PublicApiKeyService;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
publicApiKeyService = Container.get(PublicApiKeyService);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await testDb.truncate(['User']);
|
||||||
|
mockInstance(GlobalConfig, { publicApi: { disabled: false } });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When public API is disabled', () => {
|
||||||
|
let owner: User;
|
||||||
|
let authAgent: SuperAgentTest;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
owner = await createOwnerWithApiKey();
|
||||||
|
|
||||||
|
authAgent = testServer.authAgentFor(owner);
|
||||||
|
mockInstance(GlobalConfig, { publicApi: { disabled: true } });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('POST /api-keys should 404', async () => {
|
||||||
|
await authAgent.post('/api-keys').expect(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET /api-keys should 404', async () => {
|
||||||
|
await authAgent.get('/api-keys').expect(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DELETE /api-key/:id should 404', async () => {
|
||||||
|
await authAgent.delete(`/api-keys/${1}`).expect(404);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Owner shell', () => {
|
||||||
|
let ownerShell: User;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
ownerShell = await createUserShell('global:owner');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('POST /api-keys should create an api key', async () => {
|
||||||
|
const newApiKeyResponse = await testServer.authAgentFor(ownerShell).post('/api-keys');
|
||||||
|
|
||||||
|
const newApiKey = newApiKeyResponse.body.data as ApiKey;
|
||||||
|
|
||||||
|
expect(newApiKeyResponse.statusCode).toBe(200);
|
||||||
|
expect(newApiKey).toBeDefined();
|
||||||
|
|
||||||
|
const newStoredApiKey = await Container.get(ApiKeyRepository).findOneByOrFail({
|
||||||
|
userId: ownerShell.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(newStoredApiKey).toEqual({
|
||||||
|
id: expect.any(String),
|
||||||
|
label: 'My API Key',
|
||||||
|
userId: ownerShell.id,
|
||||||
|
apiKey: newApiKey.apiKey,
|
||||||
|
createdAt: expect.any(Date),
|
||||||
|
updatedAt: expect.any(Date),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET /api-keys should fetch the api key redacted', async () => {
|
||||||
|
const newApiKeyResponse = await testServer.authAgentFor(ownerShell).post('/api-keys');
|
||||||
|
|
||||||
|
const retrieveAllApiKeysResponse = await testServer.authAgentFor(ownerShell).get('/api-keys');
|
||||||
|
|
||||||
|
expect(retrieveAllApiKeysResponse.statusCode).toBe(200);
|
||||||
|
|
||||||
|
expect(retrieveAllApiKeysResponse.body.data[0]).toEqual({
|
||||||
|
id: newApiKeyResponse.body.data.id,
|
||||||
|
label: 'My API Key',
|
||||||
|
userId: ownerShell.id,
|
||||||
|
apiKey: publicApiKeyService.redactApiKey(newApiKeyResponse.body.data.apiKey),
|
||||||
|
createdAt: expect.any(String),
|
||||||
|
updatedAt: expect.any(String),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DELETE /api-keys/:id should delete the api key', async () => {
|
||||||
|
const newApiKeyResponse = await testServer.authAgentFor(ownerShell).post('/api-keys');
|
||||||
|
|
||||||
|
const deleteApiKeyResponse = await testServer
|
||||||
|
.authAgentFor(ownerShell)
|
||||||
|
.delete(`/api-keys/${newApiKeyResponse.body.data.id}`);
|
||||||
|
|
||||||
|
const retrieveAllApiKeysResponse = await testServer.authAgentFor(ownerShell).get('/api-keys');
|
||||||
|
|
||||||
|
expect(deleteApiKeyResponse.body.data.success).toBe(true);
|
||||||
|
expect(retrieveAllApiKeysResponse.body.data.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Member', () => {
|
||||||
|
const memberPassword = randomValidPassword();
|
||||||
|
let member: User;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
member = await createUser({
|
||||||
|
password: memberPassword,
|
||||||
|
role: 'global:member',
|
||||||
|
});
|
||||||
|
await utils.setInstanceOwnerSetUp(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('POST /api-keys should create an api key', async () => {
|
||||||
|
const newApiKeyResponse = await testServer.authAgentFor(member).post('/api-keys');
|
||||||
|
|
||||||
|
expect(newApiKeyResponse.statusCode).toBe(200);
|
||||||
|
expect(newApiKeyResponse.body.data.apiKey).toBeDefined();
|
||||||
|
expect(newApiKeyResponse.body.data.apiKey).not.toBeNull();
|
||||||
|
|
||||||
|
const newStoredApiKey = await Container.get(ApiKeyRepository).findOneByOrFail({
|
||||||
|
userId: member.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(newStoredApiKey).toEqual({
|
||||||
|
id: expect.any(String),
|
||||||
|
label: 'My API Key',
|
||||||
|
userId: member.id,
|
||||||
|
apiKey: newApiKeyResponse.body.data.apiKey,
|
||||||
|
createdAt: expect.any(Date),
|
||||||
|
updatedAt: expect.any(Date),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('GET /api-keys should fetch the api key redacted', async () => {
|
||||||
|
const newApiKeyResponse = await testServer.authAgentFor(member).post('/api-keys');
|
||||||
|
|
||||||
|
const retrieveAllApiKeysResponse = await testServer.authAgentFor(member).get('/api-keys');
|
||||||
|
|
||||||
|
expect(retrieveAllApiKeysResponse.statusCode).toBe(200);
|
||||||
|
|
||||||
|
expect(retrieveAllApiKeysResponse.body.data[0]).toEqual({
|
||||||
|
id: newApiKeyResponse.body.data.id,
|
||||||
|
label: 'My API Key',
|
||||||
|
userId: member.id,
|
||||||
|
apiKey: publicApiKeyService.redactApiKey(newApiKeyResponse.body.data.apiKey),
|
||||||
|
createdAt: expect.any(String),
|
||||||
|
updatedAt: expect.any(String),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(newApiKeyResponse.body.data.apiKey).not.toEqual(
|
||||||
|
retrieveAllApiKeysResponse.body.data[0].apiKey,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('DELETE /api-keys/:id should delete the api key', async () => {
|
||||||
|
const newApiKeyResponse = await testServer.authAgentFor(member).post('/api-keys');
|
||||||
|
|
||||||
|
const deleteApiKeyResponse = await testServer
|
||||||
|
.authAgentFor(member)
|
||||||
|
.delete(`/api-keys/${newApiKeyResponse.body.data.id}`);
|
||||||
|
|
||||||
|
const retrieveAllApiKeysResponse = await testServer.authAgentFor(member).get('/api-keys');
|
||||||
|
|
||||||
|
expect(deleteApiKeyResponse.body.data.success).toBe(true);
|
||||||
|
expect(retrieveAllApiKeysResponse.body.data.length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
|
@ -3,57 +3,25 @@ import type { IPersonalizationSurveyAnswersV4 } from 'n8n-workflow';
|
||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import validator from 'validator';
|
import validator from 'validator';
|
||||||
|
|
||||||
import type { ApiKey } from '@/databases/entities/api-key';
|
|
||||||
import type { User } from '@/databases/entities/user';
|
import type { User } from '@/databases/entities/user';
|
||||||
import { ApiKeyRepository } from '@/databases/repositories/api-key.repository';
|
|
||||||
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
import { ProjectRepository } from '@/databases/repositories/project.repository';
|
||||||
import { UserRepository } from '@/databases/repositories/user.repository';
|
import { UserRepository } from '@/databases/repositories/user.repository';
|
||||||
import { PublicApiKeyService } from '@/services/public-api-key.service';
|
|
||||||
import { mockInstance } from '@test/mocking';
|
import { mockInstance } from '@test/mocking';
|
||||||
|
|
||||||
import { SUCCESS_RESPONSE_BODY } from './shared/constants';
|
import { SUCCESS_RESPONSE_BODY } from './shared/constants';
|
||||||
import { createOwnerWithApiKey, createUser, createUserShell } from './shared/db/users';
|
import { createUser, createUserShell } from './shared/db/users';
|
||||||
import { randomEmail, randomName, randomValidPassword } from './shared/random';
|
import { randomEmail, randomName, randomValidPassword } from './shared/random';
|
||||||
import * as testDb from './shared/test-db';
|
import * as testDb from './shared/test-db';
|
||||||
import type { SuperAgentTest } from './shared/types';
|
import type { SuperAgentTest } from './shared/types';
|
||||||
import * as utils from './shared/utils/';
|
import * as utils from './shared/utils/';
|
||||||
|
|
||||||
const testServer = utils.setupTestServer({ endpointGroups: ['me'] });
|
const testServer = utils.setupTestServer({ endpointGroups: ['me'] });
|
||||||
let publicApiKeyService: PublicApiKeyService;
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
publicApiKeyService = Container.get(PublicApiKeyService);
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await testDb.truncate(['User']);
|
await testDb.truncate(['User']);
|
||||||
mockInstance(GlobalConfig, { publicApi: { disabled: false } });
|
mockInstance(GlobalConfig, { publicApi: { disabled: false } });
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When public API is disabled', () => {
|
|
||||||
let owner: User;
|
|
||||||
let authAgent: SuperAgentTest;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
owner = await createOwnerWithApiKey();
|
|
||||||
|
|
||||||
authAgent = testServer.authAgentFor(owner);
|
|
||||||
mockInstance(GlobalConfig, { publicApi: { disabled: true } });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('POST /me/api-keys should 404', async () => {
|
|
||||||
await authAgent.post('/me/api-keys').expect(404);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GET /me/api-keys should 404', async () => {
|
|
||||||
await authAgent.get('/me/api-keys').expect(404);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('DELETE /me/api-key/:id should 404', async () => {
|
|
||||||
await authAgent.delete(`/me/api-keys/${1}`).expect(404);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Owner shell', () => {
|
describe('Owner shell', () => {
|
||||||
let ownerShell: User;
|
let ownerShell: User;
|
||||||
let authOwnerShellAgent: SuperAgentTest;
|
let authOwnerShellAgent: SuperAgentTest;
|
||||||
|
@ -156,58 +124,6 @@ describe('Owner shell', () => {
|
||||||
expect(storedShellOwner.personalizationAnswers).toEqual(validPayload);
|
expect(storedShellOwner.personalizationAnswers).toEqual(validPayload);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('POST /me/api-keys should create an api key', async () => {
|
|
||||||
const newApiKeyResponse = await authOwnerShellAgent.post('/me/api-keys');
|
|
||||||
|
|
||||||
const newApiKey = newApiKeyResponse.body.data as ApiKey;
|
|
||||||
|
|
||||||
expect(newApiKeyResponse.statusCode).toBe(200);
|
|
||||||
expect(newApiKey).toBeDefined();
|
|
||||||
|
|
||||||
const newStoredApiKey = await Container.get(ApiKeyRepository).findOneByOrFail({
|
|
||||||
userId: ownerShell.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(newStoredApiKey).toEqual({
|
|
||||||
id: expect.any(String),
|
|
||||||
label: 'My API Key',
|
|
||||||
userId: ownerShell.id,
|
|
||||||
apiKey: newApiKey.apiKey,
|
|
||||||
createdAt: expect.any(Date),
|
|
||||||
updatedAt: expect.any(Date),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GET /me/api-keys should fetch the api key redacted', async () => {
|
|
||||||
const newApiKeyResponse = await authOwnerShellAgent.post('/me/api-keys');
|
|
||||||
|
|
||||||
const retrieveAllApiKeysResponse = await authOwnerShellAgent.get('/me/api-keys');
|
|
||||||
|
|
||||||
expect(retrieveAllApiKeysResponse.statusCode).toBe(200);
|
|
||||||
|
|
||||||
expect(retrieveAllApiKeysResponse.body.data[0]).toEqual({
|
|
||||||
id: newApiKeyResponse.body.data.id,
|
|
||||||
label: 'My API Key',
|
|
||||||
userId: ownerShell.id,
|
|
||||||
apiKey: publicApiKeyService.redactApiKey(newApiKeyResponse.body.data.apiKey),
|
|
||||||
createdAt: expect.any(String),
|
|
||||||
updatedAt: expect.any(String),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('DELETE /me/api-keys/:id should delete the api key', async () => {
|
|
||||||
const newApiKeyResponse = await authOwnerShellAgent.post('/me/api-keys');
|
|
||||||
|
|
||||||
const deleteApiKeyResponse = await authOwnerShellAgent.delete(
|
|
||||||
`/me/api-keys/${newApiKeyResponse.body.data.id}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const retrieveAllApiKeysResponse = await authOwnerShellAgent.get('/me/api-keys');
|
|
||||||
|
|
||||||
expect(deleteApiKeyResponse.body.data.success).toBe(true);
|
|
||||||
expect(retrieveAllApiKeysResponse.body.data.length).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Member', () => {
|
describe('Member', () => {
|
||||||
|
@ -318,61 +234,6 @@ describe('Member', () => {
|
||||||
expect(storedAnswers).toEqual(validPayload);
|
expect(storedAnswers).toEqual(validPayload);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('POST /me/api-keys should create an api key', async () => {
|
|
||||||
const newApiKeyResponse = await testServer.authAgentFor(member).post('/me/api-keys');
|
|
||||||
|
|
||||||
expect(newApiKeyResponse.statusCode).toBe(200);
|
|
||||||
expect(newApiKeyResponse.body.data.apiKey).toBeDefined();
|
|
||||||
expect(newApiKeyResponse.body.data.apiKey).not.toBeNull();
|
|
||||||
|
|
||||||
const newStoredApiKey = await Container.get(ApiKeyRepository).findOneByOrFail({
|
|
||||||
userId: member.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(newStoredApiKey).toEqual({
|
|
||||||
id: expect.any(String),
|
|
||||||
label: 'My API Key',
|
|
||||||
userId: member.id,
|
|
||||||
apiKey: newApiKeyResponse.body.data.apiKey,
|
|
||||||
createdAt: expect.any(Date),
|
|
||||||
updatedAt: expect.any(Date),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('GET /me/api-keys should fetch the api key redacted', async () => {
|
|
||||||
const newApiKeyResponse = await testServer.authAgentFor(member).post('/me/api-keys');
|
|
||||||
|
|
||||||
const retrieveAllApiKeysResponse = await testServer.authAgentFor(member).get('/me/api-keys');
|
|
||||||
|
|
||||||
expect(retrieveAllApiKeysResponse.statusCode).toBe(200);
|
|
||||||
|
|
||||||
expect(retrieveAllApiKeysResponse.body.data[0]).toEqual({
|
|
||||||
id: newApiKeyResponse.body.data.id,
|
|
||||||
label: 'My API Key',
|
|
||||||
userId: member.id,
|
|
||||||
apiKey: publicApiKeyService.redactApiKey(newApiKeyResponse.body.data.apiKey),
|
|
||||||
createdAt: expect.any(String),
|
|
||||||
updatedAt: expect.any(String),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(newApiKeyResponse.body.data.apiKey).not.toEqual(
|
|
||||||
retrieveAllApiKeysResponse.body.data[0].apiKey,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('DELETE /me/api-keys/:id should delete the api key', async () => {
|
|
||||||
const newApiKeyResponse = await testServer.authAgentFor(member).post('/me/api-keys');
|
|
||||||
|
|
||||||
const deleteApiKeyResponse = await testServer
|
|
||||||
.authAgentFor(member)
|
|
||||||
.delete(`/me/api-keys/${newApiKeyResponse.body.data.id}`);
|
|
||||||
|
|
||||||
const retrieveAllApiKeysResponse = await testServer.authAgentFor(member).get('/me/api-keys');
|
|
||||||
|
|
||||||
expect(deleteApiKeyResponse.body.data.success).toBe(true);
|
|
||||||
expect(retrieveAllApiKeysResponse.body.data.length).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Owner', () => {
|
describe('Owner', () => {
|
||||||
|
|
|
@ -40,7 +40,8 @@ type EndpointGroup =
|
||||||
| 'debug'
|
| 'debug'
|
||||||
| 'project'
|
| 'project'
|
||||||
| 'role'
|
| 'role'
|
||||||
| 'dynamic-node-parameters';
|
| 'dynamic-node-parameters'
|
||||||
|
| 'apiKeys';
|
||||||
|
|
||||||
export interface SetupProps {
|
export interface SetupProps {
|
||||||
endpointGroups?: EndpointGroup[];
|
endpointGroups?: EndpointGroup[];
|
||||||
|
|
|
@ -273,6 +273,10 @@ export const setupTestServer = ({
|
||||||
case 'dynamic-node-parameters':
|
case 'dynamic-node-parameters':
|
||||||
await import('@/controllers/dynamic-node-parameters.controller');
|
await import('@/controllers/dynamic-node-parameters.controller');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'apiKeys':
|
||||||
|
await import('@/controllers/api-keys.controller');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,16 +2,16 @@ import type { ApiKey, IRestApiContext } from '@/Interface';
|
||||||
import { makeRestApiRequest } from '@/utils/apiUtils';
|
import { makeRestApiRequest } from '@/utils/apiUtils';
|
||||||
|
|
||||||
export async function getApiKeys(context: IRestApiContext): Promise<ApiKey[]> {
|
export async function getApiKeys(context: IRestApiContext): Promise<ApiKey[]> {
|
||||||
return await makeRestApiRequest(context, 'GET', '/me/api-keys');
|
return await makeRestApiRequest(context, 'GET', '/api-keys');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createApiKey(context: IRestApiContext): Promise<ApiKey> {
|
export async function createApiKey(context: IRestApiContext): Promise<ApiKey> {
|
||||||
return await makeRestApiRequest(context, 'POST', '/me/api-keys');
|
return await makeRestApiRequest(context, 'POST', '/api-keys');
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteApiKey(
|
export async function deleteApiKey(
|
||||||
context: IRestApiContext,
|
context: IRestApiContext,
|
||||||
id: string,
|
id: string,
|
||||||
): Promise<{ success: boolean }> {
|
): Promise<{ success: boolean }> {
|
||||||
return await makeRestApiRequest(context, 'DELETE', `/me/api-keys/${id}`);
|
return await makeRestApiRequest(context, 'DELETE', `/api-keys/${id}`);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue