mirror of
https://github.com/n08i40k/schedule-parser-next.git
synced 2025-12-06 09:47:46 +03:00
3.0.0.
- Updated package version to 3.0.0 - Improved FCM topic handling logic - Enhanced schedule parser accuracy - Removed HTTPS options for dev simplicity - Added detailed API documentation - Removed support for older api versions
This commit is contained in:
@@ -25,12 +25,12 @@ export class AuthService {
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Получение пользователя по его токену
|
||||
* @param token - jwt токен
|
||||
* @returns {User} - пользователь
|
||||
* @throws {UnauthorizedException} - некорректный или недействительный токен
|
||||
* @throws {UnauthorizedException} - токен указывает на несуществующего пользователя
|
||||
* @throws {UnauthorizedException} - текущий токен устарел и был обновлён на новый
|
||||
* Получает пользователя по его JWT токену
|
||||
* @param {string} token - JWT токен для аутентификации
|
||||
* @returns {Promise<User>} - Объект пользователя, если токен валиден
|
||||
* @throws {UnauthorizedException} - Если токен некорректен или недействителен
|
||||
* @throws {UnauthorizedException} - Если пользователь, указанный в токене, не существует
|
||||
* @throws {UnauthorizedException} - Если токен устарел и был заменён на новый
|
||||
* @async
|
||||
*/
|
||||
async decodeUserToken(token: string): Promise<User> {
|
||||
@@ -52,6 +52,13 @@ export class AuthService {
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Регистрирует нового пользователя в системе.
|
||||
*
|
||||
* @param signUpDto - Объект с данными для регистрации, включая имя пользователя, пароль, роль и группу.
|
||||
* @returns Возвращает объект UserDto с данными зарегистрированного пользователя или объект SignUpErrorDto в случае ошибки.
|
||||
* @throws SignUpErrorDto - Если роль пользователя недопустима или имя пользователя уже существует.
|
||||
*/
|
||||
async signUp(signUpDto: SignUpDto): Promise<UserDto | SignUpErrorDto> {
|
||||
if (![UserRole.STUDENT, UserRole.TEACHER].includes(signUpDto.role))
|
||||
return new SignUpErrorDto(SignUpErrorCode.DISALLOWED_ROLE);
|
||||
@@ -75,6 +82,23 @@ export class AuthService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Асинхронная функция для входа пользователя в систему.
|
||||
*
|
||||
* @param {SignInDto} signIn - Объект, содержащий данные для входа (имя пользователя и пароль).
|
||||
* @returns {Promise<UserDto | SignInErrorDto>} - Возвращает объект UserDto в случае успешного входа или SignInErrorDto в случае ошибки.
|
||||
*
|
||||
* @throws {SignInErrorDto} - Если пользователь не найден или пароль неверный, возвращается объект SignInErrorDto с кодом ошибки INCORRECT_CREDENTIALS.
|
||||
*
|
||||
* @example
|
||||
* const signInData = { username: 'user123', password: 'password123' };
|
||||
* const result = await signIn(signInData);
|
||||
* if (result instanceof UserDto) {
|
||||
* console.log('Вход выполнен успешно:', result);
|
||||
* } else {
|
||||
* console.log('Ошибка входа:', result);
|
||||
* }
|
||||
*/
|
||||
async signIn(signIn: SignInDto): Promise<UserDto | SignInErrorDto> {
|
||||
const user = await this.usersService.findUnique({
|
||||
username: signIn.username,
|
||||
@@ -96,6 +120,20 @@ export class AuthService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Парсит VK ID пользователя по access token
|
||||
*
|
||||
* @param accessToken - Access token пользователя VK
|
||||
* @returns Promise, который разрешается в VK ID пользователя или null в случае ошибки
|
||||
*
|
||||
* @example
|
||||
* const vkId = await parseVKID('access_token_here');
|
||||
* if (vkId) {
|
||||
* console.log(`VK ID пользователя: ${vkId}`);
|
||||
* } else {
|
||||
* console.error('Ошибка при получении VK ID');
|
||||
* }
|
||||
*/
|
||||
private static async parseVKID(accessToken: string): Promise<number> {
|
||||
const form = new FormData();
|
||||
form.append("access_token", accessToken);
|
||||
@@ -115,6 +153,12 @@ export class AuthService {
|
||||
return data.response.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Регистрация пользователя через VK
|
||||
* @param signUpDto - DTO с данными для регистрации через VK
|
||||
* @returns Promise<UserDto | SignUpErrorDto> - возвращает DTO пользователя в случае успешной регистрации
|
||||
* или DTO ошибки в случае возникновения проблем
|
||||
*/
|
||||
async signUpVK(signUpDto: SignUpVKDto): Promise<UserDto | SignUpErrorDto> {
|
||||
if (![UserRole.STUDENT, UserRole.TEACHER].includes(signUpDto.role))
|
||||
return new SignUpErrorDto(SignUpErrorCode.DISALLOWED_ROLE);
|
||||
@@ -148,6 +192,11 @@ export class AuthService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Авторизация пользователя через VK
|
||||
* @param signInVKDto - DTO с данными для авторизации через VK
|
||||
* @returns Promise<UserDto | SignInErrorDto> - возвращает DTO пользователя в случае успешной авторизации или DTO ошибки в случае неудачи
|
||||
*/
|
||||
async signInVK(
|
||||
signInVKDto: SignInVKDto,
|
||||
): Promise<UserDto | SignInErrorDto> {
|
||||
@@ -172,11 +221,12 @@ export class AuthService {
|
||||
|
||||
/**
|
||||
* Смена пароля пользователя
|
||||
* @param user - пользователь
|
||||
* @param changePassword - старый и новый пароли
|
||||
* @throws {ConflictException} - пароли идентичны
|
||||
* @throws {UnauthorizedException} - неверный исходный пароль
|
||||
* @param user - пользователь, для которого меняется пароль
|
||||
* @param changePassword - объект, содержащий старый и новый пароли
|
||||
* @throws {ConflictException} - выбрасывается, если старый и новый пароли идентичны
|
||||
* @throws {UnauthorizedException} - выбрасывается, если передан неверный исходный пароль
|
||||
* @async
|
||||
* @returns {Promise<void>} - возвращает Promise, который разрешается, когда пароль успешно изменен
|
||||
*/
|
||||
async changePassword(
|
||||
user: User,
|
||||
@@ -187,13 +237,13 @@ export class AuthService {
|
||||
if (oldPassword == newPassword)
|
||||
throw new ConflictException("Пароли идентичны");
|
||||
|
||||
if (user.password !== (await hash(oldPassword, user.salt)))
|
||||
if (!(await compare(oldPassword, user.password)))
|
||||
throw new UnauthorizedException("Передан неверный исходный пароль");
|
||||
|
||||
await this.usersService.update({
|
||||
where: { id: user.id },
|
||||
data: {
|
||||
password: await hash(newPassword, user.salt),
|
||||
password: await hash(newPassword, await genSalt(8)),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
Controller,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Param,
|
||||
Post,
|
||||
Param, Patch,
|
||||
Post, Query,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { AuthGuard } from "../auth/auth.guard";
|
||||
@@ -54,11 +54,11 @@ export class FirebaseAdminController {
|
||||
status: HttpStatus.OK,
|
||||
description: "Установка токена удалась",
|
||||
})
|
||||
@Post("set-token/:token")
|
||||
@Patch("set-token")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ResultDto(null)
|
||||
async setToken(
|
||||
@Param("token") token: string,
|
||||
@Query("token") token: string,
|
||||
@UserToken(UserPipe) user: User,
|
||||
): Promise<void> {
|
||||
if (user.fcm?.token === token) return;
|
||||
|
||||
@@ -86,17 +86,17 @@ export class FirebaseAdminService implements OnModuleInit {
|
||||
if (!user.fcm) throw new Error("User does not have fcm data!");
|
||||
|
||||
const fcm = user.fcm;
|
||||
const newTopics = new Set<string>();
|
||||
const userTopics = new Set<string>([...fcm.topics]);
|
||||
|
||||
for (const topic of topics) {
|
||||
if (!fcm.topics.includes(topic)) continue;
|
||||
|
||||
await this.messaging.unsubscribeFromTopic(fcm.token, topic);
|
||||
newTopics.add(topic);
|
||||
userTopics.delete(topic);
|
||||
}
|
||||
if (newTopics.size === fcm.topics.length) return user;
|
||||
if (userTopics.size === fcm.topics.length) return user;
|
||||
|
||||
fcm.topics = Array.from(newTopics);
|
||||
fcm.topics = Array.from(userTopics);
|
||||
|
||||
return await this.usersService.update({
|
||||
where: { id: user.id },
|
||||
@@ -118,10 +118,12 @@ export class FirebaseAdminService implements OnModuleInit {
|
||||
if (force)
|
||||
await this.messaging.unsubscribeFromTopic(fcm.token, topic);
|
||||
else if (fcm.topics.includes(topic)) continue;
|
||||
else newTopics.add(topic);
|
||||
|
||||
newTopics.add(topic);
|
||||
|
||||
await this.messaging.subscribeToTopic(fcm.token, topic);
|
||||
}
|
||||
|
||||
if (newTopics.size === fcm.topics.length) return user;
|
||||
|
||||
fcm.topics = Array.from(newTopics);
|
||||
|
||||
25
src/main.ts
25
src/main.ts
@@ -3,8 +3,7 @@ import { AppModule } from "./app.module";
|
||||
import { ValidatorOptions } from "class-validator";
|
||||
import { PartialValidationPipe } from "./utility/validation/partial-validation.pipe";
|
||||
import { ClassValidatorInterceptor } from "./utility/validation/class-validator.interceptor";
|
||||
import { apiConstants, httpsConstants } from "./contants";
|
||||
import * as fs from "node:fs";
|
||||
import { apiConstants } from "./contants";
|
||||
import { VersioningType } from "@nestjs/common";
|
||||
import {
|
||||
FastifyAdapter,
|
||||
@@ -16,12 +15,6 @@ async function bootstrap() {
|
||||
const app = await NestFactory.create<NestFastifyApplication>(
|
||||
AppModule,
|
||||
new FastifyAdapter(),
|
||||
{
|
||||
httpsOptions: {
|
||||
cert: fs.readFileSync(httpsConstants.certPath),
|
||||
key: fs.readFileSync(httpsConstants.keyPath),
|
||||
},
|
||||
},
|
||||
);
|
||||
const validatorOptions: ValidatorOptions = {
|
||||
enableDebugMessages: true,
|
||||
@@ -33,13 +26,13 @@ async function bootstrap() {
|
||||
app.enableCors();
|
||||
|
||||
app.setGlobalPrefix("api");
|
||||
app.enableVersioning({
|
||||
type: VersioningType.URI,
|
||||
});
|
||||
app.enableVersioning({ type: VersioningType.URI });
|
||||
|
||||
const swaggerConfig = new DocumentBuilder()
|
||||
.setTitle("Schedule Parser")
|
||||
.setDescription("Парсер расписания")
|
||||
.setDescription(
|
||||
"API для парсинга и управления расписанием учебных занятий",
|
||||
)
|
||||
.setVersion(apiConstants.version)
|
||||
.build();
|
||||
|
||||
@@ -49,8 +42,12 @@ async function bootstrap() {
|
||||
|
||||
swaggerDocument.servers = [
|
||||
{
|
||||
url: `https://localhost:${apiConstants.port}`,
|
||||
description: "Локальный сервер для разработки",
|
||||
url: "https://polytechnic-dev.n08i40k.ru",
|
||||
description: "Сервер для разработки и тестирования",
|
||||
},
|
||||
{
|
||||
url: "https://polytechnic.n08i40k.ru",
|
||||
description: "Сервер для продакшн окружения",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -16,14 +16,14 @@ export default class CacheStatusDto {
|
||||
cacheUpdateRequired: boolean;
|
||||
|
||||
/**
|
||||
* Дата обновления кеша
|
||||
* Время последнего обновления кеша в формате timestamp
|
||||
* @example 1729288173002
|
||||
*/
|
||||
@IsNumber()
|
||||
lastCacheUpdate: number;
|
||||
|
||||
/**
|
||||
* Дата обновления расписания
|
||||
* Время последнего обновления расписания в формате timestamp
|
||||
* @example 1729288173002
|
||||
*/
|
||||
@IsNumber()
|
||||
|
||||
@@ -69,13 +69,14 @@ describe("ScheduleParser", () => {
|
||||
);
|
||||
expect(schedule).toBeDefined();
|
||||
|
||||
const group: Group | undefined = schedule.groups.get("ИС-214/23");
|
||||
const group: Group | undefined = schedule.groups.get("ИС-114/23");
|
||||
expect(group).toBeDefined();
|
||||
|
||||
const day = group.days[0];
|
||||
expect(day).toBeDefined();
|
||||
|
||||
expect(day.lessons.length).toBeGreaterThan(0);
|
||||
expect(day.lessons[0].name).toBe("Линейка");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { XlsDownloaderInterface } from "../xls-downloader/xls-downloader.interfa
|
||||
import * as XLSX from "xlsx";
|
||||
import { Range, WorkSheet } from "xlsx";
|
||||
import { toNormalString, trimAll } from "../../../utility/string.util";
|
||||
import { plainToClass, plainToInstance, Type } from "class-transformer";
|
||||
import { plainToInstance, Type } from "class-transformer";
|
||||
import * as objectHash from "object-hash";
|
||||
import LessonTime from "../../entities/lesson-time.entity";
|
||||
import { LessonType } from "../../enum/lesson-type.enum";
|
||||
@@ -607,7 +607,7 @@ export class ScheduleParser {
|
||||
}
|
||||
|
||||
private static readonly consultationRegExp = /\(?[кК]онсультация\)?/;
|
||||
private static readonly otherStreetRegExp = /^[А-Я][а-я]+,\s?[0-9]+$/;
|
||||
private static readonly otherStreetRegExp = /^[А-Я][а-я]+,?\s?[0-9]+$/;
|
||||
|
||||
private static parseLesson(
|
||||
workSheet: XLSX.Sheet,
|
||||
@@ -689,17 +689,25 @@ export class ScheduleParser {
|
||||
column + 1,
|
||||
);
|
||||
|
||||
// Если количество кабинетов равно 1, назначаем этот кабинет всем подгруппам
|
||||
if (cabinets.length === 1) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-for-in-array
|
||||
for (const index in lessonData.subGroups)
|
||||
lessonData.subGroups[index].cabinet = cabinets[0];
|
||||
} else if (cabinets.length === lessonData.subGroups.length) {
|
||||
lessonData.subGroups[index].cabinet = cabinets[0] ?? "";
|
||||
}
|
||||
// Если количество кабинетов совпадает с количеством подгрупп, назначаем кабинеты по порядку
|
||||
else if (cabinets.length === lessonData.subGroups.length) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-for-in-array
|
||||
for (const index in lessonData.subGroups) {
|
||||
lessonData.subGroups[index].cabinet =
|
||||
cabinets[lessonData.subGroups[index].number - 1];
|
||||
cabinets[lessonData.subGroups[index].number - 1] ??
|
||||
cabinets[0] ??
|
||||
"";
|
||||
}
|
||||
} else if (cabinets.length !== 0) {
|
||||
}
|
||||
// Если количество кабинетов не равно нулю, но не совпадает с количеством подгрупп
|
||||
else if (cabinets.length !== 0) {
|
||||
// Если кабинетов больше, чем подгрупп, добавляем новые подгруппы с ошибкой
|
||||
if (cabinets.length > lessonData.subGroups.length) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-for-in-array
|
||||
for (const index in cabinets) {
|
||||
@@ -717,7 +725,14 @@ export class ScheduleParser {
|
||||
|
||||
lessonData.subGroups[index].cabinet = cabinets[index];
|
||||
}
|
||||
} else throw new Error("Разное кол-во кабинетов и подгрупп!");
|
||||
}
|
||||
// Если кабинетов меньше, чем подгрупп, выбрасываем ошибку
|
||||
else throw new Error("Разное кол-во кабинетов и подгрупп!");
|
||||
}
|
||||
// Если кабинетов нет, но есть подгруппы, назначаем им значение "??"
|
||||
else if (lessonData.subGroups.length !== 0) {
|
||||
for (const subGroup of lessonData.subGroups)
|
||||
subGroup.cabinet = "??";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@ import TeacherSchedule from "./entities/teacher-schedule.entity";
|
||||
import GetGroupNamesDto from "./dto/get-group-names.dto";
|
||||
import TeacherNamesDto from "./dto/teacher-names.dto";
|
||||
|
||||
/**
|
||||
* Сервис для работы с расписанием
|
||||
*/
|
||||
@Injectable()
|
||||
export class ScheduleService {
|
||||
readonly scheduleParser: ScheduleParser;
|
||||
@@ -26,6 +29,12 @@ export class ScheduleService {
|
||||
|
||||
private scheduleUpdatedAt: Date = new Date(0);
|
||||
|
||||
/**
|
||||
* Конструктор сервиса
|
||||
* @param cacheManager Менеджер кэша
|
||||
* @param scheduleReplacerService Сервис замены расписания
|
||||
* @param firebaseAdminService Сервис работы с Firebase
|
||||
*/
|
||||
constructor(
|
||||
@Inject(CACHE_MANAGER) private readonly cacheManager: Cache,
|
||||
private readonly scheduleReplacerService: ScheduleReplacerService,
|
||||
@@ -54,6 +63,10 @@ export class ScheduleService {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение статуса кэша
|
||||
* @returns Объект с информацией о состоянии кэша
|
||||
*/
|
||||
getCacheStatus(): CacheStatusDto {
|
||||
return plainToInstance(CacheStatusDto, {
|
||||
cacheHash: this.cacheHash,
|
||||
@@ -65,6 +78,10 @@ export class ScheduleService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение исходного расписания
|
||||
* @returns Результат парсинга расписания
|
||||
*/
|
||||
async getSourceSchedule(): Promise<ScheduleParseResult> {
|
||||
const schedule = await this.scheduleParser.getSchedule();
|
||||
|
||||
@@ -93,6 +110,10 @@ export class ScheduleService {
|
||||
return schedule;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение расписания
|
||||
* @returns Объект расписания
|
||||
*/
|
||||
async getSchedule(): Promise<Schedule> {
|
||||
const sourceSchedule = await this.getSourceSchedule();
|
||||
|
||||
@@ -103,6 +124,12 @@ export class ScheduleService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение расписания для группы
|
||||
* @param name Название группы
|
||||
* @returns Расписание группы
|
||||
* @throws NotFoundException Если группа не найдена
|
||||
*/
|
||||
async getGroup(name: string): Promise<GroupSchedule> {
|
||||
const schedule = await this.getSourceSchedule();
|
||||
|
||||
@@ -120,6 +147,10 @@ export class ScheduleService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение списка названий групп
|
||||
* @returns Объект с массивом названий групп
|
||||
*/
|
||||
async getGroupNames(): Promise<GetGroupNamesDto> {
|
||||
const schedule = await this.getSourceSchedule();
|
||||
const names: Array<string> = [];
|
||||
@@ -131,6 +162,12 @@ export class ScheduleService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение расписания для преподавателя
|
||||
* @param name ФИО преподавателя
|
||||
* @returns Расписание преподавателя
|
||||
* @throws NotFoundException Если преподаватель не найден
|
||||
*/
|
||||
async getTeacher(name: string): Promise<TeacherSchedule> {
|
||||
const schedule = await this.getSourceSchedule();
|
||||
|
||||
@@ -148,6 +185,10 @@ export class ScheduleService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение списка ФИО преподавателей
|
||||
* @returns Объект с массивом ФИО преподавателей
|
||||
*/
|
||||
async getTeacherNames(): Promise<TeacherNamesDto> {
|
||||
const schedule = await this.getSourceSchedule();
|
||||
const names: Array<string> = [];
|
||||
@@ -162,6 +203,11 @@ export class ScheduleService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление URL для загрузки расписания
|
||||
* @param url Новый URL
|
||||
* @returns Объект с информацией о состоянии кэша
|
||||
*/
|
||||
async updateDownloadUrl(url: string): Promise<CacheStatusDto> {
|
||||
await this.scheduleParser.getXlsDownloader().setDownloadUrl(url);
|
||||
|
||||
@@ -170,6 +216,9 @@ export class ScheduleService {
|
||||
return this.getCacheStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновление кэша
|
||||
*/
|
||||
async refreshCache() {
|
||||
await this.cacheManager.clear();
|
||||
|
||||
|
||||
@@ -33,13 +33,6 @@ export default class User {
|
||||
@MaxLength(20)
|
||||
username: string;
|
||||
|
||||
/**
|
||||
* Соль пароля
|
||||
* @example "$2b$08$34xwFv1WVJpvpVi3tZZuv."
|
||||
*/
|
||||
@IsString()
|
||||
salt: string;
|
||||
|
||||
/**
|
||||
* Хеш пароля
|
||||
* @example "$2b$08$34xwFv1WVJpvpVi3tZZuv."
|
||||
|
||||
Reference in New Issue
Block a user