From f1337775594865e762e2887d1dd171d3a5221c4d Mon Sep 17 00:00:00 2001 From: Anand Chowdhary Date: Fri, 30 Oct 2020 15:55:04 +0530 Subject: [PATCH] :sparkles: Add group API keys module --- src/app.module.ts | 2 + src/modules/api-keys/api-keys.controller.ts | 96 ++++++++++++++++ src/modules/api-keys/api-keys.dto.ts | 76 +++++++++++++ src/modules/api-keys/api-keys.module.ts | 11 ++ src/modules/api-keys/api-keys.service.ts | 115 ++++++++++++++++++++ 5 files changed, 300 insertions(+) create mode 100644 src/modules/api-keys/api-keys.controller.ts create mode 100644 src/modules/api-keys/api-keys.dto.ts create mode 100644 src/modules/api-keys/api-keys.module.ts create mode 100644 src/modules/api-keys/api-keys.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index 768edd2c5..6e24a9835 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,6 +5,7 @@ import { ScheduleModule } from '@nestjs/schedule'; import { RateLimiterInterceptor, RateLimiterModule } from 'nestjs-rate-limiter'; import configuration from './config/configuration'; import { AccessTokensModule } from './modules/access-tokens/access-tokens.module'; +import { ApiKeysModule } from './modules/api-keys/api-keys.module'; import { ApprovedSubnetsModule } from './modules/approved-subnets/approved-subnets.module'; import { AuthModule } from './modules/auth/auth.module'; import { JwtAuthGuard } from './modules/auth/jwt-auth.guard'; @@ -39,6 +40,7 @@ import { UsersModule } from './modules/users/users.module'; EmailsModule, GroupsModule, MultiFactorAuthenticationModule, + ApiKeysModule, ApprovedSubnetsModule, GeolocationModule, ], diff --git a/src/modules/api-keys/api-keys.controller.ts b/src/modules/api-keys/api-keys.controller.ts new file mode 100644 index 000000000..a12e25cbe --- /dev/null +++ b/src/modules/api-keys/api-keys.controller.ts @@ -0,0 +1,96 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + ParseIntPipe, + Patch, + Post, + Put, + Query, +} from '@nestjs/common'; +import { apiKeys } from '@prisma/client'; +import { Expose } from 'src/modules/prisma/prisma.interface'; +import { CursorPipe } from 'src/pipes/cursor.pipe'; +import { OptionalIntPipe } from 'src/pipes/optional-int.pipe'; +import { OrderByPipe } from 'src/pipes/order-by.pipe'; +import { WherePipe } from 'src/pipes/where.pipe'; +import { Scopes } from '../auth/scope.decorator'; +import { + CreateApiKeyDto, + ReplaceApiKeyDto, + UpdateApiKeyDto, +} from './api-keys.dto'; +import { ApiKeysService } from './api-keys.service'; + +@Controller('groups/:groupId/api-keys') +export class ApiKeyController { + constructor(private apiKeysService: ApiKeysService) {} + + @Post() + @Scopes('group-{groupId}:write-api-key') + async create( + @Param('groupId', ParseIntPipe) groupId: number, + @Body() data: CreateApiKeyDto, + ): Promise> { + return this.apiKeysService.createApiKey(groupId, data); + } + + @Get() + @Scopes('group-{groupId}:read-api-key') + async getAll( + @Param('groupId', ParseIntPipe) groupId: number, + @Query('skip', OptionalIntPipe) skip?: number, + @Query('take', OptionalIntPipe) take?: number, + @Query('cursor', CursorPipe) cursor?: Record, + @Query('where', WherePipe) where?: Record, + @Query('orderBy', OrderByPipe) orderBy?: Record, + ): Promise[]> { + return this.apiKeysService.getApiKeys(groupId, { + skip, + take, + orderBy, + cursor, + where, + }); + } + + @Get(':id') + @Scopes('group-{groupId}:read-api-key-{id}') + async get( + @Param('groupId', ParseIntPipe) groupId: number, + @Param('id', ParseIntPipe) id: number, + ): Promise> { + return this.apiKeysService.getApiKey(groupId, Number(id)); + } + + @Patch(':id') + @Scopes('group-{groupId}:write-api-key-{id}') + async update( + @Body() data: UpdateApiKeyDto, + @Param('groupId', ParseIntPipe) groupId: number, + @Param('id', ParseIntPipe) id: number, + ): Promise> { + return this.apiKeysService.updateApiKey(groupId, Number(id), data); + } + + @Put(':id') + @Scopes('group-{groupId}:write-api-key-{id}') + async replace( + @Body() data: ReplaceApiKeyDto, + @Param('groupId', ParseIntPipe) groupId: number, + @Param('id', ParseIntPipe) id: number, + ): Promise> { + return this.apiKeysService.updateApiKey(groupId, Number(id), data); + } + + @Delete(':id') + @Scopes('group-{groupId}:delete-api-key-{id}') + async remove( + @Param('groupId', ParseIntPipe) groupId: number, + @Param('id', ParseIntPipe) id: number, + ): Promise> { + return this.apiKeysService.deleteApiKey(groupId, Number(id)); + } +} diff --git a/src/modules/api-keys/api-keys.dto.ts b/src/modules/api-keys/api-keys.dto.ts new file mode 100644 index 000000000..c1e9b8862 --- /dev/null +++ b/src/modules/api-keys/api-keys.dto.ts @@ -0,0 +1,76 @@ +import { IsArray, IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class CreateApiKeyDto { + @IsString() + @IsOptional() + description?: string; + + @IsString() + @IsOptional() + name?: string; + + @IsArray() + @IsString({ each: true }) + @IsOptional() + scopes?: string[]; + + @IsArray() + @IsString({ each: true }) + @IsOptional() + ipRestrictions?: string[]; + + @IsArray() + @IsString({ each: true }) + @IsOptional() + referrerRestrictions?: string[]; +} + +export class UpdateApiKeyDto { + @IsString() + @IsOptional() + description?: string; + + @IsString() + @IsOptional() + name?: string; + + @IsArray() + @IsString({ each: true }) + @IsOptional() + scopes?: string[]; + + @IsArray() + @IsString({ each: true }) + @IsOptional() + ipRestrictions?: string[]; + + @IsArray() + @IsString({ each: true }) + @IsOptional() + referrerRestrictions?: string[]; +} + +export class ReplaceApiKeyDto { + @IsString() + @IsNotEmpty() + description: string; + + @IsString() + @IsNotEmpty() + name: string; + + @IsArray() + @IsString({ each: true }) + @IsNotEmpty() + scopes: string[]; + + @IsArray() + @IsString({ each: true }) + @IsNotEmpty() + ipRestrictions: string[]; + + @IsArray() + @IsString({ each: true }) + @IsNotEmpty() + referrerRestrictions: string[]; +} diff --git a/src/modules/api-keys/api-keys.module.ts b/src/modules/api-keys/api-keys.module.ts new file mode 100644 index 000000000..d6585263d --- /dev/null +++ b/src/modules/api-keys/api-keys.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { PrismaModule } from '../prisma/prisma.module'; +import { ApiKeyController } from './api-keys.controller'; +import { ApiKeysService } from './api-keys.service'; + +@Module({ + imports: [PrismaModule], + controllers: [ApiKeyController], + providers: [ApiKeysService], +}) +export class ApiKeysModule {} diff --git a/src/modules/api-keys/api-keys.service.ts b/src/modules/api-keys/api-keys.service.ts new file mode 100644 index 000000000..03c7f935b --- /dev/null +++ b/src/modules/api-keys/api-keys.service.ts @@ -0,0 +1,115 @@ +import { + HttpException, + HttpStatus, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { randomStringGenerator } from '@nestjs/common/utils/random-string-generator.util'; +import { + apiKeys, + apiKeysCreateInput, + apiKeysOrderByInput, + apiKeysUpdateInput, + apiKeysWhereInput, + apiKeysWhereUniqueInput, +} from '@prisma/client'; +import { Expose } from 'src/modules/prisma/prisma.interface'; +import { PrismaService } from '../prisma/prisma.service'; + +@Injectable() +export class ApiKeysService { + constructor(private prisma: PrismaService) {} + + async createApiKey( + groupId: number, + data: Omit, 'group'>, + ): Promise { + const apiKey = randomStringGenerator(); + return this.prisma.apiKeys.create({ + data: { ...data, apiKey, group: { connect: { id: groupId } } }, + }); + } + + async getApiKeys( + groupId: number, + params: { + skip?: number; + take?: number; + cursor?: apiKeysWhereUniqueInput; + where?: apiKeysWhereInput; + orderBy?: apiKeysOrderByInput; + }, + ): Promise[]> { + const { skip, take, cursor, where, orderBy } = params; + const apiKeys = await this.prisma.apiKeys.findMany({ + skip, + take, + cursor, + where: { ...where, group: { id: groupId } }, + orderBy, + }); + return apiKeys.map(group => this.prisma.expose(group)); + } + + async getApiKey( + groupId: number, + id: number, + ): Promise | null> { + const apiKey = await this.prisma.apiKeys.findOne({ + where: { id }, + }); + if (!apiKey) + throw new HttpException('ApiKey not found', HttpStatus.NOT_FOUND); + if (apiKey.groupId !== groupId) throw new UnauthorizedException(); + return this.prisma.expose(apiKey); + } + + async updateApiKey( + groupId: number, + id: number, + data: apiKeysUpdateInput, + ): Promise> { + const testApiKey = await this.prisma.apiKeys.findOne({ + where: { id }, + }); + if (!testApiKey) + throw new HttpException('ApiKey not found', HttpStatus.NOT_FOUND); + if (testApiKey.groupId !== groupId) throw new UnauthorizedException(); + const apiKey = await this.prisma.apiKeys.update({ + where: { id }, + data, + }); + return this.prisma.expose(apiKey); + } + + async replaceApiKey( + groupId: number, + id: number, + data: apiKeysCreateInput, + ): Promise> { + const testApiKey = await this.prisma.apiKeys.findOne({ + where: { id }, + }); + if (!testApiKey) + throw new HttpException('ApiKey not found', HttpStatus.NOT_FOUND); + if (testApiKey.groupId !== groupId) throw new UnauthorizedException(); + const apiKey = await this.prisma.apiKeys.update({ + where: { id }, + data, + }); + return this.prisma.expose(apiKey); + } + + async deleteApiKey(groupId: number, id: number): Promise> { + const testApiKey = await this.prisma.apiKeys.findOne({ + where: { id }, + }); + if (!testApiKey) + throw new HttpException('ApiKey not found', HttpStatus.NOT_FOUND); + if (testApiKey.groupId !== groupId) throw new UnauthorizedException(); + const apiKey = await this.prisma.apiKeys.delete({ + where: { id }, + }); + return this.prisma.expose(apiKey); + } +}