Skip to content

Commit

Permalink
refactor(auth): use @guard instead of authService.audit()
Browse files Browse the repository at this point in the history
  • Loading branch information
Nictheboy committed Aug 2, 2024
1 parent 60883b0 commit 2c8de27
Show file tree
Hide file tree
Showing 39 changed files with 1,047 additions and 841 deletions.
4 changes: 2 additions & 2 deletions jest.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
"!src/*/**/*.dto.ts",
"!src/*/**/*.es-doc.ts",
"!src/*/**/*.enum.ts",
"!src/*/**/*.deprecated.entity.ts",
"!src/common/config/configuration.ts"
"!src/common/config/configuration.ts",
"!src/auth/definitions.ts"
],
"testTimeout": 60000
}
83 changes: 27 additions & 56 deletions src/answer/answer.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,20 @@ import {
Post,
Put,
Query,
UseFilters,
UseInterceptors,
} from '@nestjs/common';
import { AttitudeTypeDto } from '../attitude/DTO/attitude.dto';
import { UpdateAttitudeResponseDto } from '../attitude/DTO/update-attitude.dto';
import { AuthService } from '../auth/auth.service';
import { AuthorizedAction } from '../auth/definitions';
import {
AuthToken,
CurrentUserOwnResource,
Guard,
ResourceId,
ResourceOwnerIdGetter,
} from '../auth/guard.decorator';
import { UserId } from '../auth/user-id.decorator';
import { BaseResponseDto } from '../common/DTO/base-response.dto';
import { PageDto } from '../common/DTO/page.dto';
import { BaseErrorExceptionFilter } from '../common/error/error-filter';
import { TokenValidateInterceptor } from '../common/interceptor/token-validate.interceptor';
import { QuestionsService } from '../questions/questions.service';
import { CreateAnswerResponseDto } from './DTO/create-answer.dto';
import { GetAnswerDetailResponseDto } from './DTO/get-answer-detail.dto';
Expand All @@ -36,8 +32,6 @@ import { UpdateAnswerRequestDto } from './DTO/update-answer.dto';
import { AnswerService } from './answer.service';

@Controller('/questions/:question_id/answers')
@UseFilters(BaseErrorExceptionFilter)
@UseInterceptors(TokenValidateInterceptor)
export class AnswerController {
constructor(
private readonly authService: AuthService,
Expand All @@ -50,22 +44,22 @@ export class AnswerController {
return this.answerService.getCreatedByIdAcrossQuestions(answerId);
}

@ResourceOwnerIdGetter('question')
async getQuestionOwner(questionId: number): Promise<number | undefined> {
return this.questionsService.getQuestionCreatedById(questionId);
}

@Get('/')
@Guard('enumerate-answers', 'question')
async getQuestionAnswers(
@Param('question_id', ParseIntPipe) questionId: number,
@Param('question_id', ParseIntPipe) @ResourceId() questionId: number,
@Query()
{ page_start: pageStart, page_size: pageSize }: PageDto,
@Headers('Authorization') @AuthToken() auth: string | undefined,
@UserId() userId: number | undefined,
@Ip() ip: string,
@Headers('User-Agent') userAgent: string,
): Promise<GetAnswersResponseDto> {
let userId: number | undefined;
try {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
userId = this.authService.verify(auth).userId;
} catch {
// The user is not logged in.
}
const [answers, page] = await this.answerService.getQuestionAnswers(
questionId,
pageStart,
Expand All @@ -85,14 +79,14 @@ export class AnswerController {
}

@Post('/')
@Guard(AuthorizedAction.create, 'answer')
@Guard('create', 'answer')
@CurrentUserOwnResource()
async answerQuestion(
@Param('question_id', ParseIntPipe) questionId: number,
@Body('content') content: string,
@Headers('Authorization') @AuthToken() auth: string | undefined,
@UserId(true) userId: number,
): Promise<CreateAnswerResponseDto> {
const userId = this.authService.verify(auth).userId;
const answerId = await this.answerService.createAnswer(
questionId,
userId,
Expand All @@ -108,20 +102,15 @@ export class AnswerController {
}

@Get('/:answer_id')
@Guard('query', 'answer')
async getAnswerDetail(
@Param('question_id', ParseIntPipe) questionId: number,
@Param('answer_id', ParseIntPipe) answerId: number,
@Param('answer_id', ParseIntPipe) @ResourceId() answerId: number,
@Headers('Authorization') @AuthToken() auth: string | undefined,
@UserId() userId: number | undefined,
@Ip() ip: string,
@Headers('User-Agent') userAgent: string,
): Promise<GetAnswerDetailResponseDto> {
let userId;
try {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
userId = this.authService.verify(auth).userId;
} catch {
// The user is not logged in.
}
const answerDto = await this.answerService.getAnswerDto(
questionId,
answerId,
Expand All @@ -146,14 +135,14 @@ export class AnswerController {
}

@Put('/:answer_id')
@Guard(AuthorizedAction.modify, 'answer')
@Guard('modify', 'answer')
async updateAnswer(
@Param('question_id', ParseIntPipe) questionId: number,
@Param('answer_id', ParseIntPipe) @ResourceId() answerId: number,
@Body() { content }: UpdateAnswerRequestDto,
@Headers('Authorization') @AuthToken() auth: string | undefined,
@UserId(true) userId: number,
): Promise<BaseResponseDto> {
const userId = this.authService.verify(auth).userId;
await this.answerService.updateAnswer(
questionId,
answerId,
Expand All @@ -167,31 +156,25 @@ export class AnswerController {
}

@Delete('/:answer_id')
@Guard(AuthorizedAction.delete, 'answer')
@Guard('delete', 'answer')
async deleteAnswer(
@Param('question_id', ParseIntPipe) questionId: number,
@Param('answer_id', ParseIntPipe) @ResourceId() answerId: number,
@Headers('Authorization') @AuthToken() auth: string | undefined,
@UserId(true) userId: number,
): Promise<void> {
const userId = this.authService.verify(auth).userId;
await this.answerService.deleteAnswer(questionId, answerId, userId);
}

@Post('/:answer_id/attitudes')
@Guard('attitude', 'answer')
async updateAttitudeToAnswer(
@Param('question_id', ParseIntPipe) questionId: number,
@Param('answer_id', ParseIntPipe) answerId: number,
@Param('answer_id', ParseIntPipe) @ResourceId() answerId: number,
@Body() { attitude_type: attitudeType }: AttitudeTypeDto,
@Headers('Authorization') @AuthToken() auth: string | undefined,
@UserId(true) userId: number,
): Promise<UpdateAttitudeResponseDto> {
const userId = this.authService.verify(auth).userId;
this.authService.audit(
auth,
AuthorizedAction.other,
await this.answerService.getCreatedById(questionId, answerId),
'answer/attitude',
answerId,
);
const attitudes = await this.answerService.setAttitudeToAnswer(
questionId,
answerId,
Expand All @@ -208,19 +191,13 @@ export class AnswerController {
}

@Put('/:answer_id/favorite')
@Guard('favorite', 'answer')
async favoriteAnswer(
@Param('question_id', ParseIntPipe) questionId: number,
@Param('answer_id', ParseIntPipe) answerId: number,
@Param('answer_id', ParseIntPipe) @ResourceId() answerId: number,
@Headers('Authorization') @AuthToken() auth: string | undefined,
@UserId(true) userId: number,
): Promise<BaseResponseDto> {
const userId = this.authService.verify(auth).userId;
this.authService.audit(
auth,
AuthorizedAction.other,
await this.answerService.getCreatedById(questionId, answerId),
'answer/favourite',
answerId,
);
await this.answerService.favoriteAnswer(questionId, answerId, userId);
return {
code: 200,
Expand All @@ -229,19 +206,13 @@ export class AnswerController {
}

@Delete('/:answer_id/favorite')
@Guard('unfavorite', 'answer')
async unfavoriteAnswer(
@Param('question_id', ParseIntPipe) questionId: number,
@Param('answer_id', ParseIntPipe) answerId: number,
@Param('answer_id', ParseIntPipe) @ResourceId() answerId: number,
@Headers('Authorization') @AuthToken() auth: string | undefined,
@UserId(true) userId: number,
): Promise<void> {
const userId = this.authService.verify(auth).userId;
this.authService.audit(
auth,
AuthorizedAction.other,
await this.answerService.getCreatedById(questionId, answerId),
'answer/favourite',
answerId,
);
await this.answerService.unfavoriteAnswer(questionId, answerId, userId);
}
}
21 changes: 18 additions & 3 deletions src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { Module, ValidationPipe } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { APP_PIPE } from '@nestjs/core';
import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
import { ServeStaticModule } from '@nestjs/serve-static';
import { AnswerModule } from './answer/answer.module';
import { AttachmentsModule } from './attachments/attachments.module';
import { AvatarsModule } from './avatars/avatars.module';
import { CommentsModule } from './comments/comment.module';
import configuration from './common/config/configuration';
import { BaseErrorExceptionFilter } from './common/error/error-filter';
import { EnsureGuardInterceptor } from './common/interceptor/ensure-guard.interceptor';
import { TokenValidateInterceptor } from './common/interceptor/token-validate.interceptor';
import { GroupsModule } from './groups/groups.module';
import { MaterialbundlesModule } from './materialbundles/materialbundles.module';
import { MaterialsModule } from './materials/materials.module';
import { QuestionsModule } from './questions/questions.module';
import { UsersModule } from './users/users.module';
import { AttachmentsModule } from './attachments/attachments.module';
import { MaterialbundlesModule } from './materialbundles/materialbundles.module';
@Module({
imports: [
ConfigModule.forRoot({ load: [configuration] }),
Expand All @@ -38,6 +41,18 @@ import { MaterialbundlesModule } from './materialbundles/materialbundles.module'
disableErrorMessages: false,
}),
},
{
provide: APP_FILTER,
useClass: BaseErrorExceptionFilter,
},
{
provide: APP_INTERCEPTOR,
useClass: TokenValidateInterceptor,
},
{
provide: APP_INTERCEPTOR,
useClass: EnsureGuardInterceptor,
},
],
})
export class AppModule {}
18 changes: 6 additions & 12 deletions src/attachments/attachments.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,13 @@ import {
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { AuthService } from '../auth/auth.service';
import { AuthorizedAction } from '../auth/definitions';
import { BaseErrorExceptionFilter } from '../common/error/error-filter';
import { attachmentTypeDto } from './DTO/attachments.dto';
import { getAttachmentResponseDto } from './DTO/get-attachment.dto';
import { uploadAttachmentDto } from './DTO/upload-attachment.dto';
import { AttachmentsService } from './attachments.service';
@UsePipes(new ValidationPipe())
@UseFilters(new BaseErrorExceptionFilter())
import { AuthToken, Guard } from '../auth/guard.decorator';

@Controller('attachments')
export class AttachmentsController {
constructor(
Expand All @@ -40,19 +39,12 @@ export class AttachmentsController {

@Post()
@UseInterceptors(FileInterceptor('file'))
@Guard('create', 'attachment')
async uploadAttachment(
@Body() { type }: attachmentTypeDto,
@UploadedFile() file: Express.Multer.File,
@Headers('Authorization') auth: string | undefined,
@Headers('Authorization') @AuthToken() auth: string | undefined,
): Promise<uploadAttachmentDto> {
const uploaderId = this.authService.verify(auth).userId;
this.authService.audit(
auth,
AuthorizedAction.create,
uploaderId,
'attachment',
undefined,
);
const attachmentId = await this.attachmentsService.uploadAttachment(
type,
file,
Expand All @@ -67,8 +59,10 @@ export class AttachmentsController {
}

@Get('/:attachmentId')
@Guard('query', 'attachment')
async getAttachmentDetail(
@Param('attachmentId', ParseIntPipe) id: number,
@Headers('Authorization') @AuthToken() auth: string | undefined,
): Promise<getAttachmentResponseDto> {
const attachment = await this.attachmentsService.getAttachment(id);
return {
Expand Down
6 changes: 2 additions & 4 deletions src/auth/auth.error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

import { BaseError } from '../common/error/base-error';
import { AuthorizedAction, authorizedActionToString } from './definitions';
import { AuthorizedAction } from './definitions';

export class AuthenticationRequiredError extends BaseError {
constructor() {
Expand Down Expand Up @@ -37,9 +37,7 @@ export class PermissionDeniedError extends BaseError {
) {
super(
'PermissionDeniedError',
`The attempt to perform action '${authorizedActionToString(
action,
)}' on resource (resourceOwnerId: ${resourceOwnerId}, resourceType: ${resourceType}, resourceId: ${resourceId}) is not permitted by the given token.`,
`The attempt to perform action '${action}' on resource (resourceOwnerId: ${resourceOwnerId}, resourceType: ${resourceType}, resourceId: ${resourceId}) is not permitted by the given token.`,
403,
);
}
Expand Down
15 changes: 0 additions & 15 deletions src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,6 @@
*
*/

/*
IMPORTANT NOTICE:
If you have modified this file, please run the following linux command:
./node_modules/.bin/ts-json-schema-generator \
--path 'src/auth/auth.service.ts' \
--type 'TokenPayload' \
> src/auth/token-payload.schema.json
to update the schema file, which is used in validating the token payload.
*/

import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import Ajv from 'ajv';
Expand Down
Loading

0 comments on commit 2c8de27

Please sign in to comment.