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