From 6d77476a577b1052daeb659cf955b33a75a0e9a2 Mon Sep 17 00:00:00 2001 From: n08i40k Date: Thu, 12 Sep 2024 21:40:57 +0400 Subject: [PATCH] =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4-=D0=B4=D0=B5=D0=BF?= =?UTF-8?q?=D0=BB=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auth/auth.controller.ts | 48 +++++++++++++---- src/auth/auth.decorator.ts | 12 +++-- src/auth/auth.guard.ts | 31 ++++++----- src/auth/auth.service.ts | 52 ++++++++++++++++--- src/dto/auth.dto.ts | 24 ++++++++- src/dto/schedule.dto.ts | 13 ++++- .../schedule-parser/schedule-parser.ts | 12 ++--- src/schedule/schedule.controller.ts | 3 +- src/schedule/schedule.service.ts | 3 +- src/users/users.controller.ts | 3 -- src/users/users.service.ts | 2 +- .../validation/class-validator.interceptor.ts | 2 + 12 files changed, 152 insertions(+), 53 deletions(-) diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index ff7b33c..dd3a7b7 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -23,11 +23,13 @@ import { SignInResDto, SignUpReqDto, SignUpResDto, - UpdateTokenDto, - UpdateTokenResultDto, + ChangePasswordReqDto, + UpdateTokenReqDto, + UpdateTokenResDto, } from "../dto/auth.dto"; import { ResultDto } from "../utility/validation/class-validator.interceptor"; import { ScheduleService } from "../schedule/schedule.service"; +import { UserToken } from "./auth.decorator"; @Controller("api/v1/auth") export class AuthController { @@ -82,26 +84,52 @@ export class AuthController { return this.authService.signUp(signUpDto); } - @ApiExtraModels(UpdateTokenDto) - @ApiExtraModels(UpdateTokenResultDto) + @ApiExtraModels(UpdateTokenReqDto) + @ApiExtraModels(UpdateTokenResDto) @ApiOperation({ summary: "Обновление просроченного токена", - tags: ["auth"], + tags: ["auth", "accessToken"], }) - @ApiBody({ schema: refs(UpdateTokenDto)[0] }) + @ApiBody({ schema: refs(UpdateTokenReqDto)[0] }) @ApiOkResponse({ description: "Токен обновлён успешно", - schema: refs(UpdateTokenResultDto)[0], + schema: refs(UpdateTokenResDto)[0], }) @ApiNotFoundResponse({ description: "Передан несуществующий или недействительный токен", }) - @ResultDto(UpdateTokenResultDto) + @ResultDto(UpdateTokenResDto) @HttpCode(HttpStatus.OK) @Post("updateToken") updateToken( - @Body() updateTokenDto: UpdateTokenDto, - ): Promise { + @Body() updateTokenDto: UpdateTokenReqDto, + ): Promise { return this.authService.updateToken(updateTokenDto); } + + @ApiExtraModels(ChangePasswordReqDto) + @ApiOperation({ + summary: "Обновление пароля", + tags: ["auth", "password"], + }) + @ApiBody({ schema: refs(ChangePasswordReqDto)[0] }) + @ApiOkResponse({ description: "Пароль обновлён успешно" }) + @ApiConflictResponse({ description: "Пароли идентичны" }) + @ApiUnauthorizedResponse({ + description: + "Передан неверный текущий пароль или запрос был послан без токена", + }) + @ResultDto(null) + @HttpCode(HttpStatus.OK) + @Post("changePassword") + async changePassword( + @Body() changePasswordReqDto: ChangePasswordReqDto, + @UserToken() userToken: string, + ): Promise { + await this.authService + .decodeUserToken(userToken) + .then((user) => + this.authService.changePassword(user, changePasswordReqDto), + ); + } } diff --git a/src/auth/auth.decorator.ts b/src/auth/auth.decorator.ts index a98c96a..522993f 100644 --- a/src/auth/auth.decorator.ts +++ b/src/auth/auth.decorator.ts @@ -3,8 +3,10 @@ import { AuthGuard } from "./auth.guard"; // TODO: Найти применение этой функции // noinspection JSUnusedGlobalSymbols -export const UserToken = createParamDecorator((_, context: ExecutionContext) => { - return AuthGuard.extractTokenFromRequest( - context.switchToHttp().getRequest(), - ); -}); +export const UserToken = createParamDecorator( + (_, context: ExecutionContext) => { + return AuthGuard.extractTokenFromRequest( + context.switchToHttp().getRequest(), + ); + }, +); diff --git a/src/auth/auth.guard.ts b/src/auth/auth.guard.ts index 00b57cb..ccd9e29 100644 --- a/src/auth/auth.guard.ts +++ b/src/auth/auth.guard.ts @@ -15,28 +15,31 @@ export class AuthGuard implements CanActivate { private readonly jwtService: JwtService, ) {} - public static extractTokenFromRequest(req: Request): string | null { + public static extractTokenFromRequest(req: Request): string { const [type, token] = req.headers.authorization?.split(" ") ?? []; - return type === "Bearer" ? token : null; + + if (type !== "Bearer" || !token || token.length === 0) + throw new UnauthorizedException("Не указан токен!"); + + return token; } async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); const token = AuthGuard.extractTokenFromRequest(request); - if (!token) throw new UnauthorizedException("Не указан токен!"); - - try { - if ( - !(await this.jwtService.verifyAsync(token)) || - !(await this.usersService.contains({ accessToken: token })) - ) { - // noinspection ExceptionCaughtLocallyJS - throw new Error(); + if (!token) + try { + if ( + !(await this.jwtService.verifyAsync(token)) || + !(await this.usersService.contains({ accessToken: token })) + ) { + // noinspection ExceptionCaughtLocallyJS + throw new Error(); + } + } catch { + throw new UnauthorizedException("Указан неверный токен!"); } - } catch { - throw new UnauthorizedException("Указан неверный токен!"); - } return true; } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index ea7af98..a9e26e7 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -11,8 +11,9 @@ import { SignInResDto, SignUpReqDto, SignUpResDto, - UpdateTokenDto, - UpdateTokenResultDto, + ChangePasswordReqDto, + UpdateTokenReqDto, + UpdateTokenResDto, } from "../dto/auth.dto"; import { UsersService } from "../users/users.service"; import { genSalt, hash } from "bcrypt"; @@ -27,13 +28,30 @@ export class AuthService { private readonly jwtService: JwtService, ) {} - async decodeUserToken(token: string): Promise { - const jwtUser: { id: string } = + async decodeUserToken(token: string): Promise { + const jwtUser: { id: string } | null = await this.jwtService.verifyAsync(token); - return this.usersService + if (jwtUser === null) { + throw new UnauthorizedException( + "Некорректный или недействительный токен", + ); + } + + const user = await this.usersService .findUnique({ id: jwtUser.id }) .then((user) => user as UserDto | null); + + if (!user) + throw new UnauthorizedException("Не удалось найти пользователя!"); + + if (user.accessToken !== token) { + throw new UnauthorizedException( + "Некорректный или недействительный токен", + ); + } + + return user as UserDto; } async signUp(signUpDto: SignUpReqDto): Promise { @@ -99,8 +117,8 @@ export class AuthService { } async updateToken( - updateTokenDto: UpdateTokenDto, - ): Promise { + updateTokenDto: UpdateTokenReqDto, + ): Promise { if ( !(await this.jwtService.verifyAsync(updateTokenDto.accessToken, { ignoreExpiration: true, @@ -131,4 +149,24 @@ export class AuthService { return { accessToken: accessToken }; } + + async changePassword( + user: UserDto, + changePasswordReqDto: ChangePasswordReqDto, + ): Promise { + const { oldPassword, newPassword } = changePasswordReqDto; + + if (oldPassword == newPassword) + throw new ConflictException("Пароли идентичны"); + + if (user.password !== (await hash(oldPassword, user.salt))) + throw new UnauthorizedException("Передан неверный исходный пароль"); + + await this.usersService.update({ + where: { id: user.id }, + data: { + password: await hash(newPassword, user.salt), + }, + }); + } } diff --git a/src/dto/auth.dto.ts b/src/dto/auth.dto.ts index 1689341..7a8379d 100644 --- a/src/dto/auth.dto.ts +++ b/src/dto/auth.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty, IntersectionType, PickType } from "@nestjs/swagger"; import { UserDto } from "./user.dto"; import { IsString } from "class-validator"; +import { Expose } from "class-transformer"; // SignIn export class SignInReqDto extends PickType(UserDto, ["username"]) { @@ -20,6 +21,25 @@ export class SignUpReqDto extends IntersectionType( export class SignUpResDto extends SignInResDto {} // Update token -export class UpdateTokenDto extends PickType(UserDto, ["accessToken"]) {} +export class UpdateTokenReqDto extends PickType(UserDto, ["accessToken"]) {} -export class UpdateTokenResultDto extends UpdateTokenDto {} +export class UpdateTokenResDto extends UpdateTokenReqDto {} + +// Update password +export class ChangePasswordReqDto { + @ApiProperty({ + example: "my-old-password", + description: "Старый пароль", + }) + @IsString() + @Expose() + oldPassword: string; + + @ApiProperty({ + example: "my-new-password", + description: "Новый пароль", + }) + @IsString() + @Expose() + newPassword: string; +} diff --git a/src/dto/schedule.dto.ts b/src/dto/schedule.dto.ts index 44959c3..69fbf95 100644 --- a/src/dto/schedule.dto.ts +++ b/src/dto/schedule.dto.ts @@ -56,11 +56,18 @@ export enum LessonTypeDto { export class LessonDto { @ApiProperty({ example: LessonTypeDto.DEFAULT, - description: "Тип занятия.", + description: "Тип занятия", }) @IsEnum(LessonTypeDto) type: LessonTypeDto; + @ApiProperty({ + example: 1, + description: "Индекс пары, если присутствует", + }) + @IsNumber() + defaultIndex: number; + @ApiProperty({ example: "Элементы высшей математики", description: "Название занятия", @@ -95,14 +102,16 @@ export class LessonDto { constructor( type: LessonTypeDto, + defaultIndex: number, time: LessonTimeDto, name: string, cabinets: Array, teacherNames: Array, ) { this.type = type; - this.name = name; + this.defaultIndex = defaultIndex; this.time = time; + this.name = name; this.cabinets = cabinets; this.teacherNames = teacherNames; } diff --git a/src/schedule/internal/schedule-parser/schedule-parser.ts b/src/schedule/internal/schedule-parser/schedule-parser.ts index c1edb4f..0667a66 100644 --- a/src/schedule/internal/schedule-parser/schedule-parser.ts +++ b/src/schedule/internal/schedule-parser/schedule-parser.ts @@ -25,10 +25,7 @@ export class ScheduleParseResult { export class ScheduleParser { private lastResult: ScheduleParseResult | null = null; - public constructor( - private readonly xlsDownloader: XlsDownloaderBase, - private readonly group: string, - ) {} + public constructor(private readonly xlsDownloader: XlsDownloaderBase) {} private static getCellName( worksheet: XLSX.Sheet, @@ -162,14 +159,14 @@ export class ScheduleParser { row < daySkeleton.row + rowDistance; ++row ) { - const time = ScheduleParser.getCellName( + const time: string | null = ScheduleParser.getCellName( workSheet, row, lessonTimeColumn, )?.replaceAll(" ", ""); if (!time || typeof time !== "string") continue; - const rawName = ScheduleParser.getCellName( + const rawName: string | null = ScheduleParser.getCellName( workSheet, row, groupSkeleton.column, @@ -216,6 +213,9 @@ export class ScheduleParser { day.lessons.push( new LessonDto( type, + type === LessonTypeDto.DEFAULT + ? Number.parseInt(time[0]) + : -1, LessonTimeDto.fromString( type === LessonTypeDto.DEFAULT ? time.substring(5) diff --git a/src/schedule/schedule.controller.ts b/src/schedule/schedule.controller.ts index 544d134..add0e85 100644 --- a/src/schedule/schedule.controller.ts +++ b/src/schedule/schedule.controller.ts @@ -3,7 +3,8 @@ import { Controller, Get, HttpCode, - HttpStatus, Post, + HttpStatus, + Post, UseGuards, } from "@nestjs/common"; import { AuthGuard } from "../auth/auth.guard"; diff --git a/src/schedule/schedule.service.ts b/src/schedule/schedule.service.ts index 7234cd2..f566d38 100644 --- a/src/schedule/schedule.service.ts +++ b/src/schedule/schedule.service.ts @@ -22,7 +22,6 @@ export class ScheduleService { "https://politehnikum-eng.ru/index/raspisanie_zanjatij/0-409", XlsDownloaderCacheMode.SOFT, ), - "ИС-214/23", ); private lastCacheUpdate: Date = new Date(0); @@ -73,7 +72,7 @@ export class ScheduleService { async getGroup(group: string): Promise { const schedule = await this.getSourceSchedule(); - console.log(schedule); + if ((schedule.groups as object)[group] === undefined) { throw new NotFoundException( "Группы с таким названием не существует!", diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index ff41350..7967b1c 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -3,7 +3,6 @@ import { Get, HttpCode, HttpStatus, - NotFoundException, UseGuards, } from "@nestjs/common"; import { AuthGuard } from "../auth/auth.guard"; @@ -22,8 +21,6 @@ export class UsersController { @Get("me") async getMe(@UserToken() token: string): Promise { const userDto = await this.authService.decodeUserToken(token); - if (!userDto) - throw new NotFoundException("Не удалось найти пользователя!"); return ClientUserDto.fromUserDto(userDto); } diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 8b404d8..2ccb4a7 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -21,7 +21,7 @@ export class UsersService { async update(params: { where: Prisma.UserWhereUniqueInput; data: Prisma.UserUpdateInput; - }): Promise { + }): Promise { return this.prismaService.user .update(params) .then(UsersService.convertToDto); diff --git a/src/utility/validation/class-validator.interceptor.ts b/src/utility/validation/class-validator.interceptor.ts index a54a8a2..d948d27 100644 --- a/src/utility/validation/class-validator.interceptor.ts +++ b/src/utility/validation/class-validator.interceptor.ts @@ -32,6 +32,8 @@ export class ClassValidatorInterceptor implements NestInterceptor { handler.name, ); + if (classDto === null) return returnValue; + if (classDto === undefined) { console.warn( `Undefined DTO type for function \"${cls.name}::${handler.name}\"!`,