Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): Add slug in entities #415

Merged
merged 5 commits into from
Sep 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading