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/add public metadata #29

Merged
merged 7 commits into from
Jan 22, 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
26 changes: 25 additions & 1 deletion src/auth/guard/local.guard.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,28 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { AllowPublicToken } from 'app/decorator';

// JWT Guard returns 401 default
export class LocalGuard extends AuthGuard('local') {}
@Injectable()
export class LocalGuard extends AuthGuard('local') {
constructor(private reflector: Reflector) {
super();
}

async canActivate(context: ExecutionContext) {
const allowPublic = this.reflector.get<boolean>(
AllowPublicToken,
context.getHandler(),
);

if (allowPublic) {
try {
return (await super.canActivate(context)) as boolean;
} catch (err) {
return true;
}
}
return (await super.canActivate(context)) as boolean;
}
}
1 change: 1 addition & 0 deletions src/decorator/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './get-user.decorator';
export * from './pagination.decorator';
export * from './role.decorator';
export * from './public.decorator';
80 changes: 80 additions & 0 deletions src/decorator/multiple-definition.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { HttpStatus, Type, applyDecorators } from '@nestjs/common';
import {
ApiBody,
ApiExtraModels,
ApiResponse,
getSchemaPath,
} from '@nestjs/swagger';

interface ResponseReference {
classRef: Type;
example: any;
isArray?: boolean;
description?: string;
}

export type MultipleResponseOptions = Record<string, ResponseReference>;

// Use when Swagger requires to show multiple response
export const ApiMultipleResponse = (
statusCode: HttpStatus | number,
options: MultipleResponseOptions,
) => {
const models = Object.values(options).map((option) => {
return option.classRef;
});

const responseExample = {};
for (const [key, option] of Object.entries(options)) {
responseExample[key] = {
value: option.isArray ? [option.example] : option.example,
};
}

return applyDecorators(
ApiExtraModels(...models),
ApiResponse({
status: statusCode,
content: {
'application/json': {
schema: {
oneOf: models.map((model) => {
return {
$ref: getSchemaPath(model),
};
}),
},
examples: responseExample,
},
},
}),
);
};

// Use when Swagger requires to show multiple body
export const ApiMultipleBody = (options: MultipleResponseOptions) => {
const models = Object.values(options).map((option) => {
return option.classRef;
});

const responseExample = {};
for (const [key, option] of Object.entries(options)) {
responseExample[key] = {
value: option.isArray ? [option.example] : option.example,
};
}

return applyDecorators(
ApiExtraModels(...models),
ApiBody({
schema: {
oneOf: models.map((model) => {
return {
$ref: getSchemaPath(model),
};
}),
},
examples: responseExample,
}),
);
};
8 changes: 8 additions & 0 deletions src/decorator/public.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { SetMetadata } from '@nestjs/common';

/**
* Set metadata as allow-public true
*
*/
export const AllowPublicToken = 'allow-public';
export const AllowPublic = () => SetMetadata(AllowPublicToken, true);
10 changes: 5 additions & 5 deletions src/judge/judge.controller.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ describe('/judge Judge Controller', () => {
// Test
describe('/languages GET', () => {
it('should throw if unauthenticated', async () => {
return request(app.getHttpServer()).get('/judge').expect(401);
return request(app.getHttpServer()).get('/judge/languages').expect(401);
});
it('should get language list', async () => {
return request(app.getHttpServer())
Expand All @@ -132,8 +132,8 @@ describe('/judge Judge Controller', () => {
});

describe('/ GET', () => {
it('should throw if unauthenticated', async () => {
return request(app.getHttpServer()).get('/judge').expect(401);
it('should allow unauthenticated', async () => {
return request(app.getHttpServer()).get('/judge').expect(200);
});
it('should get problem list', async () => {
return request(app.getHttpServer())
Expand All @@ -144,10 +144,10 @@ describe('/judge Judge Controller', () => {
});

describe('/:pid GET', () => {
it('should throw if unauthenticated', async () => {
it('should allow unauthenticated', async () => {
return request(app.getHttpServer())
.get(`/judge/${problemId}`)
.expect(401);
.expect(200);
});
it('should get problem info', async () => {
return request(app.getHttpServer())
Expand Down
20 changes: 16 additions & 4 deletions src/judge/judge.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,16 @@ import {
ParseIntPipe,
Patch,
Post,
Request,
UseGuards,
} from '@nestjs/common';
import { LocalGuard } from 'app/auth/guard';
import { GetUser, PaginateObject, Pagination } from 'app/decorator';
import {
AllowPublic,
GetUser,
PaginateObject,
Pagination,
} from 'app/decorator';
import {
JudgeFilter,
JudgeFilterObject,
Expand Down Expand Up @@ -44,19 +50,25 @@ export class JudgeController {
}

@Get('/')
@AllowPublic()
@JudgeDocs.ListProblem()
listProblem(
@JudgeFilter() filter: JudgeFilterObject,
@Pagination() paginate: PaginateObject,
@Request() req: Request,
) {
return this.judgeService.listProblem(filter, paginate);
return this.judgeService.listProblem(filter, paginate, req);
}

@Get('/:pid')
@AllowPublic()
@UseGuards(ProblemGuard)
@JudgeDocs.ReadProblem()
readProblem(@Param('pid', ParseIntPipe) pid: number) {
return this.judgeService.readProblem(pid);
readProblem(
@Param('pid', ParseIntPipe) pid: number,
@Request() req: Request,
) {
return this.judgeService.readProblem(pid, req);
}

@Post('/:pid/run')
Expand Down
110 changes: 103 additions & 7 deletions src/judge/judge.docs.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { applyDecorators } from '@nestjs/common';
import { HttpStatus, applyDecorators } from '@nestjs/common';
import {
ApiBadRequestResponse,
ApiBearerAuth,
Expand All @@ -10,17 +10,20 @@ import {
} from '@nestjs/swagger';

import { PaginationDocs } from 'app/decorator';
import { ApiMultipleResponse } from 'app/decorator/multiple-definition.decorator';
import { ProblemIssueDomain, SubmissionDomain } from 'domains';
import { SubmissionFilterDocs } from './decorator/submission-filter.decorator';
import {
CreateProblemIssueCommentResponse,
DeleteProblemIssueCommentResponse,
DeleteProblemIssueResponse,
GetLanguagesResponse,
ListProblemAuthenticatedResponse,
ListProblemIssueResponse,
ListProblemResponse,
ListProblemUnAuthenticatedResponse,
ListUserSubmissionRepsonse,
ReadProblemResponse,
ReadProblemAuthenticatedResponse,
ReadProblemUnauthenticatedResponse,
ReadPublicSubmissionResponse,
RunProblemResponse,
SubmitProblemResponse,
Expand All @@ -41,16 +44,109 @@ export class JudgeDocs {
public static ListProblem() {
return applyDecorators(
ApiOperation({ summary: '문제 리스트 출력' }),
ApiOkResponse({ type: ListProblemResponse, isArray: true }),
ApiMultipleResponse(HttpStatus.OK, {
Authenticated: {
classRef: ListProblemUnAuthenticatedResponse,
example: {
id: 11,
title: 'New Problem',
contributer: {
nickname: 'admin',
},
correct: 1,
total: 1,
correctionRate: '1.000',
status: 'SUCCESS',
},
isArray: true,
description: 'If authenticated response',
},
UnAuthenticated: {
classRef: ListProblemAuthenticatedResponse,
example: {
id: 10,
title: 'string',
contributer: {
nickname: 'admin',
},
correct: 0,
total: 1,
correctionRate: '0.000',
},
isArray: true,
description: 'If Unauthenticated Response',
},
}),
...PaginationDocs,
);
}

public static ReadProblem() {
return applyDecorators(
ApiOperation({ summary: '문제 반환' }),
ApiOkResponse({
type: ReadProblemResponse,
ApiOperation({
summary:
'문제 반환. 비로그인 사용 가능 API. 비로그인 사용하는 경우에는 `isSuccess` 필드가 없습니다. Enum은 Response Schema 참고바랍니다.',
}),
ApiMultipleResponse(HttpStatus.OK, {
Authenticated: {
classRef: ReadProblemAuthenticatedResponse,
example: {
id: 11,
title: 'New Problem',
problem: 'Problem Here',
input: 'Input Here',
output: 'Output Here',
timeLimit: 5,
memoryLimit: 128,
contributerId: '97f16592-93a3-4bba-9bc5-08f55c860bd4',
tags: [],
isOpen: true,
isArchived: false,
deletedAt: null,
createdAt: '2024-01-16T14:12:07.748Z',
updatedAt: '2024-01-16T14:12:39.185Z',
examples: [
{
id: 6,
input: '',
output: 'hello world',
isPublic: true,
problemId: 11,
},
],
isSuccess: 'SUCCESS',
},
},
UnAuthenticated: {
classRef: ReadProblemUnauthenticatedResponse,
example: {
id: 11,
title: 'New Problem',
problem: 'Problem Here',
input: 'Input Here',
output: 'Output Here',
timeLimit: 5,
memoryLimit: 128,
contributerId: '97f16592-93a3-4bba-9bc5-08f55c860bd4',
tags: [],
isOpen: true,
isArchived: false,
deletedAt: null,
createdAt: '2024-01-16T14:12:07.748Z',
updatedAt: '2024-01-16T14:12:39.185Z',
examples: [
{
id: 6,
input: '',
output: 'hello world',
isPublic: true,
problemId: 11,
},
],
},
},
}),

ApiNotFoundResponse({ description: ['PROBLEM_NOT_FOUND'].join(', ') }),
);
}
Expand Down
Loading