mirror of
https://github.com/n08i40k/schedule-parser-next.git
synced 2025-12-06 09:47:46 +03:00
1.2.0
Добавлена возможность заменять файл с расписанием. Добалена возможность давать доступ к end-point'ам только определённым ролям. Чуть-чуть меньше спагетти в объявлениях модулей.
This commit is contained in:
22
package-lock.json
generated
22
package-lock.json
generated
@@ -34,11 +34,11 @@
|
|||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
"@nestjs/schematics": "^10.0.0",
|
"@nestjs/schematics": "^10.0.0",
|
||||||
"@nestjs/testing": "^10.0.0",
|
"@nestjs/testing": "^10.0.0",
|
||||||
"@types/axios": "^0.14.0",
|
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.2",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
|
"@types/multer": "^1.4.12",
|
||||||
"@types/node": "^20.16.5",
|
"@types/node": "^20.16.5",
|
||||||
"@types/supertest": "^6.0.0",
|
"@types/supertest": "^6.0.0",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
@@ -2078,16 +2078,6 @@
|
|||||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/axios": {
|
|
||||||
"version": "0.14.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz",
|
|
||||||
"integrity": "sha512-KqQnQbdYE54D7oa/UmYVMZKq7CO4l8DEENzOKc4aBRwxCXSlJXGz83flFx5L7AWrOQnmuN3kVsRdt+GZPPjiVQ==",
|
|
||||||
"deprecated": "This is a stub types definition for axios (https://github.com/mzabriskie/axios). axios provides its own type definitions, so you don't need @types/axios installed!",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"axios": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -2279,6 +2269,16 @@
|
|||||||
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
|
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/multer": {
|
||||||
|
"version": "1.4.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.12.tgz",
|
||||||
|
"integrity": "sha512-pQ2hoqvXiJt2FP9WQVLPRO+AmiIm/ZYkavPlIQnx282u4ZrVdztx0pkh3jjpQt0Kz+YI0YhSG264y08UJKoUQg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/express": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.16.5",
|
"version": "20.16.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "schedule-parser-next",
|
"name": "schedule-parser-next",
|
||||||
"version": "1.1.1",
|
"version": "1.2.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "N08I40K",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -45,11 +45,11 @@
|
|||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.0.0",
|
||||||
"@nestjs/schematics": "^10.0.0",
|
"@nestjs/schematics": "^10.0.0",
|
||||||
"@nestjs/testing": "^10.0.0",
|
"@nestjs/testing": "^10.0.0",
|
||||||
"@types/axios": "^0.14.0",
|
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"@types/jest": "^29.5.2",
|
"@types/jest": "^29.5.2",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
|
"@types/multer": "^1.4.12",
|
||||||
"@types/node": "^20.16.5",
|
"@types/node": "^20.16.5",
|
||||||
"@types/supertest": "^6.0.0",
|
"@types/supertest": "^6.0.0",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ datasource db {
|
|||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model ScheduleReplace {
|
||||||
|
id String @id @default(auto()) @map("_id") @db.ObjectId
|
||||||
|
etag String @unique
|
||||||
|
data Bytes
|
||||||
|
}
|
||||||
|
|
||||||
enum UserRole {
|
enum UserRole {
|
||||||
STUDENT
|
STUDENT
|
||||||
TEACHER
|
TEACHER
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { AuthModule } from "./auth/auth.module";
|
|||||||
import { UsersModule } from "./users/users.module";
|
import { UsersModule } from "./users/users.module";
|
||||||
import { ScheduleModule } from "./schedule/schedule.module";
|
import { ScheduleModule } from "./schedule/schedule.module";
|
||||||
import { CacheModule } from "@nestjs/cache-manager";
|
import { CacheModule } from "@nestjs/cache-manager";
|
||||||
|
import { ScheduleReplacerModule } from "./schedule-replacer/schedule-replacer.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -10,6 +11,7 @@ import { CacheModule } from "@nestjs/cache-manager";
|
|||||||
UsersModule,
|
UsersModule,
|
||||||
ScheduleModule,
|
ScheduleModule,
|
||||||
CacheModule.register({ ttl: 5 * 60 * 1000, isGlobal: true }),
|
CacheModule.register({ ttl: 5 * 60 * 1000, isGlobal: true }),
|
||||||
|
ScheduleReplacerModule,
|
||||||
],
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
|
|||||||
4
src/auth-role/auth-role.decorator.ts
Normal file
4
src/auth-role/auth-role.decorator.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { Reflector } from "@nestjs/core";
|
||||||
|
import { UserRoleDto } from "../dto/user.dto";
|
||||||
|
|
||||||
|
export const AuthRoles = Reflector.createDecorator<UserRoleDto[]>();
|
||||||
@@ -73,7 +73,7 @@ export class AuthController {
|
|||||||
async signUp(@Body() signUpDto: SignUpReqDto) {
|
async signUp(@Body() signUpDto: SignUpReqDto) {
|
||||||
if (
|
if (
|
||||||
!(await this.scheduleService.getGroupNames()).names.includes(
|
!(await this.scheduleService.getGroupNames()).names.includes(
|
||||||
signUpDto.group,
|
signUpDto.group.replaceAll(" ", ""),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
throw new NotFoundException(
|
throw new NotFoundException(
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
import {
|
import {
|
||||||
CanActivate,
|
CanActivate,
|
||||||
ExecutionContext,
|
ExecutionContext,
|
||||||
|
ForbiddenException,
|
||||||
Injectable,
|
Injectable,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { JwtService } from "@nestjs/jwt";
|
import { JwtService } from "@nestjs/jwt";
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
import { UsersService } from "../users/users.service";
|
import { UsersService } from "../users/users.service";
|
||||||
|
import { Reflector } from "@nestjs/core";
|
||||||
|
import { AuthRoles } from "../auth-role/auth-role.decorator";
|
||||||
|
import { isJWT } from "class-validator";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthGuard implements CanActivate {
|
export class AuthGuard implements CanActivate {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly usersService: UsersService,
|
private readonly usersService: UsersService,
|
||||||
private readonly jwtService: JwtService,
|
private readonly jwtService: JwtService,
|
||||||
|
private readonly reflector: Reflector,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public static extractTokenFromRequest(req: Request): string {
|
public static extractTokenFromRequest(req: Request): string {
|
||||||
@@ -28,18 +33,27 @@ export class AuthGuard implements CanActivate {
|
|||||||
const request = context.switchToHttp().getRequest();
|
const request = context.switchToHttp().getRequest();
|
||||||
const token = AuthGuard.extractTokenFromRequest(request);
|
const token = AuthGuard.extractTokenFromRequest(request);
|
||||||
|
|
||||||
if (!token)
|
let jwtUser: { id: string } | null = null;
|
||||||
try {
|
|
||||||
if (
|
if (
|
||||||
!(await this.jwtService.verifyAsync(token)) ||
|
!isJWT(token) ||
|
||||||
!(await this.usersService.contains({ accessToken: token }))
|
!(jwtUser = await this.jwtService
|
||||||
) {
|
.verifyAsync(token)
|
||||||
// noinspection ExceptionCaughtLocallyJS
|
.catch(() => null))
|
||||||
throw new Error();
|
)
|
||||||
}
|
throw new UnauthorizedException();
|
||||||
} catch {
|
|
||||||
throw new UnauthorizedException("Указан неверный токен!");
|
const user = await this.usersService.findUnique({ id: jwtUser.id });
|
||||||
}
|
if (!user || user.accessToken !== token)
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
|
||||||
|
const acceptableRoles = this.reflector.get(
|
||||||
|
AuthRoles,
|
||||||
|
context.getHandler(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (acceptableRoles != null && !acceptableRoles.includes(user.role))
|
||||||
|
throw new ForbiddenException();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,20 +4,20 @@ import { jwtConstants } from "../contants";
|
|||||||
import { AuthService } from "./auth.service";
|
import { AuthService } from "./auth.service";
|
||||||
import { AuthController } from "./auth.controller";
|
import { AuthController } from "./auth.controller";
|
||||||
import { UsersModule } from "../users/users.module";
|
import { UsersModule } from "../users/users.module";
|
||||||
import { UsersService } from "../users/users.service";
|
|
||||||
import { PrismaService } from "../prisma/prisma.service";
|
import { PrismaService } from "../prisma/prisma.service";
|
||||||
import { ScheduleService } from "../schedule/schedule.service";
|
import { ScheduleModule } from "../schedule/schedule.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
UsersModule,
|
UsersModule,
|
||||||
|
ScheduleModule,
|
||||||
JwtModule.register({
|
JwtModule.register({
|
||||||
global: true,
|
global: true,
|
||||||
secret: jwtConstants.secret,
|
secret: jwtConstants.secret,
|
||||||
signOptions: { expiresIn: "720h" },
|
signOptions: { expiresIn: "720h" },
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
providers: [AuthService, UsersService, PrismaService, ScheduleService],
|
providers: [AuthService, PrismaService],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
exports: [AuthService],
|
exports: [AuthService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -55,15 +55,16 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async signUp(signUpDto: SignUpReqDto): Promise<SignUpResDto> {
|
async signUp(signUpDto: SignUpReqDto): Promise<SignUpResDto> {
|
||||||
|
const group = signUpDto.group.replaceAll(" ", "");
|
||||||
|
const username = signUpDto.username.replaceAll(" ", "");
|
||||||
|
|
||||||
if (
|
if (
|
||||||
![UserRoleDto.STUDENT, UserRoleDto.TEACHER].includes(signUpDto.role)
|
![UserRoleDto.STUDENT, UserRoleDto.TEACHER].includes(signUpDto.role)
|
||||||
) {
|
) {
|
||||||
throw new NotAcceptableException("Передана неизвестная роль");
|
throw new NotAcceptableException("Передана неизвестная роль");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (await this.usersService.contains({ username: username })) {
|
||||||
await this.usersService.contains({ username: signUpDto.username })
|
|
||||||
) {
|
|
||||||
throw new ConflictException(
|
throw new ConflictException(
|
||||||
"Пользователь с таким именем уже существует!",
|
"Пользователь с таким именем уже существует!",
|
||||||
);
|
);
|
||||||
@@ -74,14 +75,14 @@ export class AuthService {
|
|||||||
|
|
||||||
const input: Prisma.UserCreateInput = {
|
const input: Prisma.UserCreateInput = {
|
||||||
id: id,
|
id: id,
|
||||||
username: signUpDto.username,
|
username: username,
|
||||||
salt: salt,
|
salt: salt,
|
||||||
password: await hash(signUpDto.password, salt),
|
password: await hash(signUpDto.password, salt),
|
||||||
accessToken: await this.jwtService.signAsync({
|
accessToken: await this.jwtService.signAsync({
|
||||||
id: id,
|
id: id,
|
||||||
}),
|
}),
|
||||||
role: signUpDto.role as UserRole,
|
role: signUpDto.role as UserRole,
|
||||||
group: signUpDto.group,
|
group: group,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.usersService.create(input).then((user) => {
|
return this.usersService.create(input).then((user) => {
|
||||||
@@ -94,7 +95,7 @@ export class AuthService {
|
|||||||
|
|
||||||
async signIn(signInDto: SignInReqDto): Promise<SignInResDto> {
|
async signIn(signInDto: SignInReqDto): Promise<SignInResDto> {
|
||||||
const user = await this.usersService.findUnique({
|
const user = await this.usersService.findUnique({
|
||||||
username: signInDto.username,
|
username: signInDto.username.replaceAll(" ", ""),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
29
src/dto/schedule-replacer.dto.ts
Normal file
29
src/dto/schedule-replacer.dto.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { ApiProperty, PickType } from "@nestjs/swagger";
|
||||||
|
import { IsNumber, IsObject, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class ScheduleReplacerDto {
|
||||||
|
@ApiProperty({ description: "Etag заменяемого расписания" })
|
||||||
|
@IsString()
|
||||||
|
etag: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: "Данные файла расписания" })
|
||||||
|
@IsObject()
|
||||||
|
data: ArrayBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ScheduleReplacerResDto extends PickType(ScheduleReplacerDto, [
|
||||||
|
"etag",
|
||||||
|
]) {
|
||||||
|
@ApiProperty({ example: 1405, description: "Размер файла в байтах" })
|
||||||
|
@IsNumber()
|
||||||
|
size: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ClearScheduleReplacerResDto {
|
||||||
|
@ApiProperty({
|
||||||
|
example: 1,
|
||||||
|
description: "Количество удалённых заменителей расписания",
|
||||||
|
})
|
||||||
|
@IsNumber()
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
20
src/schedule-replacer/schedule-replacer.controller.spec.ts
Normal file
20
src/schedule-replacer/schedule-replacer.controller.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
|
import { ScheduleReplacerController } from "./schedule-replacer.controller";
|
||||||
|
|
||||||
|
describe("ScheduleReplacerController", () => {
|
||||||
|
let controller: ScheduleReplacerController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [ScheduleReplacerController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<ScheduleReplacerController>(
|
||||||
|
ScheduleReplacerController,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be defined", () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
106
src/schedule-replacer/schedule-replacer.controller.ts
Normal file
106
src/schedule-replacer/schedule-replacer.controller.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Post,
|
||||||
|
UploadedFile,
|
||||||
|
UseGuards,
|
||||||
|
UseInterceptors,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { AuthGuard } from "src/auth/auth.guard";
|
||||||
|
import {
|
||||||
|
ClearScheduleReplacerResDto,
|
||||||
|
ScheduleReplacerResDto,
|
||||||
|
} from "../dto/schedule-replacer.dto";
|
||||||
|
import { AuthRoles } from "../auth-role/auth-role.decorator";
|
||||||
|
import { UserRoleDto } from "../dto/user.dto";
|
||||||
|
import { ScheduleReplacerService } from "./schedule-replacer.service";
|
||||||
|
import { ScheduleService } from "../schedule/schedule.service";
|
||||||
|
import { FileInterceptor } from "@nestjs/platform-express";
|
||||||
|
import {
|
||||||
|
ApiExtraModels,
|
||||||
|
ApiOkResponse,
|
||||||
|
ApiOperation,
|
||||||
|
refs,
|
||||||
|
} from "@nestjs/swagger";
|
||||||
|
import { ResultDto } from "src/utility/validation/class-validator.interceptor";
|
||||||
|
|
||||||
|
@Controller("/api/v1/schedule-replacer")
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
export class ScheduleReplacerController {
|
||||||
|
constructor(
|
||||||
|
private readonly scheduleService: ScheduleService,
|
||||||
|
private readonly scheduleReplaceService: ScheduleReplacerService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@ApiOperation({
|
||||||
|
description: "Замена текущего расписание на новое",
|
||||||
|
tags: ["schedule", "replacer"],
|
||||||
|
})
|
||||||
|
@ApiOkResponse({ description: "Замена прошла успешно" })
|
||||||
|
@Post("set")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@AuthRoles([UserRoleDto.ADMIN])
|
||||||
|
@ResultDto(null)
|
||||||
|
@UseInterceptors(
|
||||||
|
FileInterceptor("file", { limits: { fileSize: 1024 * 1024 } }),
|
||||||
|
)
|
||||||
|
async setSchedule(
|
||||||
|
@UploadedFile() file: Express.Multer.File,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!file) throw new BadRequestException("Файл отсутствует");
|
||||||
|
if (file.mimetype !== "application/vnd.ms-excel")
|
||||||
|
throw new BadRequestException("Некорректный тип файла");
|
||||||
|
|
||||||
|
const etag = (await this.scheduleService.getSourceSchedule()).etag;
|
||||||
|
await this.scheduleReplaceService.setByEtag(etag, file.buffer);
|
||||||
|
await this.scheduleService.refreshCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiExtraModels(ScheduleReplacerResDto)
|
||||||
|
@ApiOperation({
|
||||||
|
description: "Получение списка заменителей расписания",
|
||||||
|
tags: ["schedule", "replacer"],
|
||||||
|
})
|
||||||
|
@ApiOkResponse({ description: "Список получен успешно" }) // TODO: ааа((((
|
||||||
|
@Get("get")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@AuthRoles([UserRoleDto.ADMIN])
|
||||||
|
@ResultDto(null) // TODO: Как нибудь сделать проверку в таких случаях
|
||||||
|
async getReplacers(): Promise<ScheduleReplacerResDto[]> {
|
||||||
|
const etag = (await this.scheduleService.getSourceSchedule()).etag;
|
||||||
|
|
||||||
|
const replacer = await this.scheduleReplaceService.getByEtag(etag);
|
||||||
|
if (!replacer) return [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
etag: replacer.etag,
|
||||||
|
size: replacer.data.byteLength,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiExtraModels(ClearScheduleReplacerResDto)
|
||||||
|
@ApiOperation({
|
||||||
|
description: "Удаление всех замен расписаний",
|
||||||
|
tags: ["schedule", "replacer"],
|
||||||
|
})
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: "Отчистка прошла успешно",
|
||||||
|
schema: refs(ClearScheduleReplacerResDto)[0],
|
||||||
|
})
|
||||||
|
@Post("clear")
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@AuthRoles([UserRoleDto.ADMIN])
|
||||||
|
@ResultDto(ClearScheduleReplacerResDto)
|
||||||
|
async clear(): Promise<ClearScheduleReplacerResDto> {
|
||||||
|
const resDto = { count: await this.scheduleReplaceService.clear() };
|
||||||
|
|
||||||
|
await this.scheduleService.refreshCache();
|
||||||
|
|
||||||
|
return resDto;
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/schedule-replacer/schedule-replacer.module.ts
Normal file
15
src/schedule-replacer/schedule-replacer.module.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { ScheduleReplacerService } from "./schedule-replacer.service";
|
||||||
|
import { PrismaService } from "../prisma/prisma.service";
|
||||||
|
import { ScheduleReplacerController } from "./schedule-replacer.controller";
|
||||||
|
import { ScheduleModule } from "../schedule/schedule.module";
|
||||||
|
import { AuthService } from "../auth/auth.service";
|
||||||
|
import { UsersModule } from "../users/users.module";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [ScheduleModule, UsersModule],
|
||||||
|
providers: [AuthService, PrismaService, ScheduleReplacerService],
|
||||||
|
exports: [ScheduleReplacerService],
|
||||||
|
controllers: [ScheduleReplacerController],
|
||||||
|
})
|
||||||
|
export class ScheduleReplacerModule {}
|
||||||
18
src/schedule-replacer/schedule-replacer.service.spec.ts
Normal file
18
src/schedule-replacer/schedule-replacer.service.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
|
import { ScheduleReplacerService } from "./schedule-replacer.service";
|
||||||
|
|
||||||
|
describe("ScheduleReplacerService", () => {
|
||||||
|
let service: ScheduleReplacerService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [ScheduleReplacerService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<ScheduleReplacerService>(ScheduleReplacerService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be defined", () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
48
src/schedule-replacer/schedule-replacer.service.ts
Normal file
48
src/schedule-replacer/schedule-replacer.service.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { PrismaService } from "../prisma/prisma.service";
|
||||||
|
import { ScheduleReplacerDto } from "../dto/schedule-replacer.dto";
|
||||||
|
import { plainToClass } from "class-transformer";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ScheduleReplacerService {
|
||||||
|
constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
|
async getByEtag(etag: string): Promise<ScheduleReplacerDto | null> {
|
||||||
|
const response = await this.prismaService.scheduleReplace.findUnique({
|
||||||
|
where: { etag: etag },
|
||||||
|
});
|
||||||
|
if (response == null) return null;
|
||||||
|
|
||||||
|
return plainToClass(ScheduleReplacerDto, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clear(): Promise<number> {
|
||||||
|
const count = await this.prismaService.scheduleReplace.count();
|
||||||
|
await this.prismaService.scheduleReplace.deleteMany({});
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setByEtag(etag: string, buffer: Buffer): Promise<void> {
|
||||||
|
if (
|
||||||
|
(await this.prismaService.scheduleReplace.count({
|
||||||
|
where: { etag: etag },
|
||||||
|
})) > 0
|
||||||
|
) {
|
||||||
|
await this.prismaService.scheduleReplace.update({
|
||||||
|
where: { etag: etag },
|
||||||
|
data: {
|
||||||
|
data: buffer,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.prismaService.scheduleReplace.create({
|
||||||
|
data: {
|
||||||
|
etag: etag,
|
||||||
|
data: buffer,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -290,6 +290,10 @@ export class ScheduleParser {
|
|||||||
const llesson = lday.lessons[lessonIdx];
|
const llesson = lday.lessons[lessonIdx];
|
||||||
// noinspection SpellCheckingInspection
|
// noinspection SpellCheckingInspection
|
||||||
const rlesson = rday.lessons[lessonIdx];
|
const rlesson = rday.lessons[lessonIdx];
|
||||||
|
|
||||||
|
if (llesson === null && rlesson === null) continue;
|
||||||
|
if (!llesson || !rlesson) return false;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
llesson.name.length > 0 &&
|
llesson.name.length > 0 &&
|
||||||
(llesson.name !== rlesson.name ||
|
(llesson.name !== rlesson.name ||
|
||||||
|
|||||||
@@ -9,11 +9,22 @@ import {
|
|||||||
NotAcceptableException,
|
NotAcceptableException,
|
||||||
ServiceUnavailableException,
|
ServiceUnavailableException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
|
import { ScheduleReplacerService } from "../../../schedule-replacer/schedule-replacer.service";
|
||||||
|
import { Error } from "mongoose";
|
||||||
|
import * as crypto from "crypto";
|
||||||
|
|
||||||
export class BasicXlsDownloader extends XlsDownloaderBase {
|
export class BasicXlsDownloader extends XlsDownloaderBase {
|
||||||
cache: XlsDownloaderResult | null = null;
|
cache: XlsDownloaderResult | null = null;
|
||||||
preparedData: { downloadLink: string; updateDate: string } | null = null;
|
preparedData: { downloadLink: string; updateDate: string } | null = null;
|
||||||
|
|
||||||
|
private cacheHash: string = "0000000000000000000000000000000000000000";
|
||||||
|
|
||||||
private lastUpdate: number = 0;
|
private lastUpdate: number = 0;
|
||||||
|
private scheduleReplacerService: ScheduleReplacerService | null = null;
|
||||||
|
|
||||||
|
setScheduleReplacerService(service: ScheduleReplacerService) {
|
||||||
|
this.scheduleReplacerService = service;
|
||||||
|
}
|
||||||
|
|
||||||
private async getDOM(preparedData: any): Promise<JSDOM | null> {
|
private async getDOM(preparedData: any): Promise<JSDOM | null> {
|
||||||
try {
|
try {
|
||||||
@@ -103,17 +114,32 @@ export class BasicXlsDownloader extends XlsDownloaderBase {
|
|||||||
${response.statusText}`);
|
${response.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const replacer = await this.scheduleReplacerService.getByEtag(
|
||||||
|
response.headers["etag"]!,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileData: ArrayBuffer = replacer
|
||||||
|
? replacer.data
|
||||||
|
: response.data.buffer;
|
||||||
|
|
||||||
|
const fileDataHash = crypto
|
||||||
|
.createHash("sha1")
|
||||||
|
.update(Buffer.from(fileData).toString("base64"))
|
||||||
|
.digest("hex");
|
||||||
|
|
||||||
const result: XlsDownloaderResult = {
|
const result: XlsDownloaderResult = {
|
||||||
fileData: response.data.buffer,
|
fileData: fileData,
|
||||||
updateDate: this.preparedData.updateDate,
|
updateDate: this.preparedData.updateDate,
|
||||||
etag: response.headers["etag"],
|
etag: response.headers["etag"],
|
||||||
new:
|
new:
|
||||||
this.cacheMode === XlsDownloaderCacheMode.NONE
|
this.cacheMode === XlsDownloaderCacheMode.NONE
|
||||||
? true
|
? true
|
||||||
: this.cache?.etag !== response.headers["etag"],
|
: this.cacheHash !== fileDataHash,
|
||||||
updateRequired: this.isUpdateRequired(),
|
updateRequired: this.isUpdateRequired(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.cacheHash = fileDataHash;
|
||||||
|
|
||||||
if (this.cacheMode !== XlsDownloaderCacheMode.NONE) this.cache = result;
|
if (this.cacheMode !== XlsDownloaderCacheMode.NONE) this.cache = result;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -3,9 +3,15 @@ import { ScheduleService } from "./schedule.service";
|
|||||||
import { ScheduleController } from "./schedule.controller";
|
import { ScheduleController } from "./schedule.controller";
|
||||||
import { UsersService } from "../users/users.service";
|
import { UsersService } from "../users/users.service";
|
||||||
import { PrismaService } from "../prisma/prisma.service";
|
import { PrismaService } from "../prisma/prisma.service";
|
||||||
|
import { ScheduleReplacerService } from "../schedule-replacer/schedule-replacer.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [ScheduleService, UsersService, PrismaService],
|
providers: [
|
||||||
|
ScheduleService,
|
||||||
|
ScheduleReplacerService,
|
||||||
|
UsersService,
|
||||||
|
PrismaService,
|
||||||
|
],
|
||||||
controllers: [ScheduleController],
|
controllers: [ScheduleController],
|
||||||
exports: [ScheduleService],
|
exports: [ScheduleService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { Cache, CACHE_MANAGER } from "@nestjs/cache-manager";
|
|||||||
import { instanceToPlain } from "class-transformer";
|
import { instanceToPlain } from "class-transformer";
|
||||||
import { cacheGetOrFill } from "../utility/cache.util";
|
import { cacheGetOrFill } from "../utility/cache.util";
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
|
import { ScheduleReplacerService } from "../schedule-replacer/schedule-replacer.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ScheduleService {
|
export class ScheduleService {
|
||||||
@@ -33,7 +34,18 @@ export class ScheduleService {
|
|||||||
private lastChangedDays: Array<Array<number>> = [];
|
private lastChangedDays: Array<Array<number>> = [];
|
||||||
private scheduleUpdatedAt: Date = new Date(0);
|
private scheduleUpdatedAt: Date = new Date(0);
|
||||||
|
|
||||||
constructor(@Inject(CACHE_MANAGER) private readonly cacheManager: Cache) {}
|
constructor(
|
||||||
|
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
|
||||||
|
private readonly scheduleReplacerService: ScheduleReplacerService,
|
||||||
|
) {
|
||||||
|
const xlsDownloader = this.scheduleParser.getXlsDownloader();
|
||||||
|
|
||||||
|
if (xlsDownloader instanceof BasicXlsDownloader) {
|
||||||
|
xlsDownloader.setScheduleReplacerService(
|
||||||
|
this.scheduleReplacerService,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getCacheStatus(): CacheStatusDto {
|
getCacheStatus(): CacheStatusDto {
|
||||||
return {
|
return {
|
||||||
@@ -45,7 +57,7 @@ export class ScheduleService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getSourceSchedule(): Promise<ScheduleParseResult> {
|
async getSourceSchedule(): Promise<ScheduleParseResult> {
|
||||||
return cacheGetOrFill(this.cacheManager, "sourceSchedule", async () => {
|
return cacheGetOrFill(this.cacheManager, "sourceSchedule", async () => {
|
||||||
const schedule = await this.scheduleParser.getSchedule();
|
const schedule = await this.scheduleParser.getSchedule();
|
||||||
schedule.groups = ScheduleService.toObject(
|
schedule.groups = ScheduleService.toObject(
|
||||||
@@ -146,10 +158,13 @@ export class ScheduleService {
|
|||||||
await this.scheduleParser
|
await this.scheduleParser
|
||||||
.getXlsDownloader()
|
.getXlsDownloader()
|
||||||
.setPreparedData(siteMainPageDto.mainPage);
|
.setPreparedData(siteMainPageDto.mainPage);
|
||||||
|
await this.refreshCache();
|
||||||
await this.cacheManager.reset();
|
|
||||||
await this.getSourceSchedule();
|
|
||||||
|
|
||||||
return this.getCacheStatus();
|
return this.getCacheStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async refreshCache() {
|
||||||
|
await this.cacheManager.reset();
|
||||||
|
await this.getSourceSchedule();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import { UsersService } from "./users.service";
|
|||||||
import { PrismaService } from "../prisma/prisma.service";
|
import { PrismaService } from "../prisma/prisma.service";
|
||||||
import { UsersController } from "./users.controller";
|
import { UsersController } from "./users.controller";
|
||||||
import { AuthService } from "../auth/auth.service";
|
import { AuthService } from "../auth/auth.service";
|
||||||
import { ScheduleService } from "../schedule/schedule.service";
|
import { ScheduleModule } from "../schedule/schedule.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [PrismaService, UsersService, AuthService, ScheduleService],
|
imports: [ScheduleModule],
|
||||||
|
providers: [PrismaService, UsersService, AuthService],
|
||||||
exports: [UsersService],
|
exports: [UsersService],
|
||||||
controllers: [UsersController],
|
controllers: [UsersController],
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user