diff --git a/packages/backend/src/controllers/api/v1/user-invitations/get-user-invitations.ee.js b/packages/backend/src/controllers/api/v1/user-invitations/get-user-invitations.ee.js new file mode 100644 index 00000000..7db4e618 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/user-invitations/get-user-invitations.ee.js @@ -0,0 +1,18 @@ +import paginateRest from '../../../../helpers/pagination.js'; +import { renderObject } from '../../../../helpers/renderer.js'; +import User from '../../../../models/user.js'; + +export default async (request, response) => { + const usersQuery = User.query() + .withGraphFetched({ + role: true, + }) + .where({ + status: 'invited', + }) + .orderBy('full_name', 'asc'); + + const users = await paginateRest(usersQuery, request.query.page); + + renderObject(response, users, { serializer: 'PublicUserInvitation' }); +}; diff --git a/packages/backend/src/controllers/api/v1/user-invitations/get-user-invitations.ee.test.js b/packages/backend/src/controllers/api/v1/user-invitations/get-user-invitations.ee.test.js new file mode 100644 index 00000000..d687e5da --- /dev/null +++ b/packages/backend/src/controllers/api/v1/user-invitations/get-user-invitations.ee.test.js @@ -0,0 +1,50 @@ +import request from 'supertest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { createApiToken } from '../../../../../test/factories/api-token.js'; +import { createRole } from '../../../../../test/factories/role.js'; +import { createUser } from '../../../../../test/factories/user.js'; +import getUserInvitationsMock from '../../../../../test/mocks/rest/api/v1/user-invitations/get-user-invitations.js'; +import app from '../../../../app.js'; +import * as license from '../../../../helpers/license.ee.js'; + +describe('GET /api/v1/user-invitations', () => { + let userOne, userOneRole, userTwo, userTwoRole, token; + + beforeEach(async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + userOneRole = await createRole({ name: 'Admin' }); + + userOne = await createUser({ + roleId: userOneRole.id, + fullName: 'User 1', + status: 'invited', + }); + + userTwoRole = await createRole({ + name: 'Another user role', + }); + + userTwo = await createUser({ + roleId: userTwoRole.id, + fullName: 'User 2', + status: 'invited', + }); + + token = (await createApiToken()).token; + }); + + it('should return user invitations data', async () => { + const response = await request(app) + .get('/api/v1/user-invitations') + .set('x-api-token', token) + .expect(200); + + const expectedResponsePayload = await getUserInvitationsMock( + [userOne, userTwo], + [userOneRole, userTwoRole] + ); + + expect(response.body).toStrictEqual(expectedResponsePayload); + }); +}); diff --git a/packages/backend/src/routes/api/index.js b/packages/backend/src/routes/api/index.js index 50303493..65afba12 100644 --- a/packages/backend/src/routes/api/index.js +++ b/packages/backend/src/routes/api/index.js @@ -3,16 +3,18 @@ import appsRouter from './v1/apps.ee.js'; import executionsRouter from './v1/executions.ee.js'; import flowsRouter from './v1/flows.ee.js'; import foldersRouter from './v1/folders.ee.js'; -import usersRouter from './v1/users.ee.js'; import templatesRouter from './v1/templates.ee.js'; +import usersRouter from './v1/users.ee.js'; +import userInvitationsRouter from './v1/user-invitations.ee.js'; const router = Router(); router.use('/v1/apps', appsRouter); -router.use('/v1/flows', flowsRouter); router.use('/v1/executions', executionsRouter); +router.use('/v1/flows', flowsRouter); router.use('/v1/folders', foldersRouter); -router.use('/v1/users', usersRouter); router.use('/v1/templates', templatesRouter); +router.use('/v1/users', usersRouter); +router.use('/v1/user-invitations', userInvitationsRouter); export default router; diff --git a/packages/backend/src/routes/api/v1/user-invitations.ee.js b/packages/backend/src/routes/api/v1/user-invitations.ee.js new file mode 100644 index 00000000..d5dd151e --- /dev/null +++ b/packages/backend/src/routes/api/v1/user-invitations.ee.js @@ -0,0 +1,8 @@ +import { Router } from 'express'; +import getUserInvitationsAction from '../../../controllers/api/v1/user-invitations/get-user-invitations.ee.js'; + +const router = Router(); + +router.get('/', getUserInvitationsAction); + +export default router; diff --git a/packages/backend/src/serializers/index.js b/packages/backend/src/serializers/index.js index cec30d64..2966ff3f 100644 --- a/packages/backend/src/serializers/index.js +++ b/packages/backend/src/serializers/index.js @@ -16,6 +16,7 @@ import folderSerializer from './folder.js'; import oauthClientSerializer from './oauth-client.js'; import permissionSerializer from './permission.js'; import publicTemplateSerializer from './public-template.ee.js'; +import publicUserInvitationSerializer from './public-user-invitation.ee.js'; import samlAuthProviderRoleMappingSerializer from './role-mapping.ee.js'; import roleSerializer from './role.js'; import samlAuthProviderSerializer from './saml-auth-provider.ee.js'; @@ -45,6 +46,7 @@ const serializers = { OAuthClient: oauthClientSerializer, Permission: permissionSerializer, PublicTemplate: publicTemplateSerializer, + PublicUserInvitation: publicUserInvitationSerializer, Role: roleSerializer, RoleMapping: samlAuthProviderRoleMappingSerializer, SamlAuthProvider: samlAuthProviderSerializer, diff --git a/packages/backend/src/serializers/public-user-invitation.ee.js b/packages/backend/src/serializers/public-user-invitation.ee.js new file mode 100644 index 00000000..113837b7 --- /dev/null +++ b/packages/backend/src/serializers/public-user-invitation.ee.js @@ -0,0 +1,20 @@ +import roleSerializer from './role.js'; + +const publicUserInvitationSerializer = (user) => { + let userData = { + id: user.id, + email: user.email, + createdAt: user.createdAt.getTime(), + updatedAt: user.updatedAt.getTime(), + status: user.status, + fullName: user.fullName, + }; + + if (user.role) { + userData.role = roleSerializer(user.role); + } + + return userData; +}; + +export default publicUserInvitationSerializer; diff --git a/packages/backend/src/serializers/public-user-invitation.ee.test.js b/packages/backend/src/serializers/public-user-invitation.ee.test.js new file mode 100644 index 00000000..41e3019a --- /dev/null +++ b/packages/backend/src/serializers/public-user-invitation.ee.test.js @@ -0,0 +1,39 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import appConfig from '../config/app.js'; +import { createUser } from '../../test/factories/user.js'; +import publicUserInvitationSerializer from './public-user-invitation.ee.js'; +import roleSerializer from './role.js'; + +describe('publicUserInvitation', () => { + let user, role; + + beforeEach(async () => { + user = await createUser(); + role = await user.$relatedQuery('role'); + }); + + it('should return user data', async () => { + vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false); + + const expectedPayload = { + createdAt: user.createdAt.getTime(), + email: user.email, + fullName: user.fullName, + id: user.id, + status: user.status, + updatedAt: user.updatedAt.getTime(), + }; + + expect(publicUserInvitationSerializer(user)).toStrictEqual(expectedPayload); + }); + + it('should return user data with the role', async () => { + user.role = role; + + const expectedPayload = { + role: roleSerializer(role), + }; + + expect(publicUserInvitationSerializer(user)).toMatchObject(expectedPayload); + }); +}); diff --git a/packages/backend/test/mocks/rest/api/v1/user-invitations/get-user-invitations.js b/packages/backend/test/mocks/rest/api/v1/user-invitations/get-user-invitations.js new file mode 100644 index 00000000..7c069084 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/user-invitations/get-user-invitations.js @@ -0,0 +1,37 @@ +const getUserInvitationsAction = async (users, roles) => { + const data = users.map((user) => { + const role = roles.find((r) => r.id === user.roleId); + + return { + createdAt: user.createdAt.getTime(), + email: user.email, + fullName: user.fullName, + id: user.id, + role: role + ? { + createdAt: role.createdAt.getTime(), + description: role.description, + id: role.id, + isAdmin: role.isAdmin, + name: role.name, + updatedAt: role.updatedAt.getTime(), + } + : null, + status: user.status, + updatedAt: user.updatedAt.getTime(), + }; + }); + + return { + data: data, + meta: { + count: data.length, + currentPage: 1, + isArray: true, + totalPages: 1, + type: 'User', + }, + }; +}; + +export default getUserInvitationsAction;