diff --git a/apps/api/package.json b/apps/api/package.json index ae4d4de6..4b4ed67e 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -47,12 +47,12 @@ "passport-gitlab2": "^5.0.0", "passport-google-oauth20": "^2.0.0", "redis": "^4.6.13", - "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "socket.io": "^4.7.5", "uuid": "^9.0.1" }, "devDependencies": { + "reflect-metadata": "^0.2.2", "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 9173970e..2f492558 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -17,7 +17,6 @@ import { WorkspaceRoleModule } from '../workspace-role/workspace-role.module' import { ApiKeyGuard } from '../auth/guard/api-key/api-key.guard' import { EventModule } from '../event/event.module' import { VariableModule } from '../variable/variable.module' -import { ApprovalModule } from '../approval/approval.module' import { SocketModule } from '../socket/socket.module' import { ProviderModule } from '../provider/provider.module' import { ScheduleModule } from '@nestjs/schedule' @@ -51,7 +50,6 @@ import { FeedbackModule } from '../feedback/feedback.module' WorkspaceRoleModule, EventModule, VariableModule, - ApprovalModule, SocketModule, ProviderModule, IntegrationModule, diff --git a/apps/api/src/approval/approval.e2e.spec.ts b/apps/api/src/approval/approval.e2e.spec.ts deleted file mode 100644 index ae89d4af..00000000 --- a/apps/api/src/approval/approval.e2e.spec.ts +++ /dev/null @@ -1,2118 +0,0 @@ -import { - FastifyAdapter, - NestFastifyApplication -} from '@nestjs/platform-fastify' -import { Test } from '@nestjs/testing' -import { AppModule } from '../app/app.module' -import { EnvironmentModule } from '../environment/environment.module' -import { PrismaService } from '../prisma/prisma.service' -import { ProjectModule } from '../project/project.module' -import { SecretModule } from '../secret/secret.module' -import { WorkspaceModule } from '../workspace/workspace.module' -import { ApprovalModule } from './approval.module' -import { MAIL_SERVICE } from '../mail/services/interface.service' -import { MockMailService } from '../mail/services/mock.service' -import { ProjectService } from '../project/service/project.service' -import { WorkspaceService } from '../workspace/service/workspace.service' -import { EnvironmentService } from '../environment/service/environment.service' -import { SecretService } from '../secret/service/secret.service' -import cleanUp from '../common/cleanup' -import { v4 } from 'uuid' -import { - Approval, - ApprovalAction, - ApprovalItemType, - ApprovalStatus, - Authority, - Environment, - EventSeverity, - EventSource, - EventTriggerer, - EventType, - Project, - Secret, - User, - Variable, - Workspace -} from '@prisma/client' -import { VariableService } from '../variable/service/variable.service' -import { VariableModule } from '../variable/variable.module' -import { UserModule } from '../user/user.module' -import { WorkspaceRoleService } from '../workspace-role/service/workspace-role.service' -import { WorkspaceRoleModule } from '../workspace-role/workspace-role.module' -import { EventService } from '../event/service/event.service' -import { EventModule } from '../event/event.module' -import fetchEvents from '../common/fetch-events' - -describe('Approval Controller Tests', () => { - let app: NestFastifyApplication - let prisma: PrismaService - - let projectService: ProjectService - let workspaceService: WorkspaceService - let environmentService: EnvironmentService - let secretService: SecretService - let variableService: VariableService - let workspaceRoleService: WorkspaceRoleService - let eventService: EventService - - let workspace1: Workspace - let project1: Project - let environment1: Environment - let variable1: Variable - let secret1: Secret - - let user1: User, user2: User, user3: User - - beforeAll(async () => { - const moduleRef = await Test.createTestingModule({ - imports: [ - AppModule, - UserModule, - ApprovalModule, - WorkspaceModule, - ProjectModule, - EnvironmentModule, - SecretModule, - VariableModule, - WorkspaceRoleModule, - EventModule - ] - }) - .overrideProvider(MAIL_SERVICE) - .useClass(MockMailService) - .compile() - - app = moduleRef.createNestApplication( - new FastifyAdapter() - ) - prisma = moduleRef.get(PrismaService) - projectService = moduleRef.get(ProjectService) - workspaceService = moduleRef.get(WorkspaceService) - environmentService = moduleRef.get(EnvironmentService) - secretService = moduleRef.get(SecretService) - variableService = moduleRef.get(VariableService) - workspaceRoleService = moduleRef.get(WorkspaceRoleService) - eventService = moduleRef.get(EventService) - - await app.init() - await app.getHttpAdapter().getInstance().ready() - - await cleanUp(prisma) - - const user1Id = v4(), - user2Id = v4() - - user1 = await prisma.user.create({ - data: { - id: user1Id, - email: 'johndoe@keyshade.xyz', - name: 'John Doe', - isOnboardingFinished: true - } - }) - - user2 = await prisma.user.create({ - data: { - id: user2Id, - email: 'janedoe@keyshade.xyz', - name: 'Jane Doe', - isOnboardingFinished: true - } - }) - - user3 = await prisma.user.create({ - data: { - id: v4(), - email: 'abc@keyshade.xyz', - name: 'ABC', - isOnboardingFinished: true - } - }) - - workspace1 = await workspaceService.createWorkspace(user1, { - name: 'Workspace 1', - description: 'Workspace 1 description', - approvalEnabled: true - }) - }) - - it('should be defined', () => { - expect(app).toBeDefined() - expect(prisma).toBeDefined() - expect(projectService).toBeDefined() - expect(workspaceService).toBeDefined() - expect(environmentService).toBeDefined() - expect(secretService).toBeDefined() - expect(variableService).toBeDefined() - }) - - it('should create an approval to update a workspace with approval enabled', async () => { - const approval = (await workspaceService.updateWorkspace( - user1, - workspace1.id, - { - name: 'Workspace 1 Updated' - } - )) as Approval - - expect(approval).toBeDefined() - expect(approval.id).toBeDefined() - expect(approval.status).toBe(ApprovalStatus.PENDING) - expect(approval.action).toBe(ApprovalAction.UPDATE) - expect(approval.itemType).toBe(ApprovalItemType.WORKSPACE) - expect(approval.workspaceId).toBe(workspace1.id) - expect(approval.metadata).toStrictEqual({ - name: 'Workspace 1 Updated' - }) - }) - - it('should have created a APPROVAL_CREATED event', async () => { - const response = await fetchEvents( - eventService, - user1, - workspace1.id, - EventSource.APPROVAL - ) - - const event = response[0] - - expect(event.source).toBe(EventSource.APPROVAL) - expect(event.triggerer).toBe(EventTriggerer.USER) - expect(event.severity).toBe(EventSeverity.INFO) - expect(event.type).toBe(EventType.APPROVAL_CREATED) - expect(event.workspaceId).toBe(workspace1.id) - expect(event.itemId).toBeDefined() - }) - - it('should allow user with WORKSPACE_ADMIN to view the approval', async () => { - const approval = await prisma.approval.findFirst({ - where: { - workspaceId: workspace1.id, - status: ApprovalStatus.PENDING, - itemType: ApprovalItemType.WORKSPACE - } - }) - - const adminRole = await prisma.workspaceRole.findUnique({ - where: { - workspaceId_name: { - name: 'Admin', - workspaceId: workspace1.id - } - } - }) - - expect(adminRole).toBeDefined() - - await prisma.workspaceMember.create({ - data: { - userId: user3.id, - workspaceId: workspace1.id, - invitationAccepted: true, - roles: { - create: { - roleId: adminRole.id - } - } - } - }) - - const response = await app.inject({ - method: 'GET', - url: `/approval/${approval.id}`, - headers: { - 'x-e2e-user-email': user3.email - } - }) - - expect(response.statusCode).toBe(200) - expect(response.json().approval.id).toBe(approval.id) - }) - - it('should allow user with MANAGE_APPROVALS authority to view the approval', async () => { - const approval = await prisma.approval.findFirst({ - where: { - workspaceId: workspace1.id, - status: ApprovalStatus.PENDING, - itemType: ApprovalItemType.WORKSPACE - } - }) - - const managerRole = await workspaceRoleService.createWorkspaceRole( - user1, - workspace1.id, - { - name: 'Manager', - authorities: [Authority.MANAGE_APPROVALS] - } - ) - - await workspaceService.updateMemberRoles(user1, workspace1.id, user3.id, [ - managerRole.id - ]) - - const response = await app.inject({ - method: 'GET', - url: `/approval/${approval.id}`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - expect(response.json().approval.id).toBe(approval.id) - }) - - it('should should not be able to approve an approval with invalid id', async () => { - const response = await app.inject({ - method: 'PUT', - url: `/approval/abc/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(404) - expect(response.json().message).toBe('Approval with id abc does not exist') - }) - - it('should not allow non member to approve an approval', async () => { - const approval = await prisma.approval.findFirst({ - where: { - workspaceId: workspace1.id, - status: ApprovalStatus.PENDING, - itemType: ApprovalItemType.WORKSPACE - } - }) - - const response = await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/approve`, - headers: { - 'x-e2e-user-email': user2.email - } - }) - - expect(response.statusCode).toBe(401) - expect(response.json().message).toBe( - `User with id ${user2.id} is not authorized to view approval with id ${approval.id}` - ) - }) - - it('should allow updating the approval', async () => { - const approval = await prisma.approval.findFirst({ - where: { - workspaceId: workspace1.id, - status: ApprovalStatus.PENDING, - itemType: ApprovalItemType.WORKSPACE - } - }) - - const response = await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}?reason=updated`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - expect(response.json().id).toBe(approval.id) - expect(response.json().status).toBe(ApprovalStatus.PENDING) - expect(response.json().reason).toBe('updated') - }) - - it('should have created a APPROVAL_UPDATED event', async () => { - const response = await fetchEvents( - eventService, - user1, - workspace1.id, - EventSource.APPROVAL - ) - - const event = response[0] - - expect(event.source).toBe(EventSource.APPROVAL) - expect(event.triggerer).toBe(EventTriggerer.USER) - expect(event.severity).toBe(EventSeverity.INFO) - expect(event.type).toBe(EventType.APPROVAL_UPDATED) - expect(event.workspaceId).toBe(workspace1.id) - expect(event.itemId).toBeDefined() - }) - - it('should update the workspace if the approval is approved', async () => { - let approval = await prisma.approval.findFirst({ - where: { - workspaceId: workspace1.id, - status: ApprovalStatus.PENDING, - itemType: ApprovalItemType.WORKSPACE - } - }) - - const response = await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - - const updatedWorkspace = await workspaceService.getWorkspaceById( - user1, - workspace1.id - ) - - expect(updatedWorkspace.name).toBe('Workspace 1 Updated') - - approval = await prisma.approval.findUnique({ - where: { - id: approval.id - } - }) - - expect(approval.status).toBe(ApprovalStatus.APPROVED) - expect(approval.approvedById).toBe(user1.id) - expect(approval.approvedAt).toBeDefined() - - workspace1 = updatedWorkspace - }) - - it('should have created a APPROVAL_APPROVED event', async () => { - const response = await fetchEvents( - eventService, - user1, - workspace1.id, - EventSource.APPROVAL - ) - - const event = response[0] - - expect(event.source).toBe(EventSource.APPROVAL) - expect(event.triggerer).toBe(EventTriggerer.USER) - expect(event.severity).toBe(EventSeverity.INFO) - expect(event.type).toBe(EventType.APPROVAL_APPROVED) - expect(event.workspaceId).toBe(workspace1.id) - expect(event.itemId).toBeDefined() - }) - - it('should not be able to approve an already approved approval', async () => { - const approval = await prisma.approval.findFirst({ - where: { - workspaceId: workspace1.id, - status: ApprovalStatus.APPROVED, - itemType: ApprovalItemType.WORKSPACE - } - }) - - const response = await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(400) - expect(response.json().message).toBe( - `Approval with id ${approval.id} is already approved/rejected` - ) - }) - - it('should not be able to reject an already approved approval', async () => { - const approval = await prisma.approval.findFirst({ - where: { - workspaceId: workspace1.id, - status: ApprovalStatus.APPROVED, - itemType: ApprovalItemType.WORKSPACE - } - }) - - const response = await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/reject`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(400) - expect(response.json().message).toBe( - `Approval with id ${approval.id} is already approved/rejected` - ) - }) - - it('should create an approval if a project is created', async () => { - const result = (await projectService.createProject( - user1, - workspace1.id, - { - name: 'Project 1' - }, - 'Test reason' - )) as { - approval: Approval - project: Project - } - - const approval = result.approval - const project = result.project - - expect(approval).toBeDefined() - expect(approval.id).toBeDefined() - expect(approval.status).toBe(ApprovalStatus.PENDING) - expect(approval.itemType).toBe(ApprovalItemType.PROJECT) - expect(approval.action).toBe(ApprovalAction.CREATE) - expect(approval.workspaceId).toBe(workspace1.id) - expect(approval.metadata).toStrictEqual({}) - - expect(project).toBeDefined() - expect(project.id).toBeDefined() - expect(project.name).toBe('Project 1') - expect(project.pendingCreation).toBe(true) - }) - - it('should delete the project if the approval is deleted', async () => { - let approval = await prisma.approval.findFirst({ - where: { - workspaceId: workspace1.id, - status: ApprovalStatus.PENDING, - itemType: ApprovalItemType.PROJECT - } - }) - - const response = await app.inject({ - method: 'DELETE', - url: `/approval/${approval.id}`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - - const projectCount = await prisma.project.count({ - where: { workspaceId: workspace1.id } - }) - expect(projectCount).toBe(0) - - approval = await prisma.approval.findUnique({ - where: { - id: approval.id - } - }) - expect(approval).toBeNull() - }) - - it('should have created a APPROVAL_DELETED event', async () => { - const response = await fetchEvents( - eventService, - user1, - workspace1.id, - EventSource.APPROVAL - ) - - const event = response[0] - - expect(event.source).toBe(EventSource.APPROVAL) - expect(event.triggerer).toBe(EventTriggerer.USER) - expect(event.severity).toBe(EventSeverity.INFO) - expect(event.type).toBe(EventType.APPROVAL_DELETED) - expect(event.workspaceId).toBe(workspace1.id) - expect(event.itemId).toBeDefined() - }) - - it('should allow creating project with the same name till it is not approved', async () => { - const result1 = (await projectService.createProject( - user1, - workspace1.id, - { - name: 'Project 1' - }, - 'Test reason' - )) as { - approval: Approval - project: Project - } - - const result2 = (await projectService.createProject( - user1, - workspace1.id, - { - name: 'Project 1' - }, - 'Test reason' - )) as { - approval: Approval - project: Project - } - - expect(result1.approval).toBeDefined() - expect(result1.project).toBeDefined() - expect(result2.approval).toBeDefined() - expect(result2.project).toBeDefined() - }) - - it('should create a new project if the approval is approved', async () => { - let approval = await prisma.approval.findFirst({ - where: { - workspaceId: workspace1.id, - status: ApprovalStatus.PENDING, - itemType: ApprovalItemType.PROJECT - } - }) - - const response = await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - - const projectCount = await prisma.project.count({ - where: { workspaceId: workspace1.id, pendingCreation: false } - }) - expect(projectCount).toBe(1) - - approval = await prisma.approval.findUnique({ - where: { - id: approval.id - } - }) - - expect(approval.status).toBe(ApprovalStatus.APPROVED) - expect(approval.approvedById).toBe(user1.id) - expect(approval.approvedAt).toBeDefined() - }) - - it('should not approve an approval if the project with the same name already exists', async () => { - let approval = await prisma.approval.findFirst({ - where: { - workspaceId: workspace1.id, - status: ApprovalStatus.PENDING, - itemType: ApprovalItemType.PROJECT - } - }) - - const response = await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(409) - expect(response.json().message).toBe( - `Project with this name Project 1 already exists` - ) - - approval = await prisma.approval.findUnique({ - where: { - id: approval.id - } - }) - expect(approval.status).toBe(ApprovalStatus.PENDING) - - // Change the project name to something else - project1 = await prisma.project.update({ - where: { - id: approval.itemId - }, - data: { - name: 'Project 2' - } - }) - }) - - it('should not create an approval if an environment is added to a project pending creation', async () => { - const result = (await environmentService.createEnvironment( - user1, - { - name: 'Environment 1', - description: 'Environment 1 description', - isDefault: true - }, - project1.id - )) as Environment - - expect(result).toBeDefined() - expect(result.id).toBeDefined() - expect(result.name).toBe('Environment 1') - expect(result.description).toBe('Environment 1 description') - - environment1 = result - }) - - it('should not create an approval if a variable is added to an environment pending creation', async () => { - const result = (await variableService.createVariable( - user1, - { - environmentId: environment1.id, - name: 'KEY', - value: 'VALUE' - }, - project1.id - )) as Variable - - expect(result).toBeDefined() - expect(result.id).toBeDefined() - expect(result.name).toBe('KEY') - - variable1 = result - }) - - it('should not create an approval if a secret is added to a project pending creation', async () => { - const result = (await secretService.createSecret( - user1, - { - name: 'Secret 1', - value: 'Secret 1 value' - }, - project1.id - )) as Secret - - expect(result).toBeDefined() - expect(result.id).toBeDefined() - expect(result.name).toBe('Secret 1') - - secret1 = result - }) - - it('should not create an approval if a secret pending creation is updated', async () => { - const result = (await secretService.updateSecret(user1, secret1.id, { - name: 'Secret 1 Updated' - })) as Secret - - expect(result).toBeDefined() - expect(result.id).toBeDefined() - expect(result.name).toBe('Secret 1 Updated') - - secret1 = result - }) - - it('should not create an approval if a variable pending creation is updated', async () => { - const result = (await variableService.updateVariable(user1, variable1.id, { - name: 'KEY_UPDATED' - })) as Variable - - expect(result).toBeDefined() - expect(result.id).toBeDefined() - expect(result.name).toBe('KEY_UPDATED') - - variable1 = result - }) - - it('should not create an approval if an environment pending creation is updated', async () => { - const result = (await environmentService.updateEnvironment( - user1, - { - name: 'Environment 1 Updated' - }, - environment1.id - )) as Environment - - expect(result).toBeDefined() - expect(result.id).toBeDefined() - expect(result.name).toBe('Environment 1 Updated') - - environment1 = result - }) - - it('should not create an approval if the project pending creation is updated', async () => { - const result = (await projectService.updateProject(user1, project1.id, { - name: 'Project 2 Updated' - })) as Project - - expect(result).toBeDefined() - expect(result.id).toBeDefined() - expect(result.name).toBe('Project 2 Updated') - }) - - it('should not create an approval if a secret pending creation is deleted', async () => { - const result = await secretService.deleteSecret(user1, secret1.id) - - expect(result).toBeUndefined() - secret1 = undefined - }) - - it('should not create an approval if a variable pending creation is deleted', async () => { - const result = await variableService.deleteVariable(user1, variable1.id) - - expect(result).toBeUndefined() - variable1 = undefined - }) - - it('should not create an approval if an environment pending creation is deleted', async () => { - // Create a default environment before deleting the pending creation - const createEnvResult = (await environmentService.createEnvironment( - user1, - { - name: 'Environment 2', - description: 'Environment 2 description', - isDefault: true - }, - project1.id - )) as Environment - - const result = await environmentService.deleteEnvironment( - user1, - environment1.id - ) - - expect(result).toBeUndefined() - - environment1 = createEnvResult - }) - - it('should approve all the sub items if a project is approved', async () => { - secret1 = (await secretService.createSecret( - user1, - { - name: 'Secret 2', - value: 'Secret 2 value' - }, - project1.id - )) as Secret - - variable1 = (await variableService.createVariable( - user1, - { - environmentId: environment1.id, - name: 'KEY2', - value: 'VALUE2' - }, - project1.id - )) as Variable - - environment1 = (await environmentService.createEnvironment( - user1, - { - name: 'Environment 3', - description: 'Default description', - isDefault: true - }, - project1.id - )) as Environment - - const approval = await prisma.approval.findFirst({ - where: { - workspaceId: workspace1.id, - status: ApprovalStatus.PENDING, - itemId: project1.id - } - }) - expect(approval).not.toBeNull() - - const response = await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - - const project = await prisma.project.findUnique({ - where: { - id: approval.itemId - } - }) - expect(project.pendingCreation).toBe(false) - project1 = project - - const environment = await prisma.environment.findUnique({ - where: { - id: environment1.id - } - }) - expect(environment.pendingCreation).toBe(false) - environment1 = environment - - const variable = await prisma.variable.findUnique({ - where: { - id: variable1.id - } - }) - expect(variable.pendingCreation).toBe(false) - variable1 = variable - - const secret = await prisma.secret.findUnique({ - where: { - id: secret1.id - } - }) - expect(secret.pendingCreation).toBe(false) - secret1 = secret - }) - - it('should create an approval if a secret is updated', async () => { - const result = (await secretService.updateSecret(user1, secret1.id, { - name: 'Secret 2 Updated', - value: 'Secret 2 value updated' - })) as Approval - - expect(result).toBeDefined() - expect(result.id).toBeDefined() - expect(result.status).toBe(ApprovalStatus.PENDING) - expect(result.itemType).toBe(ApprovalItemType.SECRET) - expect(result.action).toBe(ApprovalAction.UPDATE) - expect(result.metadata).toStrictEqual({ - name: 'Secret 2 Updated', - value: expect.not.stringContaining('Secret 2 value updated') - }) - }) - - it('should create an approval if a variable is updated', async () => { - const result = (await variableService.updateVariable(user1, variable1.id, { - name: 'KEY2_UPDATED', - value: 'VALUE2_UPDATED' - })) as Approval - - expect(result).toBeDefined() - expect(result.id).toBeDefined() - expect(result.status).toBe(ApprovalStatus.PENDING) - expect(result.itemType).toBe(ApprovalItemType.VARIABLE) - expect(result.action).toBe(ApprovalAction.UPDATE) - expect(result.metadata).toStrictEqual({ - name: 'KEY2_UPDATED', - value: 'VALUE2_UPDATED' - }) - }) - - it('should create an approval if the environment of a variable is updated', async () => { - const result = (await variableService.updateVariable(user1, variable1.id, { - environmentId: environment1.id - })) as Approval - - expect(result).toBeDefined() - expect(result.id).toBeDefined() - expect(result.status).toBe(ApprovalStatus.PENDING) - expect(result.itemType).toBe(ApprovalItemType.VARIABLE) - expect(result.action).toBe(ApprovalAction.UPDATE) - expect(result.metadata).toStrictEqual({ - environmentId: environment1.id - }) - }) - - it('should create an approval if the environment of a secret is updated', async () => { - const result = (await secretService.updateSecret(user1, secret1.id, { - environmentId: environment1.id - })) as Approval - - expect(result).toBeDefined() - expect(result.id).toBeDefined() - expect(result.status).toBe(ApprovalStatus.PENDING) - expect(result.itemType).toBe(ApprovalItemType.SECRET) - expect(result.action).toBe(ApprovalAction.UPDATE) - expect(result.metadata).toStrictEqual({ - environmentId: environment1.id - }) - }) - - it('should create an approval if a secret is rolled back', async () => { - await prisma.secretVersion.create({ - data: { - secretId: secret1.id, - value: 'Secret 2 value rolled back', - version: 2 - } - }) - - const result = (await secretService.rollbackSecret( - user1, - secret1.id, - 1 - )) as Approval - - expect(result).toBeDefined() - expect(result.id).toBeDefined() - expect(result.status).toBe(ApprovalStatus.PENDING) - expect(result.itemType).toBe(ApprovalItemType.SECRET) - expect(result.action).toBe(ApprovalAction.UPDATE) - expect(result.metadata).toStrictEqual({ - rollbackVersion: 1 - }) - }) - - it('should create an approval if a variable is rolled back', async () => { - await prisma.variableVersion.create({ - data: { - variableId: variable1.id, - value: 'VALUE2 rolled back', - version: 2 - } - }) - - const result = (await variableService.rollbackVariable( - user1, - variable1.id, - 1 - )) as Approval - - expect(result).toBeDefined() - expect(result.id).toBeDefined() - expect(result.status).toBe(ApprovalStatus.PENDING) - expect(result.itemType).toBe(ApprovalItemType.VARIABLE) - expect(result.action).toBe(ApprovalAction.UPDATE) - expect(result.metadata).toStrictEqual({ - rollbackVersion: 1 - }) - }) - - it('should update the secret if the approval is approved', async () => { - const approvals = await prisma.approval.findMany({ - where: { - workspaceId: workspace1.id, - status: ApprovalStatus.PENDING, - itemType: ApprovalItemType.SECRET, - action: ApprovalAction.UPDATE, - itemId: secret1.id - } - }) - - for (const approval of approvals) { - const response = await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - } - - const secret = await prisma.secret.findUnique({ - where: { - id: secret1.id - }, - include: { - versions: true - } - }) - expect(secret.name).toBe('Secret 2 Updated') - expect(secret.versions.length).toBe(1) - expect(secret.environmentId).toBe(environment1.id) - }) - - it('should update the variable if the approval is approved', async () => { - const approvals = await prisma.approval.findMany({ - where: { - workspaceId: workspace1.id, - status: ApprovalStatus.PENDING, - itemType: ApprovalItemType.VARIABLE, - action: ApprovalAction.UPDATE, - itemId: variable1.id - } - }) - - for (const approval of approvals) { - const response = await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - } - - const variable = await prisma.variable.findUnique({ - where: { - id: variable1.id - }, - include: { - versions: true - } - }) - expect(variable.name).toBe('KEY2_UPDATED') - expect(variable.versions.length).toBe(1) - expect(variable.environmentId).toBe(environment1.id) - }) - - it('should create an approval if a secret is deleted', async () => { - const result = (await secretService.deleteSecret( - user1, - secret1.id - )) as Approval - - expect(result).toBeDefined() - expect(result.id).toBeDefined() - expect(result.status).toBe(ApprovalStatus.PENDING) - expect(result.itemType).toBe(ApprovalItemType.SECRET) - expect(result.action).toBe(ApprovalAction.DELETE) - }) - - it('should create an approval if a variable is deleted', async () => { - const result = (await variableService.deleteVariable( - user1, - variable1.id - )) as Approval - - expect(result).toBeDefined() - expect(result.id).toBeDefined() - expect(result.status).toBe(ApprovalStatus.PENDING) - expect(result.itemType).toBe(ApprovalItemType.VARIABLE) - expect(result.action).toBe(ApprovalAction.DELETE) - }) - - it('should delete the secret if the approval is approved', async () => { - const approval = await prisma.approval.findFirst({ - where: { - workspaceId: workspace1.id, - status: ApprovalStatus.PENDING, - itemType: ApprovalItemType.SECRET, - action: ApprovalAction.DELETE, - itemId: secret1.id - } - }) - - const response = await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - - const secret = await prisma.secret.findUnique({ - where: { - id: secret1.id - } - }) - expect(secret).toBeNull() - }) - - it('should delete the variable if the approval is approved', async () => { - const approval = await prisma.approval.findFirst({ - where: { - workspaceId: workspace1.id, - status: ApprovalStatus.PENDING, - itemType: ApprovalItemType.VARIABLE, - action: ApprovalAction.DELETE, - itemId: variable1.id - } - }) - - const response = await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - - const variable = await prisma.variable.findUnique({ - where: { - id: variable1.id - } - }) - expect(variable).toBeNull() - }) - - it('should create an approval if a secret is created', async () => { - const result = (await secretService.createSecret( - user1, - { - name: 'Secret 3', - value: 'Secret 3 value', - environmentId: environment1.id - }, - project1.id - )) as { - approval: Approval - secret: Secret - } - - const approval = result.approval - const secret = result.secret - - expect(approval).toBeDefined() - expect(approval.id).toBeDefined() - expect(approval.status).toBe(ApprovalStatus.PENDING) - expect(approval.itemType).toBe(ApprovalItemType.SECRET) - expect(approval.action).toBe(ApprovalAction.CREATE) - expect(approval.workspaceId).toBe(workspace1.id) - expect(approval.metadata).toStrictEqual({}) - expect(secret).toBeDefined() - expect(secret.id).toBeDefined() - expect(secret.name).toBe('Secret 3') - - secret1 = secret - }) - - it('should create an approval if a variable is created', async () => { - const result = (await variableService.createVariable( - user1, - { - environmentId: environment1.id, - name: 'KEY3', - value: 'VALUE3' - }, - project1.id - )) as { - approval: Approval - variable: Variable - } - - const approval = result.approval - const variable = result.variable - - expect(approval).toBeDefined() - expect(approval.id).toBeDefined() - expect(approval.status).toBe(ApprovalStatus.PENDING) - expect(approval.itemType).toBe(ApprovalItemType.VARIABLE) - expect(approval.action).toBe(ApprovalAction.CREATE) - expect(approval.workspaceId).toBe(workspace1.id) - expect(approval.metadata).toStrictEqual({}) - expect(variable).toBeDefined() - expect(variable.id).toBeDefined() - expect(variable.name).toBe('KEY3') - - variable1 = variable - }) - - it('should delete the approval if the secret is deleted', async () => { - const secret = await prisma.secret.findUnique({ - where: { - id: secret1.id - } - }) - - const response = await app.inject({ - method: 'DELETE', - url: `/secret/${secret.id}`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - - const approval = await prisma.approval.findFirst({ - where: { - itemId: secret.id - } - }) - expect(approval).toBeNull() - }) - - it('should delete the approval if the variable is deleted', async () => { - const variable = await prisma.variable.findUnique({ - where: { - id: variable1.id - } - }) - - const response = await app.inject({ - method: 'DELETE', - url: `/variable/${variable.id}`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - - const approval = await prisma.approval.findFirst({ - where: { - itemId: variable.id - } - }) - expect(approval).toBeNull() - }) - - it('should create an approval if an environment is deleted', async () => { - await prisma.environment.create({ - data: { - name: 'Environment 5', - description: 'Environment 2 description', - isDefault: true, - projectId: project1.id - } - }) - - await prisma.environment.update({ - where: { - id: environment1.id - }, - data: { - isDefault: false - } - }) - - const result = (await environmentService.deleteEnvironment( - user1, - environment1.id - )) as Approval - - expect(result).toBeDefined() - expect(result.id).toBeDefined() - expect(result.status).toBe(ApprovalStatus.PENDING) - expect(result.itemType).toBe(ApprovalItemType.ENVIRONMENT) - expect(result.action).toBe(ApprovalAction.DELETE) - }) - - it('should delete the environment if the approval is approved', async () => { - const approval = await prisma.approval.findFirst({ - where: { - itemId: environment1.id - } - }) - - const response = await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - - const environment = await prisma.environment.findUnique({ - where: { - id: environment1.id - } - }) - expect(environment).toBeNull() - }) - - it('should create an approval if an environment is created', async () => { - const result = (await environmentService.createEnvironment( - user1, - { - name: 'Environment 4', - description: 'Environment 4 description', - isDefault: true - }, - project1.id - )) as { - approval: Approval - environment: Environment - } - - const approval = result.approval - const environment = result.environment - - expect(approval).toBeDefined() - expect(approval.id).toBeDefined() - expect(approval.status).toBe(ApprovalStatus.PENDING) - expect(approval.itemType).toBe(ApprovalItemType.ENVIRONMENT) - expect(approval.action).toBe(ApprovalAction.CREATE) - expect(approval.workspaceId).toBe(workspace1.id) - expect(approval.metadata).toStrictEqual({}) - expect(environment).toBeDefined() - expect(environment.id).toBeDefined() - expect(environment.name).toBe('Environment 4') - - environment1 = environment - }) - - it('should not create an approval if a secret is added to an environment pending creation', async () => { - const result = (await secretService.createSecret( - user1, - { - name: 'Secret 4', - value: 'Secret 4 value', - environmentId: environment1.id - }, - project1.id - )) as Secret - - expect(result).toBeDefined() - expect(result.id).toBeDefined() - expect(result.name).toBe('Secret 4') - - secret1 = result - }) - - it('should not create an approval if a variable is added to an environment pending creation', async () => { - const result = (await variableService.createVariable( - user1, - { - environmentId: environment1.id, - name: 'KEY4', - value: 'VALUE4' - }, - project1.id - )) as Variable - - expect(result).toBeDefined() - expect(result.id).toBeDefined() - expect(result.name).toBe('KEY4') - - variable1 = result - }) - - it('should approve the child items of an environment if the environment is approved', async () => { - const approval = await prisma.approval.findFirst({ - where: { - workspaceId: workspace1.id, - status: ApprovalStatus.PENDING, - itemType: ApprovalItemType.ENVIRONMENT, - itemId: environment1.id - } - }) - - const response = await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - - const secret = await prisma.secret.findUnique({ - where: { - id: secret1.id - } - }) - expect(secret.pendingCreation).toBe(false) - - const variable = await prisma.variable.findUnique({ - where: { - id: variable1.id - } - }) - expect(variable.pendingCreation).toBe(false) - }) - - it('should create an approval if a project is deleted', async () => { - const result = (await projectService.deleteProject( - user1, - project1.id - )) as Approval - - expect(result).toBeDefined() - expect(result.id).toBeDefined() - expect(result.status).toBe(ApprovalStatus.PENDING) - expect(result.itemType).toBe(ApprovalItemType.PROJECT) - expect(result.action).toBe(ApprovalAction.DELETE) - }) - - it('should delete the project if the approval is approved', async () => { - const approval = await prisma.approval.findFirst({ - where: { - itemId: project1.id, - itemType: ApprovalItemType.PROJECT, - action: ApprovalAction.DELETE, - status: ApprovalStatus.PENDING - } - }) - - const response = await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - - const project = await prisma.project.findUnique({ - where: { - id: project1.id - } - }) - expect(project).toBeNull() - }) - - it('should be able to delete an approval', async () => { - const approval = await prisma.approval.findFirst({ - where: { - workspaceId: workspace1.id - } - }) - - const response = await app.inject({ - method: 'DELETE', - url: `/approval/${approval.id}`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - - const deletedApproval = await prisma.approval.findUnique({ - where: { - id: approval.id - } - }) - expect(deletedApproval).toBeNull() - }) - - it('should be able to fetch all approvals of a workspace', async () => { - const response = await app.inject({ - method: 'GET', - url: `/approval/${workspace1.id}/all-in-workspace`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - expect(response.json().length).not.toBe(0) - }) - - it('should have the project if project approval is fetched', async () => { - const approval = await prisma.approval.findFirst({ - where: { - workspaceId: workspace1.id, - itemType: ApprovalItemType.PROJECT - } - }) - - const response = await app.inject({ - method: 'GET', - url: `/approval/${approval.id}`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - expect(response.json().approval.id).toBe(approval.id) - expect(response.json().project).toBeDefined() - }) - - it('should have the environment if environment approval is fetched', async () => { - const approval = await prisma.approval.findFirst({ - where: { - workspaceId: workspace1.id, - itemType: ApprovalItemType.ENVIRONMENT - } - }) - - const response = await app.inject({ - method: 'GET', - url: `/approval/${approval.id}`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - expect(response.json().approval.id).toBe(approval.id) - expect(response.json().environment).toBeDefined() - }) - - it('should have the secret if secret approval is fetched', async () => { - const approval = await prisma.approval.findFirst({ - where: { - workspaceId: workspace1.id, - itemType: ApprovalItemType.SECRET - } - }) - - const response = await app.inject({ - method: 'GET', - url: `/approval/${approval.id}`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - expect(response.json().approval.id).toBe(approval.id) - expect(response.json().secret).toBeDefined() - }) - - it('should have the variable if variable approval is fetched', async () => { - const approval = await prisma.approval.findFirst({ - where: { - workspaceId: workspace1.id, - itemType: ApprovalItemType.VARIABLE - } - }) - - const response = await app.inject({ - method: 'GET', - url: `/approval/${approval.id}`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - expect(response.json().approval.id).toBe(approval.id) - expect(response.json().variable).toBeDefined() - }) - - it('should have the workspace if workspace approval is fetched', async () => { - await workspaceService.updateWorkspace(user1, workspace1.id, { - name: 'Workspace 10 Updated' - }) - - const approval = await prisma.approval.findFirst({ - where: { - workspaceId: workspace1.id, - itemType: ApprovalItemType.WORKSPACE - } - }) - - const response = await app.inject({ - method: 'GET', - url: `/approval/${approval.id}`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - expect(response.json().approval.id).toBe(approval.id) - expect(response.json().workspace).toBeDefined() - }) - - it('should be able to fetch all approvals of a user in a workspace', async () => { - const response = await app.inject({ - method: 'GET', - url: `/approval/${workspace1.id}/all-by-user/${user1.id}`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - expect(response.json().length).not.toBe(0) - }) - - it('should be able to reject an approval', async () => { - const approval = await prisma.approval.findFirst({ - where: { - workspaceId: workspace1.id, - status: ApprovalStatus.PENDING - } - }) - - const response = await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/reject`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - }) - - it('should have created a APPROVAL_REJECTED event', async () => { - const response = await fetchEvents( - eventService, - user1, - workspace1.id, - EventSource.APPROVAL - ) - - const event = response[0] - - expect(event.source).toBe(EventSource.APPROVAL) - expect(event.triggerer).toBe(EventTriggerer.USER) - expect(event.severity).toBe(EventSeverity.INFO) - expect(event.type).toBe(EventType.APPROVAL_REJECTED) - expect(event.workspaceId).toBe(workspace1.id) - expect(event.itemId).toBeDefined() - }) - - it('should delete the item if the approval is rejected', async () => { - // Create a new project - const result = (await projectService.createProject( - user1, - workspace1.id, - { - name: 'Project 2' - }, - 'Test reason' - )) as { - approval: Approval - project: Project - } - - const approval = result.approval - const project = result.project - - expect(approval).toBeDefined() - expect(project).toBeDefined() - - // Reject the approval - const response = await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/reject`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - - const approvalAfterRejection = await prisma.approval.findUnique({ - where: { - id: approval.id - } - }) - - expect(approvalAfterRejection.status).toBe(ApprovalStatus.REJECTED) - - // Project should be deleted - const deletedProject = await prisma.project.findUnique({ - where: { - id: project.id - } - }) - expect(deletedProject).toBeNull() - }) - - it('should update a project if the approval is accepted', async () => { - const createProjectResponse = (await projectService.createProject( - user1, - workspace1.id, - { - name: 'Project 3' - }, - 'Test reason' - )) as { - approval: Approval - project: Project - } - - const approval = await prisma.approval.findFirst({ - where: { - status: ApprovalStatus.PENDING, - itemType: ApprovalItemType.PROJECT, - action: ApprovalAction.CREATE, - itemId: createProjectResponse.project.id - } - }) - - await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - await projectService.updateProject( - user1, - createProjectResponse.project.id, - { - name: 'Project 3 Updated' - } - ) - - const updateProjectApproval = await prisma.approval.findFirst({ - where: { - status: ApprovalStatus.PENDING, - itemType: ApprovalItemType.PROJECT, - action: ApprovalAction.UPDATE, - itemId: createProjectResponse.project.id - } - }) - - const response = await app.inject({ - method: 'PUT', - url: `/approval/${updateProjectApproval.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - - const project = await prisma.project.findUnique({ - where: { - id: createProjectResponse.project.id - } - }) - expect(project.name).toBe('Project 3 Updated') - - project1 = project - }) - - it('should update an environment if approval is accepted', async () => { - const createEnvResponse = (await environmentService.createEnvironment( - user1, - { - name: 'Environment 6', - description: 'Environment 6 description', - isDefault: true - }, - project1.id - )) as { - approval: Approval - environment: Environment - } - - const approval = await prisma.approval.findFirst({ - where: { - status: ApprovalStatus.PENDING, - itemType: ApprovalItemType.ENVIRONMENT, - action: ApprovalAction.CREATE, - itemId: createEnvResponse.environment.id - } - }) - - await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - await environmentService.updateEnvironment( - user1, - { - name: 'Environment 6 Updated' - }, - createEnvResponse.environment.id - ) - - const updateEnvApproval = await prisma.approval.findFirst({ - where: { - status: ApprovalStatus.PENDING, - itemType: ApprovalItemType.ENVIRONMENT, - action: ApprovalAction.UPDATE, - itemId: createEnvResponse.environment.id - } - }) - - const response = await app.inject({ - method: 'PUT', - url: `/approval/${updateEnvApproval.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - - const environment = await prisma.environment.findUnique({ - where: { - id: createEnvResponse.environment.id - } - }) - expect(environment.name).toBe('Environment 6 Updated') - - environment1 = environment - }) - - it('should approve a secret if the approval is approved', async () => { - const createSecretResponse = (await secretService.createSecret( - user1, - { - name: 'Secret 5', - value: 'Secret 5 value' - }, - project1.id - )) as { - approval: Approval - secret: Secret - } - - let approval = await prisma.approval.findFirst({ - where: { - status: ApprovalStatus.PENDING, - itemType: ApprovalItemType.SECRET, - action: ApprovalAction.CREATE, - itemId: createSecretResponse.secret.id - } - }) - - await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - approval = await prisma.approval.findFirst({ - where: { - status: ApprovalStatus.APPROVED, - itemType: ApprovalItemType.SECRET, - action: ApprovalAction.CREATE, - itemId: createSecretResponse.secret.id - } - }) - - const secret = await prisma.secret.findUnique({ - where: { - id: createSecretResponse.secret.id - } - }) - - expect(secret.pendingCreation).toBe(false) - expect(approval).toBeDefined() - expect(approval.id).toBeDefined() - expect(approval.status).toBe(ApprovalStatus.APPROVED) - - secret1 = secret - }) - - it('should approve a variable if the approval is approved', async () => { - const createVariableResponse = (await variableService.createVariable( - user1, - { - environmentId: environment1.id, - name: 'KEY5', - value: 'VALUE5' - }, - project1.id - )) as { - approval: Approval - variable: Variable - } - - let approval = await prisma.approval.findFirst({ - where: { - status: ApprovalStatus.PENDING, - itemType: ApprovalItemType.VARIABLE, - action: ApprovalAction.CREATE, - itemId: createVariableResponse.variable.id - } - }) - - await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - approval = await prisma.approval.findFirst({ - where: { - status: ApprovalStatus.APPROVED, - itemType: ApprovalItemType.VARIABLE, - action: ApprovalAction.CREATE, - itemId: createVariableResponse.variable.id - } - }) - - const variable = await prisma.variable.findUnique({ - where: { - id: createVariableResponse.variable.id - } - }) - - expect(variable.pendingCreation).toBe(false) - expect(approval).toBeDefined() - expect(approval.id).toBeDefined() - expect(approval.status).toBe(ApprovalStatus.APPROVED) - - variable1 = variable - }) - - it('should throw error if the environment to which a variable is to be transferred is deleted before the approval is accepted', async () => { - const createEnvResponse = (await environmentService.createEnvironment( - user1, - { - name: 'Environment 7', - description: 'Environment 7 description' - }, - project1.id - )) as { - approval: Approval - environment: Environment - } - - const approval = createEnvResponse.approval - - await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - const updateVariableEnvironmentResponse = - (await variableService.updateVariableEnvironment( - user1, - variable1.id, - createEnvResponse.environment.id - )) as Approval - - await prisma.environment.delete({ - where: { - id: createEnvResponse.environment.id - } - }) - - const response = await app.inject({ - method: 'PUT', - url: `/approval/${updateVariableEnvironmentResponse.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(400) - }) - - it('should throw error if the environment to which a secret is to be transferred is deleted before the approval is accepted', async () => { - const createEnvResponse = (await environmentService.createEnvironment( - user1, - { - name: 'Environment 8', - description: 'Environment 8 description' - }, - project1.id - )) as { - approval: Approval - environment: Environment - } - - const approval = createEnvResponse.approval - - await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - const updateSecretEnvironmentResponse = - (await secretService.updateSecretEnvironment( - user1, - secret1.id, - createEnvResponse.environment.id - )) as Approval - - await prisma.environment.delete({ - where: { - id: createEnvResponse.environment.id - } - }) - - const response = await app.inject({ - method: 'PUT', - url: `/approval/${updateSecretEnvironmentResponse.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(400) - }) - - it('should not approve an environment approval if the environment with the same name already exists', async () => { - const createEnvResponse = (await environmentService.createEnvironment( - user1, - { - name: 'Environment 9', - description: 'Environment 9 description' - }, - project1.id - )) as { - approval: Approval - environment: Environment - } - - const createEnvResponse2 = (await environmentService.createEnvironment( - user1, - { - name: 'Environment 9', - description: 'Environment 9 description' - }, - project1.id - )) as { - approval: Approval - environment: Environment - } - - const approval = createEnvResponse.approval - - await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - const approval2 = createEnvResponse2.approval - - const response = await app.inject({ - method: 'PUT', - url: `/approval/${approval2.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(409) - }) - - it('should not approve a variable if the variable with the same name already exists in the environment', async () => { - const createVariableResponse = (await variableService.createVariable( - user1, - { - environmentId: environment1.id, - name: 'KEY6', - value: 'VALUE6' - }, - project1.id - )) as { - approval: Approval - variable: Variable - } - - const createVariableResponse2 = (await variableService.createVariable( - user1, - { - environmentId: environment1.id, - name: 'KEY6', - value: 'VALUE6' - }, - project1.id - )) as { - approval: Approval - variable: Variable - } - - const approval = createVariableResponse.approval - - await app.inject({ - method: 'PUT', - url: `/approval/${approval.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - const approval2 = createVariableResponse2.approval - - const response = await app.inject({ - method: 'PUT', - url: `/approval/${approval2.id}/approve`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(409) - }) - - afterAll(async () => { - await cleanUp(prisma) - }) -}) diff --git a/apps/api/src/approval/approval.module.ts b/apps/api/src/approval/approval.module.ts deleted file mode 100644 index d7c8b24b..00000000 --- a/apps/api/src/approval/approval.module.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Module } from '@nestjs/common' -import { ApprovalService } from './service/approval.service' -import { ApprovalController } from './controller/approval.controller' -import { WorkspaceService } from '../workspace/service/workspace.service' -import { ProjectService } from '../project/service/project.service' -import { EnvironmentService } from '../environment/service/environment.service' -import { SecretService } from '../secret/service/secret.service' -import { VariableService } from '../variable/service/variable.service' - -@Module({ - providers: [ - ApprovalService, - WorkspaceService, - ProjectService, - EnvironmentService, - SecretService, - VariableService - ], - controllers: [ApprovalController] -}) -export class ApprovalModule {} diff --git a/apps/api/src/approval/approval.types.ts b/apps/api/src/approval/approval.types.ts deleted file mode 100644 index b00b89b5..00000000 --- a/apps/api/src/approval/approval.types.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { SecretVersion, VariableVersion } from '@prisma/client' -import { UpdateEnvironment } from 'src/environment/dto/update.environment/update.environment' -import { UpdateProject } from 'src/project/dto/update.project/update.project' -import { UpdateSecret } from 'src/secret/dto/update.secret/update.secret' -import { UpdateVariable } from 'src/variable/dto/update.variable/update.variable' -import { UpdateWorkspace } from 'src/workspace/dto/update.workspace/update.workspace' - -export interface UpdateWorkspaceMetadata { - name?: UpdateWorkspace['name'] - description?: UpdateWorkspace['description'] - approvalEnabled?: UpdateWorkspace['approvalEnabled'] -} - -export interface UpdateProjectMetadata { - name?: UpdateProject['name'] - description?: UpdateProject['description'] - storePrivateKey?: UpdateProject['storePrivateKey'] - accessLevel?: UpdateProject['accessLevel'] - regenerateKeyPair?: boolean - privateKey?: UpdateProject['privateKey'] -} - -export interface UpdateEnvironmentMetadata { - name?: UpdateEnvironment['name'] - description?: UpdateEnvironment['description'] - isDefault?: UpdateEnvironment['isDefault'] -} - -export interface UpdateSecretMetadata { - name?: UpdateSecret['name'] - note?: UpdateSecret['note'] - rollbackVersion?: SecretVersion['version'] - value?: UpdateSecret['value'] - rotateAfter?: UpdateSecret['rotateAfter'] - environmentId?: UpdateSecret['environmentId'] -} - -export interface UpdateVariableMetadata { - name?: UpdateVariable['name'] - note?: UpdateVariable['note'] - value?: UpdateVariable['value'] - rollbackVersion?: VariableVersion['version'] - environmentId?: UpdateVariable['environmentId'] -} diff --git a/apps/api/src/approval/controller/approval.controller.spec.ts b/apps/api/src/approval/controller/approval.controller.spec.ts deleted file mode 100644 index 1b9025d4..00000000 --- a/apps/api/src/approval/controller/approval.controller.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing' -import { ApprovalController } from './approval.controller' -import { PrismaService } from '../../prisma/prisma.service' -import { WorkspaceService } from '../../workspace/service/workspace.service' -import { ProjectService } from '../../project/service/project.service' -import { EnvironmentService } from '../../environment/service/environment.service' -import { VariableService } from '../../variable/service/variable.service' -import { SecretService } from '../../secret/service/secret.service' -import { ApprovalService } from '../service/approval.service' -import { MAIL_SERVICE } from '../../mail/services/interface.service' -import { MockMailService } from '../../mail/services/mock.service' -import { JwtService } from '@nestjs/jwt' -import { REDIS_CLIENT } from '../../provider/redis.provider' -import { RedisClientType } from 'redis' -import { mockDeep } from 'jest-mock-extended' -import { ProviderModule } from '../../provider/provider.module' -import { AuthorityCheckerService } from '../../common/authority-checker.service' -import { CommonModule } from '../../common/common.module' - -describe('ApprovalController', () => { - let controller: ApprovalController - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [ProviderModule, CommonModule], - controllers: [ApprovalController], - providers: [ - ApprovalService, - PrismaService, - WorkspaceService, - ProjectService, - EnvironmentService, - VariableService, - SecretService, - JwtService, - { - provide: MAIL_SERVICE, - useClass: MockMailService - }, - AuthorityCheckerService - ] - }) - .overrideProvider(REDIS_CLIENT) - .useValue(mockDeep()) - .compile() - - controller = module.get(ApprovalController) - }) - - it('should be defined', () => { - expect(controller).toBeDefined() - }) -}) diff --git a/apps/api/src/approval/controller/approval.controller.ts b/apps/api/src/approval/controller/approval.controller.ts deleted file mode 100644 index 1e10a6d5..00000000 --- a/apps/api/src/approval/controller/approval.controller.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { Controller, Delete, Get, Param, Put, Query } from '@nestjs/common' -import { ApprovalService } from '../service/approval.service' -import { - Approval, - ApprovalAction, - ApprovalItemType, - ApprovalStatus, - Authority, - User -} from '@prisma/client' -import { CurrentUser } from '../../decorators/user.decorator' -import { RequiredApiKeyAuthorities } from '../../decorators/required-api-key-authorities.decorator' -import { AlphanumericReasonValidationPipe } from '../../common/alphanumeric-reason-pipe' - -@Controller('approval') -export class ApprovalController { - constructor(private readonly approvalService: ApprovalService) {} - - @Put(':approvalId') - @RequiredApiKeyAuthorities(Authority.MANAGE_APPROVALS) - async updateApproval( - @CurrentUser() user: User, - @Param('approvalId') approvalId: Approval['id'], - @Query('reason', AlphanumericReasonValidationPipe) reason: string - ) { - return this.approvalService.updateApproval(user, reason, approvalId) - } - - @Delete(':approvalId') - @RequiredApiKeyAuthorities(Authority.MANAGE_APPROVALS) - async deleteApproval( - @CurrentUser() user: User, - @Param('approvalId') approvalId: Approval['id'] - ) { - return this.approvalService.deleteApproval(user, approvalId) - } - - @Put(':approvalId/approve') - @RequiredApiKeyAuthorities(Authority.MANAGE_APPROVALS) - async approveApproval( - @CurrentUser() user: User, - @Param('approvalId') approvalId: Approval['id'] - ) { - return this.approvalService.approveApproval(user, approvalId) - } - - @Put(':approvalId/reject') - @RequiredApiKeyAuthorities(Authority.MANAGE_APPROVALS) - async rejectApproval( - @CurrentUser() user: User, - @Param('approvalId') approvalId: Approval['id'] - ) { - return this.approvalService.rejectApproval(user, approvalId) - } - - @Get(':approvalId') - @RequiredApiKeyAuthorities(Authority.MANAGE_APPROVALS) - async getApproval( - @CurrentUser() user: User, - @Param('approvalId') approvalId: Approval['id'] - ) { - return this.approvalService.getApprovalById(user, approvalId) - } - - @Get(':workspaceId/all-in-workspace') - @RequiredApiKeyAuthorities(Authority.MANAGE_APPROVALS) - async getAllApprovalsInWorkspace( - @CurrentUser() user: User, - @Param('workspaceId') workspaceId: string, - @Query('page') page: number = 1, - @Query('limit') limit: number = 10, - @Query('sort') sort: string = 'createdAt', - @Query('order') order: string = 'asc', - @Query('itemTypes') - itemTypes: ApprovalItemType[] = [ - ApprovalItemType.ENVIRONMENT, - ApprovalItemType.PROJECT, - ApprovalItemType.SECRET, - ApprovalItemType.VARIABLE, - ApprovalItemType.WORKSPACE - ], - @Query('actions') - actions: ApprovalAction[] = [ - ApprovalAction.CREATE, - ApprovalAction.DELETE, - ApprovalAction.UPDATE - ], - @Query('statuses') - statuses: ApprovalStatus[] = [ - ApprovalStatus.PENDING, - ApprovalStatus.APPROVED, - ApprovalStatus.REJECTED - ] - ) { - return this.approvalService.getApprovalsForWorkspace( - user, - workspaceId, - page, - limit, - sort, - order, - itemTypes, - actions, - statuses - ) - } - - @Get(':workspaceId/all-by-user/:userId') - @RequiredApiKeyAuthorities(Authority.READ_WORKSPACE) - async getAllApprovalsByUser( - @CurrentUser() user: User, - @Param('workspaceId') workspaceId: string, - @Param('userId') userId: string, - @Query('page') page: number = 1, - @Query('limit') limit: number = 10, - @Query('sort') sort: string = 'createdAt', - @Query('order') order: string = 'asc', - @Query('itemTypes') - itemTypes: ApprovalItemType[] = [ - ApprovalItemType.ENVIRONMENT, - ApprovalItemType.PROJECT, - ApprovalItemType.SECRET, - ApprovalItemType.VARIABLE, - ApprovalItemType.WORKSPACE - ], - @Query('actions') - actions: ApprovalAction[] = [ - ApprovalAction.CREATE, - ApprovalAction.DELETE, - ApprovalAction.UPDATE - ], - @Query('statuses') - statuses: ApprovalStatus[] = [ - ApprovalStatus.PENDING, - ApprovalStatus.APPROVED, - ApprovalStatus.REJECTED - ] - ) { - return this.approvalService.getApprovalsOfUser( - user, - userId, - workspaceId, - page, - limit, - sort, - order, - itemTypes, - actions, - statuses - ) - } -} diff --git a/apps/api/src/approval/service/approval.service.spec.ts b/apps/api/src/approval/service/approval.service.spec.ts deleted file mode 100644 index 6406b824..00000000 --- a/apps/api/src/approval/service/approval.service.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing' -import { ApprovalService } from './approval.service' -import { PrismaService } from '../../prisma/prisma.service' -import { WorkspaceService } from '../../workspace/service/workspace.service' -import { ProjectService } from '../../project/service/project.service' -import { EnvironmentService } from '../../environment/service/environment.service' -import { VariableService } from '../../variable/service/variable.service' -import { SecretService } from '../../secret/service/secret.service' -import { MAIL_SERVICE } from '../../mail/services/interface.service' -import { MockMailService } from '../../mail/services/mock.service' -import { JwtService } from '@nestjs/jwt' -import { REDIS_CLIENT } from '../../provider/redis.provider' -import { RedisClientType } from 'redis' -import { mockDeep } from 'jest-mock-extended' -import { ProviderModule } from '../../provider/provider.module' -import { AuthorityCheckerService } from '../../common/authority-checker.service' -import { CommonModule } from '../../common/common.module' - -describe('ApprovalService', () => { - let service: ApprovalService - - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - imports: [ProviderModule, CommonModule], - providers: [ - ApprovalService, - PrismaService, - WorkspaceService, - ProjectService, - EnvironmentService, - VariableService, - SecretService, - JwtService, - { - provide: MAIL_SERVICE, - useClass: MockMailService - }, - AuthorityCheckerService - ] - }) - .overrideProvider(REDIS_CLIENT) - .useValue(mockDeep()) - .compile() - - service = module.get(ApprovalService) - }) - - it('should be defined', () => { - expect(service).toBeDefined() - }) -}) diff --git a/apps/api/src/approval/service/approval.service.ts b/apps/api/src/approval/service/approval.service.ts deleted file mode 100644 index 24bc461f..00000000 --- a/apps/api/src/approval/service/approval.service.ts +++ /dev/null @@ -1,628 +0,0 @@ -import { - BadRequestException, - Injectable, - Logger, - NotFoundException, - UnauthorizedException -} from '@nestjs/common' -import { - Approval, - ApprovalAction, - ApprovalItemType, - ApprovalStatus, - Authority, - EventSource, - EventType, - Secret, - User, - Workspace -} from '@prisma/client' -import createEvent from '../../common/create-event' -import getCollectiveWorkspaceAuthorities from '../../common/get-collective-workspace-authorities' -import { EnvironmentService } from '../../environment/service/environment.service' -import { PrismaService } from '../../prisma/prisma.service' -import { ProjectService } from '../../project/service/project.service' -import { SecretService } from '../../secret/service/secret.service' -import { VariableService } from '../../variable/service/variable.service' -import { WorkspaceService } from '../../workspace/service/workspace.service' -import { - UpdateProjectMetadata, - UpdateSecretMetadata, - UpdateVariableMetadata, - UpdateWorkspaceMetadata -} from '../approval.types' -import { AuthorityCheckerService } from '../../common/authority-checker.service' - -@Injectable() -export class ApprovalService { - private readonly logger = new Logger(ApprovalService.name) - - constructor( - private readonly prisma: PrismaService, - private readonly workspaceService: WorkspaceService, - private readonly projectService: ProjectService, - private readonly environmentService: EnvironmentService, - private readonly secretService: SecretService, - private readonly variableService: VariableService, - private readonly authorityCheckerService: AuthorityCheckerService - ) {} - - async updateApproval(user: User, reason: string, approvalId: Approval['id']) { - // Check if the user has the authority to update the approval - let approval = await this.checkApprovalAuthority(user, approvalId) - - this.isApprovalInActableState(approval) - - // Update the approval - approval = await this.prisma.approval.update({ - where: { - id: approvalId - }, - data: { - reason - } - }) - - this.logger.log(`Approval with id ${approvalId} updated by ${user.id}`) - - await createEvent( - { - triggeredBy: user, - entity: approval, - type: EventType.APPROVAL_UPDATED, - source: EventSource.APPROVAL, - title: `Approval with id ${approvalId} updated`, - metadata: { - approvalId - }, - workspaceId: approval.workspaceId - }, - this.prisma - ) - - return approval - } - - async deleteApproval(user: User, approvalId: Approval['id']) { - // Check if the user has the authority to delete the approval - const approval = await this.checkApprovalAuthority(user, approvalId) - - // If the approval is of type CREATE, we need to delete the item as well - if ( - approval.status === ApprovalStatus.PENDING && - approval.action === ApprovalAction.CREATE - ) { - await this.deleteItem(approval, user) - } - - // Delete the approval - await this.prisma.approval.delete({ - where: { - id: approvalId - } - }) - - this.logger.log(`Approval with id ${approvalId} deleted by ${user.id}`) - - await createEvent( - { - triggeredBy: user, - entity: approval, - type: EventType.APPROVAL_DELETED, - source: EventSource.APPROVAL, - title: `Approval with id ${approvalId} deleted`, - metadata: { - approvalId - }, - workspaceId: approval.workspaceId - }, - this.prisma - ) - } - - async rejectApproval(user: User, approvalId: Approval['id']) { - // Check if the user has the authority to reject the approval - let approval = await this.checkApprovalAuthority(user, approvalId) - - this.isApprovalInActableState(approval) - - // Update the approval - approval = await this.prisma.approval.update({ - where: { - id: approvalId - }, - data: { - status: ApprovalStatus.REJECTED, - rejectedAt: new Date(), - rejectedBy: { - connect: { - id: user.id - } - } - } - }) - - // Delete the item if the action is CREATE - if (approval.action === ApprovalAction.CREATE) { - await this.deleteItem(approval, user) - } - - this.logger.log(`Approval with id ${approvalId} rejected by ${user.id}`) - - await createEvent( - { - triggeredBy: user, - entity: approval, - type: EventType.APPROVAL_REJECTED, - source: EventSource.APPROVAL, - title: `Approval with id ${approvalId} rejected`, - metadata: { - approvalId - }, - workspaceId: approval.workspaceId - }, - this.prisma - ) - } - - async approveApproval(user: User, approvalId: Approval['id']) { - // Check if the user has the authority to approve the approval - const approval = await this.checkApprovalAuthority(user, approvalId) - - this.isApprovalInActableState(approval) - - if (approval.action === ApprovalAction.DELETE) { - await this.deleteItem(approval, user) - } else { - switch (approval.itemType) { - case ApprovalItemType.WORKSPACE: { - switch (approval.action) { - case ApprovalAction.UPDATE: { - await this.workspaceService.update( - approval.itemId, - approval.metadata as UpdateWorkspaceMetadata, - user - ) - break - } - } - break - } - case ApprovalItemType.PROJECT: { - const project = await this.prisma.project.findUnique({ - where: { - id: approval.itemId - }, - include: { - secrets: true - } - }) - switch (approval.action) { - case ApprovalAction.CREATE: { - await this.projectService.makeProjectApproved(approval.itemId) - break - } - case ApprovalAction.UPDATE: { - await this.projectService.update( - approval.metadata as UpdateProjectMetadata, - user, - project - ) - break - } - } - break - } - case ApprovalItemType.ENVIRONMENT: { - switch (approval.action) { - case ApprovalAction.CREATE: { - await this.environmentService.makeEnvironmentApproved( - approval.itemId - ) - break - } - case ApprovalAction.UPDATE: { - const environment = await this.prisma.environment.findUnique({ - where: { - id: approval.itemId - } - }) - await this.environmentService.update( - user, - environment, - approval.metadata as UpdateProjectMetadata - ) - break - } - } - break - } - case ApprovalItemType.SECRET: { - switch (approval.action) { - case ApprovalAction.CREATE: { - await this.secretService.makeSecretApproved( - approval.itemId as Secret['id'] - ) - break - } - case ApprovalAction.UPDATE: { - const secret = await this.prisma.secret.findUnique({ - where: { - id: approval.itemId - }, - include: { - project: true, - versions: true - } - }) - const metadata = approval.metadata as UpdateSecretMetadata - - if (metadata.environmentId) { - const environment = await this.prisma.environment.findUnique({ - where: { - id: metadata.environmentId - } - }) - - if (!environment) { - throw new BadRequestException( - `Environment with id ${metadata.environmentId} does not exist` - ) - } - await this.secretService.updateEnvironment( - user, - secret, - environment - ) - } else if (metadata.rollbackVersion) { - await this.secretService.rollback( - user, - secret, - metadata.rollbackVersion - ) - } else { - await this.secretService.update( - metadata as UpdateSecretMetadata, - user, - secret - ) - } - break - } - } - break - } - case ApprovalItemType.VARIABLE: { - switch (approval.action) { - case ApprovalAction.CREATE: { - await this.variableService.makeVariableApproved(approval.itemId) - break - } - case ApprovalAction.UPDATE: { - const variable = await this.prisma.variable.findUnique({ - where: { - id: approval.itemId - }, - include: { - project: true, - versions: true - } - }) - const metadata = approval.metadata as UpdateVariableMetadata - - if (metadata.environmentId) { - const environment = await this.prisma.environment.findUnique({ - where: { - id: metadata.environmentId - } - }) - - if (!environment) { - throw new BadRequestException( - `Environment with id ${metadata.environmentId} does not exist` - ) - } - await this.variableService.updateEnvironment( - user, - variable, - environment - ) - } else if (metadata.rollbackVersion) { - await this.variableService.rollback( - user, - variable, - metadata.rollbackVersion - ) - } else { - await this.variableService.update( - metadata as UpdateVariableMetadata, - user, - variable - ) - } - break - } - } - } - } - } - - // Update the approval - await this.prisma.approval.update({ - where: { - id: approvalId - }, - data: { - status: ApprovalStatus.APPROVED, - approvedAt: new Date(), - approvedBy: { - connect: { - id: user.id - } - } - } - }) - - this.logger.log(`Approval with id ${approvalId} approved by ${user.id}`) - - await createEvent( - { - triggeredBy: user, - entity: approval, - type: EventType.APPROVAL_APPROVED, - source: EventSource.APPROVAL, - title: `Approval with id ${approvalId} approved`, - metadata: { - approvalId - }, - workspaceId: approval.workspaceId - }, - this.prisma - ) - } - - async getApprovalById(user: User, approvalId: Approval['id']) { - const approval = await this.checkApprovalAuthority(user, approvalId) - - switch (approval.itemType) { - case ApprovalItemType.PROJECT: { - const project = await this.prisma.project.findUnique({ - where: { - id: approval.itemId - } - }) - return { - approval, - project - } - } - case ApprovalItemType.ENVIRONMENT: { - const environment = await this.prisma.environment.findUnique({ - where: { - id: approval.itemId - } - }) - return { - approval, - environment - } - } - case ApprovalItemType.SECRET: { - const secret = await this.prisma.secret.findUnique({ - where: { - id: approval.itemId - } - }) - return { - approval, - secret - } - } - case ApprovalItemType.VARIABLE: { - const variable = await this.prisma.variable.findUnique({ - where: { - id: approval.itemId - } - }) - return { - approval, - variable - } - } - case ApprovalItemType.WORKSPACE: { - const workspace = await this.prisma.workspace.findUnique({ - where: { - id: approval.itemId - } - }) - return { - approval, - workspace - } - } - } - } - - async getApprovalsForWorkspace( - user: User, - workspaceId: Workspace['id'], - page: number, - limit: number, - sort: string, - order: string, - itemTypes: ApprovalItemType[], - actions: ApprovalAction[], - statuses: ApprovalStatus[] - ) { - await this.authorityCheckerService.checkAuthorityOverWorkspace({ - userId: user.id, - entity: { id: workspaceId }, - authority: Authority.MANAGE_APPROVALS, - prisma: this.prisma - }) - - return await this.prisma.approval.findMany({ - where: { - workspaceId, - itemType: { - in: itemTypes - }, - action: { - in: actions - }, - status: { - in: statuses - } - }, - orderBy: { - [sort]: order - }, - skip: page * limit, - take: limit - }) - } - - async getApprovalsOfUser( - user: User, - otherUserId: User['id'], - workspaceId: Workspace['id'], - page: number, - limit: number, - sort: string, - order: string, - itemTypes: ApprovalItemType[], - actions: ApprovalAction[], - statuses: ApprovalStatus[] - ) { - await this.authorityCheckerService.checkAuthorityOverWorkspace({ - userId: user.id, - entity: { id: workspaceId }, - authority: Authority.READ_WORKSPACE, - prisma: this.prisma - }) - - return this.prisma.approval.findMany({ - where: { - requestedById: otherUserId, - workspaceId, - itemType: { - in: itemTypes - }, - action: { - in: actions - }, - status: { - in: statuses - } - }, - orderBy: { - [sort]: order - }, - skip: page * limit, - take: limit - }) - } - - /** - * A user should only be able to fetch an approval if they are an admin, or a workspace admin, - * or if they have the MANAGE_APPROVALS authority in the workspace, or if they are the user - * who requested the approval - * @param user The user fetching the approval - * @param approvalId The id of the approval to fetch - * @returns The fetched approval - */ - private async checkApprovalAuthority(user: User, approvalId: Approval['id']) { - const approval = await this.prisma.approval.findFirst({ - where: { - id: approvalId - } - }) - - if (!approval) { - throw new NotFoundException( - `Approval with id ${approvalId} does not exist` - ) - } - - const workspaceAuthorities = await getCollectiveWorkspaceAuthorities( - approval.workspaceId, - user.id, - this.prisma - ) - - if ( - workspaceAuthorities.has(Authority.WORKSPACE_ADMIN) || - workspaceAuthorities.has(Authority.MANAGE_APPROVALS) || - approval.requestedById === user.id - ) { - return approval - } else { - throw new UnauthorizedException( - `User with id ${user.id} is not authorized to view approval with id ${approvalId}` - ) - } - } - - /** - * Check if the approval is in a state where it can be enacted upon. - * Actions -> approve, reject, update - * @param approval The approval to check - */ - private isApprovalInActableState(approval: Approval) { - if (approval.status !== ApprovalStatus.PENDING) { - throw new BadRequestException( - `Approval with id ${approval.id} is already approved/rejected` - ) - } - } - - async deleteItem(approval: Approval, user: User) { - switch (approval.itemType) { - case ApprovalItemType.PROJECT: { - const project = await this.prisma.project.findUnique({ - where: { - id: approval.itemId - } - }) - await this.projectService.delete(user, project) - break - } - case ApprovalItemType.ENVIRONMENT: { - const environment = await this.prisma.environment.findUnique({ - where: { - id: approval.itemId - }, - include: { - project: true - } - }) - await this.environmentService.delete(user, environment) - break - } - case ApprovalItemType.SECRET: { - const secret = await this.prisma.secret.findUnique({ - where: { - id: approval.itemId - }, - include: { - project: true - } - }) - await this.secretService.delete(user, secret) - break - } - case ApprovalItemType.VARIABLE: { - const variable = await this.prisma.variable.findUnique({ - where: { - id: approval.itemId - }, - include: { - project: true - } - }) - await this.variableService.delete(user, variable) - break - } - } - } -} diff --git a/apps/api/src/common/alphanumeric-reason-pipe.spec.ts b/apps/api/src/common/alphanumeric-reason-pipe.spec.ts deleted file mode 100644 index c1f9b1b9..00000000 --- a/apps/api/src/common/alphanumeric-reason-pipe.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { AlphanumericReasonValidationPipe } from './alphanumeric-reason-pipe' -import { BadRequestException } from '@nestjs/common' - -describe('AlphanumericReasonValidationPipe', () => { - let pipe: AlphanumericReasonValidationPipe - - beforeEach(() => { - pipe = new AlphanumericReasonValidationPipe() - }) - - it('should allow alphanumeric string', () => { - const validInput = 'Test123' - expect(pipe.transform(validInput)).toBe(validInput) - }) - - it('should not allow strings with only spaces', () => { - expect(() => pipe.transform(' ')).toThrow(BadRequestException) - }) - - it('should throw BadRequestException for non-alphanumeric string', () => { - const invalidInput = 'Test123$%^' - try { - pipe.transform(invalidInput) - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException) - expect(e.message).toBe( - 'Reason must contain only alphanumeric characters and no leading or trailing spaces.' - ) - } - }) -}) diff --git a/apps/api/src/common/alphanumeric-reason-pipe.ts b/apps/api/src/common/alphanumeric-reason-pipe.ts deleted file mode 100644 index a0515b57..00000000 --- a/apps/api/src/common/alphanumeric-reason-pipe.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common' - -@Injectable() -export class AlphanumericReasonValidationPipe implements PipeTransform { - transform(value: string) { - if (/^[a-zA-Z0-9]+(?: [a-zA-Z0-9]+)*$/.test(value)) { - return value - } else { - throw new BadRequestException( - 'Reason must contain only alphanumeric characters and no leading or trailing spaces.' - ) - } - } -} diff --git a/apps/api/src/common/authority-checker.service.ts b/apps/api/src/common/authority-checker.service.ts index e4418f83..ec789533 100644 --- a/apps/api/src/common/authority-checker.service.ts +++ b/apps/api/src/common/authority-checker.service.ts @@ -7,7 +7,6 @@ import { } from '@prisma/client' import { VariableWithProjectAndVersion } from '../variable/variable.types' import { - BadRequestException, Injectable, NotFoundException, UnauthorizedException @@ -21,12 +20,9 @@ import { CustomLoggerService } from './logger.service' export interface AuthorityInput { userId: string - entity: { - id?: string - name?: string - } authority: Authority prisma: PrismaClient + entity: { id?: string; name?: string } } @Injectable() @@ -41,7 +37,7 @@ export class AuthorityCheckerService { let workspace: Workspace try { - if (entity?.id) { + if (entity.id) { workspace = await prisma.workspace.findUnique({ where: { id: entity.id @@ -50,7 +46,7 @@ export class AuthorityCheckerService { } else { workspace = await prisma.workspace.findFirst({ where: { - name: entity?.name, + name: entity.name, members: { some: { userId: userId } } } }) @@ -93,7 +89,7 @@ export class AuthorityCheckerService { let project: ProjectWithSecrets try { - if (entity?.id) { + if (entity.id) { project = await prisma.project.findUnique({ where: { id: entity.id @@ -105,7 +101,7 @@ export class AuthorityCheckerService { } else { project = await prisma.project.findFirst({ where: { - name: entity?.name, + name: entity.name, workspace: { members: { some: { userId: userId } } } }, include: { @@ -120,7 +116,7 @@ export class AuthorityCheckerService { // If the project is not found, throw an error if (!project) { - throw new NotFoundException(`Project with id ${entity?.id} not found`) + throw new NotFoundException(`Project with id ${entity.id} not found`) } // Get the authorities of the user in the workspace with the project @@ -134,19 +130,6 @@ export class AuthorityCheckerService { prisma ) - // If the project is pending creation, only the user who created the project, a workspace admin or - // a user with the MANAGE_APPROVALS authority can fetch the project - if ( - project.pendingCreation && - !permittedAuthoritiesForWorkspace.has(Authority.WORKSPACE_ADMIN) && - !permittedAuthoritiesForWorkspace.has(Authority.MANAGE_APPROVALS) && - project.lastUpdatedById !== userId - ) { - throw new BadRequestException( - `The project with id ${entity?.id} is pending creation and cannot be fetched by the user with id ${userId}` - ) - } - const projectAccessLevel = project.accessLevel switch (projectAccessLevel) { case ProjectAccessLevel.GLOBAL: @@ -159,7 +142,7 @@ export class AuthorityCheckerService { !permittedAuthoritiesForWorkspace.has(Authority.WORKSPACE_ADMIN) ) { throw new UnauthorizedException( - `User with id ${userId} does not have the authority in the project with id ${entity?.id}` + `User with id ${userId} does not have the authority in the project with id ${entity.id}` ) } } @@ -172,7 +155,7 @@ export class AuthorityCheckerService { !permittedAuthoritiesForWorkspace.has(Authority.WORKSPACE_ADMIN) ) { throw new UnauthorizedException( - `User with id ${userId} does not have the authority in the project with id ${entity?.id}` + `User with id ${userId} does not have the authority in the project with id ${entity.id}` ) } break @@ -185,7 +168,7 @@ export class AuthorityCheckerService { !permittedAuthoritiesForProject.has(Authority.WORKSPACE_ADMIN) ) { throw new UnauthorizedException( - `User with id ${userId} does not have the authority in the project with id ${entity?.id}` + `User with id ${userId} does not have the authority in the project with id ${entity.id}` ) } @@ -204,7 +187,7 @@ export class AuthorityCheckerService { let environment: EnvironmentWithProject try { - if (entity?.id) { + if (entity.id) { environment = await prisma.environment.findUnique({ where: { id: entity.id @@ -216,7 +199,7 @@ export class AuthorityCheckerService { } else { environment = await prisma.environment.findFirst({ where: { - name: entity?.name, + name: entity.name, project: { workspace: { members: { some: { userId: userId } } } } }, include: { @@ -249,19 +232,6 @@ export class AuthorityCheckerService { ) } - // If the environment is pending creation, only the user who created the environment, a workspace admin or - // a user with the MANAGE_APPROVALS authority can fetch the environment - if ( - environment.pendingCreation && - !permittedAuthorities.has(Authority.WORKSPACE_ADMIN) && - !permittedAuthorities.has(Authority.MANAGE_APPROVALS) && - environment.lastUpdatedById !== userId - ) { - throw new BadRequestException( - `The environment with id ${entity.id} is pending creation and cannot be fetched by the user with id ${userId}` - ) - } - return environment } @@ -274,39 +244,25 @@ export class AuthorityCheckerService { let variable: VariableWithProjectAndVersion try { - if (entity?.id) { + if (entity.id) { variable = await prisma.variable.findUnique({ where: { id: entity.id }, include: { versions: true, - project: true, - environment: { - select: { - id: true, - name: true - } - } + project: true } }) } else { variable = await prisma.variable.findFirst({ where: { - name: entity?.name, - environment: { - project: { workspace: { members: { some: { userId: userId } } } } - } + name: entity.name, + project: { workspace: { members: { some: { userId: userId } } } } }, include: { versions: true, - project: true, - environment: { - select: { - id: true, - name: true - } - } + project: true } }) } @@ -336,19 +292,6 @@ export class AuthorityCheckerService { ) } - // If the variable is pending creation, only the user who created the variable, a workspace admin or - // a user with the MANAGE_APPROVALS authority can fetch the variable - if ( - variable.pendingCreation && - !permittedAuthorities.has(Authority.WORKSPACE_ADMIN) && - !permittedAuthorities.has(Authority.MANAGE_APPROVALS) && - variable.lastUpdatedById !== userId - ) { - throw new BadRequestException( - `The variable with id ${entity.id} is pending creation and cannot be fetched by the user with id ${userId}` - ) - } - return variable } @@ -361,39 +304,25 @@ export class AuthorityCheckerService { let secret: SecretWithProjectAndVersion try { - if (entity?.id) { + if (entity.id) { secret = await prisma.secret.findUnique({ where: { id: entity.id }, include: { versions: true, - project: true, - environment: { - select: { - id: true, - name: true - } - } + project: true } }) } else { secret = await prisma.secret.findFirst({ where: { - name: entity?.name, - environment: { - project: { workspace: { members: { some: { userId: userId } } } } - } + name: entity.name, + project: { workspace: { members: { some: { userId: userId } } } } }, include: { versions: true, - project: true, - environment: { - select: { - id: true, - name: true - } - } + project: true } }) } @@ -423,19 +352,6 @@ export class AuthorityCheckerService { ) } - // If the secret is pending creation, only the user who created the secret, a workspace admin or - // a user with the MANAGE_APPROVALS authority can fetch the secret - if ( - secret.pendingCreation && - !permittedAuthorities.has(Authority.WORKSPACE_ADMIN) && - !permittedAuthorities.has(Authority.MANAGE_APPROVALS) && - secret.lastUpdatedById !== userId - ) { - throw new BadRequestException( - `The secret with id ${entity.id} is pending creation and cannot be fetched by the user with id ${userId}` - ) - } - return secret } @@ -448,7 +364,7 @@ export class AuthorityCheckerService { let integration: Integration | null try { - if (entity?.id) { + if (entity.id) { integration = await prisma.integration.findUnique({ where: { id: entity.id @@ -457,7 +373,7 @@ export class AuthorityCheckerService { } else { integration = await prisma.integration.findFirst({ where: { - name: entity?.name, + name: entity.name, workspace: { members: { some: { userId: userId } } } } }) diff --git a/apps/api/src/common/create-approval.ts b/apps/api/src/common/create-approval.ts deleted file mode 100644 index c97744c0..00000000 --- a/apps/api/src/common/create-approval.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { - ApprovalAction, - ApprovalItemType, - EventSource, - EventType, - User, - Workspace -} from '@prisma/client' -import { PrismaService } from 'src/prisma/prisma.service' -import createEvent from './create-event' -import { Logger } from '@nestjs/common' - -const logger = new Logger('CreateApproval') - -/** - * Given the itemType, action, itemId, userId and reason, create an approval, - * if an approval already exists for the item, throw a ConflictException. - * Creating an approval changes the state of the item to pending - * @param itemType The type of item to approve - * @param action Action performed if the approval is approved - * @param itemId The id of the item that will undergo approval - * @param userId The id of the user creating the approval - * @param reason The reason for approving the item - * @param metadata Data that the item will be updated with if approved (only applicable for UPDATE approvals) - * @returns The created approval - * @throws ConflictException if an approval already exists for the item - */ -export default async function createApproval( - data: { - itemType: ApprovalItemType - action: ApprovalAction - itemId: string - workspaceId: Workspace['id'] - user: User - reason?: string - metadata?: Record - }, - prisma: PrismaService -) { - // Create the approval - const approval = await prisma.approval.create({ - data: { - itemType: data.itemType, - itemId: data.itemId, - action: data.action, - metadata: data.metadata ?? {}, - reason: data.reason, - workspace: { - connect: { - id: data.workspaceId - } - }, - requestedBy: { - connect: { - id: data.user.id - } - } - } - }) - - logger.log( - `Approval for ${data.itemType} with id ${data.itemId} created by ${data.user.id}` - ) - - await createEvent( - { - triggeredBy: data.user, - entity: approval, - type: EventType.APPROVAL_CREATED, - source: EventSource.APPROVAL, - title: `Approval for ${data.itemType} with id ${data.itemId} created`, - metadata: { - itemType: data.itemType, - itemId: data.itemId, - action: data.action, - reason: data.reason - }, - workspaceId: data.workspaceId - }, - prisma - ) - - return approval -} diff --git a/apps/api/src/common/create-event.ts b/apps/api/src/common/create-event.ts index 74c93d2b..15d0809c 100644 --- a/apps/api/src/common/create-event.ts +++ b/apps/api/src/common/create-event.ts @@ -12,7 +12,6 @@ import { Workspace, WorkspaceRole, Variable, - Approval, Integration } from '@prisma/client' import { JsonObject } from '@prisma/client/runtime/library' @@ -32,7 +31,6 @@ export default async function createEvent( | WorkspaceRole | Secret | Variable - | Approval | Integration type: EventType source: EventSource @@ -95,16 +93,11 @@ export default async function createEvent( case EventSource.SECRET: const secret = data.entity as Secret projectId = secret.projectId - environmentId = secret?.environmentId break case EventSource.VARIABLE: const variable = data.entity as Variable projectId = variable.projectId - environmentId = variable?.environmentId - break - - case EventSource.APPROVAL: break case EventSource.INTEGRATION: diff --git a/apps/api/src/common/create-workspace.ts b/apps/api/src/common/create-workspace.ts index e1ee7a21..b7ae158d 100644 --- a/apps/api/src/common/create-workspace.ts +++ b/apps/api/src/common/create-workspace.ts @@ -19,7 +19,6 @@ export default async function createWorkspace( id: workspaceId, name: dto.name, description: dto.description, - approvalEnabled: dto.approvalEnabled, isFreeTier: true, ownerId: user.id, isDefault, diff --git a/apps/api/src/common/get-default-project-environment.ts b/apps/api/src/common/get-default-project-environment.ts deleted file mode 100644 index f451c902..00000000 --- a/apps/api/src/common/get-default-project-environment.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Environment, PrismaClient, Project } from '@prisma/client' - -export default async function getDefaultEnvironmentOfProject( - projectId: Project['id'], - prisma: PrismaClient -): Promise { - return await prisma.environment.findFirst({ - where: { - projectId, - isDefault: true - } - }) -} diff --git a/apps/api/src/common/workspace-approval-enabled.ts b/apps/api/src/common/workspace-approval-enabled.ts deleted file mode 100644 index c73579a4..00000000 --- a/apps/api/src/common/workspace-approval-enabled.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Workspace } from '@prisma/client' -import { PrismaService } from 'src/prisma/prisma.service' - -/** - * Given a workspaceId, return whether approval workflow is enabled for a workspace - * @param workspaceId The id of the workspace - * @param prisma The PrismaService - * @returns Whether approval workflow is enabled for the workspace - */ -export default async function workspaceApprovalEnabled( - workspaceId: Workspace['id'], - prisma: PrismaService -): Promise { - const workspace = await prisma.workspace.findUnique({ - where: { - id: workspaceId - }, - select: { - approvalEnabled: true - } - }) - - return workspace.approvalEnabled -} diff --git a/apps/api/src/environment/controller/environment.controller.ts b/apps/api/src/environment/controller/environment.controller.ts index 57e894ab..f0f09f52 100644 --- a/apps/api/src/environment/controller/environment.controller.ts +++ b/apps/api/src/environment/controller/environment.controller.ts @@ -14,7 +14,6 @@ import { CreateEnvironment } from '../dto/create.environment/create.environment' import { Authority, User } from '@prisma/client' import { UpdateEnvironment } from '../dto/update.environment/update.environment' import { RequiredApiKeyAuthorities } from '../../decorators/required-api-key-authorities.decorator' -import { AlphanumericReasonValidationPipe } from '../../common/alphanumeric-reason-pipe' @Controller('environment') export class EnvironmentController { @@ -25,15 +24,9 @@ export class EnvironmentController { async createEnvironment( @CurrentUser() user: User, @Body() dto: CreateEnvironment, - @Param('projectId') projectId: string, - @Query('reason', AlphanumericReasonValidationPipe) reason: string + @Param('projectId') projectId: string ) { - return await this.environmentService.createEnvironment( - user, - dto, - projectId, - reason - ) + return await this.environmentService.createEnvironment(user, dto, projectId) } @Put(':environmentId') @@ -41,14 +34,12 @@ export class EnvironmentController { async updateEnvironment( @CurrentUser() user: User, @Body() dto: UpdateEnvironment, - @Param('environmentId') environmentId: string, - @Query('reason', AlphanumericReasonValidationPipe) reason: string + @Param('environmentId') environmentId: string ) { return await this.environmentService.updateEnvironment( user, dto, - environmentId, - reason + environmentId ) } @@ -87,13 +78,8 @@ export class EnvironmentController { @RequiredApiKeyAuthorities(Authority.DELETE_ENVIRONMENT) async deleteEnvironment( @CurrentUser() user: User, - @Param('environmentId') environmentId: string, - @Query('reason', AlphanumericReasonValidationPipe) reason: string + @Param('environmentId') environmentId: string ) { - return await this.environmentService.deleteEnvironment( - user, - environmentId, - reason - ) + return await this.environmentService.deleteEnvironment(user, environmentId) } } diff --git a/apps/api/src/environment/dto/create.environment/create.environment.ts b/apps/api/src/environment/dto/create.environment/create.environment.ts index 00631dfd..db1adb9a 100644 --- a/apps/api/src/environment/dto/create.environment/create.environment.ts +++ b/apps/api/src/environment/dto/create.environment/create.environment.ts @@ -1,4 +1,4 @@ -import { IsBoolean, IsOptional, IsString } from 'class-validator' +import { IsOptional, IsString } from 'class-validator' export class CreateEnvironment { @IsString() @@ -7,8 +7,4 @@ export class CreateEnvironment { @IsString() @IsOptional() description?: string - - @IsBoolean() - @IsOptional() - isDefault?: boolean } diff --git a/apps/api/src/environment/environment.e2e.spec.ts b/apps/api/src/environment/environment.e2e.spec.ts index dea45961..2c4517f0 100644 --- a/apps/api/src/environment/environment.e2e.spec.ts +++ b/apps/api/src/environment/environment.e2e.spec.ts @@ -96,8 +96,7 @@ describe('Environment Controller Tests', () => { environments: [ { name: 'Environment 1', - description: 'Default environment', - isDefault: true + description: 'Environment 1 description' }, { name: 'Environment 2', @@ -144,8 +143,7 @@ describe('Environment Controller Tests', () => { url: `/environment/${project1.id}`, payload: { name: 'Environment 3', - description: 'Environment 3 description', - isDefault: true + description: 'Environment 3 description' }, headers: { 'x-e2e-user-email': user1.email @@ -155,7 +153,6 @@ describe('Environment Controller Tests', () => { expect(response.statusCode).toBe(201) expect(response.json().name).toBe('Environment 3') expect(response.json().description).toBe('Environment 3 description') - expect(response.json().isDefault).toBe(true) const environmentFromDb = await prisma.environment.findUnique({ where: { @@ -166,24 +163,13 @@ describe('Environment Controller Tests', () => { expect(environmentFromDb).toBeDefined() }) - it('should ensure there is only one default environment per project', async () => { - const environments = await prisma.environment.findMany({ - where: { - projectId: project1.id - } - }) - - expect(environments.filter((e) => e.isDefault).length).toBe(1) - }) - it('should not be able to create an environment in a project that does not exist', async () => { const response = await app.inject({ method: 'POST', url: `/environment/123`, payload: { name: 'Environment 1', - description: 'Environment 1 description', - isDefault: true + description: 'Environment 1 description' }, headers: { 'x-e2e-user-email': user1.email @@ -200,8 +186,7 @@ describe('Environment Controller Tests', () => { url: `/environment/${project1.id}`, payload: { name: 'Environment 1', - description: 'Environment 1 description', - isDefault: true + description: 'Environment 1 description' }, headers: { 'x-e2e-user-email': user2.email @@ -220,8 +205,7 @@ describe('Environment Controller Tests', () => { url: `/environment/${project1.id}`, payload: { name: 'Environment 1', - description: 'Environment 1 description', - isDefault: true + description: 'Environment 1 description' }, headers: { 'x-e2e-user-email': user1.email @@ -230,39 +214,10 @@ describe('Environment Controller Tests', () => { expect(response.statusCode).toBe(409) expect(response.json().message).toBe( - `Environment with name Environment 1 already exists in project ${project1.name} (${project1.id})` + `Environment with name Environment 1 already exists in project ${project1.id}` ) }) - it('should not make other environments non-default if the current environment is not the default one', async () => { - const response = await app.inject({ - method: 'POST', - url: `/environment/${project1.id}`, - payload: { - name: 'Environment 3', - description: 'Environment 3 description', - isDefault: false - }, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(201) - expect(response.json().name).toBe('Environment 3') - expect(response.json().description).toBe('Environment 3 description') - expect(response.json().isDefault).toBe(false) - - const environments = await prisma.environment.findMany({ - where: { - projectId: project1.id - } - }) - - expect(environments.length).toBe(3) - expect(environments.filter((e) => e.isDefault).length).toBe(1) - }) - it('should have created a ENVIRONMENT_ADDED event', async () => { // Create an environment await environmentService.createEnvironment( @@ -308,14 +263,11 @@ describe('Environment Controller Tests', () => { id: environment1.id, name: 'Environment 1 Updated', description: 'Environment 1 description updated', - isDefault: true, projectId: project1.id, lastUpdatedById: user1.id, lastUpdatedBy: expect.any(Object), - secrets: [], createdAt: expect.any(String), updatedAt: expect.any(String), - pendingCreation: false, project: expect.any(Object) }) @@ -404,40 +356,6 @@ describe('Environment Controller Tests', () => { expect(event.itemId).toBeDefined() }) - it('should make other environments non-default if the current environment is the default one', async () => { - const response = await app.inject({ - method: 'PUT', - url: `/environment/${environment2.id}`, - payload: { - name: 'Environment 2 Updated', - description: 'Environment 2 description updated', - isDefault: true - }, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - expect(response.json().name).toBe('Environment 2 Updated') - expect(response.json().description).toBe( - 'Environment 2 description updated' - ) - expect(response.json().isDefault).toBe(true) - - const environments = await prisma.environment.findMany({ - where: { - projectId: project1.id - } - }) - - expect(environments.length).toBe(2) - expect(environments.filter((e) => e.isDefault).length).toBe(1) - - environment2 = response.json() - environment1.isDefault = false - }) - it('should be able to fetch an environment', async () => { const response = await app.inject({ method: 'GET', @@ -449,8 +367,7 @@ describe('Environment Controller Tests', () => { expect(response.statusCode).toBe(200) expect(response.json().name).toBe('Environment 1') - expect(response.json().description).toBe('Default environment') - expect(response.json().isDefault).toBe(true) + expect(response.json().description).toBe('Environment 1 description') }) it('should not be able to fetch an environment that does not exist', async () => { @@ -582,35 +499,13 @@ describe('Environment Controller Tests', () => { ) }) - it('should not be able to delete the default environment of a project', async () => { - const response = await app.inject({ - method: 'DELETE', - url: `/environment/${environment1.id}`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(400) - expect(response.json().message).toBe( - 'Cannot delete the default environment' - ) - }) - - it('should not be able to make the only environment non-default', async () => { - await prisma.environment.deleteMany({ - where: { - projectId: project1.id, - isDefault: false - } - }) + it('should not be able to delete the only environment in a project', async () => { + // Delete the other environment + await environmentService.deleteEnvironment(user1, environment2.id) const response = await app.inject({ - method: 'PUT', + method: 'DELETE', url: `/environment/${environment1.id}`, - payload: { - isDefault: false - }, headers: { 'x-e2e-user-email': user1.email } @@ -618,7 +513,7 @@ describe('Environment Controller Tests', () => { expect(response.statusCode).toBe(400) expect(response.json().message).toBe( - 'Cannot make the last environment non-default' + 'Cannot delete the last environment in the project' ) }) }) diff --git a/apps/api/src/environment/service/environment.service.ts b/apps/api/src/environment/service/environment.service.ts index 1a0eb800..2da5c7ba 100644 --- a/apps/api/src/environment/service/environment.service.ts +++ b/apps/api/src/environment/service/environment.service.ts @@ -1,12 +1,10 @@ import { BadRequestException, ConflictException, - Injectable + Injectable, + Logger } from '@nestjs/common' import { - ApprovalAction, - ApprovalItemType, - ApprovalStatus, Authority, Environment, EventSource, @@ -18,14 +16,12 @@ import { CreateEnvironment } from '../dto/create.environment/create.environment' import { UpdateEnvironment } from '../dto/update.environment/update.environment' import { PrismaService } from '../../prisma/prisma.service' import createEvent from '../../common/create-event' -import workspaceApprovalEnabled from '../../common/workspace-approval-enabled' -import { EnvironmentWithProject } from '../environment.types' -import createApproval from '../../common/create-approval' -import { UpdateEnvironmentMetadata } from '../../approval/approval.types' import { AuthorityCheckerService } from '../../common/authority-checker.service' @Injectable() export class EnvironmentService { + private readonly logger = new Logger(EnvironmentService.name) + constructor( private readonly prisma: PrismaService, private readonly authorityCheckerService: AuthorityCheckerService @@ -34,8 +30,7 @@ export class EnvironmentService { async createEnvironment( user: User, dto: CreateEnvironment, - projectId: Project['id'], - reason?: string + projectId: Project['id'] ) { // Check if the user has the required role to create an environment const project = @@ -47,52 +42,28 @@ export class EnvironmentService { }) // Check if an environment with the same name already exists - if (await this.environmentExists(dto.name, projectId)) { - throw new ConflictException( - `Environment with name ${dto.name} already exists in project ${project.name} (${project.id})` - ) - } - - // If the current environment needs to be the default one, we will - // need to update the existing default environment to be a regular one - const ops = [] - - if (dto.isDefault) { - ops.push(this.makeAllNonDefault(projectId)) - } - - const approvalEnabled = await workspaceApprovalEnabled( - project.workspaceId, - this.prisma - ) + await this.environmentExists(dto.name, projectId) // Create the environment - ops.push( - this.prisma.environment.create({ - data: { - name: dto.name, - description: dto.description, - isDefault: dto.isDefault, - pendingCreation: project.pendingCreation || approvalEnabled, - project: { - connect: { - id: projectId - } - }, - lastUpdatedBy: { - connect: { - id: user.id - } + const environment = await this.prisma.environment.create({ + data: { + name: dto.name, + description: dto.description, + project: { + connect: { + id: projectId } }, - include: { - project: true + lastUpdatedBy: { + connect: { + id: user.id + } } - }) - ) - - const result = await this.prisma.$transaction(ops) - const environment: EnvironmentWithProject = result[result.length - 1] + }, + include: { + project: true + } + }) await createEvent( { @@ -112,32 +83,17 @@ export class EnvironmentService { this.prisma ) - if (!project.pendingCreation && approvalEnabled) { - const approval = await createApproval( - { - action: ApprovalAction.CREATE, - itemType: ApprovalItemType.ENVIRONMENT, - itemId: environment.id, - reason, - user, - workspaceId: project.workspaceId - }, - this.prisma - ) - return { - environment, - approval - } - } else { - return environment - } + this.logger.log( + `Environment ${environment.name} created in project ${project.name} (${project.id})` + ) + + return environment } async updateEnvironment( user: User, dto: UpdateEnvironment, - environmentId: Environment['id'], - reason?: string + environmentId: Environment['id'] ) { const environment = await this.authorityCheckerService.checkAuthorityOverEnvironment({ @@ -148,38 +104,46 @@ export class EnvironmentService { }) // Check if an environment with the same name already exists - if ( - dto.name && - (environment.name === dto.name || - (await this.environmentExists(dto.name, environment.projectId))) - ) { - throw new ConflictException( - `Environment with name ${dto.name} already exists in project ${environment.projectId}` - ) - } + dto.name && (await this.environmentExists(dto.name, environment.projectId)) - if ( - !environment.pendingCreation && - (await workspaceApprovalEnabled( - environment.project.workspaceId, - this.prisma - )) - ) { - return await createApproval( - { - action: ApprovalAction.UPDATE, - itemType: ApprovalItemType.ENVIRONMENT, - itemId: environment.id, - reason, - user, - workspaceId: environment.project.workspaceId, - metadata: dto + // Update the environment + const updatedEnvironment = await this.prisma.environment.update({ + where: { + id: environment.id + }, + data: { + name: dto.name, + description: dto.description, + lastUpdatedById: user.id + }, + include: { + project: true, + lastUpdatedBy: true + } + }) + + await createEvent( + { + triggeredBy: user, + entity: updatedEnvironment, + type: EventType.ENVIRONMENT_UPDATED, + source: EventSource.ENVIRONMENT, + title: `Environment updated`, + metadata: { + environmentId: updatedEnvironment.id, + name: updatedEnvironment.name, + projectId: updatedEnvironment.projectId }, - this.prisma - ) - } else { - return this.update(user, environment, dto) - } + workspaceId: updatedEnvironment.project.workspaceId + }, + this.prisma + ) + + this.logger.log( + `Environment ${updatedEnvironment.name} updated in project ${updatedEnvironment.project.name} (${updatedEnvironment.project.id})` + ) + + return updatedEnvironment } async getEnvironment(user: User, environmentId: Environment['id']) { @@ -214,7 +178,6 @@ export class EnvironmentService { return await this.prisma.environment.findMany({ where: { projectId, - pendingCreation: false, name: { contains: search } @@ -230,11 +193,7 @@ export class EnvironmentService { }) } - async deleteEnvironment( - user: User, - environmentId: Environment['id'], - reason?: string - ) { + async deleteEnvironment(user: User, environmentId: Environment['id']) { const environment = await this.authorityCheckerService.checkAuthorityOverEnvironment({ userId: user.id, @@ -243,232 +202,64 @@ export class EnvironmentService { prisma: this.prisma }) - // Check if the environment is the default one - if (environment.isDefault) { - throw new BadRequestException('Cannot delete the default environment') - } - - if ( - !environment.pendingCreation && - (await workspaceApprovalEnabled( - environment.project.workspaceId, - this.prisma - )) - ) { - return await createApproval( - { - action: ApprovalAction.DELETE, - itemType: ApprovalItemType.ENVIRONMENT, - itemId: environment.id, - reason, - user, - workspaceId: environment.project.workspaceId - }, - this.prisma - ) - } else { - return this.delete(user, environment) - } - } - - private async environmentExists( - name: Environment['name'], - projectId: Project['id'] - ) { - return await this.prisma.environment.findFirst({ - where: { - name, - projectId, - pendingCreation: false - } - }) - } - - private makeAllNonDefault(projectId: Project['id']) { - return this.prisma.environment.updateMany({ - where: { - projectId - }, - data: { - isDefault: false - } - }) - } - - async makeEnvironmentApproved(environmentId: Environment['id']) { - const environment = await this.prisma.environment.findUnique({ - where: { - id: environmentId - } - }) - - const environmentExists = await this.prisma.environment.count({ + // Check if this is the only existing environment + const count = await this.prisma.environment.count({ where: { - name: environment.name, - pendingCreation: false, projectId: environment.projectId } }) - - if (environmentExists > 0) { - throw new ConflictException( - `Environment with name ${environment.name} already exists in project ${environment.projectId}` + if (count === 1) { + throw new BadRequestException( + 'Cannot delete the last environment in the project' ) } - await this.prisma.environment.update({ - where: { - id: environmentId - }, - data: { - pendingCreation: false, - secrets: { - updateMany: { - where: { - environmentId - }, - data: { - pendingCreation: false - } - } - }, - variables: { - updateMany: { - where: { - environmentId - }, - data: { - pendingCreation: false - } - } - } - } - }) - } - - async update( - user: User, - environment: Environment, - dto: UpdateEnvironment | UpdateEnvironmentMetadata - ) { - const ops = [] - - // If this environment is the last one, and is being updated to be non-default - // we will skip this operation - const count = await this.prisma.environment.count({ + // Delete the environment + await this.prisma.environment.delete({ where: { - projectId: environment.projectId + id: environment.id } }) - if (dto.isDefault === false && environment.isDefault && count === 1) { - throw new BadRequestException( - 'Cannot make the last environment non-default' - ) - } - - // If the current environment needs to be the default one, we will - // need to update the existing default environment to be a regular one - if (dto.isDefault) { - ops.push(this.makeAllNonDefault(environment.projectId)) - } - - // Update the environment - ops.push( - this.prisma.environment.update({ - where: { - id: environment.id - }, - data: { - name: dto.name, - description: dto.description, - isDefault: - dto.isDefault !== undefined && dto.isDefault !== null - ? dto.isDefault - : environment.isDefault, - lastUpdatedById: user.id - }, - include: { - project: true, - secrets: true, - lastUpdatedBy: true - } - }) - ) - - const result = await this.prisma.$transaction(ops) - const updatedEnvironment = result[result.length - 1] - await createEvent( { triggeredBy: user, - entity: updatedEnvironment, - type: EventType.ENVIRONMENT_UPDATED, + type: EventType.ENVIRONMENT_DELETED, source: EventSource.ENVIRONMENT, - title: `Environment updated`, + entity: environment, + title: `Environment deleted`, metadata: { - environmentId: updatedEnvironment.id, - name: updatedEnvironment.name, - projectId: updatedEnvironment.projectId + environmentId: environment.id, + name: environment.name, + projectId: environment.projectId }, - workspaceId: updatedEnvironment.project.workspaceId + workspaceId: environment.project.workspaceId }, this.prisma ) - return updatedEnvironment + this.logger.log( + `Environment ${environment.name} deleted in project ${environment.project.name} (${environment.project.id})` + ) } - async delete(user: User, environment: EnvironmentWithProject) { - const op = [] - - // Delete the environment - op.push( - this.prisma.environment.delete({ + private async environmentExists( + name: Environment['name'], + projectId: Project['id'] + ) { + if ( + (await this.prisma.environment.findUnique({ where: { - id: environment.id + projectId_name: { + projectId, + name + } } - }) - ) - - // If the environment is in pending creation state and the workspace has approval enabled, - // we will need to delete the approval as well - if ( - environment.pendingCreation && - (await workspaceApprovalEnabled( - environment.project.workspaceId, - this.prisma - )) + })) !== null ) { - op.push( - this.prisma.approval.deleteMany({ - where: { - itemId: environment.id, - itemType: ApprovalItemType.ENVIRONMENT, - action: ApprovalAction.CREATE, - status: ApprovalStatus.PENDING - } - }) + throw new ConflictException( + `Environment with name ${name} already exists in project ${projectId}` ) } - - await this.prisma.$transaction(op) - - await createEvent( - { - triggeredBy: user, - type: EventType.ENVIRONMENT_DELETED, - source: EventSource.ENVIRONMENT, - entity: environment, - title: `Environment deleted`, - metadata: { - environmentId: environment.id, - name: environment.name, - projectId: environment.projectId - }, - workspaceId: environment.project.workspaceId - }, - this.prisma - ) } } diff --git a/apps/api/src/event/event.e2e.spec.ts b/apps/api/src/event/event.e2e.spec.ts index 0bdad54c..3ee3aa4a 100644 --- a/apps/api/src/event/event.e2e.spec.ts +++ b/apps/api/src/event/event.e2e.spec.ts @@ -3,7 +3,6 @@ import { NestFastifyApplication } from '@nestjs/platform-fastify' import { - Approval, Environment, EventSeverity, EventSource, @@ -103,8 +102,7 @@ describe('Event Controller Tests', () => { it('should be able to fetch a workspace event', async () => { const newWorkspace = await workspaceService.createWorkspace(user, { name: 'My workspace', - description: 'Some description', - approvalEnabled: false + description: 'Some description' }) workspace = newWorkspace @@ -173,8 +171,7 @@ describe('Event Controller Tests', () => { user, { name: 'My environment', - description: 'Some description', - isDefault: false + description: 'Some description' }, project.id )) as Environment @@ -210,9 +207,13 @@ describe('Event Controller Tests', () => { user, { name: 'My secret', - value: 'My value', + entries: [ + { + value: 'My value', + environmentId: environment.id + } + ], note: 'Some note', - environmentId: environment.id, rotateAfter: '720' }, project.id @@ -248,9 +249,13 @@ describe('Event Controller Tests', () => { user, { name: 'My variable', - value: 'My value', - note: 'Some note', - environmentId: environment.id + entries: [ + { + value: 'My value', + environmentId: environment.id + } + ], + note: 'Some note' }, project.id )) as Variable @@ -319,45 +324,6 @@ describe('Event Controller Tests', () => { expect(event.workspaceId).toBe(workspace.id) }) - it('should be able to fetch a approval event', async () => { - const workspace = await workspaceService.createWorkspace(user, { - name: 'My workspace 100', - description: 'Some description', - approvalEnabled: true - }) - - const updateWorkspaceResponse = (await workspaceService.updateWorkspace( - user, - workspace.id, - { - name: 'My workspace 10' - } - )) as Approval - - expect(updateWorkspaceResponse).toBeDefined() - - const response = await app.inject({ - method: 'GET', - url: `/event/${workspace.id}?source=APPROVAL`, - headers: { - 'x-e2e-user-email': user.email - } - }) - - expect(response.statusCode).toBe(200) - const event = response.json()[0] - - expect(event.id).toBeDefined() - expect(event.title).toBeDefined() - expect(event.source).toBe(EventSource.APPROVAL) - expect(event.triggerer).toBe(EventTriggerer.USER) - expect(event.severity).toBe(EventSeverity.INFO) - expect(event.type).toBe(EventType.APPROVAL_CREATED) - expect(event.timestamp).toBeDefined() - expect(event.itemId).toBe(updateWorkspaceResponse.id) - expect(event.userId).toBe(user.id) - }) - it('should be able to fetch all events', async () => { const response = await app.inject({ method: 'GET', diff --git a/apps/api/src/integration/integration.types.ts b/apps/api/src/integration/integration.types.ts index 422b93bf..b40581b7 100644 --- a/apps/api/src/integration/integration.types.ts +++ b/apps/api/src/integration/integration.types.ts @@ -1,5 +1,4 @@ import { - Approval, Environment, EventSource, EventType, @@ -22,7 +21,6 @@ export interface IntegrationEventData { | WorkspaceRole | Secret | Variable - | Approval | Integration source: EventSource eventType: EventType diff --git a/apps/api/src/integration/plugins/discord/discord.integration.spec.ts b/apps/api/src/integration/plugins/discord/discord.integration.spec.ts index 14d16b40..d27e44bd 100644 --- a/apps/api/src/integration/plugins/discord/discord.integration.spec.ts +++ b/apps/api/src/integration/plugins/discord/discord.integration.spec.ts @@ -16,7 +16,7 @@ describe('Discord Integration Test', () => { it('should have the correct permitted events', () => { const events = integration.getPermittedEvents() expect(events).toBeDefined() - expect(events.size).toBe(31) + expect(events.size).toBe(26) }) it('should have the correct required metadata parameters', () => { diff --git a/apps/api/src/integration/plugins/discord/discord.integration.ts b/apps/api/src/integration/plugins/discord/discord.integration.ts index cafda0eb..4ac55a80 100644 --- a/apps/api/src/integration/plugins/discord/discord.integration.ts +++ b/apps/api/src/integration/plugins/discord/discord.integration.ts @@ -15,8 +15,6 @@ export class DiscordIntegration extends BaseIntegration { public getPermittedEvents(): Set { return new Set([ - EventType.APPROVAL_APPROVED, - EventType.APPROVAL_REJECTED, EventType.INTEGRATION_ADDED, EventType.INTEGRATION_UPDATED, EventType.INTEGRATION_DELETED, @@ -43,11 +41,6 @@ export class DiscordIntegration extends BaseIntegration { EventType.ENVIRONMENT_UPDATED, EventType.ENVIRONMENT_DELETED, EventType.ENVIRONMENT_ADDED, - EventType.APPROVAL_CREATED, - EventType.APPROVAL_UPDATED, - EventType.APPROVAL_DELETED, - EventType.APPROVAL_APPROVED, - EventType.APPROVAL_REJECTED, EventType.INTEGRATION_ADDED, EventType.INTEGRATION_UPDATED, EventType.INTEGRATION_DELETED diff --git a/apps/api/src/integration/service/integration.service.ts b/apps/api/src/integration/service/integration.service.ts index 784e1d72..d1f7bab2 100644 --- a/apps/api/src/integration/service/integration.service.ts +++ b/apps/api/src/integration/service/integration.service.ts @@ -109,7 +109,7 @@ export class IntegrationService { { triggeredBy: user, entity: integration, - type: EventType.APPROVAL_REJECTED, + type: EventType.INTEGRATION_ADDED, source: EventSource.INTEGRATION, title: `Integration ${integration.name} created`, metadata: { diff --git a/apps/api/src/prisma/migrations/20240611165845_refactor_environment_and_versions/migration.sql b/apps/api/src/prisma/migrations/20240611165845_refactor_environment_and_versions/migration.sql new file mode 100644 index 00000000..5cff87b5 --- /dev/null +++ b/apps/api/src/prisma/migrations/20240611165845_refactor_environment_and_versions/migration.sql @@ -0,0 +1,144 @@ +/* + Warnings: + + - The values [MANAGE_APPROVALS] on the enum `Authority` will be removed. If these variants are still used in the database, this will fail. + - The values [APPROVAL] on the enum `EventSource` will be removed. If these variants are still used in the database, this will fail. + - The values [APPROVAL_CREATED,APPROVAL_UPDATED,APPROVAL_DELETED,APPROVAL_APPROVED,APPROVAL_REJECTED] on the enum `EventType` will be removed. If these variants are still used in the database, this will fail. + - The values [APPROVAL_CREATED,APPROVAL_UPDATED,APPROVAL_DELETED,APPROVAL_APPROVED,APPROVAL_REJECTED] on the enum `NotificationType` will be removed. If these variants are still used in the database, this will fail. + - You are about to drop the column `isDefault` on the `Environment` table. All the data in the column will be lost. + - You are about to drop the column `pendingCreation` on the `Environment` table. All the data in the column will be lost. + - You are about to drop the column `pendingCreation` on the `Project` table. All the data in the column will be lost. + - You are about to drop the column `environmentId` on the `Secret` table. All the data in the column will be lost. + - You are about to drop the column `pendingCreation` on the `Secret` table. All the data in the column will be lost. + - You are about to drop the column `environmentId` on the `Variable` table. All the data in the column will be lost. + - You are about to drop the column `pendingCreation` on the `Variable` table. All the data in the column will be lost. + - You are about to drop the column `approvalEnabled` on the `Workspace` table. All the data in the column will be lost. + - You are about to drop the `Approval` table. If the table is not empty, all the data it contains will be lost. + - A unique constraint covering the columns `[projectId,name]` on the table `Environment` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[projectId,name]` on the table `Secret` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[projectId,name]` on the table `Variable` will be added. If there are existing duplicate values, this will fail. + - Added the required column `environmentId` to the `SecretVersion` table without a default value. This is not possible if the table is not empty. + - Added the required column `environmentId` to the `VariableVersion` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterEnum +BEGIN; +CREATE TYPE "Authority_new" AS ENUM ('CREATE_PROJECT', 'READ_USERS', 'ADD_USER', 'REMOVE_USER', 'UPDATE_USER_ROLE', 'READ_WORKSPACE', 'UPDATE_WORKSPACE', 'DELETE_WORKSPACE', 'CREATE_WORKSPACE_ROLE', 'READ_WORKSPACE_ROLE', 'UPDATE_WORKSPACE_ROLE', 'DELETE_WORKSPACE_ROLE', 'WORKSPACE_ADMIN', 'READ_PROJECT', 'UPDATE_PROJECT', 'DELETE_PROJECT', 'CREATE_SECRET', 'READ_SECRET', 'UPDATE_SECRET', 'DELETE_SECRET', 'CREATE_ENVIRONMENT', 'READ_ENVIRONMENT', 'UPDATE_ENVIRONMENT', 'DELETE_ENVIRONMENT', 'CREATE_VARIABLE', 'READ_VARIABLE', 'UPDATE_VARIABLE', 'DELETE_VARIABLE', 'CREATE_INTEGRATION', 'READ_INTEGRATION', 'UPDATE_INTEGRATION', 'DELETE_INTEGRATION', 'CREATE_WORKSPACE', 'CREATE_API_KEY', 'READ_API_KEY', 'UPDATE_API_KEY', 'DELETE_API_KEY', 'UPDATE_PROFILE', 'READ_SELF', 'UPDATE_SELF', 'READ_EVENT'); +ALTER TABLE "WorkspaceRole" ALTER COLUMN "authorities" TYPE "Authority_new"[] USING ("authorities"::text::"Authority_new"[]); +ALTER TABLE "ApiKey" ALTER COLUMN "authorities" TYPE "Authority_new"[] USING ("authorities"::text::"Authority_new"[]); +ALTER TYPE "Authority" RENAME TO "Authority_old"; +ALTER TYPE "Authority_new" RENAME TO "Authority"; +DROP TYPE "Authority_old"; +COMMIT; + +-- AlterEnum +BEGIN; +CREATE TYPE "EventSource_new" AS ENUM ('SECRET', 'VARIABLE', 'ENVIRONMENT', 'PROJECT', 'WORKSPACE', 'WORKSPACE_ROLE', 'INTEGRATION'); +ALTER TABLE "Event" ALTER COLUMN "source" TYPE "EventSource_new" USING ("source"::text::"EventSource_new"); +ALTER TYPE "EventSource" RENAME TO "EventSource_old"; +ALTER TYPE "EventSource_new" RENAME TO "EventSource"; +DROP TYPE "EventSource_old"; +COMMIT; + +-- AlterEnum +BEGIN; +CREATE TYPE "EventType_new" AS ENUM ('INVITED_TO_WORKSPACE', 'REMOVED_FROM_WORKSPACE', 'ACCEPTED_INVITATION', 'DECLINED_INVITATION', 'CANCELLED_INVITATION', 'LEFT_WORKSPACE', 'WORKSPACE_MEMBERSHIP_UPDATED', 'WORKSPACE_UPDATED', 'WORKSPACE_CREATED', 'WORKSPACE_ROLE_CREATED', 'WORKSPACE_ROLE_UPDATED', 'WORKSPACE_ROLE_DELETED', 'PROJECT_CREATED', 'PROJECT_UPDATED', 'PROJECT_DELETED', 'SECRET_UPDATED', 'SECRET_DELETED', 'SECRET_ADDED', 'VARIABLE_UPDATED', 'VARIABLE_DELETED', 'VARIABLE_ADDED', 'ENVIRONMENT_UPDATED', 'ENVIRONMENT_DELETED', 'ENVIRONMENT_ADDED', 'INTEGRATION_ADDED', 'INTEGRATION_UPDATED', 'INTEGRATION_DELETED'); +ALTER TABLE "Event" ALTER COLUMN "type" TYPE "EventType_new" USING ("type"::text::"EventType_new"); +ALTER TABLE "Integration" ALTER COLUMN "notifyOn" TYPE "EventType_new"[] USING ("notifyOn"::text::"EventType_new"[]); +ALTER TYPE "EventType" RENAME TO "EventType_old"; +ALTER TYPE "EventType_new" RENAME TO "EventType"; +DROP TYPE "EventType_old"; +COMMIT; + +-- AlterEnum +BEGIN; +CREATE TYPE "NotificationType_new" AS ENUM ('INVITED_TO_PROJECT', 'REMOVED_FROM_PROJECT', 'PROJECT_UPDATED', 'PROJECT_DELETED', 'SECRET_UPDATED', 'SECRET_DELETED', 'SECRET_ADDED', 'API_KEY_UPDATED', 'API_KEY_DELETED', 'API_KEY_ADDED', 'ENVIRONMENT_UPDATED', 'ENVIRONMENT_DELETED', 'ENVIRONMENT_ADDED', 'VARIABLE_UPDATED', 'VARIABLE_DELETED', 'VARIABLE_ADDED'); +ALTER TABLE "Notification" ALTER COLUMN "type" TYPE "NotificationType_new" USING ("type"::text::"NotificationType_new"); +ALTER TYPE "NotificationType" RENAME TO "NotificationType_old"; +ALTER TYPE "NotificationType_new" RENAME TO "NotificationType"; +DROP TYPE "NotificationType_old"; +COMMIT; + +-- DropForeignKey +ALTER TABLE "Approval" DROP CONSTRAINT "Approval_approvedById_fkey"; + +-- DropForeignKey +ALTER TABLE "Approval" DROP CONSTRAINT "Approval_rejectedById_fkey"; + +-- DropForeignKey +ALTER TABLE "Approval" DROP CONSTRAINT "Approval_requestedById_fkey"; + +-- DropForeignKey +ALTER TABLE "Approval" DROP CONSTRAINT "Approval_workspaceId_fkey"; + +-- DropForeignKey +ALTER TABLE "Secret" DROP CONSTRAINT "Secret_environmentId_fkey"; + +-- DropForeignKey +ALTER TABLE "Variable" DROP CONSTRAINT "Variable_environmentId_fkey"; + +-- DropIndex +DROP INDEX "SecretVersion_secretId_version_key"; + +-- DropIndex +DROP INDEX "VariableVersion_variableId_version_key"; + +-- AlterTable +ALTER TABLE "Environment" DROP COLUMN "isDefault", +DROP COLUMN "pendingCreation"; + +-- AlterTable +ALTER TABLE "Project" DROP COLUMN "pendingCreation"; + +-- AlterTable +ALTER TABLE "Secret" DROP COLUMN "environmentId", +DROP COLUMN "pendingCreation"; + +-- AlterTable +ALTER TABLE "SecretVersion" ADD COLUMN "environmentId" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "Variable" DROP COLUMN "environmentId", +DROP COLUMN "pendingCreation"; + +-- AlterTable +ALTER TABLE "VariableVersion" ADD COLUMN "environmentId" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "Workspace" DROP COLUMN "approvalEnabled"; + +-- DropTable +DROP TABLE "Approval"; + +-- DropEnum +DROP TYPE "ApprovalAction"; + +-- DropEnum +DROP TYPE "ApprovalItemType"; + +-- DropEnum +DROP TYPE "ApprovalStatus"; + +-- CreateIndex +CREATE INDEX "Environment_name_idx" ON "Environment"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "Environment_projectId_name_key" ON "Environment"("projectId", "name"); + +-- CreateIndex +CREATE UNIQUE INDEX "Secret_projectId_name_key" ON "Secret"("projectId", "name"); + +-- CreateIndex +CREATE INDEX "SecretVersion_secretId_environmentId_idx" ON "SecretVersion"("secretId", "environmentId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Variable_projectId_name_key" ON "Variable"("projectId", "name"); + +-- CreateIndex +CREATE INDEX "VariableVersion_variableId_environmentId_idx" ON "VariableVersion"("variableId", "environmentId"); + +-- AddForeignKey +ALTER TABLE "SecretVersion" ADD CONSTRAINT "SecretVersion_environmentId_fkey" FOREIGN KEY ("environmentId") REFERENCES "Environment"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VariableVersion" ADD CONSTRAINT "VariableVersion_environmentId_fkey" FOREIGN KEY ("environmentId") REFERENCES "Environment"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/api/src/prisma/schema.prisma b/apps/api/src/prisma/schema.prisma index c67509b5..551bc7f1 100644 --- a/apps/api/src/prisma/schema.prisma +++ b/apps/api/src/prisma/schema.prisma @@ -7,26 +7,6 @@ datasource db { url = env("DATABASE_URL") } -enum ApprovalItemType { - SECRET - VARIABLE - ENVIRONMENT - PROJECT - WORKSPACE -} - -enum ApprovalStatus { - PENDING - APPROVED - REJECTED -} - -enum ApprovalAction { - CREATE - UPDATE - DELETE -} - enum EventSource { SECRET VARIABLE @@ -34,7 +14,6 @@ enum EventSource { PROJECT WORKSPACE WORKSPACE_ROLE - APPROVAL INTEGRATION } @@ -74,11 +53,6 @@ enum EventType { ENVIRONMENT_UPDATED ENVIRONMENT_DELETED ENVIRONMENT_ADDED - APPROVAL_CREATED - APPROVAL_UPDATED - APPROVAL_DELETED - APPROVAL_APPROVED - APPROVAL_REJECTED INTEGRATION_ADDED INTEGRATION_UPDATED INTEGRATION_DELETED @@ -99,7 +73,6 @@ enum Authority { UPDATE_WORKSPACE_ROLE DELETE_WORKSPACE_ROLE WORKSPACE_ADMIN - MANAGE_APPROVALS // Project authorities READ_PROJECT @@ -151,11 +124,6 @@ enum NotificationType { VARIABLE_UPDATED VARIABLE_DELETED VARIABLE_ADDED - APPROVAL_CREATED - APPROVAL_UPDATED - APPROVAL_DELETED - APPROVAL_APPROVED - APPROVAL_REJECTED } enum IntegrationType { @@ -218,21 +186,18 @@ model User { authProvider AuthProvider? subscription Subscription? - workspaceMembers WorkspaceMember[] - workspaces Workspace[] - apiKeys ApiKey[] - otp Otp? - notifications Notification[] - secrets Secret[] // Stores the secrets the user updated - variables Variable[] // Stores the variables the user updated - projects Project[] // Stores the projects the user updated - environments Environment[] // Stores the environments the user updated - secretVersion SecretVersion[] - variableVersion VariableVersion[] - events Event[] - requestedApprovals Approval[] @relation("requestedBy") - approvedApprovals Approval[] @relation("approvedBy") - rejectedApprovals Approval[] @relation("rejectedBy") + workspaceMembers WorkspaceMember[] + workspaces Workspace[] + apiKeys ApiKey[] + otp Otp? + notifications Notification[] + secrets Secret[] // Stores the secrets the user updated + variables Variable[] // Stores the variables the user updated + projects Project[] // Stores the projects the user updated + environments Environment[] // Stores the environments the user updated + secretVersion SecretVersion[] + variableVersion VariableVersion[] + events Event[] @@index([email], name: "email") } @@ -269,20 +234,18 @@ model Integration { } model Environment { - id String @id @default(cuid()) - name String - description String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - isDefault Boolean @default(false) - pendingCreation Boolean @default(false) + id String @id @default(cuid()) + name String + description String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt lastUpdatedBy User? @relation(fields: [lastUpdatedById], references: [id], onUpdate: Cascade, onDelete: SetNull) lastUpdatedById String? - secrets Secret[] - variables Variable[] - integrations Integration[] + secretVersions SecretVersion[] + variableVersions VariableVersion[] + integrations Integration[] project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) projectId String @@ -292,18 +255,17 @@ model Environment { } model Project { - id String @id @default(cuid()) + id String @id @default(cuid()) name String description String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt publicKey String privateKey String? // We store this only if the user wants us to do so! - storePrivateKey Boolean @default(false) - isDisabled Boolean @default(false) // This is set to true when the user stops his subscription and still has premium features in use - accessLevel ProjectAccessLevel @default(PRIVATE) - pendingCreation Boolean @default(false) - isForked Boolean @default(false) + storePrivateKey Boolean @default(false) + isDisabled Boolean @default(false) // This is set to true when the user stops his subscription and still has premium features in use + accessLevel ProjectAccessLevel @default(PRIVATE) + isForked Boolean @default(false) lastUpdatedBy User? @relation(fields: [lastUpdatedById], references: [id], onUpdate: Cascade, onDelete: SetNull) lastUpdatedById String? @@ -389,18 +351,20 @@ model SecretVersion { createdBy User? @relation(fields: [createdById], references: [id], onUpdate: Cascade, onDelete: SetNull) createdById String? - @@unique([secretId, version]) + environmentId String + environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + + @@index([secretId, environmentId]) } model Secret { - id String @id @default(cuid()) - name String - versions SecretVersion[] // Stores the versions of the secret - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - rotateAt DateTime? - note String? - pendingCreation Boolean @default(false) + id String @id @default(cuid()) + name String + versions SecretVersion[] // Stores the versions of the secret + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + rotateAt DateTime? + note String? lastUpdatedBy User? @relation(fields: [lastUpdatedById], references: [id], onUpdate: Cascade, onDelete: SetNull) lastUpdatedById String? @@ -408,8 +372,7 @@ model Secret { projectId String project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) - environmentId String - environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + @@unique([projectId, name]) } model VariableVersion { @@ -424,17 +387,19 @@ model VariableVersion { createdBy User? @relation(fields: [createdById], references: [id], onUpdate: Cascade, onDelete: SetNull) createdById String? - @@unique([variableId, version]) + environmentId String + environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + + @@index([variableId, environmentId]) } model Variable { - id String @id @default(cuid()) - name String - versions VariableVersion[] // Stores the versions of the variable - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - note String? - pendingCreation Boolean @default(false) + id String @id @default(cuid()) + name String + versions VariableVersion[] // Stores the versions of the variable + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + note String? lastUpdatedBy User? @relation(fields: [lastUpdatedById], references: [id], onUpdate: Cascade, onDelete: SetNull) lastUpdatedById String? @@ -442,8 +407,7 @@ model Variable { projectId String project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade) - environmentId String - environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade) + @@unique([projectId, name]) } model ApiKey { @@ -460,11 +424,11 @@ model ApiKey { } model Otp { - id String @id @default(cuid()) - code String @unique - user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) - userId String @unique - createdAt DateTime @default(now()) + id String @id @default(cuid()) + code String @unique + user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade) + userId String @unique + createdAt DateTime @default(now()) expiresAt DateTime emailChange UserEmailChange? @@ -473,15 +437,14 @@ model Otp { } model Workspace { - id String @id @default(cuid()) - name String - description String? - isFreeTier Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - ownerId String - approvalEnabled Boolean @default(false) - isDefault Boolean @default(false) + id String @id @default(cuid()) + name String + description String? + isFreeTier Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + ownerId String + isDefault Boolean @default(false) lastUpdatedBy User? @relation(fields: [lastUpdatedById], references: [id], onUpdate: Cascade, onDelete: SetNull) lastUpdatedById String? @@ -490,37 +453,11 @@ model Workspace { members WorkspaceMember[] roles WorkspaceRole[] events Event[] - approvals Approval[] integrations Integration[] @@unique([name, ownerId]) } -model Approval { - id String @id @default(cuid()) - itemType ApprovalItemType - status ApprovalStatus @default(PENDING) - action ApprovalAction - metadata Json - itemId String - reason String? - createdAt DateTime @default(now()) - approvedAt DateTime? - rejectedAt DateTime? - - requestedBy User? @relation(fields: [requestedById], references: [id], onUpdate: Cascade, onDelete: SetNull, name: "requestedBy") - requestedById String? - approvedBy User? @relation(fields: [approvedById], references: [id], onUpdate: Cascade, onDelete: SetNull, name: "approvedBy") - approvedById String? - rejectedBy User? @relation(fields: [rejectedById], references: [id], onUpdate: Cascade, onDelete: SetNull, name: "rejectedBy") - rejectedById String? - - workspaceId String - workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade, onUpdate: Cascade) - - @@index([itemType, itemId]) -} - model ChangeNotificationSocketMap { id String @id @default(cuid()) socketId String @@ -530,8 +467,8 @@ model ChangeNotificationSocketMap { } model UserEmailChange { - id String @id @default(cuid()) - otp Otp @relation(fields: [otpId], references: [id], onDelete: Cascade, onUpdate: Cascade) - otpId String @unique - newEmail String -} \ No newline at end of file + id String @id @default(cuid()) + otp Otp @relation(fields: [otpId], references: [id], onDelete: Cascade, onUpdate: Cascade) + otpId String @unique + newEmail String +} diff --git a/apps/api/src/project/controller/project.controller.ts b/apps/api/src/project/controller/project.controller.ts index ef928f75..bcc8fa51 100644 --- a/apps/api/src/project/controller/project.controller.ts +++ b/apps/api/src/project/controller/project.controller.ts @@ -14,7 +14,6 @@ import { Authority, Project, User, Workspace } from '@prisma/client' import { CreateProject } from '../dto/create.project/create.project' import { UpdateProject } from '../dto/update.project/update.project' import { RequiredApiKeyAuthorities } from '../../decorators/required-api-key-authorities.decorator' -import { AlphanumericReasonValidationPipe } from '../../common/alphanumeric-reason-pipe' import { ForkProject } from '../dto/fork.project/fork.project' @Controller('project') @@ -26,10 +25,9 @@ export class ProjectController { async createProject( @CurrentUser() user: User, @Param('workspaceId') workspaceId: Workspace['id'], - @Body() dto: CreateProject, - @Query('reason', AlphanumericReasonValidationPipe) reason: string + @Body() dto: CreateProject ) { - return await this.service.createProject(user, workspaceId, dto, reason) + return await this.service.createProject(user, workspaceId, dto) } @Put(':projectId') @@ -37,20 +35,18 @@ export class ProjectController { async updateProject( @CurrentUser() user: User, @Param('projectId') projectId: Project['id'], - @Body() dto: UpdateProject, - @Query('reason', AlphanumericReasonValidationPipe) reason: string + @Body() dto: UpdateProject ) { - return await this.service.updateProject(user, projectId, dto, reason) + return await this.service.updateProject(user, projectId, dto) } @Delete(':projectId') @RequiredApiKeyAuthorities(Authority.DELETE_PROJECT) async deleteProject( @CurrentUser() user: User, - @Param('projectId') projectId: Project['id'], - @Query('reason', AlphanumericReasonValidationPipe) reason: string + @Param('projectId') projectId: Project['id'] ) { - return await this.service.deleteProject(user, projectId, reason) + return await this.service.deleteProject(user, projectId) } @Get(':projectId') diff --git a/apps/api/src/project/project.e2e.spec.ts b/apps/api/src/project/project.e2e.spec.ts index 7a98d0e2..698dfb6e 100644 --- a/apps/api/src/project/project.e2e.spec.ts +++ b/apps/api/src/project/project.e2e.spec.ts @@ -175,13 +175,11 @@ describe('Project Controller Tests', () => { expect(response.json().storePrivateKey).toBe(true) expect(response.json().workspaceId).toBe(workspace1.id) expect(response.json().lastUpdatedById).toBe(user1.id) - expect(response.json().isDisabled).toBe(false) expect(response.json().accessLevel).toBe(ProjectAccessLevel.PRIVATE) expect(response.json().publicKey).toBeDefined() expect(response.json().privateKey).toBeDefined() expect(response.json().createdAt).toBeDefined() expect(response.json().updatedAt).toBeDefined() - expect(response.json().pendingCreation).toBe(false) }) it('should have created a default environment', async () => { @@ -578,9 +576,6 @@ describe('Project Controller Tests', () => { }) expect(environments).toHaveLength(3) - expect(environments[0].isDefault).toBe(true) - expect(environments[1].isDefault).toBe(false) - expect(environments[2].isDefault).toBe(false) }) it('should generate new key-pair if regenerateKeyPair is true and and the project stores the private key or a private key is specified', async () => { @@ -1175,8 +1170,12 @@ describe('Project Controller Tests', () => { user1, { name: 'API_KEY', - value: 'some_key', - environmentId: environment.id + entries: [ + { + value: 'some_key', + environmentId: environment.id + } + ] }, project3.id )) as Secret @@ -1185,7 +1184,12 @@ describe('Project Controller Tests', () => { user1, { name: 'DB_PASSWORD', - value: 'password' + entries: [ + { + value: 'password', + environmentId: environment.id + } + ] }, project3.id )) as Secret @@ -1195,8 +1199,12 @@ describe('Project Controller Tests', () => { user1, { name: 'PORT', - value: '8080', - environmentId: environment.id + entries: [ + { + value: '8080', + environmentId: environment.id + } + ] }, project3.id )) as Variable @@ -1205,7 +1213,12 @@ describe('Project Controller Tests', () => { user1, { name: 'EXPIRY', - value: '3600' + entries: [ + { + value: '3600', + environmentId: environment.id + } + ] }, project3.id )) as Variable @@ -1251,24 +1264,22 @@ describe('Project Controller Tests', () => { forkedVariables expect(secretInDefaultEnvironment).toBeDefined() - expect(secretInDefaultEnvironment.name).toBe(secret2.name) - expect(secretInDefaultEnvironment.environmentId).toBe( - defaultEnvironment.id - ) + expect(secretInDefaultEnvironment.name).toBe(secret1.name) expect(secretInDevEnvironment).toBeDefined() - expect(secretInDevEnvironment.name).toBe(secret1.name) - expect(secretInDevEnvironment.environmentId).toBe(devEnvironment.id) + expect(secretInDevEnvironment.name).toBe(secret2.name) expect(variableInDefaultEnvironment).toBeDefined() - expect(variableInDefaultEnvironment.name).toBe(variable2.name) - expect(variableInDefaultEnvironment.environmentId).toBe( - defaultEnvironment.id - ) + expect(variableInDefaultEnvironment.name).toBe(variable1.name) expect(variableInDevEnvironment).toBeDefined() - expect(variableInDevEnvironment.name).toBe(variable1.name) - expect(variableInDevEnvironment.environmentId).toBe(devEnvironment.id) + expect(variableInDevEnvironment.name).toBe(variable2.name) + + expect(devEnvironment).toBeDefined() + expect(devEnvironment.name).toBe(environment.name) + + expect(defaultEnvironment).toBeDefined() + expect(defaultEnvironment.name).toBe('Default') }) it('should only copy new environments, secrets and variables if sync is not hard', async () => { @@ -1286,8 +1297,12 @@ describe('Project Controller Tests', () => { user1, { name: 'API_KEY', - value: 'some_key', - environmentId: environment.id + entries: [ + { + value: 'some_key', + environmentId: environment.id + } + ] }, project3.id ) @@ -1296,7 +1311,12 @@ describe('Project Controller Tests', () => { user1, { name: 'DB_PASSWORD', - value: 'password' + entries: [ + { + value: 'password', + environmentId: environment.id + } + ] }, project3.id ) @@ -1306,8 +1326,12 @@ describe('Project Controller Tests', () => { user1, { name: 'PORT', - value: '8080', - environmentId: environment.id + entries: [ + { + value: '8080', + environmentId: environment.id + } + ] }, project3.id ) @@ -1316,7 +1340,12 @@ describe('Project Controller Tests', () => { user1, { name: 'EXPIRY', - value: '3600' + entries: [ + { + value: '3600', + environmentId: environment.id + } + ] }, project3.id ) @@ -1345,8 +1374,12 @@ describe('Project Controller Tests', () => { user1, { name: 'NEW_SECRET', - value: 'new_secret', - environmentId: newEnvironmentOriginal.id + entries: [ + { + value: 'new_secret', + environmentId: newEnvironmentOriginal.id + } + ] }, project3.id ) @@ -1356,8 +1389,12 @@ describe('Project Controller Tests', () => { user1, { name: 'NEW_VARIABLE', - value: 'new_variable', - environmentId: newEnvironmentOriginal.id + entries: [ + { + value: 'new_variable', + environmentId: newEnvironmentOriginal.id + } + ] }, project3.id ) @@ -1376,8 +1413,12 @@ describe('Project Controller Tests', () => { user2, { name: 'NEW_SECRET_2', - value: 'new_secret', - environmentId: newEnvironmentForked.id + entries: [ + { + value: 'new_secret', + environmentId: newEnvironmentForked.id + } + ] }, forkedProject.id ) @@ -1387,8 +1428,12 @@ describe('Project Controller Tests', () => { user2, { name: 'NEW_VARIABLE_2', - value: 'new_variable', - environmentId: newEnvironmentForked.id + entries: [ + { + value: 'new_variable', + environmentId: newEnvironmentForked.id + } + ] }, forkedProject.id ) @@ -1442,8 +1487,12 @@ describe('Project Controller Tests', () => { user1, { name: 'API_KEY', - value: 'some_key', - environmentId: environment.id + entries: [ + { + value: 'some_key', + environmentId: environment.id + } + ] }, project3.id ) @@ -1452,7 +1501,12 @@ describe('Project Controller Tests', () => { user1, { name: 'DB_PASSWORD', - value: 'password' + entries: [ + { + value: 'password', + environmentId: environment.id + } + ] }, project3.id ) @@ -1462,8 +1516,12 @@ describe('Project Controller Tests', () => { user1, { name: 'PORT', - value: '8080', - environmentId: environment.id + entries: [ + { + value: '8080', + environmentId: environment.id + } + ] }, project3.id ) @@ -1472,7 +1530,12 @@ describe('Project Controller Tests', () => { user1, { name: 'EXPIRY', - value: '3600' + entries: [ + { + value: '3600', + environmentId: environment.id + } + ] }, project3.id ) @@ -1501,8 +1564,12 @@ describe('Project Controller Tests', () => { user1, { name: 'NEW_SECRET', - value: 'new_secret', - environmentId: newEnvironmentOriginal.id + entries: [ + { + value: 'new_secret', + environmentId: newEnvironmentOriginal.id + } + ] }, project3.id ) @@ -1512,8 +1579,12 @@ describe('Project Controller Tests', () => { user1, { name: 'NEW_VARIABLE', - value: 'new_variable', - environmentId: newEnvironmentOriginal.id + entries: [ + { + value: 'new_variable', + environmentId: newEnvironmentOriginal.id + } + ] }, project3.id ) @@ -1532,8 +1603,12 @@ describe('Project Controller Tests', () => { user2, { name: 'NEW_SECRET', - value: 'new_secret', - environmentId: newEnvironmentForked.id + entries: [ + { + value: 'new_secret', + environmentId: newEnvironmentForked.id + } + ] }, forkedProject.id ) @@ -1543,8 +1618,12 @@ describe('Project Controller Tests', () => { user2, { name: 'NEW_VARIABLE', - value: 'new_variable', - environmentId: newEnvironmentForked.id + entries: [ + { + value: 'new_variable', + environmentId: newEnvironmentForked.id + } + ] }, forkedProject.id ) @@ -1584,6 +1663,23 @@ describe('Project Controller Tests', () => { expect(forkedVariables).toHaveLength(3) }) + it('should not be able to sync a project that is not forked', async () => { + const response = await app.inject({ + method: 'PUT', + url: `/project/${project3.id}/sync-fork`, + headers: { + 'x-e2e-user-email': user1.email + } + }) + + expect(response.statusCode).toBe(400) + expect(response.json()).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: `Project with id ${project3.id} is not a forked project` + }) + }) + it('should be able to unlink a forked project', async () => { const forkedProject = await projectService.forkProject( user2, @@ -1630,5 +1726,35 @@ describe('Project Controller Tests', () => { expect(response.statusCode).toBe(200) expect(response.json()).toHaveLength(1) }) + + it('should not contain a forked project that has access level other than GLOBAL', async () => { + // Make a hidden fork + const hiddenProject = await projectService.forkProject( + user2, + project3.id, + { + name: 'Hidden Forked Project' + } + ) + await projectService.updateProject(user2, hiddenProject.id, { + accessLevel: ProjectAccessLevel.INTERNAL + }) + + // Make a public fork + await projectService.forkProject(user2, project3.id, { + name: 'Forked Project' + }) + + const response = await app.inject({ + method: 'GET', + url: `/project/${project3.id}/forks`, + headers: { + 'x-e2e-user-email': user1.email + } + }) + + expect(response.statusCode).toBe(200) + expect(response.json()).toHaveLength(1) + }) }) }) diff --git a/apps/api/src/project/service/project.service.ts b/apps/api/src/project/service/project.service.ts index 0cb88471..18253372 100644 --- a/apps/api/src/project/service/project.service.ts +++ b/apps/api/src/project/service/project.service.ts @@ -5,9 +5,6 @@ import { Logger } from '@nestjs/common' import { - ApprovalAction, - ApprovalItemType, - ApprovalStatus, Authority, Environment, EventSource, @@ -29,14 +26,9 @@ import { decrypt } from '../../common/decrypt' import { encrypt } from '../../common/encrypt' import { v4 } from 'uuid' import createEvent from '../../common/create-event' -import workspaceApprovalEnabled from '../../common/workspace-approval-enabled' -import createApproval from '../../common/create-approval' -import { UpdateProjectMetadata } from '../../approval/approval.types' import { ProjectWithSecrets } from '../project.types' import { AuthorityCheckerService } from '../../common/authority-checker.service' import { ForkProject } from '../dto/fork.project/fork.project' -import { SecretWithEnvironment } from 'src/secret/secret.types' -import { VariableWithEnvironment } from 'src/variable/variable.types' @Injectable() export class ProjectService { @@ -50,8 +42,7 @@ export class ProjectService { async createProject( user: User, workspaceId: Workspace['id'], - dto: CreateProject, - reason?: string + dto: CreateProject ) { // Check if the workspace exists or not const workspace = @@ -71,11 +62,6 @@ export class ProjectService { // Create the public and private key pair const { publicKey, privateKey } = createKeyPair() - const approvalEnabled = await workspaceApprovalEnabled( - workspaceId, - this.prisma - ) - const data: any = { name: dto.name, description: dto.description, @@ -84,8 +70,7 @@ export class ProjectService { ? true : dto.storePrivateKey, // If the project is global, the private key must be stored publicKey, - accessLevel: dto.accessLevel, - pendingCreation: approvalEnabled + accessLevel: dto.accessLevel } // Check if the private key should be stored @@ -146,25 +131,17 @@ export class ProjectService { // Create and assign the environments provided in the request, if any // or create a default environment if (dto.environments && dto.environments.length > 0) { - let defaultEnvironmentExists = false for (const environment of dto.environments) { createEnvironmentOps.push( this.prisma.environment.create({ data: { name: environment.name, description: environment.description, - isDefault: - defaultEnvironmentExists === false - ? environment.isDefault - : false, projectId: newProjectId, lastUpdatedById: user.id } }) ) - - defaultEnvironmentExists = - defaultEnvironmentExists || environment.isDefault } } else { createEnvironmentOps.push( @@ -172,7 +149,6 @@ export class ProjectService { data: { name: 'Default', description: 'Default environment for the project', - isDefault: true, projectId: newProjectId, lastUpdatedById: user.id } @@ -210,32 +186,13 @@ export class ProjectService { // in order to not log the private key newProject.privateKey = privateKey - if (approvalEnabled) { - const approval = await createApproval( - { - action: ApprovalAction.CREATE, - itemType: ApprovalItemType.PROJECT, - itemId: newProjectId, - reason, - user, - workspaceId - }, - this.prisma - ) - return { - project: newProject, - approval - } - } else { - return newProject - } + return newProject } async updateProject( user: User, projectId: Project['id'], - dto: UpdateProject, - reason?: string + dto: UpdateProject ) { // Check if the user has the authority to update the project let authority: Authority = Authority.UPDATE_PROJECT @@ -291,24 +248,85 @@ export class ProjectService { } } + const data: Partial = { + name: dto.name, + description: dto.description, + storePrivateKey: dto.storePrivateKey, + privateKey: dto.storePrivateKey ? dto.privateKey : null, + accessLevel: dto.accessLevel + } + + // If the access level is changed to PRIVATE or internal, we would + // also need to unlink all the forks if ( - !project.pendingCreation && - (await workspaceApprovalEnabled(project.workspaceId, this.prisma)) + dto.accessLevel !== ProjectAccessLevel.GLOBAL && + project.accessLevel === ProjectAccessLevel.GLOBAL ) { - return await createApproval( - { - action: ApprovalAction.UPDATE, - itemType: ApprovalItemType.PROJECT, - itemId: projectId, - reason, - user, - workspaceId: project.workspaceId, - metadata: dto + data.isForked = false + data.forkedFromId = null + } + + const versionUpdateOps = [] + let privateKey = dto.privateKey + let publicKey = project.publicKey + + if (dto.regenerateKeyPair) { + if (dto.privateKey || project.privateKey) { + const { txs, newPrivateKey, newPublicKey } = + await this.updateProjectKeyPair( + project, + dto.privateKey || project.privateKey, + dto.storePrivateKey + ) + + privateKey = newPrivateKey + publicKey = newPublicKey + + versionUpdateOps.push(...txs) + } else { + throw new BadRequestException( + 'Private key is required to regenerate the key pair' + ) + } + } + + // Update and return the project + const updateProjectOp = this.prisma.project.update({ + where: { + id: project.id + }, + data: { + ...data, + lastUpdatedById: user.id + } + }) + + const [updatedProject] = await this.prisma.$transaction([ + updateProjectOp, + ...versionUpdateOps + ]) + + await createEvent( + { + triggeredBy: user, + entity: updatedProject, + type: EventType.PROJECT_UPDATED, + source: EventSource.PROJECT, + title: `Project updated`, + metadata: { + projectId: updatedProject.id, + name: updatedProject.name }, - this.prisma - ) - } else { - return this.update(dto, user, project) + workspaceId: updatedProject.workspaceId + }, + this.prisma + ) + + this.log.debug(`Updated project ${updatedProject.id}`) + return { + ...updatedProject, + privateKey, + publicKey } } @@ -378,7 +396,6 @@ export class ProjectService { ? privateKey : null, accessLevel: project.accessLevel, - pendingCreation: false, isForked: true, forkedFromId: project.id, workspaceId: workspaceId, @@ -503,7 +520,7 @@ export class ProjectService { await this.prisma.$transaction(copyProjectOp) } - async deleteProject(user: User, projectId: Project['id'], reason?: string) { + async deleteProject(user: User, projectId: Project['id']) { const project = await this.authorityCheckerService.checkAuthorityOverProject({ userId: user.id, @@ -512,21 +529,49 @@ export class ProjectService { prisma: this.prisma }) - if (await workspaceApprovalEnabled(project.workspaceId, this.prisma)) { - return await createApproval( - { - action: ApprovalAction.DELETE, - itemType: ApprovalItemType.PROJECT, - itemId: projectId, - reason, - user, - workspaceId: project.workspaceId + const op = [] + + // Remove the fork relationships + op.push( + this.prisma.project.updateMany({ + where: { + forkedFromId: project.id }, - this.prisma - ) - } else { - return this.delete(user, project) - } + data: { + isForked: false, + forkedFromId: null + } + }) + ) + + // Delete the project + op.push( + this.prisma.project.delete({ + where: { + id: project.id + } + }) + ) + + await this.prisma.$transaction(op) + + await createEvent( + { + triggeredBy: user, + type: EventType.PROJECT_DELETED, + source: EventSource.PROJECT, + entity: project, + title: `Project deleted`, + metadata: { + projectId: project.id, + name: project.name + }, + workspaceId: project.workspaceId + }, + this.prisma + ) + + this.log.debug(`Deleted project ${project}`) } async getAllProjectForks( @@ -601,7 +646,6 @@ export class ProjectService { [sort]: order }, where: { - pendingCreation: false, workspaceId, OR: [ { @@ -638,8 +682,7 @@ export class ProjectService { workspace: { projects: { some: { - name: projectName, - pendingCreation: false + name: projectName } } } @@ -648,78 +691,18 @@ export class ProjectService { ) } - async makeProjectApproved(projectId: Project['id']) { - const project = await this.prisma.project.findUnique({ - where: { - id: projectId - } - }) - - // Check if a project with this name already exists - const projectExists = await this.prisma.project.count({ - where: { - name: project.name, - pendingCreation: false, - workspaceId: project.workspaceId - } - }) - - if (projectExists > 0) { - throw new ConflictException( - `Project with this name ${project.name} already exists` - ) - } - - return this.prisma.project.update({ - where: { - id: projectId - }, - data: { - pendingCreation: false, - environments: { - updateMany: { - where: { - projectId - }, - data: { - pendingCreation: false - } - } - }, - secrets: { - updateMany: { - where: { - projectId - }, - data: { - pendingCreation: false - } - } - }, - variables: { - updateMany: { - where: { - projectId - }, - data: { - pendingCreation: false - } - } - } - } - }) - } - private async copyProjectData( user: User, fromProject: { id: Project['id'] - privateKey: string + privateKey: string // Need the private key to decrypt the secrets }, toProject: { id: Project['id'] - publicKey: string + publicKey: string // Need the public key to encrypt the secrets }, + // hardCopy = true: Replace everything in the toProject with the fromProject + // hardCopy = false: Only add those items in the toProject that are not already present in it hardCopy: boolean = false ) { // Get all the environments that belongs to the parent project @@ -732,64 +715,50 @@ export class ProjectService { // items in the toProject that are not already present in it with // comparison to the fromProject const toProjectEnvironments: Set = new Set() - const toProjectSecrets: Set<{ - secret: Secret['name'] - environment: Environment['name'] - }> = new Set() - const toProjectVariables: Set<{ - variable: Variable['name'] - environment: Environment['name'] - }> = new Set() + const toProjectSecrets: Set = new Set() + const toProjectVariables: Set = new Set() if (!hardCopy) { - const environments: Environment[] = - await this.prisma.environment.findMany({ - where: { - projectId: toProject.id - } - }) + const [environments, secrets, variables] = await this.prisma.$transaction( + [ + this.prisma.environment.findMany({ + where: { + projectId: toProject.id + } + }), + this.prisma.secret.findMany({ + where: { + projectId: toProject.id + } + }), + this.prisma.variable.findMany({ + where: { + projectId: toProject.id + } + }) + ] + ) environments.forEach((env) => { envNameToIdMap[env.name] = env.id toProjectEnvironments.add(env.name) }) - const secrets: SecretWithEnvironment[] = - await this.prisma.secret.findMany({ - where: { - projectId: toProject.id - }, - include: { - environment: true - } - }) - secrets.forEach((secret) => { - toProjectSecrets.add({ - secret: secret.name, - environment: secret.environment.name - }) + toProjectSecrets.add(secret.name) }) - const variables: VariableWithEnvironment[] = - await this.prisma.variable.findMany({ - where: { - projectId: toProject.id - }, - include: { - environment: true - } - }) - variables.forEach((variable) => { - toProjectVariables.add({ - variable: variable.name, - environment: variable.environment.name - }) + toProjectVariables.add(variable.name) }) } - const environments = await this.prisma.environment.findMany({ + // We want to find all such environments in the fromProject that + // is not present in the toProject. You can think of this as a set + // difference operation. + // In case of a hard copy, we would just copy all the environments + // since toProjectEnvironments will be empty. + const missingEnvironments = await this.prisma.environment.findMany({ where: { projectId: fromProject.id, name: { @@ -798,7 +767,9 @@ export class ProjectService { } }) - for (const environment of environments) { + // For all the new environments that we are creating, we want to map + // the name of the environment to the id of the newly created environment + for (const environment of missingEnvironments) { const newEnvironmentId = v4() envNameToIdMap[environment.name] = newEnvironmentId @@ -808,7 +779,6 @@ export class ProjectService { id: newEnvironmentId, name: environment.name, description: environment.description, - isDefault: environment.isDefault, projectId: toProject.id, lastUpdatedById: user.id } @@ -816,25 +786,28 @@ export class ProjectService { ) } - // Get all the secrets that belongs to the parent project and - // replicate them for the new project const createSecretOps = [] + // Get all the secrets that belongs to the parent project and + // replicate them for the new project. This too is a set difference + // operation. const secrets = await this.prisma.secret.findMany({ where: { projectId: fromProject.id, name: { - notIn: Array.from(toProjectSecrets).map((s) => s.secret) - }, - environment: { - name: { - notIn: Array.from(toProjectSecrets).map((s) => s.environment) - } + notIn: Array.from(toProjectSecrets) } }, include: { - environment: true, - versions: true + versions: { + include: { + environment: { + select: { + name: true + } + } + } + } } }) @@ -844,25 +817,30 @@ export class ProjectService { toProject.publicKey, await decrypt(fromProject.privateKey, version.value) ), - version: version.version + version: version.version, + environmentName: version.environment.name })) createSecretOps.push( this.prisma.secret.create({ data: { name: secret.name, - environmentId: envNameToIdMap[secret.environment.name], projectId: toProject.id, lastUpdatedById: user.id, note: secret.note, rotateAt: secret.rotateAt, versions: { create: await Promise.all( - secretVersions.map(async (secretVersion) => ({ - value: (await secretVersion).value, - version: (await secretVersion).version, - createdById: user.id - })) + secretVersions.map(async (secretVersion) => { + const awaitedSecretVersion = await secretVersion + return { + value: awaitedSecretVersion.value, + version: awaitedSecretVersion.version, + environmentId: + envNameToIdMap[awaitedSecretVersion.environmentName], + createdById: user.id + } + }) ) } } @@ -878,17 +856,19 @@ export class ProjectService { where: { projectId: fromProject.id, name: { - notIn: Array.from(toProjectVariables).map((v) => v.variable) - }, - environment: { - name: { - notIn: Array.from(toProjectVariables).map((v) => v.environment) - } + notIn: Array.from(toProjectVariables) } }, include: { - environment: true, - versions: true + versions: { + include: { + environment: { + select: { + name: true + } + } + } + } } }) @@ -897,7 +877,6 @@ export class ProjectService { this.prisma.variable.create({ data: { name: variable.name, - environmentId: envNameToIdMap[variable.environment.name], projectId: toProject.id, lastUpdatedById: user.id, note: variable.note, @@ -905,7 +884,8 @@ export class ProjectService { create: variable.versions.map((version) => ({ value: version.value, version: version.version, - createdById: user.id + createdById: user.id, + environmentId: envNameToIdMap[version.environment.name] })) } } @@ -939,6 +919,8 @@ export class ProjectService { const updatedVersions: Partial[] = [] + // First, encrypt the values with the new key and store + // them in a temporary array for (const version of versions) { updatedVersions.push({ id: version.id, @@ -949,6 +931,7 @@ export class ProjectService { }) } + // Apply the changes to the database for (const version of updatedVersions) { txs.push( this.prisma.secretVersion.update({ @@ -963,6 +946,7 @@ export class ProjectService { } } + // Update the project with the new key pair txs.push( this.prisma.project.update({ where: { @@ -977,155 +961,4 @@ export class ProjectService { return { txs, newPrivateKey, newPublicKey } } - - async update( - dto: UpdateProject | UpdateProjectMetadata, - user: User, - project: ProjectWithSecrets - ) { - const data: Partial = { - name: dto.name, - description: dto.description, - storePrivateKey: dto.storePrivateKey, - privateKey: dto.storePrivateKey ? dto.privateKey : null, - accessLevel: dto.accessLevel - } - - // If the access level is changed to PRIVATE or internal, we would - // also need to unlink all the forks - if ( - dto.accessLevel !== ProjectAccessLevel.GLOBAL && - project.accessLevel === ProjectAccessLevel.GLOBAL - ) { - data.isForked = false - data.forkedFromId = null - } - - const versionUpdateOps = [] - let privateKey = dto.privateKey - let publicKey = project.publicKey - - if (dto.regenerateKeyPair) { - if (dto.privateKey || project.privateKey) { - const { txs, newPrivateKey, newPublicKey } = - await this.updateProjectKeyPair( - project, - dto.privateKey || project.privateKey, - dto.storePrivateKey - ) - - privateKey = newPrivateKey - publicKey = newPublicKey - - versionUpdateOps.push(...txs) - } else { - throw new BadRequestException( - 'Private key is required to regenerate the key pair' - ) - } - } - - // Update and return the project - const updateProjectOp = this.prisma.project.update({ - where: { - id: project.id - }, - data: { - ...data, - lastUpdatedById: user.id - } - }) - - const [updatedProject] = await this.prisma.$transaction([ - updateProjectOp, - ...versionUpdateOps - ]) - - await createEvent( - { - triggeredBy: user, - entity: updatedProject, - type: EventType.PROJECT_UPDATED, - source: EventSource.PROJECT, - title: `Project updated`, - metadata: { - projectId: updatedProject.id, - name: updatedProject.name - }, - workspaceId: updatedProject.workspaceId - }, - this.prisma - ) - - this.log.debug(`Updated project ${updatedProject.id}`) - return { - ...updatedProject, - privateKey, - publicKey - } - } - - async delete(user: User, project: Project) { - const op = [] - - // Remove the fork relationships - op.push( - this.prisma.project.updateMany({ - where: { - forkedFromId: project.id - }, - data: { - isForked: false, - forkedFromId: null - } - }) - ) - - // Delete the project - op.push( - this.prisma.project.delete({ - where: { - id: project.id - } - }) - ) - - // If the project is in pending creation and the workspace approval is enabled, we need to - // delete the approval as well - if ( - project.pendingCreation && - (await workspaceApprovalEnabled(project.workspaceId, this.prisma)) - ) { - op.push( - this.prisma.approval.deleteMany({ - where: { - itemId: project.id, - itemType: ApprovalItemType.PROJECT, - action: ApprovalAction.DELETE, - status: ApprovalStatus.PENDING - } - }) - ) - } - - await this.prisma.$transaction(op) - - await createEvent( - { - triggeredBy: user, - type: EventType.PROJECT_DELETED, - source: EventSource.PROJECT, - entity: project, - title: `Project deleted`, - metadata: { - projectId: project.id, - name: project.name - }, - workspaceId: project.workspaceId - }, - this.prisma - ) - - this.log.debug(`Deleted project ${project}`) - } } diff --git a/apps/api/src/secret/controller/secret.controller.ts b/apps/api/src/secret/controller/secret.controller.ts index 3fa82476..7f7d305b 100644 --- a/apps/api/src/secret/controller/secret.controller.ts +++ b/apps/api/src/secret/controller/secret.controller.ts @@ -14,7 +14,6 @@ import { Authority, User } from '@prisma/client' import { CreateSecret } from '../dto/create.secret/create.secret' import { UpdateSecret } from '../dto/update.secret/update.secret' import { RequiredApiKeyAuthorities } from '../../decorators/required-api-key-authorities.decorator' -import { AlphanumericReasonValidationPipe } from '../../common/alphanumeric-reason-pipe' @Controller('secret') export class SecretController { @@ -25,10 +24,9 @@ export class SecretController { async createSecret( @CurrentUser() user: User, @Param('projectId') projectId: string, - @Body() dto: CreateSecret, - @Query('reason', AlphanumericReasonValidationPipe) reason: string + @Body() dto: CreateSecret ) { - return await this.secretService.createSecret(user, dto, projectId, reason) + return await this.secretService.createSecret(user, dto, projectId) } @Put(':secretId') @@ -36,29 +34,9 @@ export class SecretController { async updateSecret( @CurrentUser() user: User, @Param('secretId') secretId: string, - @Body() dto: UpdateSecret, - @Query('reason', AlphanumericReasonValidationPipe) reason: string + @Body() dto: UpdateSecret ) { - return await this.secretService.updateSecret(user, secretId, dto, reason) - } - - @Put(':secretId/environment/:environmentId') - @RequiredApiKeyAuthorities( - Authority.UPDATE_SECRET, - Authority.READ_ENVIRONMENT - ) - async updateSecretEnvironment( - @CurrentUser() user: User, - @Param('secretId') secretId: string, - @Param('environmentId') environmentId: string, - @Query('reason', AlphanumericReasonValidationPipe) reason: string - ) { - return await this.secretService.updateSecretEnvironment( - user, - secretId, - environmentId, - reason - ) + return await this.secretService.updateSecret(user, secretId, dto) } @Put(':secretId/rollback/:rollbackVersion') @@ -66,14 +44,14 @@ export class SecretController { async rollbackSecret( @CurrentUser() user: User, @Param('secretId') secretId: string, - @Param('rollbackVersion') rollbackVersion: number, - @Query('reason', AlphanumericReasonValidationPipe) reason: string + @Query('environmentId') environmentId: string, + @Param('rollbackVersion') rollbackVersion: number ) { return await this.secretService.rollbackSecret( user, secretId, - rollbackVersion, - reason + environmentId, + rollbackVersion ) } @@ -81,20 +59,9 @@ export class SecretController { @RequiredApiKeyAuthorities(Authority.DELETE_SECRET) async deleteSecret( @CurrentUser() user: User, - @Param('secretId') secretId: string, - @Query('reason', AlphanumericReasonValidationPipe) reason: string - ) { - return await this.secretService.deleteSecret(user, secretId, reason) - } - - @Get(':secretId') - @RequiredApiKeyAuthorities(Authority.READ_SECRET) - async getSecret( - @CurrentUser() user: User, - @Param('secretId') secretId: string, - @Query('decryptValue') decryptValue: boolean = false + @Param('secretId') secretId: string ) { - return await this.secretService.getSecretById(user, secretId, decryptValue) + return await this.secretService.deleteSecret(user, secretId) } @Get('/all/:projectId') diff --git a/apps/api/src/secret/dto/create.secret/create.secret.ts b/apps/api/src/secret/dto/create.secret/create.secret.ts index be971e2a..ba4c1548 100644 --- a/apps/api/src/secret/dto/create.secret/create.secret.ts +++ b/apps/api/src/secret/dto/create.secret/create.secret.ts @@ -1,22 +1,39 @@ -import { IsOptional, IsString, Length } from 'class-validator' +import 'reflect-metadata' +import { Transform, Type } from 'class-transformer' +import { + IsArray, + IsOptional, + IsString, + Length, + ValidateNested +} from 'class-validator' export class CreateSecret { @IsString() name: string - @IsString() - value: string - @IsString() @IsOptional() @Length(0, 100) note?: string + @IsString() + @IsOptional() + rotateAfter?: '24' | '168' | '720' | '8760' | 'never' = 'never' + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => Entry) + entries?: Entry[] +} + +class Entry { @IsString() - environmentId?: string + @Transform(({ value }) => value.trim()) + environmentId: string @IsString() - @IsOptional() - rotateAfter?: '24' | '168' | '720' | '8760' | 'never' = 'never' + @Transform(({ value }) => value.trim()) + value: string } diff --git a/apps/api/src/secret/secret.e2e.spec.ts b/apps/api/src/secret/secret.e2e.spec.ts index efa15bb6..f722c542 100644 --- a/apps/api/src/secret/secret.e2e.spec.ts +++ b/apps/api/src/secret/secret.e2e.spec.ts @@ -46,13 +46,10 @@ describe('Secret Controller Tests', () => { let secretService: SecretService let eventService: EventService let userService: UserService - let user1: User, user2: User - let workspace1: Workspace, workspace2: Workspace - let project1: Project, project2: Project, workspace2Project: Project + let workspace1: Workspace + let project1: Project, project2: Project let environment1: Environment - let environment2: Environment - let workspace2Environment: Environment let secret1: Secret beforeAll(async () => { @@ -102,7 +99,6 @@ describe('Secret Controller Tests', () => { }) workspace1 = createUser1.defaultWorkspace - workspace2 = createUser2.defaultWorkspace delete createUser1.defaultWorkspace delete createUser2.defaultWorkspace @@ -118,13 +114,11 @@ describe('Secret Controller Tests', () => { environments: [ { name: 'Environment 1', - description: 'Environment 1 description', - isDefault: true + description: 'Environment 1 description' }, { name: 'Environment 2', - description: 'Environment 2 description', - isDefault: false + description: 'Environment 2 description' } ] })) as Project @@ -137,37 +131,11 @@ describe('Secret Controller Tests', () => { environments: [ { name: 'Environment 1', - description: 'Environment 1 description', - isDefault: true + description: 'Environment 1 description' } ] })) as Project - workspace2Project = (await projectService.createProject( - user2, - workspace2.id, - { - name: 'Workspace 2 Project', - description: 'Workspace 2 Project description', - storePrivateKey: true, - accessLevel: ProjectAccessLevel.PRIVATE, - environments: [ - { - name: 'Environment 1', - description: 'Environment 1 description', - isDefault: true - } - ] - } - )) as Project - - workspace2Environment = await prisma.environment.findFirst({ - where: { - projectId: workspace2Project.id, - name: 'Environment 1' - } - }) - environment1 = await prisma.environment.findFirst({ where: { projectId: project1.id, @@ -175,21 +143,18 @@ describe('Secret Controller Tests', () => { } }) - environment2 = await prisma.environment.findFirst({ - where: { - projectId: project1.id, - name: 'Environment 2' - } - }) - secret1 = (await secretService.createSecret( user1, { - environmentId: environment2.id, name: 'Secret 1', - value: 'Secret 1 value', rotateAfter: '24', - note: 'Secret 1 note' + note: 'Secret 1 note', + entries: [ + { + environmentId: environment1.id, + value: 'Secret 1 value' + } + ] }, project1.id )) as Secret @@ -215,10 +180,14 @@ describe('Secret Controller Tests', () => { method: 'POST', url: `/secret/${project1.id}`, payload: { - environmentId: environment2.id, name: 'Secret 2', note: 'Secret 2 note', - value: 'Secret 2 value', + entries: [ + { + value: 'Secret 2 value', + environmentId: environment1.id + } + ], rotateAfter: '24' }, headers: { @@ -233,8 +202,9 @@ describe('Secret Controller Tests', () => { expect(body).toBeDefined() expect(body.name).toBe('Secret 2') expect(body.note).toBe('Secret 2 note') - expect(body.environmentId).toBe(environment2.id) expect(body.projectId).toBe(project1.id) + expect(body.versions.length).toBe(1) + expect(body.versions[0].value).not.toBe('Secret 2 value') }) it('should have created a secret version', async () => { @@ -247,30 +217,7 @@ describe('Secret Controller Tests', () => { expect(secretVersion).toBeDefined() expect(secretVersion.value).not.toBe('Secret 1 value') expect(secretVersion.version).toBe(1) - }) - - it('should create secret in default environment if environmentId is not provided', async () => { - const response = await app.inject({ - method: 'POST', - url: `/secret/${project1.id}`, - payload: { - name: 'Secret 2', - value: 'Secret 2 value', - rotateAfter: '24' - }, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(201) - - const body = response.json() - - expect(body).toBeDefined() - expect(body.name).toBe('Secret 2') - expect(body.environmentId).toBe(environment1.id) - expect(body.projectId).toBe(project1.id) + expect(secretVersion.environmentId).toBe(environment1.id) }) it('should not be able to create a secret with a non-existing environment', async () => { @@ -278,10 +225,14 @@ describe('Secret Controller Tests', () => { method: 'POST', url: `/secret/${project1.id}`, payload: { - environmentId: 'non-existing-environment-id', name: 'Secret 3', - value: 'Secret 3 value', - rotateAfter: '24' + rotateAfter: '24', + entries: [ + { + value: 'Secret 3 value', + environmentId: 'non-existing-environment-id' + } + ] }, headers: { 'x-e2e-user-email': user1.email @@ -297,7 +248,6 @@ describe('Secret Controller Tests', () => { url: `/secret/${project1.id}`, payload: { name: 'Secret 3', - value: 'Secret 3 value', rotateAfter: '24' }, headers: { @@ -311,54 +261,12 @@ describe('Secret Controller Tests', () => { ) }) - it('should fail if project has no default environment(hypothetical case)', async () => { - await prisma.environment.updateMany({ - where: { - projectId: project1.id, - name: 'Environment 1' - }, - data: { - isDefault: false - } - }) - + it('should not be able to create a duplicate secret in the same project', async () => { const response = await app.inject({ method: 'POST', url: `/secret/${project1.id}`, payload: { - name: 'Secret 4', - value: 'Secret 4 value', - rotateAfter: '24' - }, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(404) - expect(response.json().message).toEqual( - `No default environment found for project: ${project1.id}` - ) - - await prisma.environment.updateMany({ - where: { - projectId: project1.id, - name: 'Environment 1' - }, - data: { - isDefault: true - } - }) - }) - - it('should not be able to create a duplicate secret in the same environment', async () => { - const response = await app.inject({ - method: 'POST', - url: `/secret/${project1.id}`, - payload: { - environmentId: environment2.id, name: 'Secret 1', - value: 'Secret 1 value', rotateAfter: '24' }, headers: { @@ -368,7 +276,7 @@ describe('Secret Controller Tests', () => { expect(response.statusCode).toBe(409) expect(response.json().message).toEqual( - `Secret already exists: Secret 1 in environment ${environment2.name} in project ${project1.id}` + `Secret already exists: Secret 1 in project ${project1.id}` ) }) @@ -396,7 +304,6 @@ describe('Secret Controller Tests', () => { url: `/secret/non-existing-secret-id`, payload: { name: 'Updated Secret 1', - value: 'Updated Secret 1 value', rotateAfter: '24' }, headers: { @@ -410,26 +317,6 @@ describe('Secret Controller Tests', () => { ) }) - it('should not be able to update a secret with same name in the same environment', async () => { - const response = await app.inject({ - method: 'PUT', - url: `/secret/${secret1.id}`, - payload: { - name: 'Secret 1', - value: 'Updated Secret 1 value', - rotateAfter: '24' - }, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(409) - expect(response.json().message).toEqual( - `Secret already exists: Secret 1 in environment ${environment2.id}` - ) - }) - it('should be able to update the secret name and note without creating a new version', async () => { const response = await app.inject({ method: 'PUT', @@ -461,7 +348,12 @@ describe('Secret Controller Tests', () => { method: 'PUT', url: `/secret/${secret1.id}`, payload: { - value: 'Updated Secret 1 value' + entries: [ + { + value: 'Updated Secret 1 value', + environmentId: environment1.id + } + ] }, headers: { 'x-e2e-user-email': user1.email @@ -472,7 +364,8 @@ describe('Secret Controller Tests', () => { const secretVersion = await prisma.secretVersion.findMany({ where: { - secretId: secret1.id + secretId: secret1.id, + environmentId: environment1.id } }) @@ -502,112 +395,61 @@ describe('Secret Controller Tests', () => { expect(event.itemId).toBe(secret1.id) }) - it('should be able to update the environment of a secret', async () => { - const response = await app.inject({ - method: 'PUT', - url: `/secret/${secret1.id}/environment/${environment1.id}`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - expect(response.json().environmentId).toBe(environment1.id) - }) - - it('should not be able to move to a non-existing environment', async () => { + it('should not be able to roll back a non-existing secret', async () => { const response = await app.inject({ method: 'PUT', - url: `/secret/${secret1.id}/environment/non-existing-environment-id`, + url: `/secret/non-existing-secret-id/rollback/1?environmentId=${environment1.id}`, headers: { 'x-e2e-user-email': user1.email } }) expect(response.statusCode).toBe(404) - }) - - it('should not be able to move to an environment in another project', async () => { - const otherEnvironment = await prisma.environment.findFirst({ - where: { - projectId: project2.id, - name: 'Environment 1' - } - }) - - const response = await app.inject({ - method: 'PUT', - url: `/secret/${secret1.id}/environment/${otherEnvironment.id}`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(400) expect(response.json().message).toEqual( - `Environment ${otherEnvironment.id} does not belong to project ${project1.id}` + 'Secret with id non-existing-secret-id not found' ) }) - it('should not be able to move the secret to the same environment', async () => { + it('should not be able to roll back a secret it does not have access to', async () => { const response = await app.inject({ method: 'PUT', - url: `/secret/${secret1.id}/environment/${environment2.id}`, + url: `/secret/${secret1.id}/rollback/1?environmentId=${environment1.id}`, headers: { - 'x-e2e-user-email': user1.email + 'x-e2e-user-email': user2.email } }) - expect(response.statusCode).toBe(400) + expect(response.statusCode).toBe(401) expect(response.json().message).toEqual( - `Can not update the environment of the secret to the same environment: ${environment2.id} in project ${project1.id}` + `User ${user2.id} does not have the required authorities` ) }) - it('should not be able to move the secret if the user has no access to the project', async () => { + it('should not be able to roll back to a non-existing version', async () => { const response = await app.inject({ method: 'PUT', - url: `/secret/${secret1.id}/environment/${workspace2Environment.id}`, + url: `/secret/${secret1.id}/rollback/2?environmentId=${environment1.id}`, headers: { 'x-e2e-user-email': user1.email } }) - expect(response.statusCode).toBe(401) + expect(response.statusCode).toBe(404) expect(response.json().message).toEqual( - `User ${user1.id} does not have the required authorities` + `Invalid rollback version: 2 for secret: ${secret1.id}` ) }) - it('should not be able to move a secret of the same name to an environment', async () => { - const newSecret = (await secretService.createSecret( - user1, - { - environmentId: environment1.id, - name: 'Secret 1', - value: 'Some value' - }, - project1.id - )) as Secret - - const response = await app.inject({ - method: 'PUT', - url: `/secret/${newSecret.id}/environment/${environment2.id}`, - headers: { - 'x-e2e-user-email': user1.email + it('should not be able to roll back if the secret has no versions', async () => { + await prisma.secretVersion.deleteMany({ + where: { + secretId: secret1.id } }) - expect(response.statusCode).toBe(409) - expect(response.json().message).toEqual( - `Secret already exists: Secret 1 in environment ${environment2.id} in project ${project1.id}` - ) - }) - - it('should not be able to roll back a non-existing secret', async () => { const response = await app.inject({ method: 'PUT', - url: `/secret/non-existing-secret-id/rollback/1`, + url: `/secret/${secret1.id}/rollback/1?environmentId=${environment1.id}`, headers: { 'x-e2e-user-email': user1.email } @@ -615,48 +457,48 @@ describe('Secret Controller Tests', () => { expect(response.statusCode).toBe(404) expect(response.json().message).toEqual( - 'Secret with id non-existing-secret-id not found' + `No versions found for environment: ${environment1.id} for secret: ${secret1.id}` ) }) - it('should not be able to roll back a secret it does not have access to', async () => { - const response = await app.inject({ - method: 'PUT', - url: `/secret/${secret1.id}/rollback/1`, - headers: { - 'x-e2e-user-email': user2.email - } - }) - - expect(response.statusCode).toBe(401) - expect(response.json().message).toEqual( - `User ${user2.id} does not have the required authorities` + it('should not create a secret version entity if value-environmentId is not provided during creation', async () => { + const secret = await secretService.createSecret( + user1, + { + name: 'Secret 4', + note: 'Secret 4 note', + rotateAfter: '24' + }, + project1.id ) - }) - it('should not be able to roll back to a non-existing version', async () => { - const response = await app.inject({ - method: 'PUT', - url: `/secret/${secret1.id}/rollback/2`, - headers: { - 'x-e2e-user-email': user1.email + const secretVersion = await prisma.secretVersion.findMany({ + where: { + secretId: secret.id } }) - expect(response.statusCode).toBe(404) - expect(response.json().message).toEqual( - `Invalid rollback version: 2 for secret: ${secret1.id}` - ) + expect(secretVersion.length).toBe(0) }) it('should be able to roll back a secret', async () => { // Creating a few versions first await secretService.updateSecret(user1, secret1.id, { - value: 'Updated Secret 1 value' + entries: [ + { + value: 'Updated Secret 1 value', + environmentId: environment1.id + } + ] }) await secretService.updateSecret(user1, secret1.id, { - value: 'Updated Secret 1 value 2' + entries: [ + { + value: 'Updated Secret 1 value 2', + environmentId: environment1.id + } + ] }) let versions: SecretVersion[] @@ -671,7 +513,7 @@ describe('Secret Controller Tests', () => { const response = await app.inject({ method: 'PUT', - url: `/secret/${secret1.id}/rollback/1`, + url: `/secret/${secret1.id}/rollback/1?environmentId=${environment1.id}`, headers: { 'x-e2e-user-email': user1.email } @@ -689,131 +531,42 @@ describe('Secret Controller Tests', () => { expect(versions.length).toBe(1) }) - it('should not be able to fetch a non existing secret', async () => { - const response = await app.inject({ - method: 'GET', - url: `/secret/non-existing-secret-id`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(404) - expect(response.json().message).toEqual( - 'Secret with id non-existing-secret-id not found' - ) - }) - - it('should not be able to fetch a secret it does not have access to', async () => { - const response = await app.inject({ - method: 'GET', - url: `/secret/${secret1.id}`, - headers: { - 'x-e2e-user-email': user2.email - } - }) - - expect(response.statusCode).toBe(401) - expect(response.json().message).toEqual( - `User ${user2.id} does not have the required authorities` - ) - }) - - it('should be able to fetch a secret', async () => { - const response = await app.inject({ - method: 'GET', - url: `/secret/${secret1.id}`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - expect(response.json().id).toEqual(secret1.id) - - const versions = await response.json().versions - - expect(versions.length).toBe(1) - expect(versions[0].value).not.toEqual('Secret 1 value') // Secret should be in encrypted form until specified otherwise - }) - - it('should be able to fetch a decrypted secret', async () => { - const response = await app.inject({ - method: 'GET', - url: `/secret/${secret1.id}?decryptValue=true`, - headers: { - 'x-e2e-user-email': user1.email + it('should not be able to fetch decrypted secrets if the project does not store the private key', async () => { + // Fetch the environment of the project + const environment = await prisma.environment.findFirst({ + where: { + projectId: project2.id } }) - expect(response.statusCode).toBe(200) - expect(response.json().id).toEqual(secret1.id) - - const versions = await response.json().versions - - expect(versions.length).toBe(1) - expect(versions[0].value).toEqual('Secret 1 value') - }) - - it('should not be able to fetch a decrypted secret if the project does not store the private key', async () => { - const secret = (await secretService.createSecret( + await secretService.createSecret( user1, { - environmentId: environment1.id, name: 'Secret 20', - value: 'Secret 20 value', + entries: [ + { + environmentId: environment.id, + value: 'Secret 20 value' + } + ], rotateAfter: '24', note: 'Secret 20 note' }, project2.id - )) as Secret - - const response = await app.inject({ - method: 'GET', - url: `/secret/${secret.id}?decryptValue=true`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(400) - expect(response.json().message).toEqual( - `Cannot decrypt secret value: ${secret.id} as the project does not store the private key` ) - }) - - it('should not be able to fetch a decrypted secret if somehow the project does not have a private key even though it stores it (hypothetical)', async () => { - await prisma.project.update({ - where: { - id: project1.id - }, - data: { - storePrivateKey: true, - privateKey: null - } - }) const response = await app.inject({ method: 'GET', - url: `/secret/${secret1.id}?decryptValue=true`, + url: `/secret/all/${project2.id}?decryptValue=true`, headers: { 'x-e2e-user-email': user1.email } }) - expect(response.statusCode).toBe(404) + expect(response.statusCode).toBe(400) expect(response.json().message).toEqual( - `Cannot decrypt secret value: ${secret1.id} as the project does not have a private key` + `Cannot decrypt secret values as the project does not store the private key` ) - - await prisma.project.update({ - where: { - id: project1.id - }, - data: { - privateKey: project1.privateKey - } - }) }) it('should be able to fetch all secrets', async () => { @@ -827,6 +580,17 @@ describe('Secret Controller Tests', () => { expect(response.statusCode).toBe(200) expect(response.json().length).toBe(1) + + const { secret, values } = response.json()[0] + expect(secret.id).toBeDefined() + expect(secret.name).toBeDefined() + expect(secret.note).toBeDefined() + expect(secret.projectId).toBeDefined() + expect(values.length).toBe(1) + + const value = values[0] + expect(value.environment).toBeDefined() + expect(value.value).not.toEqual('Secret 1 value') }) it('should be able to fetch all secrets decrypted', async () => { @@ -840,9 +604,17 @@ describe('Secret Controller Tests', () => { expect(response.statusCode).toBe(200) expect(response.json().length).toBe(1) - const envSecret = response.json()[0] - expect(envSecret.environment).toHaveProperty('id') - expect(envSecret.environment).toHaveProperty('name') + + const { secret, values } = response.json()[0] + expect(secret.id).toBeDefined() + expect(secret.name).toBeDefined() + expect(secret.note).toBeDefined() + expect(secret.projectId).toBeDefined() + expect(values.length).toBe(1) + + const value = values[0] + expect(value.environment).toBeDefined() + expect(value.value).toEqual('Secret 1 value') }) it('should not be able to fetch all secrets decrypted if the project does not store the private key', async () => { diff --git a/apps/api/src/secret/secret.types.ts b/apps/api/src/secret/secret.types.ts index 322aed65..9a070f2d 100644 --- a/apps/api/src/secret/secret.types.ts +++ b/apps/api/src/secret/secret.types.ts @@ -1,4 +1,4 @@ -import { Environment, Project, Secret, SecretVersion } from '@prisma/client' +import { Project, Secret, SecretVersion } from '@prisma/client' export interface SecretWithValue extends Secret { value: string @@ -12,10 +12,4 @@ export interface SecretWithProject extends Secret { project: Project } -export interface SecretWithEnvironment extends Secret { - environment: Environment -} - export type SecretWithProjectAndVersion = SecretWithProject & SecretWithVersion -export type SecretWithVersionAndEnvironment = SecretWithVersion & - SecretWithEnvironment diff --git a/apps/api/src/secret/service/secret.service.ts b/apps/api/src/secret/service/secret.service.ts index 8bd1ee6d..7a143872 100644 --- a/apps/api/src/secret/service/secret.service.ts +++ b/apps/api/src/secret/service/secret.service.ts @@ -7,9 +7,6 @@ import { NotFoundException } from '@nestjs/common' import { - ApprovalAction, - ApprovalItemType, - ApprovalStatus, Authority, Environment, EventSource, @@ -25,16 +22,7 @@ import { decrypt } from '../../common/decrypt' import { PrismaService } from '../../prisma/prisma.service' import { addHoursToDate } from '../../common/add-hours-to-date' import { encrypt } from '../../common/encrypt' -import { - SecretWithProject, - SecretWithProjectAndVersion, - SecretWithVersionAndEnvironment -} from '../secret.types' import createEvent from '../../common/create-event' -import getDefaultEnvironmentOfProject from '../../common/get-default-project-environment' -import workspaceApprovalEnabled from '../../common/workspace-approval-enabled' -import createApproval from '../../common/create-approval' -import { UpdateSecretMetadata } from '../../approval/approval.types' import { RedisClientType } from 'redis' import { REDIS_CLIENT } from '../../provider/redis.provider' import { CHANGE_NOTIFIER_RSC } from '../../socket/change-notifier.socket' @@ -56,13 +44,7 @@ export class SecretService { this.redis = redisClient.publisher } - async createSecret( - user: User, - dto: CreateSecret, - projectId: Project['id'], - reason?: string - ) { - const environmentId = dto.environmentId + async createSecret(user: User, dto: CreateSecret, projectId: Project['id']) { // Fetch the project const project = await this.authorityCheckerService.checkAuthorityOverProject({ @@ -72,60 +54,50 @@ export class SecretService { prisma: this.prisma }) - // Check if the environment exists - let environment: Environment | null = null - if (environmentId) { - environment = - await this.authorityCheckerService.checkAuthorityOverEnvironment({ - userId: user.id, - entity: { id: environmentId }, - authority: Authority.READ_ENVIRONMENT, - prisma: this.prisma + // Check if the secret with the same name already exists in the project + await this.secretExists(dto.name, projectId) + + const shouldCreateRevisions = dto.entries && dto.entries.length > 0 + + // Check if the user has access to the environments + if (shouldCreateRevisions) { + const environmentIds = dto.entries.map((entry) => entry.environmentId) + await Promise.all( + environmentIds.map(async (environmentId) => { + const environment = + await this.authorityCheckerService.checkAuthorityOverEnvironment({ + userId: user.id, + entity: { id: environmentId }, + authority: Authority.READ_ENVIRONMENT, + prisma: this.prisma + }) + + // Check if the environment belongs to the project + if (environment.projectId !== projectId) { + throw new BadRequestException( + `Environment: ${environmentId} does not belong to project: ${projectId}` + ) + } }) - } - if (!environment) { - environment = await getDefaultEnvironmentOfProject(projectId, this.prisma) - } - - // If any default environment was not found, throw an error - if (!environment) { - throw new NotFoundException( - `No default environment found for project: ${projectId}` - ) - } - - // Check if the secret already exists in the environment - if (await this.secretExists(dto.name, environment.id)) { - throw new ConflictException( - `Secret already exists: ${dto.name} in environment ${environment.name} in project ${projectId}` ) } - const approvalEnabled = await workspaceApprovalEnabled( - project.workspaceId, - this.prisma - ) - // Create the secret const secret = await this.prisma.secret.create({ data: { name: dto.name, note: dto.note, rotateAt: addHoursToDate(dto.rotateAfter), - pendingCreation: - project.pendingCreation || - environment.pendingCreation || - approvalEnabled, - versions: { - create: { - value: await encrypt(project.publicKey, dto.value), - version: 1, - createdById: user.id - } - }, - environment: { - connect: { - id: environment.id + versions: shouldCreateRevisions && { + createMany: { + data: await Promise.all( + dto.entries.map(async (entry) => ({ + value: await encrypt(project.publicKey, entry.value), + version: 1, + createdById: user.id, + environmentId: entry.environmentId + })) + ) } }, project: { @@ -138,6 +110,19 @@ export class SecretService { id: user.id } } + }, + include: { + project: { + select: { + workspaceId: true + } + }, + versions: { + select: { + environmentId: true, + value: true + } + } } }) @@ -152,9 +137,7 @@ export class SecretService { secretId: secret.id, name: secret.name, projectId, - projectName: project.name, - environmentId: environment.id, - environmentName: environment.name + projectName: project.name }, workspaceId: project.workspaceId }, @@ -163,37 +146,10 @@ export class SecretService { this.logger.log(`User ${user.id} created secret ${secret.id}`) - if ( - !project.pendingCreation && - !environment.pendingCreation && - approvalEnabled - ) { - const approval = await createApproval( - { - action: ApprovalAction.CREATE, - itemType: ApprovalItemType.SECRET, - itemId: secret.id, - reason, - user, - workspaceId: project.workspaceId - }, - this.prisma - ) - return { - secret, - approval - } - } else { - return secret - } + return secret } - async updateSecret( - user: User, - secretId: Secret['id'], - dto: UpdateSecret, - reason?: string - ) { + async updateSecret(user: User, secretId: Secret['id'], dto: UpdateSecret) { const secret = await this.authorityCheckerService.checkAuthorityOverSecret({ userId: user.id, entity: { id: secretId }, @@ -201,111 +157,154 @@ export class SecretService { prisma: this.prisma }) - // Check if the secret already exists in the environment - if ( - (dto.name && (await this.secretExists(dto.name, secret.environmentId))) || - secret.name === dto.name - ) { - throw new ConflictException( - `Secret already exists: ${dto.name} in environment ${secret.environmentId}` - ) - } - - // Encrypt the secret value before storing/processing it - if (dto.value) { - dto.value = await encrypt(secret.project.publicKey, dto.value) - } - - if ( - !secret.pendingCreation && - (await workspaceApprovalEnabled(secret.project.workspaceId, this.prisma)) - ) { - return await createApproval( - { - action: ApprovalAction.UPDATE, - itemType: ApprovalItemType.SECRET, - itemId: secret.id, - reason, - user, - workspaceId: secret.project.workspaceId, - metadata: dto - }, - this.prisma + const shouldCreateRevisions = dto.entries && dto.entries.length > 0 + + // Check if the secret with the same name already exists in the project + dto.name && (await this.secretExists(dto.name, secret.projectId)) + + // Check if the user has access to the environments + if (shouldCreateRevisions) { + const environmentIds = dto.entries.map((entry) => entry.environmentId) + await Promise.all( + environmentIds.map(async (environmentId) => { + const environment = + await this.authorityCheckerService.checkAuthorityOverEnvironment({ + userId: user.id, + entity: { id: environmentId }, + authority: Authority.READ_ENVIRONMENT, + prisma: this.prisma + }) + + // Check if the environment belongs to the project + if (environment.projectId !== secret.projectId) { + throw new BadRequestException( + `Environment: ${environmentId} does not belong to project: ${secret.projectId}` + ) + } + }) ) - } else { - return this.update(dto, user, secret) } - } - async updateSecretEnvironment( - user: User, - secretId: Secret['id'], - environmentId: Environment['id'], - reason?: string - ) { - const secret = await this.authorityCheckerService.checkAuthorityOverSecret({ - userId: user.id, - entity: { id: secretId }, - authority: Authority.UPDATE_SECRET, - prisma: this.prisma - }) + const op = [] - if (secret.environmentId === environmentId) { - throw new BadRequestException( - `Can not update the environment of the secret to the same environment: ${environmentId} in project ${secret.projectId}` - ) - } + // Update the secret - // Check if the environment exists - const environment = - await this.authorityCheckerService.checkAuthorityOverEnvironment({ - userId: user.id, - entity: { id: environmentId }, - authority: Authority.READ_ENVIRONMENT, - prisma: this.prisma + // Update the other fields + op.push( + this.prisma.secret.update({ + where: { + id: secret.id + }, + data: { + name: dto.name, + note: dto.note, + rotateAt: dto.rotateAfter + ? addHoursToDate(dto.rotateAfter) + : undefined, + lastUpdatedById: user.id + }, + include: { + project: { + select: { + workspaceId: true + } + }, + versions: { + select: { + environmentId: true, + value: true + } + } + } }) + ) - if (environment.projectId !== secret.projectId) { - throw new BadRequestException( - `Environment ${environmentId} does not belong to project ${secret.projectId}` - ) + // If new values for various environments are proposed, + // we want to create new versions for those environments + if (shouldCreateRevisions) { + for (const entry of dto.entries) { + // Fetch the latest version of the secret for the environment + const latestVersion = await this.prisma.secretVersion.findFirst({ + where: { + secretId: secret.id, + environmentId: entry.environmentId + }, + select: { + version: true + }, + orderBy: { + version: 'desc' + }, + take: 1 + }) + + // Create the new version + // Create the new version + op.push( + this.prisma.secretVersion.create({ + data: { + value: await encrypt(secret.project.publicKey, entry.value), + version: latestVersion ? latestVersion.version + 1 : 1, + createdById: user.id, + environmentId: entry.environmentId, + secretId: secret.id + } + }) + ) + } } - // Check if the secret already exists in the environment - if (await this.secretExists(secret.name, environmentId)) { - throw new ConflictException( - `Secret already exists: ${secret.name} in environment ${environmentId} in project ${secret.projectId}` - ) + // Make the transaction + const tx = await this.prisma.$transaction(op) + const updatedSecret = tx[0] + + // Notify the new secret version through Redis + if (dto.entries && dto.entries.length > 0) { + for (const entry of dto.entries) { + try { + await this.redis.publish( + CHANGE_NOTIFIER_RSC, + JSON.stringify({ + environmentId: entry.environmentId, + name: updatedSecret.name, + value: entry.value, + isSecret: true + }) + ) + } catch (error) { + this.logger.error(`Error publishing secret update to Redis: ${error}`) + } + } } - if ( - !secret.pendingCreation && - (await workspaceApprovalEnabled(secret.project.workspaceId, this.prisma)) - ) { - return await createApproval( - { - action: ApprovalAction.UPDATE, - itemType: ApprovalItemType.SECRET, - itemId: secret.id, - reason, - user, - workspaceId: secret.project.workspaceId, - metadata: { - environmentId - } + await createEvent( + { + triggeredBy: user, + entity: secret, + type: EventType.SECRET_UPDATED, + source: EventSource.SECRET, + title: `Secret updated`, + metadata: { + secretId: secret.id, + name: secret.name, + projectId: secret.projectId, + projectName: secret.project.name }, - this.prisma - ) - } else { - return this.updateEnvironment(user, secret, environment) - } + workspaceId: secret.project.workspaceId + }, + this.prisma + ) + + this.logger.log(`User ${user.id} updated secret ${secret.id}`) + + return updatedSecret } async rollbackSecret( user: User, secretId: Secret['id'], - rollbackVersion: SecretVersion['version'], - reason?: string + environmentId: Environment['id'], + rollbackVersion: SecretVersion['version'] ) { // Fetch the secret const secret = await this.authorityCheckerService.checkAuthorityOverSecret({ @@ -315,6 +314,18 @@ export class SecretService { prisma: this.prisma }) + // Filter the secret versions by the environment + secret.versions = secret.versions.filter( + (version) => version.environmentId === environmentId + ) + + if (secret.versions.length === 0) { + throw new NotFoundException( + `No versions found for environment: ${environmentId} for secret: ${secretId}` + ) + } + + // Sorting is in ascending order of dates. So the last element is the latest version const maxVersion = secret.versions[secret.versions.length - 1].version // Check if the rollback version is valid @@ -324,30 +335,54 @@ export class SecretService { ) } - if ( - !secret.pendingCreation && - (await workspaceApprovalEnabled(secret.project.workspaceId, this.prisma)) - ) { - return await createApproval( - { - action: ApprovalAction.UPDATE, - itemType: ApprovalItemType.SECRET, - itemId: secret.id, - reason, - user, - workspaceId: secret.project.workspaceId, - metadata: { - rollbackVersion - } - }, - this.prisma + // Rollback the secret + const result = await this.prisma.secretVersion.deleteMany({ + where: { + secretId: secret.id, + version: { + gt: Number(rollbackVersion) + } + } + }) + + try { + await this.redis.publish( + CHANGE_NOTIFIER_RSC, + JSON.stringify({ + environmentId, + name: secret.name, + value: secret.versions[rollbackVersion - 1].value, + isSecret: true + }) ) - } else { - return this.rollback(user, secret, rollbackVersion) + } catch (error) { + this.logger.error(`Error publishing secret update to Redis: ${error}`) } + + await createEvent( + { + triggeredBy: user, + entity: secret, + type: EventType.SECRET_UPDATED, + source: EventSource.SECRET, + title: `Secret rolled back`, + metadata: { + secretId: secret.id, + name: secret.name, + projectId: secret.projectId, + projectName: secret.project.name + }, + workspaceId: secret.project.workspaceId + }, + this.prisma + ) + + this.logger.log(`User ${user.id} rolled back secret ${secret.id}`) + + return result } - async deleteSecret(user: User, secretId: Secret['id'], reason?: string) { + async deleteSecret(user: User, secretId: Secret['id']) { // Check if the user has the required role const secret = await this.authorityCheckerService.checkAuthorityOverSecret({ userId: user.id, @@ -356,69 +391,29 @@ export class SecretService { prisma: this.prisma }) - if ( - !secret.pendingCreation && - (await workspaceApprovalEnabled(secret.project.workspaceId, this.prisma)) - ) { - return await createApproval( - { - action: ApprovalAction.DELETE, - itemType: ApprovalItemType.SECRET, - itemId: secretId, - reason, - user, - workspaceId: secret.project.workspaceId - }, - this.prisma - ) - } else { - return this.delete(user, secret) - } - } - - async getSecretById( - user: User, - secretId: Secret['id'], - decryptValue: boolean - ) { - // Fetch the secret - const secret = await this.authorityCheckerService.checkAuthorityOverSecret({ - userId: user.id, - entity: { id: secretId }, - authority: Authority.READ_SECRET, - prisma: this.prisma + // Delete the secret + await this.prisma.secret.delete({ + where: { + id: secret.id + } }) - const project = secret.project - - // Check if the project is allowed to store the private key - if (decryptValue && !project.storePrivateKey) { - throw new BadRequestException( - `Cannot decrypt secret value: ${secretId} as the project does not store the private key` - ) - } - - // Check if the project has a private key. This is just to ensure that we don't run into any - // problems while decrypting the secret - if (decryptValue && !project.privateKey) { - throw new NotFoundException( - `Cannot decrypt secret value: ${secretId} as the project does not have a private key` - ) - } - - if (decryptValue) { - // Decrypt the secret value - for (let i = 0; i < secret.versions.length; i++) { - const decryptedValue = await decrypt( - project.privateKey, - secret.versions[i].value - ) - secret.versions[i].value = decryptedValue - } - } + await createEvent( + { + triggeredBy: user, + type: EventType.SECRET_DELETED, + source: EventSource.SECRET, + entity: secret, + title: `Secret deleted`, + metadata: { + secretId: secret.id + }, + workspaceId: secret.project.workspaceId + }, + this.prisma + ) - // Return the secret - return secret + this.logger.log(`User ${user.id} deleted secret ${secret.id}`) } async getAllSecretsOfProject( @@ -430,12 +425,7 @@ export class SecretService { sort: string, order: string, search: string - ): Promise< - { - environment: { id: string; name: string } - secrets: any[] - }[] - > { + ) { // Fetch the project const project = await this.authorityCheckerService.checkAuthorityOverProject({ @@ -445,47 +435,22 @@ export class SecretService { prisma: this.prisma }) - // Check if the project is allowed to store the private key - if (decryptValue && !project.storePrivateKey) { - throw new BadRequestException( - `Cannot decrypt secret values as the project does not store the private key` - ) - } - - // Check if the project has a private key. This is just to ensure that we don't run into any - // problems while decrypting the secret - if (decryptValue && !project.privateKey) { - throw new NotFoundException( - `Cannot decrypt secret values as the project does not have a private key` - ) - } + // Check if the secret values can be decrypted + await this.checkAutoDecrypt(decryptValue, project) const secrets = await this.prisma.secret.findMany({ where: { projectId, - pendingCreation: false, name: { contains: search } }, include: { - versions: { - orderBy: { - version: 'desc' - }, - take: 1 - }, lastUpdatedBy: { select: { id: true, name: true } - }, - environment: { - select: { - id: true, - name: true - } } }, skip: page * limit, @@ -495,326 +460,117 @@ export class SecretService { } }) - // Group variables by environment - const secretsByEnvironment: { - [key: string]: { - environment: { id: string; name: string } - secrets: any[] - } - } = {} - - for (const secret of secrets) { - // Optionally decrypt secret value if decryptValue is true - if (decryptValue) { - const latestSecretVersion = secret.versions[0] - const decryptedValue = await decrypt( - project.privateKey, - latestSecretVersion.value - ) - latestSecretVersion.value = decryptedValue - } - - const { id, name } = secret.environment - if (!secretsByEnvironment[id]) { - secretsByEnvironment[id] = { - environment: { id, name }, - secrets: [] - } - } - secretsByEnvironment[id].secrets.push(secret) - } - - // Convert the object to an array and return - return Object.values(secretsByEnvironment) - } - - private async secretExists( - secretName: Secret['name'], - environmentId: Environment['id'] - ): Promise { - return ( - (await this.prisma.secret.count({ - where: { - pendingCreation: false, - name: secretName, + const secretsWithEnvironmentalValues = new Map< + Secret['id'], + { + secret: Secret + values: { environment: { - id: environmentId + name: Environment['name'] + id: Environment['id'] } - } - })) > 0 - ) - } - - async makeSecretApproved(secretId: Secret['id']) { - const secret = await this.prisma.secret.findUnique({ - where: { - id: secretId - } - }) - - const secretExists = await this.prisma.secret.count({ - where: { - name: secret.name, - environmentId: secret.environmentId, - pendingCreation: false, - projectId: secret.projectId + value: SecretVersion['value'] + }[] } - }) - - if (secretExists > 0) { - throw new ConflictException( - `Secret already exists: ${secret.name} in environment ${secret.environmentId} in project ${secret.projectId}` - ) - } + >() - return this.prisma.secret.update({ + // Find all the environments for this project + const environments = await this.prisma.environment.findMany({ where: { - id: secretId - }, - data: { - pendingCreation: false + projectId } }) - } - - async update( - dto: UpdateSecret | UpdateSecretMetadata, - user: User, - secret: SecretWithProjectAndVersion - ) { - let result + const environmentIds = new Map( + environments.map((env) => [env.id, env.name]) + ) - // Update the secret - // If a new secret value is proposed, we want to create a new version for - // that secret - if (dto.value) { - const previousVersion = await this.prisma.secretVersion.findFirst({ - where: { - secretId: secret.id - }, - select: { - version: true - }, - orderBy: { - version: 'desc' - }, - take: 1 - }) + for (const secret of secrets) { + // Make a copy of the environment IDs + const envIds = new Map(environmentIds) + let iterations = envIds.size - result = await this.prisma.secret.update({ - where: { - id: secret.id - }, - data: { - name: dto.name, - note: dto.note, - rotateAt: addHoursToDate(dto.rotateAfter), - lastUpdatedById: user.id, - versions: { - create: { - value: dto.value, // The value is already encrypted - version: previousVersion.version + 1, - createdById: user.id + // Find the latest version for each environment + while (iterations--) { + const latestVersion = await this.prisma.secretVersion.findFirst({ + where: { + secretId: secret.id, + environmentId: { + in: Array.from(envIds.keys()) } + }, + orderBy: { + version: 'desc' } - } - }) + }) - try { - await this.redis.publish( - CHANGE_NOTIFIER_RSC, - JSON.stringify({ - environmentId: secret.environmentId, - name: secret.name, - value: dto.value, - isSecret: true + if (!latestVersion) continue + + if (secretsWithEnvironmentalValues.has(secret.id)) { + secretsWithEnvironmentalValues.get(secret.id).values.push({ + environment: { + id: latestVersion.environmentId, + name: envIds.get(latestVersion.environmentId) + }, + value: decryptValue + ? await decrypt(project.privateKey, latestVersion.value) + : latestVersion.value + }) + } else { + secretsWithEnvironmentalValues.set(secret.id, { + secret, + values: [ + { + environment: { + id: latestVersion.environmentId, + name: envIds.get(latestVersion.environmentId) + }, + value: decryptValue + ? await decrypt(project.privateKey, latestVersion.value) + : latestVersion.value + } + ] }) - ) - } catch (error) { - this.logger.error( - this.logger.error(`Error publishing secret update to Redis: ${error}`) - ) - } - } else { - result = await this.prisma.secret.update({ - where: { - id: secret.id - }, - data: { - note: dto.note, - name: dto.name, - rotateAt: dto.rotateAfter - ? addHoursToDate(dto.rotateAfter) - : undefined, - lastUpdatedById: user.id } - }) - } - - await createEvent( - { - triggeredBy: user, - entity: secret, - type: EventType.SECRET_UPDATED, - source: EventSource.SECRET, - title: `Secret updated`, - metadata: { - secretId: secret.id, - name: secret.name, - projectId: secret.projectId, - projectName: secret.project.name - }, - workspaceId: secret.project.workspaceId - }, - this.prisma - ) - - this.logger.log(`User ${user.id} updated secret ${secret.id}`) - - return result - } - async updateEnvironment( - user: User, - secret: SecretWithProjectAndVersion, - environment: Environment - ) { - // Update the secret - const result = await this.prisma.secret.update({ - where: { - id: secret.id - }, - data: { - environmentId: environment.id + envIds.delete(latestVersion.environmentId) } - }) - - await createEvent( - { - triggeredBy: user, - entity: secret, - type: EventType.SECRET_UPDATED, - source: EventSource.SECRET, - title: `Secret environment updated`, - metadata: { - secretId: secret.id, - name: secret.name, - projectId: secret.projectId, - projectName: secret.project.name, - environmentId: environment.id, - environmentName: environment.name - }, - workspaceId: secret.project.workspaceId - }, - this.prisma - ) - - this.logger.log(`User ${user.id} updated secret ${secret.id}`) + } - return result + return Array.from(secretsWithEnvironmentalValues.values()) } - async rollback( - user: User, - secret: SecretWithProjectAndVersion, - rollbackVersion: number + private async secretExists( + secretName: Secret['name'], + projectId: Project['id'] ) { - // Rollback the secret - const result = await this.prisma.secretVersion.deleteMany({ - where: { - secretId: secret.id, - version: { - gt: Number(rollbackVersion) + if ( + (await this.prisma.secret.findFirst({ + where: { + name: secretName, + projectId } - } - }) - - try { - await this.redis.publish( - CHANGE_NOTIFIER_RSC, - JSON.stringify({ - environmentId: secret.environmentId, - name: secret.name, - value: secret.versions[rollbackVersion - 1].value, - isSecret: true - }) - ) - } catch (error) { - this.logger.error( - this.logger.error(`Error publishing secret update to Redis: ${error}`) + })) !== null + ) { + throw new ConflictException( + `Secret already exists: ${secretName} in project ${projectId}` ) } - - await createEvent( - { - triggeredBy: user, - entity: secret, - type: EventType.SECRET_UPDATED, - source: EventSource.SECRET, - title: `Secret rolled back`, - metadata: { - secretId: secret.id, - name: secret.name, - projectId: secret.projectId, - projectName: secret.project.name - }, - workspaceId: secret.project.workspaceId - }, - this.prisma - ) - - this.logger.log(`User ${user.id} rolled back secret ${secret.id}`) - - return result } - async delete(user: User, secret: SecretWithProject) { - const op = [] - - // Delete the secret - op.push( - this.prisma.secret.delete({ - where: { - id: secret.id - } - }) - ) - - // If the secret is in pending creation and the workspace approval is enabled, we need to - // delete the approval as well - if ( - secret.pendingCreation && - (await workspaceApprovalEnabled(secret.project.workspaceId, this.prisma)) - ) { - op.push( - this.prisma.approval.deleteMany({ - where: { - itemId: secret.id, - itemType: ApprovalItemType.SECRET, - action: ApprovalAction.CREATE, - status: ApprovalStatus.PENDING - } - }) + private async checkAutoDecrypt(decryptValue: boolean, project: Project) { + // Check if the project is allowed to store the private key + if (decryptValue && !project.storePrivateKey) { + throw new BadRequestException( + `Cannot decrypt secret values as the project does not store the private key` ) } - await this.prisma.$transaction(op) - - await createEvent( - { - triggeredBy: user, - type: EventType.SECRET_DELETED, - source: EventSource.SECRET, - entity: secret, - title: `Secret deleted`, - metadata: { - secretId: secret.id - }, - workspaceId: secret.project.workspaceId - }, - this.prisma - ) - - this.logger.log(`User ${user.id} deleted secret ${secret.id}`) + // Check if the project has a private key. This is just to ensure that we don't run into any + // problems while decrypting the secret + if (decryptValue && !project.privateKey) { + throw new NotFoundException( + `Cannot decrypt secret values as the project does not have a private key` + ) + } } } diff --git a/apps/api/src/variable/controller/variable.controller.ts b/apps/api/src/variable/controller/variable.controller.ts index 895145f6..39de2ea2 100644 --- a/apps/api/src/variable/controller/variable.controller.ts +++ b/apps/api/src/variable/controller/variable.controller.ts @@ -11,7 +11,6 @@ import { import { VariableService } from '../service/variable.service' import { RequiredApiKeyAuthorities } from '../../decorators/required-api-key-authorities.decorator' import { Authority, User } from '@prisma/client' -import { AlphanumericReasonValidationPipe } from '../../common/alphanumeric-reason-pipe' import { CurrentUser } from '../../decorators/user.decorator' import { CreateVariable } from '../dto/create.variable/create.variable' @@ -24,15 +23,9 @@ export class VariableController { async createVariable( @CurrentUser() user: User, @Param('projectId') projectId: string, - @Body() dto: CreateVariable, - @Query('reason', AlphanumericReasonValidationPipe) reason: string + @Body() dto: CreateVariable ) { - return await this.variableService.createVariable( - user, - dto, - projectId, - reason - ) + return await this.variableService.createVariable(user, dto, projectId) } @Put(':variableId') @@ -40,34 +33,9 @@ export class VariableController { async updateVariable( @CurrentUser() user: User, @Param('variableId') variableId: string, - @Body() dto: CreateVariable, - @Query('reason', AlphanumericReasonValidationPipe) reason: string - ) { - return await this.variableService.updateVariable( - user, - variableId, - dto, - reason - ) - } - - @Put(':variableId/environment/:environmentId') - @RequiredApiKeyAuthorities( - Authority.UPDATE_VARIABLE, - Authority.READ_ENVIRONMENT - ) - async updateVariableEnvironment( - @CurrentUser() user: User, - @Param('variableId') variableId: string, - @Param('environmentId') environmentId: string, - @Query('reason', AlphanumericReasonValidationPipe) reason: string + @Body() dto: CreateVariable ) { - return await this.variableService.updateVariableEnvironment( - user, - variableId, - environmentId, - reason - ) + return await this.variableService.updateVariable(user, variableId, dto) } @Put(':variableId/rollback/:rollbackVersion') @@ -75,34 +43,24 @@ export class VariableController { async rollbackVariable( @CurrentUser() user: User, @Param('variableId') variableId: string, - @Param('rollbackVersion') rollbackVersion: number, - @Query('reason', AlphanumericReasonValidationPipe) reason: string + @Query('environmentId') environmentId: string, + @Param('rollbackVersion') rollbackVersion: number ) { return await this.variableService.rollbackVariable( user, variableId, - rollbackVersion, - reason + environmentId, + rollbackVersion ) } @Delete(':variableId') @RequiredApiKeyAuthorities(Authority.DELETE_VARIABLE) async deleteVariable( - @CurrentUser() user: User, - @Param('variableId') variableId: string, - @Query('reason', AlphanumericReasonValidationPipe) reason: string - ) { - return await this.variableService.deleteVariable(user, variableId, reason) - } - - @Get(':variableId') - @RequiredApiKeyAuthorities(Authority.READ_VARIABLE) - async getVariable( @CurrentUser() user: User, @Param('variableId') variableId: string ) { - return await this.variableService.getVariableById(user, variableId) + return await this.variableService.deleteVariable(user, variableId) } @Get('/all/:projectId') diff --git a/apps/api/src/variable/dto/create.variable/create.variable.ts b/apps/api/src/variable/dto/create.variable/create.variable.ts index a3563d45..20502a4f 100644 --- a/apps/api/src/variable/dto/create.variable/create.variable.ts +++ b/apps/api/src/variable/dto/create.variable/create.variable.ts @@ -1,18 +1,37 @@ -import { IsOptional, IsString, Length } from 'class-validator' +import 'reflect-metadata' +import { Transform, Type } from 'class-transformer' +import { + IsArray, + IsOptional, + IsString, + Length, + ValidateNested +} from 'class-validator' export class CreateVariable { @IsString() + @Transform(({ value }) => value.trim()) name: string - @IsString() - value: string - @IsString() @IsOptional() @Length(0, 100) + @Transform(({ value }) => (value ? value.trim() : null)) note?: string - @IsString() @IsOptional() - environmentId?: string + @IsArray() + @ValidateNested({ each: true }) + @Type(() => Entry) + entries?: Entry[] +} + +class Entry { + @IsString() + @Transform(({ value }) => value.trim()) + environmentId: string + + @IsString() + @Transform(({ value }) => value.trim()) + value: string } diff --git a/apps/api/src/variable/service/variable.service.ts b/apps/api/src/variable/service/variable.service.ts index b115bb6f..7c8b7d37 100644 --- a/apps/api/src/variable/service/variable.service.ts +++ b/apps/api/src/variable/service/variable.service.ts @@ -8,9 +8,6 @@ import { } from '@nestjs/common' import { PrismaService } from '../../prisma/prisma.service' import { - ApprovalAction, - ApprovalItemType, - ApprovalStatus, Authority, Environment, EventSource, @@ -21,16 +18,8 @@ import { VariableVersion } from '@prisma/client' import { CreateVariable } from '../dto/create.variable/create.variable' -import getDefaultEnvironmentOfProject from '../../common/get-default-project-environment' import createEvent from '../../common/create-event' import { UpdateVariable } from '../dto/update.variable/update.variable' -import workspaceApprovalEnabled from '../../common/workspace-approval-enabled' -import createApproval from '../../common/create-approval' -import { UpdateVariableMetadata } from '../../approval/approval.types' -import { - VariableWithProject, - VariableWithProjectAndVersion -} from '../variable.types' import { RedisClientType } from 'redis' import { REDIS_CLIENT } from '../../provider/redis.provider' import { CHANGE_NOTIFIER_RSC } from '../../socket/change-notifier.socket' @@ -55,10 +44,8 @@ export class VariableService { async createVariable( user: User, dto: CreateVariable, - projectId: Project['id'], - reason?: string + projectId: Project['id'] ) { - const environmentId = dto.environmentId // Fetch the project const project = await this.authorityCheckerService.checkAuthorityOverProject({ @@ -68,63 +55,46 @@ export class VariableService { prisma: this.prisma }) - // Check i the environment exists - let environment: Environment | null = null - if (environmentId) { - environment = - await this.authorityCheckerService.checkAuthorityOverEnvironment({ - userId: user.id, - entity: { id: environmentId }, - authority: Authority.READ_ENVIRONMENT, - prisma: this.prisma + // Check if a variable with the same name already exists in the project + await this.variableExists(dto.name, projectId) + + const shouldCreateRevisions = dto.entries && dto.entries.length > 0 + + // Check if the user has access to the environments + if (shouldCreateRevisions) { + const environmentIds = dto.entries.map((entry) => entry.environmentId) + await Promise.all( + environmentIds.map(async (environmentId) => { + const environment = + await this.authorityCheckerService.checkAuthorityOverEnvironment({ + userId: user.id, + entity: { id: environmentId }, + authority: Authority.READ_ENVIRONMENT, + prisma: this.prisma + }) + + // Check if the environment belongs to the project + if (environment.projectId !== projectId) { + throw new BadRequestException( + `Environment: ${environmentId} does not belong to project: ${projectId}` + ) + } }) - } - if (!environment) { - environment = await getDefaultEnvironmentOfProject(projectId, this.prisma) - } - - // If any default environment was not found, throw an error - if (!environment) { - throw new NotFoundException( - `No default environment found for project with id ${projectId}` ) } - // Check if the variable already exists in the environment - if (await this.variableExists(dto.name, environment.id)) { - throw new ConflictException( - `Variable already exists: ${dto.name} in environment ${environment.id} in project ${projectId}` - ) - } - - const approvalEnabled = await workspaceApprovalEnabled( - project.workspaceId, - this.prisma - ) - // Create the variable const variable = await this.prisma.variable.create({ data: { name: dto.name, note: dto.note, - pendingCreation: - project.pendingCreation || - environment.pendingCreation || - approvalEnabled, - versions: { - create: { - value: dto.value, - version: 1, - createdBy: { - connect: { - id: user.id - } - } - } - }, - environment: { - connect: { - id: environment.id + versions: shouldCreateRevisions && { + createMany: { + data: dto.entries.map((entry) => ({ + value: entry.value, + createdById: user.id, + environmentId: entry.environmentId + })) } }, project: { @@ -143,6 +113,12 @@ export class VariableService { select: { workspaceId: true } + }, + versions: { + select: { + environmentId: true, + value: true + } } } }) @@ -158,9 +134,7 @@ export class VariableService { variableId: variable.id, name: variable.name, projectId, - projectName: project.name, - environmentId: environment.id, - environmentName: environment.name + projectName: project.name }, workspaceId: project.workspaceId }, @@ -169,153 +143,13 @@ export class VariableService { this.logger.log(`User ${user.id} created variable ${variable.id}`) - if ( - !project.pendingCreation && - !environment.pendingCreation && - approvalEnabled - ) { - const approval = await createApproval( - { - action: ApprovalAction.CREATE, - itemType: ApprovalItemType.VARIABLE, - itemId: variable.id, - reason, - user, - workspaceId: project.workspaceId - }, - this.prisma - ) - return { - variable, - approval - } - } else { - return variable - } + return variable } async updateVariable( user: User, variableId: Variable['id'], - dto: UpdateVariable, - reason?: string - ) { - const variable = - await this.authorityCheckerService.checkAuthorityOverVariable({ - userId: user.id, - entity: { id: variableId }, - authority: Authority.UPDATE_VARIABLE, - prisma: this.prisma - }) - - // Check if the variable already exists in the environment - if ( - (dto.name && - (await this.variableExists(dto.name, variable.environmentId))) || - variable.name === dto.name - ) { - throw new ConflictException( - `Variable already exists: ${dto.name} in environment ${variable.environmentId}` - ) - } - - if ( - !variable.pendingCreation && - (await workspaceApprovalEnabled( - variable.project.workspaceId, - this.prisma - )) - ) { - return await createApproval( - { - action: ApprovalAction.UPDATE, - itemType: ApprovalItemType.VARIABLE, - itemId: variable.id, - reason, - user, - workspaceId: variable.project.workspaceId, - metadata: dto - }, - this.prisma - ) - } else { - return this.update(dto, user, variable) - } - } - - async updateVariableEnvironment( - user: User, - variableId: Variable['id'], - environmentId: Environment['id'], - reason?: string - ) { - const variable = - await this.authorityCheckerService.checkAuthorityOverVariable({ - userId: user.id, - entity: { id: variableId }, - authority: Authority.UPDATE_VARIABLE, - prisma: this.prisma - }) - - if (variable.environmentId === environmentId) { - throw new BadRequestException( - `Can not update the environment of the variable to the same environment: ${environmentId}` - ) - } - - // Check if the environment exists - const environment = - await this.authorityCheckerService.checkAuthorityOverEnvironment({ - userId: user.id, - entity: { id: environmentId }, - authority: Authority.READ_ENVIRONMENT, - prisma: this.prisma - }) - - // Check if the environment belongs to the same project - if (environment.projectId !== variable.projectId) { - throw new BadRequestException( - `Environment ${environmentId} does not belong to the same project ${variable.projectId}` - ) - } - - // Check if the variable already exists in the environment - if ( - !variable.pendingCreation && - (await this.variableExists(variable.name, environment.id)) - ) { - throw new ConflictException( - `Variable already exists: ${variable.name} in environment ${environment.id} in project ${variable.projectId}` - ) - } - - if ( - await workspaceApprovalEnabled(variable.project.workspaceId, this.prisma) - ) { - return await createApproval( - { - action: ApprovalAction.UPDATE, - itemType: ApprovalItemType.VARIABLE, - itemId: variable.id, - reason, - user, - workspaceId: variable.project.workspaceId, - metadata: { - environmentId - } - }, - this.prisma - ) - } else { - return this.updateEnvironment(user, variable, environment) - } - } - - async rollbackVariable( - user: User, - variableId: Variable['id'], - rollbackVersion: VariableVersion['version'], - reason?: string + dto: UpdateVariable ) { const variable = await this.authorityCheckerService.checkAuthorityOverVariable({ @@ -325,279 +159,122 @@ export class VariableService { prisma: this.prisma }) - const maxVersion = variable.versions[variable.versions.length - 1].version - - // Check if the rollback version is valid - if (rollbackVersion < 1 || rollbackVersion >= maxVersion) { - throw new NotFoundException( - `Invalid rollback version: ${rollbackVersion} for variable: ${variableId}` - ) - } - - if ( - !variable.pendingCreation && - (await workspaceApprovalEnabled( - variable.project.workspaceId, - this.prisma - )) - ) { - return await createApproval( - { - action: ApprovalAction.UPDATE, - itemType: ApprovalItemType.VARIABLE, - itemId: variable.id, - reason, - user, - workspaceId: variable.project.workspaceId, - metadata: { - rollbackVersion - } - }, - this.prisma - ) - } else { - return this.rollback(user, variable, rollbackVersion) - } - } - - async deleteVariable( - user: User, - variableId: Variable['id'], - reason?: string - ) { - const variable = - await this.authorityCheckerService.checkAuthorityOverVariable({ - userId: user.id, - entity: { id: variableId }, - authority: Authority.DELETE_VARIABLE, - prisma: this.prisma - }) - - if ( - !variable.pendingCreation && - (await workspaceApprovalEnabled( - variable.project.workspaceId, - this.prisma - )) - ) { - return await createApproval( - { - action: ApprovalAction.DELETE, - itemType: ApprovalItemType.VARIABLE, - itemId: variable.id, - reason, - user, - workspaceId: variable.project.workspaceId - }, - this.prisma - ) - } else { - return this.delete(user, variable) - } - } - - async getVariableById(user: User, variableId: Variable['id']) { - return this.authorityCheckerService.checkAuthorityOverVariable({ - userId: user.id, - entity: { id: variableId }, - authority: Authority.READ_VARIABLE, - prisma: this.prisma - }) - } - - async getAllVariablesOfProject( - user: User, - projectId: Project['id'], - page: number, - limit: number, - sort: string, - order: string, - search: string - ): Promise< - { - environment: { id: string; name: string } - variables: any[] - }[] - > { - // Check if the user has the required authorities in the project - await this.authorityCheckerService.checkAuthorityOverProject({ - userId: user.id, - entity: { id: projectId }, - authority: Authority.READ_VARIABLE, - prisma: this.prisma - }) - - const variables = await this.prisma.variable.findMany({ - where: { - projectId, - pendingCreation: false, - name: { - contains: search - } - }, - include: { - versions: { - orderBy: { - version: 'desc' - }, - take: 1 - }, - lastUpdatedBy: { - select: { - id: true, - name: true - } - }, - environment: { - select: { - id: true, - name: true - } - } - }, - skip: page * limit, - take: limit, - orderBy: { - [sort]: order - } - }) - - // Group variables by environment - const variablesByEnvironment: { - [key: string]: { - environment: { id: string; name: string } - variables: any[] - } - } = {} - for (const variable of variables) { - const { id, name } = variable.environment - if (!variablesByEnvironment[id]) { - variablesByEnvironment[id] = { - environment: { id, name }, - variables: [] - } - } - variablesByEnvironment[id].variables.push(variable) - } - - // Convert the object to an array and return - return Object.values(variablesByEnvironment) - } - - private async variableExists( - variableName: Variable['name'], - environmentId: Environment['id'] - ): Promise { - return ( - (await this.prisma.variable.count({ - where: { - name: variableName, - pendingCreation: false, - environment: { - id: environmentId + // Check if the variable already exists in the project + dto.name && (await this.variableExists(dto.name, variable.projectId)) + + const shouldCreateRevisions = dto.entries && dto.entries.length > 0 + + // Check if the user has access to the environments + if (shouldCreateRevisions) { + const environmentIds = dto.entries.map((entry) => entry.environmentId) + await Promise.all( + environmentIds.map(async (environmentId) => { + const environment = + await this.authorityCheckerService.checkAuthorityOverEnvironment({ + userId: user.id, + entity: { id: environmentId }, + authority: Authority.READ_ENVIRONMENT, + prisma: this.prisma + }) + + // Check if the environment belongs to the project + if (environment.projectId !== variable.projectId) { + throw new BadRequestException( + `Environment: ${environmentId} does not belong to project: ${variable.projectId}` + ) } - } - })) > 0 - ) - } - - async makeVariableApproved(variableId: Variable['id']): Promise { - const variable = await this.prisma.variable.findUnique({ - where: { - id: variableId - } - }) - - const variableExists = await this.prisma.variable.count({ - where: { - name: variable.name, - pendingCreation: false, - environmentId: variable.environmentId, - projectId: variable.projectId - } - }) - - if (variableExists > 0) { - throw new ConflictException( - `Variable already exists: ${variable.name} in environment ${variable.environmentId} in project ${variable.projectId}` + }) ) } - return this.prisma.variable.update({ - where: { - id: variableId - }, - data: { - pendingCreation: false - } - }) - } - - async update( - dto: UpdateVariable | UpdateVariableMetadata, - user: User, - variable: VariableWithProjectAndVersion - ) { - let result + const op = [] // Update the variable - // If a new variable value is proposed, we want to create a new version for that variable - if (dto.value) { - const previousVersion = await this.prisma.variableVersion.findFirst({ - where: { - variableId: variable.id - }, - select: { - version: true - }, - orderBy: { - version: 'desc' - }, - take: 1 - }) - result = await this.prisma.variable.update({ + // Update the fields + op.push( + this.prisma.variable.update({ where: { id: variable.id }, data: { name: dto.name, note: dto.note, - lastUpdatedById: user.id, + lastUpdatedById: user.id + }, + include: { + project: { + select: { + workspaceId: true + } + }, versions: { - create: { - value: dto.value, - version: previousVersion.version + 1, - createdById: user.id + select: { + environmentId: true, + value: true } } } }) + ) + + // If new values for various environments are proposed, + // we want to create new versions for those environments + if (shouldCreateRevisions) { + for (const entry of dto.entries) { + // Fetch the latest version of the variable for the environment + const latestVersion = await this.prisma.variableVersion.findFirst({ + where: { + variableId: variable.id, + environmentId: entry.environmentId + }, + select: { + version: true + }, + orderBy: { + version: 'desc' + }, + take: 1 + }) - try { - await this.redis.publish( - CHANGE_NOTIFIER_RSC, - JSON.stringify({ - environmentId: variable.environmentId, - name: variable.name, - value: dto.value, - isSecret: false + // Create the new version + op.push( + this.prisma.variableVersion.create({ + data: { + value: entry.value, + version: latestVersion ? latestVersion.version + 1 : 1, + createdById: user.id, + environmentId: entry.environmentId, + variableId: variable.id + } }) ) - } catch (error) { - this.logger.error(`Error publishing variable update to Redis: ${error}`) } - } else { - result = await this.prisma.variable.update({ - where: { - id: variable.id - }, - data: { - note: dto.note, - name: dto.name, - lastUpdatedById: user.id + } + + // Make the transaction + const tx = await this.prisma.$transaction(op) + const updatedVariable = tx[0] + + // Notify the new variable version through Redis + if (dto.entries && dto.entries.length > 0) { + for (const entry of dto.entries) { + try { + await this.redis.publish( + CHANGE_NOTIFIER_RSC, + JSON.stringify({ + environmentId: entry.environmentId, + name: updatedVariable.name, + value: entry.value, + isSecret: false + }) + ) + } catch (error) { + this.logger.error( + `Error publishing variable update to Redis: ${error}` + ) } - }) + } } await createEvent( @@ -620,58 +297,44 @@ export class VariableService { this.logger.log(`User ${user.id} updated variable ${variable.id}`) - return result + return updatedVariable } - async updateEnvironment( + async rollbackVariable( user: User, - variable: VariableWithProject, - environment: Environment + variableId: Variable['id'], + environmentId: Environment['id'], + rollbackVersion: VariableVersion['version'] ) { - // Update the variable - const result = await this.prisma.variable.update({ - where: { - id: variable.id - }, - data: { - environment: { - connect: { - id: environment.id - } - } - } - }) + const variable = + await this.authorityCheckerService.checkAuthorityOverVariable({ + userId: user.id, + entity: { id: variableId }, + authority: Authority.UPDATE_VARIABLE, + prisma: this.prisma + }) - await createEvent( - { - triggeredBy: user, - entity: variable, - type: EventType.VARIABLE_UPDATED, - source: EventSource.VARIABLE, - title: `Variable environment updated`, - metadata: { - variableId: variable.id, - name: variable.name, - projectId: variable.projectId, - projectName: variable.project.name, - environmentId: environment.id, - environmentName: environment.name - }, - workspaceId: variable.project.workspaceId - }, - this.prisma + // Filter the variable versions by the environment + variable.versions = variable.versions.filter( + (version) => version.environmentId === environmentId ) - this.logger.log(`User ${user.id} updated variable ${variable.id}`) + if (variable.versions.length === 0) { + throw new NotFoundException( + `No versions found for environment: ${environmentId} for variable: ${variableId}` + ) + } - return result - } + // Sorting is in ascending order of dates. So the last element is the latest version + const maxVersion = variable.versions[variable.versions.length - 1].version + + // Check if the rollback version is valid + if (rollbackVersion < 1 || rollbackVersion >= maxVersion) { + throw new NotFoundException( + `Invalid rollback version: ${rollbackVersion} for variable: ${variableId}` + ) + } - async rollback( - user: User, - variable: VariableWithProjectAndVersion, - rollbackVersion: VariableVersion['version'] - ) { // Rollback the variable const result = await this.prisma.variableVersion.deleteMany({ where: { @@ -683,10 +346,11 @@ export class VariableService { }) try { + // Notify the new variable version through Redis await this.redis.publish( CHANGE_NOTIFIER_RSC, JSON.stringify({ - environmentId: variable.environmentId, + environmentId, name: variable.name, value: variable.versions[rollbackVersion - 1].value, isSecret: false @@ -720,39 +384,21 @@ export class VariableService { return result } - async delete(user: User, variable: VariableWithProject) { - const op = [] - - // Delete the variable - op.push( - this.prisma.variable.delete({ - where: { - id: variable.id - } + async deleteVariable(user: User, variableId: Variable['id']) { + const variable = + await this.authorityCheckerService.checkAuthorityOverVariable({ + userId: user.id, + entity: { id: variableId }, + authority: Authority.DELETE_VARIABLE, + prisma: this.prisma }) - ) - - // If the variable is in pending creation and the workspace approval is enabled, we need to delete the approval - if ( - variable.pendingCreation && - (await workspaceApprovalEnabled( - variable.project.workspaceId, - this.prisma - )) - ) { - op.push( - this.prisma.approval.deleteMany({ - where: { - itemId: variable.id, - itemType: ApprovalItemType.VARIABLE, - status: ApprovalStatus.PENDING, - action: ApprovalAction.CREATE - } - }) - ) - } - await this.prisma.$transaction(op) + // Delete the variable + await this.prisma.variable.delete({ + where: { + id: variable.id + } + }) await createEvent( { @@ -774,4 +420,136 @@ export class VariableService { this.logger.log(`User ${user.id} deleted variable ${variable.id}`) } + + async getAllVariablesOfProject( + user: User, + projectId: Project['id'], + page: number, + limit: number, + sort: string, + order: string, + search: string + ) { + // Check if the user has the required authorities in the project + await this.authorityCheckerService.checkAuthorityOverProject({ + userId: user.id, + entity: { id: projectId }, + authority: Authority.READ_VARIABLE, + prisma: this.prisma + }) + + const variables = await this.prisma.variable.findMany({ + where: { + projectId, + name: { + contains: search + } + }, + include: { + lastUpdatedBy: { + select: { + id: true, + name: true + } + } + }, + skip: page * limit, + take: limit, + orderBy: { + [sort]: order + } + }) + + const variablesWithEnvironmentalValues = new Map< + Variable['id'], + { + variable: Variable + values: { + environment: { + name: Environment['name'] + id: Environment['id'] + } + value: VariableVersion['value'] + }[] + } + >() + + // Find all the environments for this project + const environments = await this.prisma.environment.findMany({ + where: { + projectId + } + }) + const environmentIds = new Map( + environments.map((env) => [env.id, env.name]) + ) + + for (const variable of variables) { + // Make a copy of the environment IDs + const envIds = new Map(environmentIds) + let iterations = envIds.size + + // Find the latest version for each environment + while (iterations--) { + const latestVersion = await this.prisma.variableVersion.findFirst({ + where: { + variableId: variable.id, + environmentId: { + in: Array.from(envIds.keys()) + } + }, + orderBy: { + version: 'desc' + } + }) + + if (!latestVersion) continue + + if (variablesWithEnvironmentalValues.has(variable.id)) { + variablesWithEnvironmentalValues.get(variable.id).values.push({ + environment: { + id: latestVersion.environmentId, + name: envIds.get(latestVersion.environmentId) + }, + value: latestVersion.value + }) + } else { + variablesWithEnvironmentalValues.set(variable.id, { + variable, + values: [ + { + environment: { + id: latestVersion.environmentId, + name: envIds.get(latestVersion.environmentId) + }, + value: latestVersion.value + } + ] + }) + } + + envIds.delete(latestVersion.environmentId) + } + } + + return Array.from(variablesWithEnvironmentalValues.values()) + } + + private async variableExists( + variableName: Variable['name'], + projectId: Project['id'] + ) { + if ( + (await this.prisma.variable.findFirst({ + where: { + name: variableName, + projectId + } + })) !== null + ) { + throw new ConflictException( + `Variable already exists: ${variableName} in project ${projectId}` + ) + } + } } diff --git a/apps/api/src/variable/variable.e2e.spec.ts b/apps/api/src/variable/variable.e2e.spec.ts index 4a05ae13..203ff34b 100644 --- a/apps/api/src/variable/variable.e2e.spec.ts +++ b/apps/api/src/variable/variable.e2e.spec.ts @@ -48,11 +48,10 @@ describe('Variable Controller Tests', () => { let userService: UserService let user1: User, user2: User - let workspace1: Workspace, workspace2: Workspace - let project1: Project, project2: Project, workspace2Project: Project + let workspace1: Workspace + let project1: Project let environment1: Environment let environment2: Environment - let workspace2Environment: Environment let variable1: Variable beforeAll(async () => { @@ -102,7 +101,6 @@ describe('Variable Controller Tests', () => { }) workspace1 = createUser1.defaultWorkspace - workspace2 = createUser2.defaultWorkspace delete createUser1.defaultWorkspace delete createUser2.defaultWorkspace @@ -118,56 +116,15 @@ describe('Variable Controller Tests', () => { environments: [ { name: 'Environment 1', - description: 'Environment 1 description', - isDefault: true + description: 'Environment 1 description' }, { name: 'Environment 2', - description: 'Environment 2 description', - isDefault: false + description: 'Environment 2 description' } ] })) as Project - project2 = (await projectService.createProject(user1, workspace1.id, { - name: 'Project 2', - description: 'Project 2 description', - storePrivateKey: false, - accessLevel: ProjectAccessLevel.PRIVATE, - environments: [ - { - name: 'Environment 1', - description: 'Environment 1 description', - isDefault: true - } - ] - })) as Project - - workspace2Project = (await projectService.createProject( - user2, - workspace2.id, - { - name: 'Workspace 2 Project', - description: 'Workspace 2 Project description', - storePrivateKey: true, - accessLevel: ProjectAccessLevel.PRIVATE, - environments: [ - { - name: 'Environment 1', - description: 'Environment 1 description', - isDefault: true - } - ] - } - )) as Project - - workspace2Environment = await prisma.environment.findFirst({ - where: { - projectId: workspace2Project.id, - name: 'Environment 1' - } - }) - environment1 = await prisma.environment.findFirst({ where: { projectId: project1.id, @@ -186,8 +143,12 @@ describe('Variable Controller Tests', () => { user1, { name: 'Variable 1', - value: 'Variable 1 value', - environmentId: environment2.id + entries: [ + { + environmentId: environment1.id, + value: 'Variable 1 value' + } + ] }, project1.id )) as Variable @@ -213,11 +174,15 @@ describe('Variable Controller Tests', () => { method: 'POST', url: `/variable/${project1.id}`, payload: { - environmentId: environment2.id, name: 'Variable 3', - value: 'Variable 3 value', note: 'Variable 3 note', - rotateAfter: '24' + rotateAfter: '24', + entries: [ + { + value: 'Variable 3 value', + environmentId: environment2.id + } + ] }, headers: { 'x-e2e-user-email': user1.email @@ -231,8 +196,9 @@ describe('Variable Controller Tests', () => { expect(body).toBeDefined() expect(body.name).toBe('Variable 3') expect(body.note).toBe('Variable 3 note') - expect(body.environmentId).toBe(environment2.id) expect(body.projectId).toBe(project1.id) + expect(body.versions.length).toBe(1) + expect(body.versions[0].value).toBe('Variable 3 value') const variable = await prisma.variable.findUnique({ where: { @@ -255,41 +221,19 @@ describe('Variable Controller Tests', () => { expect(variableVersion.version).toBe(1) }) - it('should create variable in default environment if environmentId is not provided', async () => { - const response = await app.inject({ - method: 'POST', - url: `/variable/${project1.id}`, - payload: { - name: 'Variable 2', - value: 'Variable 2 value', - note: 'Variable 2 note', - rotateAfter: '24' - }, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(201) - - const body = response.json() - - expect(body).toBeDefined() - expect(body.name).toBe('Variable 2') - expect(body.note).toBe('Variable 2 note') - expect(body.environmentId).toBe(environment1.id) - expect(body.projectId).toBe(project1.id) - }) - it('should not be able to create a variable with a non-existing environment', async () => { const response = await app.inject({ method: 'POST', url: `/variable/${project1.id}`, payload: { - environmentId: 'non-existing-environment-id', name: 'Variable 3', - value: 'Variable 3 value', - rotateAfter: '24' + rotateAfter: '24', + entries: [ + { + value: 'Variable 3 value', + environmentId: 'non-existing-environment-id' + } + ] }, headers: { 'x-e2e-user-email': user1.email @@ -305,7 +249,6 @@ describe('Variable Controller Tests', () => { url: `/variable/${project1.id}`, payload: { name: 'Variable 3', - value: 'Variable 3 value', rotateAfter: '24' }, headers: { @@ -319,54 +262,12 @@ describe('Variable Controller Tests', () => { ) }) - it('should fail if project has no default environment(hypothetical case)', async () => { - await prisma.environment.updateMany({ - where: { - projectId: project1.id, - name: 'Environment 1' - }, - data: { - isDefault: false - } - }) - + it('should not be able to create a duplicate variable in the same project', async () => { const response = await app.inject({ method: 'POST', url: `/variable/${project1.id}`, payload: { - name: 'Variable 4', - value: 'Variable 4 value', - rotateAfter: '24' - }, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(404) - expect(response.json().message).toEqual( - `No default environment found for project with id ${project1.id}` - ) - - await prisma.environment.updateMany({ - where: { - projectId: project1.id, - name: 'Environment 1' - }, - data: { - isDefault: true - } - }) - }) - - it('should not be able to create a duplicate variable in the same environment', async () => { - const response = await app.inject({ - method: 'POST', - url: `/variable/${project1.id}`, - payload: { - environmentId: environment2.id, name: 'Variable 1', - value: 'Variable 1 value', rotateAfter: '24' }, headers: { @@ -376,7 +277,7 @@ describe('Variable Controller Tests', () => { expect(response.statusCode).toBe(409) expect(response.json().message).toEqual( - `Variable already exists: Variable 1 in environment ${environment2.id} in project ${project1.id}` + `Variable already exists: Variable 1 in project ${project1.id}` ) }) @@ -404,7 +305,6 @@ describe('Variable Controller Tests', () => { url: `/variable/non-existing-variable-id`, payload: { name: 'Updated Variable 1', - value: 'Updated Variable 1 value', rotateAfter: '24' }, headers: { @@ -418,13 +318,12 @@ describe('Variable Controller Tests', () => { ) }) - it('should not be able to update a variable with same name in the same environment', async () => { + it('should not be able to update a variable with same name in the same project', async () => { const response = await app.inject({ method: 'PUT', url: `/variable/${variable1.id}`, payload: { name: 'Variable 1', - value: 'Updated Variable 1 value', rotateAfter: '24' }, headers: { @@ -434,7 +333,7 @@ describe('Variable Controller Tests', () => { expect(response.statusCode).toBe(409) expect(response.json().message).toEqual( - `Variable already exists: Variable 1 in environment ${environment2.id}` + `Variable already exists: Variable 1 in project ${project1.id}` ) }) @@ -469,7 +368,12 @@ describe('Variable Controller Tests', () => { method: 'PUT', url: `/variable/${variable1.id}`, payload: { - value: 'Updated Variable 1 value' + entries: [ + { + value: 'Updated Variable 1 value', + environmentId: environment1.id + } + ] }, headers: { 'x-e2e-user-email': user1.email @@ -480,7 +384,8 @@ describe('Variable Controller Tests', () => { const variableVersion = await prisma.variableVersion.findMany({ where: { - variableId: variable1.id + variableId: variable1.id, + environmentId: environment1.id } }) @@ -490,7 +395,7 @@ describe('Variable Controller Tests', () => { it('should have created a VARIABLE_UPDATED event', async () => { // Update a variable await variableService.updateVariable(user1, variable1.id, { - value: 'Updated Variable 1 value' + name: 'Updated Variable 1' }) const response = await fetchEvents( @@ -510,112 +415,10 @@ describe('Variable Controller Tests', () => { expect(event.itemId).toBeDefined() }) - it('should be able to update the environment of a variable', async () => { - const response = await app.inject({ - method: 'PUT', - url: `/variable/${variable1.id}/environment/${environment1.id}`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - expect(response.json().environmentId).toBe(environment1.id) - }) - - it('should not be able to move to a non-existing environment', async () => { - const response = await app.inject({ - method: 'PUT', - url: `/variable/${variable1.id}/environment/non-existing-environment-id`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(404) - }) - - it('should not be able to move to an environment in another project', async () => { - const otherEnvironment = await prisma.environment.findFirst({ - where: { - projectId: project2.id, - name: 'Environment 1' - } - }) - - const response = await app.inject({ - method: 'PUT', - url: `/variable/${variable1.id}/environment/${otherEnvironment.id}`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(400) - expect(response.json().message).toEqual( - `Environment ${otherEnvironment.id} does not belong to the same project ${project1.id}` - ) - }) - - it('should not be able to move the variable to the same environment', async () => { - const response = await app.inject({ - method: 'PUT', - url: `/variable/${variable1.id}/environment/${environment2.id}`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(400) - expect(response.json().message).toEqual( - `Can not update the environment of the variable to the same environment: ${environment2.id}` - ) - }) - - it('should not be able to move the variable if the user has no access to the project', async () => { - const response = await app.inject({ - method: 'PUT', - url: `/variable/${variable1.id}/environment/${workspace2Environment.id}`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(401) - expect(response.json().message).toEqual( - `User ${user1.id} does not have the required authorities` - ) - }) - - it('should not be able to move a variable of the same name to an environment', async () => { - const newVariable = (await variableService.createVariable( - user1, - { - environmentId: environment1.id, - name: 'Variable 1', - value: 'Variable 1 value' - }, - project1.id - )) as Variable - - const response = await app.inject({ - method: 'PUT', - url: `/variable/${newVariable.id}/environment/${environment2.id}`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(409) - expect(response.json().message).toEqual( - `Variable already exists: Variable 1 in environment ${environment2.id} in project ${project1.id}` - ) - }) - it('should not be able to roll back a non-existing variable', async () => { const response = await app.inject({ method: 'PUT', - url: `/variable/non-existing-variable-id/rollback/1`, + url: `/variable/non-existing-variable-id/rollback/1?environmentId=${environment1.id}`, headers: { 'x-e2e-user-email': user1.email } @@ -630,7 +433,7 @@ describe('Variable Controller Tests', () => { it('should not be able to roll back a variable it does not have access to', async () => { const response = await app.inject({ method: 'PUT', - url: `/variable/${variable1.id}/rollback/1`, + url: `/variable/${variable1.id}/rollback/1?environmentId=${environment1.id}`, headers: { 'x-e2e-user-email': user2.email } @@ -645,7 +448,7 @@ describe('Variable Controller Tests', () => { it('should not be able to roll back to a non-existing version', async () => { const response = await app.inject({ method: 'PUT', - url: `/variable/${variable1.id}/rollback/2`, + url: `/variable/${variable1.id}/rollback/2?environmentId=${environment1.id}`, headers: { 'x-e2e-user-email': user1.email } @@ -660,11 +463,21 @@ describe('Variable Controller Tests', () => { it('should be able to roll back a variable', async () => { // Creating a few versions first await variableService.updateVariable(user1, variable1.id, { - value: 'Updated Variable 1 value' + entries: [ + { + value: 'Updated Variable 1 value', + environmentId: environment1.id + } + ] }) await variableService.updateVariable(user1, variable1.id, { - value: 'Updated Variable 1 value 2' + entries: [ + { + value: 'Updated Variable 1 value 2', + environmentId: environment1.id + } + ] }) let versions: VariableVersion[] @@ -679,7 +492,7 @@ describe('Variable Controller Tests', () => { const response = await app.inject({ method: 'PUT', - url: `/variable/${variable1.id}/rollback/1`, + url: `/variable/${variable1.id}/rollback/1?environmentId=${environment1.id}`, headers: { 'x-e2e-user-email': user1.email } @@ -697,70 +510,44 @@ describe('Variable Controller Tests', () => { expect(versions.length).toBe(1) }) - it('should not be able to fetch a non existing variable', async () => { - const response = await app.inject({ - method: 'GET', - url: `/variable/non-existing-variable-id`, - headers: { - 'x-e2e-user-email': user1.email + it('should not be able to roll back if the variable has no versions', async () => { + await prisma.variableVersion.deleteMany({ + where: { + variableId: variable1.id } }) - expect(response.statusCode).toBe(404) - expect(response.json().message).toEqual( - 'Variable with id non-existing-variable-id not found' - ) - }) - - it('should not be able to fetch a variable it does not have access to', async () => { const response = await app.inject({ - method: 'GET', - url: `/variable/${variable1.id}`, + method: 'PUT', + url: `/variable/${variable1.id}/rollback/1?environmentId=${environment1.id}`, headers: { - 'x-e2e-user-email': user2.email + 'x-e2e-user-email': user1.email } }) - expect(response.statusCode).toBe(401) + expect(response.statusCode).toBe(404) expect(response.json().message).toEqual( - `User ${user2.id} does not have the required authorities` + `No versions found for environment: ${environment1.id} for variable: ${variable1.id}` ) }) - it('should be able to fetch a variable', async () => { - const response = await app.inject({ - method: 'GET', - url: `/variable/${variable1.id}`, - headers: { - 'x-e2e-user-email': user1.email - } - }) - - expect(response.statusCode).toBe(200) - expect(response.json().id).toEqual(variable1.id) - - const versions = await response.json().versions - - expect(versions.length).toBe(1) - expect(versions[0].value).toEqual('Variable 1 value') // Variable should be in encrypted form until specified otherwise - }) + it('should not create a secret version entity if value-environmentId is not provided during creation', async () => { + const variable = await variableService.createVariable( + user1, + { + name: 'Var 3', + note: 'Var 3 note' + }, + project1.id + ) - it('should be able to fetch a variable', async () => { - const response = await app.inject({ - method: 'GET', - url: `/variable/${variable1.id}`, - headers: { - 'x-e2e-user-email': user1.email + const variableVersions = await prisma.variableVersion.findMany({ + where: { + variableId: variable.id } }) - expect(response.statusCode).toBe(200) - expect(response.json().id).toEqual(variable1.id) - - const versions = await response.json().versions - - expect(versions.length).toBe(1) - expect(versions[0].value).toEqual('Variable 1 value') + expect(variableVersions.length).toBe(0) }) it('should be able to fetch all variables', async () => { @@ -774,9 +561,15 @@ describe('Variable Controller Tests', () => { expect(response.statusCode).toBe(200) expect(response.json().length).toBe(1) - const envVariable = response.json()[0] - expect(envVariable.environment).toHaveProperty('id') - expect(envVariable.environment).toHaveProperty('name') + + const { variable, values } = response.json()[0] + expect(variable).toBeDefined() + expect(values).toBeDefined() + expect(values.length).toBe(1) + expect(values[0].value).toBe('Variable 1 value') + expect(values[0].environment.id).toBe(environment1.id) + expect(variable.id).toBe(variable1.id) + expect(variable.name).toBe('Variable 1') }) it('should not be able to fetch all variables if the user has no access to the project', async () => { diff --git a/apps/api/src/variable/variable.types.ts b/apps/api/src/variable/variable.types.ts index ebef53af..89eb040c 100644 --- a/apps/api/src/variable/variable.types.ts +++ b/apps/api/src/variable/variable.types.ts @@ -1,4 +1,4 @@ -import { Environment, Project, Variable, VariableVersion } from '@prisma/client' +import { Project, Variable, VariableVersion } from '@prisma/client' export interface VariableWithValue extends Variable { value: string @@ -12,9 +12,5 @@ export interface VariableWithProject extends Variable { project: Project } -export interface VariableWithEnvironment extends Variable { - environment: Environment -} - export type VariableWithProjectAndVersion = VariableWithProject & VariableWithVersion diff --git a/apps/api/src/workspace/controller/workspace.controller.ts b/apps/api/src/workspace/controller/workspace.controller.ts index 86f79b76..4bbc7ca8 100644 --- a/apps/api/src/workspace/controller/workspace.controller.ts +++ b/apps/api/src/workspace/controller/workspace.controller.ts @@ -11,7 +11,6 @@ import { import { WorkspaceService } from '../service/workspace.service' import { CurrentUser } from '../../decorators/user.decorator' import { Authority, User, Workspace, WorkspaceRole } from '@prisma/client' -import { AlphanumericReasonValidationPipe } from '../../common/alphanumeric-reason-pipe' import { CreateWorkspace, WorkspaceMemberDTO @@ -34,10 +33,9 @@ export class WorkspaceController { async update( @CurrentUser() user: User, @Param('workspaceId') workspaceId: Workspace['id'], - @Body() dto: UpdateWorkspace, - @Query('reason', AlphanumericReasonValidationPipe) reason: string + @Body() dto: UpdateWorkspace ) { - return this.workspaceService.updateWorkspace(user, workspaceId, dto, reason) + return this.workspaceService.updateWorkspace(user, workspaceId, dto) } @Put(':workspaceId/transfer-ownership/:userId') diff --git a/apps/api/src/workspace/dto/create.workspace/create.workspace.ts b/apps/api/src/workspace/dto/create.workspace/create.workspace.ts index 627a1346..01ad5bfc 100644 --- a/apps/api/src/workspace/dto/create.workspace/create.workspace.ts +++ b/apps/api/src/workspace/dto/create.workspace/create.workspace.ts @@ -1,5 +1,5 @@ import { WorkspaceRole } from '@prisma/client' -import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator' +import { IsNotEmpty, IsOptional, IsString } from 'class-validator' export class CreateWorkspace { @IsString() @@ -9,10 +9,6 @@ export class CreateWorkspace { @IsString() @IsOptional() description?: string - - @IsBoolean() - @IsOptional() - approvalEnabled?: boolean } export interface WorkspaceMemberDTO { diff --git a/apps/api/src/workspace/service/workspace.service.ts b/apps/api/src/workspace/service/workspace.service.ts index 166b5890..2a6b9319 100644 --- a/apps/api/src/workspace/service/workspace.service.ts +++ b/apps/api/src/workspace/service/workspace.service.ts @@ -9,8 +9,6 @@ import { } from '@nestjs/common' import { PrismaService } from '../../prisma/prisma.service' import { - ApprovalAction, - ApprovalItemType, Authority, EventSource, EventType, @@ -31,9 +29,6 @@ import { JwtService } from '@nestjs/jwt' import { UpdateWorkspace } from '../dto/update.workspace/update.workspace' import { v4 } from 'uuid' import createEvent from '../../common/create-event' -import { UpdateWorkspaceMetadata } from '../../approval/approval.types' -import workspaceApprovalEnabled from '../../common/workspace-approval-enabled' -import createApproval from '../../common/create-approval' import createWorkspace from '../../common/create-workspace' import { AuthorityCheckerService } from '../../common/authority-checker.service' @@ -59,8 +54,7 @@ export class WorkspaceService { async updateWorkspace( user: User, workspaceId: Workspace['id'], - dto: UpdateWorkspace, - reason?: string + dto: UpdateWorkspace ) { // Fetch the workspace const workspace = @@ -80,24 +74,39 @@ export class WorkspaceService { throw new ConflictException('Workspace already exists') } - if (await workspaceApprovalEnabled(workspaceId, this.prisma)) { - // Create the update approval - return await createApproval( - { - action: ApprovalAction.UPDATE, - itemType: ApprovalItemType.WORKSPACE, - itemId: workspaceId, - reason, - user, - workspaceId, - metadata: dto as UpdateWorkspaceMetadata + const updatedWorkspace = await this.prisma.workspace.update({ + where: { + id: workspaceId + }, + data: { + name: dto.name, + description: dto.description, + lastUpdatedBy: { + connect: { + id: user.id + } + } + } + }) + this.log.debug(`Updated workspace ${workspace.name} (${workspace.id})`) + + await createEvent( + { + triggeredBy: user, + entity: workspace, + type: EventType.WORKSPACE_UPDATED, + source: EventSource.WORKSPACE, + title: `Workspace updated`, + metadata: { + workspaceId: workspace.id, + name: workspace.name }, - this.prisma - ) - } else { - // Update the workspace - return await this.update(workspaceId, dto, user) - } + workspaceId: workspace.id + }, + this.prisma + ) + + return updatedWorkspace } async transferOwnership( @@ -796,31 +805,30 @@ export class WorkspaceService { environments: { select: { name: true, - description: true, - isDefault: true, - secrets: { + description: true + } + }, + secrets: { + select: { + name: true, + rotateAt: true, + note: true, + versions: { select: { - name: true, - rotateAt: true, - note: true, - versions: { - select: { - value: true, - version: true - } - } + value: true, + version: true } - }, - variables: { + } + } + }, + variables: { + select: { + name: true, + note: true, + versions: { select: { - name: true, - note: true, - versions: { - select: { - value: true, - version: true - } - } + value: true, + version: true } } } @@ -1035,45 +1043,4 @@ export class WorkspaceService { `User ${userId} is not invited to workspace ${workspaceId}` ) } - - async update( - workspaceId: Workspace['id'], - data: UpdateWorkspace | UpdateWorkspaceMetadata, - user: User - ) { - const workspace = await this.prisma.workspace.update({ - where: { - id: workspaceId - }, - data: { - name: data.name, - description: data.description, - approvalEnabled: data.approvalEnabled, - lastUpdatedBy: { - connect: { - id: user.id - } - } - } - }) - this.log.debug(`Updated workspace ${workspace.name} (${workspace.id})`) - - await createEvent( - { - triggeredBy: user, - entity: workspace, - type: EventType.WORKSPACE_UPDATED, - source: EventSource.WORKSPACE, - title: `Workspace updated`, - metadata: { - workspaceId: workspace.id, - name: workspace.name - }, - workspaceId: workspace.id - }, - this.prisma - ) - - return workspace - } } diff --git a/apps/api/src/workspace/workspace.e2e.spec.ts b/apps/api/src/workspace/workspace.e2e.spec.ts index a802dfb1..e40c546c 100644 --- a/apps/api/src/workspace/workspace.e2e.spec.ts +++ b/apps/api/src/workspace/workspace.e2e.spec.ts @@ -162,7 +162,6 @@ describe('Workspace Controller Tests', () => { expect(workspace1.description).toBe('Workspace 1 description') expect(workspace1.ownerId).toBe(user1.id) expect(workspace1.isFreeTier).toBe(true) - expect(workspace1.approvalEnabled).toBe(false) expect(workspace1.isDefault).toBe(false) }) @@ -212,7 +211,6 @@ describe('Workspace Controller Tests', () => { expect(workspace2.description).toBe('Workspace 1 description') expect(workspace2.ownerId).toBe(user2.id) expect(workspace2.isFreeTier).toBe(true) - expect(workspace2.approvalEnabled).toBe(false) expect(workspace2.isDefault).toBe(false) }) diff --git a/apps/platform/src/types/index.ts b/apps/platform/src/types/index.ts index c7c059f4..4f5bcc2f 100644 --- a/apps/platform/src/types/index.ts +++ b/apps/platform/src/types/index.ts @@ -43,7 +43,7 @@ export const zProject = z.object({ export const zEnvironment = z.object({ name: z.string(), description: z.string().nullable(), - isDefault: z.boolean().optional(), + isDefault: z.boolean().optional() }) export const zNewProject = z.object({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f438a7c..75b2e25b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,22 +10,22 @@ importers: dependencies: '@million/lint': specifier: ^0.0.73 - version: 0.0.73 + version: 0.0.73(@types/react-dom@18.3.0)(@types/react@18.3.3) '@semantic-release/changelog': specifier: ^6.0.3 - version: 6.0.3(semantic-release@23.1.1) + version: 6.0.3(semantic-release@23.1.1(typescript@5.4.5)) '@semantic-release/commit-analyzer': specifier: ^12.0.0 - version: 12.0.0(semantic-release@23.1.1) + version: 12.0.0(semantic-release@23.1.1(typescript@5.4.5)) '@semantic-release/git': specifier: ^10.0.1 - version: 10.0.1(semantic-release@23.1.1) + version: 10.0.1(semantic-release@23.1.1(typescript@5.4.5)) '@semantic-release/github': specifier: ^10.0.3 - version: 10.0.5(semantic-release@23.1.1) + version: 10.0.5(semantic-release@23.1.1(typescript@5.4.5)) '@semantic-release/release-notes-generator': specifier: ^13.0.0 - version: 13.0.0(semantic-release@23.1.1) + version: 13.0.0(semantic-release@23.1.1(typescript@5.4.5)) '@sentry/node': specifier: ^7.102.0 version: 7.116.0 @@ -43,7 +43,7 @@ importers: version: 0.33.4 ts-node: specifier: ^10.9.2 - version: 10.9.2(@types/node@17.0.45)(typescript@5.4.5) + version: 10.9.2(@types/node@20.12.12)(typescript@5.4.5) zod: specifier: ^3.23.6 version: 3.23.8 @@ -80,34 +80,34 @@ importers: version: 10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/config': specifier: ^3.2.0 - version: 3.2.2(@nestjs/common@10.3.8)(rxjs@7.8.1) + version: 3.2.2(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(rxjs@7.8.1) '@nestjs/core': specifier: ^10.0.0 - version: 10.3.8(@nestjs/common@10.3.8)(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1) + version: 10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nestjs/jwt': specifier: ^10.2.0 - version: 10.2.0(@nestjs/common@10.3.8) + version: 10.2.0(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)) '@nestjs/passport': specifier: ^10.0.3 - version: 10.0.3(@nestjs/common@10.3.8)(passport@0.7.0) + version: 10.0.3(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(passport@0.7.0) '@nestjs/platform-express': specifier: ^10.0.0 - version: 10.3.8(@nestjs/common@10.3.8)(@nestjs/core@10.3.8) + version: 10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8) '@nestjs/platform-fastify': specifier: ^10.3.3 - version: 10.3.8(@nestjs/common@10.3.8)(@nestjs/core@10.3.8) + version: 10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1)) '@nestjs/platform-socket.io': specifier: ^10.3.7 - version: 10.3.8(@nestjs/common@10.3.8)(@nestjs/websockets@10.3.8)(rxjs@7.8.1) + version: 10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.3.8)(rxjs@7.8.1) '@nestjs/schedule': specifier: ^4.0.1 - version: 4.0.2(@nestjs/common@10.3.8)(@nestjs/core@10.3.8) + version: 4.0.2(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1)) '@nestjs/swagger': specifier: ^7.3.0 - version: 7.3.1(@nestjs/common@10.3.8)(@nestjs/core@10.3.8)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + version: 7.3.1(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) '@nestjs/websockets': specifier: ^10.3.7 - version: 10.3.8(@nestjs/common@10.3.8)(@nestjs/core@10.3.8)(@nestjs/platform-socket.io@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1) + version: 10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8)(@nestjs/platform-socket.io@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@socket.io/redis-adapter': specifier: ^8.3.0 version: 8.3.0(socket.io-adapter@2.5.4) @@ -150,9 +150,6 @@ importers: redis: specifier: ^4.6.13 version: 4.6.14 - reflect-metadata: - specifier: ^0.2.0 - version: 0.2.2 rxjs: specifier: ^7.8.1 version: 7.8.1 @@ -168,10 +165,10 @@ importers: version: 10.3.2 '@nestjs/schematics': specifier: ^10.0.0 - version: 10.1.1(typescript@5.4.5) + version: 10.1.1(chokidar@3.6.0)(typescript@5.4.5) '@nestjs/testing': specifier: ^10.0.0 - version: 10.3.8(@nestjs/common@10.3.8)(@nestjs/core@10.3.8)(@nestjs/platform-express@10.3.8) + version: 10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8)) '@prisma/client': specifier: ^5.13.0 version: 5.14.0(prisma@5.13.0) @@ -201,7 +198,7 @@ importers: version: 9.0.8 '@typescript-eslint/eslint-plugin': specifier: ^6.0.0 - version: 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.5) + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5) '@typescript-eslint/parser': specifier: ^6.0.0 version: 6.21.0(eslint@8.57.0)(typescript@5.4.5) @@ -222,19 +219,22 @@ importers: version: 11.1.0(eslint@8.57.0) eslint-plugin-prettier: specifier: ^5.0.0 - version: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5) + version: 5.1.3(@types/eslint@8.56.10)(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.2.5) jest: specifier: ^29.5.0 - version: 29.7.0(@types/node@20.12.12)(ts-node@10.9.2) + version: 29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)) jest-mock-extended: specifier: ^3.0.5 - version: 3.0.7(jest@29.7.0)(typescript@5.4.5) + version: 3.0.7(jest@29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)))(typescript@5.4.5) prettier: specifier: ^3.0.0 version: 3.2.5 prisma: specifier: 5.13.0 version: 5.13.0 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 source-map-support: specifier: ^0.5.21 version: 0.5.21 @@ -243,10 +243,10 @@ importers: version: 6.3.4 ts-jest: specifier: ^29.1.0 - version: 29.1.3(@babel/core@7.24.6)(jest@29.7.0)(typescript@5.4.5) + version: 29.1.3(@babel/core@7.24.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.6))(jest@29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)))(typescript@5.4.5) ts-loader: specifier: ^9.4.3 - version: 9.5.1(typescript@5.4.5)(webpack@5.91.0) + version: 9.5.1(typescript@5.4.5)(webpack@5.90.1) tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 @@ -258,52 +258,52 @@ importers: dependencies: '@radix-ui/react-avatar': specifier: ^1.0.4 - version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-checkbox': specifier: ^1.0.4 - version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-context-menu': specifier: ^2.1.5 - version: 2.1.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + version: 2.1.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-dialog': specifier: ^1.0.5 - version: 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + version: 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-direction': specifier: ^1.0.1 version: 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-dropdown-menu': specifier: ^2.0.6 - version: 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + version: 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-icons': specifier: ^1.3.0 version: 1.3.0(react@18.3.1) '@radix-ui/react-label': specifier: ^2.0.2 - version: 2.0.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + version: 2.0.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-menubar': specifier: ^1.0.4 - version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-popover': specifier: ^1.0.7 - version: 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + version: 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-scroll-area': specifier: ^1.0.5 - version: 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + version: 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-separator': specifier: ^1.0.3 - version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': specifier: ^1.0.2 version: 1.0.2(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-switch': specifier: ^1.0.3 - version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tanstack/react-table': specifier: ^8.16.0 - version: 8.17.3(react-dom@18.3.1)(react@18.3.1) + version: 8.17.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) avvvatars-react: specifier: ^0.4.2 - version: 0.4.2(csstype@3.1.3)(react-dom@18.3.1)(react@18.3.1) + version: 0.4.2(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) class-variance-authority: specifier: ^0.7.0 version: 0.7.0 @@ -312,16 +312,16 @@ importers: version: 2.1.1 cmdk: specifier: ^1.0.0 - version: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + version: 1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) framer-motion: specifier: ^11.1.7 - version: 11.2.6(react-dom@18.3.1)(react@18.3.1) + version: 11.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) geist: specifier: ^1.2.2 - version: 1.3.0(next@13.5.6) + version: 1.3.0(next@13.5.6(@babel/core@7.24.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) input-otp: specifier: ^1.2.4 - version: 1.2.4(react-dom@18.3.1)(react@18.3.1) + version: 1.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) jotai: specifier: ^2.8.0 version: 2.8.2(@types/react@18.3.3)(react@18.3.1) @@ -333,10 +333,10 @@ importers: version: 0.340.0(react@18.3.1) next: specifier: ^13.5.6 - version: 13.5.6(@babel/core@7.24.6)(react-dom@18.3.1)(react@18.3.1) + version: 13.5.6(@babel/core@7.24.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes: specifier: ^0.3.0 - version: 0.3.0(react-dom@18.3.1)(react@18.3.1) + version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.3.1 version: 18.3.1 @@ -345,13 +345,13 @@ importers: version: 18.3.1(react@18.3.1) sonner: specifier: ^1.4.41 - version: 1.4.41(react-dom@18.3.1)(react@18.3.1) + version: 1.4.41(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwind-merge: specifier: ^2.2.2 version: 2.3.0 tailwindcss-animate: specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.3) + version: 1.0.7(tailwindcss@3.4.3(ts-node@10.9.2(@types/node@17.0.45)(typescript@4.9.5))) zod: specifier: ^3.23.8 version: 3.23.8 @@ -364,7 +364,7 @@ importers: version: 8.1.0(typescript@4.9.5) '@tailwindcss/forms': specifier: ^0.5.7 - version: 0.5.7(tailwindcss@3.4.3) + version: 0.5.7(tailwindcss@3.4.3(ts-node@10.9.2(@types/node@17.0.45)(typescript@4.9.5))) '@types/js-cookie': specifier: ^3.0.6 version: 3.0.6 @@ -388,7 +388,7 @@ importers: version: 8.4.38 tailwindcss: specifier: ^3.3.3 - version: 3.4.3(ts-node@10.9.2) + version: 3.4.3(ts-node@10.9.2(@types/node@17.0.45)(typescript@4.9.5)) tsconfig: specifier: workspace:* version: link:../../packages/tsconfig @@ -406,13 +406,13 @@ importers: version: 3.0.1(@types/react@18.3.3)(react@18.3.1) '@next/mdx': specifier: ^14.2.3 - version: 14.2.3(@mdx-js/loader@3.0.1)(@mdx-js/react@3.0.1) + version: 14.2.3(@mdx-js/loader@3.0.1(webpack@5.91.0))(@mdx-js/react@3.0.1(@types/react@18.3.3)(react@18.3.1)) '@tsparticles/engine': specifier: ^3.3.0 version: 3.4.0 '@tsparticles/react': specifier: ^3.0.0 - version: 3.0.0(@tsparticles/engine@3.4.0)(react-dom@18.3.1)(react@18.3.1) + version: 3.0.0(@tsparticles/engine@3.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tsparticles/slim': specifier: ^3.3.0 version: 3.4.0 @@ -424,13 +424,13 @@ importers: version: 2.1.1 framer-motion: specifier: ^11.0.6 - version: 11.2.6(react-dom@18.3.1)(react@18.3.1) + version: 11.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) geist: specifier: ^1.2.2 - version: 1.3.0(next@13.5.6) + version: 1.3.0(next@13.5.6(@babel/core@7.24.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) next: specifier: ^13.5.6 - version: 13.5.6(@babel/core@7.24.6)(react-dom@18.3.1)(react@18.3.1) + version: 13.5.6(@babel/core@7.24.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.2.0 version: 18.3.1 @@ -442,7 +442,7 @@ importers: version: 0.33.4 sonner: specifier: ^1.4.41 - version: 1.4.41(react-dom@18.3.1)(react@18.3.1) + version: 1.4.41(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwind-merge: specifier: ^2.2.1 version: 2.3.0 @@ -455,7 +455,7 @@ importers: version: 8.1.0(typescript@4.9.5) '@tailwindcss/forms': specifier: ^0.5.7 - version: 0.5.7(tailwindcss@3.4.3) + version: 0.5.7(tailwindcss@3.4.3(ts-node@10.9.2(@types/node@17.0.45)(typescript@4.9.5))) '@types/jest': specifier: ^29.5.2 version: 29.5.12 @@ -479,7 +479,7 @@ importers: version: 8.4.38 tailwindcss: specifier: ^3.3.3 - version: 3.4.3(ts-node@10.9.2) + version: 3.4.3(ts-node@10.9.2(@types/node@17.0.45)(typescript@4.9.5)) tsconfig: specifier: workspace:* version: link:../../packages/tsconfig @@ -491,7 +491,7 @@ importers: devDependencies: '@vercel/style-guide': specifier: ^5.0.0 - version: 5.2.0(eslint@8.57.0)(prettier@3.2.5)(typescript@4.9.5) + version: 5.2.0(@next/eslint-plugin-next@13.5.6)(eslint@8.57.0)(jest@29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)))(prettier@3.2.5)(typescript@4.9.5) eslint-config-turbo: specifier: ^1.10.12 version: 1.13.3(eslint@8.57.0) @@ -8254,11 +8254,12 @@ snapshots: dependencies: ajv: 8.12.0 ajv-formats: 2.1.1(ajv@8.12.0) - chokidar: 3.6.0 jsonc-parser: 3.2.0 picomatch: 3.0.1 rxjs: 7.8.1 source-map: 0.7.4 + optionalDependencies: + chokidar: 3.6.0 '@angular-devkit/schematics-cli@17.1.2(chokidar@3.6.0)': dependencies: @@ -9233,7 +9234,7 @@ snapshots: '@floating-ui/core': 1.6.2 '@floating-ui/utils': 0.2.2 - '@floating-ui/react-dom@2.1.0(react-dom@18.3.1)(react@18.3.1)': + '@floating-ui/react-dom@2.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@floating-ui/dom': 1.6.5 react: 18.3.1 @@ -9356,7 +9357,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2)': + '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -9370,7 +9371,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.12.12)(ts-node@10.9.2) + jest-config: 29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -9609,25 +9610,25 @@ snapshots: - debug - supports-color - '@million/lint@0.0.73': + '@million/lint@0.0.73(@types/react-dom@18.3.0)(@types/react@18.3.3)': dependencies: '@babel/core': 7.24.6 '@babel/helper-module-imports': 7.24.6 '@babel/types': 7.24.6 - '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-tooltip': 1.0.7(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tooltip': 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@rollup/pluginutils': 5.1.0 - cmdk: 0.2.1(react-dom@18.3.1)(react@18.3.1) + cmdk: 0.2.1(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) esbuild: 0.20.2 escalade: 3.1.2 isomorphic-fetch: 3.0.0 posthog-node: 3.6.3 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-draggable: 4.4.6(react-dom@18.3.1)(react@18.3.1) + react-draggable: 4.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-reconciler: 0.29.2(react@18.3.1) unplugin: 1.10.1 - zustand: 4.5.2(react@18.3.1) + zustand: 4.5.2(@types/react@18.3.3)(react@18.3.1) transitivePeerDependencies: - '@types/react' - '@types/react-dom' @@ -9668,15 +9669,16 @@ snapshots: '@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1)': dependencies: - class-transformer: 0.5.1 - class-validator: 0.14.1 iterare: 1.2.1 reflect-metadata: 0.2.2 rxjs: 7.8.1 tslib: 2.6.2 uid: 2.0.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.1 - '@nestjs/config@3.2.2(@nestjs/common@10.3.8)(rxjs@7.8.1)': + '@nestjs/config@3.2.2(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(rxjs@7.8.1)': dependencies: '@nestjs/common': 10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) dotenv: 16.4.5 @@ -9685,11 +9687,9 @@ snapshots: rxjs: 7.8.1 uuid: 9.0.1 - '@nestjs/core@10.3.8(@nestjs/common@10.3.8)(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1)': + '@nestjs/core@10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1)': dependencies: '@nestjs/common': 10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/platform-express': 10.3.8(@nestjs/common@10.3.8)(@nestjs/core@10.3.8) - '@nestjs/websockets': 10.3.8(@nestjs/common@10.3.8)(@nestjs/core@10.3.8)(@nestjs/platform-socket.io@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@nuxtjs/opencollective': 0.3.2 fast-safe-stringify: 2.1.1 iterare: 1.2.1 @@ -9698,31 +9698,35 @@ snapshots: rxjs: 7.8.1 tslib: 2.6.2 uid: 2.0.2 + optionalDependencies: + '@nestjs/platform-express': 10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8) + '@nestjs/websockets': 10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8)(@nestjs/platform-socket.io@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1) transitivePeerDependencies: - encoding - '@nestjs/jwt@10.2.0(@nestjs/common@10.3.8)': + '@nestjs/jwt@10.2.0(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))': dependencies: '@nestjs/common': 10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) '@types/jsonwebtoken': 9.0.5 jsonwebtoken: 9.0.2 - '@nestjs/mapped-types@2.0.5(@nestjs/common@10.3.8)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)': + '@nestjs/mapped-types@2.0.5(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)': dependencies: '@nestjs/common': 10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) + reflect-metadata: 0.2.2 + optionalDependencies: class-transformer: 0.5.1 class-validator: 0.14.1 - reflect-metadata: 0.2.2 - '@nestjs/passport@10.0.3(@nestjs/common@10.3.8)(passport@0.7.0)': + '@nestjs/passport@10.0.3(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(passport@0.7.0)': dependencies: '@nestjs/common': 10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) passport: 0.7.0 - '@nestjs/platform-express@10.3.8(@nestjs/common@10.3.8)(@nestjs/core@10.3.8)': + '@nestjs/platform-express@10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8)': dependencies: '@nestjs/common': 10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/core': 10.3.8(@nestjs/common@10.3.8)(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1) body-parser: 1.20.2 cors: 2.8.5 express: 4.19.2 @@ -9731,22 +9735,22 @@ snapshots: transitivePeerDependencies: - supports-color - '@nestjs/platform-fastify@10.3.8(@nestjs/common@10.3.8)(@nestjs/core@10.3.8)': + '@nestjs/platform-fastify@10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1))': dependencies: '@fastify/cors': 9.0.1 '@fastify/formbody': 7.4.0 '@fastify/middie': 8.3.0 '@nestjs/common': 10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/core': 10.3.8(@nestjs/common@10.3.8)(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1) fastify: 4.26.2 light-my-request: 5.13.0 path-to-regexp: 3.2.0 tslib: 2.6.2 - '@nestjs/platform-socket.io@10.3.8(@nestjs/common@10.3.8)(@nestjs/websockets@10.3.8)(rxjs@7.8.1)': + '@nestjs/platform-socket.io@10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.3.8)(rxjs@7.8.1)': dependencies: '@nestjs/common': 10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/websockets': 10.3.8(@nestjs/common@10.3.8)(@nestjs/core@10.3.8)(@nestjs/platform-socket.io@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/websockets': 10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8)(@nestjs/platform-socket.io@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1) rxjs: 7.8.1 socket.io: 4.7.5 tslib: 2.6.2 @@ -9755,10 +9759,10 @@ snapshots: - supports-color - utf-8-validate - '@nestjs/schedule@4.0.2(@nestjs/common@10.3.8)(@nestjs/core@10.3.8)': + '@nestjs/schedule@4.0.2(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1))': dependencies: '@nestjs/common': 10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/core': 10.3.8(@nestjs/common@10.3.8)(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/core': 10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1) cron: 3.1.7 uuid: 9.0.1 @@ -9773,7 +9777,7 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/schematics@10.1.1(typescript@5.4.5)': + '@nestjs/schematics@10.1.1(chokidar@3.6.0)(typescript@5.4.5)': dependencies: '@angular-devkit/core': 17.1.2(chokidar@3.6.0) '@angular-devkit/schematics': 17.1.2(chokidar@3.6.0) @@ -9784,37 +9788,40 @@ snapshots: transitivePeerDependencies: - chokidar - '@nestjs/swagger@7.3.1(@nestjs/common@10.3.8)(@nestjs/core@10.3.8)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)': + '@nestjs/swagger@7.3.1(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)': dependencies: '@microsoft/tsdoc': 0.14.2 '@nestjs/common': 10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/core': 10.3.8(@nestjs/common@10.3.8)(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/mapped-types': 2.0.5(@nestjs/common@10.3.8)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) - class-transformer: 0.5.1 - class-validator: 0.14.1 + '@nestjs/core': 10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1) + '@nestjs/mapped-types': 2.0.5(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) js-yaml: 4.1.0 lodash: 4.17.21 path-to-regexp: 3.2.0 reflect-metadata: 0.2.2 swagger-ui-dist: 5.11.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.1 - '@nestjs/testing@10.3.8(@nestjs/common@10.3.8)(@nestjs/core@10.3.8)(@nestjs/platform-express@10.3.8)': + '@nestjs/testing@10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8))': dependencies: '@nestjs/common': 10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/core': 10.3.8(@nestjs/common@10.3.8)(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/platform-express': 10.3.8(@nestjs/common@10.3.8)(@nestjs/core@10.3.8) + '@nestjs/core': 10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1) tslib: 2.6.2 + optionalDependencies: + '@nestjs/platform-express': 10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8) - '@nestjs/websockets@10.3.8(@nestjs/common@10.3.8)(@nestjs/core@10.3.8)(@nestjs/platform-socket.io@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1)': + '@nestjs/websockets@10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.3.8)(@nestjs/platform-socket.io@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1)': dependencies: '@nestjs/common': 10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/core': 10.3.8(@nestjs/common@10.3.8)(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1) - '@nestjs/platform-socket.io': 10.3.8(@nestjs/common@10.3.8)(@nestjs/websockets@10.3.8)(rxjs@7.8.1) + '@nestjs/core': 10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.3.8)(@nestjs/websockets@10.3.8)(reflect-metadata@0.2.2)(rxjs@7.8.1) iterare: 1.2.1 object-hash: 3.0.0 reflect-metadata: 0.2.2 rxjs: 7.8.1 tslib: 2.6.2 + optionalDependencies: + '@nestjs/platform-socket.io': 10.3.8(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/websockets@10.3.8)(rxjs@7.8.1) '@next/env@13.5.6': {} @@ -9822,11 +9829,12 @@ snapshots: dependencies: glob: 7.1.7 - '@next/mdx@14.2.3(@mdx-js/loader@3.0.1)(@mdx-js/react@3.0.1)': + '@next/mdx@14.2.3(@mdx-js/loader@3.0.1(webpack@5.91.0))(@mdx-js/react@3.0.1(@types/react@18.3.3)(react@18.3.1))': dependencies: + source-map: 0.7.4 + optionalDependencies: '@mdx-js/loader': 3.0.1(webpack@5.91.0) '@mdx-js/react': 3.0.1(@types/react@18.3.3)(react@18.3.1) - source-map: 0.7.4 '@next/swc-darwin-arm64@13.5.6': optional: true @@ -9955,7 +9963,7 @@ snapshots: config-chain: 1.1.13 '@prisma/client@5.14.0(prisma@5.13.0)': - dependencies: + optionalDependencies: prisma: 5.13.0 '@prisma/debug@5.13.0': {} @@ -9991,54 +9999,58 @@ snapshots: dependencies: '@babel/runtime': 7.24.6 - '@radix-ui/react-arrow@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-arrow@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 - '@radix-ui/react-avatar@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-avatar@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 - '@radix-ui/react-checkbox@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-checkbox@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-use-previous': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-use-size': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 - '@radix-ui/react-collection@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-collection@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1) - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 '@radix-ui/react-compose-refs@1.0.0(react@18.3.1)': dependencies: @@ -10048,22 +10060,24 @@ snapshots: '@radix-ui/react-compose-refs@1.0.1(@types/react@18.3.3)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 - '@types/react': 18.3.3 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 - '@radix-ui/react-context-menu@2.1.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-context-menu@2.1.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-menu': 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-menu': 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 '@radix-ui/react-context@1.0.0(react@18.3.1)': dependencies: @@ -10073,97 +10087,102 @@ snapshots: '@radix-ui/react-context@1.0.1(@types/react@18.3.3)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 - '@types/react': 18.3.3 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 - '@radix-ui/react-dialog@1.0.0(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-dialog@1.0.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/primitive': 1.0.0 '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) '@radix-ui/react-context': 1.0.0(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.0.0(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-focus-guards': 1.0.0(react@18.3.1) - '@radix-ui/react-focus-scope': 1.0.0(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.0.0(react@18.3.1) - '@radix-ui/react-portal': 1.0.0(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-presence': 1.0.0(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.0(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-portal': 1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.0.0(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.0.0(react@18.3.1) aria-hidden: 1.2.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - react-remove-scroll: 2.5.4(react@18.3.1) + react-remove-scroll: 2.5.4(@types/react@18.3.3)(react@18.3.1) transitivePeerDependencies: - '@types/react' - '@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-dialog@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 aria-hidden: 1.2.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-remove-scroll: 2.5.5(@types/react@18.3.3)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 '@radix-ui/react-direction@1.0.1(@types/react@18.3.3)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 - '@types/react': 18.3.3 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 - '@radix-ui/react-dismissable-layer@1.0.0(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-dismissable-layer@1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/primitive': 1.0.0 '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) - '@radix-ui/react-primitive': 1.0.0(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.0.0(react@18.3.1) '@radix-ui/react-use-escape-keydown': 1.0.0(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-dismissable-layer@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-use-escape-keydown': 1.0.3(@types/react@18.3.3)(react@18.3.1) - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 - '@radix-ui/react-dropdown-menu@2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-dropdown-menu@2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-menu': 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-menu': 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 '@radix-ui/react-focus-guards@1.0.0(react@18.3.1)': dependencies: @@ -10173,28 +10192,30 @@ snapshots: '@radix-ui/react-focus-guards@1.0.1(@types/react@18.3.3)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 - '@types/react': 18.3.3 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 - '@radix-ui/react-focus-scope@1.0.0(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-focus-scope@1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) - '@radix-ui/react-primitive': 1.0.0(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.0.0(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 '@radix-ui/react-icons@1.3.0(react@18.3.1)': dependencies: @@ -10210,120 +10231,127 @@ snapshots: dependencies: '@babel/runtime': 7.24.6 '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@types/react': 18.3.3 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 - '@radix-ui/react-label@2.0.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-label@2.0.2(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 - '@radix-ui/react-menu@2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-menu@2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-direction': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 aria-hidden: 1.2.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-remove-scroll: 2.5.5(@types/react@18.3.3)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 - '@radix-ui/react-menubar@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-menubar@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-direction': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-menu': 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-menu': 2.0.6(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 - '@radix-ui/react-popover@1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-popover@1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 aria-hidden: 1.2.4 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) react-remove-scroll: 2.5.5(@types/react@18.3.3)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 - '@radix-ui/react-popper@1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-popper@1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 - '@floating-ui/react-dom': 2.1.0(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@floating-ui/react-dom': 2.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-use-rect': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-use-size': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/rect': 1.0.1 - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 - '@radix-ui/react-portal@1.0.0(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-portal@1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 - '@radix-ui/react-primitive': 1.0.0(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@radix-ui/react-portal@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-portal@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 - '@radix-ui/react-presence@1.0.0(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-presence@1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/react-compose-refs': 1.0.0(react@18.3.1) @@ -10331,50 +10359,53 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@radix-ui/react-presence@1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-presence@1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 - '@radix-ui/react-primitive@1.0.0(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-primitive@1.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/react-slot': 1.0.0(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-primitive@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1) - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 - '@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-roving-focus@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/primitive': 1.0.1 - '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-direction': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 - '@radix-ui/react-scroll-area@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-scroll-area@1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/number': 1.0.1 @@ -10382,23 +10413,25 @@ snapshots: '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-direction': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 - '@radix-ui/react-separator@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-separator@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 '@radix-ui/react-slot@1.0.0(react@18.3.1)': dependencies: @@ -10410,41 +10443,46 @@ snapshots: dependencies: '@babel/runtime': 7.24.6 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@types/react': 18.3.3 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 - '@radix-ui/react-switch@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-switch@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-use-previous': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-use-size': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@types/react': 18.3.3 - '@types/react-dom': 18.3.0 react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 - '@radix-ui/react-tooltip@1.0.7(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-tooltip@1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@radix-ui/react-slot': 1.0.2(@types/react@18.3.3)(react@18.3.1) '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@radix-ui/react-visually-hidden': 1.0.3(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 '@radix-ui/react-use-callback-ref@1.0.0(react@18.3.1)': dependencies: @@ -10454,8 +10492,9 @@ snapshots: '@radix-ui/react-use-callback-ref@1.0.1(@types/react@18.3.3)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 - '@types/react': 18.3.3 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 '@radix-ui/react-use-controllable-state@1.0.0(react@18.3.1)': dependencies: @@ -10467,8 +10506,9 @@ snapshots: dependencies: '@babel/runtime': 7.24.6 '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@types/react': 18.3.3 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 '@radix-ui/react-use-escape-keydown@1.0.0(react@18.3.1)': dependencies: @@ -10480,8 +10520,9 @@ snapshots: dependencies: '@babel/runtime': 7.24.6 '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@types/react': 18.3.3 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 '@radix-ui/react-use-layout-effect@1.0.0(react@18.3.1)': dependencies: @@ -10491,35 +10532,42 @@ snapshots: '@radix-ui/react-use-layout-effect@1.0.1(@types/react@18.3.3)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 - '@types/react': 18.3.3 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 '@radix-ui/react-use-previous@1.0.1(@types/react@18.3.3)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 - '@types/react': 18.3.3 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 '@radix-ui/react-use-rect@1.0.1(@types/react@18.3.3)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/rect': 1.0.1 - '@types/react': 18.3.3 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 '@radix-ui/react-use-size@1.0.1(@types/react@18.3.3)(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.3.3)(react@18.3.1) - '@types/react': 18.3.3 react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.3 - '@radix-ui/react-visually-hidden@1.0.3(react-dom@18.3.1)(react@18.3.1)': + '@radix-ui/react-visually-hidden@1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@babel/runtime': 7.24.6 - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + '@types/react-dom': 18.3.0 '@radix-ui/rect@1.0.1': dependencies: @@ -10561,7 +10609,7 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@semantic-release/changelog@6.0.3(semantic-release@23.1.1)': + '@semantic-release/changelog@6.0.3(semantic-release@23.1.1(typescript@5.4.5))': dependencies: '@semantic-release/error': 3.0.0 aggregate-error: 3.1.0 @@ -10569,7 +10617,7 @@ snapshots: lodash: 4.17.21 semantic-release: 23.1.1(typescript@5.4.5) - '@semantic-release/commit-analyzer@12.0.0(semantic-release@23.1.1)': + '@semantic-release/commit-analyzer@12.0.0(semantic-release@23.1.1(typescript@5.4.5))': dependencies: conventional-changelog-angular: 7.0.0 conventional-commits-filter: 4.0.0 @@ -10586,7 +10634,7 @@ snapshots: '@semantic-release/error@4.0.0': {} - '@semantic-release/git@10.0.1(semantic-release@23.1.1)': + '@semantic-release/git@10.0.1(semantic-release@23.1.1(typescript@5.4.5))': dependencies: '@semantic-release/error': 3.0.0 aggregate-error: 3.1.0 @@ -10600,7 +10648,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@semantic-release/github@10.0.5(semantic-release@23.1.1)': + '@semantic-release/github@10.0.5(semantic-release@23.1.1(typescript@5.4.5))': dependencies: '@octokit/core': 6.1.2 '@octokit/plugin-paginate-rest': 11.3.0(@octokit/core@6.1.2) @@ -10622,7 +10670,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@semantic-release/npm@12.0.1(semantic-release@23.1.1)': + '@semantic-release/npm@12.0.1(semantic-release@23.1.1(typescript@5.4.5))': dependencies: '@semantic-release/error': 4.0.0 aggregate-error: 5.0.0 @@ -10639,7 +10687,7 @@ snapshots: semver: 7.6.2 tempy: 3.1.0 - '@semantic-release/release-notes-generator@13.0.0(semantic-release@23.1.1)': + '@semantic-release/release-notes-generator@13.0.0(semantic-release@23.1.1(typescript@5.4.5))': dependencies: conventional-changelog-angular: 7.0.0 conventional-changelog-writer: 7.0.1 @@ -10887,7 +10935,7 @@ snapshots: '@babel/types': 7.24.6 entities: 4.5.0 - '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0)': + '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@4.9.5))': dependencies: '@babel/core': 7.24.6 '@svgr/babel-preset': 8.1.0(@babel/core@7.24.6) @@ -10897,7 +10945,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0)(typescript@4.9.5)': + '@svgr/plugin-svgo@8.1.0(@svgr/core@8.1.0(typescript@4.9.5))(typescript@4.9.5)': dependencies: '@svgr/core': 8.1.0(typescript@4.9.5) cosmiconfig: 8.3.6(typescript@4.9.5) @@ -10914,8 +10962,8 @@ snapshots: '@babel/preset-react': 7.24.6(@babel/core@7.24.6) '@babel/preset-typescript': 7.24.6(@babel/core@7.24.6) '@svgr/core': 8.1.0(typescript@4.9.5) - '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0) - '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0)(typescript@4.9.5) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@4.9.5)) + '@svgr/plugin-svgo': 8.1.0(@svgr/core@8.1.0(typescript@4.9.5))(typescript@4.9.5) transitivePeerDependencies: - supports-color - typescript @@ -10924,12 +10972,12 @@ snapshots: dependencies: tslib: 2.6.2 - '@tailwindcss/forms@0.5.7(tailwindcss@3.4.3)': + '@tailwindcss/forms@0.5.7(tailwindcss@3.4.3(ts-node@10.9.2(@types/node@17.0.45)(typescript@4.9.5)))': dependencies: mini-svg-data-uri: 1.4.4 - tailwindcss: 3.4.3(ts-node@10.9.2) + tailwindcss: 3.4.3(ts-node@10.9.2(@types/node@17.0.45)(typescript@4.9.5)) - '@tanstack/react-table@8.17.3(react-dom@18.3.1)(react@18.3.1)': + '@tanstack/react-table@8.17.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tanstack/table-core': 8.17.3 react: 18.3.1 @@ -11023,7 +11071,7 @@ snapshots: dependencies: '@tsparticles/engine': 3.4.0 - '@tsparticles/react@3.0.0(@tsparticles/engine@3.4.0)(react-dom@18.3.1)(react@18.3.1)': + '@tsparticles/react@3.0.0(@tsparticles/engine@3.4.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@tsparticles/engine': 3.4.0 react: 18.3.1 @@ -11323,7 +11371,7 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@4.9.5)': + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@4.9.5))(eslint@8.57.0)(typescript@4.9.5)': dependencies: '@eslint-community/regexpp': 4.10.0 '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@4.9.5) @@ -11338,11 +11386,12 @@ snapshots: natural-compare: 1.4.0 semver: 7.6.2 ts-api-utils: 1.3.0(typescript@4.9.5) + optionalDependencies: typescript: 4.9.5 transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@5.4.5)': + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)': dependencies: '@eslint-community/regexpp': 4.10.0 '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.5) @@ -11357,6 +11406,7 @@ snapshots: natural-compare: 1.4.0 semver: 7.6.2 ts-api-utils: 1.3.0(typescript@5.4.5) + optionalDependencies: typescript: 5.4.5 transitivePeerDependencies: - supports-color @@ -11369,6 +11419,7 @@ snapshots: '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.3.4 eslint: 8.57.0 + optionalDependencies: typescript: 4.9.5 transitivePeerDependencies: - supports-color @@ -11381,6 +11432,7 @@ snapshots: '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.3.4 eslint: 8.57.0 + optionalDependencies: typescript: 5.4.5 transitivePeerDependencies: - supports-color @@ -11402,6 +11454,7 @@ snapshots: debug: 4.3.4 eslint: 8.57.0 ts-api-utils: 1.3.0(typescript@4.9.5) + optionalDependencies: typescript: 4.9.5 transitivePeerDependencies: - supports-color @@ -11413,6 +11466,7 @@ snapshots: debug: 4.3.4 eslint: 8.57.0 ts-api-utils: 1.3.0(typescript@5.4.5) + optionalDependencies: typescript: 5.4.5 transitivePeerDependencies: - supports-color @@ -11430,6 +11484,7 @@ snapshots: is-glob: 4.0.3 semver: 7.6.2 tsutils: 3.21.0(typescript@4.9.5) + optionalDependencies: typescript: 4.9.5 transitivePeerDependencies: - supports-color @@ -11444,6 +11499,7 @@ snapshots: minimatch: 9.0.3 semver: 7.6.2 ts-api-utils: 1.3.0(typescript@4.9.5) + optionalDependencies: typescript: 4.9.5 transitivePeerDependencies: - supports-color @@ -11458,6 +11514,7 @@ snapshots: minimatch: 9.0.3 semver: 7.6.2 ts-api-utils: 1.3.0(typescript@5.4.5) + optionalDependencies: typescript: 5.4.5 transitivePeerDependencies: - supports-color @@ -11517,29 +11574,31 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vercel/style-guide@5.2.0(eslint@8.57.0)(prettier@3.2.5)(typescript@4.9.5)': + '@vercel/style-guide@5.2.0(@next/eslint-plugin-next@13.5.6)(eslint@8.57.0)(jest@29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)))(prettier@3.2.5)(typescript@4.9.5)': dependencies: '@babel/core': 7.24.6 '@babel/eslint-parser': 7.24.6(@babel/core@7.24.6)(eslint@8.57.0) '@rushstack/eslint-patch': 1.10.3 - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@4.9.5) + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@4.9.5))(eslint@8.57.0)(typescript@4.9.5) '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@4.9.5) - eslint: 8.57.0 eslint-config-prettier: 9.1.0(eslint@8.57.0) - eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.29.1) - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0)(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-import-resolver-alias: 1.1.2(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@4.9.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@4.9.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0) eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) - eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint@8.57.0)(typescript@4.9.5) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@4.9.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@4.9.5))(eslint@8.57.0)(typescript@4.9.5))(eslint@8.57.0)(jest@29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)))(typescript@4.9.5) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0) - eslint-plugin-playwright: 0.16.0(eslint-plugin-jest@27.9.0)(eslint@8.57.0) + eslint-plugin-playwright: 0.16.0(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@4.9.5))(eslint@8.57.0)(typescript@4.9.5))(eslint@8.57.0)(jest@29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)))(typescript@4.9.5))(eslint@8.57.0) eslint-plugin-react: 7.34.1(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) eslint-plugin-testing-library: 6.2.2(eslint@8.57.0)(typescript@4.9.5) eslint-plugin-tsdoc: 0.2.17 eslint-plugin-unicorn: 48.0.1(eslint@8.57.0) - prettier: 3.2.5 prettier-plugin-packagejson: 2.5.0(prettier@3.2.5) + optionalDependencies: + '@next/eslint-plugin-next': 13.5.6 + eslint: 8.57.0 + prettier: 3.2.5 typescript: 4.9.5 transitivePeerDependencies: - eslint-import-resolver-node @@ -11683,15 +11742,15 @@ snapshots: indent-string: 5.0.0 ajv-formats@2.1.1(ajv@8.12.0): - dependencies: + optionalDependencies: ajv: 8.12.0 ajv-formats@2.1.1(ajv@8.14.0): - dependencies: + optionalDependencies: ajv: 8.14.0 ajv-formats@3.0.1(ajv@8.14.0): - dependencies: + optionalDependencies: ajv: 8.14.0 ajv-keywords@3.5.2(ajv@6.12.6): @@ -11890,7 +11949,7 @@ snapshots: '@fastify/error': 3.4.1 fastq: 1.17.1 - avvvatars-react@0.4.2(csstype@3.1.3)(react-dom@18.3.1)(react@18.3.1): + avvvatars-react@0.4.2(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: goober: 2.1.14(csstype@3.1.3) react: 18.3.1 @@ -12267,18 +12326,18 @@ snapshots: cluster-key-slot@1.1.2: {} - cmdk@0.2.1(react-dom@18.3.1)(react@18.3.1): + cmdk@0.2.1(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@radix-ui/react-dialog': 1.0.0(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-dialog': 1.0.0(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: - '@types/react' - cmdk@1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1): + cmdk@1.0.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) - '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1) + '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: 18.3.1 react-dom: 18.3.1(react@18.3.1) transitivePeerDependencies: @@ -12424,6 +12483,7 @@ snapshots: js-yaml: 4.1.0 parse-json: 5.2.0 path-type: 4.0.0 + optionalDependencies: typescript: 4.9.5 cosmiconfig@8.3.6(typescript@5.3.3): @@ -12432,6 +12492,7 @@ snapshots: js-yaml: 4.1.0 parse-json: 5.2.0 path-type: 4.0.0 + optionalDependencies: typescript: 5.3.3 cosmiconfig@9.0.0(typescript@5.4.5): @@ -12440,6 +12501,7 @@ snapshots: import-fresh: 3.3.0 js-yaml: 4.1.0 parse-json: 5.2.0 + optionalDependencies: typescript: 5.4.5 create-hash@1.2.0: @@ -12461,13 +12523,13 @@ snapshots: sha.js: 2.4.11 optional: true - create-jest@29.7.0(@types/node@20.12.12)(ts-node@10.9.2): + create-jest@29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.12.12)(ts-node@10.9.2) + jest-config: 29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -12906,9 +12968,9 @@ snapshots: eslint: 8.57.0 eslint-plugin-turbo: 1.13.3(eslint@8.57.0) - eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.29.1): + eslint-import-resolver-alias@1.1.2(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@4.9.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0)): dependencies: - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@4.9.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-import-resolver-node@0.3.9: dependencies: @@ -12918,13 +12980,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0)(eslint-plugin-import@2.29.1)(eslint@8.57.0): + eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@4.9.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0): dependencies: debug: 4.3.4 enhanced-resolve: 5.16.1 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@4.9.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@4.9.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.5 is-core-module: 2.13.1 @@ -12935,13 +12997,14 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@4.9.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): dependencies: - '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@4.9.5) debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@4.9.5) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0)(eslint-plugin-import@2.29.1)(eslint@8.57.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@4.9.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0) transitivePeerDependencies: - supports-color @@ -12957,9 +13020,8 @@ snapshots: eslint: 8.57.0 ignore: 5.3.1 - eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@4.9.5))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: - '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@4.9.5) array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 array.prototype.flat: 1.3.2 @@ -12968,7 +13030,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@4.9.5))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@4.9.5))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -12978,16 +13040,20 @@ snapshots: object.values: 1.2.0 semver: 6.3.1 tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@4.9.5) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint@8.57.0)(typescript@4.9.5): + eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@4.9.5))(eslint@8.57.0)(typescript@4.9.5))(eslint@8.57.0)(jest@29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)))(typescript@4.9.5): dependencies: - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.57.0)(typescript@4.9.5) '@typescript-eslint/utils': 5.62.0(eslint@8.57.0)(typescript@4.9.5) eslint: 8.57.0 + optionalDependencies: + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@4.9.5))(eslint@8.57.0)(typescript@4.9.5) + jest: 29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)) transitivePeerDependencies: - supports-color - typescript @@ -13022,18 +13088,21 @@ snapshots: resolve: 1.22.8 semver: 6.3.1 - eslint-plugin-playwright@0.16.0(eslint-plugin-jest@27.9.0)(eslint@8.57.0): + eslint-plugin-playwright@0.16.0(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@4.9.5))(eslint@8.57.0)(typescript@4.9.5))(eslint@8.57.0)(jest@29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)))(typescript@4.9.5))(eslint@8.57.0): dependencies: eslint: 8.57.0 - eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint@8.57.0)(typescript@4.9.5) + optionalDependencies: + eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@4.9.5))(eslint@8.57.0)(typescript@4.9.5))(eslint@8.57.0)(jest@29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)))(typescript@4.9.5) - eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0)(eslint@8.57.0)(prettier@3.2.5): + eslint-plugin-prettier@5.1.3(@types/eslint@8.56.10)(eslint-config-prettier@9.1.0(eslint@8.57.0))(eslint@8.57.0)(prettier@3.2.5): dependencies: eslint: 8.57.0 - eslint-config-prettier: 9.1.0(eslint@8.57.0) prettier: 3.2.5 prettier-linter-helpers: 1.0.0 synckit: 0.8.8 + optionalDependencies: + '@types/eslint': 8.56.10 + eslint-config-prettier: 9.1.0(eslint@8.57.0) eslint-plugin-react-hooks@4.6.2(eslint@8.57.0): dependencies: @@ -13517,11 +13586,12 @@ snapshots: fraction.js@4.3.7: {} - framer-motion@11.2.6(react-dom@18.3.1)(react@18.3.1): + framer-motion@11.2.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: + tslib: 2.6.2 + optionalDependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - tslib: 2.6.2 fresh@0.5.2: {} @@ -13562,9 +13632,9 @@ snapshots: functions-have-names@1.2.3: {} - geist@1.3.0(next@13.5.6): + geist@1.3.0(next@13.5.6(@babel/core@7.24.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): dependencies: - next: 13.5.6(@babel/core@7.24.6)(react-dom@18.3.1)(react@18.3.1) + next: 13.5.6(@babel/core@7.24.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) generic-pool@3.9.0: {} @@ -13917,7 +13987,7 @@ snapshots: inline-style-parser@0.2.3: {} - input-otp@1.2.4(react-dom@18.3.1)(react@18.3.1): + input-otp@1.2.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -14247,16 +14317,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.12.12)(ts-node@10.9.2): + jest-cli@29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.12.12)(ts-node@10.9.2) + create-jest: 29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)) exit: 0.1.2 import-local: 3.1.0 - jest-config: 29.7.0(@types/node@20.12.12)(ts-node@10.9.2) + jest-config: 29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -14266,12 +14336,11 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.12.12)(ts-node@10.9.2): + jest-config@29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)): dependencies: '@babel/core': 7.24.6 '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.12.12 babel-jest: 29.7.0(@babel/core@7.24.6) chalk: 4.1.2 ci-info: 3.9.0 @@ -14291,7 +14360,9 @@ snapshots: pretty-format: 29.7.0 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.2(@types/node@17.0.45)(typescript@5.4.5) + optionalDependencies: + '@types/node': 20.12.12 + ts-node: 10.9.2(@types/node@20.12.12)(typescript@5.4.5) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -14366,9 +14437,9 @@ snapshots: slash: 3.0.0 stack-utils: 2.0.6 - jest-mock-extended@3.0.7(jest@29.7.0)(typescript@5.4.5): + jest-mock-extended@3.0.7(jest@29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)))(typescript@5.4.5): dependencies: - jest: 29.7.0(@types/node@20.12.12)(ts-node@10.9.2) + jest: 29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)) ts-essentials: 10.0.0(typescript@5.4.5) typescript: 5.4.5 @@ -14379,7 +14450,7 @@ snapshots: jest-util: 29.7.0 jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): - dependencies: + optionalDependencies: jest-resolve: 29.7.0 jest-regex-util@29.6.3: {} @@ -14523,12 +14594,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.12.12)(ts-node@10.9.2): + jest@29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2) + '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)) '@jest/types': 29.6.3 import-local: 3.1.0 - jest-cli: 29.7.0(@types/node@20.12.12)(ts-node@10.9.2) + jest-cli: 29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -14540,7 +14611,7 @@ snapshots: jju@1.4.0: {} jotai@2.8.2(@types/react@18.3.3)(react@18.3.1): - dependencies: + optionalDependencies: '@types/react': 18.3.3 react: 18.3.1 @@ -15248,12 +15319,12 @@ snapshots: nerf-dart@1.0.0: {} - next-themes@0.3.0(react-dom@18.3.1)(react@18.3.1): + next-themes@0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next@13.5.6(@babel/core@7.24.6)(react-dom@18.3.1)(react@18.3.1): + next@13.5.6(@babel/core@7.24.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 13.5.6 '@swc/helpers': 0.5.2 @@ -15645,12 +15716,13 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.4.38 - postcss-load-config@4.0.2(postcss@8.4.38)(ts-node@10.9.2): + postcss-load-config@4.0.2(postcss@8.4.38)(ts-node@10.9.2(@types/node@17.0.45)(typescript@4.9.5)): dependencies: lilconfig: 3.1.1 - postcss: 8.4.38 - ts-node: 10.9.2(@types/node@17.0.45)(typescript@5.4.5) yaml: 2.4.2 + optionalDependencies: + postcss: 8.4.38 + ts-node: 10.9.2(@types/node@17.0.45)(typescript@4.9.5) postcss-nested@6.0.1(postcss@8.4.38): dependencies: @@ -15691,9 +15763,10 @@ snapshots: prettier-plugin-packagejson@2.5.0(prettier@3.2.5): dependencies: - prettier: 3.2.5 sort-package-json: 2.10.0 synckit: 0.9.0 + optionalDependencies: + prettier: 3.2.5 prettier-plugin-tailwindcss@0.5.14(prettier@3.2.5): dependencies: @@ -15794,7 +15867,7 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 - react-draggable@4.4.6(react-dom@18.3.1)(react@18.3.1): + react-draggable@4.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: clsx: 1.2.1 prop-types: 15.8.1 @@ -15813,12 +15886,13 @@ snapshots: react-remove-scroll-bar@2.3.6(@types/react@18.3.3)(react@18.3.1): dependencies: - '@types/react': 18.3.3 react: 18.3.1 react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1) tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.3.3 - react-remove-scroll@2.5.4(react@18.3.1): + react-remove-scroll@2.5.4(@types/react@18.3.3)(react@18.3.1): dependencies: react: 18.3.1 react-remove-scroll-bar: 2.3.6(@types/react@18.3.3)(react@18.3.1) @@ -15826,24 +15900,28 @@ snapshots: tslib: 2.6.2 use-callback-ref: 1.3.2(@types/react@18.3.3)(react@18.3.1) use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 react-remove-scroll@2.5.5(@types/react@18.3.3)(react@18.3.1): dependencies: - '@types/react': 18.3.3 react: 18.3.1 react-remove-scroll-bar: 2.3.6(@types/react@18.3.3)(react@18.3.1) react-style-singleton: 2.2.1(@types/react@18.3.3)(react@18.3.1) tslib: 2.6.2 use-callback-ref: 1.3.2(@types/react@18.3.3)(react@18.3.1) use-sidecar: 1.1.2(@types/react@18.3.3)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 react-style-singleton@2.2.1(@types/react@18.3.3)(react@18.3.1): dependencies: - '@types/react': 18.3.3 get-nonce: 1.0.1 invariant: 2.2.4 react: 18.3.1 tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.3.3 react@18.3.1: dependencies: @@ -16136,11 +16214,11 @@ snapshots: semantic-release@23.1.1(typescript@5.4.5): dependencies: - '@semantic-release/commit-analyzer': 12.0.0(semantic-release@23.1.1) + '@semantic-release/commit-analyzer': 12.0.0(semantic-release@23.1.1(typescript@5.4.5)) '@semantic-release/error': 4.0.0 - '@semantic-release/github': 10.0.5(semantic-release@23.1.1) - '@semantic-release/npm': 12.0.1(semantic-release@23.1.1) - '@semantic-release/release-notes-generator': 13.0.0(semantic-release@23.1.1) + '@semantic-release/github': 10.0.5(semantic-release@23.1.1(typescript@5.4.5)) + '@semantic-release/npm': 12.0.1(semantic-release@23.1.1(typescript@5.4.5)) + '@semantic-release/release-notes-generator': 13.0.0(semantic-release@23.1.1(typescript@5.4.5)) aggregate-error: 5.0.0 cosmiconfig: 9.0.0(typescript@5.4.5) debug: 4.3.4 @@ -16348,7 +16426,7 @@ snapshots: dependencies: atomic-sleep: 1.0.0 - sonner@1.4.41(react-dom@18.3.1)(react@18.3.1): + sonner@1.4.41(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) @@ -16533,9 +16611,10 @@ snapshots: styled-jsx@5.1.1(@babel/core@7.24.6)(react@18.3.1): dependencies: - '@babel/core': 7.24.6 client-only: 0.0.1 react: 18.3.1 + optionalDependencies: + '@babel/core': 7.24.6 sucrase@3.35.0: dependencies: @@ -16625,11 +16704,11 @@ snapshots: dependencies: '@babel/runtime': 7.24.6 - tailwindcss-animate@1.0.7(tailwindcss@3.4.3): + tailwindcss-animate@1.0.7(tailwindcss@3.4.3(ts-node@10.9.2(@types/node@17.0.45)(typescript@4.9.5))): dependencies: - tailwindcss: 3.4.3(ts-node@10.9.2) + tailwindcss: 3.4.3(ts-node@10.9.2(@types/node@17.0.45)(typescript@4.9.5)) - tailwindcss@3.4.3(ts-node@10.9.2): + tailwindcss@3.4.3(ts-node@10.9.2(@types/node@17.0.45)(typescript@4.9.5)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -16648,7 +16727,7 @@ snapshots: postcss: 8.4.38 postcss-import: 15.1.0(postcss@8.4.38) postcss-js: 4.0.1(postcss@8.4.38) - postcss-load-config: 4.0.2(postcss@8.4.38)(ts-node@10.9.2) + postcss-load-config: 4.0.2(postcss@8.4.38)(ts-node@10.9.2(@types/node@17.0.45)(typescript@4.9.5)) postcss-nested: 6.0.1(postcss@8.4.38) postcss-selector-parser: 6.1.0 resolve: 1.22.8 @@ -16768,17 +16847,16 @@ snapshots: typescript: 5.4.5 ts-essentials@10.0.0(typescript@5.4.5): - dependencies: + optionalDependencies: typescript: 5.4.5 ts-interface-checker@0.1.13: {} - ts-jest@29.1.3(@babel/core@7.24.6)(jest@29.7.0)(typescript@5.4.5): + ts-jest@29.1.3(@babel/core@7.24.6)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.24.6))(jest@29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)))(typescript@5.4.5): dependencies: - '@babel/core': 7.24.6 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.12.12)(ts-node@10.9.2) + jest: 29.7.0(@types/node@20.12.12)(ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -16786,8 +16864,13 @@ snapshots: semver: 7.6.2 typescript: 5.4.5 yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.24.6 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-jest: 29.7.0(@babel/core@7.24.6) - ts-loader@9.5.1(typescript@5.4.5)(webpack@5.91.0): + ts-loader@9.5.1(typescript@5.4.5)(webpack@5.90.1): dependencies: chalk: 4.1.2 enhanced-resolve: 5.16.1 @@ -16795,9 +16878,9 @@ snapshots: semver: 7.6.2 source-map: 0.7.4 typescript: 5.4.5 - webpack: 5.91.0 + webpack: 5.90.1 - ts-node@10.9.2(@types/node@17.0.45)(typescript@5.4.5): + ts-node@10.9.2(@types/node@17.0.45)(typescript@4.9.5): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 @@ -16811,6 +16894,25 @@ snapshots: create-require: 1.1.1 diff: 4.0.2 make-error: 1.3.6 + typescript: 4.9.5 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optional: true + + ts-node@10.9.2(@types/node@20.12.12)(typescript@5.4.5): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.12.12 + acorn: 8.11.3 + acorn-walk: 8.3.2 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 typescript: 5.4.5 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 @@ -17061,16 +17163,18 @@ snapshots: use-callback-ref@1.3.2(@types/react@18.3.3)(react@18.3.1): dependencies: - '@types/react': 18.3.3 react: 18.3.1 tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.3.3 use-sidecar@1.1.2(@types/react@18.3.3)(react@18.3.1): dependencies: - '@types/react': 18.3.3 detect-node-es: 1.1.0 react: 18.3.1 tslib: 2.6.2 + optionalDependencies: + '@types/react': 18.3.3 use-sync-external-store@1.2.0(react@18.3.1): dependencies: @@ -17349,9 +17453,11 @@ snapshots: zod@3.23.8: {} - zustand@4.5.2(react@18.3.1): + zustand@4.5.2(@types/react@18.3.3)(react@18.3.1): dependencies: - react: 18.3.1 use-sync-external-store: 1.2.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.3 + react: 18.3.1 zwitch@2.0.4: {}