Skip to content
This repository has been archived by the owner on Apr 19, 2023. It is now read-only.

Commit

Permalink
✨ Add group API keys module
Browse files Browse the repository at this point in the history
  • Loading branch information
AnandChowdhary committed Oct 30, 2020
1 parent ffd8fdf commit f133777
Show file tree
Hide file tree
Showing 5 changed files with 300 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -39,6 +40,7 @@ import { UsersModule } from './modules/users/users.module';
EmailsModule,
GroupsModule,
MultiFactorAuthenticationModule,
ApiKeysModule,
ApprovedSubnetsModule,
GeolocationModule,
],
Expand Down
96 changes: 96 additions & 0 deletions src/modules/api-keys/api-keys.controller.ts
Original file line number Diff line number Diff line change
@@ -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<Expose<apiKeys>> {
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<string, number | string>,
@Query('where', WherePipe) where?: Record<string, number | string>,
@Query('orderBy', OrderByPipe) orderBy?: Record<string, 'asc' | 'desc'>,
): Promise<Expose<apiKeys>[]> {
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<Expose<apiKeys>> {
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<Expose<apiKeys>> {
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<Expose<apiKeys>> {
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<Expose<apiKeys>> {
return this.apiKeysService.deleteApiKey(groupId, Number(id));
}
}
76 changes: 76 additions & 0 deletions src/modules/api-keys/api-keys.dto.ts
Original file line number Diff line number Diff line change
@@ -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[];
}
11 changes: 11 additions & 0 deletions src/modules/api-keys/api-keys.module.ts
Original file line number Diff line number Diff line change
@@ -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 {}
115 changes: 115 additions & 0 deletions src/modules/api-keys/api-keys.service.ts
Original file line number Diff line number Diff line change
@@ -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<Omit<apiKeysCreateInput, 'apiKey'>, 'group'>,
): Promise<apiKeys> {
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<Expose<apiKeys>[]> {
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<apiKeys>(group));
}

async getApiKey(
groupId: number,
id: number,
): Promise<Expose<apiKeys> | 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<apiKeys>(apiKey);
}

async updateApiKey(
groupId: number,
id: number,
data: apiKeysUpdateInput,
): Promise<Expose<apiKeys>> {
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<apiKeys>(apiKey);
}

async replaceApiKey(
groupId: number,
id: number,
data: apiKeysCreateInput,
): Promise<Expose<apiKeys>> {
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<apiKeys>(apiKey);
}

async deleteApiKey(groupId: number, id: number): Promise<Expose<apiKeys>> {
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<apiKeys>(apiKey);
}
}

0 comments on commit f133777

Please sign in to comment.