Skip to content

Commit

Permalink
feat: add api-keys module
Browse files Browse the repository at this point in the history
  • Loading branch information
rajdip-b committed Jan 14, 2024
1 parent b15dbb0 commit abb2863
Show file tree
Hide file tree
Showing 18 changed files with 412 additions and 19 deletions.
9 changes: 9 additions & 0 deletions apps/api/src/api-key/api-key.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common'
import { ApiKeyController } from './controller/api-key.controller'
import { ApiKeyService } from './service/api-key.service'

@Module({
controllers: [ApiKeyController],
providers: [ApiKeyService]
})
export class ApiKeyModule {}
15 changes: 15 additions & 0 deletions apps/api/src/api-key/api-key.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {
ApiKey,
ApiKeyProjectRole,
Project,
ProjectScope
} from '@prisma/client'

export interface Scope {
projectId: Project['id']
roles: ApiKeyProjectRole[]
}

export interface ApiKeyWithProjectScopes extends Partial<ApiKey> {
projectScopes: ProjectScope[]
}
18 changes: 18 additions & 0 deletions apps/api/src/api-key/controller/api-key.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ApiKeyController } from './api-key.controller';

describe('ApiKeyController', () => {
let controller: ApiKeyController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ApiKeyController],
}).compile();

controller = module.get<ApiKeyController>(ApiKeyController);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});
});
4 changes: 4 additions & 0 deletions apps/api/src/api-key/controller/api-key.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Controller } from '@nestjs/common';

@Controller('api-key')
export class ApiKeyController {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { CreateApiKey } from './create.api-key';

describe('CreateApiKey', () => {
it('should be defined', () => {
expect(new CreateApiKey()).toBeDefined();
});
});
14 changes: 14 additions & 0 deletions apps/api/src/api-key/dto/create.api-key/create.api-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ApiKey, ApiKeyGeneralRole, ProjectScope } from '@prisma/client'
import { IsString } from 'class-validator'

export class CreateApiKey {
@IsString()
name: ApiKey['name']

@IsString()
expiresAfter: '1d' | '7d' | '30d' | '90d' | '365d' | 'never'

generalRoles: ApiKeyGeneralRole[]

scopes: ProjectScope[]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { UpdateApiKey } from './update.api-key';

describe('UpdateApiKey', () => {
it('should be defined', () => {
expect(new UpdateApiKey()).toBeDefined();
});
});
15 changes: 15 additions & 0 deletions apps/api/src/api-key/dto/update.api-key/update.api-key.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { PartialType } from '@nestjs/swagger'
import { CreateApiKey } from '../create.api-key/create.api-key'
import { IsOptional } from 'class-validator'
import { ProjectScope } from '@prisma/client'

export class UpdateApiKey extends PartialType(CreateApiKey) {
@IsOptional()
projectToAdd: ProjectScope[]

@IsOptional()
projectToRemove: ProjectScope['id'][]

@IsOptional()
projectToUpdate: ProjectScope[]
}
115 changes: 115 additions & 0 deletions apps/api/src/api-key/repository/api-key.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { ApiKey, ApiKeyProjectRole, User } from '@prisma/client'
import { PrismaService } from '../../prisma/prisma.service'
import { IApiKeyRepository } from './interface.repository'
import { ApiKeyWithProjectScopes } from '../api-key.types'

export class ApiKeyRepository implements IApiKeyRepository {
constructor(private readonly prisma: PrismaService) {}

async createApiKey(user: User, apiKey: Partial<ApiKey>) {
return this.prisma.apiKey.create({
data: {
name: apiKey.name,
value: apiKey.value,
expiresAt: apiKey.expiresAt,
generalRoles: apiKey.generalRoles,
user: {
connect: {
id: user.id
}
}
}
})
}

async updateApiKey(apiKeyId: ApiKey['id'], apiKey: ApiKeyWithProjectScopes) {
return this.prisma.apiKey.update({
where: {
id: apiKeyId
},
data: {
name: apiKey.name,
expiresAt: apiKey.expiresAt,
generalRoles: apiKey.generalRoles,
projectScopes: {
deleteMany: {
projectId: {
in: apiKey.projectScopes.map((scope) => scope.projectId)
}
},
createMany: {
data: apiKey.projectScopes
}
}
}
})
}

async updateRolesOfProjectScope(
userId: string,
projectId: string,
roles: ApiKeyProjectRole[]
): Promise<void> {
if (roles.length === 0) {
await this.prisma.projectScope.deleteMany({
where: {
projectId,
apiKey: {
userId
}
}
})
} else {
await this.prisma.projectScope.updateMany({
where: {
projectId,
apiKey: {
userId
}
},
data: {
roles
}
})
}
}

async deleteApiKey(apiKeyId: ApiKey['id']) {
await this.prisma.apiKey.delete({
where: {
id: apiKeyId
}
})
}

async findApiKeyByValue(apiKeyValue: ApiKey['value']) {
return this.prisma.apiKey.findUnique({
where: {
value: apiKeyValue
},
include: {
user: true
}
})
}

async findApiKeyByIdAndUserId(apiKeyId: ApiKey['id'], userId: User['id']) {
return this.prisma.apiKey.findUnique({
where: {
id: apiKeyId,
userId
},
include: {
projectScopes: true
}
})
}

async findAllApiKeysByUserId(userId: User['id']) {
return this.prisma.apiKey.findMany({
where: {
userId
}
})
}
}
73 changes: 73 additions & 0 deletions apps/api/src/api-key/repository/interface.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ApiKey, ApiKeyProjectRole, ProjectScope, User } from '@prisma/client'
import { ApiKeyWithProjectScopes } from '../api-key.types'

export const API_KEY_REPOSITORY = 'API_KEY_REPOSITORY'

/**
* Interface for the ApiKey Repository.
*/
export interface IApiKeyRepository {
/**
* Creates a new API key for a user.
* @param {User} user - The user for whom the API key is created.
* @param {Partial<ApiKey>} apiKey - The data for the new API key.
* @returns {Promise<ApiKey>} - A promise that resolves to the created API key.
*/
createApiKey(user: User, apiKey: Partial<ApiKey>): Promise<ApiKey>

/**
* Updates an existing API key.
* @param {ApiKey['id']} apiKeyId - The ID of the API key to update.
* @param {ApiKeyWithProjectScopes} apiKey - The updated API key data.
* @returns {Promise<ApiKey>} - A promise that resolves to the updated API key.
*/
updateApiKey(
apiKeyId: ApiKey['id'],
apiKey: ApiKeyWithProjectScopes
): Promise<ApiKey>

/**
* Updates the roles of a project scope. If the roles array is empty, the project scope is deleted.
* @param {User['id']} userId - The ID of the user.
* @param {ProjectScope['projectId']} projectId - The ID of the project.
* @param {ApiKeyProjectRole[]} roles - The new roles of the project scope.
* @returns {Promise<void>} - A promise that resolves when the roles are successfully updated.
*/
updateRolesOfProjectScope(
userId: User['id'],
projectId: ProjectScope['projectId'],
roles: ApiKeyProjectRole[]
): Promise<void>

/**
* Deletes an API key.
* @param {ApiKey['id']} apiKeyId - The ID of the API key to delete.
* @returns {Promise<void>} - A promise that resolves when the API key is successfully deleted.
*/
deleteApiKey(apiKeyId: ApiKey['id']): Promise<void>

/**
* Finds an API key by its value.
* @param {ApiKey['value']} apiKeyValue - The value of the API key to find.
* @returns {Promise<ApiKey | null>} - A promise that resolves to the found API key or null if not found.
*/
findApiKeyByValue(apiKeyValue: ApiKey['value']): Promise<ApiKey | null>

/**
* Finds an API key by its ID and user ID.
* @param {ApiKey['id']} apiKeyId - The ID of the API key to find.
* @param {User['id']} userId - The ID of the user.
* @returns {Promise<ApiKey | null>} - A promise that resolves to the found API key or null if not found.
*/
findApiKeyByIdAndUserId(
apiKeyId: ApiKey['id'],
userId: User['id']
): Promise<ApiKey | null>

/**
* Finds all API keys for a user.
* @param {User['id']} userId - The ID of the user.
* @returns {Promise<ApiKey[]>} - A promise that resolves to an array of API keys for the user.
*/
findAllApiKeysByUserId(userId: User['id']): Promise<ApiKey[]>
}
1 change: 1 addition & 0 deletions apps/api/src/api-key/repository/mock.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class MockApiKeyRepository {}
18 changes: 18 additions & 0 deletions apps/api/src/api-key/service/api-key.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ApiKeyService } from './api-key.service';

describe('ApiKeyService', () => {
let service: ApiKeyService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ApiKeyService],
}).compile();

service = module.get<ApiKeyService>(ApiKeyService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
47 changes: 47 additions & 0 deletions apps/api/src/api-key/service/api-key.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Inject, Injectable } from '@nestjs/common'
import {
API_KEY_REPOSITORY,
IApiKeyRepository
} from '../repository/interface.repository'
import {
IProjectRepository,
PROJECT_REPOSITORY
} from '../../project/repository/interface.repository'
import {
IUserRepository,
USER_REPOSITORY
} from '../../user/repository/interface.repository'
import { ApiKeyProjectRole, Project, User } from '@prisma/client'
// import { CreateApiKey } from '../dto/create.api-key/create.api-key'
import { ProjectPermission } from '../../project/misc/project.permission'

@Injectable()
export class ApiKeyService {
constructor(
@Inject(API_KEY_REPOSITORY)
private readonly apiKeyRepository: IApiKeyRepository,
@Inject(PROJECT_REPOSITORY)
private readonly projectRepository: IProjectRepository,
private readonly projectPermissionService: ProjectPermission,
@Inject(USER_REPOSITORY)
private readonly userRepository: IUserRepository
) {}

async getPermissableScopesOfProjecr(user: User, projectId: Project['id']) {
const roles: ApiKeyProjectRole[] = []

if (this.projectPermissionService.isProjectMember(user, projectId)) {
roles.push(
...[
ApiKeyProjectRole.READ_PROJECT,
ApiKeyProjectRole.READ_SECRET,
ApiKeyProjectRole.READ_ENVIRONMENT
]
)
}
}

// async createApiKey(user: User, dto: CreateApiKey) {
// // For each project scope, check if the user has the required roles to perform the action.
// }
}
4 changes: 4 additions & 0 deletions apps/api/src/common/api-key-generator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { randomBytes } from 'crypto'

export const generateApiKey = (): string =>
'ks_' + randomBytes(48).toString('hex')
25 changes: 25 additions & 0 deletions apps/api/src/common/api-key-roles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ApiKeyProjectRole } from '@prisma/client'

export const MEMBERSHIP_ROLES: ApiKeyProjectRole[] = [
ApiKeyProjectRole.READ_PROJECT,
ApiKeyProjectRole.READ_SECRET,
ApiKeyProjectRole.READ_ENVIRONMENT,
ApiKeyProjectRole.READ_USERS
]

export const MAINTAINER_ROLES: ApiKeyProjectRole[] = [
ApiKeyProjectRole.CREATE_SECRET,
ApiKeyProjectRole.UPDATE_SECRET,
ApiKeyProjectRole.DELETE_SECRET,
ApiKeyProjectRole.CREATE_ENVIRONMENT,
ApiKeyProjectRole.UPDATE_ENVIRONMENT,
ApiKeyProjectRole.DELETE_ENVIRONMENT
]

export const OWNER_ROLES: ApiKeyProjectRole[] = [
ApiKeyProjectRole.UPDATE_PROJECT,
ApiKeyProjectRole.DELETE_PROJECT,
ApiKeyProjectRole.ADD_USER,
ApiKeyProjectRole.REMOVE_USER,
ApiKeyProjectRole.UPDATE_USER_ROLE
]
4 changes: 4 additions & 0 deletions apps/api/src/common/to-sha256.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createHash } from 'crypto'

export const toSHA256 = (value: string): string =>
createHash('sha256').update(value).digest().toString('hex')
Loading

0 comments on commit abb2863

Please sign in to comment.