diff --git a/apps/api/src/common/get-environmets-of-project.ts b/apps/api/src/common/get-environmets-of-project.ts new file mode 100644 index 00000000..205e619f --- /dev/null +++ b/apps/api/src/common/get-environmets-of-project.ts @@ -0,0 +1,37 @@ +import { PrismaService } from 'src/prisma/prisma.service' +import { AuthorityCheckerService } from './authority-checker.service' +import { Authority, Project, User } from '@prisma/client' + +export default async function getEnvironmentsOfProject( + prisma: PrismaService, + authorityCheckerService: AuthorityCheckerService, + user: User, + projectId: Project['id'], + sort: string, + order: string, + search: string +) { + // Check authority + await authorityCheckerService.checkAuthorityOverProject({ + userId: user.id, + entity: { id: projectId }, + authority: Authority.READ_ENVIRONMENT, + prisma + }) + + // Get the environments + return await prisma.environment.findMany({ + where: { + projectId, + name: { + contains: search + } + }, + include: { + lastUpdatedBy: true + }, + orderBy: { + [sort]: order + } + }) +} diff --git a/apps/api/src/common/get-secrets-of-project.ts b/apps/api/src/common/get-secrets-of-project.ts new file mode 100644 index 00000000..750c3b28 --- /dev/null +++ b/apps/api/src/common/get-secrets-of-project.ts @@ -0,0 +1,150 @@ +import { + User, + Project, + Authority, + Secret, + Environment, + SecretVersion +} from '@prisma/client' +import { PrismaService } from 'src/prisma/prisma.service' +import { AuthorityCheckerService } from './authority-checker.service' +import { decrypt } from './decrypt' +import { BadRequestException, NotFoundException } from '@nestjs/common' + +async function 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` + ) + } + + // 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` + ) + } +} + +export default async function getAllSecretsOfProject( + prisma: PrismaService, + authorityCheckerService: AuthorityCheckerService, + user: User, + projectId: Project['id'], + decryptValue: boolean, + sort: string, + order: string, + search: string +) { + // Fetch the project + const project = await authorityCheckerService.checkAuthorityOverProject({ + userId: user.id, + entity: { id: projectId }, + authority: Authority.READ_SECRET, + prisma: prisma + }) + + // Check if the secret values can be decrypted + await checkAutoDecrypt(decryptValue, project) + + const secrets = await prisma.secret.findMany({ + where: { + projectId, + name: { + contains: search + } + }, + include: { + lastUpdatedBy: { + select: { + id: true, + name: true + } + } + }, + orderBy: { + [sort]: order + } + }) + + const secretsWithEnvironmentalValues = new Map< + Secret['id'], + { + secret: Secret + values: { + environment: { + name: Environment['name'] + id: Environment['id'] + } + value: SecretVersion['value'] + version: SecretVersion['version'] + }[] + } + >() + + // Find all the environments for this project + const environments = await prisma.environment.findMany({ + where: { + projectId + } + }) + const environmentIds = new Map(environments.map((env) => [env.id, env.name])) + + for (const secret of secrets) { + // 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 prisma.secretVersion.findFirst({ + where: { + secretId: secret.id, + environmentId: { + in: Array.from(envIds.keys()) + } + }, + orderBy: { + version: 'desc' + } + }) + + 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, + version: latestVersion.version + }) + } 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, + version: latestVersion.version + } + ] + }) + } + + envIds.delete(latestVersion.environmentId) + } + } + + return Array.from(secretsWithEnvironmentalValues.values()) +} diff --git a/apps/api/src/common/get-variables-of-project.ts b/apps/api/src/common/get-variables-of-project.ts new file mode 100644 index 00000000..1d42c360 --- /dev/null +++ b/apps/api/src/common/get-variables-of-project.ts @@ -0,0 +1,123 @@ +import { + Authority, + Environment, + Project, + User, + Variable, + VariableVersion +} from '@prisma/client' +import { PrismaService } from 'src/prisma/prisma.service' +import { AuthorityCheckerService } from './authority-checker.service' + +export default async function getAllVariablesOfProject( + prisma: PrismaService, + authorityCheckerService: AuthorityCheckerService, + user: User, + projectId: Project['id'], + sort: string, + order: string, + search: string +) { + // Check if the user has the required authorities in the project + await authorityCheckerService.checkAuthorityOverProject({ + userId: user.id, + entity: { id: projectId }, + authority: Authority.READ_VARIABLE, + prisma: prisma + }) + + const variables = await prisma.variable.findMany({ + where: { + projectId, + name: { + contains: search + } + }, + include: { + lastUpdatedBy: { + select: { + id: true, + name: true + } + } + }, + orderBy: { + [sort]: order + } + }) + + const variablesWithEnvironmentalValues = new Map< + Variable['id'], + { + variable: Variable + values: { + environment: { + name: Environment['name'] + id: Environment['id'] + } + value: VariableVersion['value'] + version: VariableVersion['version'] + }[] + } + >() + + // Find all the environments for this project + const environments = await 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 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, + version: latestVersion.version + }) + } else { + variablesWithEnvironmentalValues.set(variable.id, { + variable, + values: [ + { + environment: { + id: latestVersion.environmentId, + name: envIds.get(latestVersion.environmentId) + }, + value: latestVersion.value, + version: latestVersion.version + } + ] + }) + } + + envIds.delete(latestVersion.environmentId) + } + } + + return Array.from(variablesWithEnvironmentalValues.values()) +} diff --git a/apps/api/src/project/service/project.service.ts b/apps/api/src/project/service/project.service.ts index 18253372..a2c5f12f 100644 --- a/apps/api/src/project/service/project.service.ts +++ b/apps/api/src/project/service/project.service.ts @@ -29,6 +29,9 @@ import createEvent from '../../common/create-event' import { ProjectWithSecrets } from '../project.types' import { AuthorityCheckerService } from '../../common/authority-checker.service' import { ForkProject } from '../dto/fork.project/fork.project' +import getEnvironmentsOfProject from 'src/common/get-environmets-of-project' +import getAllVariablesOfProject from 'src/common/get-variables-of-project' +import getAllSecretsOfProject from 'src/common/get-secrets-of-project' @Injectable() export class ProjectService { @@ -638,7 +641,7 @@ export class ProjectService { prisma: this.prisma }) - return ( + const projects = ( await this.prisma.project.findMany({ skip: page * limit, take: limit, @@ -669,6 +672,49 @@ export class ProjectService { } }) ).map((project) => excludeFields(project, 'privateKey', 'publicKey')) + + const response = await Promise.all( + projects.map(async (project) => { + const totalenvironmets = await getEnvironmentsOfProject( + this.prisma, + this.authorityCheckerService, + user, + project.id, + 'name', + 'asc', + '' + ) + const totalvariables = await getAllVariablesOfProject( + this.prisma, + this.authorityCheckerService, + user, + project.id, + 'name', + 'asc', + '' + ) + const totalsecrets = await getAllSecretsOfProject( + this.prisma, + this.authorityCheckerService, + user, + project.id, + false, + 'name', + 'asc', + '' + ) + + // Append counts to the project object + return { + ...project, + totalenvironmets, + totalvariables, + totalsecrets + } + }) + ) + + return response } private async projectExists(