From 616c1a92bcd7d2e1112bfe168cff2548024503de Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Tue, 8 Apr 2025 10:54:04 +0200 Subject: [PATCH 1/6] refactor: Move api token related files to ee --- .../{api-token.test.js.snap => api-token.ee.test.js.snap} | 0 packages/backend/src/models/{api-token.js => api-token.ee.js} | 0 .../src/models/{api-token.test.js => api-token.test.ee.js} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename packages/backend/src/models/__snapshots__/{api-token.test.js.snap => api-token.ee.test.js.snap} (100%) rename packages/backend/src/models/{api-token.js => api-token.ee.js} (100%) rename packages/backend/src/models/{api-token.test.js => api-token.test.ee.js} (100%) diff --git a/packages/backend/src/models/__snapshots__/api-token.test.js.snap b/packages/backend/src/models/__snapshots__/api-token.ee.test.js.snap similarity index 100% rename from packages/backend/src/models/__snapshots__/api-token.test.js.snap rename to packages/backend/src/models/__snapshots__/api-token.ee.test.js.snap diff --git a/packages/backend/src/models/api-token.js b/packages/backend/src/models/api-token.ee.js similarity index 100% rename from packages/backend/src/models/api-token.js rename to packages/backend/src/models/api-token.ee.js diff --git a/packages/backend/src/models/api-token.test.js b/packages/backend/src/models/api-token.test.ee.js similarity index 100% rename from packages/backend/src/models/api-token.test.js rename to packages/backend/src/models/api-token.test.ee.js From add8a44f404710ebf62e540d2b301f68aa7f69fc Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Tue, 8 Apr 2025 11:01:16 +0200 Subject: [PATCH 2/6] feat: Implement admin api token serializer --- .../src/serializers/admin/api-token.ee.js | 10 +++++++++ .../serializers/admin/api-token.ee.test.js | 22 +++++++++++++++++++ packages/backend/src/serializers/index.js | 1 + 3 files changed, 33 insertions(+) create mode 100644 packages/backend/src/serializers/admin/api-token.ee.js create mode 100644 packages/backend/src/serializers/admin/api-token.ee.test.js diff --git a/packages/backend/src/serializers/admin/api-token.ee.js b/packages/backend/src/serializers/admin/api-token.ee.js new file mode 100644 index 00000000..40c4c52b --- /dev/null +++ b/packages/backend/src/serializers/admin/api-token.ee.js @@ -0,0 +1,10 @@ +const adminApiTokenSerializer = (apiToken) => { + return { + id: apiToken.id, + token: apiToken.token, + createdAt: apiToken.createdAt.getTime(), + updatedAt: apiToken.updatedAt.getTime(), + }; +}; + +export default adminApiTokenSerializer; diff --git a/packages/backend/src/serializers/admin/api-token.ee.test.js b/packages/backend/src/serializers/admin/api-token.ee.test.js new file mode 100644 index 00000000..d66054d1 --- /dev/null +++ b/packages/backend/src/serializers/admin/api-token.ee.test.js @@ -0,0 +1,22 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import adminApiTokenSerializer from './api-token.ee.js'; +import { createApiToken } from '../../../test/factories/api-token.js'; + +describe('adminApiTokenSerializer', () => { + let apiToken; + + beforeEach(async () => { + apiToken = await createApiToken(); + }); + + it('should return flow data', async () => { + const expectedPayload = { + id: apiToken.id, + token: apiToken.token, + createdAt: apiToken.createdAt.getTime(), + updatedAt: apiToken.updatedAt.getTime(), + }; + + expect(adminApiTokenSerializer(apiToken)).toStrictEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/serializers/index.js b/packages/backend/src/serializers/index.js index 4cd59010..1a28c798 100644 --- a/packages/backend/src/serializers/index.js +++ b/packages/backend/src/serializers/index.js @@ -3,6 +3,7 @@ import roleSerializer from './role.js'; import permissionSerializer from './permission.js'; import adminSamlAuthProviderSerializer from './admin-saml-auth-provider.ee.js'; import adminTemplateSerializer from './admin/template.ee.js'; +import adminApiTokenSerializer from './admin/api-token.ee.js'; import templateSerializer from './template.ee.js'; import samlAuthProviderSerializer from './saml-auth-provider.ee.js'; import samlAuthProviderRoleMappingSerializer from './role-mapping.ee.js'; From 59510dcb0a3fcc275d4d46e1c1154727516da25d Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Tue, 8 Apr 2025 11:22:07 +0200 Subject: [PATCH 3/6] feat: Implement token generation logic on api tokens --- .../__snapshots__/api-token.ee.test.js.snap | 3 -- packages/backend/src/models/api-token.ee.js | 12 +++++-- .../backend/src/models/api-token.ee.test.js | 31 +++++++++++++++++++ .../backend/src/models/api-token.test.ee.js | 12 ------- packages/backend/test/factories/api-token.js | 2 +- 5 files changed, 42 insertions(+), 18 deletions(-) create mode 100644 packages/backend/src/models/api-token.ee.test.js delete mode 100644 packages/backend/src/models/api-token.test.ee.js diff --git a/packages/backend/src/models/__snapshots__/api-token.ee.test.js.snap b/packages/backend/src/models/__snapshots__/api-token.ee.test.js.snap index 6f52bfc8..4fe18729 100644 --- a/packages/backend/src/models/__snapshots__/api-token.ee.test.js.snap +++ b/packages/backend/src/models/__snapshots__/api-token.ee.test.js.snap @@ -12,9 +12,6 @@ exports[`ApiToken model > jsonSchema should have correct validations 1`] = ` "type": "string", }, }, - "required": [ - "token", - ], "type": "object", } `; diff --git a/packages/backend/src/models/api-token.ee.js b/packages/backend/src/models/api-token.ee.js index f5a520d1..aab7506f 100644 --- a/packages/backend/src/models/api-token.ee.js +++ b/packages/backend/src/models/api-token.ee.js @@ -1,17 +1,25 @@ import Base from './base.js'; - +import crypto from 'crypto'; class ApiToken extends Base { static tableName = 'api_tokens'; static jsonSchema = { type: 'object', - required: ['token'], properties: { id: { type: 'string', format: 'uuid' }, token: { type: 'string', minLength: 32 }, }, }; + + async assignToken() { + this.token = crypto.randomBytes(48).toString('hex'); + } + + async $beforeInsert(queryContext) { + await super.$beforeInsert(queryContext); + await this.assignToken(); + } } export default ApiToken; diff --git a/packages/backend/src/models/api-token.ee.test.js b/packages/backend/src/models/api-token.ee.test.js new file mode 100644 index 00000000..c28549f9 --- /dev/null +++ b/packages/backend/src/models/api-token.ee.test.js @@ -0,0 +1,31 @@ +import { describe, it, expect, vi } from 'vitest'; +import ApiToken from './api-token.ee.js'; + +describe('ApiToken model', () => { + it('tableName should return correct name', () => { + expect(ApiToken.tableName).toBe('api_tokens'); + }); + + it('jsonSchema should have correct validations', () => { + expect(ApiToken.jsonSchema).toMatchSnapshot(); + }); + + describe('assignToken', () => { + it('should assign a new token', async () => { + const apiToken = new ApiToken(); + await apiToken.assignToken(); + + expect(apiToken.token).toBeDefined(); + }); + }); + + describe('beforeInsert', () => { + it('should call assignToken method', async () => { + const apiToken = new ApiToken(); + const assignTokenSpy = vi.spyOn(apiToken, 'assignToken'); + + await apiToken.$beforeInsert(); + expect(assignTokenSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/backend/src/models/api-token.test.ee.js b/packages/backend/src/models/api-token.test.ee.js deleted file mode 100644 index 0fcbb806..00000000 --- a/packages/backend/src/models/api-token.test.ee.js +++ /dev/null @@ -1,12 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import ApiToken from './api-token.js'; - -describe('ApiToken model', () => { - it('tableName should return correct name', () => { - expect(ApiToken.tableName).toBe('api_tokens'); - }); - - it('jsonSchema should have correct validations', () => { - expect(ApiToken.jsonSchema).toMatchSnapshot(); - }); -}); diff --git a/packages/backend/test/factories/api-token.js b/packages/backend/test/factories/api-token.js index 7eaac70c..f283e0a3 100644 --- a/packages/backend/test/factories/api-token.js +++ b/packages/backend/test/factories/api-token.js @@ -1,5 +1,5 @@ import crypto from 'crypto'; -import ApiToken from '../../src/models/api-token.js'; +import ApiToken from '../../src/models/api-token.ee.js'; export const createApiToken = async (params = {}) => { params.token = params.token || crypto.randomBytes(48).toString('hex'); From 540525d6fbb12b84cf1aca9dd743620759cf79e2 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Tue, 8 Apr 2025 11:34:45 +0200 Subject: [PATCH 4/6] feat: Implement create API Token endpoint --- .../admin/api-tokens/create-api-token.ee.js | 11 ++++++ .../api-tokens/create-api-token.ee.test.js | 37 +++++++++++++++++++ packages/backend/src/models/api-token.ee.js | 2 + .../src/routes/api/v1/admin/api-tokens.ee.js | 17 +++++++++ packages/backend/src/routes/index.js | 2 + packages/backend/src/serializers/index.js | 1 + .../v1/admin/api-tokens/create-api-token.js | 21 +++++++++++ 7 files changed, 91 insertions(+) create mode 100644 packages/backend/src/controllers/api/v1/admin/api-tokens/create-api-token.ee.js create mode 100644 packages/backend/src/controllers/api/v1/admin/api-tokens/create-api-token.ee.test.js create mode 100644 packages/backend/src/routes/api/v1/admin/api-tokens.ee.js create mode 100644 packages/backend/test/mocks/rest/api/v1/admin/api-tokens/create-api-token.js diff --git a/packages/backend/src/controllers/api/v1/admin/api-tokens/create-api-token.ee.js b/packages/backend/src/controllers/api/v1/admin/api-tokens/create-api-token.ee.js new file mode 100644 index 00000000..1a763c78 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/api-tokens/create-api-token.ee.js @@ -0,0 +1,11 @@ +import { renderObject } from '../../../../../helpers/renderer.js'; +import ApiToken from '../../../../../models/api-token.ee.js'; + +export default async (request, response) => { + const apiToken = await ApiToken.query().insertAndFetch({}); + + renderObject(response, apiToken, { + serializer: 'AdminApiToken', + status: 201, + }); +}; diff --git a/packages/backend/src/controllers/api/v1/admin/api-tokens/create-api-token.ee.test.js b/packages/backend/src/controllers/api/v1/admin/api-tokens/create-api-token.ee.test.js new file mode 100644 index 00000000..7f2a735e --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/api-tokens/create-api-token.ee.test.js @@ -0,0 +1,37 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../../app.js'; +import ApiToken from '../../../../../models/api-token.ee.js'; +import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js'; +import { createRole } from '../../../../../../test/factories/role.js'; +import { createUser } from '../../../../../../test/factories/user.js'; +import createApiTokenMock from '../../../../../../test/mocks/rest/api/v1/admin/api-tokens/create-api-token.js'; +import * as license from '../../../../../helpers/license.ee.js'; + +describe('POST /api/v1/admin/api-tokens', () => { + let currentUser, token, role; + + beforeEach(async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + role = await createRole({ name: 'Admin' }); + currentUser = await createUser({ roleId: role.id }); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return the created api token', async () => { + const response = await request(app) + .post('/api/v1/admin/api-tokens') + .set('Authorization', token) + .expect(201); + + const refetchedToken = await ApiToken.query().findById( + response.body.data.id + ); + + const expectedPayload = await createApiTokenMock(refetchedToken); + + expect(response.body).toStrictEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/models/api-token.ee.js b/packages/backend/src/models/api-token.ee.js index aab7506f..9a38cdcb 100644 --- a/packages/backend/src/models/api-token.ee.js +++ b/packages/backend/src/models/api-token.ee.js @@ -9,6 +9,8 @@ class ApiToken extends Base { properties: { id: { type: 'string', format: 'uuid' }, token: { type: 'string', minLength: 32 }, + createdAt: { type: 'string' }, + updatedAt: { type: 'string' }, }, }; diff --git a/packages/backend/src/routes/api/v1/admin/api-tokens.ee.js b/packages/backend/src/routes/api/v1/admin/api-tokens.ee.js new file mode 100644 index 00000000..9c2e9276 --- /dev/null +++ b/packages/backend/src/routes/api/v1/admin/api-tokens.ee.js @@ -0,0 +1,17 @@ +import { Router } from 'express'; +import { authenticateUser } from '../../../../helpers/authentication.js'; +import { authorizeAdmin } from '../../../../helpers/authorization.js'; +import { checkIsEnterprise } from '../../../../helpers/check-is-enterprise.js'; +import createApiTokenAction from '../../../../controllers/api/v1/admin/api-tokens/create-api-token.ee.js'; + +const router = Router(); + +router.post( + '/', + authenticateUser, + authorizeAdmin, + checkIsEnterprise, + createApiTokenAction +); + +export default router; diff --git a/packages/backend/src/routes/index.js b/packages/backend/src/routes/index.js index 13634f77..5cfa3b6e 100644 --- a/packages/backend/src/routes/index.js +++ b/packages/backend/src/routes/index.js @@ -16,6 +16,7 @@ import adminAppsRouter from './api/v1/admin/apps.ee.js'; import adminConfigRouter from './api/v1/admin/config.ee.js'; import adminSamlAuthProvidersRouter from './api/v1/admin/saml-auth-providers.ee.js'; import adminTemplatesRouter from './api/v1/admin/templates.ee.js'; +import adminApiTokensRouter from './api/v1/admin/api-tokens.ee.js'; import templatesRouter from './api/v1/templates.ee.js'; import rolesRouter from './api/v1/admin/roles.ee.js'; import permissionsRouter from './api/v1/admin/permissions.ee.js'; @@ -45,6 +46,7 @@ router.use('/api/v1/admin/roles', rolesRouter); router.use('/api/v1/admin/permissions', permissionsRouter); router.use('/api/v1/admin/saml-auth-providers', adminSamlAuthProvidersRouter); router.use('/api/v1/admin/templates', adminTemplatesRouter); +router.use('/api/v1/admin/api-tokens', adminApiTokensRouter); router.use('/api/v1/templates', templatesRouter); router.use('/api/v1/installation/users', installationUsersRouter); router.use('/api/v1/folders', foldersRouter); diff --git a/packages/backend/src/serializers/index.js b/packages/backend/src/serializers/index.js index 1a28c798..96dbebeb 100644 --- a/packages/backend/src/serializers/index.js +++ b/packages/backend/src/serializers/index.js @@ -31,6 +31,7 @@ const serializers = { Permission: permissionSerializer, AdminSamlAuthProvider: adminSamlAuthProviderSerializer, AdminTemplate: adminTemplateSerializer, + AdminApiToken: adminApiTokenSerializer, Template: templateSerializer, SamlAuthProvider: samlAuthProviderSerializer, RoleMapping: samlAuthProviderRoleMappingSerializer, diff --git a/packages/backend/test/mocks/rest/api/v1/admin/api-tokens/create-api-token.js b/packages/backend/test/mocks/rest/api/v1/admin/api-tokens/create-api-token.js new file mode 100644 index 00000000..1bd0f310 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/admin/api-tokens/create-api-token.js @@ -0,0 +1,21 @@ +const createApiTokenMock = async (apiToken) => { + const data = { + id: apiToken.id, + token: apiToken.token, + createdAt: apiToken.createdAt.getTime(), + updatedAt: apiToken.updatedAt.getTime(), + }; + + return { + data: data, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'ApiToken', + }, + }; +}; + +export default createApiTokenMock; From 1162a52605b60db40917ea0e15374584d55d56ab Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Wed, 9 Apr 2025 11:51:05 +0200 Subject: [PATCH 5/6] fix: Api token serializer test description --- packages/backend/src/serializers/admin/api-token.ee.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/serializers/admin/api-token.ee.test.js b/packages/backend/src/serializers/admin/api-token.ee.test.js index d66054d1..9ca7fd40 100644 --- a/packages/backend/src/serializers/admin/api-token.ee.test.js +++ b/packages/backend/src/serializers/admin/api-token.ee.test.js @@ -9,7 +9,7 @@ describe('adminApiTokenSerializer', () => { apiToken = await createApiToken(); }); - it('should return flow data', async () => { + it('should return api token data', async () => { const expectedPayload = { id: apiToken.id, token: apiToken.token, From 6f39be703903ee4b1f566ef54c576c3f9db362e5 Mon Sep 17 00:00:00 2001 From: Faruk AYDIN Date: Wed, 9 Apr 2025 11:53:54 +0200 Subject: [PATCH 6/6] fix: Update api token snapshot for model tests --- .../src/models/__snapshots__/api-token.ee.test.js.snap | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/backend/src/models/__snapshots__/api-token.ee.test.js.snap b/packages/backend/src/models/__snapshots__/api-token.ee.test.js.snap index 4fe18729..19860d3b 100644 --- a/packages/backend/src/models/__snapshots__/api-token.ee.test.js.snap +++ b/packages/backend/src/models/__snapshots__/api-token.ee.test.js.snap @@ -3,6 +3,9 @@ exports[`ApiToken model > jsonSchema should have correct validations 1`] = ` { "properties": { + "createdAt": { + "type": "string", + }, "id": { "format": "uuid", "type": "string", @@ -11,6 +14,9 @@ exports[`ApiToken model > jsonSchema should have correct validations 1`] = ` "minLength": 32, "type": "string", }, + "updatedAt": { + "type": "string", + }, }, "type": "object", }