Пред-деплой

This commit is contained in:
2024-09-12 21:40:57 +04:00
parent 8fb9214246
commit 6d77476a57
12 changed files with 152 additions and 53 deletions

View File

@@ -23,11 +23,13 @@ import {
SignInResDto, SignInResDto,
SignUpReqDto, SignUpReqDto,
SignUpResDto, SignUpResDto,
UpdateTokenDto, ChangePasswordReqDto,
UpdateTokenResultDto, UpdateTokenReqDto,
UpdateTokenResDto,
} from "../dto/auth.dto"; } from "../dto/auth.dto";
import { ResultDto } from "../utility/validation/class-validator.interceptor"; import { ResultDto } from "../utility/validation/class-validator.interceptor";
import { ScheduleService } from "../schedule/schedule.service"; import { ScheduleService } from "../schedule/schedule.service";
import { UserToken } from "./auth.decorator";
@Controller("api/v1/auth") @Controller("api/v1/auth")
export class AuthController { export class AuthController {
@@ -82,26 +84,52 @@ export class AuthController {
return this.authService.signUp(signUpDto); return this.authService.signUp(signUpDto);
} }
@ApiExtraModels(UpdateTokenDto) @ApiExtraModels(UpdateTokenReqDto)
@ApiExtraModels(UpdateTokenResultDto) @ApiExtraModels(UpdateTokenResDto)
@ApiOperation({ @ApiOperation({
summary: "Обновление просроченного токена", summary: "Обновление просроченного токена",
tags: ["auth"], tags: ["auth", "accessToken"],
}) })
@ApiBody({ schema: refs(UpdateTokenDto)[0] }) @ApiBody({ schema: refs(UpdateTokenReqDto)[0] })
@ApiOkResponse({ @ApiOkResponse({
description: "Токен обновлён успешно", description: "Токен обновлён успешно",
schema: refs(UpdateTokenResultDto)[0], schema: refs(UpdateTokenResDto)[0],
}) })
@ApiNotFoundResponse({ @ApiNotFoundResponse({
description: "Передан несуществующий или недействительный токен", description: "Передан несуществующий или недействительный токен",
}) })
@ResultDto(UpdateTokenResultDto) @ResultDto(UpdateTokenResDto)
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Post("updateToken") @Post("updateToken")
updateToken( updateToken(
@Body() updateTokenDto: UpdateTokenDto, @Body() updateTokenDto: UpdateTokenReqDto,
): Promise<UpdateTokenResultDto> { ): Promise<UpdateTokenResDto> {
return this.authService.updateToken(updateTokenDto); 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<void> {
await this.authService
.decodeUserToken(userToken)
.then((user) =>
this.authService.changePassword(user, changePasswordReqDto),
);
}
} }

View File

@@ -3,8 +3,10 @@ import { AuthGuard } from "./auth.guard";
// TODO: Найти применение этой функции // TODO: Найти применение этой функции
// noinspection JSUnusedGlobalSymbols // noinspection JSUnusedGlobalSymbols
export const UserToken = createParamDecorator((_, context: ExecutionContext) => { export const UserToken = createParamDecorator(
return AuthGuard.extractTokenFromRequest( (_, context: ExecutionContext) => {
context.switchToHttp().getRequest(), return AuthGuard.extractTokenFromRequest(
); context.switchToHttp().getRequest(),
}); );
},
);

View File

@@ -15,28 +15,31 @@ export class AuthGuard implements CanActivate {
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
) {} ) {}
public static extractTokenFromRequest(req: Request): string | null { public static extractTokenFromRequest(req: Request): string {
const [type, token] = req.headers.authorization?.split(" ") ?? []; 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<boolean> { async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest(); const request = context.switchToHttp().getRequest();
const token = AuthGuard.extractTokenFromRequest(request); const token = AuthGuard.extractTokenFromRequest(request);
if (!token) throw new UnauthorizedException("Не указан токен!"); if (!token)
try {
try { if (
if ( !(await this.jwtService.verifyAsync(token)) ||
!(await this.jwtService.verifyAsync(token)) || !(await this.usersService.contains({ accessToken: token }))
!(await this.usersService.contains({ accessToken: token })) ) {
) { // noinspection ExceptionCaughtLocallyJS
// noinspection ExceptionCaughtLocallyJS throw new Error();
throw new Error(); }
} catch {
throw new UnauthorizedException("Указан неверный токен!");
} }
} catch {
throw new UnauthorizedException("Указан неверный токен!");
}
return true; return true;
} }

View File

@@ -11,8 +11,9 @@ import {
SignInResDto, SignInResDto,
SignUpReqDto, SignUpReqDto,
SignUpResDto, SignUpResDto,
UpdateTokenDto, ChangePasswordReqDto,
UpdateTokenResultDto, UpdateTokenReqDto,
UpdateTokenResDto,
} from "../dto/auth.dto"; } from "../dto/auth.dto";
import { UsersService } from "../users/users.service"; import { UsersService } from "../users/users.service";
import { genSalt, hash } from "bcrypt"; import { genSalt, hash } from "bcrypt";
@@ -27,13 +28,30 @@ export class AuthService {
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
) {} ) {}
async decodeUserToken(token: string): Promise<UserDto | null> { async decodeUserToken(token: string): Promise<UserDto> {
const jwtUser: { id: string } = const jwtUser: { id: string } | null =
await this.jwtService.verifyAsync(token); await this.jwtService.verifyAsync(token);
return this.usersService if (jwtUser === null) {
throw new UnauthorizedException(
"Некорректный или недействительный токен",
);
}
const user = await this.usersService
.findUnique({ id: jwtUser.id }) .findUnique({ id: jwtUser.id })
.then((user) => user as UserDto | null); .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<SignUpResDto> { async signUp(signUpDto: SignUpReqDto): Promise<SignUpResDto> {
@@ -99,8 +117,8 @@ export class AuthService {
} }
async updateToken( async updateToken(
updateTokenDto: UpdateTokenDto, updateTokenDto: UpdateTokenReqDto,
): Promise<UpdateTokenResultDto> { ): Promise<UpdateTokenResDto> {
if ( if (
!(await this.jwtService.verifyAsync(updateTokenDto.accessToken, { !(await this.jwtService.verifyAsync(updateTokenDto.accessToken, {
ignoreExpiration: true, ignoreExpiration: true,
@@ -131,4 +149,24 @@ export class AuthService {
return { accessToken: accessToken }; return { accessToken: accessToken };
} }
async changePassword(
user: UserDto,
changePasswordReqDto: ChangePasswordReqDto,
): Promise<void> {
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),
},
});
}
} }

View File

@@ -1,6 +1,7 @@
import { ApiProperty, IntersectionType, PickType } from "@nestjs/swagger"; import { ApiProperty, IntersectionType, PickType } from "@nestjs/swagger";
import { UserDto } from "./user.dto"; import { UserDto } from "./user.dto";
import { IsString } from "class-validator"; import { IsString } from "class-validator";
import { Expose } from "class-transformer";
// SignIn // SignIn
export class SignInReqDto extends PickType(UserDto, ["username"]) { export class SignInReqDto extends PickType(UserDto, ["username"]) {
@@ -20,6 +21,25 @@ export class SignUpReqDto extends IntersectionType(
export class SignUpResDto extends SignInResDto {} export class SignUpResDto extends SignInResDto {}
// Update token // 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;
}

View File

@@ -56,11 +56,18 @@ export enum LessonTypeDto {
export class LessonDto { export class LessonDto {
@ApiProperty({ @ApiProperty({
example: LessonTypeDto.DEFAULT, example: LessonTypeDto.DEFAULT,
description: "Тип занятия.", description: "Тип занятия",
}) })
@IsEnum(LessonTypeDto) @IsEnum(LessonTypeDto)
type: LessonTypeDto; type: LessonTypeDto;
@ApiProperty({
example: 1,
description: "Индекс пары, если присутствует",
})
@IsNumber()
defaultIndex: number;
@ApiProperty({ @ApiProperty({
example: "Элементы высшей математики", example: "Элементы высшей математики",
description: "Название занятия", description: "Название занятия",
@@ -95,14 +102,16 @@ export class LessonDto {
constructor( constructor(
type: LessonTypeDto, type: LessonTypeDto,
defaultIndex: number,
time: LessonTimeDto, time: LessonTimeDto,
name: string, name: string,
cabinets: Array<string>, cabinets: Array<string>,
teacherNames: Array<string>, teacherNames: Array<string>,
) { ) {
this.type = type; this.type = type;
this.name = name; this.defaultIndex = defaultIndex;
this.time = time; this.time = time;
this.name = name;
this.cabinets = cabinets; this.cabinets = cabinets;
this.teacherNames = teacherNames; this.teacherNames = teacherNames;
} }

View File

@@ -25,10 +25,7 @@ export class ScheduleParseResult {
export class ScheduleParser { export class ScheduleParser {
private lastResult: ScheduleParseResult | null = null; private lastResult: ScheduleParseResult | null = null;
public constructor( public constructor(private readonly xlsDownloader: XlsDownloaderBase) {}
private readonly xlsDownloader: XlsDownloaderBase,
private readonly group: string,
) {}
private static getCellName( private static getCellName(
worksheet: XLSX.Sheet, worksheet: XLSX.Sheet,
@@ -162,14 +159,14 @@ export class ScheduleParser {
row < daySkeleton.row + rowDistance; row < daySkeleton.row + rowDistance;
++row ++row
) { ) {
const time = ScheduleParser.getCellName( const time: string | null = ScheduleParser.getCellName(
workSheet, workSheet,
row, row,
lessonTimeColumn, lessonTimeColumn,
)?.replaceAll(" ", ""); )?.replaceAll(" ", "");
if (!time || typeof time !== "string") continue; if (!time || typeof time !== "string") continue;
const rawName = ScheduleParser.getCellName( const rawName: string | null = ScheduleParser.getCellName(
workSheet, workSheet,
row, row,
groupSkeleton.column, groupSkeleton.column,
@@ -216,6 +213,9 @@ export class ScheduleParser {
day.lessons.push( day.lessons.push(
new LessonDto( new LessonDto(
type, type,
type === LessonTypeDto.DEFAULT
? Number.parseInt(time[0])
: -1,
LessonTimeDto.fromString( LessonTimeDto.fromString(
type === LessonTypeDto.DEFAULT type === LessonTypeDto.DEFAULT
? time.substring(5) ? time.substring(5)

View File

@@ -3,7 +3,8 @@ import {
Controller, Controller,
Get, Get,
HttpCode, HttpCode,
HttpStatus, Post, HttpStatus,
Post,
UseGuards, UseGuards,
} from "@nestjs/common"; } from "@nestjs/common";
import { AuthGuard } from "../auth/auth.guard"; import { AuthGuard } from "../auth/auth.guard";

View File

@@ -22,7 +22,6 @@ export class ScheduleService {
"https://politehnikum-eng.ru/index/raspisanie_zanjatij/0-409", "https://politehnikum-eng.ru/index/raspisanie_zanjatij/0-409",
XlsDownloaderCacheMode.SOFT, XlsDownloaderCacheMode.SOFT,
), ),
"ИС-214/23",
); );
private lastCacheUpdate: Date = new Date(0); private lastCacheUpdate: Date = new Date(0);
@@ -73,7 +72,7 @@ export class ScheduleService {
async getGroup(group: string): Promise<GroupScheduleDto> { async getGroup(group: string): Promise<GroupScheduleDto> {
const schedule = await this.getSourceSchedule(); const schedule = await this.getSourceSchedule();
console.log(schedule);
if ((schedule.groups as object)[group] === undefined) { if ((schedule.groups as object)[group] === undefined) {
throw new NotFoundException( throw new NotFoundException(
"Группы с таким названием не существует!", "Группы с таким названием не существует!",

View File

@@ -3,7 +3,6 @@ import {
Get, Get,
HttpCode, HttpCode,
HttpStatus, HttpStatus,
NotFoundException,
UseGuards, UseGuards,
} from "@nestjs/common"; } from "@nestjs/common";
import { AuthGuard } from "../auth/auth.guard"; import { AuthGuard } from "../auth/auth.guard";
@@ -22,8 +21,6 @@ export class UsersController {
@Get("me") @Get("me")
async getMe(@UserToken() token: string): Promise<ClientUserDto> { async getMe(@UserToken() token: string): Promise<ClientUserDto> {
const userDto = await this.authService.decodeUserToken(token); const userDto = await this.authService.decodeUserToken(token);
if (!userDto)
throw new NotFoundException("Не удалось найти пользователя!");
return ClientUserDto.fromUserDto(userDto); return ClientUserDto.fromUserDto(userDto);
} }

View File

@@ -21,7 +21,7 @@ export class UsersService {
async update(params: { async update(params: {
where: Prisma.UserWhereUniqueInput; where: Prisma.UserWhereUniqueInput;
data: Prisma.UserUpdateInput; data: Prisma.UserUpdateInput;
}): Promise<UserDto | null> { }): Promise<UserDto> {
return this.prismaService.user return this.prismaService.user
.update(params) .update(params)
.then(UsersService.convertToDto); .then(UsersService.convertToDto);

View File

@@ -32,6 +32,8 @@ export class ClassValidatorInterceptor implements NestInterceptor {
handler.name, handler.name,
); );
if (classDto === null) return returnValue;
if (classDto === undefined) { if (classDto === undefined) {
console.warn( console.warn(
`Undefined DTO type for function \"${cls.name}::${handler.name}\"!`, `Undefined DTO type for function \"${cls.name}::${handler.name}\"!`,