Пред-деплой

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,
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<UpdateTokenResultDto> {
@Body() updateTokenDto: UpdateTokenReqDto,
): Promise<UpdateTokenResDto> {
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: Найти применение этой функции
// 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(),
);
},
);

View File

@@ -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<boolean> {
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;
}

View File

@@ -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<UserDto | null> {
const jwtUser: { id: string } =
async decodeUserToken(token: string): Promise<UserDto> {
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<SignUpResDto> {
@@ -99,8 +117,8 @@ export class AuthService {
}
async updateToken(
updateTokenDto: UpdateTokenDto,
): Promise<UpdateTokenResultDto> {
updateTokenDto: UpdateTokenReqDto,
): Promise<UpdateTokenResDto> {
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<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 { 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;
}

View File

@@ -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<string>,
teacherNames: Array<string>,
) {
this.type = type;
this.name = name;
this.defaultIndex = defaultIndex;
this.time = time;
this.name = name;
this.cabinets = cabinets;
this.teacherNames = teacherNames;
}

View File

@@ -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)

View File

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

View File

@@ -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<GroupScheduleDto> {
const schedule = await this.getSourceSchedule();
console.log(schedule);
if ((schedule.groups as object)[group] === undefined) {
throw new NotFoundException(
"Группы с таким названием не существует!",

View File

@@ -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<ClientUserDto> {
const userDto = await this.authService.decodeUserToken(token);
if (!userDto)
throw new NotFoundException("Не удалось найти пользователя!");
return ClientUserDto.fromUserDto(userDto);
}

View File

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

View File

@@ -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}\"!`,