diff --git a/packages/cli/src/controllers/__tests__/api-keys.controller.test.ts b/packages/cli/src/controllers/__tests__/api-keys.controller.test.ts new file mode 100644 index 0000000000..81025fb2ca --- /dev/null +++ b/packages/cli/src/controllers/__tests__/api-keys.controller.test.ts @@ -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({ user: mock({ 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({ + id: '123', + password: 'password', + authIdentities: [], + role: 'global:member', + mfaEnabled: false, + }); + const req = mock({ user, params: { id: user.id } }); + await controller.deleteAPIKey(req); + expect(apiKeysRepository.delete).toHaveBeenCalledWith({ + userId: req.user.id, + id: req.params.id, + }); + }); + }); +}); diff --git a/packages/cli/src/controllers/__tests__/me.controller.test.ts b/packages/cli/src/controllers/__tests__/me.controller.test.ts index 7f5f861b0e..37c391a2dc 100644 --- a/packages/cli/src/controllers/__tests__/me.controller.test.ts +++ b/packages/cli/src/controllers/__tests__/me.controller.test.ts @@ -2,14 +2,11 @@ import { UserUpdateRequestDto } from '@n8n/api-types'; import type { Response } from 'express'; import { mock, anyObject } from 'jest-mock-extended'; import jwt from 'jsonwebtoken'; -import { randomString } from 'n8n-workflow'; import { Container } from 'typedi'; import { AUTH_COOKIE_NAME } from '@/constants'; import { MeController } from '@/controllers/me.controller'; -import type { ApiKey } from '@/databases/entities/api-key'; import type { User } from '@/databases/entities/user'; -import { ApiKeyRepository } from '@/databases/repositories/api-key.repository'; import { AuthUserRepository } from '@/databases/repositories/auth-user.repository'; import { InvalidAuthTokenRepository } from '@/databases/repositories/invalid-auth-token.repository'; import { UserRepository } from '@/databases/repositories/user.repository'; @@ -21,7 +18,6 @@ import type { PublicUser } from '@/interfaces'; import { License } from '@/license'; import { MfaService } from '@/mfa/mfa.service'; import type { AuthenticatedRequest, MeRequest } from '@/requests'; -import { API_KEY_PREFIX } from '@/services/public-api-key.service'; import { UserService } from '@/services/user.service'; import { mockInstance } from '@test/mocking'; import { badPasswords } from '@test/test-data'; @@ -34,7 +30,6 @@ describe('MeController', () => { const userService = mockInstance(UserService); const userRepository = mockInstance(UserRepository); const mockMfaService = mockInstance(MfaService); - const apiKeysRepository = mockInstance(ApiKeyRepository); mockInstance(AuthUserRepository); mockInstance(InvalidAuthTokenRepository); mockInstance(License).isWithinUsersLimit.mockReturnValue(true); @@ -413,68 +408,4 @@ describe('MeController', () => { await expect(controller.storeSurveyAnswers(req)).rejects.toThrowError(BadRequestError); }); }); - - describe('API Key methods', () => { - let req: AuthenticatedRequest; - beforeAll(() => { - req = mock({ user: mock({ 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({ - id: '123', - password: 'password', - authIdentities: [], - role: 'global:member', - mfaEnabled: false, - }); - const req = mock({ user, params: { id: user.id } }); - await controller.deleteAPIKey(req); - expect(apiKeysRepository.delete).toHaveBeenCalledWith({ - userId: req.user.id, - id: req.params.id, - }); - }); - }); - }); }); diff --git a/packages/cli/src/controllers/api-keys.controller.ts b/packages/cli/src/controllers/api-keys.controller.ts new file mode 100644 index 0000000000..db53a00449 --- /dev/null +++ b/packages/cli/src/controllers/api-keys.controller.ts @@ -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 }; + } +} diff --git a/packages/cli/src/controllers/me.controller.ts b/packages/cli/src/controllers/me.controller.ts index aac1b48833..b0d97bfdf2 100644 --- a/packages/cli/src/controllers/me.controller.ts +++ b/packages/cli/src/controllers/me.controller.ts @@ -4,12 +4,12 @@ import { UserUpdateRequestDto, } from '@n8n/api-types'; import { plainToInstance } from 'class-transformer'; -import { type RequestHandler, Response } from 'express'; +import { Response } from 'express'; import { AuthService } from '@/auth/auth.service'; import type { User } from '@/databases/entities/user'; 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 { InvalidMfaCodeError } from '@/errors/response-errors/invalid-mfa-code.error'; import { EventService } from '@/events/event.service'; @@ -18,23 +18,12 @@ import { validateEntity } from '@/generic-helpers'; import type { PublicUser } from '@/interfaces'; import { Logger } from '@/logger'; import { MfaService } from '@/mfa/mfa.service'; -import { isApiEnabled } from '@/public-api'; import { AuthenticatedRequest, MeRequest } from '@/requests'; import { PasswordUtility } from '@/services/password.utility'; -import { PublicApiKeyService } from '@/services/public-api-key.service'; import { UserService } from '@/services/user.service'; import { isSamlLicensedAndEnabled } from '@/sso/saml/saml-helpers'; import { PersonalizationSurveyAnswersV4 } from './survey-answers.dto'; - -export const isApiEnabledMiddleware: RequestHandler = (_, res, next) => { - if (isApiEnabled()) { - next(); - } else { - res.status(404).end(); - } -}; - @RestController('/me') export class MeController { constructor( @@ -46,7 +35,6 @@ export class MeController { private readonly userRepository: UserRepository, private readonly eventService: EventService, private readonly mfaService: MfaService, - private readonly publicApiKeyService: PublicApiKeyService, ) {} /** @@ -217,39 +205,6 @@ export class MeController { 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. */ diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index ab4c32ad19..b8fa4b99ca 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -180,13 +180,20 @@ export declare namespace CredentialRequest { >; } +// ---------------------------------- +// /api-keys +// ---------------------------------- + +export declare namespace ApiKeysRequest { + export type DeleteAPIKey = AuthenticatedRequest<{ id: string }>; +} + // ---------------------------------- // /me // ---------------------------------- export declare namespace MeRequest { export type SurveyAnswers = AuthenticatedRequest<{}, {}, IPersonalizationSurveyAnswersV4>; - export type DeleteAPIKey = AuthenticatedRequest<{ id: string }>; } export interface UserSetupPayload { diff --git a/packages/cli/src/server.ts b/packages/cli/src/server.ts index eeb6cdae46..b83e2bdb2a 100644 --- a/packages/cli/src/server.ts +++ b/packages/cli/src/server.ts @@ -56,6 +56,7 @@ import '@/controllers/translation.controller'; import '@/controllers/users.controller'; import '@/controllers/user-settings.controller'; import '@/controllers/workflow-statistics.controller'; +import '@/controllers/api-keys.controller'; import '@/credentials/credentials.controller'; import '@/eventbus/event-bus.controller'; import '@/events/events.controller'; diff --git a/packages/cli/test/integration/api-keys.api.test.ts b/packages/cli/test/integration/api-keys.api.test.ts new file mode 100644 index 0000000000..f577e0cf78 --- /dev/null +++ b/packages/cli/test/integration/api-keys.api.test.ts @@ -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); + }); +}); diff --git a/packages/cli/test/integration/me.api.test.ts b/packages/cli/test/integration/me.api.test.ts index 2fc9b07870..a29f158a32 100644 --- a/packages/cli/test/integration/me.api.test.ts +++ b/packages/cli/test/integration/me.api.test.ts @@ -3,57 +3,25 @@ import type { IPersonalizationSurveyAnswersV4 } from 'n8n-workflow'; import { Container } from 'typedi'; import validator from 'validator'; -import type { ApiKey } from '@/databases/entities/api-key'; import type { User } from '@/databases/entities/user'; -import { ApiKeyRepository } from '@/databases/repositories/api-key.repository'; import { ProjectRepository } from '@/databases/repositories/project.repository'; import { UserRepository } from '@/databases/repositories/user.repository'; -import { PublicApiKeyService } from '@/services/public-api-key.service'; import { mockInstance } from '@test/mocking'; 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 * as testDb from './shared/test-db'; import type { SuperAgentTest } from './shared/types'; import * as utils from './shared/utils/'; const testServer = utils.setupTestServer({ endpointGroups: ['me'] }); -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 /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', () => { let ownerShell: User; let authOwnerShellAgent: SuperAgentTest; @@ -156,58 +124,6 @@ describe('Owner shell', () => { 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', () => { @@ -318,61 +234,6 @@ describe('Member', () => { 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', () => { diff --git a/packages/cli/test/integration/shared/types.ts b/packages/cli/test/integration/shared/types.ts index 87f349fb79..8dc922dda2 100644 --- a/packages/cli/test/integration/shared/types.ts +++ b/packages/cli/test/integration/shared/types.ts @@ -40,7 +40,8 @@ type EndpointGroup = | 'debug' | 'project' | 'role' - | 'dynamic-node-parameters'; + | 'dynamic-node-parameters' + | 'apiKeys'; export interface SetupProps { endpointGroups?: EndpointGroup[]; diff --git a/packages/cli/test/integration/shared/utils/test-server.ts b/packages/cli/test/integration/shared/utils/test-server.ts index cb66b7868d..c0dcd0ae4c 100644 --- a/packages/cli/test/integration/shared/utils/test-server.ts +++ b/packages/cli/test/integration/shared/utils/test-server.ts @@ -273,6 +273,10 @@ export const setupTestServer = ({ case 'dynamic-node-parameters': await import('@/controllers/dynamic-node-parameters.controller'); break; + + case 'apiKeys': + await import('@/controllers/api-keys.controller'); + break; } } diff --git a/packages/editor-ui/src/api/api-keys.ts b/packages/editor-ui/src/api/api-keys.ts index 7fdd201b4a..b4b44c8e13 100644 --- a/packages/editor-ui/src/api/api-keys.ts +++ b/packages/editor-ui/src/api/api-keys.ts @@ -2,16 +2,16 @@ import type { ApiKey, IRestApiContext } from '@/Interface'; import { makeRestApiRequest } from '@/utils/apiUtils'; export async function getApiKeys(context: IRestApiContext): Promise { - return await makeRestApiRequest(context, 'GET', '/me/api-keys'); + return await makeRestApiRequest(context, 'GET', '/api-keys'); } export async function createApiKey(context: IRestApiContext): Promise { - return await makeRestApiRequest(context, 'POST', '/me/api-keys'); + return await makeRestApiRequest(context, 'POST', '/api-keys'); } export async function deleteApiKey( context: IRestApiContext, id: string, ): Promise<{ success: boolean }> { - return await makeRestApiRequest(context, 'DELETE', `/me/api-keys/${id}`); + return await makeRestApiRequest(context, 'DELETE', `/api-keys/${id}`); }