Skip to content

Commit

Permalink
feat(api): Add slug in entities (keyshade-xyz#415)
Browse files Browse the repository at this point in the history
  • Loading branch information
rajdip-b authored and Kiranchaudhary537 committed Oct 13, 2024
1 parent 27e9a66 commit ef3703d
Show file tree
Hide file tree
Showing 107 changed files with 8,041 additions and 6,243 deletions.
510 changes: 272 additions & 238 deletions apps/api/src/api-key/api-key.e2e.spec.ts

Large diffs are not rendered by default.

24 changes: 15 additions & 9 deletions apps/api/src/api-key/controller/api-key.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,24 @@ export class ApiKeyController {
return this.apiKeyService.createApiKey(user, dto)
}

@Put(':id')
@Put(':apiKeySlug')
@RequiredApiKeyAuthorities(Authority.UPDATE_API_KEY)
async updateApiKey(
@CurrentUser() user: User,
@Body() dto: UpdateApiKey,
@Param('id') id: string
@Param('apiKeySlug') apiKeySlug: string
) {
return this.apiKeyService.updateApiKey(user, id, dto)
return this.apiKeyService.updateApiKey(user, apiKeySlug, dto)
}

@Delete(':id')
@Delete(':apiKeySlug')
@RequiredApiKeyAuthorities(Authority.DELETE_API_KEY)
@HttpCode(204)
async deleteApiKey(@CurrentUser() user: User, @Param('id') id: string) {
return this.apiKeyService.deleteApiKey(user, id)
async deleteApiKey(
@CurrentUser() user: User,
@Param('apiKeySlug') apiKeySlug: string
) {
return this.apiKeyService.deleteApiKey(user, apiKeySlug)
}

@Get('/')
Expand All @@ -63,10 +66,13 @@ export class ApiKeyController {
)
}

@Get(':id')
@Get(':apiKeySlug')
@RequiredApiKeyAuthorities(Authority.READ_API_KEY)
async getApiKey(@CurrentUser() user: User, @Param('id') id: string) {
return this.apiKeyService.getApiKeyById(user, id)
async getApiKey(
@CurrentUser() user: User,
@Param('apiKeySlug') apiKeySlug: string
) {
return this.apiKeyService.getApiKeyBySlug(user, apiKeySlug)
}

@Get('/access/live-updates')
Expand Down
123 changes: 85 additions & 38 deletions apps/api/src/api-key/service/api-key.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,36 @@ import {
} from '@nestjs/common'
import { PrismaService } from '@/prisma/prisma.service'
import { CreateApiKey } from '../dto/create.api-key/create.api-key'
import { addHoursToDate } from '@/common/add-hours-to-date'
import { generateApiKey } from '@/common/api-key-generator'
import { toSHA256 } from '@/common/to-sha256'
import { UpdateApiKey } from '../dto/update.api-key/update.api-key'
import { ApiKey, User } from '@prisma/client'
import { limitMaxItemsPerPage } from '@/common/limit-max-items-per-page'
import generateEntitySlug from '@/common/slug-generator'
import { generateApiKey, toSHA256 } from '@/common/cryptography'
import { addHoursToDate, limitMaxItemsPerPage } from '@/common/util'

@Injectable()
export class ApiKeyService {
private readonly logger = new Logger(ApiKeyService.name)

constructor(private readonly prisma: PrismaService) {}

private apiKeySelect = {
id: true,
expiresAt: true,
name: true,
slug: true,
authorities: true,
createdAt: true,
updatedAt: true
}

/**
* Creates a new API key for the given user.
*
* @throws `ConflictException` if the API key already exists.
* @param user The user to create the API key for.
* @param dto The data to create the API key with.
* @returns The created API key.
*/
async createApiKey(user: User, dto: CreateApiKey) {
await this.isApiKeyUnique(user, dto.name)

Expand All @@ -27,6 +44,7 @@ export class ApiKeyService {
const apiKey = await this.prisma.apiKey.create({
data: {
name: dto.name,
slug: await generateEntitySlug(dto.name, 'API_KEY', this.prisma),
value: hashedApiKey,
authorities: dto.authorities
? {
Expand All @@ -50,15 +68,29 @@ export class ApiKeyService {
}
}

async updateApiKey(user: User, apiKeyId: string, dto: UpdateApiKey) {
/**
* Updates an existing API key of the given user.
*
* @throws `ConflictException` if the API key name already exists.
* @throws `NotFoundException` if the API key with the given slug does not exist.
* @param user The user to update the API key for.
* @param apiKeySlug The slug of the API key to update.
* @param dto The data to update the API key with.
* @returns The updated API key.
*/
async updateApiKey(
user: User,
apiKeySlug: ApiKey['slug'],
dto: UpdateApiKey
) {
await this.isApiKeyUnique(user, dto.name)

const apiKey = await this.prisma.apiKey.findUnique({
where: {
id: apiKeyId,
userId: user.id
slug: apiKeySlug
}
})
const apiKeyId = apiKey.id

if (!apiKey) {
throw new NotFoundException(`API key with id ${apiKeyId} not found`)
Expand All @@ -71,66 +103,81 @@ export class ApiKeyService {
},
data: {
name: dto.name,
slug: dto.name
? await generateEntitySlug(dto.name, 'API_KEY', this.prisma)
: apiKey.slug,
authorities: {
set: dto.authorities ? dto.authorities : apiKey.authorities
},
expiresAt: dto.expiresAfter
? addHoursToDate(dto.expiresAfter)
: undefined
},
select: {
id: true,
expiresAt: true,
name: true,
authorities: true,
createdAt: true,
updatedAt: true
}
select: this.apiKeySelect
})

this.logger.log(`User ${user.id} updated API key ${apiKeyId}`)

return updatedApiKey
}

async deleteApiKey(user: User, apiKeyId: string) {
/**
* Deletes an API key of the given user.
*
* @throws `NotFoundException` if the API key with the given slug does not exist.
* @param user The user to delete the API key for.
* @param apiKeySlug The slug of the API key to delete.
*/
async deleteApiKey(user: User, apiKeySlug: ApiKey['slug']) {
try {
await this.prisma.apiKey.delete({
where: {
id: apiKeyId,
slug: apiKeySlug,
userId: user.id
}
})
} catch (error) {
throw new NotFoundException(`API key with id ${apiKeyId} not found`)
throw new NotFoundException(`API key with id ${apiKeySlug} not found`)
}

this.logger.log(`User ${user.id} deleted API key ${apiKeyId}`)
this.logger.log(`User ${user.id} deleted API key ${apiKeySlug}`)
}

async getApiKeyById(user: User, apiKeyId: string) {
/**
* Retrieves an API key of the given user by slug.
*
* @throws `NotFoundException` if the API key with the given slug does not exist.
* @param user The user to retrieve the API key for.
* @param apiKeySlug The slug of the API key to retrieve.
* @returns The API key with the given slug.
*/
async getApiKeyBySlug(user: User, apiKeySlug: ApiKey['slug']) {
const apiKey = await this.prisma.apiKey.findUnique({
where: {
id: apiKeyId,
slug: apiKeySlug,
userId: user.id
},
select: {
id: true,
expiresAt: true,
name: true,
authorities: true,
createdAt: true,
updatedAt: true
}
select: this.apiKeySelect
})

if (!apiKey) {
throw new NotFoundException(`API key with id ${apiKeyId} not found`)
throw new NotFoundException(`API key with id ${apiKeySlug} not found`)
}

return apiKey
}

/**
* Retrieves all API keys of the given user.
*
* @param user The user to retrieve the API keys for.
* @param page The page number to retrieve.
* @param limit The maximum number of items to retrieve per page.
* @param sort The column to sort by.
* @param order The order to sort by.
* @param search The search string to filter the API keys by.
* @returns The API keys of the given user, filtered by the search string.
*/
async getAllApiKeysOfUser(
user: User,
page: number,
Expand All @@ -151,17 +198,17 @@ export class ApiKeyService {
orderBy: {
[sort]: order
},
select: {
id: true,
expiresAt: true,
name: true,
authorities: true,
createdAt: true,
updatedAt: true
}
select: this.apiKeySelect
})
}

/**
* Checks if an API key with the given name already exists for the given user.
*
* @throws `ConflictException` if the API key already exists.
* @param user The user to check for.
* @param apiKeyName The name of the API key to check.
*/
private async isApiKeyUnique(user: User, apiKeyName: string) {
let apiKey: ApiKey | null = null

Expand Down
2 changes: 1 addition & 1 deletion apps/api/src/auth/controller/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ import { GoogleOAuthStrategyFactory } from '@/config/factory/google/google-strat
import { GitlabOAuthStrategyFactory } from '@/config/factory/gitlab/gitlab-strategy.factory'
import { Response } from 'express'
import { AuthProvider } from '@prisma/client'
import setCookie from '@/common/set-cookie'
import {
sendOAuthFailureRedirect,
sendOAuthSuccessRedirect
} from '@/common/redirect'
import { setCookie } from '@/common/util'

@Controller('auth')
export class AuthController {
Expand Down
8 changes: 8 additions & 0 deletions apps/api/src/auth/guard/admin/admin.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ import { Observable } from 'rxjs'

@Injectable()
export class AdminGuard implements CanActivate {
/**
* This guard will check if the request's user is an admin.
* If the user is an admin, then the canActivate function will return true.
* If the user is not an admin, then the canActivate function will return false.
*
* @param context The ExecutionContext for the request.
* @returns A boolean indicating whether or not the request's user is an admin.
*/
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
Expand Down
19 changes: 19 additions & 0 deletions apps/api/src/auth/guard/api-key/api-key.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,25 @@ import { IS_PUBLIC_KEY } from '@/decorators/public.decorator'
export class ApiKeyGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}

/**
* This method will check if the user is authenticated via an API key,
* and if the API key has the required authorities for the route.
*
* If the user is not authenticated via an API key, or if the API key does not have the required authorities,
* then the canActivate method will return true.
*
* If the user is authenticated via an API key, and the API key has the required authorities,
* then the canActivate method will return true.
*
* If the user is authenticated via an API key, but the API key does not have the required authorities,
* then the canActivate method will throw an UnauthorizedException.
*
* If the user is authenticated via an API key, but the API key is forbidden for the route,
* then the canActivate method will throw an UnauthorizedException.
*
* @param context The ExecutionContext for the request.
* @returns A boolean indicating whether or not the user is authenticated via an API key and has the required authorities for the route.
*/
canActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
Expand Down
15 changes: 12 additions & 3 deletions apps/api/src/auth/guard/auth/auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ import { IS_PUBLIC_KEY } from '@/decorators/public.decorator'
import { PrismaService } from '@/prisma/prisma.service'
import { ONBOARDING_BYPASSED } from '@/decorators/bypass-onboarding.decorator'
import { AuthenticatedUserContext } from '../../auth.types'
import { toSHA256 } from '@/common/to-sha256'
import { EnvSchema } from '@/common/env/env.schema'
import { CacheService } from '@/cache/cache.service'
import { toSHA256 } from '@/common/cryptography'

const X_E2E_USER_EMAIL = 'x-e2e-user-email'
const X_KEYSHADE_TOKEN = 'x-keyshade-token'
Expand All @@ -25,10 +25,19 @@ export class AuthGuard implements CanActivate {
constructor(
private readonly jwtService: JwtService,
private readonly prisma: PrismaService,
private reflector: Reflector,
private cache: CacheService
private readonly reflector: Reflector,
private readonly cache: CacheService
) {}

/**
* This method is called by NestJS every time an HTTP request is made to an endpoint
* that is protected by this guard. It checks if the request is authenticated and if
* the user is active. If the user is not active, it throws an UnauthorizedException.
* If the onboarding is not finished, it throws an UnauthorizedException.
* @param context The ExecutionContext object that contains information about the
* request.
* @returns A boolean indicating if the request is authenticated and the user is active.
*/
async canActivate(context: ExecutionContext): Promise<boolean> {
// Get the kind of route. Routes marked with the @Public() decorator are public.
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
Expand Down
Loading

0 comments on commit ef3703d

Please sign in to comment.