Skip to content

Commit

Permalink
feat: 🔥 [EXL-77] support secret management page (#315)
Browse files Browse the repository at this point in the history
  • Loading branch information
tal-rofe committed Sep 17, 2022
2 parents 3ecf9b3 + d751fcc commit 5a203b9
Show file tree
Hide file tree
Showing 69 changed files with 1,716 additions and 227 deletions.
31 changes: 21 additions & 10 deletions apps/backend/src/modules/database/client-secret.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,49 +7,60 @@ export class DBClientSecretService {
constructor(private prisma: PrismaService) {}

public async doesSecretBelongUser(userId: string, secretId: string) {
const secretDB = await this.prisma.clientSecret.findFirst({ where: { userId, id: secretId } });
const secretDB = await this.prisma.secret.findFirst({ where: { userId, id: secretId } });

return secretDB !== null;
}

public async deleteSecret(secretId: string) {
await this.prisma.clientSecret.delete({ where: { id: secretId } });
await this.prisma.secret.delete({ where: { id: secretId } });
}

public async revokeAllSecrets(userId: string) {
await this.prisma.clientSecret.deleteMany({ where: { userId } });
await this.prisma.secret.deleteMany({ where: { userId } });
}

public async refreshSecret(secretId: string, newSecret: string) {
await this.prisma.clientSecret.update({ where: { id: secretId }, data: { secret: newSecret } });
await this.prisma.secret.update({ where: { id: secretId }, data: { secret: newSecret } });
}

public async editSecretLabel(secretId: string, newLabel: string) {
await this.prisma.clientSecret.update({ where: { id: secretId }, data: { label: newLabel } });
await this.prisma.secret.update({ where: { id: secretId }, data: { label: newLabel } });
}

public async createSecret(userId: string, secret: string, label: string, expiration: Date | null) {
await this.prisma.clientSecret.create({ data: { secret, userId, label, expiration } });
public createSecret(userId: string, secret: string, label: string, expiration: Date | null) {
return this.prisma.secret.create({
data: { secret, userId, label, expiration },
select: { id: true },
});
}

public getSecrets(userId: string) {
return this.prisma.clientSecret.findMany({
return this.prisma.secret.findMany({
where: { userId },
select: {
id: true,
label: true,
createdAt: true,
expiration: true,
},
orderBy: { createdAt: 'asc' },
});
}

public async getSecretExpiration(secretId: string) {
const secret = await this.prisma.clientSecret.findUniqueOrThrow({
const secret = await this.prisma.secret.findUniqueOrThrow({
where: { id: secretId },
select: { expiration: true },
});

return secret.expiration;
}

public async isLabelAvailable(userId: string, label: string) {
const record = await this.prisma.secret.findFirst({
where: { userId, label },
});

return record === null;
}
}
2 changes: 1 addition & 1 deletion apps/backend/src/modules/database/group.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export class DBGroupService {

public async createGroup(userId: string) {
const createdGroup = await this.prisma.group.create({
data: { userId, policyIDs: [] },
data: { userId },
select: { id: true },
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Controller, Get, HttpCode, HttpStatus, Logger, Param } from '@nestjs/common';
import { QueryBus } from '@nestjs/cqrs';
import {
ApiBearerAuth,
ApiInternalServerErrorResponse,
ApiOkResponse,
ApiOperation,
ApiTags,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';

import { CurrentUserId } from '@/decorators/current-user-id.decorator';

import Routes from './secrets.routes';
import { AvailableLabelResponse } from './classes/responses';
import { AvailableLabelContract } from './queries/contracts/available-label.contract';

@ApiTags('Secrets')
@Controller(Routes.CONTROLLER)
export class AvailableLabelController {
private readonly logger = new Logger(AvailableLabelController.name);

constructor(private readonly queryBus: QueryBus) {}

@ApiOperation({ description: 'Check whether a provided label is availble' })
@ApiBearerAuth('access-token')
@ApiOkResponse({
description: 'Returns whether the provided label is available',
type: AvailableLabelResponse,
})
@ApiUnauthorizedResponse({
description: 'If access token is invalid or missing',
})
@ApiInternalServerErrorResponse({ description: 'If get availability status of the label' })
@Get(Routes.AVAILABLE_LABEL)
@HttpCode(HttpStatus.OK)
public async availableLabel(
@CurrentUserId() userId: string,
@Param('label') label: string,
): Promise<AvailableLabelResponse> {
this.logger.log(`Will try to get availability status of label: "${label}" with an Id: "${userId}"`);

const isAvailable = await this.queryBus.execute<AvailableLabelContract, boolean>(
new AvailableLabelContract(userId, label),
);

this.logger.log(`Successfully got availability status of label: "${label}" with an Id: "${userId}"`);

return {
isAvailable,
};
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IsISO8601, IsString, MinLength } from 'class-validator';
import { IsString, MaxLength, MinLength } from 'class-validator';
import { Transform } from 'class-transformer';
import { ApiProperty } from '@nestjs/swagger';

import { IsNullable } from '@/decorators/is-nullable.decorator';
Expand All @@ -9,16 +10,26 @@ export class CreateSecretDto {
@ApiProperty({ type: String, description: 'The label of the new secret', example: 'Yazif Secret' })
@IsString()
@MinLength(1)
@MaxLength(30)
readonly label!: string;

@ApiProperty({
type: String,
description: 'The expiration date of the secret in ISO8601 format. Null for no expiration',
type: Number,
description: 'The expiration date of the secret (in ms)',
nullable: true,
example: '2019-02-11',
example: 111122222,
})
@IsISO8601()
@IsFutureDate()
@IsNullable()
readonly expiration!: string | null;
@Transform(({ value }: { value: number | null }) => {
if (!value) {
return null;
}

const date = new Date(value);
const endOfDate = new Date(date.setHours(23, 59, 59, 999));

return endOfDate.getTime();
})
readonly expiration!: number | null;
}
23 changes: 15 additions & 8 deletions apps/backend/src/modules/user/modules/secrets/classes/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,22 @@ class UserSecretGetAll implements IUserSecretsGetAll {
type: Number,
example: 12345678,
})
public expiration!: number;
public expiration!: number | null;
}

export class CreateClientSecretResponse {
@ApiResponseProperty({
type: Number,
example: 12345678,
type: String,
example: '62e5362119bea07115434f4a',
})
public createdAt!: number;
}
public secretId!: string;

export class CreateClientSecretResponse {
@ApiResponseProperty({
type: String,
example:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',
})
public clientSecret!: string;
public secretValue!: string;
}

export class RefreshClientSecretResponse {
Expand All @@ -43,7 +43,7 @@ export class RefreshClientSecretResponse {
example:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',
})
public clientSecret!: string;
public secretValue!: string;
}

export class GetAllSecretsResponse {
Expand All @@ -52,3 +52,10 @@ export class GetAllSecretsResponse {
})
public secrets!: IUserSecretsGetAll[];
}

export class AvailableLabelResponse {
@ApiResponseProperty({
type: Boolean,
})
public isAvailable!: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export class CreateController {
): Promise<CreateClientSecretResponse> {
this.logger.log(`Will try to create a client secret with to user with an Id: "${userId}"`);

const secret = await this.queryBus.execute<CreateSecretContract, string>(
const secret = await this.queryBus.execute<CreateSecretContract, CreateClientSecretResponse>(
new CreateSecretContract(
userId,
userEmail,
Expand All @@ -52,8 +52,8 @@ export class CreateController {
),
);

this.logger.log('Successfully deleted a client secret');
this.logger.log('Successfully created a client secret');

return { clientSecret: secret };
return secret;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ import { ValidateIf, type ValidationOptions } from 'class-validator';

export function IsFutureDate(validationOptions?: ValidationOptions) {
return ValidateIf((_object, value) => {
if (typeof value !== 'string') {
if (typeof value !== 'number') {
return false;
}

const currentDate = new Date();
const inputDate = new Date(value);

return !isNaN(inputDate.getTime()) && inputDate >= currentDate;
return value >= currentDate.getTime();
}, validationOptions);
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,6 @@ export class DeleteController {

await this.commandBus.execute<RevokeSecretsContract, void>(new RevokeSecretsContract(userId));

this.logger.log('Successfully deleted a client secret');
this.logger.log("Successfully deleted revoked user's secrets");
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export interface IUserSecretsGetAll {
readonly id: string;
readonly label: string;
readonly expiration: number;
readonly createdAt: number;
readonly expiration: number | null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class AvailableLabelContract {
constructor(public readonly userId: string, public readonly label: string) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { QueryHandler, type IQueryHandler } from '@nestjs/cqrs';

import { DBClientSecretService } from '@/modules/database/client-secret.service';

import { AvailableLabelContract } from '../contracts/available-label.contract';

@QueryHandler(AvailableLabelContract)
export class AvailableLabelHandler implements IQueryHandler<AvailableLabelContract> {
constructor(private readonly dbClientSecretsService: DBClientSecretService) {}

execute(contract: AvailableLabelContract) {
return this.dbClientSecretsService.isLabelAvailable(contract.userId, contract.label);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ export class CreateSecretHandler implements IQueryHandler<CreateSecretContract>

const expirationDate = contract.expiration ? new Date(contract.expiration) : null;

await this.dbClientSecretService.createSecret(
const createdSecret = await this.dbClientSecretService.createSecret(
contract.userId,
secret,
contract.label,
expirationDate,
);

return secret;
return { secretId: createdSecret.id, secretValue: secret };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export class GetAllSecretsHandler implements IQueryHandler<GetAllSecretsContract

return secrets.map((secret) => ({
...secret,
createdAt: secret.createdAt.getTime(),
expiration: secret.expiration ? secret.expiration.getTime() : null,
}));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { AvailableLabelHandler } from './available-label.handler';
import { CreateSecretHandler } from './create-secret.handler';
import { GetAllSecretsHandler } from './get-all-secrets.handler';
import { RefreshSecretHandler } from './refresh-secret.handler';

export const QueryHandlers = [CreateSecretHandler, RefreshSecretHandler, GetAllSecretsHandler];
export const QueryHandlers = [
CreateSecretHandler,
RefreshSecretHandler,
GetAllSecretsHandler,
AvailableLabelHandler,
];
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export class RefreshSecretController {
this.logger.log('Successfully refreshed a client secret');

return {
clientSecret: secret,
secretValue: secret,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { JwtModule } from '@nestjs/jwt';

import { AvailableLabelController } from './available-label.controller';
import { CommandHandlers } from './commands/handlers';
import { CreateController } from './create.controller';
import { DeleteController } from './delete.controller';
Expand All @@ -20,6 +21,7 @@ import { SecretsService } from './secrets.service';
CreateController,
EditSecretController,
GetAllController,
AvailableLabelController,
],
providers: [...CommandHandlers, ...QueryHandlers, BelongingSecretGuard, SecretsService],
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const Routes = {
REFRSH_SECRET: 'refresh-secret/:secret_id',
EDIT_LABEL: 'edit-label/:secret_id',
GET_ALL: '',
AVAILABLE_LABEL: ':label',
};

export default Routes;
2 changes: 1 addition & 1 deletion apps/frontend/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ module.exports = {
},
overrides: [
{
files: ['./src/assets/icons.ts', './src/data/**/*.ts'],
files: ['./src/assets/icons.ts', './src/data/**/*.ts', './src/i18n/en.ts'],
rules: {
'max-lines': 'off',
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
---
to: src/components/ui/V<%= h.changeCase.pascalCase(name.toLowerCase()) %>/V<%= h.changeCase.pascalCase(name.toLowerCase()) %>.module.scss
to: src/components/ui/ED<%= h.changeCase.pascalCase(name.toLowerCase()) %>/ED<%= h.changeCase.pascalCase(name.toLowerCase()) %>.module.scss
---
15 changes: 0 additions & 15 deletions apps/frontend/_templates/component/new-ui/component.test.tsx.t

This file was deleted.

Loading

0 comments on commit 5a203b9

Please sign in to comment.