diff --git a/package-lock.json b/package-lock.json index 4de20bf..f187e83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "schedule-parser-next", - "version": "1.3.5", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "schedule-parser-next", - "version": "1.3.5", + "version": "1.4.0", "license": "UNLICENSED", "dependencies": { "@nestjs/cache-manager": "^2.2.2", diff --git a/package.json b/package.json index 7bd2e8e..e1f7866 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "schedule-parser-next", - "version": "1.3.5", + "version": "1.4.0", "description": "", "author": "N08I40K", "private": true, diff --git a/src/auth-role/auth-role.decorator.ts b/src/auth-role/auth-role.decorator.ts index d97157f..b9b535f 100644 --- a/src/auth-role/auth-role.decorator.ts +++ b/src/auth-role/auth-role.decorator.ts @@ -2,3 +2,4 @@ import { Reflector } from "@nestjs/core"; import { UserRoleDto } from "../dto/user.dto"; export const AuthRoles = Reflector.createDecorator(); +export const AuthUnauthorized = Reflector.createDecorator(); diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index bce5781..505b542 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -26,10 +26,13 @@ import { ChangePasswordReqDto, UpdateTokenReqDto, UpdateTokenResDto, + SignInResDtoV0, + SignInResDtoV1, } from "../dto/auth.dto"; import { ResultDto } from "../utility/validation/class-validator.interceptor"; import { ScheduleService } from "../schedule/schedule.service"; import { UserToken } from "./auth.decorator"; +import { ResponseVersion } from "../version/response-version.decorator"; @Controller("api/v1/auth") export class AuthController { @@ -39,21 +42,30 @@ export class AuthController { ) {} @ApiExtraModels(SignInReqDto) - @ApiExtraModels(SignInResDto) + @ApiExtraModels(SignInResDtoV0) + @ApiExtraModels(SignInResDtoV1) @ApiOperation({ summary: "Авторизация по логину и паролю", tags: ["auth"] }) @ApiBody({ schema: refs(SignInReqDto)[0] }) @ApiOkResponse({ description: "Авторизация прошла успешно", - schema: refs(SignInResDto)[0], + schema: refs(SignInResDtoV0)[0], + }) + @ApiOkResponse({ + description: "Авторизация прошла успешно", + schema: refs(SignInResDtoV1)[0], }) @ApiUnauthorizedResponse({ description: "Некорректное имя пользователя или пароль", }) - @ResultDto(SignInResDto) + @ResultDto([SignInResDtoV0, SignInResDtoV1]) @HttpCode(HttpStatus.OK) @Post("sign-in") - signIn(@Body() signInDto: SignInReqDto) { - return this.authService.signIn(signInDto); + async signIn( + @Body() signInDto: SignInReqDto, + @ResponseVersion() responseVersion: number, + ): Promise { + const data = await this.authService.signIn(signInDto); + return SignInResDto.stripVersion(data, responseVersion); } @ApiExtraModels(SignUpReqDto) diff --git a/src/auth/auth.guard.ts b/src/auth/auth.guard.ts index f5a249b..76a7ef5 100644 --- a/src/auth/auth.guard.ts +++ b/src/auth/auth.guard.ts @@ -9,7 +9,7 @@ import { JwtService } from "@nestjs/jwt"; import { Request } from "express"; import { UsersService } from "../users/users.service"; import { Reflector } from "@nestjs/core"; -import { AuthRoles } from "../auth-role/auth-role.decorator"; +import { AuthRoles, AuthUnauthorized } from "../auth-role/auth-role.decorator"; import { isJWT } from "class-validator"; @Injectable() @@ -30,6 +30,9 @@ export class AuthGuard implements CanActivate { } async canActivate(context: ExecutionContext): Promise { + if (this.reflector.get(AuthUnauthorized, context.getHandler())) + return true; + const request = context.switchToHttp().getRequest(); const token = AuthGuard.extractTokenFromRequest(request); diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 5da8b40..daa11bc 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -115,7 +115,7 @@ export class AuthService { data: { accessToken: accessToken }, }); - return { id: user.id, accessToken: accessToken }; + return { id: user.id, accessToken: accessToken, group: user.group }; } async updateToken( diff --git a/src/dto/auth.dto.ts b/src/dto/auth.dto.ts index ca9965c..cb2dd6f 100644 --- a/src/dto/auth.dto.ts +++ b/src/dto/auth.dto.ts @@ -5,8 +5,8 @@ import { PickType, } from "@nestjs/swagger"; import { UserDto } from "./user.dto"; -import { IsString } from "class-validator"; -import { Expose } from "class-transformer"; +import { IsJWT, IsMongoId, IsString } from "class-validator"; +import { Expose, instanceToPlain, plainToClass } from "class-transformer"; // SignIn export class SignInReqDto extends PickType(UserDto, ["username"]) { @@ -18,7 +18,50 @@ export class SignInReqDto extends PickType(UserDto, ["username"]) { password: string; } -export class SignInResDto extends PickType(UserDto, ["id", "accessToken"]) {} +export class SignInResDtoV0 { + @ApiProperty({ + example: "66e1b7e255c5d5f1268cce90", + description: "Идентификатор (ObjectId)", + }) + @IsMongoId() + @Expose() + id: string; + + @ApiProperty({ + example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + description: "Последний токен доступа", + }) + @IsJWT() + @Expose() + accessToken: string; +} + +export class SignInResDtoV1 extends SignInResDtoV0 { + @ApiProperty({ + example: "ИС-214/23", + description: "Группа", + }) + @IsString() + @Expose() + group: string; +} + +export class SignInResDto extends SignInResDtoV1 { + public static stripVersion( + instance: SignInResDto, + version: number, + ): SignInResDtoV0 | SignInResDtoV1 { + switch (version) { + default: + return instance; + case 0: { + return plainToClass(SignInResDtoV0, instanceToPlain(instance), { + excludeExtraneousValues: true, + }); + } + } + } +} // SignUp export class SignUpReqDto extends IntersectionType( @@ -27,7 +70,10 @@ export class SignUpReqDto extends IntersectionType( PartialType(PickType(UserDto, ["version"])), ) {} -export class SignUpResDto extends SignInResDto {} +export class SignUpResDto extends PickType(SignInResDto, [ + "id", + "accessToken", +]) {} // Update token export class UpdateTokenReqDto extends PickType(UserDto, ["accessToken"]) {} diff --git a/src/dto/schedule.dto.ts b/src/dto/schedule.dto.ts index b5a4b8d..448c3b1 100644 --- a/src/dto/schedule.dto.ts +++ b/src/dto/schedule.dto.ts @@ -11,7 +11,7 @@ import { IsString, ValidateNested, } from "class-validator"; -import { ApiProperty, OmitType, PickType } from "@nestjs/swagger"; +import { ApiProperty, OmitType, PartialType, PickType } from "@nestjs/swagger"; import { Expose, instanceToPlain, @@ -296,7 +296,9 @@ export class ScheduleDto { lastChangedDays: Array>; } -export class GroupScheduleRequestDto extends PickType(GroupDto, ["name"]) {} +export class GroupScheduleReqDto extends PartialType( + PickType(GroupDto, ["name"]), +) {} export class ScheduleGroupsDto { @ApiProperty({ diff --git a/src/schedule/schedule.controller.ts b/src/schedule/schedule.controller.ts index 11dc8ea..800ab6c 100644 --- a/src/schedule/schedule.controller.ts +++ b/src/schedule/schedule.controller.ts @@ -14,7 +14,7 @@ import { CacheStatusV0Dto, CacheStatusV1Dto, GroupScheduleDto, - GroupScheduleRequestDto, + GroupScheduleReqDto, ScheduleDto, ScheduleGroupsDto, SiteMainPageDto, @@ -28,7 +28,11 @@ import { ApiOperation, refs, } from "@nestjs/swagger"; -import { ClientVersion } from "../version/client-version.decorator"; +import { ResponseVersion } from "../version/response-version.decorator"; +import { AuthRoles, AuthUnauthorized } from "../auth-role/auth-role.decorator"; +import { UserDto, UserRoleDto } from "../dto/user.dto"; +import { UserToken } from "../auth/auth.decorator"; +import { UserFromTokenPipe } from "../auth/auth.pipe"; @Controller("api/v1/schedule") @UseGuards(AuthGuard) @@ -36,12 +40,16 @@ export class ScheduleController { constructor(private readonly scheduleService: ScheduleService) {} @ApiExtraModels(ScheduleDto) - @ApiOperation({ summary: "Получение расписания", tags: ["schedule"] }) + @ApiOperation({ + summary: "Получение расписания", + tags: ["schedule", "admin"], + }) @ApiOkResponse({ description: "Расписание получено успешно", schema: refs(ScheduleDto)[0], }) @ResultDto(ScheduleDto) + @AuthRoles([UserRoleDto.ADMIN]) @HttpCode(HttpStatus.OK) @Get("get") async getSchedule(): Promise { @@ -62,9 +70,10 @@ export class ScheduleController { @HttpCode(HttpStatus.OK) @Post("get-group") async getGroupSchedule( - @Body() groupDto: GroupScheduleRequestDto, + @Body() groupDto: GroupScheduleReqDto, + @UserToken(UserFromTokenPipe) user: UserDto, ): Promise { - return await this.scheduleService.getGroup(groupDto.name); + return await this.scheduleService.getGroup(groupDto.name ?? user.group); } @ApiExtraModels(ScheduleGroupsDto) @@ -78,6 +87,7 @@ export class ScheduleController { }) @ApiNotFoundResponse({ description: "Требуемая группа не найдена" }) @ResultDto(ScheduleGroupsDto) + @AuthUnauthorized() @HttpCode(HttpStatus.OK) @Get("get-group-names") async getGroupNames(): Promise { @@ -107,7 +117,7 @@ export class ScheduleController { @Post("update-site-main-page") async updateSiteMainPage( @Body() siteMainPageDto: SiteMainPageDto, - @ClientVersion() version: number, + @ResponseVersion() version: number, ): Promise { return CacheStatusDto.stripVersion( await this.scheduleService.updateSiteMainPage(siteMainPageDto), @@ -133,7 +143,7 @@ export class ScheduleController { @HttpCode(HttpStatus.OK) @Get("cache-status") getCacheStatus( - @ClientVersion() version: number, + @ResponseVersion() version: number, ): CacheStatusV0Dto | CacheStatusV1Dto { return CacheStatusDto.stripVersion( this.scheduleService.getCacheStatus(), diff --git a/src/version/client-version.decorator.ts b/src/version/response-version.decorator.ts similarity index 87% rename from src/version/client-version.decorator.ts rename to src/version/response-version.decorator.ts index 6162bdc..e213551 100644 --- a/src/version/client-version.decorator.ts +++ b/src/version/response-version.decorator.ts @@ -1,6 +1,6 @@ import { createParamDecorator, ExecutionContext } from "@nestjs/common"; -export const ClientVersion = createParamDecorator( +export const ResponseVersion = createParamDecorator( (_, context: ExecutionContext) => { const sourceVersion: string | null = context.switchToHttp().getRequest() .headers.version;