mirror of
https://github.com/n08i40k/schedule-parser-next.git
synced 2025-12-06 09:47:46 +03:00
Refactor and reorganize codebase for better maintainability and clarity
- Rename DTOs to entities and move them to appropriate directories - Remove deprecated controllers and services - Update imports and dependencies - Implement new class transformer decorators for better serialization - Add VK authentication support - Improve error handling and validation - Update ESLint configuration and TypeScript settings - Refactor schedule parsing logic - Enhance user and authentication services - Update Prisma schema and related entities - Improve code organization and structure This commit introduces significant changes to improve the overall structure and maintainability of the codebase, including better organization of DTOs, enhanced authentication features, and updated tooling configurations.
This commit is contained in:
25
.eslintrc.js
25
.eslintrc.js
@@ -1,25 +0,0 @@
|
||||
module.exports = {
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
extends: [
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
jest: false,
|
||||
},
|
||||
ignorePatterns: ['.eslintrc.js'],
|
||||
rules: {
|
||||
'@typescript-eslint/interface-name-prefix': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
};
|
||||
42
eslint.config.mjs
Normal file
42
eslint.config.mjs
Normal file
@@ -0,0 +1,42 @@
|
||||
// @ts-check
|
||||
import eslint from "@eslint/js";
|
||||
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
|
||||
import globals from "globals";
|
||||
import tseslint from "typescript-eslint";
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ["eslint.config.mjs", "**/node_modules/"],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
eslintPluginPrettierRecommended,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
ecmaVersion: 5,
|
||||
sourceType: "module",
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-floating-promises": "off",
|
||||
"@typescript-eslint/no-unsafe-argument": "warn",
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/no-unsafe-assignment": "warn",
|
||||
"@typescript-eslint/no-unsafe-call": "warn",
|
||||
"@typescript-eslint/no-unsafe-member-access": "warn",
|
||||
"@typescript-eslint/require-await": "warn",
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
},
|
||||
},
|
||||
);
|
||||
84
package.json
84
package.json
@@ -8,74 +8,78 @@
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\"",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"precommit": "npm run format && npm run lint",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"precommit": "npm run format && npm run lint"
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/cache-manager": "^2.2.2",
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/core": "^10.4.5",
|
||||
"@nestjs/jwt": "^10.2.0",
|
||||
"@nestjs/platform-express": "^10.4.4",
|
||||
"@nestjs/swagger": "^7.4.2",
|
||||
"@prisma/client": "^5.19.1",
|
||||
"axios": "^1.7.7",
|
||||
"@fastify/static": "^8.0.4",
|
||||
"@nestjs/cache-manager": "^3.0.0",
|
||||
"@nestjs/common": "^11.0.5",
|
||||
"@nestjs/core": "^11.0.5",
|
||||
"@nestjs/jwt": "^11.0.0",
|
||||
"@nestjs/platform-express": "^11.0.5",
|
||||
"@nestjs/platform-fastify": "^11.0.5",
|
||||
"@nestjs/swagger": "^11.0.3",
|
||||
"@prisma/client": "^6.2.1",
|
||||
"axios": "^1.7.9",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cache-manager": "^5.7.6",
|
||||
"bson-objectid": "^2.0.4",
|
||||
"cache-manager": "^6.4.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.1",
|
||||
"cookie": ">=0.7.0",
|
||||
"cookie": ">=1.0.2",
|
||||
"create-map-transform-fn": "gist:f65ddd8f17f8c388659aab76890f194b",
|
||||
"dotenv": "^16.4.5",
|
||||
"firebase-admin": "^12.6.0",
|
||||
"jsdom": "^25.0.0",
|
||||
"mongoose": "^8.6.1",
|
||||
"nest-redoc": "^1.1.2",
|
||||
"dotenv": "^16.4.7",
|
||||
"firebase-admin": "^13.0.2",
|
||||
"jsdom": "^26.0.0",
|
||||
"object-hash": "^3.0.0",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1",
|
||||
"swagger-ui-express": "^5.0.1",
|
||||
"uuid": "^10.0.0",
|
||||
"uuid": "^11.0.5",
|
||||
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@eslint/eslintrc": "3.2.0",
|
||||
"@eslint/js": "9.18.0",
|
||||
"@nestjs/cli": "^11.0.2",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.5",
|
||||
"@swc/cli": "^0.6.0",
|
||||
"@swc/core": "^1.10.9",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "^29.5.2",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/node": "^20.16.5",
|
||||
"@types/node": "^22.10.9",
|
||||
"@types/object-hash": "^3.0.6",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"cookie": ">=0.7.0",
|
||||
"eslint": "^8.42.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"jest": "^29.1.0",
|
||||
"prettier": "^3.0.0",
|
||||
"prisma": "^5.19.1",
|
||||
"cookie": ">=1.0.2",
|
||||
"eslint": "9.18.0",
|
||||
"eslint-plugin-prettier": "5.2.3",
|
||||
"fastify": "^5.2.1",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "3.4.2",
|
||||
"prisma": "^6.2.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-loader": "^9.4.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.1.3"
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "8.21.0"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
|
||||
@@ -35,9 +35,10 @@ model User {
|
||||
//
|
||||
username String @unique
|
||||
//
|
||||
salt String
|
||||
password String
|
||||
//
|
||||
vkId Int?
|
||||
//
|
||||
accessToken String @unique
|
||||
//
|
||||
group String
|
||||
|
||||
@@ -13,7 +13,6 @@ import { FirebaseAdminModule } from "./firebase-admin/firebase-admin.module";
|
||||
CacheModule.register({ ttl: 5 * 60 * 1000, isGlobal: true }),
|
||||
FirebaseAdminModule,
|
||||
],
|
||||
controllers: [],
|
||||
providers: [],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Reflector } from "@nestjs/core";
|
||||
|
||||
import { UserRole } from "../users/user-role.enum";
|
||||
import UserRole from "../users/user-role.enum";
|
||||
|
||||
export const AuthRoles = Reflector.createDecorator<UserRole[]>();
|
||||
export const AuthUnauthorized = Reflector.createDecorator<true>();
|
||||
|
||||
158
src/auth/auth.controller.ts
Normal file
158
src/auth/auth.controller.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Post,
|
||||
Res,
|
||||
} from "@nestjs/common";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { ResultDto } from "../utility/validation/class-validator.interceptor";
|
||||
import { SignInDto, SignInVKDto } from "./dto/sign-in.dto";
|
||||
import { SignUpDto, SignUpVKDto } from "./dto/sign-up.dto";
|
||||
import { ScheduleService } from "../schedule/schedule.service";
|
||||
import SignInErrorDto from "./dto/sign-in-error.dto";
|
||||
import { FastifyReply } from "fastify";
|
||||
import SignUpErrorDto, { SignUpErrorCode } from "./dto/sign-up-error.dto";
|
||||
import UserDto from "src/users/dto/user.dto";
|
||||
import GetGroupNamesDto from "../schedule/dto/get-group-names.dto";
|
||||
|
||||
@ApiTags("v2/auth")
|
||||
@Controller({ path: "auth", version: "1" })
|
||||
export class AuthController {
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly scheduleService: ScheduleService,
|
||||
) {}
|
||||
|
||||
@ApiOperation({ summary: "Авторизация по логину и паролю" })
|
||||
@ApiBody({ type: SignInDto })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Авторизация прошла успешно",
|
||||
type: UserDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_ACCEPTABLE,
|
||||
description: "Переданы неверные входные данные",
|
||||
type: SignInErrorDto,
|
||||
})
|
||||
@ResultDto([UserDto, SignInErrorDto])
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("sign-in")
|
||||
async signIn(
|
||||
@Body() signInDto: SignInDto,
|
||||
@Res({ passthrough: true }) response: FastifyReply,
|
||||
): Promise<UserDto | SignInErrorDto> {
|
||||
const result = await this.authService.signIn(signInDto);
|
||||
|
||||
if (result instanceof SignInErrorDto)
|
||||
response.status(HttpStatus.NOT_ACCEPTABLE);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Регистрация по логину и паролю" })
|
||||
@ApiBody({ type: SignUpDto })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.CREATED,
|
||||
description: "Регистрация прошла успешно",
|
||||
type: UserDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_ACCEPTABLE,
|
||||
description: "Переданы неверные входные данные",
|
||||
type: SignUpErrorDto,
|
||||
})
|
||||
@ResultDto([UserDto, SignUpErrorDto])
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@Post("sign-up")
|
||||
async signUp(
|
||||
@Body() signUpDto: SignUpDto,
|
||||
@Res({ passthrough: true }) response: FastifyReply,
|
||||
): Promise<UserDto | SignUpErrorDto> {
|
||||
const groupNames = await this.scheduleService
|
||||
.getGroupNames()
|
||||
.catch((): GetGroupNamesDto => null);
|
||||
|
||||
if (
|
||||
groupNames &&
|
||||
!groupNames.names.includes(signUpDto.group.replaceAll(" ", ""))
|
||||
) {
|
||||
response.status(HttpStatus.NOT_ACCEPTABLE);
|
||||
return new SignUpErrorDto(SignUpErrorCode.INVALID_GROUP_NAME);
|
||||
}
|
||||
|
||||
const result = await this.authService.signUp(signUpDto);
|
||||
if (result instanceof SignUpErrorDto)
|
||||
response.status(HttpStatus.NOT_ACCEPTABLE);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Авторизация с помощью VK ID" })
|
||||
@ApiBody({ type: SignInVKDto })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Авторизация прошла успешно",
|
||||
type: UserDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_ACCEPTABLE,
|
||||
description: "Переданы неверные входные данные",
|
||||
type: SignInErrorDto,
|
||||
})
|
||||
@ResultDto([UserDto, SignInErrorDto])
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("sign-in-vk")
|
||||
async signInVK(
|
||||
@Body() signInVKDto: SignInVKDto,
|
||||
@Res({ passthrough: true }) response: FastifyReply,
|
||||
): Promise<UserDto | SignInErrorDto> {
|
||||
const result = await this.authService.signInVK(signInVKDto);
|
||||
|
||||
if (result instanceof SignInErrorDto)
|
||||
response.status(HttpStatus.NOT_ACCEPTABLE);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Регистрация с помощью VK ID" })
|
||||
@ApiBody({ type: SignUpVKDto })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.CREATED,
|
||||
description: "Регистрация прошла успешно",
|
||||
type: UserDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_ACCEPTABLE,
|
||||
description: "Переданы неверные входные данные",
|
||||
type: SignUpErrorDto,
|
||||
})
|
||||
@ResultDto([UserDto, SignUpErrorDto])
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@Post("sign-up-vk")
|
||||
async signUpVK(
|
||||
@Body() signUpVKDto: SignUpVKDto,
|
||||
@Res({ passthrough: true }) response: FastifyReply,
|
||||
): Promise<UserDto | SignUpErrorDto> {
|
||||
const groupNames = await this.scheduleService
|
||||
.getGroupNames()
|
||||
.catch((): GetGroupNamesDto => null);
|
||||
|
||||
if (
|
||||
groupNames &&
|
||||
!groupNames.names.includes(signUpVKDto.group.replaceAll(" ", ""))
|
||||
) {
|
||||
response.status(HttpStatus.NOT_ACCEPTABLE);
|
||||
return new SignUpErrorDto(SignUpErrorCode.INVALID_GROUP_NAME);
|
||||
}
|
||||
|
||||
const result = await this.authService.signUpVK(signUpVKDto);
|
||||
if (result instanceof SignUpErrorDto)
|
||||
response.status(HttpStatus.NOT_ACCEPTABLE);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,19 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
UnauthorizedException,
|
||||
} from "@nestjs/common";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import { Request } from "express";
|
||||
import { UsersService } from "../users/users.service";
|
||||
import { Reflector } from "@nestjs/core";
|
||||
import { AuthRoles, AuthUnauthorized } from "./auth-role.decorator";
|
||||
import { isJWT } from "class-validator";
|
||||
import { FastifyRequest } from "fastify";
|
||||
|
||||
interface JWTUser {
|
||||
id: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuard implements CanActivate {
|
||||
@@ -20,11 +23,10 @@ export class AuthGuard implements CanActivate {
|
||||
private readonly reflector: Reflector,
|
||||
) {}
|
||||
|
||||
public static extractTokenFromRequest(req: Request): string {
|
||||
public static extractTokenFromRequest(req: FastifyRequest): string {
|
||||
const [type, token] = req.headers.authorization?.split(" ") ?? [];
|
||||
|
||||
if (type !== "Bearer" || !token || token.length === 0)
|
||||
throw new UnauthorizedException("Не указан токен!");
|
||||
if (type !== "Bearer" || !token || token.length === 0) return null;
|
||||
|
||||
return token;
|
||||
}
|
||||
@@ -33,31 +35,24 @@ export class AuthGuard implements CanActivate {
|
||||
if (this.reflector.get(AuthUnauthorized, context.getHandler()))
|
||||
return true;
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const request: FastifyRequest = context.switchToHttp().getRequest();
|
||||
const token = AuthGuard.extractTokenFromRequest(request);
|
||||
|
||||
let jwtUser: { id: string } | null = null;
|
||||
if (!token || !isJWT(token)) throw new UnauthorizedException();
|
||||
|
||||
if (
|
||||
!isJWT(token) ||
|
||||
!(jwtUser = await this.jwtService
|
||||
.verifyAsync(token)
|
||||
.catch(() => null))
|
||||
)
|
||||
throw new UnauthorizedException();
|
||||
const jwtUser = await this.jwtService
|
||||
.verifyAsync<JWTUser>(token)
|
||||
.catch((): JWTUser => null);
|
||||
if (!jwtUser) throw new UnauthorizedException();
|
||||
|
||||
const user = await this.usersService.findUnique({ id: jwtUser.id });
|
||||
if (!user || user.accessToken !== token)
|
||||
throw new UnauthorizedException();
|
||||
if (!user) throw new UnauthorizedException();
|
||||
|
||||
const acceptableRoles = this.reflector.get(
|
||||
AuthRoles,
|
||||
context.getHandler(),
|
||||
);
|
||||
|
||||
if (acceptableRoles != null && !acceptableRoles.includes(user.role))
|
||||
throw new ForbiddenException();
|
||||
|
||||
return true;
|
||||
return !(acceptableRoles && !acceptableRoles.includes(user.role));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,10 @@ import { forwardRef, Module } from "@nestjs/common";
|
||||
import { JwtModule } from "@nestjs/jwt";
|
||||
import { jwtConstants } from "../contants";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { V1AuthController } from "./v1-auth.controller";
|
||||
import { UsersModule } from "../users/users.module";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { ScheduleModule } from "../schedule/schedule.module";
|
||||
import { V2AuthController } from "./v2-auth.controller";
|
||||
import { AuthController } from "./auth.controller";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -19,7 +18,7 @@ import { V2AuthController } from "./v2-auth.controller";
|
||||
}),
|
||||
],
|
||||
providers: [AuthService, PrismaService],
|
||||
controllers: [V1AuthController, V2AuthController],
|
||||
controllers: [AuthController],
|
||||
exports: [AuthService],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import { UsersService } from "../users/users.service";
|
||||
|
||||
import { User } from "../users/entity/user.entity";
|
||||
import User from "../users/entity/user.entity";
|
||||
|
||||
@Injectable()
|
||||
export class UserPipe implements PipeTransform {
|
||||
@@ -25,6 +25,6 @@ export class UserPipe implements PipeTransform {
|
||||
if (!user)
|
||||
throw new UnauthorizedException("Передан некорректный токен!");
|
||||
|
||||
return user as User;
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import {
|
||||
ConflictException,
|
||||
Injectable,
|
||||
NotAcceptableException,
|
||||
NotFoundException,
|
||||
UnauthorizedException,
|
||||
} from "@nestjs/common";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import { UsersService } from "../users/users.service";
|
||||
import { genSalt, hash } from "bcrypt";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { Types } from "mongoose";
|
||||
import { UserRole } from "../users/user-role.enum";
|
||||
import { User } from "../users/entity/user.entity";
|
||||
import { SignInDto } from "./dto/sign-in.dto";
|
||||
import { SignUpDto } from "./dto/sign-up.dto";
|
||||
import { ChangePasswordDto } from "./dto/change-password.dto";
|
||||
import { compare, genSalt, hash } from "bcrypt";
|
||||
import UserRole from "../users/user-role.enum";
|
||||
import User from "../users/entity/user.entity";
|
||||
import ChangePasswordDto from "./dto/change-password.dto";
|
||||
import axios from "axios";
|
||||
import SignInErrorDto, { SignInErrorCode } from "./dto/sign-in-error.dto";
|
||||
import { SignUpDto, SignUpVKDto } from "./dto/sign-up.dto";
|
||||
import SignUpErrorDto, { SignUpErrorCode } from "./dto/sign-up-error.dto";
|
||||
import { SignInDto, SignInVKDto } from "./dto/sign-in.dto";
|
||||
import ObjectID from "bson-objectid";
|
||||
import UserDto from "../users/dto/user.dto";
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@@ -51,112 +52,124 @@ export class AuthService {
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Регистрация нового пользователя
|
||||
* @param signUp - данные нового пользователя
|
||||
* @returns {User} - пользователь
|
||||
* @throws {NotAcceptableException} - передана недопустимая роль
|
||||
* @throws {ConflictException} - пользователь с таким именем уже существует
|
||||
* @async
|
||||
*/
|
||||
async signUp(signUp: SignUpDto): Promise<User> {
|
||||
const group = signUp.group.replaceAll(" ", "");
|
||||
const username = signUp.username.trim();
|
||||
async signUp(signUpDto: SignUpDto): Promise<UserDto | SignUpErrorDto> {
|
||||
if (![UserRole.STUDENT, UserRole.TEACHER].includes(signUpDto.role))
|
||||
return new SignUpErrorDto(SignUpErrorCode.DISALLOWED_ROLE);
|
||||
|
||||
if (![UserRole.STUDENT, UserRole.TEACHER].includes(signUp.role))
|
||||
throw new NotAcceptableException("Передана неизвестная роль");
|
||||
if (await this.usersService.contains({ username: signUpDto.username }))
|
||||
return new SignUpErrorDto(SignUpErrorCode.USERNAME_ALREADY_EXISTS);
|
||||
|
||||
if (await this.usersService.contains({ username: username })) {
|
||||
throw new ConflictException(
|
||||
"Пользователь с таким именем уже существует!",
|
||||
const id = ObjectID().toHexString();
|
||||
|
||||
return UserDto.fromPlain(
|
||||
await this.usersService.create({
|
||||
id: id,
|
||||
username: signUpDto.username,
|
||||
password: await hash(signUpDto.password, await genSalt(8)),
|
||||
accessToken: await this.jwtService.signAsync({ id: id }),
|
||||
group: signUpDto.group,
|
||||
role: signUpDto.role,
|
||||
version: signUpDto.version,
|
||||
}),
|
||||
["auth"],
|
||||
);
|
||||
}
|
||||
|
||||
const salt = await genSalt(8);
|
||||
const id = new Types.ObjectId().toString("hex");
|
||||
|
||||
const input: Prisma.UserCreateInput = {
|
||||
id: id,
|
||||
username: username,
|
||||
salt: salt,
|
||||
password: await hash(signUp.password, salt),
|
||||
accessToken: await this.jwtService.signAsync({
|
||||
id: id,
|
||||
}),
|
||||
role: signUp.role as UserRole,
|
||||
group: group,
|
||||
version: signUp.version ?? "1.0.0",
|
||||
};
|
||||
|
||||
return await this.usersService.create(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Авторизация пользователя
|
||||
* @param signIn - данные авторизации
|
||||
* @returns {User} - пользователь
|
||||
* @throws {UnauthorizedException} - некорректное имя пользователя или пароль
|
||||
* @async
|
||||
*/
|
||||
async signIn(signIn: SignInDto): Promise<User> {
|
||||
async signIn(signIn: SignInDto): Promise<UserDto | SignInErrorDto> {
|
||||
const user = await this.usersService.findUnique({
|
||||
username: signIn.username,
|
||||
});
|
||||
|
||||
if (
|
||||
!user ||
|
||||
user.password !== (await hash(signIn.password, user.salt))
|
||||
) {
|
||||
throw new UnauthorizedException(
|
||||
"Некорректное имя пользователя или пароль!",
|
||||
if (!user || !(await compare(signIn.password, user.password)))
|
||||
return new SignInErrorDto(SignInErrorCode.INCORRECT_CREDENTIALS);
|
||||
|
||||
return UserDto.fromPlain(
|
||||
await this.usersService.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
accessToken: await this.jwtService.signAsync({
|
||||
id: user.id,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
["auth"],
|
||||
);
|
||||
}
|
||||
|
||||
private static async parseVKID(accessToken: string): Promise<number> {
|
||||
const form = new FormData();
|
||||
form.append("access_token", accessToken);
|
||||
form.append("v", "5.199");
|
||||
|
||||
const response = await axios.post(
|
||||
"https://api.vk.com/method/account.getProfileInfo",
|
||||
form,
|
||||
{ responseType: "json" },
|
||||
);
|
||||
|
||||
const data: { error?: any; response?: { id: number } } =
|
||||
response.data as object;
|
||||
|
||||
if (response.status !== 200 || data.error !== undefined) return null;
|
||||
|
||||
return data.response.id;
|
||||
}
|
||||
|
||||
async signUpVK(signUpDto: SignUpVKDto): Promise<UserDto | SignUpErrorDto> {
|
||||
if (![UserRole.STUDENT, UserRole.TEACHER].includes(signUpDto.role))
|
||||
return new SignUpErrorDto(SignUpErrorCode.DISALLOWED_ROLE);
|
||||
|
||||
if (await this.usersService.contains({ username: signUpDto.username }))
|
||||
return new SignUpErrorDto(SignUpErrorCode.USERNAME_ALREADY_EXISTS);
|
||||
|
||||
const vkId = await AuthService.parseVKID(signUpDto.accessToken);
|
||||
if (!vkId)
|
||||
return new SignUpErrorDto(SignUpErrorCode.INVALID_VK_ACCESS_TOKEN);
|
||||
|
||||
if (await this.usersService.contains({ vkId: vkId }))
|
||||
return new SignUpErrorDto(SignUpErrorCode.VK_ALREADY_EXISTS);
|
||||
|
||||
const id = ObjectID().toHexString();
|
||||
|
||||
return UserDto.fromPlain(
|
||||
await this.usersService.create({
|
||||
id: id,
|
||||
username: signUpDto.username,
|
||||
password: await hash(await genSalt(8), await genSalt(8)),
|
||||
vkId: vkId,
|
||||
accessToken: await this.jwtService.signAsync({
|
||||
id: id,
|
||||
}),
|
||||
role: signUpDto.role,
|
||||
group: signUpDto.group,
|
||||
version: signUpDto.version,
|
||||
}),
|
||||
["auth"],
|
||||
);
|
||||
}
|
||||
|
||||
async signInVK(
|
||||
signInVKDto: SignInVKDto,
|
||||
): Promise<UserDto | SignInErrorDto> {
|
||||
const vkId = await AuthService.parseVKID(signInVKDto.accessToken);
|
||||
if (!vkId)
|
||||
return new SignInErrorDto(SignInErrorCode.INVALID_VK_ACCESS_TOKEN);
|
||||
|
||||
const user = await this.usersService.findOne({ vkId: vkId });
|
||||
if (!user)
|
||||
return new SignInErrorDto(SignInErrorCode.INCORRECT_CREDENTIALS);
|
||||
|
||||
const accessToken = await this.jwtService.signAsync({ id: user.id });
|
||||
|
||||
return await this.usersService.update({
|
||||
return UserDto.fromPlain(
|
||||
await this.usersService.update({
|
||||
where: { id: user.id },
|
||||
data: { accessToken: accessToken },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление токена пользователя
|
||||
* @param oldToken - старый токен
|
||||
* @returns {User} - пользователь
|
||||
* @throws {NotFoundException} - некорректный или недействительный токен
|
||||
* @throws {NotFoundException} - токен указывает на несуществующего пользователя
|
||||
* @throws {NotFoundException} - текущий токен устарел и был обновлён на новый
|
||||
* @async
|
||||
*/
|
||||
async updateToken(oldToken: string): Promise<User> {
|
||||
if (
|
||||
!(await this.jwtService.verifyAsync(oldToken, {
|
||||
ignoreExpiration: true,
|
||||
}))
|
||||
) {
|
||||
throw new NotFoundException(
|
||||
"Некорректный или недействительный токен!",
|
||||
}),
|
||||
["auth"],
|
||||
);
|
||||
}
|
||||
|
||||
const jwtUser: { id: string } = await this.jwtService.decode(oldToken);
|
||||
|
||||
const user = await this.usersService.findUnique({ id: jwtUser.id });
|
||||
if (!user || user.accessToken !== oldToken) {
|
||||
throw new NotFoundException(
|
||||
"Некорректный или недействительный токен!",
|
||||
);
|
||||
}
|
||||
|
||||
const accessToken = await this.jwtService.signAsync({ id: user.id });
|
||||
|
||||
return await this.usersService.update({
|
||||
where: { id: user.id },
|
||||
data: { accessToken: accessToken },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Смена пароля пользователя
|
||||
* @param user - пользователь
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IsString } from "class-validator";
|
||||
|
||||
export class ChangePasswordDto {
|
||||
export default class ChangePasswordDto {
|
||||
/**
|
||||
* Старый пароль
|
||||
* @example "my-old-password"
|
||||
|
||||
15
src/auth/dto/sign-in-error.dto.ts
Normal file
15
src/auth/dto/sign-in-error.dto.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { IsEnum } from "class-validator";
|
||||
|
||||
export enum SignInErrorCode {
|
||||
INCORRECT_CREDENTIALS = "INCORRECT_CREDENTIALS",
|
||||
INVALID_VK_ACCESS_TOKEN = "INVALID_VK_ACCESS_TOKEN",
|
||||
}
|
||||
|
||||
export default class SignInErrorDto {
|
||||
@IsEnum(SignInErrorCode)
|
||||
code: SignInErrorCode;
|
||||
|
||||
constructor(errorCode: SignInErrorCode) {
|
||||
this.code = errorCode;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IsJWT, IsMongoId, IsOptional, IsString } from "class-validator";
|
||||
|
||||
export class SignInResponseDto {
|
||||
export default class SignInResponseDto {
|
||||
/**
|
||||
* Идентификатор (ObjectId)
|
||||
* @example "66e1b7e255c5d5f1268cce90"
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
import { PickType } from "@nestjs/swagger";
|
||||
import { User } from "../../users/entity/user.entity";
|
||||
import { IsString } from "class-validator";
|
||||
import { IsString, MaxLength, MinLength } from "class-validator";
|
||||
|
||||
export class SignInDto {
|
||||
/**
|
||||
* Имя
|
||||
* @example "n08i40k"
|
||||
*/
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(20)
|
||||
username: string;
|
||||
|
||||
export class SignInDto extends PickType(User, ["username"]) {
|
||||
/**
|
||||
* Пароль в исходном виде
|
||||
* @example "my-password"
|
||||
@@ -10,3 +17,12 @@ export class SignInDto extends PickType(User, ["username"]) {
|
||||
@IsString()
|
||||
password: string;
|
||||
}
|
||||
|
||||
export class SignInVKDto {
|
||||
/**
|
||||
* Токен VK
|
||||
* @example "хз"
|
||||
*/
|
||||
@IsString()
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
18
src/auth/dto/sign-up-error.dto.ts
Normal file
18
src/auth/dto/sign-up-error.dto.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { IsEnum } from "class-validator";
|
||||
|
||||
export enum SignUpErrorCode {
|
||||
USERNAME_ALREADY_EXISTS = "USERNAME_ALREADY_EXISTS",
|
||||
VK_ALREADY_EXISTS = "VK_ALREADY_EXISTS",
|
||||
INVALID_VK_ACCESS_TOKEN = "INVALID_VK_ACCESS_TOKEN",
|
||||
INVALID_GROUP_NAME = "INVALID_GROUP_NAME",
|
||||
DISALLOWED_ROLE = "DISALLOWED_ROLE",
|
||||
}
|
||||
|
||||
export default class SignUpErrorDto {
|
||||
@IsEnum(SignUpErrorCode)
|
||||
code: SignUpErrorCode;
|
||||
|
||||
constructor(errorCode: SignUpErrorCode) {
|
||||
this.code = errorCode;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
import { PickType } from "@nestjs/swagger";
|
||||
import { User } from "../../users/entity/user.entity";
|
||||
import User from "../../users/entity/user.entity";
|
||||
|
||||
export class SignUpResponseDto extends PickType(User, ["id", "accessToken"]) {}
|
||||
export default class SignUpResponseDto extends PickType(User, [
|
||||
"id",
|
||||
"accessToken",
|
||||
]) {}
|
||||
|
||||
@@ -1,9 +1,86 @@
|
||||
import { IntersectionType, PartialType, PickType } from "@nestjs/swagger";
|
||||
import { SignInDto } from "./sign-in.dto";
|
||||
import { User } from "../../users/entity/user.entity";
|
||||
import {
|
||||
IsEnum,
|
||||
IsSemVer,
|
||||
IsString,
|
||||
MaxLength,
|
||||
MinLength,
|
||||
} from "class-validator";
|
||||
import UserRole from "../../users/user-role.enum";
|
||||
|
||||
export class SignUpDto extends IntersectionType(
|
||||
SignInDto,
|
||||
PickType(User, ["role", "group"]),
|
||||
PartialType(PickType(User, ["version"])),
|
||||
) {}
|
||||
export class SignUpDto {
|
||||
/**
|
||||
* Имя
|
||||
* @example "n08i40k"
|
||||
*/
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(20)
|
||||
username: string;
|
||||
|
||||
/**
|
||||
* Пароль в исходном виде
|
||||
* @example "my-password"
|
||||
*/
|
||||
@IsString()
|
||||
password: string;
|
||||
|
||||
/**
|
||||
* Группа
|
||||
* @example "ИС-214/23"
|
||||
*/
|
||||
@IsString()
|
||||
group: string;
|
||||
|
||||
/**
|
||||
* Роль
|
||||
* @example STUDENT
|
||||
*/
|
||||
@IsEnum(UserRole)
|
||||
role: UserRole;
|
||||
|
||||
/**
|
||||
* Версия установленного приложения
|
||||
* @example "2.0.0"
|
||||
*/
|
||||
@IsSemVer()
|
||||
version: string;
|
||||
}
|
||||
|
||||
export class SignUpVKDto {
|
||||
/**
|
||||
* Токен VK
|
||||
* @example "хз"
|
||||
*/
|
||||
@IsString()
|
||||
accessToken: string;
|
||||
|
||||
/**
|
||||
* Имя
|
||||
* @example "n08i40k"
|
||||
*/
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(20)
|
||||
username: string;
|
||||
|
||||
/**
|
||||
* Группа
|
||||
* @example "ИС-214/23"
|
||||
*/
|
||||
@IsString()
|
||||
group: string;
|
||||
|
||||
/**
|
||||
* Роль
|
||||
* @example STUDENT
|
||||
*/
|
||||
@IsEnum(UserRole)
|
||||
role: UserRole;
|
||||
|
||||
/**
|
||||
* Версия установленного приложения
|
||||
* @example "2.0.0"
|
||||
*/
|
||||
@IsSemVer()
|
||||
version: string;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { UpdateTokenDto } from "./update-token.dto";
|
||||
import UpdateTokenDto from "./update-token.dto";
|
||||
|
||||
export class UpdateTokenResponseDto extends UpdateTokenDto {}
|
||||
export default class UpdateTokenResponseDto extends UpdateTokenDto {}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PickType } from "@nestjs/swagger";
|
||||
import { User } from "../../users/entity/user.entity";
|
||||
import User from "../../users/entity/user.entity";
|
||||
|
||||
export class UpdateTokenDto extends PickType(User, ["accessToken"]) {}
|
||||
export default class UpdateTokenDto extends PickType(User, ["accessToken"]) {}
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
NotFoundException,
|
||||
Post,
|
||||
} from "@nestjs/common";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { ResultDto } from "../utility/validation/class-validator.interceptor";
|
||||
import { UserToken } from "./auth.decorator";
|
||||
import { ResponseVersion } from "../version/response-version.decorator";
|
||||
import { SignInDto } from "./dto/sign-in.dto";
|
||||
import { SignInResponseDto } from "./dto/sign-in-response.dto";
|
||||
import { SignUpResponseDto } from "./dto/sign-up-response.dto";
|
||||
import { SignUpDto } from "./dto/sign-up.dto";
|
||||
import { UpdateTokenDto } from "./dto/update-token.dto";
|
||||
import { UpdateTokenResponseDto } from "./dto/update-token-response.dto";
|
||||
import { ChangePasswordDto } from "./dto/change-password.dto";
|
||||
import { ScheduleService } from "../schedule/schedule.service";
|
||||
|
||||
@ApiTags("v1/auth")
|
||||
@Controller({ path: "auth", version: "1" })
|
||||
export class V1AuthController {
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly scheduleService: ScheduleService,
|
||||
) {}
|
||||
|
||||
@ApiOperation({ summary: "Авторизация по логину и паролю" })
|
||||
@ApiBody({ type: SignInDto })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Авторизация прошла успешно",
|
||||
type: SignInResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.UNAUTHORIZED,
|
||||
description: "Некорректное имя пользователя или пароль",
|
||||
})
|
||||
@ResultDto(SignInResponseDto)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("sign-in")
|
||||
async signIn(
|
||||
@Body() signInDto: SignInDto,
|
||||
@ResponseVersion() responseVersion: number,
|
||||
): Promise<SignInResponseDto> {
|
||||
const data = await this.authService.signIn(signInDto);
|
||||
|
||||
return {
|
||||
id: data.id,
|
||||
accessToken: data.accessToken,
|
||||
group: responseVersion ? data.group : null,
|
||||
};
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Регистрация по логину и паролю" })
|
||||
@ApiBody({ type: SignUpDto })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.CREATED,
|
||||
description: "Регистрация прошла успешно",
|
||||
type: SignUpResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.CONFLICT,
|
||||
description: "Такой пользователь уже существует",
|
||||
})
|
||||
@ResultDto(SignUpResponseDto)
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@Post("sign-up")
|
||||
async signUp(@Body() signUpDto: SignUpDto): Promise<SignUpResponseDto> {
|
||||
if (
|
||||
!(await this.scheduleService.getGroupNames()).names.includes(
|
||||
signUpDto.group.replaceAll(" ", ""),
|
||||
)
|
||||
) {
|
||||
throw new NotFoundException(
|
||||
"Передано название несуществующей группы",
|
||||
);
|
||||
}
|
||||
|
||||
const user = await this.authService.signUp(signUpDto);
|
||||
return {
|
||||
id: user.id,
|
||||
accessToken: user.accessToken,
|
||||
};
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Обновление просроченного токена" })
|
||||
@ApiBody({ type: UpdateTokenDto })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Токен обновлён успешно",
|
||||
type: UpdateTokenResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: "Передан несуществующий или недействительный токен",
|
||||
})
|
||||
@ResultDto(UpdateTokenResponseDto)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("update-token")
|
||||
updateToken(
|
||||
@Body() updateTokenDto: UpdateTokenDto,
|
||||
): Promise<UpdateTokenResponseDto> {
|
||||
return this.authService.updateToken(updateTokenDto.accessToken);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Обновление пароля" })
|
||||
@ApiBody({ type: ChangePasswordDto })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Пароль обновлён успешно",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.CONFLICT,
|
||||
description: "Пароли идентичны",
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.UNAUTHORIZED,
|
||||
description:
|
||||
"Передан неверный текущий пароль или запрос был послан без токена",
|
||||
})
|
||||
@ResultDto(null)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("change-password")
|
||||
async changePassword(
|
||||
@Body() changePasswordReqDto: ChangePasswordDto,
|
||||
@UserToken() userToken: string,
|
||||
): Promise<void> {
|
||||
await this.authService
|
||||
.decodeUserToken(userToken)
|
||||
.then((user) =>
|
||||
this.authService.changePassword(user, changePasswordReqDto),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
NotFoundException,
|
||||
Post,
|
||||
} from "@nestjs/common";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { ApiBody, ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { ResultDto } from "../utility/validation/class-validator.interceptor";
|
||||
import { SignInDto } from "./dto/sign-in.dto";
|
||||
import { SignUpDto } from "./dto/sign-up.dto";
|
||||
import { UpdateTokenDto } from "./dto/update-token.dto";
|
||||
import { V2ClientUserDto } from "../users/dto/v2/v2-client-user.dto";
|
||||
import { ScheduleService } from "../schedule/schedule.service";
|
||||
|
||||
@ApiTags("v2/auth")
|
||||
@Controller({ path: "auth", version: "2" })
|
||||
export class V2AuthController {
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly scheduleService: ScheduleService,
|
||||
) {}
|
||||
|
||||
@ApiOperation({ summary: "Авторизация по логину и паролю" })
|
||||
@ApiBody({ type: SignInDto })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Авторизация прошла успешно",
|
||||
type: V2ClientUserDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.UNAUTHORIZED,
|
||||
description: "Некорректное имя пользователя или пароль",
|
||||
})
|
||||
@ResultDto(V2ClientUserDto)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("sign-in")
|
||||
async signIn(@Body() reqDto: SignInDto): Promise<V2ClientUserDto> {
|
||||
return V2ClientUserDto.fromUser(await this.authService.signIn(reqDto));
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Регистрация по логину и паролю" })
|
||||
@ApiBody({ type: SignUpDto })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.CREATED,
|
||||
description: "Регистрация прошла успешно",
|
||||
type: V2ClientUserDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.CONFLICT,
|
||||
description: "Такой пользователь уже существует",
|
||||
})
|
||||
@ResultDto(V2ClientUserDto)
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@Post("sign-up")
|
||||
async signUp(@Body() reqDto: SignUpDto): Promise<V2ClientUserDto> {
|
||||
if (
|
||||
!(await this.scheduleService.getGroupNames()).names.includes(
|
||||
reqDto.group.replaceAll(" ", ""),
|
||||
)
|
||||
) {
|
||||
throw new NotFoundException(
|
||||
"Передано название несуществующей группы",
|
||||
);
|
||||
}
|
||||
|
||||
return V2ClientUserDto.fromUser(await this.authService.signUp(reqDto));
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Обновление просроченного токена" })
|
||||
@ApiBody({ type: UpdateTokenDto })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Токен обновлён успешно",
|
||||
type: V2ClientUserDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: "Передан несуществующий или недействительный токен",
|
||||
})
|
||||
@ResultDto(V2ClientUserDto)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("update-token")
|
||||
async updateToken(
|
||||
@Body() reqDto: UpdateTokenDto,
|
||||
): Promise<V2ClientUserDto> {
|
||||
return V2ClientUserDto.fromUser(
|
||||
await this.authService.updateToken(reqDto.accessToken),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,23 +4,23 @@ import * as process from "node:process";
|
||||
configDotenv();
|
||||
|
||||
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!,
|
||||
certPath: process.env.CERT_PEM_PATH,
|
||||
keyPath: process.env.KEY_PEM_PATH,
|
||||
};
|
||||
|
||||
export const apiConstants = {
|
||||
port: +(process.env.API_PORT ?? 5050),
|
||||
version: process.env.SERVER_VERSION!,
|
||||
version: process.env.SERVER_VERSION,
|
||||
};
|
||||
|
||||
export const firebaseConstants = {
|
||||
serviceAccountPath: process.env.FIREBASE_ACCOUNT_PATH!,
|
||||
serviceAccountPath: process.env.FIREBASE_ACCOUNT_PATH,
|
||||
};
|
||||
|
||||
export const scheduleConstants = {
|
||||
cacheInvalidateDelay: +(process.env.SERVER_CACHE_INVALIDATE_DELAY! ?? 5),
|
||||
cacheInvalidateDelay: +(process.env.SERVER_CACHE_INVALIDATE_DELAY ?? 5),
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ import { ResultDto } from "../utility/validation/class-validator.interceptor";
|
||||
import { FirebaseAdminService } from "./firebase-admin.service";
|
||||
import { FcmPostUpdateDto } from "./dto/fcm-post-update.dto";
|
||||
import { isSemVer } from "class-validator";
|
||||
import { User } from "../users/entity/user.entity";
|
||||
import User from "../users/entity/user.entity";
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiBody,
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
ApiTags,
|
||||
} from "@nestjs/swagger";
|
||||
import { AuthRoles } from "../auth/auth-role.decorator";
|
||||
import { UserRole } from "../users/user-role.enum";
|
||||
import UserRole from "../users/user-role.enum";
|
||||
import {
|
||||
TokenMessage,
|
||||
TopicMessage,
|
||||
|
||||
@@ -12,9 +12,9 @@ import {
|
||||
import { firebaseConstants } from "../contants";
|
||||
import { UsersService } from "../users/users.service";
|
||||
|
||||
import { User } from "../users/entity/user.entity";
|
||||
import User from "../users/entity/user.entity";
|
||||
import { TokenMessage } from "firebase-admin/lib/messaging/messaging-api";
|
||||
import { FcmUser } from "../users/entity/fcm-user.entity";
|
||||
import FCM from "../users/entity/fcm-user.entity";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
|
||||
@Injectable()
|
||||
@@ -47,12 +47,12 @@ export class FirebaseAdminService implements OnModuleInit {
|
||||
await this.messaging.send(message);
|
||||
}
|
||||
|
||||
private getFcmOrDefault(user: User, token: string): FcmUser {
|
||||
private getFcmOrDefault(user: User, token: string): FCM {
|
||||
if (!user.fcm) {
|
||||
return plainToInstance(FcmUser, {
|
||||
return plainToInstance(FCM, {
|
||||
token: token,
|
||||
topics: [],
|
||||
} as FcmUser);
|
||||
} as FCM);
|
||||
}
|
||||
|
||||
return user.fcm;
|
||||
@@ -68,7 +68,7 @@ export class FirebaseAdminService implements OnModuleInit {
|
||||
if (!isNew) {
|
||||
if (fcm.token === token) return { userDto: user, isNew: false };
|
||||
|
||||
for (const topic in fcm.topics)
|
||||
for (const topic of fcm.topics)
|
||||
await this.messaging.subscribeToTopic(token, topic);
|
||||
fcm.token = token;
|
||||
}
|
||||
|
||||
25
src/main.ts
25
src/main.ts
@@ -3,22 +3,30 @@ import { AppModule } from "./app.module";
|
||||
import { ValidatorOptions } from "class-validator";
|
||||
import { PartialValidationPipe } from "./utility/validation/partial-validation.pipe";
|
||||
import { ClassValidatorInterceptor } from "./utility/validation/class-validator.interceptor";
|
||||
import { RedocModule } from "nest-redoc";
|
||||
import { apiConstants, httpsConstants } from "./contants";
|
||||
import * as fs from "node:fs";
|
||||
import { VersioningType } from "@nestjs/common";
|
||||
import {
|
||||
FastifyAdapter,
|
||||
NestFastifyApplication,
|
||||
} from "@nestjs/platform-fastify";
|
||||
import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
const app = await NestFactory.create<NestFastifyApplication>(
|
||||
AppModule,
|
||||
new FastifyAdapter(),
|
||||
{
|
||||
httpsOptions: {
|
||||
cert: fs.readFileSync(httpsConstants.certPath),
|
||||
key: fs.readFileSync(httpsConstants.keyPath),
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
const validatorOptions: ValidatorOptions = {
|
||||
enableDebugMessages: true,
|
||||
forbidNonWhitelisted: true,
|
||||
whitelist: true,
|
||||
strictGroups: true,
|
||||
};
|
||||
app.useGlobalPipes(new PartialValidationPipe(validatorOptions));
|
||||
app.useGlobalInterceptors(new ClassValidatorInterceptor(validatorOptions));
|
||||
@@ -29,21 +37,24 @@ async function bootstrap() {
|
||||
type: VersioningType.URI,
|
||||
});
|
||||
|
||||
const swaggerConfig = RedocModule.createDocumentBuilder()
|
||||
const swaggerConfig = new DocumentBuilder()
|
||||
.setTitle("Schedule Parser")
|
||||
.setDescription("Парсер расписания")
|
||||
.setVersion(apiConstants.version)
|
||||
.build();
|
||||
const swaggerDocument = RedocModule.createDocument(app, swaggerConfig, {
|
||||
|
||||
const swaggerDocument = SwaggerModule.createDocument(app, swaggerConfig, {
|
||||
deepScanRoutes: true,
|
||||
});
|
||||
|
||||
swaggerDocument.servers = [
|
||||
{
|
||||
url: `https://localhost:${apiConstants.port}`,
|
||||
description: "Локальный сервер для разработки",
|
||||
},
|
||||
];
|
||||
await RedocModule.setup("api-docs", app, swaggerDocument, {});
|
||||
|
||||
SwaggerModule.setup("api-docs", app, swaggerDocument, {});
|
||||
|
||||
await app.listen(apiConstants.port);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IsBoolean, IsHash, IsNumber } from "class-validator";
|
||||
|
||||
export class CacheStatusDto {
|
||||
export default class CacheStatusDto {
|
||||
/**
|
||||
* Хеш данных парсера
|
||||
* @example "40bd001563085fc35165329ea1ff5c5ecbdbbeef"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IsNumber } from "class-validator";
|
||||
|
||||
export class ClearScheduleReplacerDto {
|
||||
export default class ClearReplacerDto {
|
||||
/**
|
||||
* Количество удалённых заменителей расписания
|
||||
* @example 1
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IsArray } from "class-validator";
|
||||
|
||||
export class ScheduleGroupNamesDto {
|
||||
export default class GetGroupNamesDto {
|
||||
/**
|
||||
* Группы
|
||||
* @example ["ИС-214/23", "ИС-213/23"]
|
||||
@@ -1,112 +0,0 @@
|
||||
import "reflect-metadata";
|
||||
|
||||
import { V2LessonType } from "../enum/v2-lesson-type.enum";
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
import { Transform, Type } from "class-transformer";
|
||||
import { NullIf } from "../../utility/class-validators/conditional-field";
|
||||
import { LessonTimeDto } from "./lesson-time.dto";
|
||||
import { LessonSubGroupDto } from "./lesson-sub-group.dto";
|
||||
|
||||
export class LessonDto {
|
||||
/**
|
||||
* Тип занятия
|
||||
* @example DEFAULT
|
||||
*/
|
||||
@IsEnum(V2LessonType)
|
||||
@Transform(({ value, options }) => {
|
||||
if (options?.groups?.includes("v1")) {
|
||||
switch (value as V2LessonType) {
|
||||
case V2LessonType.CONSULTATION:
|
||||
case V2LessonType.INDEPENDENT_WORK:
|
||||
case V2LessonType.EXAM:
|
||||
case V2LessonType.EXAM_WITH_GRADE:
|
||||
case V2LessonType.EXAM_DEFAULT:
|
||||
return V2LessonType.DEFAULT;
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
} else if (options?.groups?.includes("v2")) {
|
||||
switch (value as V2LessonType) {
|
||||
case V2LessonType.EXAM_DEFAULT:
|
||||
return V2LessonType.EXAM;
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
})
|
||||
type: V2LessonType;
|
||||
|
||||
/**
|
||||
* Индексы пар, если присутствуют
|
||||
* @example [1, 3]
|
||||
* @optional
|
||||
*/
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Number)
|
||||
@IsOptional()
|
||||
@NullIf((self: LessonDto) => {
|
||||
return self.type !== V2LessonType.DEFAULT;
|
||||
})
|
||||
defaultRange: Array<number> | null;
|
||||
|
||||
/**
|
||||
* Название занятия
|
||||
* @example "Элементы высшей математики"
|
||||
* @optional
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@NullIf((self: LessonDto) => {
|
||||
return self.type === V2LessonType.BREAK;
|
||||
})
|
||||
@Transform(({ value, obj, options }) => {
|
||||
if (!value) return value;
|
||||
|
||||
if (options?.groups?.includes("v1")) {
|
||||
switch (obj.type as V2LessonType) {
|
||||
case V2LessonType.INDEPENDENT_WORK:
|
||||
return `Самостоятельная | ${value}`;
|
||||
case V2LessonType.CONSULTATION:
|
||||
return `Консультация | ${value}`;
|
||||
case V2LessonType.EXAM:
|
||||
return `ЗАЧЕТ | ${value}`;
|
||||
case V2LessonType.EXAM_WITH_GRADE:
|
||||
return `ЗАЧЕТ С ОЦЕНКОЙ | ${value}`;
|
||||
case V2LessonType.EXAM_DEFAULT:
|
||||
return `ЗАЧЕТ С ОЦЕНКОЙ | ${value}`;
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
})
|
||||
name: string | null;
|
||||
|
||||
/**
|
||||
* Начало и конец занятия
|
||||
*/
|
||||
@Type(() => LessonTimeDto)
|
||||
time: LessonTimeDto;
|
||||
|
||||
/**
|
||||
* Тип занятия
|
||||
*/
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => LessonSubGroupDto)
|
||||
@IsOptional()
|
||||
@NullIf((self: LessonDto) => {
|
||||
return self.type !== V2LessonType.DEFAULT;
|
||||
})
|
||||
subGroups: Array<LessonSubGroupDto> | null;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { PickType } from "@nestjs/swagger";
|
||||
import { IsNumber } from "class-validator";
|
||||
import { SetScheduleReplacerDto } from "./set-schedule-replacer.dto";
|
||||
import SetScheduleReplacerDto from "./set-schedule-replacer.dto";
|
||||
|
||||
export class ScheduleReplacerDto extends PickType(SetScheduleReplacerDto, [
|
||||
export default class ReplacerDto extends PickType(SetScheduleReplacerDto, [
|
||||
"etag",
|
||||
]) {
|
||||
/**
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IsMongoId, IsObject, IsString } from "class-validator";
|
||||
|
||||
export class SetScheduleReplacerDto {
|
||||
export default class SetScheduleReplacerDto {
|
||||
/**
|
||||
* Идентификатор заменителя (ObjectId)
|
||||
* @example "66e6f1c8775ffeda400d7967"
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { DayDto } from "./day.dto";
|
||||
import { IsArray, ValidateNested } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { OmitType } from "@nestjs/swagger";
|
||||
import { TeacherLessonDto } from "./teacher-lesson.dto";
|
||||
|
||||
export class TeacherDayDto extends OmitType(DayDto, ["lessons"]) {
|
||||
/**
|
||||
* Занятия
|
||||
*/
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => TeacherLessonDto)
|
||||
lessons: Array<TeacherLessonDto>;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IsArray } from "class-validator";
|
||||
|
||||
export class ScheduleTeacherNamesDto {
|
||||
export default class TeacherNamesDto {
|
||||
/**
|
||||
* Группы
|
||||
* @example ["Хомченко Н.Е."]
|
||||
@@ -1,6 +1,6 @@
|
||||
import { IsUrl } from "class-validator";
|
||||
|
||||
export class UpdateDownloadUrlDto {
|
||||
export default class UpdateDownloadUrlDto {
|
||||
/**
|
||||
* Прямая ссылка на скачивание расписания
|
||||
* @example "https://politehnikum-eng.ru/2024/poltavskaja_07_s_14_po_20_10-5-.xls"
|
||||
|
||||
@@ -5,21 +5,16 @@ import {
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
import { Transform, Type } from "class-transformer";
|
||||
import { LessonDto } from "./lesson.dto";
|
||||
import { Type } from "class-transformer";
|
||||
import Lesson from "./lesson.entity";
|
||||
|
||||
export class DayDto {
|
||||
// noinspection JSClassNamingConvention
|
||||
export default class Day {
|
||||
/**
|
||||
* День недели
|
||||
* @example "Понедельник"
|
||||
*/
|
||||
@IsString()
|
||||
@Transform(({ value, obj, options }) => {
|
||||
if ((obj as DayDto).street && options?.groups?.includes("v1"))
|
||||
return `${value} | ${(obj as DayDto).street}`;
|
||||
|
||||
return value;
|
||||
})
|
||||
name: string;
|
||||
|
||||
/**
|
||||
@@ -27,11 +22,6 @@ export class DayDto {
|
||||
* @example "Железнодорожная, 13"
|
||||
*/
|
||||
@IsString()
|
||||
@Transform(({ value, options }) => {
|
||||
if (value && options?.groups?.includes("v1")) return undefined;
|
||||
|
||||
return value;
|
||||
})
|
||||
@IsOptional()
|
||||
street?: string;
|
||||
|
||||
@@ -47,6 +37,6 @@ export class DayDto {
|
||||
*/
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => LessonDto)
|
||||
lessons: Array<LessonDto>;
|
||||
@Type(() => Lesson)
|
||||
lessons: Array<Lesson>;
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import { PickType } from "@nestjs/swagger";
|
||||
import { IsArray, IsObject, ValidateNested } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { ScheduleDto } from "./schedule.dto";
|
||||
import { GroupDto } from "./group.dto";
|
||||
import Schedule from "./schedule.entity";
|
||||
import Group from "./group.entity";
|
||||
|
||||
export class GroupScheduleDto extends PickType(ScheduleDto, ["updatedAt"]) {
|
||||
export default class GroupSchedule extends PickType(Schedule, ["updatedAt"]) {
|
||||
/**
|
||||
* Расписание группы
|
||||
*/
|
||||
@IsObject()
|
||||
@Type(() => GroupDto)
|
||||
group: GroupDto;
|
||||
@Type(() => Group)
|
||||
group: Group;
|
||||
|
||||
/**
|
||||
* Обновлённые дни с последнего изменения расписания
|
||||
@@ -1,8 +1,8 @@
|
||||
import { IsArray, IsString, ValidateNested } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { DayDto } from "./day.dto";
|
||||
import Day from "./day.entity";
|
||||
|
||||
export class GroupDto {
|
||||
export default class Group {
|
||||
/**
|
||||
* Название группы
|
||||
* @example "ИС-214/23"
|
||||
@@ -15,6 +15,6 @@ export class GroupDto {
|
||||
*/
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => DayDto)
|
||||
days: Array<DayDto>;
|
||||
@Type(() => Day)
|
||||
days: Array<Day>;
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
import { IsNumber, IsOptional, IsString } from "class-validator";
|
||||
import {
|
||||
ClassTransformerCtor,
|
||||
Ctor,
|
||||
} from "../../utility/class-trasformer/class-transformer-ctor";
|
||||
|
||||
export class LessonSubGroupDto {
|
||||
@ClassTransformerCtor()
|
||||
export default class LessonSubGroup extends Ctor<LessonSubGroup> {
|
||||
/**
|
||||
* Номер подгруппы
|
||||
* @example 1
|
||||
@@ -1,6 +1,11 @@
|
||||
import { IsDateString } from "class-validator";
|
||||
import {
|
||||
ClassTransformerCtor,
|
||||
Ctor,
|
||||
} from "../../utility/class-trasformer/class-transformer-ctor";
|
||||
|
||||
export class LessonTimeDto {
|
||||
@ClassTransformerCtor()
|
||||
export default class LessonTime extends Ctor<LessonTime> {
|
||||
/**
|
||||
* Начало занятия
|
||||
* @example "2024-10-07T04:30:00.000Z"
|
||||
72
src/schedule/entities/lesson.entity.ts
Normal file
72
src/schedule/entities/lesson.entity.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import "reflect-metadata";
|
||||
|
||||
import { LessonType } from "../enum/lesson-type.enum";
|
||||
import {
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { NullIf } from "../../utility/class-validators/conditional-field";
|
||||
import LessonTime from "./lesson-time.entity";
|
||||
import LessonSubGroup from "./lesson-sub-group.entity";
|
||||
import {
|
||||
ClassTransformerCtor,
|
||||
Ctor,
|
||||
} from "../../utility/class-trasformer/class-transformer-ctor";
|
||||
|
||||
@ClassTransformerCtor()
|
||||
export default class Lesson extends Ctor<Lesson> {
|
||||
/**
|
||||
* Тип занятия
|
||||
* @example DEFAULT
|
||||
*/
|
||||
@IsEnum(LessonType)
|
||||
type: LessonType;
|
||||
|
||||
/**
|
||||
* Индексы пар, если присутствуют
|
||||
* @example [1, 3]
|
||||
* @optional
|
||||
*/
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Number)
|
||||
@IsOptional()
|
||||
@NullIf((self: Lesson) => {
|
||||
return self.type !== LessonType.DEFAULT;
|
||||
})
|
||||
defaultRange: Array<number> | null;
|
||||
|
||||
/**
|
||||
* Название занятия
|
||||
* @example "Элементы высшей математики"
|
||||
* @optional
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@NullIf((self: Lesson) => {
|
||||
return self.type === LessonType.BREAK;
|
||||
})
|
||||
name: string | null;
|
||||
|
||||
/**
|
||||
* Начало и конец занятия
|
||||
*/
|
||||
@Type(() => LessonTime)
|
||||
time: LessonTime;
|
||||
|
||||
/**
|
||||
* Тип занятия
|
||||
*/
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => LessonSubGroup)
|
||||
@IsOptional()
|
||||
@NullIf((self: Lesson) => {
|
||||
return self.type !== LessonType.DEFAULT;
|
||||
})
|
||||
subGroups: Array<LessonSubGroup> | null;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { IsArray, IsDate, ValidateNested } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { GroupDto } from "./group.dto";
|
||||
import Group from "./group.entity";
|
||||
import { ToMap } from "create-map-transform-fn";
|
||||
|
||||
export class ScheduleDto {
|
||||
export default class Schedule {
|
||||
/**
|
||||
* Дата когда последний раз расписание было скачано с сервера политехникума
|
||||
* @example "2024-10-18T21:50:06.680Z"
|
||||
@@ -14,8 +14,8 @@ export class ScheduleDto {
|
||||
/**
|
||||
* Расписание групп
|
||||
*/
|
||||
@ToMap({ mapValueClass: GroupDto })
|
||||
groups: Map<string, GroupDto>;
|
||||
@ToMap({ mapValueClass: Group })
|
||||
groups: Map<string, Group>;
|
||||
|
||||
/**
|
||||
* Обновлённые дни с последнего изменения расписания
|
||||
46
src/schedule/entities/teacher-day.entity.ts
Normal file
46
src/schedule/entities/teacher-day.entity.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
IsArray,
|
||||
IsDateString,
|
||||
IsOptional,
|
||||
IsString,
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import TeacherLesson from "./teacher-lesson.entity";
|
||||
import {
|
||||
ClassTransformerCtor,
|
||||
Ctor,
|
||||
} from "../../utility/class-trasformer/class-transformer-ctor";
|
||||
|
||||
@ClassTransformerCtor()
|
||||
export default class TeacherDay extends Ctor<TeacherDay> {
|
||||
/**
|
||||
* День недели
|
||||
* @example "Понедельник"
|
||||
*/
|
||||
@IsString()
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Улица (v2)
|
||||
* @example "Железнодорожная, 13"
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
street?: string;
|
||||
|
||||
/**
|
||||
* Дата
|
||||
* @example "2024-10-06T20:00:00.000Z"
|
||||
*/
|
||||
@IsDateString()
|
||||
date: Date;
|
||||
|
||||
/**
|
||||
* Занятия
|
||||
*/
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => TeacherLesson)
|
||||
lessons: Array<TeacherLesson>;
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { LessonDto } from "./lesson.dto";
|
||||
import Lesson from "./lesson.entity";
|
||||
import { IsOptional, IsString } from "class-validator";
|
||||
import { NullIf } from "../../utility/class-validators/conditional-field";
|
||||
import { V2LessonType } from "../enum/v2-lesson-type.enum";
|
||||
import { LessonType } from "../enum/lesson-type.enum";
|
||||
|
||||
export class TeacherLessonDto extends LessonDto {
|
||||
export default class TeacherLesson extends Lesson {
|
||||
/**
|
||||
* Название группы
|
||||
* @example "ИС-214/23"
|
||||
@@ -11,8 +11,8 @@ export class TeacherLessonDto extends LessonDto {
|
||||
*/
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@NullIf((self: TeacherLessonDto) => {
|
||||
return self.type === V2LessonType.BREAK;
|
||||
@NullIf((self: TeacherLesson) => {
|
||||
return self.type === LessonType.BREAK;
|
||||
})
|
||||
group: string | null;
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
import { PickType } from "@nestjs/swagger";
|
||||
import { ScheduleDto } from "./schedule.dto";
|
||||
import Schedule from "./schedule.entity";
|
||||
import { IsArray, IsObject, ValidateNested } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { TeacherDto } from "./teacher.dto";
|
||||
import Teacher from "./teacher.entity";
|
||||
|
||||
export class TeacherScheduleDto extends PickType(ScheduleDto, ["updatedAt"]) {
|
||||
export default class TeacherSchedule extends PickType(Schedule, ["updatedAt"]) {
|
||||
/**
|
||||
* Расписание преподавателя
|
||||
*/
|
||||
@IsObject()
|
||||
teacher: TeacherDto;
|
||||
teacher: Teacher;
|
||||
|
||||
/**
|
||||
* Обновлённые дни с последнего изменения расписания
|
||||
@@ -1,8 +1,13 @@
|
||||
import { IsArray, IsString, ValidateNested } from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { TeacherDayDto } from "./teacher-day.dto";
|
||||
import TeacherDay from "./teacher-day.entity";
|
||||
import {
|
||||
ClassTransformerCtor,
|
||||
Ctor,
|
||||
} from "../../utility/class-trasformer/class-transformer-ctor";
|
||||
|
||||
export class TeacherDto {
|
||||
@ClassTransformerCtor()
|
||||
export default class Teacher extends Ctor<Teacher> {
|
||||
/**
|
||||
* ФИО преподавателя
|
||||
* @example "Хомченко Н.Е."
|
||||
@@ -15,6 +20,6 @@ export class TeacherDto {
|
||||
*/
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => TeacherDayDto)
|
||||
days: Array<TeacherDayDto>;
|
||||
@Type(() => TeacherDay)
|
||||
days: Array<TeacherDay>;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export enum V2LessonType {
|
||||
export enum LessonType {
|
||||
DEFAULT = 0, // Обычная
|
||||
ADDITIONAL, // Допы
|
||||
BREAK, // Перемена
|
||||
@@ -1,15 +1,15 @@
|
||||
import { V2ScheduleParser, V2ScheduleParseResult } from "./v2-schedule-parser";
|
||||
import { ScheduleParser, ScheduleParseResult } from "./schedule-parser";
|
||||
import { BasicXlsDownloader } from "../xls-downloader/basic-xls-downloader";
|
||||
import { DayDto } from "../../dto/day.dto";
|
||||
import { GroupDto } from "../../dto/group.dto";
|
||||
import Day from "../../entities/day.entity";
|
||||
import Group from "../../entities/group.entity";
|
||||
import instanceToInstance2 from "../../../utility/class-trasformer/instance-to-instance-2";
|
||||
|
||||
describe("V2ScheduleParser", () => {
|
||||
let parser: V2ScheduleParser;
|
||||
describe("ScheduleParser", () => {
|
||||
let parser: ScheduleParser;
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
const xlsDownloader = new BasicXlsDownloader();
|
||||
parser = new V2ScheduleParser(xlsDownloader);
|
||||
parser = new ScheduleParser(xlsDownloader);
|
||||
});
|
||||
|
||||
describe("Ошибки", () => {
|
||||
@@ -32,10 +32,10 @@ describe("V2ScheduleParser", () => {
|
||||
const schedule = await parser.getSchedule();
|
||||
expect(schedule).toBeDefined();
|
||||
|
||||
const group: GroupDto | undefined = schedule.groups.get("ИС-214/23");
|
||||
const group: Group | undefined = schedule.groups.get("ИС-214/23");
|
||||
expect(group).toBeDefined();
|
||||
|
||||
const monday: DayDto = group.days[0];
|
||||
const monday: Day = group.days[0];
|
||||
expect(monday).toBeDefined();
|
||||
|
||||
const name = monday.name;
|
||||
@@ -63,14 +63,13 @@ describe("V2ScheduleParser", () => {
|
||||
|
||||
it("Зачёт с оценкой v2", async () => {
|
||||
const schedule = await parser.getSchedule().then((v) =>
|
||||
instanceToInstance2(V2ScheduleParseResult, v, {
|
||||
instanceToInstance2(ScheduleParseResult, v, {
|
||||
groups: ["v2"],
|
||||
}),
|
||||
);
|
||||
expect(schedule).toBeDefined();
|
||||
|
||||
const group: GroupDto | undefined =
|
||||
schedule.groups.get("ИС-214/23");
|
||||
const group: Group | undefined = schedule.groups.get("ИС-214/23");
|
||||
expect(group).toBeDefined();
|
||||
|
||||
const day = group.days[0];
|
||||
@@ -5,17 +5,17 @@ import { Range, WorkSheet } from "xlsx";
|
||||
import { toNormalString, trimAll } from "../../../utility/string.util";
|
||||
import { plainToClass, plainToInstance, Type } from "class-transformer";
|
||||
import * as objectHash from "object-hash";
|
||||
import { LessonTimeDto } from "../../dto/lesson-time.dto";
|
||||
import { V2LessonType } from "../../enum/v2-lesson-type.enum";
|
||||
import { LessonSubGroupDto } from "../../dto/lesson-sub-group.dto";
|
||||
import { LessonDto } from "../../dto/lesson.dto";
|
||||
import { DayDto } from "../../dto/day.dto";
|
||||
import { GroupDto } from "../../dto/group.dto";
|
||||
import LessonTime from "../../entities/lesson-time.entity";
|
||||
import { LessonType } from "../../enum/lesson-type.enum";
|
||||
import LessonSubGroup from "../../entities/lesson-sub-group.entity";
|
||||
import Lesson from "../../entities/lesson.entity";
|
||||
import Day from "../../entities/day.entity";
|
||||
import Group from "../../entities/group.entity";
|
||||
import * as assert from "node:assert";
|
||||
import { ScheduleReplacerService } from "../../schedule-replacer.service";
|
||||
import { TeacherDto } from "../../dto/teacher.dto";
|
||||
import { TeacherDayDto } from "../../dto/teacher-day.dto";
|
||||
import { TeacherLessonDto } from "../../dto/teacher-lesson.dto";
|
||||
import Teacher from "../../entities/teacher.entity";
|
||||
import TeacherDay from "../../entities/teacher-day.entity";
|
||||
import TeacherLesson from "../../entities/teacher-lesson.entity";
|
||||
import {
|
||||
IsArray,
|
||||
IsDate,
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
ValidateNested,
|
||||
} from "class-validator";
|
||||
import { ToMap } from "create-map-transform-fn";
|
||||
import { ClassProperties } from "../../../utility/class-trasformer/class-transformer-ctor";
|
||||
|
||||
type InternalId = {
|
||||
/**
|
||||
@@ -46,12 +47,12 @@ type InternalTime = {
|
||||
/**
|
||||
* Временной отрезок
|
||||
*/
|
||||
timeRange: LessonTimeDto;
|
||||
timeRange: LessonTime;
|
||||
|
||||
/**
|
||||
* Тип пары на этой строке
|
||||
*/
|
||||
lessonType: V2LessonType;
|
||||
lessonType: LessonType;
|
||||
|
||||
/**
|
||||
* Индекс пары на этой строке
|
||||
@@ -64,7 +65,7 @@ type InternalTime = {
|
||||
xlsxRange: Range;
|
||||
};
|
||||
|
||||
export class V2ScheduleParseResult {
|
||||
export class ScheduleParseResult {
|
||||
/**
|
||||
* ETag расписания
|
||||
*/
|
||||
@@ -94,15 +95,15 @@ export class V2ScheduleParseResult {
|
||||
* Расписание групп в виде списка.
|
||||
* Ключ - название группы.
|
||||
*/
|
||||
@ToMap({ mapValueClass: GroupDto })
|
||||
groups: Map<string, GroupDto>;
|
||||
@ToMap({ mapValueClass: Group })
|
||||
groups: Map<string, Group>;
|
||||
|
||||
/**
|
||||
* Расписание преподавателей в виде списка.
|
||||
* Ключ - ФИО преподавателя
|
||||
*/
|
||||
@ToMap({ mapValueClass: TeacherDto })
|
||||
teachers: Map<string, TeacherDto>;
|
||||
@ToMap({ mapValueClass: Teacher })
|
||||
teachers: Map<string, Teacher>;
|
||||
|
||||
/**
|
||||
* Список групп у которых было обновлено расписание с момента последнего обновления файла.
|
||||
@@ -126,8 +127,8 @@ export class V2ScheduleParseResult {
|
||||
updatedTeachers: Array<Array<number>>;
|
||||
}
|
||||
|
||||
export class V2ScheduleParser {
|
||||
private lastResult: V2ScheduleParseResult | null = null;
|
||||
export class ScheduleParser {
|
||||
private lastResult: ScheduleParseResult | null = null;
|
||||
|
||||
/**
|
||||
* @param xlsDownloader - класс для загрузки расписания с сайта политехникума
|
||||
@@ -176,113 +177,94 @@ export class V2ScheduleParser {
|
||||
row: number,
|
||||
column: number,
|
||||
): string | null {
|
||||
const cell: XLSX.CellObject | null =
|
||||
worksheet[XLSX.utils.encode_cell({ r: row, c: column })];
|
||||
const cell = worksheet[
|
||||
XLSX.utils.encode_cell({ r: row, c: column })
|
||||
] as XLSX.CellObject;
|
||||
|
||||
return toNormalString(cell?.w);
|
||||
}
|
||||
|
||||
/**
|
||||
* Парсит информацию о паре исходя из текста в записи
|
||||
* @param lessonName - текст в записи
|
||||
* @param text - текст в записи
|
||||
* @returns {{
|
||||
* name: string;
|
||||
* subGroups: Array<LessonSubGroupDto>;
|
||||
* subGroups: Array<LessonSubGroup>;
|
||||
* }} - название пары и список подгрупп
|
||||
* @private
|
||||
* @static
|
||||
*/
|
||||
private static parseNameAndSubGroups(lessonName: string): {
|
||||
private static parseNameAndSubGroups(text: string): {
|
||||
name: string;
|
||||
subGroups: Array<LessonSubGroupDto>;
|
||||
subGroups: Array<LessonSubGroup>;
|
||||
} {
|
||||
if (!text) return { name: text, subGroups: [] };
|
||||
|
||||
// хд
|
||||
const lessonRegExp = /(?:[А-Я][а-я]+[А-Я]{2}(?:\([0-9][а-я]+\))?)+$/m;
|
||||
const teacherRegExp =
|
||||
/([А-Я][а-я]+)([А-Я])([А-Я])(?:\(([0-9])[а-я]+\))?/g;
|
||||
|
||||
const allRegex =
|
||||
/(?:[А-ЯЁ][а-яё]+\s[А-ЯЁ]\.\s?[А-ЯЁ]\.(?:\s?\(\s?[0-9]\s?подгруппа\s?\))?(?:,\s)?)+$/gm;
|
||||
const teacherAndSubGroupRegex =
|
||||
/(?:[А-ЯЁ][а-яё]+\s[А-ЯЁ]\.\s?[А-ЯЁ]\.(?:\s?\(\s?[0-9]\s?подгруппа\s?\))?)+/gm;
|
||||
const rawTeachers = (text
|
||||
.replaceAll(/[\s\n\t.,]+/g, "")
|
||||
.match(lessonRegExp) ?? [])[0];
|
||||
|
||||
const allMatch = allRegex.exec(lessonName);
|
||||
// если не ничего не найдено
|
||||
if (!rawTeachers)
|
||||
return {
|
||||
name: text
|
||||
.replaceAll(/[\t\n]+/g, "") // Убираем все переносы
|
||||
.replaceAll(/\s+/g, " ") // Убираем все лишние пробелы
|
||||
.trim() // Убираем пробелы по краям
|
||||
.replace(/\.$/m, ""), // Убираем точку в конце названия, если присутствует
|
||||
subGroups: [],
|
||||
};
|
||||
|
||||
// если не ничё не найдено
|
||||
if (allMatch === null) return { name: lessonName, subGroups: [] };
|
||||
const teacherIt = rawTeachers.matchAll(teacherRegExp);
|
||||
|
||||
const all: Array<string> = [];
|
||||
const subGroups: Array<LessonSubGroup> = [];
|
||||
let lessonName: string;
|
||||
|
||||
let allInnerMatch: RegExpExecArray;
|
||||
while (
|
||||
(allInnerMatch = teacherAndSubGroupRegex.exec(allMatch[0])) !== null
|
||||
) {
|
||||
if (allInnerMatch.index === teacherAndSubGroupRegex.lastIndex)
|
||||
teacherAndSubGroupRegex.lastIndex++;
|
||||
|
||||
all.push(allInnerMatch[0].trim());
|
||||
let m: RegExpMatchArray;
|
||||
while ((m = teacherIt.next().value as RegExpMatchArray)) {
|
||||
if (!lessonName) {
|
||||
lessonName = text
|
||||
.substring(0, text.indexOf(m[1]))
|
||||
.replaceAll(/[\t\n]+/g, "") // Убираем все переносы
|
||||
.replaceAll(/\s+/g, " ") // Убираем все лишние пробелы
|
||||
.trim() // Убираем пробелы по краям
|
||||
.replace(/\.$/m, ""); // Убираем точку в конце названия, если присутствует
|
||||
}
|
||||
|
||||
// парадокс
|
||||
if (all.length === 0) {
|
||||
throw new Error("Парадокс");
|
||||
}
|
||||
|
||||
const subGroups: Array<LessonSubGroupDto> = [];
|
||||
|
||||
for (const teacherAndSubGroup of all) {
|
||||
const teacherRegex = /[А-ЯЁ][а-яё]+\s[А-ЯЁ]\.\s?[А-ЯЁ]\./g;
|
||||
const subGroupRegex = /\([0-9]подгруппа\)/g;
|
||||
|
||||
const teacherMatch = teacherRegex.exec(teacherAndSubGroup);
|
||||
if (teacherMatch === null) throw new Error("Парадокс");
|
||||
|
||||
let teacherFIO = teacherMatch[0];
|
||||
const teacherSpaceIndex = teacherFIO.indexOf(" ") + 1;
|
||||
const teacherIO = teacherFIO
|
||||
.substring(teacherSpaceIndex)
|
||||
.replaceAll("s", "");
|
||||
|
||||
teacherFIO = `${teacherFIO.substring(0, teacherSpaceIndex)}${teacherIO}`;
|
||||
|
||||
const subGroupMatch = subGroupRegex.exec(
|
||||
teacherAndSubGroup.replaceAll(" ", ""),
|
||||
);
|
||||
const subGroup = subGroupMatch
|
||||
? Number.parseInt(subGroupMatch[0][1])
|
||||
: 1;
|
||||
|
||||
subGroups.push(
|
||||
plainToClass(LessonSubGroupDto, {
|
||||
teacher: teacherFIO,
|
||||
number: subGroup,
|
||||
cabinet: "",
|
||||
new LessonSubGroup({
|
||||
number: +(m[4] ?? "0"),
|
||||
cabinet: null,
|
||||
teacher: `${m[1]} ${m[2]}.${m[3]}.`,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
for (const index in subGroups) {
|
||||
if (subGroups.length === 1) {
|
||||
break;
|
||||
}
|
||||
// фикс, если у кого-то отсутствует индекс подгруппы
|
||||
|
||||
// бляздец
|
||||
switch (index) {
|
||||
case "0":
|
||||
subGroups[index].number =
|
||||
subGroups[+index + 1].number === 2 ? 1 : 2;
|
||||
continue;
|
||||
case "1":
|
||||
subGroups[index].number =
|
||||
subGroups[+index - 1].number === 1 ? 2 : 1;
|
||||
continue;
|
||||
default:
|
||||
subGroups[index].number = +index;
|
||||
// если 1 преподаватель
|
||||
if (subGroups.length === 1) subGroups[0].number = 1;
|
||||
else if (subGroups.length === 2) {
|
||||
// если индексы отсутствуют у обоих, ставим поочерёдно
|
||||
if (subGroups[0].number === 0 && subGroups[1].number === 0) {
|
||||
subGroups[0].number = 1;
|
||||
subGroups[1].number = 2;
|
||||
}
|
||||
// если индекс отсутствует у первого, ставим 2, если у второго индекс 1 и наоборот
|
||||
else if (subGroups[0].number === 0)
|
||||
subGroups[0].number = subGroups[1].number === 1 ? 2 : 1;
|
||||
// если индекс отсутствует у второго, ставим 2, если у первого индекс 1 и наоборот
|
||||
else if (subGroups[1].number === 0)
|
||||
subGroups[1].number = subGroups[0].number === 1 ? 2 : 1;
|
||||
}
|
||||
|
||||
return {
|
||||
name: lessonName
|
||||
.substring(0, allMatch.index)
|
||||
.replaceAll(".", "")
|
||||
.trim(),
|
||||
name: lessonName,
|
||||
subGroups: subGroups,
|
||||
};
|
||||
}
|
||||
@@ -308,7 +290,7 @@ export class V2ScheduleParser {
|
||||
const days: Array<InternalId> = [];
|
||||
|
||||
for (let row = range.s.r + 1; row <= range.e.r; ++row) {
|
||||
const dayName = V2ScheduleParser.getCellData(workSheet, row, 0);
|
||||
const dayName = ScheduleParser.getCellData(workSheet, row, 0);
|
||||
if (!dayName) continue;
|
||||
|
||||
if (!isHeaderParsed) {
|
||||
@@ -320,7 +302,7 @@ export class V2ScheduleParser {
|
||||
column <= range.e.c;
|
||||
++column
|
||||
) {
|
||||
const groupName = V2ScheduleParser.getCellData(
|
||||
const groupName = ScheduleParser.getCellData(
|
||||
workSheet,
|
||||
row,
|
||||
column,
|
||||
@@ -359,47 +341,45 @@ export class V2ScheduleParser {
|
||||
}
|
||||
|
||||
private static convertGroupsToTeachers(
|
||||
groups: Map<string, GroupDto>,
|
||||
): Map<string, TeacherDto> {
|
||||
const result = new Map<string, TeacherDto>();
|
||||
groups: Map<string, Group>,
|
||||
): Map<string, Teacher> {
|
||||
const result = new Map<string, Teacher>();
|
||||
|
||||
for (const groupName of groups.keys()) {
|
||||
const group = groups.get(groupName);
|
||||
|
||||
for (const day of group.days) {
|
||||
for (const lesson of day.lessons) {
|
||||
if (lesson.type !== V2LessonType.DEFAULT) continue;
|
||||
if (lesson.type !== LessonType.DEFAULT) continue;
|
||||
|
||||
for (const subGroup of lesson.subGroups) {
|
||||
let teacherDto: TeacherDto = result.get(
|
||||
subGroup.teacher,
|
||||
);
|
||||
let teacherDto: Teacher = result.get(subGroup.teacher);
|
||||
|
||||
if (!teacherDto) {
|
||||
teacherDto = new TeacherDto();
|
||||
result.set(subGroup.teacher, teacherDto);
|
||||
teacherDto = new Teacher({
|
||||
name: subGroup.teacher,
|
||||
days: [],
|
||||
});
|
||||
|
||||
teacherDto.name = subGroup.teacher;
|
||||
teacherDto.days = [];
|
||||
result.set(subGroup.teacher, teacherDto);
|
||||
}
|
||||
|
||||
let teacherDay: TeacherDayDto =
|
||||
teacherDto.days[day.name];
|
||||
let teacherDay = teacherDto.days[
|
||||
day.name
|
||||
] as TeacherDay;
|
||||
|
||||
if (!teacherDay) {
|
||||
teacherDay = teacherDto.days[day.name] =
|
||||
new TeacherDayDto();
|
||||
|
||||
// TODO: Что это блять такое?
|
||||
// noinspection JSConstantReassignment
|
||||
teacherDay.name = day.name;
|
||||
teacherDay.date = day.date;
|
||||
teacherDay.lessons = [];
|
||||
new TeacherDay({
|
||||
name: day.name,
|
||||
date: day.date,
|
||||
lessons: [],
|
||||
});
|
||||
}
|
||||
|
||||
const teacherLesson = structuredClone(
|
||||
lesson,
|
||||
) as TeacherLessonDto;
|
||||
) as TeacherLesson;
|
||||
teacherLesson.group = groupName;
|
||||
|
||||
teacherDay.lessons.push(teacherLesson);
|
||||
@@ -413,8 +393,11 @@ export class V2ScheduleParser {
|
||||
|
||||
const days = teacher.days;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-for-in-array
|
||||
for (const dayName in days) {
|
||||
const day = days[dayName];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-array-delete
|
||||
delete days[dayName];
|
||||
|
||||
day.lessons.sort(
|
||||
@@ -432,10 +415,10 @@ export class V2ScheduleParser {
|
||||
|
||||
/**
|
||||
* Возвращает текущее расписание
|
||||
* @returns {V2ScheduleParseResult} - расписание
|
||||
* @returns {ScheduleParseResult} - расписание
|
||||
* @async
|
||||
*/
|
||||
async getSchedule(): Promise<V2ScheduleParseResult> {
|
||||
async getSchedule(): Promise<ScheduleParseResult> {
|
||||
const headData = await this.xlsDownloader.fetch(true);
|
||||
this.xlsDownloader.verifyFetchResult(headData);
|
||||
|
||||
@@ -467,9 +450,9 @@ export class V2ScheduleParser {
|
||||
const workSheet = workBook.Sheets[workBook.SheetNames[0]];
|
||||
|
||||
const { groupSkeletons, daySkeletons } =
|
||||
V2ScheduleParser.parseSkeleton(workSheet);
|
||||
ScheduleParser.parseSkeleton(workSheet);
|
||||
|
||||
const groups = new Map<string, GroupDto>();
|
||||
const groups = new Map<string, Group>();
|
||||
|
||||
const daysTimes: Array<Array<InternalTime>> = [];
|
||||
let daysTimesFilled = false;
|
||||
@@ -478,13 +461,13 @@ export class V2ScheduleParser {
|
||||
.e.r;
|
||||
|
||||
for (const groupSkeleton of groupSkeletons) {
|
||||
const group = new GroupDto();
|
||||
const group = new Group();
|
||||
group.name = groupSkeleton.name;
|
||||
group.days = [];
|
||||
|
||||
for (let dayIdx = 0; dayIdx < daySkeletons.length; ++dayIdx) {
|
||||
const daySkeleton = daySkeletons[dayIdx];
|
||||
const day = new DayDto();
|
||||
const day = new Day();
|
||||
{
|
||||
const daySpaceIndex = daySkeleton.name.indexOf(" ");
|
||||
day.name = daySkeleton.name.substring(0, daySpaceIndex);
|
||||
@@ -504,8 +487,8 @@ export class V2ScheduleParser {
|
||||
? daySkeletons[dayIdx + 1].row
|
||||
: saturdayEndRow) - daySkeleton.row;
|
||||
|
||||
const dayTimes: Array<InternalTime> = daysTimesFilled
|
||||
? daysTimes[day.name]
|
||||
const dayTimes = daysTimesFilled
|
||||
? (daysTimes[day.name] as Array<InternalTime>)
|
||||
: [];
|
||||
|
||||
if (!daysTimesFilled) {
|
||||
@@ -514,7 +497,7 @@ export class V2ScheduleParser {
|
||||
row < daySkeleton.row + rowDistance;
|
||||
++row
|
||||
) {
|
||||
const time = V2ScheduleParser.getCellData(
|
||||
const time = ScheduleParser.getCellData(
|
||||
workSheet,
|
||||
row,
|
||||
lessonTimeColumn,
|
||||
@@ -524,19 +507,17 @@ export class V2ScheduleParser {
|
||||
|
||||
// type
|
||||
const lessonType = time.includes("пара")
|
||||
? V2LessonType.DEFAULT
|
||||
: V2LessonType.ADDITIONAL;
|
||||
? LessonType.DEFAULT
|
||||
: LessonType.ADDITIONAL;
|
||||
|
||||
const defaultIndex =
|
||||
lessonType === V2LessonType.DEFAULT
|
||||
? +time[0]
|
||||
: null;
|
||||
lessonType === LessonType.DEFAULT ? +time[0] : null;
|
||||
|
||||
// time
|
||||
const timeRange = new LessonTimeDto();
|
||||
|
||||
timeRange.start = new Date(day.date);
|
||||
timeRange.end = new Date(day.date);
|
||||
const timeRange = new LessonTime({
|
||||
start: new Date(day.date),
|
||||
end: new Date(day.date),
|
||||
});
|
||||
|
||||
const timeString = time.replaceAll(".", ":");
|
||||
const timeRegex = /(\d+:\d+)-(\d+:\d+)/g;
|
||||
@@ -562,7 +543,7 @@ export class V2ScheduleParser {
|
||||
lessonType: lessonType,
|
||||
defaultIndex: defaultIndex,
|
||||
|
||||
xlsxRange: V2ScheduleParser.getMergeFromStart(
|
||||
xlsxRange: ScheduleParser.getMergeFromStart(
|
||||
workSheet,
|
||||
row,
|
||||
lessonTimeColumn,
|
||||
@@ -574,7 +555,7 @@ export class V2ScheduleParser {
|
||||
}
|
||||
|
||||
for (const time of dayTimes) {
|
||||
const lessonsOrStreet = V2ScheduleParser.parseLesson(
|
||||
const lessonsOrStreet = ScheduleParser.parseLesson(
|
||||
workSheet,
|
||||
day,
|
||||
dayTimes,
|
||||
@@ -583,11 +564,11 @@ export class V2ScheduleParser {
|
||||
);
|
||||
|
||||
if (typeof lessonsOrStreet === "string") {
|
||||
day.street = lessonsOrStreet as string;
|
||||
day.street = lessonsOrStreet;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const lesson of lessonsOrStreet as Array<LessonDto>)
|
||||
for (const lesson of lessonsOrStreet)
|
||||
day.lessons.push(lesson);
|
||||
}
|
||||
|
||||
@@ -599,12 +580,12 @@ export class V2ScheduleParser {
|
||||
groups.set(group.name, group);
|
||||
}
|
||||
|
||||
const updatedGroups = V2ScheduleParser.getUpdatedGroups(
|
||||
const updatedGroups = ScheduleParser.getUpdatedGroups(
|
||||
this.lastResult?.groups,
|
||||
groups,
|
||||
);
|
||||
|
||||
const teachers = V2ScheduleParser.convertGroupsToTeachers(groups);
|
||||
const teachers = ScheduleParser.convertGroupsToTeachers(groups);
|
||||
|
||||
return (this.lastResult = {
|
||||
downloadedAt: headData.requestedAt,
|
||||
@@ -630,16 +611,16 @@ export class V2ScheduleParser {
|
||||
|
||||
private static parseLesson(
|
||||
workSheet: XLSX.Sheet,
|
||||
day: DayDto,
|
||||
day: Day,
|
||||
dayTimes: Array<InternalTime>,
|
||||
time: InternalTime,
|
||||
column: number,
|
||||
): Array<LessonDto> | string {
|
||||
): Array<Lesson> | string {
|
||||
const row = time.xlsxRange.s.r;
|
||||
|
||||
// name
|
||||
let rawName = trimAll(
|
||||
V2ScheduleParser.getCellData(workSheet, row, column)?.replaceAll(
|
||||
ScheduleParser.getCellData(workSheet, row, column)?.replaceAll(
|
||||
/[\n\r]/g,
|
||||
" ",
|
||||
) ?? "",
|
||||
@@ -647,87 +628,94 @@ export class V2ScheduleParser {
|
||||
|
||||
if (rawName.length === 0) return [];
|
||||
|
||||
const lesson = new LessonDto();
|
||||
const lessonData = {} as ClassProperties<Lesson>;
|
||||
|
||||
if (this.otherStreetRegExp.test(rawName)) return rawName;
|
||||
else if (rawName.includes("ЭКЗАМЕН")) {
|
||||
lesson.type = V2LessonType.EXAM_DEFAULT;
|
||||
lessonData.type = LessonType.EXAM_DEFAULT;
|
||||
rawName = trimAll(rawName.replace("ЭКЗАМЕН", ""));
|
||||
} else if (rawName.includes("ЗАЧЕТ С ОЦЕНКОЙ")) {
|
||||
lesson.type = V2LessonType.EXAM_WITH_GRADE;
|
||||
lessonData.type = LessonType.EXAM_WITH_GRADE;
|
||||
rawName = trimAll(rawName.replace("ЗАЧЕТ С ОЦЕНКОЙ", ""));
|
||||
} else if (rawName.includes("ЗАЧЕТ")) {
|
||||
lesson.type = V2LessonType.EXAM;
|
||||
lessonData.type = LessonType.EXAM;
|
||||
rawName = trimAll(rawName.replace("ЗАЧЕТ", ""));
|
||||
} else if (rawName.includes("(консультация)")) {
|
||||
lesson.type = V2LessonType.CONSULTATION;
|
||||
lessonData.type = LessonType.CONSULTATION;
|
||||
rawName = trimAll(rawName.replace("(консультация)", ""));
|
||||
} else if (this.consultationRegExp.test(rawName)) {
|
||||
lesson.type = V2LessonType.CONSULTATION;
|
||||
lessonData.type = LessonType.CONSULTATION;
|
||||
rawName = trimAll(rawName.replace(this.consultationRegExp, ""));
|
||||
} else if (rawName.includes("САМОСТОЯТЕЛЬНАЯ РАБОТА")) {
|
||||
lesson.type = V2LessonType.INDEPENDENT_WORK;
|
||||
lessonData.type = LessonType.INDEPENDENT_WORK;
|
||||
rawName = trimAll(rawName.replace("САМОСТОЯТЕЛЬНАЯ РАБОТА", ""));
|
||||
} else lesson.type = time.lessonType;
|
||||
} else lessonData.type = time.lessonType;
|
||||
|
||||
lesson.defaultRange =
|
||||
lessonData.defaultRange =
|
||||
time.defaultIndex !== null
|
||||
? [time.defaultIndex, time.defaultIndex]
|
||||
: null;
|
||||
|
||||
lesson.time = new LessonTimeDto();
|
||||
lesson.time.start = time.timeRange.start;
|
||||
|
||||
// check if multi-lesson
|
||||
const range = this.getMergeFromStart(workSheet, row, column);
|
||||
const endTime = dayTimes.filter((dayTime) => {
|
||||
return dayTime.xlsxRange.e.r === range.e.r;
|
||||
})[0];
|
||||
lesson.time.end = endTime?.timeRange.end ?? time.timeRange.end;
|
||||
|
||||
if (lesson.defaultRange !== null)
|
||||
lesson.defaultRange[1] = endTime?.defaultIndex ?? time.defaultIndex;
|
||||
lessonData.time = new LessonTime({
|
||||
start: time.timeRange.start,
|
||||
end: endTime?.timeRange.end ?? time.timeRange.end,
|
||||
});
|
||||
|
||||
if (lessonData.defaultRange !== null)
|
||||
lessonData.defaultRange[1] =
|
||||
endTime?.defaultIndex ?? time.defaultIndex;
|
||||
|
||||
// name and subGroups (subGroups unfilled)
|
||||
{
|
||||
const nameAndGroups = V2ScheduleParser.parseNameAndSubGroups(
|
||||
trimAll(rawName?.replaceAll(/[\n\r]/g, "") ?? ""),
|
||||
const nameAndGroups = ScheduleParser.parseNameAndSubGroups(
|
||||
rawName ?? "",
|
||||
);
|
||||
|
||||
lesson.name = nameAndGroups.name;
|
||||
lesson.subGroups = nameAndGroups.subGroups;
|
||||
lessonData.name = nameAndGroups.name;
|
||||
lessonData.subGroups = nameAndGroups.subGroups;
|
||||
}
|
||||
|
||||
// cabinets
|
||||
{
|
||||
const cabinets = V2ScheduleParser.parseCabinets(
|
||||
const cabinets = ScheduleParser.parseCabinets(
|
||||
workSheet,
|
||||
row,
|
||||
column + 1,
|
||||
);
|
||||
|
||||
if (cabinets.length === 1) {
|
||||
for (const index in lesson.subGroups)
|
||||
lesson.subGroups[index].cabinet = cabinets[0];
|
||||
} else if (cabinets.length === lesson.subGroups.length) {
|
||||
for (const index in lesson.subGroups)
|
||||
lesson.subGroups[index].cabinet = cabinets[index];
|
||||
// eslint-disable-next-line @typescript-eslint/no-for-in-array
|
||||
for (const index in lessonData.subGroups)
|
||||
lessonData.subGroups[index].cabinet = cabinets[0];
|
||||
} else if (cabinets.length === lessonData.subGroups.length) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-for-in-array
|
||||
for (const index in lessonData.subGroups) {
|
||||
lessonData.subGroups[index].cabinet =
|
||||
cabinets[lessonData.subGroups[index].number - 1];
|
||||
}
|
||||
} else if (cabinets.length !== 0) {
|
||||
if (cabinets.length > lesson.subGroups.length) {
|
||||
if (cabinets.length > lessonData.subGroups.length) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-for-in-array
|
||||
for (const index in cabinets) {
|
||||
if (lesson.subGroups[index] === undefined) {
|
||||
lesson.subGroups.push(
|
||||
plainToInstance(LessonSubGroupDto, {
|
||||
if (lessonData.subGroups[index] === undefined) {
|
||||
lessonData.subGroups.push(
|
||||
plainToInstance(LessonSubGroup, {
|
||||
number: +index + 1,
|
||||
teacher: "Ошибка в расписании",
|
||||
cabinet: cabinets[index],
|
||||
} as LessonSubGroupDto),
|
||||
} as LessonSubGroup),
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
lesson.subGroups[index].cabinet = cabinets[index];
|
||||
lessonData.subGroups[index].cabinet = cabinets[index];
|
||||
}
|
||||
} else throw new Error("Разное кол-во кабинетов и подгрупп!");
|
||||
}
|
||||
@@ -738,20 +726,20 @@ export class V2ScheduleParser {
|
||||
? null
|
||||
: day.lessons[day.lessons.length - 1];
|
||||
|
||||
if (!prevLesson) return [lesson];
|
||||
if (!prevLesson) return [lessonData];
|
||||
|
||||
return [
|
||||
plainToInstance(LessonDto, {
|
||||
type: V2LessonType.BREAK,
|
||||
new Lesson({
|
||||
type: LessonType.BREAK,
|
||||
defaultRange: null,
|
||||
name: null,
|
||||
time: plainToInstance(LessonTimeDto, {
|
||||
time: new LessonTime({
|
||||
start: prevLesson.time.end,
|
||||
end: lesson.time.start,
|
||||
} as LessonTimeDto),
|
||||
end: lessonData.time.start,
|
||||
}),
|
||||
subGroups: [],
|
||||
} as LessonDto),
|
||||
lesson,
|
||||
}),
|
||||
new Lesson(lessonData),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -762,7 +750,7 @@ export class V2ScheduleParser {
|
||||
) {
|
||||
const cabinets: Array<string> = [];
|
||||
{
|
||||
const rawCabinets = V2ScheduleParser.getCellData(
|
||||
const rawCabinets = ScheduleParser.getCellData(
|
||||
workSheet,
|
||||
row,
|
||||
column,
|
||||
@@ -782,8 +770,8 @@ export class V2ScheduleParser {
|
||||
}
|
||||
|
||||
private static getUpdatedGroups(
|
||||
cachedGroups: Map<string, GroupDto> | null,
|
||||
currentGroups: Map<string, GroupDto>,
|
||||
cachedGroups: Map<string, Group> | null,
|
||||
currentGroups: Map<string, Group>,
|
||||
): Array<Array<number>> {
|
||||
if (!cachedGroups) return [];
|
||||
|
||||
@@ -795,6 +783,7 @@ export class V2ScheduleParser {
|
||||
|
||||
const affectedGroupDays: Array<number> = [];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-for-in-array
|
||||
for (const dayIdx in currentGroup.days) {
|
||||
if (
|
||||
objectHash.sha1(currentGroup.days[dayIdx]) !==
|
||||
@@ -4,7 +4,7 @@ import { XlsDownloaderInterface } from "./xls-downloader.interface";
|
||||
describe("BasicXlsDownloader", () => {
|
||||
let downloader: XlsDownloaderInterface;
|
||||
|
||||
beforeEach(async () => {
|
||||
beforeEach(() => {
|
||||
downloader = new BasicXlsDownloader();
|
||||
});
|
||||
|
||||
|
||||
@@ -52,10 +52,10 @@ export class BasicXlsDownloader implements XlsDownloaderInterface {
|
||||
|
||||
type HeaderValue = string | undefined;
|
||||
|
||||
const contentType: HeaderValue = response.headers["content-type"];
|
||||
const etag: HeaderValue = response.headers["etag"];
|
||||
const uploadedAt: HeaderValue = response.headers["last-modified"];
|
||||
const requestedAt: HeaderValue = response.headers["date"];
|
||||
const contentType = response.headers["content-type"] as HeaderValue;
|
||||
const etag = response.headers["etag"] as HeaderValue;
|
||||
const uploadedAt = response.headers["last-modified"] as HeaderValue;
|
||||
const requestedAt = response.headers["date"] as HeaderValue;
|
||||
|
||||
if (!contentType || !etag || !uploadedAt || !requestedAt) {
|
||||
return {
|
||||
@@ -77,7 +77,7 @@ export class BasicXlsDownloader implements XlsDownloaderInterface {
|
||||
etag: etag,
|
||||
uploadedAt: new Date(uploadedAt),
|
||||
requestedAt: new Date(requestedAt),
|
||||
data: head ? undefined : response.data.buffer,
|
||||
data: head ? undefined : (response.data as Buffer).buffer,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -20,11 +20,11 @@ import {
|
||||
ApiTags,
|
||||
} from "@nestjs/swagger";
|
||||
import { ResultDto } from "src/utility/validation/class-validator.interceptor";
|
||||
import { UserRole } from "../users/user-role.enum";
|
||||
import { ScheduleReplacerDto } from "./dto/schedule-replacer.dto";
|
||||
import { ClearScheduleReplacerDto } from "./dto/clear-schedule-replacer.dto";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
import { ScheduleService } from "./schedule.service";
|
||||
import UserRole from "../users/user-role.enum";
|
||||
import ReplacerDto from "./dto/replacer.dto";
|
||||
import ClearReplacerDto from "./dto/clear-replacer.dto";
|
||||
|
||||
@ApiTags("v1/schedule-replacer")
|
||||
@ApiBearerAuth()
|
||||
@@ -69,13 +69,13 @@ export class ScheduleReplacerController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@AuthRoles([UserRole.ADMIN])
|
||||
@ResultDto(null) // TODO: Как нибудь сделать проверку в таких случаях
|
||||
async getReplacers(): Promise<ScheduleReplacerDto[]> {
|
||||
async getReplacers(): Promise<ReplacerDto[]> {
|
||||
return await this.scheduleReplaceService.getAll().then((result) => {
|
||||
return result.map((replacer) => {
|
||||
return plainToInstance(ScheduleReplacerDto, {
|
||||
return plainToInstance(ReplacerDto, {
|
||||
etag: replacer.etag,
|
||||
size: replacer.data.byteLength,
|
||||
} as ScheduleReplacerDto);
|
||||
} as ReplacerDto);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -84,13 +84,13 @@ export class ScheduleReplacerController {
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Отчистка прошла успешно",
|
||||
type: ClearScheduleReplacerDto,
|
||||
type: ClearReplacerDto,
|
||||
})
|
||||
@Post("clear")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@AuthRoles([UserRole.ADMIN])
|
||||
@ResultDto(ClearScheduleReplacerDto)
|
||||
async clear(): Promise<ClearScheduleReplacerDto> {
|
||||
@ResultDto(ClearReplacerDto)
|
||||
async clear(): Promise<ClearReplacerDto> {
|
||||
const response = { count: await this.scheduleReplaceService.clear() };
|
||||
|
||||
await this.scheduleService.refreshCache();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { SetScheduleReplacerDto } from "./dto/set-schedule-replacer.dto";
|
||||
import SetScheduleReplacerDto from "./dto/set-schedule-replacer.dto";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
NotAcceptableException,
|
||||
Param,
|
||||
Patch,
|
||||
UseGuards,
|
||||
@@ -20,22 +22,22 @@ import { AuthRoles, AuthUnauthorized } from "../auth/auth-role.decorator";
|
||||
import { UserToken } from "../auth/auth.decorator";
|
||||
import { UserPipe } from "../auth/auth.pipe";
|
||||
import { ScheduleService } from "./schedule.service";
|
||||
import { ScheduleDto } from "./dto/schedule.dto";
|
||||
import { CacheInterceptor, CacheKey } from "@nestjs/cache-manager";
|
||||
import { UserRole } from "../users/user-role.enum";
|
||||
import { User } from "../users/entity/user.entity";
|
||||
import { CacheStatusDto } from "./dto/cache-status.dto";
|
||||
import { GroupScheduleDto } from "./dto/group-schedule.dto";
|
||||
import { ScheduleGroupNamesDto } from "./dto/schedule-group-names.dto";
|
||||
import { TeacherScheduleDto } from "./dto/teacher-schedule.dto";
|
||||
import { ScheduleTeacherNamesDto } from "./dto/schedule-teacher-names.dto";
|
||||
import instanceToInstance2 from "../utility/class-trasformer/instance-to-instance-2";
|
||||
import Schedule from "./entities/schedule.entity";
|
||||
import UserRole from "src/users/user-role.enum";
|
||||
import GroupSchedule from "./entities/group-schedule.entity";
|
||||
import User from "src/users/entity/user.entity";
|
||||
import GroupNamesDto from "./dto/get-group-names.dto";
|
||||
import TeacherSchedule from "./entities/teacher-schedule.entity";
|
||||
import TeacherNamesDto from "./dto/teacher-names.dto";
|
||||
import CacheStatusDto from "./dto/cache-status.dto";
|
||||
import UpdateDownloadUrlDto from "./dto/update-download-url.dto";
|
||||
|
||||
@ApiTags("v2/schedule")
|
||||
@ApiTags("v1/schedule")
|
||||
@ApiBearerAuth()
|
||||
@Controller({ path: "schedule", version: "2" })
|
||||
@Controller({ path: "schedule", version: "1" })
|
||||
@UseGuards(AuthGuard)
|
||||
export class V2ScheduleController {
|
||||
export class ScheduleController {
|
||||
constructor(private readonly scheduleService: ScheduleService) {}
|
||||
|
||||
@ApiOperation({
|
||||
@@ -45,58 +47,49 @@ export class V2ScheduleController {
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Расписание получено успешно",
|
||||
type: ScheduleDto,
|
||||
type: Schedule,
|
||||
})
|
||||
@ResultDto(ScheduleDto)
|
||||
@ResultDto(Schedule)
|
||||
@AuthRoles([UserRole.ADMIN])
|
||||
@CacheKey("v2-schedule")
|
||||
@CacheKey("schedule")
|
||||
@UseInterceptors(CacheInterceptor)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get()
|
||||
async getSchedule(): Promise<ScheduleDto> {
|
||||
return await this.scheduleService.getSchedule().then((result) =>
|
||||
instanceToInstance2(ScheduleDto, result, {
|
||||
groups: ["v1"],
|
||||
}),
|
||||
);
|
||||
async getSchedule(): Promise<Schedule> {
|
||||
return await this.scheduleService.getSchedule();
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Получение расписания группы" })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Расписание получено успешно",
|
||||
type: GroupScheduleDto,
|
||||
type: GroupSchedule,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: "Требуемая группа не найдена",
|
||||
})
|
||||
@ResultDto(GroupScheduleDto)
|
||||
@ResultDto(null)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("group")
|
||||
async getGroupSchedule(
|
||||
@UserToken(UserPipe) user: User,
|
||||
): Promise<GroupScheduleDto> {
|
||||
return await this.scheduleService.getGroup(user.group).then((result) =>
|
||||
instanceToInstance2(GroupScheduleDto, result, {
|
||||
groups: ["v1"],
|
||||
}),
|
||||
);
|
||||
): Promise<GroupSchedule> {
|
||||
return await this.scheduleService.getGroup(user.group);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Получение списка названий групп" })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Список получен успешно",
|
||||
type: ScheduleGroupNamesDto,
|
||||
type: GroupNamesDto,
|
||||
})
|
||||
@ResultDto(ScheduleGroupNamesDto)
|
||||
@CacheKey("v2-schedule-group-names")
|
||||
@ResultDto(null)
|
||||
@CacheKey("schedule-group-names")
|
||||
@UseInterceptors(CacheInterceptor)
|
||||
@AuthUnauthorized()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("group-names")
|
||||
async getGroupNames(): Promise<ScheduleGroupNamesDto> {
|
||||
async getGroupNames(): Promise<GroupNamesDto> {
|
||||
return await this.scheduleService.getGroupNames();
|
||||
}
|
||||
|
||||
@@ -104,38 +97,40 @@ export class V2ScheduleController {
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Расписание получено успешно",
|
||||
type: TeacherScheduleDto,
|
||||
type: TeacherSchedule,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: "Требуемый преподаватель не найден",
|
||||
})
|
||||
@ResultDto(TeacherScheduleDto)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ResultDto(null)
|
||||
@Get("teacher/:name")
|
||||
async getTeacherSchedule(
|
||||
@Param("name") name: string,
|
||||
): Promise<TeacherScheduleDto> {
|
||||
return await this.scheduleService.getTeacher(name).then((result) =>
|
||||
instanceToInstance2(TeacherScheduleDto, result, {
|
||||
groups: ["v1"],
|
||||
}),
|
||||
);
|
||||
@UserToken(UserPipe) user: User,
|
||||
): Promise<TeacherSchedule> {
|
||||
if (name === "self") {
|
||||
if (user.role === UserRole.STUDENT)
|
||||
throw new NotAcceptableException();
|
||||
|
||||
return await this.scheduleService.getTeacher(user.username);
|
||||
}
|
||||
|
||||
return await this.scheduleService.getTeacher(name);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Получение списка ФИО преподавателей" })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Список получен успешно",
|
||||
type: ScheduleTeacherNamesDto,
|
||||
type: TeacherNamesDto,
|
||||
})
|
||||
@ResultDto(ScheduleTeacherNamesDto)
|
||||
@CacheKey("v2-schedule-teacher-names")
|
||||
@ResultDto(null)
|
||||
@CacheKey("schedule-teacher-names")
|
||||
@UseInterceptors(CacheInterceptor)
|
||||
@AuthUnauthorized()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("teacher-names")
|
||||
async getTeacherNames(): Promise<ScheduleTeacherNamesDto> {
|
||||
async getTeacherNames(): Promise<TeacherNamesDto> {
|
||||
return await this.scheduleService.getTeacherNames();
|
||||
}
|
||||
|
||||
@@ -152,8 +147,10 @@ export class V2ScheduleController {
|
||||
@ResultDto(CacheStatusDto)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Patch("update-download-url")
|
||||
async updateDownloadUrl(): Promise<CacheStatusDto> {
|
||||
return this.scheduleService.getCacheStatus();
|
||||
async updateDownloadUrl(
|
||||
@Body() reqDto: UpdateDownloadUrlDto,
|
||||
): Promise<CacheStatusDto> {
|
||||
return await this.scheduleService.updateDownloadUrl(reqDto.url);
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
@@ -166,7 +163,6 @@ export class V2ScheduleController {
|
||||
type: CacheStatusDto,
|
||||
})
|
||||
@ResultDto(CacheStatusDto)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("cache-status")
|
||||
getCacheStatus(): CacheStatusDto {
|
||||
return this.scheduleService.getCacheStatus();
|
||||
@@ -5,19 +5,12 @@ import { UsersModule } from "src/users/users.module";
|
||||
import { ScheduleReplacerService } from "./schedule-replacer.service";
|
||||
import { ScheduleReplacerController } from "./schedule-replacer.controller";
|
||||
import { ScheduleService } from "./schedule.service";
|
||||
import { V2ScheduleController } from "./v2-schedule.controller";
|
||||
import { V3ScheduleController } from "./v3-schedule.controller";
|
||||
import { V4ScheduleController } from "./v4-schedule.controller";
|
||||
import { ScheduleController } from "./schedule.controller";
|
||||
|
||||
@Module({
|
||||
imports: [forwardRef(() => UsersModule), FirebaseAdminModule],
|
||||
providers: [PrismaService, ScheduleService, ScheduleReplacerService],
|
||||
controllers: [
|
||||
V2ScheduleController,
|
||||
V3ScheduleController,
|
||||
V4ScheduleController,
|
||||
ScheduleReplacerController,
|
||||
],
|
||||
controllers: [ScheduleController, ScheduleReplacerController],
|
||||
exports: [ScheduleService],
|
||||
})
|
||||
export class ScheduleModule {}
|
||||
|
||||
@@ -5,21 +5,21 @@ import { plainToInstance } from "class-transformer";
|
||||
import { ScheduleReplacerService } from "./schedule-replacer.service";
|
||||
import { FirebaseAdminService } from "../firebase-admin/firebase-admin.service";
|
||||
import { scheduleConstants } from "../contants";
|
||||
import { ScheduleDto } from "./dto/schedule.dto";
|
||||
import {
|
||||
V2ScheduleParser,
|
||||
V2ScheduleParseResult,
|
||||
} from "./internal/schedule-parser/v2-schedule-parser";
|
||||
ScheduleParser,
|
||||
ScheduleParseResult,
|
||||
} from "./internal/schedule-parser/schedule-parser";
|
||||
import * as objectHash from "object-hash";
|
||||
import { CacheStatusDto } from "./dto/cache-status.dto";
|
||||
import { GroupScheduleDto } from "./dto/group-schedule.dto";
|
||||
import { ScheduleGroupNamesDto } from "./dto/schedule-group-names.dto";
|
||||
import { TeacherScheduleDto } from "./dto/teacher-schedule.dto";
|
||||
import { ScheduleTeacherNamesDto } from "./dto/schedule-teacher-names.dto";
|
||||
import CacheStatusDto from "./dto/cache-status.dto";
|
||||
import Schedule from "./entities/schedule.entity";
|
||||
import GroupSchedule from "./entities/group-schedule.entity";
|
||||
import TeacherSchedule from "./entities/teacher-schedule.entity";
|
||||
import GetGroupNamesDto from "./dto/get-group-names.dto";
|
||||
import TeacherNamesDto from "./dto/teacher-names.dto";
|
||||
|
||||
@Injectable()
|
||||
export class ScheduleService {
|
||||
readonly scheduleParser: V2ScheduleParser;
|
||||
readonly scheduleParser: ScheduleParser;
|
||||
|
||||
private cacheUpdatedAt: Date = new Date(0);
|
||||
private cacheHash: string = "0000000000000000000000000000000000000000";
|
||||
@@ -31,11 +31,12 @@ export class ScheduleService {
|
||||
private readonly scheduleReplacerService: ScheduleReplacerService,
|
||||
private readonly firebaseAdminService: FirebaseAdminService,
|
||||
) {
|
||||
setInterval(async () => {
|
||||
setInterval(() => {
|
||||
const now = new Date();
|
||||
if (now.getHours() != 7 || now.getMinutes() != 30) return;
|
||||
|
||||
await this.firebaseAdminService.sendByTopic("common", {
|
||||
this.firebaseAdminService
|
||||
.sendByTopic("common", {
|
||||
android: {
|
||||
priority: "high",
|
||||
ttl: 60 * 60 * 1000,
|
||||
@@ -43,10 +44,11 @@ export class ScheduleService {
|
||||
data: {
|
||||
type: "lessons-start",
|
||||
},
|
||||
});
|
||||
})
|
||||
.then();
|
||||
}, 60000);
|
||||
|
||||
this.scheduleParser = new V2ScheduleParser(
|
||||
this.scheduleParser = new ScheduleParser(
|
||||
new BasicXlsDownloader(),
|
||||
this.scheduleReplacerService,
|
||||
);
|
||||
@@ -63,7 +65,7 @@ export class ScheduleService {
|
||||
});
|
||||
}
|
||||
|
||||
async getSourceSchedule(): Promise<V2ScheduleParseResult> {
|
||||
async getSourceSchedule(): Promise<ScheduleParseResult> {
|
||||
const schedule = await this.scheduleParser.getSchedule();
|
||||
|
||||
this.cacheUpdatedAt = new Date();
|
||||
@@ -91,7 +93,7 @@ export class ScheduleService {
|
||||
return schedule;
|
||||
}
|
||||
|
||||
async getSchedule(): Promise<ScheduleDto> {
|
||||
async getSchedule(): Promise<Schedule> {
|
||||
const sourceSchedule = await this.getSourceSchedule();
|
||||
|
||||
return {
|
||||
@@ -101,7 +103,7 @@ export class ScheduleService {
|
||||
};
|
||||
}
|
||||
|
||||
async getGroup(name: string): Promise<GroupScheduleDto> {
|
||||
async getGroup(name: string): Promise<GroupSchedule> {
|
||||
const schedule = await this.getSourceSchedule();
|
||||
|
||||
const group = schedule.groups.get(name);
|
||||
@@ -114,22 +116,22 @@ export class ScheduleService {
|
||||
return {
|
||||
updatedAt: this.cacheUpdatedAt,
|
||||
group: group,
|
||||
updated: schedule.updatedGroups[name] ?? [],
|
||||
updated: (schedule.updatedGroups[name] as Array<number>) ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
async getGroupNames(): Promise<ScheduleGroupNamesDto> {
|
||||
async getGroupNames(): Promise<GetGroupNamesDto> {
|
||||
const schedule = await this.getSourceSchedule();
|
||||
const names: Array<string> = [];
|
||||
|
||||
for (const name of schedule.groups.keys()) names.push(name);
|
||||
|
||||
return plainToInstance(ScheduleGroupNamesDto, {
|
||||
return plainToInstance(GetGroupNamesDto, {
|
||||
names: names,
|
||||
});
|
||||
}
|
||||
|
||||
async getTeacher(name: string): Promise<TeacherScheduleDto> {
|
||||
async getTeacher(name: string): Promise<TeacherSchedule> {
|
||||
const schedule = await this.getSourceSchedule();
|
||||
|
||||
const teacher = schedule.teachers.get(name);
|
||||
@@ -142,17 +144,20 @@ export class ScheduleService {
|
||||
return {
|
||||
updatedAt: this.cacheUpdatedAt,
|
||||
teacher: teacher,
|
||||
updated: schedule.updatedGroups[name] ?? [],
|
||||
updated: (schedule.updatedGroups[name] as Array<number>) ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
async getTeacherNames(): Promise<ScheduleTeacherNamesDto> {
|
||||
async getTeacherNames(): Promise<TeacherNamesDto> {
|
||||
const schedule = await this.getSourceSchedule();
|
||||
const names: Array<string> = [];
|
||||
|
||||
for (const name of schedule.teachers.keys()) names.push(name);
|
||||
for (const name of schedule.teachers.keys()) {
|
||||
if (name === "Ошибка в расписании") continue;
|
||||
names.push(name);
|
||||
}
|
||||
|
||||
return plainToInstance(ScheduleTeacherNamesDto, {
|
||||
return plainToInstance(TeacherNamesDto, {
|
||||
names: names,
|
||||
});
|
||||
}
|
||||
@@ -166,7 +171,7 @@ export class ScheduleService {
|
||||
}
|
||||
|
||||
async refreshCache() {
|
||||
await this.cacheManager.reset();
|
||||
await this.cacheManager.clear();
|
||||
|
||||
await this.getSourceSchedule();
|
||||
}
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Param,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
} from "@nestjs/common";
|
||||
import { AuthGuard } from "../auth/auth.guard";
|
||||
import { ResultDto } from "../utility/validation/class-validator.interceptor";
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
} from "@nestjs/swagger";
|
||||
import { AuthRoles } from "../auth/auth-role.decorator";
|
||||
import { UserToken } from "../auth/auth.decorator";
|
||||
import { UserPipe } from "../auth/auth.pipe";
|
||||
import { ScheduleService } from "./schedule.service";
|
||||
import { ScheduleDto } from "./dto/schedule.dto";
|
||||
import { CacheInterceptor, CacheKey } from "@nestjs/cache-manager";
|
||||
import { UserRole } from "../users/user-role.enum";
|
||||
import { User } from "../users/entity/user.entity";
|
||||
import { GroupScheduleDto } from "./dto/group-schedule.dto";
|
||||
import { TeacherScheduleDto } from "./dto/teacher-schedule.dto";
|
||||
import instanceToInstance2 from "../utility/class-trasformer/instance-to-instance-2";
|
||||
|
||||
@ApiTags("v3/schedule")
|
||||
@ApiBearerAuth()
|
||||
@Controller({ path: "schedule", version: "3" })
|
||||
@UseGuards(AuthGuard)
|
||||
export class V3ScheduleController {
|
||||
constructor(private readonly scheduleService: ScheduleService) {}
|
||||
|
||||
@ApiOperation({
|
||||
summary: "Получение расписания",
|
||||
tags: ["admin"],
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Расписание получено успешно",
|
||||
type: ScheduleDto,
|
||||
})
|
||||
@ResultDto(ScheduleDto)
|
||||
@AuthRoles([UserRole.ADMIN])
|
||||
@CacheKey("v3-schedule")
|
||||
@UseInterceptors(CacheInterceptor)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get()
|
||||
async getSchedule(): Promise<ScheduleDto> {
|
||||
return await this.scheduleService.getSchedule().then((result) =>
|
||||
instanceToInstance2(ScheduleDto, result, {
|
||||
groups: ["v2"],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Получение расписания группы" })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Расписание получено успешно",
|
||||
type: GroupScheduleDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: "Требуемая группа не найдена",
|
||||
})
|
||||
@ResultDto(GroupScheduleDto)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("group")
|
||||
async getGroupSchedule(
|
||||
@UserToken(UserPipe) user: User,
|
||||
): Promise<GroupScheduleDto> {
|
||||
return await this.scheduleService.getGroup(user.group).then((result) =>
|
||||
instanceToInstance2(GroupScheduleDto, result, {
|
||||
groups: ["v2"],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Получение расписания преподавателя" })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Расписание получено успешно",
|
||||
type: TeacherScheduleDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: "Требуемый преподаватель не найден",
|
||||
})
|
||||
@ResultDto(TeacherScheduleDto)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("teacher/:name")
|
||||
async getTeacherSchedule(
|
||||
@Param("name") name: string,
|
||||
): Promise<TeacherScheduleDto> {
|
||||
return await this.scheduleService.getTeacher(name).then((result) =>
|
||||
instanceToInstance2(TeacherScheduleDto, result, {
|
||||
groups: ["v2"],
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Param,
|
||||
Patch,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
} from "@nestjs/common";
|
||||
import { AuthGuard } from "../auth/auth.guard";
|
||||
import { ResultDto } from "../utility/validation/class-validator.interceptor";
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
} from "@nestjs/swagger";
|
||||
import { AuthRoles } from "../auth/auth-role.decorator";
|
||||
import { UserToken } from "../auth/auth.decorator";
|
||||
import { UserPipe } from "../auth/auth.pipe";
|
||||
import { ScheduleService } from "./schedule.service";
|
||||
import { ScheduleDto } from "./dto/schedule.dto";
|
||||
import { CacheInterceptor, CacheKey } from "@nestjs/cache-manager";
|
||||
import { UserRole } from "../users/user-role.enum";
|
||||
import { User } from "../users/entity/user.entity";
|
||||
import { GroupScheduleDto } from "./dto/group-schedule.dto";
|
||||
import { TeacherScheduleDto } from "./dto/teacher-schedule.dto";
|
||||
import instanceToInstance2 from "../utility/class-trasformer/instance-to-instance-2";
|
||||
import { CacheStatusDto } from "./dto/cache-status.dto";
|
||||
import { UpdateDownloadUrlDto } from "./dto/update-download-url.dto";
|
||||
|
||||
@ApiTags("v4/schedule")
|
||||
@ApiBearerAuth()
|
||||
@Controller({ path: "schedule", version: "4" })
|
||||
@UseGuards(AuthGuard)
|
||||
export class V4ScheduleController {
|
||||
constructor(private readonly scheduleService: ScheduleService) {}
|
||||
|
||||
@ApiOperation({
|
||||
summary: "Получение расписания",
|
||||
tags: ["admin"],
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Расписание получено успешно",
|
||||
type: ScheduleDto,
|
||||
})
|
||||
@ResultDto(ScheduleDto)
|
||||
@AuthRoles([UserRole.ADMIN])
|
||||
@CacheKey("v4-schedule")
|
||||
@UseInterceptors(CacheInterceptor)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get()
|
||||
async getSchedule(): Promise<ScheduleDto> {
|
||||
return await this.scheduleService.getSchedule().then((result) =>
|
||||
instanceToInstance2(ScheduleDto, result, {
|
||||
groups: ["v3"],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Получение расписания группы" })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Расписание получено успешно",
|
||||
type: GroupScheduleDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: "Требуемая группа не найдена",
|
||||
})
|
||||
@ResultDto(GroupScheduleDto)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("group")
|
||||
async getGroupSchedule(
|
||||
@UserToken(UserPipe) user: User,
|
||||
): Promise<GroupScheduleDto> {
|
||||
return await this.scheduleService.getGroup(user.group).then((result) =>
|
||||
instanceToInstance2(GroupScheduleDto, result, {
|
||||
groups: ["v3"],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Получение расписания преподавателя" })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Расписание получено успешно",
|
||||
type: TeacherScheduleDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_FOUND,
|
||||
description: "Требуемый преподаватель не найден",
|
||||
})
|
||||
@ResultDto(TeacherScheduleDto)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("teacher/:name")
|
||||
async getTeacherSchedule(
|
||||
@Param("name") name: string,
|
||||
): Promise<TeacherScheduleDto> {
|
||||
return await this.scheduleService.getTeacher(name).then((result) =>
|
||||
instanceToInstance2(TeacherScheduleDto, result, {
|
||||
groups: ["v3"],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Обновление основной страницы политехникума" })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Данные обновлены успешно",
|
||||
type: CacheStatusDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: HttpStatus.NOT_ACCEPTABLE,
|
||||
description: "Передан некорректный код страницы",
|
||||
})
|
||||
@ResultDto(CacheStatusDto)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Patch("update-download-url")
|
||||
async updateDownloadUrl(
|
||||
@Body() reqDto: UpdateDownloadUrlDto,
|
||||
): Promise<CacheStatusDto> {
|
||||
return await this.scheduleService.updateDownloadUrl(reqDto.url);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
import { PickType } from "@nestjs/swagger";
|
||||
import { User } from "../entity/user.entity";
|
||||
import { IsString } from "class-validator";
|
||||
|
||||
export class ChangeGroupDto extends PickType(User, ["group"]) {}
|
||||
export default class ChangeGroupDto {
|
||||
/**
|
||||
* Группа
|
||||
* @example "ИС-214/23"
|
||||
*/
|
||||
@IsString()
|
||||
group: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { PickType } from "@nestjs/swagger";
|
||||
import { User } from "../entity/user.entity";
|
||||
import { IsString, MaxLength, MinLength } from "class-validator";
|
||||
|
||||
export class ChangeUsernameDto extends PickType(User, ["username"]) {}
|
||||
export default class ChangeUsernameDto {
|
||||
/**
|
||||
* Имя
|
||||
* @example "n08i40k"
|
||||
*/
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(20)
|
||||
username: string;
|
||||
}
|
||||
|
||||
70
src/users/dto/user.dto.ts
Normal file
70
src/users/dto/user.dto.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
IsEnum,
|
||||
IsJWT,
|
||||
IsMongoId,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
MinLength,
|
||||
} from "class-validator";
|
||||
import { Exclude, Expose, plainToInstance } from "class-transformer";
|
||||
import UserRole from "../user-role.enum";
|
||||
|
||||
@Exclude()
|
||||
export default class UserDto {
|
||||
/**
|
||||
* Идентификатор (ObjectId)
|
||||
* @example "66e1b7e255c5d5f1268cce90"
|
||||
*/
|
||||
@Expose()
|
||||
@IsMongoId()
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* Имя
|
||||
* @example "n08i40k"
|
||||
*/
|
||||
@Expose()
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
@MaxLength(20)
|
||||
username: string;
|
||||
|
||||
/**
|
||||
* Группа
|
||||
* @example "ИС-214/23"
|
||||
*/
|
||||
@Expose()
|
||||
@IsString()
|
||||
group: string;
|
||||
|
||||
/**
|
||||
* Роль
|
||||
* @example STUDENT
|
||||
*/
|
||||
@Expose()
|
||||
@IsEnum(UserRole)
|
||||
role: UserRole;
|
||||
|
||||
/**
|
||||
* Идентификатор аккаунта VK
|
||||
* @example "2.0.0"
|
||||
*/
|
||||
@Expose()
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
vkId?: number;
|
||||
|
||||
/**
|
||||
* Последний токен доступа
|
||||
* @example "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXCJ9..."
|
||||
*/
|
||||
@Expose({ groups: ["auth"] })
|
||||
@IsJWT({ groups: ["auth"] })
|
||||
accessToken: string;
|
||||
|
||||
static fromPlain(plain: object, groups: Array<string> = []): UserDto {
|
||||
return plainToInstance(UserDto, plain, { groups: groups });
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { OmitType } from "@nestjs/swagger";
|
||||
import { User } from "../../entity/user.entity";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
|
||||
export class V1ClientUserDto extends OmitType(User, [
|
||||
"accessToken",
|
||||
"password",
|
||||
"salt",
|
||||
"fcm",
|
||||
"version",
|
||||
]) {
|
||||
static fromUser(userDto: User): V1ClientUserDto {
|
||||
return plainToInstance(V1ClientUserDto, {
|
||||
id: userDto.id,
|
||||
username: userDto.username,
|
||||
group: userDto.group,
|
||||
role: userDto.role,
|
||||
} as V1ClientUserDto);
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { OmitType } from "@nestjs/swagger";
|
||||
import { User } from "../../entity/user.entity";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
|
||||
export class V2ClientUserDto extends OmitType(User, [
|
||||
"password",
|
||||
"salt",
|
||||
"fcm",
|
||||
"version",
|
||||
]) {
|
||||
static fromUser(userDto: User): V2ClientUserDto {
|
||||
return plainToInstance(V2ClientUserDto, {
|
||||
id: userDto.id,
|
||||
username: userDto.username,
|
||||
accessToken: userDto.accessToken,
|
||||
group: userDto.group,
|
||||
role: userDto.role,
|
||||
} as V2ClientUserDto);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { IsArray, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
export class FcmUser {
|
||||
// noinspection JSClassNamingConvention
|
||||
export default class FCM {
|
||||
/**
|
||||
* Токен Firebase Cloud Messaging
|
||||
* @example "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXCJ9..."
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
IsEnum,
|
||||
IsJWT,
|
||||
IsMongoId,
|
||||
IsNumber,
|
||||
IsObject,
|
||||
IsOptional,
|
||||
IsSemVer,
|
||||
@@ -9,12 +10,13 @@ import {
|
||||
MaxLength,
|
||||
MinLength,
|
||||
} from "class-validator";
|
||||
import { Type } from "class-transformer";
|
||||
import { UserRole } from "../user-role.enum";
|
||||
import { plainToInstance, Type } from "class-transformer";
|
||||
import UserRole from "../user-role.enum";
|
||||
|
||||
import { FcmUser } from "./fcm-user.entity";
|
||||
import FCM from "./fcm-user.entity";
|
||||
import UserDto from "../dto/user.dto";
|
||||
|
||||
export class User {
|
||||
export default class User {
|
||||
/**
|
||||
* Идентификатор (ObjectId)
|
||||
* @example "66e1b7e255c5d5f1268cce90"
|
||||
@@ -70,9 +72,9 @@ export class User {
|
||||
* Данные Firebase Cloud Messaging
|
||||
*/
|
||||
@IsObject()
|
||||
@Type(() => FcmUser)
|
||||
@Type(() => FCM)
|
||||
@IsOptional()
|
||||
fcm?: FcmUser;
|
||||
fcm?: FCM;
|
||||
|
||||
/**
|
||||
* Версия установленного приложения
|
||||
@@ -80,4 +82,19 @@ export class User {
|
||||
*/
|
||||
@IsSemVer()
|
||||
version: string;
|
||||
|
||||
/**
|
||||
* Идентификатор аккаунта VK
|
||||
* @example "2.0.0"
|
||||
*/
|
||||
@IsNumber()
|
||||
vkId?: number;
|
||||
|
||||
static fromPlain(plain: object): User {
|
||||
return plainToInstance(User, plain);
|
||||
}
|
||||
|
||||
toDto(groups: Array<string> = []): UserDto {
|
||||
return plainToInstance(UserDto, this, { groups: groups });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export enum UserRole {
|
||||
enum UserRole {
|
||||
STUDENT = "STUDENT",
|
||||
TEACHER = "TEACHER",
|
||||
ADMIN = "ADMIN",
|
||||
}
|
||||
|
||||
export default UserRole;
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import {
|
||||
Body,
|
||||
ConflictException,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
NotFoundException,
|
||||
Post,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { AuthGuard } from "../auth/auth.guard";
|
||||
import { ResultDto } from "../utility/validation/class-validator.interceptor";
|
||||
import { UserToken } from "../auth/auth.decorator";
|
||||
import { AuthService } from "../auth/auth.service";
|
||||
import { UsersService } from "./users.service";
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
@@ -19,34 +20,34 @@ import {
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
} from "@nestjs/swagger";
|
||||
import { ChangeUsernameDto } from "./dto/change-username.dto";
|
||||
import { ChangeGroupDto } from "./dto/change-group.dto";
|
||||
import { V1ClientUserDto } from "./dto/v1/v1-client-user.dto";
|
||||
import { UserRole } from "./user-role.enum";
|
||||
import User from "./entity/user.entity";
|
||||
import ChangeUsernameDto from "./dto/change-username.dto";
|
||||
import UserRole from "./user-role.enum";
|
||||
import ChangeGroupDto from "./dto/change-group.dto";
|
||||
import { UserPipe } from "../auth/auth.pipe";
|
||||
import { ScheduleService } from "../schedule/schedule.service";
|
||||
import UserDto from "./dto/user.dto";
|
||||
|
||||
@ApiTags("v1/users")
|
||||
@ApiBearerAuth()
|
||||
@Controller({ path: "users", version: "1" })
|
||||
@UseGuards(AuthGuard)
|
||||
export class V1UsersController {
|
||||
export class UsersController {
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly usersService: UsersService,
|
||||
private readonly scheduleService: ScheduleService,
|
||||
) {}
|
||||
|
||||
@ApiOperation({ summary: "Получение данных о профиле пользователя" })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Получение профиля прошло успешно",
|
||||
type: V1ClientUserDto,
|
||||
type: UserDto,
|
||||
})
|
||||
@ResultDto(V1ClientUserDto)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ResultDto(UserDto)
|
||||
@Get("me")
|
||||
async getMe(@UserToken() token: string): Promise<V1ClientUserDto> {
|
||||
return V1ClientUserDto.fromUser(
|
||||
await this.authService.decodeUserToken(token),
|
||||
);
|
||||
getMe(@UserToken(UserPipe) user: User): UserDto {
|
||||
return user.toDto();
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Смена имени пользователя" })
|
||||
@@ -63,17 +64,30 @@ export class V1UsersController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("change-username")
|
||||
async changeUsername(
|
||||
@Body() reqDto: ChangeUsernameDto,
|
||||
@UserToken() token: string,
|
||||
@Body() changeUsernameDto: ChangeUsernameDto,
|
||||
@UserToken(UserPipe) user: User,
|
||||
): Promise<void> {
|
||||
const user = await this.authService.decodeUserToken(token);
|
||||
|
||||
reqDto.username =
|
||||
changeUsernameDto.username =
|
||||
user.role == UserRole.ADMIN
|
||||
? reqDto.username
|
||||
: reqDto.username.replace(/\s/g, "");
|
||||
? changeUsernameDto.username
|
||||
: changeUsernameDto.username.replace(/\s/g, "");
|
||||
|
||||
return await this.usersService.changeUsername(user, reqDto);
|
||||
if (user.username === changeUsernameDto.username) return;
|
||||
|
||||
if (
|
||||
await this.usersService.contains({
|
||||
username: changeUsernameDto.username,
|
||||
})
|
||||
) {
|
||||
throw new ConflictException(
|
||||
"Пользователь с таким именем уже существует",
|
||||
);
|
||||
}
|
||||
|
||||
await this.usersService.update({
|
||||
where: { id: user.id },
|
||||
data: { username: changeUsernameDto.username },
|
||||
});
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: "Смена группы пользователя" })
|
||||
@@ -90,11 +104,21 @@ export class V1UsersController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Post("change-group")
|
||||
async changeGroup(
|
||||
@Body() reqDto: ChangeGroupDto,
|
||||
@UserToken() token: string,
|
||||
@Body() changeGroupDto: ChangeGroupDto,
|
||||
@UserToken(UserPipe) user: User,
|
||||
): Promise<void> {
|
||||
const user = await this.authService.decodeUserToken(token);
|
||||
if (user.group === changeGroupDto.group) return;
|
||||
|
||||
return await this.usersService.changeGroup(user, reqDto);
|
||||
const groupNames = await this.scheduleService.getGroupNames();
|
||||
if (!groupNames.names.includes(changeGroupDto.group)) {
|
||||
throw new NotFoundException(
|
||||
"Группа с таким названием не существует",
|
||||
);
|
||||
}
|
||||
|
||||
await this.usersService.update({
|
||||
where: { id: user.id },
|
||||
data: { group: changeGroupDto.group },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
import { forwardRef, Module } from "@nestjs/common";
|
||||
import { UsersService } from "./users.service";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { V2UsersController } from "./v2-users.controller";
|
||||
import { ScheduleModule } from "../schedule/schedule.module";
|
||||
import { AuthModule } from "../auth/auth.module";
|
||||
import { V1UsersController } from "./v1-users.controller";
|
||||
import { UsersController } from "./users.controller";
|
||||
|
||||
@Module({
|
||||
imports: [forwardRef(() => ScheduleModule), forwardRef(() => AuthModule)],
|
||||
providers: [PrismaService, UsersService],
|
||||
exports: [UsersService],
|
||||
controllers: [V1UsersController, V2UsersController],
|
||||
controllers: [UsersController],
|
||||
})
|
||||
export class UsersModule {}
|
||||
|
||||
@@ -1,90 +1,38 @@
|
||||
import {
|
||||
ConflictException,
|
||||
forwardRef,
|
||||
Inject,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { PrismaService } from "../prisma/prisma.service";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { User } from "./entity/user.entity";
|
||||
import { ChangeUsernameDto } from "./dto/change-username.dto";
|
||||
import { ChangeGroupDto } from "./dto/change-group.dto";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
import { ScheduleService } from "../schedule/schedule.service";
|
||||
import User from "./entity/user.entity";
|
||||
|
||||
@Injectable()
|
||||
export class UsersService {
|
||||
constructor(
|
||||
private readonly prismaService: PrismaService,
|
||||
@Inject(forwardRef(() => ScheduleService))
|
||||
private readonly scheduleService: ScheduleService,
|
||||
) {}
|
||||
constructor(private readonly prismaService: PrismaService) {}
|
||||
|
||||
async findUnique(where: Prisma.UserWhereUniqueInput): Promise<User | null> {
|
||||
return plainToInstance(
|
||||
User,
|
||||
async findUnique(where: Prisma.UserWhereUniqueInput): Promise<User> {
|
||||
return User.fromPlain(
|
||||
await this.prismaService.user.findUnique({ where: where }),
|
||||
);
|
||||
}
|
||||
|
||||
async findOne(where: Prisma.UserWhereInput): Promise<User> {
|
||||
return User.fromPlain(
|
||||
await this.prismaService.user.findFirst({ where: where }),
|
||||
);
|
||||
}
|
||||
|
||||
async update(params: {
|
||||
where: Prisma.UserWhereUniqueInput;
|
||||
data: Prisma.UserUpdateInput;
|
||||
}): Promise<User> {
|
||||
return plainToInstance(
|
||||
User,
|
||||
await this.prismaService.user.update(params),
|
||||
);
|
||||
return User.fromPlain(await this.prismaService.user.update(params));
|
||||
}
|
||||
|
||||
async create(data: Prisma.UserCreateInput): Promise<User> {
|
||||
return plainToInstance(
|
||||
User,
|
||||
await this.prismaService.user.create({ data }),
|
||||
);
|
||||
return User.fromPlain(await this.prismaService.user.create({ data }));
|
||||
}
|
||||
|
||||
async contains(where: Prisma.UserWhereUniqueInput): Promise<boolean> {
|
||||
async contains(where: Prisma.UserWhereInput): Promise<boolean> {
|
||||
return await this.prismaService.user
|
||||
.count({ where })
|
||||
.then((count) => count > 0);
|
||||
}
|
||||
|
||||
async changeUsername(
|
||||
user: User,
|
||||
changeUsernameDto: ChangeUsernameDto,
|
||||
): Promise<void> {
|
||||
if (user.username === changeUsernameDto.username) return;
|
||||
|
||||
if (await this.contains({ username: changeUsernameDto.username })) {
|
||||
throw new ConflictException(
|
||||
"Пользователь с таким именем уже существует",
|
||||
);
|
||||
}
|
||||
|
||||
await this.update({
|
||||
where: { id: user.id },
|
||||
data: { username: changeUsernameDto.username },
|
||||
});
|
||||
}
|
||||
|
||||
async changeGroup(
|
||||
user: User,
|
||||
changeGroupDto: ChangeGroupDto,
|
||||
): Promise<void> {
|
||||
if (user.group === changeGroupDto.group) return;
|
||||
|
||||
const groupNames = await this.scheduleService.getGroupNames();
|
||||
if (!groupNames.names.includes(changeGroupDto.group)) {
|
||||
throw new NotFoundException(
|
||||
"Группа с таким названием не существует",
|
||||
);
|
||||
}
|
||||
|
||||
await this.update({
|
||||
where: { id: user.id },
|
||||
data: { group: changeGroupDto.group },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { AuthGuard } from "../auth/auth.guard";
|
||||
import { ResultDto } from "../utility/validation/class-validator.interceptor";
|
||||
import { UserToken } from "../auth/auth.decorator";
|
||||
import { AuthService } from "../auth/auth.service";
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiTags,
|
||||
} from "@nestjs/swagger";
|
||||
import { V2ClientUserDto } from "./dto/v2/v2-client-user.dto";
|
||||
|
||||
@ApiTags("v2/users")
|
||||
@ApiBearerAuth()
|
||||
@Controller({ path: "users", version: "2" })
|
||||
@UseGuards(AuthGuard)
|
||||
export class V2UsersController {
|
||||
constructor(private readonly authService: AuthService) {}
|
||||
|
||||
@ApiOperation({ summary: "Получение данных о профиле пользователя" })
|
||||
@ApiResponse({
|
||||
status: HttpStatus.OK,
|
||||
description: "Получение профиля прошло успешно",
|
||||
type: V2ClientUserDto,
|
||||
})
|
||||
@ResultDto(V2ClientUserDto)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Get("me")
|
||||
async getMe(@UserToken() token: string): Promise<V2ClientUserDto> {
|
||||
return V2ClientUserDto.fromUser(
|
||||
await this.authService.decodeUserToken(token),
|
||||
);
|
||||
}
|
||||
}
|
||||
33
src/utility/class-trasformer/class-transformer-ctor.ts
Normal file
33
src/utility/class-trasformer/class-transformer-ctor.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import "reflect-metadata";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
|
||||
export type ClassProperties<T> = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
[K in keyof T]: T[K] extends Function ? never : T[K];
|
||||
};
|
||||
|
||||
export class Ctor<T> {
|
||||
constructor(object: ClassProperties<T>) {
|
||||
if (object != undefined)
|
||||
throw new Error(
|
||||
"You should use ClassTransformerCtor decorator on class to use this feature!",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// noinspection FunctionNamingConventionJS
|
||||
export function ClassTransformerCtor() {
|
||||
return function (target: any) {
|
||||
const newCtor: any = function (object: object) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment
|
||||
const obj: any = new target();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
Object.assign(obj, plainToInstance(target, object));
|
||||
|
||||
return obj;
|
||||
};
|
||||
|
||||
return newCtor;
|
||||
};
|
||||
}
|
||||
@@ -18,17 +18,12 @@ export function NullIf(
|
||||
constraints: [canBeNull],
|
||||
validator: {
|
||||
validate(value: any, args: ValidationArguments) {
|
||||
const canBeNullFunc: (cls: object) => boolean =
|
||||
args.constraints[0];
|
||||
|
||||
const canBeNull = canBeNullFunc(args.object);
|
||||
const currentValue = value;
|
||||
const canBeNull = (
|
||||
args.constraints[0] as (cls: object) => boolean
|
||||
)(args.object);
|
||||
|
||||
// Логика валидации: если одно из полей null, то другое тоже должно быть null
|
||||
|
||||
return canBeNull
|
||||
? currentValue !== null
|
||||
: currentValue === null;
|
||||
return canBeNull ? value !== null : value === null;
|
||||
},
|
||||
defaultMessage(args: ValidationArguments) {
|
||||
return `${args.property} must be ${args.property === null ? "non-null" : "null"}!`;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable */
|
||||
import "reflect-metadata";
|
||||
|
||||
import {
|
||||
@@ -10,7 +11,7 @@ import {
|
||||
UnprocessableEntityException,
|
||||
} from "@nestjs/common";
|
||||
import { map, Observable } from "rxjs";
|
||||
import { instanceToPlain, plainToInstance } from "class-transformer";
|
||||
import { instanceToPlain } from "class-transformer";
|
||||
import { validate, ValidationOptions } from "class-validator";
|
||||
|
||||
@Injectable()
|
||||
@@ -32,13 +33,19 @@ export class ClassValidatorInterceptor implements NestInterceptor {
|
||||
handler.name,
|
||||
);
|
||||
|
||||
const providedGroups: Array<string> = Reflect.getMetadata(
|
||||
"design:result-dto-groups",
|
||||
cls.prototype,
|
||||
handler.name,
|
||||
);
|
||||
|
||||
const isArrayOfDto = Reflect.getMetadata(
|
||||
"design:result-dto-array",
|
||||
cls.prototype,
|
||||
handler.name,
|
||||
);
|
||||
|
||||
if (classDto === null) return returnValue;
|
||||
if (classDto === null) return instanceToPlain(returnValue);
|
||||
|
||||
if (classDto === undefined) {
|
||||
console.warn(
|
||||
@@ -47,26 +54,26 @@ export class ClassValidatorInterceptor implements NestInterceptor {
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
const groups = [
|
||||
...providedGroups,
|
||||
...(this.validatorOptions.groups ?? []),
|
||||
];
|
||||
|
||||
const dtoArray: Array<any> = isArrayOfDto
|
||||
? classDto
|
||||
: [classDto];
|
||||
|
||||
for (let idx = 0; idx < dtoArray.length; idx++) {
|
||||
const returnValueDto = plainToInstance(
|
||||
dtoArray[idx],
|
||||
instanceToPlain(returnValue),
|
||||
);
|
||||
|
||||
if (!(returnValueDto instanceof Object))
|
||||
if (!(returnValue instanceof Object))
|
||||
throw new InternalServerErrorException(
|
||||
returnValueDto,
|
||||
returnValue,
|
||||
"Return value is not object!",
|
||||
);
|
||||
|
||||
const validationErrors = await validate(
|
||||
returnValueDto,
|
||||
this.validatorOptions,
|
||||
);
|
||||
const validationErrors = await validate(returnValue, {
|
||||
...this.validatorOptions,
|
||||
groups: groups,
|
||||
});
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
if (idx !== dtoArray.length - 1) continue;
|
||||
@@ -82,6 +89,7 @@ export class ClassValidatorInterceptor implements NestInterceptor {
|
||||
statusCode: HttpStatus.UNPROCESSABLE_ENTITY,
|
||||
});
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
}),
|
||||
@@ -90,7 +98,7 @@ export class ClassValidatorInterceptor implements NestInterceptor {
|
||||
}
|
||||
|
||||
// noinspection FunctionNamingConventionJS
|
||||
export function ResultDto(dtoType: any) {
|
||||
export function ResultDto(dtoType: any, groups: Array<string> = []) {
|
||||
return (target: NonNullable<unknown>, propertyKey: string | symbol) => {
|
||||
Reflect.defineMetadata(
|
||||
"design:result-dto",
|
||||
@@ -104,5 +112,11 @@ export function ResultDto(dtoType: any) {
|
||||
target,
|
||||
propertyKey,
|
||||
);
|
||||
Reflect.defineMetadata(
|
||||
"design:result-dto-groups",
|
||||
groups,
|
||||
target,
|
||||
propertyKey,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { createParamDecorator, ExecutionContext } from "@nestjs/common";
|
||||
|
||||
export const ResponseVersion = createParamDecorator(
|
||||
(_, context: ExecutionContext) => {
|
||||
const sourceVersion: string | null = context.switchToHttp().getRequest()
|
||||
.headers.version;
|
||||
const parsedVersion = Number.parseInt(sourceVersion);
|
||||
|
||||
if (Number.isNaN(parsedVersion) || parsedVersion < 0) return 0;
|
||||
|
||||
return parsedVersion;
|
||||
},
|
||||
);
|
||||
1215
test/mainPage
1215
test/mainPage
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"builder": "swc",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
|
||||
Reference in New Issue
Block a user