mirror of
https://github.com/n08i40k/schedule-parser-next.git
synced 2025-12-06 09:47:46 +03:00
Много
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -54,3 +54,7 @@ pids
|
|||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# HTTPS cerificates
|
||||||
|
/cert/
|
||||||
|
*.pem
|
||||||
50
package-lock.json
generated
50
package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nestjs/cache-manager": "^2.2.2",
|
||||||
"@nestjs/common": "^10.0.0",
|
"@nestjs/common": "^10.0.0",
|
||||||
"@nestjs/core": "^10.0.0",
|
"@nestjs/core": "^10.0.0",
|
||||||
"@nestjs/jwt": "^10.2.0",
|
"@nestjs/jwt": "^10.2.0",
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
"@prisma/client": "^5.19.1",
|
"@prisma/client": "^5.19.1",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
|
"cache-manager": "^5.7.6",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
@@ -1626,6 +1628,17 @@
|
|||||||
"sparse-bitfield": "^3.0.3"
|
"sparse-bitfield": "^3.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nestjs/cache-manager": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-+n7rpU1QABeW2WV17Dl1vZCG3vWjJU1MaamWgZvbGxYE9EeCM0lVLfw3z7acgDTNwOy+K68xuQPoIMxD0bhjlA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@nestjs/common": "^9.0.0 || ^10.0.0",
|
||||||
|
"@nestjs/core": "^9.0.0 || ^10.0.0",
|
||||||
|
"cache-manager": "<=5",
|
||||||
|
"rxjs": "^7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nestjs/cli": {
|
"node_modules/@nestjs/cli": {
|
||||||
"version": "10.4.5",
|
"version": "10.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.5.tgz",
|
||||||
@@ -3370,6 +3383,25 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cache-manager": {
|
||||||
|
"version": "5.7.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.7.6.tgz",
|
||||||
|
"integrity": "sha512-wBxnBHjDxF1RXpHCBD6HGvKER003Ts7IIm0CHpggliHzN1RZditb7rXoduE1rplc2DEFYKxhLKgFuchXMJje9w==",
|
||||||
|
"dependencies": {
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
|
"lodash.clonedeep": "^4.5.0",
|
||||||
|
"lru-cache": "^10.2.2",
|
||||||
|
"promise-coalesce": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cache-manager/node_modules/lru-cache": {
|
||||||
|
"version": "10.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
|
||||||
|
},
|
||||||
"node_modules/call-bind": {
|
"node_modules/call-bind": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
||||||
@@ -4539,6 +4571,11 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
|
||||||
|
},
|
||||||
"node_modules/events": {
|
"node_modules/events": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||||
@@ -6780,6 +6817,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.clonedeep": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ=="
|
||||||
|
},
|
||||||
"node_modules/lodash.includes": {
|
"node_modules/lodash.includes": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
@@ -7808,6 +7850,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
|
||||||
},
|
},
|
||||||
|
"node_modules/promise-coalesce": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/promise-coalesce/-/promise-coalesce-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-zLaJ9b8hnC564fnJH6NFSOGZYYdzrAJn2JUUIwzoQb32fG2QAakpDNM+CZo1km6keXkRXRM+hml1BFAPVnPkxg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prompts": {
|
"node_modules/prompts": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@nestjs/cache-manager": "^2.2.2",
|
||||||
"@nestjs/common": "^10.0.0",
|
"@nestjs/common": "^10.0.0",
|
||||||
"@nestjs/core": "^10.0.0",
|
"@nestjs/core": "^10.0.0",
|
||||||
"@nestjs/jwt": "^10.2.0",
|
"@nestjs/jwt": "^10.2.0",
|
||||||
@@ -28,6 +29,7 @@
|
|||||||
"@prisma/client": "^5.19.1",
|
"@prisma/client": "^5.19.1",
|
||||||
"axios": "^1.7.7",
|
"axios": "^1.7.7",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
|
"cache-manager": "^5.7.6",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
|
|||||||
@@ -13,7 +13,13 @@ datasource db {
|
|||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
model user {
|
enum UserRole {
|
||||||
|
STUDENT
|
||||||
|
TEACHER
|
||||||
|
ADMIN
|
||||||
|
}
|
||||||
|
|
||||||
|
model User {
|
||||||
id String @id @map("_id") @db.ObjectId
|
id String @id @map("_id") @db.ObjectId
|
||||||
//
|
//
|
||||||
username String @unique
|
username String @unique
|
||||||
@@ -22,4 +28,7 @@ model user {
|
|||||||
password String
|
password String
|
||||||
//
|
//
|
||||||
accessToken String @unique
|
accessToken String @unique
|
||||||
|
//
|
||||||
|
group String
|
||||||
|
role UserRole
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,15 @@ import { Module } from "@nestjs/common";
|
|||||||
import { AuthModule } from "./auth/auth.module";
|
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";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AuthModule, UsersModule, ScheduleModule],
|
imports: [
|
||||||
|
AuthModule,
|
||||||
|
UsersModule,
|
||||||
|
ScheduleModule,
|
||||||
|
CacheModule.register({ ttl: 5 * 60 * 1000, isGlobal: true }),
|
||||||
|
],
|
||||||
controllers: [],
|
controllers: [],
|
||||||
providers: [],
|
providers: [],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,11 @@
|
|||||||
import { Body, Controller, HttpCode, HttpStatus, Post } from "@nestjs/common";
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
NotFoundException,
|
||||||
|
Post,
|
||||||
|
} from "@nestjs/common";
|
||||||
import { AuthService } from "./auth.service";
|
import { AuthService } from "./auth.service";
|
||||||
import {
|
import {
|
||||||
ApiBody,
|
ApiBody,
|
||||||
@@ -12,52 +19,66 @@ import {
|
|||||||
refs,
|
refs,
|
||||||
} from "@nestjs/swagger";
|
} from "@nestjs/swagger";
|
||||||
import {
|
import {
|
||||||
SignInDto,
|
SignInReqDto,
|
||||||
SignInResultDto,
|
SignInResDto,
|
||||||
SignUpDto,
|
SignUpReqDto,
|
||||||
SignUpResultDto,
|
SignUpResDto,
|
||||||
UpdateTokenDto,
|
UpdateTokenDto,
|
||||||
UpdateTokenResultDto,
|
UpdateTokenResultDto,
|
||||||
} 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";
|
||||||
|
|
||||||
@Controller("api/v1/auth")
|
@Controller("api/v1/auth")
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(private readonly authService: AuthService) {}
|
constructor(
|
||||||
|
private readonly scheduleService: ScheduleService,
|
||||||
|
private readonly authService: AuthService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@ApiExtraModels(SignInDto)
|
@ApiExtraModels(SignInReqDto)
|
||||||
@ApiExtraModels(SignInResultDto)
|
@ApiExtraModels(SignInResDto)
|
||||||
@ApiOperation({ summary: "Авторизация по логину и паролю", tags: ["auth"] })
|
@ApiOperation({ summary: "Авторизация по логину и паролю", tags: ["auth"] })
|
||||||
@ApiBody({ schema: refs(SignInDto)[0] })
|
@ApiBody({ schema: refs(SignInReqDto)[0] })
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
description: "Авторизация прошла успешно",
|
description: "Авторизация прошла успешно",
|
||||||
schema: refs(SignInResultDto)[0],
|
schema: refs(SignInResDto)[0],
|
||||||
})
|
})
|
||||||
@ApiUnauthorizedResponse({
|
@ApiUnauthorizedResponse({
|
||||||
description: "Некорректное имя пользователя или пароль",
|
description: "Некорректное имя пользователя или пароль",
|
||||||
})
|
})
|
||||||
@ResultDto(SignInResultDto)
|
@ResultDto(SignInResDto)
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Post("signIn")
|
@Post("signIn")
|
||||||
signIn(@Body() signInDto: SignInDto) {
|
signIn(@Body() signInDto: SignInReqDto) {
|
||||||
return this.authService.signIn(signInDto);
|
return this.authService.signIn(signInDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiExtraModels(SignUpDto)
|
@ApiExtraModels(SignUpReqDto)
|
||||||
@ApiExtraModels(SignUpResultDto)
|
@ApiExtraModels(SignUpResDto)
|
||||||
@ApiOperation({ summary: "Регистрация по логину и паролю", tags: ["auth"] })
|
@ApiOperation({ summary: "Регистрация по логину и паролю", tags: ["auth"] })
|
||||||
@ApiBody({ schema: refs(SignUpDto)[0] })
|
@ApiBody({ schema: refs(SignUpReqDto)[0] })
|
||||||
@ApiCreatedResponse({
|
@ApiCreatedResponse({
|
||||||
description: "Регистрация прошла успешно",
|
description: "Регистрация прошла успешно",
|
||||||
schema: refs(SignUpResultDto)[0],
|
schema: refs(SignUpResDto)[0],
|
||||||
})
|
})
|
||||||
@ApiConflictResponse({
|
@ApiConflictResponse({
|
||||||
description: "Такой пользователь уже существует",
|
description: "Такой пользователь уже существует",
|
||||||
})
|
})
|
||||||
@ResultDto(SignUpResultDto)
|
@ResultDto(SignUpResDto)
|
||||||
@HttpCode(HttpStatus.CREATED)
|
@HttpCode(HttpStatus.CREATED)
|
||||||
@Post("signUp")
|
@Post("signUp")
|
||||||
signUp(@Body() signUpDto: SignUpDto) {
|
async signUp(@Body() signUpDto: SignUpReqDto) {
|
||||||
|
if (
|
||||||
|
!(await this.scheduleService.getGroupNames()).names.includes(
|
||||||
|
signUpDto.group,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
"Передано название несуществующей группы",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return this.authService.signUp(signUpDto);
|
return this.authService.signUp(signUpDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { AuthGuard } from "./auth.guard";
|
|||||||
|
|
||||||
// TODO: Найти применение этой функции
|
// TODO: Найти применение этой функции
|
||||||
// noinspection JSUnusedGlobalSymbols
|
// noinspection JSUnusedGlobalSymbols
|
||||||
export const UserId = createParamDecorator((_, context: ExecutionContext) => {
|
export const UserToken = createParamDecorator((_, context: ExecutionContext) => {
|
||||||
return AuthGuard.extractTokenFromRequest(
|
return AuthGuard.extractTokenFromRequest(
|
||||||
context.switchToHttp().getRequest(),
|
context.switchToHttp().getRequest(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { AuthController } from "./auth.controller";
|
|||||||
import { UsersModule } from "../users/users.module";
|
import { UsersModule } from "../users/users.module";
|
||||||
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 { ScheduleService } from "../schedule/schedule.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -16,7 +17,7 @@ import { PrismaService } from "../prisma/prisma.service";
|
|||||||
signOptions: { expiresIn: "720h" },
|
signOptions: { expiresIn: "720h" },
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
providers: [AuthService, UsersService, PrismaService],
|
providers: [AuthService, UsersService, PrismaService, ScheduleService],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
exports: [AuthService],
|
exports: [AuthService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import {
|
|||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { JwtService } from "@nestjs/jwt";
|
import { JwtService } from "@nestjs/jwt";
|
||||||
import { user } from "@prisma/client";
|
|
||||||
import { UsersService } from "../users/users.service";
|
import { UsersService } from "../users/users.service";
|
||||||
|
import { UserDto } from "../dto/user.dto";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserFromTokenPipe implements PipeTransform {
|
export class UserFromTokenPipe implements PipeTransform {
|
||||||
@@ -14,7 +14,7 @@ export class UserFromTokenPipe implements PipeTransform {
|
|||||||
private readonly usersService: UsersService,
|
private readonly usersService: UsersService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async transform(token: string): Promise<user | null> {
|
async transform(token: string): Promise<UserDto> {
|
||||||
const jwtUser: { id: string } = await this.jwtService.decode(token);
|
const jwtUser: { id: string } = await this.jwtService.decode(token);
|
||||||
|
|
||||||
if (!jwtUser)
|
if (!jwtUser)
|
||||||
@@ -24,6 +24,6 @@ export class UserFromTokenPipe implements PipeTransform {
|
|||||||
if (!user)
|
if (!user)
|
||||||
throw new UnauthorizedException("Передан некорректный токен!");
|
throw new UnauthorizedException("Передан некорректный токен!");
|
||||||
|
|
||||||
return user;
|
return user as UserDto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
import {
|
import {
|
||||||
ConflictException,
|
ConflictException,
|
||||||
Injectable,
|
Injectable,
|
||||||
|
NotAcceptableException,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { JwtService } from "@nestjs/jwt";
|
import { JwtService } from "@nestjs/jwt";
|
||||||
import {
|
import {
|
||||||
SignInDto,
|
SignInReqDto,
|
||||||
SignInResultDto,
|
SignInResDto,
|
||||||
SignUpDto,
|
SignUpReqDto,
|
||||||
SignUpResultDto,
|
SignUpResDto,
|
||||||
UpdateTokenDto,
|
UpdateTokenDto,
|
||||||
UpdateTokenResultDto,
|
UpdateTokenResultDto,
|
||||||
} 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";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma, UserRole } from "@prisma/client";
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
|
import { UserDto, UserRoleDto } from "../dto/user.dto";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@@ -25,16 +27,34 @@ export class AuthService {
|
|||||||
private readonly jwtService: JwtService,
|
private readonly jwtService: JwtService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async signUp(signUpDto: SignUpDto): Promise<SignUpResultDto> {
|
async decodeUserToken(token: string): Promise<UserDto | null> {
|
||||||
if (await this.usersService.contains({ username: signUpDto.username }))
|
const jwtUser: { id: string } =
|
||||||
|
await this.jwtService.verifyAsync(token);
|
||||||
|
|
||||||
|
return this.usersService
|
||||||
|
.findUnique({ id: jwtUser.id })
|
||||||
|
.then((user) => user as UserDto | null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async signUp(signUpDto: SignUpReqDto): Promise<SignUpResDto> {
|
||||||
|
if (
|
||||||
|
![UserRoleDto.STUDENT, UserRoleDto.TEACHER].includes(signUpDto.role)
|
||||||
|
) {
|
||||||
|
throw new NotAcceptableException("Передана неизвестная роль");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
await this.usersService.contains({ username: signUpDto.username })
|
||||||
|
) {
|
||||||
throw new ConflictException(
|
throw new ConflictException(
|
||||||
"Пользователь с таким именем уже существует!",
|
"Пользователь с таким именем уже существует!",
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const salt = await genSalt(8);
|
const salt = await genSalt(8);
|
||||||
const id = new Types.ObjectId().toString("hex");
|
const id = new Types.ObjectId().toString("hex");
|
||||||
|
|
||||||
const input: Prisma.userCreateInput = {
|
const input: Prisma.UserCreateInput = {
|
||||||
id: id,
|
id: id,
|
||||||
username: signUpDto.username,
|
username: signUpDto.username,
|
||||||
salt: salt,
|
salt: salt,
|
||||||
@@ -42,6 +62,8 @@ export class AuthService {
|
|||||||
accessToken: await this.jwtService.signAsync({
|
accessToken: await this.jwtService.signAsync({
|
||||||
id: id,
|
id: id,
|
||||||
}),
|
}),
|
||||||
|
role: signUpDto.role as UserRole,
|
||||||
|
group: signUpDto.group,
|
||||||
};
|
};
|
||||||
|
|
||||||
return this.usersService.create(input).then((user) => {
|
return this.usersService.create(input).then((user) => {
|
||||||
@@ -52,7 +74,7 @@ export class AuthService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async signIn(signInDto: SignInDto): Promise<SignInResultDto> {
|
async signIn(signInDto: SignInReqDto): Promise<SignInResDto> {
|
||||||
const user = await this.usersService.findUnique({
|
const user = await this.usersService.findUnique({
|
||||||
username: signInDto.username,
|
username: signInDto.username,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { configDotenv } from "dotenv";
|
import { configDotenv } from "dotenv";
|
||||||
|
|
||||||
configDotenv();
|
configDotenv();
|
||||||
|
|
||||||
export const jwtConstants = {
|
export const jwtConstants = {
|
||||||
secret: process.env.JWT_SECRET!,
|
secret: process.env.JWT_SECRET!,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const httpsConstants = {
|
||||||
|
certPath: process.env.CERT_PEM_PATH!,
|
||||||
|
keyPath: process.env.KEY_PEM_PATH!,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
import { ApiProperty, 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";
|
||||||
|
|
||||||
export class SignInDto extends PickType(UserDto, ["username"]) {
|
// SignIn
|
||||||
|
export class SignInReqDto extends PickType(UserDto, ["username"]) {
|
||||||
@ApiProperty({ description: "Пароль в исходном виде" })
|
@ApiProperty({ description: "Пароль в исходном виде" })
|
||||||
@IsString()
|
@IsString()
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SignInResultDto extends PickType(UserDto, ["id", "accessToken"]) {}
|
export class SignInResDto extends PickType(UserDto, ["id", "accessToken"]) {}
|
||||||
|
|
||||||
export class SignUpDto extends SignInDto {}
|
// SignUp
|
||||||
|
export class SignUpReqDto extends IntersectionType(
|
||||||
|
SignInReqDto,
|
||||||
|
PickType(UserDto, ["role", "group"]),
|
||||||
|
) {}
|
||||||
|
|
||||||
export class SignUpResultDto extends SignInResultDto {}
|
export class SignUpResDto extends SignInResDto {}
|
||||||
|
|
||||||
|
// Update token
|
||||||
export class UpdateTokenDto extends PickType(UserDto, ["accessToken"]) {}
|
export class UpdateTokenDto extends PickType(UserDto, ["accessToken"]) {}
|
||||||
|
|
||||||
export class UpdateTokenResultDto extends UpdateTokenDto {}
|
export class UpdateTokenResultDto extends UpdateTokenDto {}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import {
|
|||||||
IsString,
|
IsString,
|
||||||
ValidateNested,
|
ValidateNested,
|
||||||
} from "class-validator";
|
} from "class-validator";
|
||||||
import { ApiProperty } from "@nestjs/swagger";
|
import { ApiProperty, OmitType, PickType } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Transform, Type } from "class-transformer";
|
||||||
|
|
||||||
export class LessonTimeDto {
|
export class LessonTimeDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
@@ -49,8 +49,7 @@ export class LessonTimeDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum LessonTypeDto {
|
export enum LessonTypeDto {
|
||||||
NONE = 0,
|
DEFAULT = 0,
|
||||||
DEFAULT,
|
|
||||||
CUSTOM,
|
CUSTOM,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,8 +137,9 @@ export class DayDto {
|
|||||||
@ApiProperty({ example: [], description: "Занятия" })
|
@ApiProperty({ example: [], description: "Занятия" })
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
|
@IsOptional()
|
||||||
@Type(() => LessonDto)
|
@Type(() => LessonDto)
|
||||||
lessons: Array<LessonDto>;
|
lessons: Array<LessonDto | null>;
|
||||||
|
|
||||||
constructor(name: string) {
|
constructor(name: string) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@@ -160,7 +160,7 @@ export class DayDto {
|
|||||||
const lessonIdx = Number.parseInt(lessonRawIdx);
|
const lessonIdx = Number.parseInt(lessonRawIdx);
|
||||||
|
|
||||||
const lesson = this.lessons[lessonIdx];
|
const lesson = this.lessons[lessonIdx];
|
||||||
if (lesson.type === LessonTypeDto.NONE) continue;
|
if (lesson === null) continue;
|
||||||
|
|
||||||
this.nonNullIndices.push(lessonIdx);
|
this.nonNullIndices.push(lessonIdx);
|
||||||
|
|
||||||
@@ -183,8 +183,9 @@ export class GroupDto {
|
|||||||
@ApiProperty({ example: [], description: "Дни недели" })
|
@ApiProperty({ example: [], description: "Дни недели" })
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
|
@IsOptional()
|
||||||
@Type(() => DayDto)
|
@Type(() => DayDto)
|
||||||
days: Array<DayDto>;
|
days: Array<DayDto | null>;
|
||||||
|
|
||||||
constructor(name: string) {
|
constructor(name: string) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
@@ -208,9 +209,48 @@ export class ScheduleDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
etag: string;
|
etag: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: "Расписание групп" })
|
||||||
|
@IsObject()
|
||||||
|
@IsOptional()
|
||||||
|
groups: any;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: { "ИС-214/23": [5, 6] },
|
||||||
|
description: "Обновлённые дни с последнего изменения расписания",
|
||||||
|
})
|
||||||
|
@IsObject()
|
||||||
|
@Type(() => Object)
|
||||||
|
@Transform(({ value }) => {
|
||||||
|
const object = {};
|
||||||
|
|
||||||
|
for (const key in value) {
|
||||||
|
object[key] = value[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return object;
|
||||||
|
})
|
||||||
|
@Type(() => Object)
|
||||||
|
lastChangedDays: Array<Array<number>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GroupScheduleRequestDto extends PickType(GroupDto, ["name"]) {}
|
||||||
|
|
||||||
|
export class ScheduleGroupsDto {
|
||||||
|
@ApiProperty({
|
||||||
|
example: ["ИС-214/23", "ИС-213/23"],
|
||||||
|
description: "Список названий всех групп в текущем расписании",
|
||||||
|
})
|
||||||
|
@IsArray()
|
||||||
|
names: Array<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GroupScheduleDto extends OmitType(ScheduleDto, [
|
||||||
|
"groups",
|
||||||
|
"lastChangedDays",
|
||||||
|
]) {
|
||||||
@ApiProperty({ description: "Расписание группы" })
|
@ApiProperty({ description: "Расписание группы" })
|
||||||
@IsObject()
|
@IsObject()
|
||||||
data: GroupDto;
|
group: GroupDto;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: [5, 6],
|
example: [5, 6],
|
||||||
|
|||||||
@@ -1,36 +1,79 @@
|
|||||||
import { ApiProperty, OmitType } from "@nestjs/swagger";
|
import { ApiProperty, OmitType } from "@nestjs/swagger";
|
||||||
import {
|
import {
|
||||||
|
IsEnum,
|
||||||
IsJWT,
|
IsJWT,
|
||||||
IsMongoId,
|
IsMongoId,
|
||||||
IsString,
|
IsString,
|
||||||
MaxLength,
|
MaxLength,
|
||||||
MinLength,
|
MinLength,
|
||||||
} from "class-validator";
|
} from "class-validator";
|
||||||
|
import { Expose, plainToClass } from "class-transformer";
|
||||||
|
|
||||||
|
export enum UserRoleDto {
|
||||||
|
STUDENT = "STUDENT",
|
||||||
|
TEACHER = "TEACHER",
|
||||||
|
ADMIN = "ADMIN",
|
||||||
|
}
|
||||||
|
|
||||||
export class UserDto {
|
export class UserDto {
|
||||||
@ApiProperty({ description: "Идентификатор (ObjectId)" })
|
@ApiProperty({ description: "Идентификатор (ObjectId)" })
|
||||||
@IsMongoId()
|
@IsMongoId()
|
||||||
|
@Expose()
|
||||||
id: string;
|
id: string;
|
||||||
|
|
||||||
@ApiProperty({ example: "n08i40k", description: "Имя" })
|
@ApiProperty({ example: "n08i40k", description: "Имя" })
|
||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(4)
|
@MinLength(4)
|
||||||
@MaxLength(10)
|
@MaxLength(10)
|
||||||
|
@Expose()
|
||||||
username: string;
|
username: string;
|
||||||
@ApiProperty({ description: "Соль пароля" })
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: "$2b$08$34xwFv1WVJpvpVi3tZZuv.",
|
||||||
|
description: "Соль пароля",
|
||||||
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@Expose()
|
||||||
salt: string;
|
salt: string;
|
||||||
@ApiProperty({ description: "Хеш пароля" })
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: "$2b$08$34xwFv1WVJpvpVi3tZZuv...",
|
||||||
|
description: "Хеш пароля",
|
||||||
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
|
@Expose()
|
||||||
password: string;
|
password: string;
|
||||||
@ApiProperty({ description: "Последний токен доступа" })
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||||
|
description: "Последний токен доступа",
|
||||||
|
})
|
||||||
@IsJWT()
|
@IsJWT()
|
||||||
|
@Expose()
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: "ИС-214/23", description: "Группа пользователя" })
|
||||||
|
@IsString()
|
||||||
|
@Expose()
|
||||||
|
group: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
example: UserRoleDto.STUDENT,
|
||||||
|
description: "Роль пользователя",
|
||||||
|
})
|
||||||
|
@IsEnum(UserRoleDto)
|
||||||
|
@Expose()
|
||||||
|
role: UserRoleDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Доделать пользователей
|
|
||||||
// noinspection JSUnusedGlobalSymbols
|
|
||||||
export class ClientUserDto extends OmitType(UserDto, [
|
export class ClientUserDto extends OmitType(UserDto, [
|
||||||
"password",
|
"password",
|
||||||
"salt",
|
"salt",
|
||||||
"accessToken",
|
"accessToken",
|
||||||
]) {}
|
]) {
|
||||||
|
static fromUserDto(userDto: UserDto): ClientUserDto {
|
||||||
|
return plainToClass(ClientUserDto, userDto, {
|
||||||
|
excludeExtraneousValues: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
12
src/main.ts
12
src/main.ts
@@ -4,9 +4,19 @@ import { ValidatorOptions } from "class-validator";
|
|||||||
import { PartialValidationPipe } from "./utility/validation/partial-validation.pipe";
|
import { PartialValidationPipe } from "./utility/validation/partial-validation.pipe";
|
||||||
import { ClassValidatorInterceptor } from "./utility/validation/class-validator.interceptor";
|
import { ClassValidatorInterceptor } from "./utility/validation/class-validator.interceptor";
|
||||||
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
|
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
|
||||||
|
import { httpsConstants } from "./contants";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule, {
|
||||||
|
httpsOptions: {
|
||||||
|
cert: fs.readFileSync(
|
||||||
|
path.join(__dirname, httpsConstants.certPath),
|
||||||
|
),
|
||||||
|
key: fs.readFileSync(path.join(__dirname, httpsConstants.keyPath)),
|
||||||
|
},
|
||||||
|
});
|
||||||
const validatorOptions: ValidatorOptions = {
|
const validatorOptions: ValidatorOptions = {
|
||||||
enableDebugMessages: true,
|
enableDebugMessages: true,
|
||||||
forbidNonWhitelisted: true,
|
forbidNonWhitelisted: true,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
XlsDownloaderBase,
|
XlsDownloaderBase,
|
||||||
XlsDownloaderCacheMode,
|
XlsDownloaderCacheMode,
|
||||||
XlsDownloaderResult,
|
|
||||||
} from "../xls-downloader/xls-downloader.base";
|
} from "../xls-downloader/xls-downloader.base";
|
||||||
|
|
||||||
import * as XLSX from "xlsx";
|
import * as XLSX from "xlsx";
|
||||||
@@ -17,11 +16,11 @@ import { trimAll } from "../../../utility/string.util";
|
|||||||
type InternalId = { row: number; column: number; name: string };
|
type InternalId = { row: number; column: number; name: string };
|
||||||
type InternalDay = InternalId & { lessons: Array<InternalId> };
|
type InternalDay = InternalId & { lessons: Array<InternalId> };
|
||||||
|
|
||||||
export type ScheduleParseResult = {
|
export class ScheduleParseResult {
|
||||||
etag: string;
|
etag: string;
|
||||||
group: GroupDto;
|
groups: Array<GroupDto>;
|
||||||
affectedDays: Array<number>;
|
affectedDays: Array<Array<number>>;
|
||||||
};
|
}
|
||||||
|
|
||||||
export class ScheduleParser {
|
export class ScheduleParser {
|
||||||
private lastResult: ScheduleParseResult | null = null;
|
private lastResult: ScheduleParseResult | null = null;
|
||||||
@@ -72,13 +71,13 @@ export class ScheduleParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parseSkeleton(worksheet: XLSX.Sheet): {
|
parseSkeleton(worksheet: XLSX.Sheet): {
|
||||||
groupSkeleton: InternalId;
|
groupSkeletons: Array<InternalId>;
|
||||||
daySkeletons: Array<InternalDay>;
|
daySkeletons: Array<InternalDay>;
|
||||||
} {
|
} {
|
||||||
const range = XLSX.utils.decode_range(worksheet["!ref"] || "");
|
const range = XLSX.utils.decode_range(worksheet["!ref"] || "");
|
||||||
let isHeaderParsed: boolean = false;
|
let isHeaderParsed: boolean = false;
|
||||||
|
|
||||||
let group: InternalId = null;
|
const groups: Array<InternalId> = [];
|
||||||
const days: Array<InternalDay> = [];
|
const days: Array<InternalDay> = [];
|
||||||
|
|
||||||
for (let row = range.s.r + 1; row <= range.e.r; ++row) {
|
for (let row = range.s.r + 1; row <= range.e.r; ++row) {
|
||||||
@@ -99,10 +98,9 @@ export class ScheduleParser {
|
|||||||
row,
|
row,
|
||||||
column,
|
column,
|
||||||
);
|
);
|
||||||
if (!groupName || this.group !== groupName) continue;
|
if (!groupName) continue;
|
||||||
|
|
||||||
group = { row: row, column: column, name: groupName };
|
groups.push({ row: row, column: column, name: groupName });
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
++row;
|
++row;
|
||||||
}
|
}
|
||||||
@@ -116,25 +114,20 @@ export class ScheduleParser {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { daySkeletons: days, groupSkeleton: group };
|
return { daySkeletons: days, groupSkeletons: groups };
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSchedule(
|
async getSchedule(
|
||||||
forceCached: boolean = false,
|
forceCached: boolean = false,
|
||||||
): Promise<ScheduleParseResult> {
|
): Promise<ScheduleParseResult> {
|
||||||
let downloadData: XlsDownloaderResult;
|
if (forceCached && this.lastResult !== null) return this.lastResult;
|
||||||
|
|
||||||
if (
|
const downloadData = await this.xlsDownloader.downloadXLS();
|
||||||
!forceCached ||
|
|
||||||
(downloadData = await this.xlsDownloader.getCachedXLS()) === null
|
|
||||||
) {
|
|
||||||
console.debug("Обновление кеша...");
|
|
||||||
downloadData = await this.xlsDownloader.downloadXLS();
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!downloadData.new &&
|
!downloadData.new &&
|
||||||
this.lastResult &&
|
this.lastResult &&
|
||||||
this.xlsDownloader.getCacheMode() != XlsDownloaderCacheMode.NONE
|
this.xlsDownloader.getCacheMode() !== XlsDownloaderCacheMode.NONE
|
||||||
) {
|
) {
|
||||||
console.debug(
|
console.debug(
|
||||||
"Так как скачанный XLS не новый, присутствует уже готовый результат и кеширование не отключено...",
|
"Так как скачанный XLS не новый, присутствует уже готовый результат и кеширование не отключено...",
|
||||||
@@ -143,15 +136,17 @@ export class ScheduleParser {
|
|||||||
|
|
||||||
return this.lastResult;
|
return this.lastResult;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
console.debug("Чтение кешированного XLS документа...");
|
console.debug("Чтение кешированного XLS документа...");
|
||||||
|
|
||||||
const workBook = XLSX.read(downloadData.fileData);
|
const workBook = XLSX.read(downloadData.fileData);
|
||||||
const workSheet = workBook.Sheets[workBook.SheetNames[0]];
|
const workSheet = workBook.Sheets[workBook.SheetNames[0]];
|
||||||
|
|
||||||
const { groupSkeleton, daySkeletons } = this.parseSkeleton(workSheet);
|
const { groupSkeletons, daySkeletons } = this.parseSkeleton(workSheet);
|
||||||
|
|
||||||
|
const groups: Array<GroupDto> = [];
|
||||||
|
|
||||||
|
for (const groupSkeleton of groupSkeletons) {
|
||||||
const group = new GroupDto(groupSkeleton.name);
|
const group = new GroupDto(groupSkeleton.name);
|
||||||
|
|
||||||
for (let dayIdx = 0; dayIdx < daySkeletons.length - 1; ++dayIdx) {
|
for (let dayIdx = 0; dayIdx < daySkeletons.length - 1; ++dayIdx) {
|
||||||
@@ -159,7 +154,8 @@ export class ScheduleParser {
|
|||||||
const day = new DayDto(daySkeleton.name);
|
const day = new DayDto(daySkeleton.name);
|
||||||
|
|
||||||
const lessonTimeColumn = daySkeletons[0].column + 1;
|
const lessonTimeColumn = daySkeletons[0].column + 1;
|
||||||
const rowDistance = daySkeletons[dayIdx + 1].row - daySkeleton.row;
|
const rowDistance =
|
||||||
|
daySkeletons[dayIdx + 1].row - daySkeleton.row;
|
||||||
|
|
||||||
for (
|
for (
|
||||||
let row = daySkeleton.row;
|
let row = daySkeleton.row;
|
||||||
@@ -188,7 +184,8 @@ export class ScheduleParser {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (rawCabinets !== "null") {
|
if (rawCabinets !== "null") {
|
||||||
const rawLessonCabinetParts = rawCabinets.split(/(\n|\s)/g);
|
const rawLessonCabinetParts =
|
||||||
|
rawCabinets.split(/(\n|\s)/g);
|
||||||
|
|
||||||
for (const cabinet of rawLessonCabinetParts) {
|
for (const cabinet of rawLessonCabinetParts) {
|
||||||
if (
|
if (
|
||||||
@@ -202,14 +199,17 @@ export class ScheduleParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const type =
|
if (!rawName || rawName.length === 0) {
|
||||||
!rawName || rawName.length === 0
|
day.lessons.push(null);
|
||||||
? LessonTypeDto.NONE
|
continue;
|
||||||
: time?.includes("пара")
|
}
|
||||||
|
|
||||||
|
const type = time?.includes("пара")
|
||||||
? LessonTypeDto.DEFAULT
|
? LessonTypeDto.DEFAULT
|
||||||
: LessonTypeDto.CUSTOM;
|
: LessonTypeDto.CUSTOM;
|
||||||
|
|
||||||
const { name, teacherFullNames } = this.parseTeacherFullNames(
|
const { name, teacherFullNames } =
|
||||||
|
this.parseTeacherFullNames(
|
||||||
trimAll(rawName?.replace("\n", "") ?? ""),
|
trimAll(rawName?.replace("\n", "") ?? ""),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -229,23 +229,28 @@ export class ScheduleParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
day.fillIndices();
|
day.fillIndices();
|
||||||
group.days.push(day);
|
|
||||||
|
if (day.nonNullIndices.length == 0) group.days.push(null);
|
||||||
|
else group.days.push(day);
|
||||||
|
}
|
||||||
|
|
||||||
|
groups[group.name] = group;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (this.lastResult = {
|
return (this.lastResult = {
|
||||||
etag: downloadData.etag,
|
etag: downloadData.etag,
|
||||||
group: group,
|
groups: groups,
|
||||||
affectedDays: this.getAffectedDays(this.lastResult?.group, group),
|
affectedDays: this.getAffectedDays(this.lastResult?.groups, groups),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private getAffectedDays(
|
private getAffectedDays(
|
||||||
cachedGroup: GroupDto | null,
|
cachedGroups: Array<GroupDto> | null,
|
||||||
group: GroupDto,
|
groups: Array<GroupDto>,
|
||||||
): Array<number> {
|
): Array<Array<number>> {
|
||||||
const affectedDays: Array<number> = [];
|
const affectedDays: Array<Array<number>> = [];
|
||||||
|
|
||||||
if (!cachedGroup) return affectedDays;
|
if (!cachedGroups) return affectedDays;
|
||||||
|
|
||||||
// noinspection SpellCheckingInspection
|
// noinspection SpellCheckingInspection
|
||||||
const dayEquals = (lday: DayDto | null, rday: DayDto): boolean => {
|
const dayEquals = (lday: DayDto | null, rday: DayDto): boolean => {
|
||||||
@@ -276,6 +281,12 @@ export class ScheduleParser {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
for (const groupName in cachedGroups) {
|
||||||
|
const cachedGroup = cachedGroups[groupName];
|
||||||
|
const group = groups[groupName];
|
||||||
|
|
||||||
|
const affectedGroupDays: Array<number> = [];
|
||||||
|
|
||||||
for (const dayIdx in group.days) {
|
for (const dayIdx in group.days) {
|
||||||
// noinspection SpellCheckingInspection
|
// noinspection SpellCheckingInspection
|
||||||
const lday = group.days[dayIdx];
|
const lday = group.days[dayIdx];
|
||||||
@@ -283,7 +294,10 @@ export class ScheduleParser {
|
|||||||
const rday = cachedGroup.days[dayIdx];
|
const rday = cachedGroup.days[dayIdx];
|
||||||
|
|
||||||
if (!dayEquals(lday, rday))
|
if (!dayEquals(lday, rday))
|
||||||
affectedDays.push(Number.parseInt(dayIdx));
|
affectedGroupDays.push(Number.parseInt(dayIdx));
|
||||||
|
}
|
||||||
|
|
||||||
|
affectedDays[groupName] = affectedGroupDays;
|
||||||
}
|
}
|
||||||
|
|
||||||
return affectedDays;
|
return affectedDays;
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
import {
|
import {
|
||||||
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
HttpStatus,
|
HttpStatus, Post,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { AuthGuard } from "../auth/auth.guard";
|
import { AuthGuard } from "../auth/auth.guard";
|
||||||
import { ScheduleService } from "./schedule.service";
|
import { ScheduleService } from "./schedule.service";
|
||||||
import { ScheduleDto } from "../dto/schedule.dto";
|
import {
|
||||||
|
GroupScheduleDto,
|
||||||
|
GroupScheduleRequestDto,
|
||||||
|
ScheduleDto,
|
||||||
|
ScheduleGroupsDto,
|
||||||
|
} from "../dto/schedule.dto";
|
||||||
import { ResultDto } from "../utility/validation/class-validator.interceptor";
|
import { ResultDto } from "../utility/validation/class-validator.interceptor";
|
||||||
import {
|
import {
|
||||||
ApiExtraModels,
|
ApiExtraModels,
|
||||||
|
ApiNotFoundResponse,
|
||||||
ApiOkResponse,
|
ApiOkResponse,
|
||||||
ApiOperation,
|
ApiOperation,
|
||||||
refs,
|
refs,
|
||||||
@@ -19,7 +26,7 @@ import {
|
|||||||
@Controller("api/v1/schedule")
|
@Controller("api/v1/schedule")
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
export class ScheduleController {
|
export class ScheduleController {
|
||||||
constructor(private scheduleService: ScheduleService) {}
|
constructor(private readonly scheduleService: ScheduleService) {}
|
||||||
|
|
||||||
@ApiExtraModels(ScheduleDto)
|
@ApiExtraModels(ScheduleDto)
|
||||||
@ApiOperation({ summary: "Получение расписания", tags: ["schedule"] })
|
@ApiOperation({ summary: "Получение расписания", tags: ["schedule"] })
|
||||||
@@ -33,4 +40,40 @@ export class ScheduleController {
|
|||||||
getSchedule(): Promise<ScheduleDto> {
|
getSchedule(): Promise<ScheduleDto> {
|
||||||
return this.scheduleService.getSchedule();
|
return this.scheduleService.getSchedule();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ApiExtraModels(GroupScheduleDto)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Получение расписания группы",
|
||||||
|
tags: ["schedule"],
|
||||||
|
})
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: "Расписание получено успешно",
|
||||||
|
schema: refs(GroupScheduleDto)[0],
|
||||||
|
})
|
||||||
|
@ApiNotFoundResponse({ description: "Требуемая группа не найдена" })
|
||||||
|
@ResultDto(GroupScheduleDto)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post("getGroup")
|
||||||
|
getGroupSchedule(
|
||||||
|
@Body() groupDto: GroupScheduleRequestDto,
|
||||||
|
): Promise<GroupScheduleDto> {
|
||||||
|
return this.scheduleService.getGroup(groupDto.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiExtraModels(ScheduleGroupsDto)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "Получение списка названий всех групп в расписании",
|
||||||
|
tags: ["schedule"],
|
||||||
|
})
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: "Список получен успешно",
|
||||||
|
schema: refs(ScheduleGroupsDto)[0],
|
||||||
|
})
|
||||||
|
@ApiNotFoundResponse({ description: "Требуемая группа не найдена" })
|
||||||
|
@ResultDto(ScheduleGroupsDto)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Get("getGroupNames")
|
||||||
|
async getGroupNames(): Promise<ScheduleGroupsDto> {
|
||||||
|
return this.scheduleService.getGroupNames();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { UsersService } from "../users/users.service";
|
|||||||
import { PrismaService } from "../prisma/prisma.service";
|
import { PrismaService } from "../prisma/prisma.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
|
||||||
providers: [ScheduleService, UsersService, PrismaService],
|
providers: [ScheduleService, UsersService, PrismaService],
|
||||||
controllers: [ScheduleController],
|
controllers: [ScheduleController],
|
||||||
exports: [ScheduleService],
|
exports: [ScheduleService],
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
import { Inject, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
import { ScheduleParser } from "./internal/schedule-parser/schedule-parser";
|
import {
|
||||||
|
ScheduleParser,
|
||||||
|
ScheduleParseResult,
|
||||||
|
} from "./internal/schedule-parser/schedule-parser";
|
||||||
import { BasicXlsDownloader } from "./internal/xls-downloader/basic-xls-downloader";
|
import { BasicXlsDownloader } from "./internal/xls-downloader/basic-xls-downloader";
|
||||||
import { XlsDownloaderCacheMode } from "./internal/xls-downloader/xls-downloader.base";
|
import { XlsDownloaderCacheMode } from "./internal/xls-downloader/xls-downloader.base";
|
||||||
import { ScheduleDto } from "../dto/schedule.dto";
|
import {
|
||||||
|
GroupDto,
|
||||||
|
GroupScheduleDto,
|
||||||
|
ScheduleDto,
|
||||||
|
ScheduleGroupsDto,
|
||||||
|
} from "../dto/schedule.dto";
|
||||||
|
import { Cache, CACHE_MANAGER } from "@nestjs/cache-manager";
|
||||||
|
import { instanceToPlain } from "class-transformer";
|
||||||
|
import { cacheGetOrFill } from "../utility/cache.util";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ScheduleService {
|
export class ScheduleService {
|
||||||
@@ -15,26 +26,86 @@ export class ScheduleService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
private lastCacheUpdate: Date = new Date(0);
|
private lastCacheUpdate: Date = new Date(0);
|
||||||
private lastChangedDays: Array<number> = [];
|
private lastChangedDays: Array<Array<number>> = [];
|
||||||
|
|
||||||
constructor() {}
|
constructor(@Inject(CACHE_MANAGER) private readonly cacheManager: Cache) {}
|
||||||
|
|
||||||
|
private async getSourceSchedule(): Promise<ScheduleParseResult> {
|
||||||
|
return cacheGetOrFill(this.cacheManager, "sourceSchedule", async () => {
|
||||||
|
this.lastCacheUpdate = new Date();
|
||||||
|
|
||||||
|
const schedule = await this.scheduleParser.getSchedule();
|
||||||
|
schedule.groups = ScheduleService.toObject(
|
||||||
|
schedule.groups,
|
||||||
|
) as Array<GroupDto>;
|
||||||
|
|
||||||
|
return schedule;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static toObject<T>(array: Array<T>): object {
|
||||||
|
const object = {};
|
||||||
|
|
||||||
|
for (const item in array) object[item] = array[item];
|
||||||
|
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
async getSchedule(): Promise<ScheduleDto> {
|
async getSchedule(): Promise<ScheduleDto> {
|
||||||
const now = new Date();
|
return cacheGetOrFill(this.cacheManager, "schedule", async () => {
|
||||||
const cacheExpired =
|
const sourceSchedule = await this.getSourceSchedule();
|
||||||
(this.lastCacheUpdate.valueOf() - now.valueOf()) / 1000 / 60 > 5;
|
|
||||||
|
|
||||||
if (cacheExpired) this.lastCacheUpdate = now;
|
for (const groupName in sourceSchedule.affectedDays) {
|
||||||
|
const affectedDays = sourceSchedule.affectedDays[groupName];
|
||||||
|
|
||||||
const schedule = await this.scheduleParser.getSchedule(!cacheExpired);
|
if (affectedDays?.length !== 0)
|
||||||
if (schedule.affectedDays.length !== 0)
|
this.lastChangedDays[groupName] = affectedDays;
|
||||||
this.lastChangedDays = schedule.affectedDays;
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
updatedAt: this.lastCacheUpdate,
|
updatedAt: this.lastCacheUpdate,
|
||||||
data: schedule.group,
|
groups: ScheduleService.toObject(sourceSchedule.groups),
|
||||||
etag: schedule.etag,
|
etag: sourceSchedule.etag,
|
||||||
lastChangedDays: this.lastChangedDays,
|
lastChangedDays: this.lastChangedDays,
|
||||||
};
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGroup(group: string): Promise<GroupScheduleDto> {
|
||||||
|
const schedule = await this.getSourceSchedule();
|
||||||
|
console.log(schedule);
|
||||||
|
if ((schedule.groups as object)[group] === undefined) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
"Группы с таким названием не существует!",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updatedAt: this.lastCacheUpdate,
|
||||||
|
group: schedule.groups[group],
|
||||||
|
etag: schedule.etag,
|
||||||
|
lastChangedDays: this.lastChangedDays[group] ?? [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getGroupNames(): Promise<ScheduleGroupsDto> {
|
||||||
|
let groupNames: ScheduleGroupsDto | undefined =
|
||||||
|
await this.cacheManager.get("groupNames");
|
||||||
|
|
||||||
|
if (!groupNames) {
|
||||||
|
const schedule = await this.getSourceSchedule();
|
||||||
|
const names: Array<string> = [];
|
||||||
|
|
||||||
|
for (const groupName in schedule.groups) names.push(groupName);
|
||||||
|
|
||||||
|
groupNames = { names };
|
||||||
|
await this.cacheManager.set(
|
||||||
|
"groupNames",
|
||||||
|
instanceToPlain(groupNames),
|
||||||
|
24 * 60 * 60 * 1000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupNames;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/users/users.controller.ts
Normal file
30
src/users/users.controller.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
NotFoundException,
|
||||||
|
UseGuards,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { AuthGuard } from "../auth/auth.guard";
|
||||||
|
import { ClientUserDto } from "../dto/user.dto";
|
||||||
|
import { ResultDto } from "../utility/validation/class-validator.interceptor";
|
||||||
|
import { UserToken } from "../auth/auth.decorator";
|
||||||
|
import { AuthService } from "../auth/auth.service";
|
||||||
|
|
||||||
|
@Controller("api/v1/users")
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
export class UsersController {
|
||||||
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
|
@ResultDto(ClientUserDto)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { UsersService } from "./users.service";
|
import { UsersService } from "./users.service";
|
||||||
import { PrismaService } from "../prisma/prisma.service";
|
import { PrismaService } from "../prisma/prisma.service";
|
||||||
|
import { UsersController } from "./users.controller";
|
||||||
|
import { AuthService } from "../auth/auth.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [PrismaService, UsersService],
|
providers: [PrismaService, UsersService, AuthService],
|
||||||
exports: [UsersService],
|
exports: [UsersService],
|
||||||
|
controllers: [UsersController],
|
||||||
})
|
})
|
||||||
export class UsersModule {}
|
export class UsersModule {}
|
||||||
|
|||||||
@@ -1,27 +1,41 @@
|
|||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import { PrismaService } from "../prisma/prisma.service";
|
import { PrismaService } from "../prisma/prisma.service";
|
||||||
import { Prisma, user } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { UserDto } from "../dto/user.dto";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UsersService {
|
export class UsersService {
|
||||||
constructor(private readonly prismaService: PrismaService) {}
|
constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
async findUnique(where: Prisma.userWhereUniqueInput): Promise<user | null> {
|
private static convertToDto = (user: UserDto | null) =>
|
||||||
return this.prismaService.user.findUnique({ where: where });
|
user as UserDto | null;
|
||||||
|
|
||||||
|
async findUnique(
|
||||||
|
where: Prisma.UserWhereUniqueInput,
|
||||||
|
): Promise<UserDto | null> {
|
||||||
|
return this.prismaService.user
|
||||||
|
.findUnique({ where: where })
|
||||||
|
.then(UsersService.convertToDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(params: {
|
async update(params: {
|
||||||
where: Prisma.userWhereUniqueInput;
|
where: Prisma.UserWhereUniqueInput;
|
||||||
data: Prisma.userUpdateInput;
|
data: Prisma.UserUpdateInput;
|
||||||
}): Promise<user | null> {
|
}): Promise<UserDto | null> {
|
||||||
return this.prismaService.user.update(params);
|
return this.prismaService.user
|
||||||
|
.update(params)
|
||||||
|
.then(UsersService.convertToDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(data: Prisma.userCreateInput): Promise<user> {
|
async create(data: Prisma.UserCreateInput): Promise<UserDto> {
|
||||||
return this.prismaService.user.create({ data });
|
return this.prismaService.user
|
||||||
|
.create({ data })
|
||||||
|
.then(UsersService.convertToDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
async contains(where: Prisma.userWhereUniqueInput): Promise<boolean> {
|
async contains(where: Prisma.UserWhereUniqueInput): Promise<boolean> {
|
||||||
return (await this.prismaService.user.count({ where })) > 0;
|
return this.prismaService.user
|
||||||
|
.count({ where })
|
||||||
|
.then((count) => count > 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/utility/cache.util.ts
Normal file
16
src/utility/cache.util.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Cache } from "@nestjs/cache-manager";
|
||||||
|
import { instanceToPlain } from "class-transformer";
|
||||||
|
|
||||||
|
export async function cacheGetOrFill<T>(
|
||||||
|
cache: Cache,
|
||||||
|
key: string,
|
||||||
|
onMiss: () => Promise<T>,
|
||||||
|
): Promise<T> {
|
||||||
|
const value: Record<string, any> | undefined = await cache.get(key);
|
||||||
|
if (value !== undefined) return value as T;
|
||||||
|
|
||||||
|
const newValue = await onMiss();
|
||||||
|
await cache.set(key, instanceToPlain<T>(newValue));
|
||||||
|
|
||||||
|
return newValue;
|
||||||
|
}
|
||||||
39
src/utility/validation/is-map.ts
Normal file
39
src/utility/validation/is-map.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
isObject,
|
||||||
|
registerDecorator,
|
||||||
|
ValidationArguments,
|
||||||
|
ValidationOptions,
|
||||||
|
} from "class-validator";
|
||||||
|
|
||||||
|
export function IsMap(
|
||||||
|
keyValidators: ((value: unknown) => boolean)[],
|
||||||
|
valueValidators: ((value: unknown) => boolean)[],
|
||||||
|
validationOptions?: ValidationOptions,
|
||||||
|
) {
|
||||||
|
return function (object: unknown, propertyName: string) {
|
||||||
|
registerDecorator({
|
||||||
|
name: "isMap",
|
||||||
|
target: (object as any).constructor,
|
||||||
|
propertyName: propertyName,
|
||||||
|
options: validationOptions,
|
||||||
|
validator: {
|
||||||
|
validate(value: unknown, args: ValidationArguments): boolean {
|
||||||
|
if (!isObject(value)) return false;
|
||||||
|
const keys = Object.keys(value);
|
||||||
|
const isInvalid = keys.some((key) => {
|
||||||
|
const isKeyInvalid = keyValidators.some(
|
||||||
|
(validator) => !validator(key),
|
||||||
|
);
|
||||||
|
if (isKeyInvalid) return true;
|
||||||
|
|
||||||
|
return valueValidators.some(
|
||||||
|
(validator) => !validator(value[key]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return !isInvalid;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user