Skip to content

Commit

Permalink
feat: add secret module
Browse files Browse the repository at this point in the history
  • Loading branch information
rajdip-b committed Jan 7, 2024
1 parent e59d410 commit cd79172
Show file tree
Hide file tree
Showing 28 changed files with 1,475 additions and 125 deletions.
20 changes: 20 additions & 0 deletions apps/api/src/environment/repository/environment.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,26 @@ export class EnvironmentRepository implements IEnvironmentRepository {
.then((count) => count > 0)
}

async getDefaultEnvironmentOfProject(
projectId: string
): Promise<{
id: string
name: string
description: string
createdAt: Date
updatedAt: Date
isDefault: boolean
lastUpdatedById: string
projectId: string
}> {
return await this.prisma.environment.findFirst({
where: {
projectId,
isDefault: true
}
})
}

async getEnvironmentByProjectIdAndId(
projectId: string,
environmentId: string
Expand Down
9 changes: 9 additions & 0 deletions apps/api/src/environment/repository/interface.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,15 @@ export interface IEnvironmentRepository {
projectId: Project['id']
): Promise<boolean>

/**
* Retrieves the default environment of a project.
* @param {Project['id']} projectId - The ID of the project.
* @returns {Promise<Environment | null>} - A promise that resolves to the default environment or null if not found.
*/
getDefaultEnvironmentOfProject(
projectId: Project['id']
): Promise<Environment | null>

/**
* Retrieves an environment by project ID and environment ID.
* @param {Project['id']} projectId - The ID of the project.
Expand Down
14 changes: 14 additions & 0 deletions apps/api/src/environment/repository/mock.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@ import { Environment } from '@prisma/client'
import { IEnvironmentRepository } from './interface.repository'

export class MockEnvironmentRepository implements IEnvironmentRepository {
getDefaultEnvironmentOfProject(
projectId: string
): Promise<{
id: string
name: string
description: string
createdAt: Date
updatedAt: Date
isDefault: boolean
lastUpdatedById: string
projectId: string
}> {
throw new Error('Method not implemented.')
}
countTotalEnvironmentsInProject(projectId: string): Promise<number> {
throw new Error('Method not implemented.')
}
Expand Down
26 changes: 4 additions & 22 deletions apps/api/src/environment/service/environment.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,7 @@ export class EnvironmentService {
}

// Check if the user can manage environments of the project
await this.projectPermissionService.canManageEnvironmentsOfProject(
user,
projectId
)
await this.projectPermissionService.isProjectMaintainer(user, projectId)

// Check if an environment with the same name already exists
if (
Expand Down Expand Up @@ -80,10 +77,7 @@ export class EnvironmentService {
}

// Check if the user can manage environments of the project
await this.projectPermissionService.canManageEnvironmentsOfProject(
user,
projectId
)
await this.projectPermissionService.isProjectMaintainer(user, projectId)

// Check if the environment exists
const environment =
Expand Down Expand Up @@ -132,10 +126,7 @@ export class EnvironmentService {
}

// Check if the user can manage environments of the project
await this.projectPermissionService.canManageEnvironmentsOfProject(
user,
projectId
)
await this.projectPermissionService.isProjectMaintainer(user, projectId)

// Check if the environment exists
const environment =
Expand Down Expand Up @@ -168,12 +159,6 @@ export class EnvironmentService {
throw new NotFoundException('Project not found')
}

// Check if the user can manage environments of the project
await this.projectPermissionService.canManageEnvironmentsOfProject(
user,
projectId
)

// Get the environments
return this.environmentRepository.getEnvironmentsOfProject(
projectId,
Expand Down Expand Up @@ -217,10 +202,7 @@ export class EnvironmentService {
}

// Check if the user can manage environments of the project
await this.projectPermissionService.canManageEnvironmentsOfProject(
user,
projectId
)
await this.projectPermissionService.isProjectMaintainer(user, projectId)

// Check if the environment exists
const environment =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ CREATE TABLE "SecretVersion" (
"value" TEXT NOT NULL,
"version" INTEGER NOT NULL DEFAULT 1,
"secretId" TEXT NOT NULL,
"createdOn" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdById" TEXT NOT NULL,

CONSTRAINT "SecretVersion_pkey" PRIMARY KEY ("id")
);
Expand Down Expand Up @@ -180,6 +182,9 @@ ALTER TABLE "ApiKeyScope" ADD CONSTRAINT "ApiKeyScope_projectId_fkey" FOREIGN KE
-- AddForeignKey
ALTER TABLE "SecretVersion" ADD CONSTRAINT "SecretVersion_secretId_fkey" FOREIGN KEY ("secretId") REFERENCES "Secret"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "SecretVersion" ADD CONSTRAINT "SecretVersion_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Secret" ADD CONSTRAINT "Secret_lastUpdatedById_fkey" FOREIGN KEY ("lastUpdatedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

Expand Down
5 changes: 5 additions & 0 deletions apps/api/src/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ model User {
Secret Secret[] // Stores the secrets the user updated
project Project[] // Stores the projects the user updated
environments Environment[] // Stores the environments the user updated
SecretVersion SecretVersion[]
}

model Subscription {
Expand Down Expand Up @@ -158,6 +159,10 @@ model SecretVersion {
secretId String
secret Secret @relation(fields: [secretId], references: [id], onDelete: Cascade, onUpdate: Cascade)
createdOn DateTime @default(now())
createdBy User @relation(fields: [createdById], references: [id], onDelete: Cascade, onUpdate: Cascade)
createdById String
}

model Secret {
Expand Down
3 changes: 3 additions & 0 deletions apps/api/src/project/controller/project.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { MAIL_SERVICE } from '../../mail/services/interface.service'
import { MockMailService } from '../../mail/services/mock.service'
import { JwtService } from '@nestjs/jwt'
import { ProjectPermission } from '../misc/project.permission'
import { SECRET_REPOSITORY } from '../../secret/repository/interface.repository'
import { MockSecretRepository } from '../../secret/repository/mock.repository'

describe('ProjectController', () => {
let controller: ProjectController
Expand All @@ -27,6 +29,7 @@ describe('ProjectController', () => {
},
{ provide: USER_REPOSITORY, useClass: MockUserRepository },
{ provide: MAIL_SERVICE, useClass: MockMailService },
{ provide: SECRET_REPOSITORY, useClass: MockSecretRepository },
JwtService,
ProjectPermission
]
Expand Down
55 changes: 10 additions & 45 deletions apps/api/src/project/misc/project.permission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class ProjectPermission {
@Inject(PROJECT_REPOSITORY) private readonly repository: ProjectRepository
) {}

async canUpdateProject(user: User, projectId: Project['id']): Promise<void> {
async isProjectAdmin(user: User, projectId: Project['id']): Promise<void> {
// Admins can do everything
if (user.isAdmin) Promise.resolve()

Expand All @@ -21,44 +21,15 @@ export class ProjectPermission {
if (!membership) {
throw new UnauthorizedException('User is not a member of the project')
}
if (membership.role === ProjectRole.VIEWER) {
throw new UnauthorizedException('OWNER or MAINTAINER role is required')
if (membership.role !== ProjectRole.OWNER) {
throw new UnauthorizedException('Atleast OWNER role is required')
}
}

async canDeleteProject(user: User, projectId: Project['id']): Promise<void> {
await this.isProjectAdmin(user, projectId)
}

async canAddUserToProject(
user: User,
projectId: Project['id']
): Promise<void> {
await this.isProjectAdmin(user, projectId)
}

async canRemoveUserFromProject(
user: User,
projectId: Project['id']
): Promise<void> {
await this.isProjectAdmin(user, projectId)
}

async canUpdateUserPermissionsOfProject(
user: User,
projectId: Project['id']
): Promise<void> {
await this.isProjectAdmin(user, projectId)
}

async canManageEnvironmentsOfProject(
async isProjectMaintainer(
user: User,
projectId: Project['id']
): Promise<void> {
await this.isProjectMaintainer(user, projectId)
}

async isProjectAdmin(user: User, projectId: Project['id']): Promise<void> {
// Admins can do everything
if (user.isAdmin) Promise.resolve()

Expand All @@ -70,15 +41,15 @@ export class ProjectPermission {
if (!membership) {
throw new UnauthorizedException('User is not a member of the project')
}
if (membership.role !== ProjectRole.OWNER) {
throw new UnauthorizedException('Atleast OWNER role is required')
if (
membership.role !== ProjectRole.OWNER &&
membership.role !== ProjectRole.MAINTAINER
) {
throw new UnauthorizedException('Atleast MAINTAINER role is required')
}
}

async isProjectMaintainer(
user: User,
projectId: Project['id']
): Promise<void> {
async isProjectMember(user: User, projectId: Project['id']): Promise<void> {
// Admins can do everything
if (user.isAdmin) Promise.resolve()

Expand All @@ -90,12 +61,6 @@ export class ProjectPermission {
if (!membership) {
throw new UnauthorizedException('User is not a member of the project')
}
if (
membership.role !== ProjectRole.OWNER &&
membership.role !== ProjectRole.MAINTAINER
) {
throw new UnauthorizedException('Atleast MAINTAINER role is required')
}
}

private async resolveProjectsOfUser(
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/project/project.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { PROJECT_REPOSITORY } from './repository/interface.repository'
import { ProjectPermission } from './misc/project.permission'
import { EnvironmentModule } from '../environment/environment.module'
import { UserModule } from '../user/user.module'
import { SecretModule } from '../secret/secret.module'

@Module({
providers: [
Expand All @@ -18,6 +19,6 @@ import { UserModule } from '../user/user.module'
],
controllers: [ProjectController],
exports: [ProjectPermission],
imports: [UserModule, EnvironmentModule]
imports: [UserModule, EnvironmentModule, SecretModule]
})
export class ProjectModule {}
30 changes: 30 additions & 0 deletions apps/api/src/project/project.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {
Project,
ProjectMember,
ProjectRole,
Secret,
User
} from '@prisma/client'

export interface ProjectWithMembers extends Project {
members: ProjectMember[]
}

export interface ProjectWithUserRole extends Project {
role: ProjectMember['role']
}

export interface ProjectWithSecrets extends Project {
secrets: Secret[]
}

export interface ProjectWithMembersAndSecrets
extends ProjectWithMembers,
ProjectWithSecrets {}

export interface ProjectMembership {
id: string
role: ProjectRole
user: User
invitationAccepted: boolean
}
17 changes: 11 additions & 6 deletions apps/api/src/project/repository/interface.repository.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { Project, ProjectMember, ProjectRole, User } from '@prisma/client'
import {
ProjectWithMembersAndSecrets,
ProjectWithSecrets,
ProjectWithUserRole
} from '../project.types'

export const PROJECT_REPOSITORY = 'PROJECT_REPOSITORY'

Expand Down Expand Up @@ -113,19 +118,19 @@ export interface IProjectRepository {
* Retrieves a project by user ID and project ID.
* @param {User['id']} userId - The ID of the user.
* @param {Project['id']} projectId - The ID of the project.
* @returns {Promise<Project | null>} - A promise that resolves to the project or null if not found.
* @returns {Promise<ProjectWithSecrets | null>} - A promise that resolves to the project or null if not found.
*/
getProjectByUserIdAndId(
userId: User['id'],
projectId: Project['id']
): Promise<Project | null>
): Promise<ProjectWithMembersAndSecrets | null>

/**
* Retrieves a project by ID.
* @param {Project['id']} projectId - The ID of the project.
* @returns {Promise<Project | null>} - A promise that resolves to the project or null if not found.
* @returns {Promise<ProjectWithSecrets | null>} - A promise that resolves to the project or null if not found.
*/
getProjectById(projectId: Project['id']): Promise<Project | null>
getProjectById(projectId: Project['id']): Promise<ProjectWithSecrets | null>

/**
* Retrieves projects of a user with optional pagination, sorting, and search.
Expand All @@ -135,7 +140,7 @@ export interface IProjectRepository {
* @param {string} sort - The field to sort by.
* @param {string} order - The sort order ('asc' or 'desc').
* @param {string} search - The search term for project names or descriptions.
* @returns {Promise<Array<Project & { role: ProjectRole }>>} - A promise that resolves to an array of projects and permission.
* @returns {Promise<Array<ProjectWithUserRole>>} - A promise that resolves to an array of projects and permission.
*/
getProjectsOfUser(
userId: User['id'],
Expand All @@ -144,7 +149,7 @@ export interface IProjectRepository {
sort: string,
order: string,
search: string
): Promise<Array<Project & { role: ProjectRole }>>
): Promise<Array<ProjectWithUserRole>>

/**
* Retrieves projects with optional pagination, sorting, and search.
Expand Down
Loading

0 comments on commit cd79172

Please sign in to comment.