Рефакторинг: улучшение системы аутентификации и UI компонентов
- Удалены устаревшие файлы (mock.js, old-schedule.txt, loading-overlay.tsx) - Переработана система аутентификации (login, logout, check-auth) - Добавлен компонент toast для уведомлений - Улучшен контекст загрузки (loading-context) - Обновлен парсер расписания (schedule.ts) - Улучшена админ-панель - Обновлена документация (README.md) - Старые файлы перемещены в директорию old/
This commit is contained in:
@@ -1,360 +0,0 @@
|
||||
export const content = `<html><head><title>Расписание занятий</title>
|
||||
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<link rel="StyleSheet" type="text/css" href="lib/css/wv_20230927.css">
|
||||
</head>
|
||||
<body bgcolor="#FFFFFF" text="#000000" link="#000000" vlink="#000000" alink="#ABB8DA" leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" bgcolor="3481A6"><tbody><tr><td height="30" align="right" bgcolor="ffffff" colspan="2" background="lib/img/itf_ht/mn_top_bg.gif"><a href="?mnt=2"></a><a href="?mnt=3"></a><table border="0" cellpadding="0" cellspacing="0"><tbody><tr><td background="lib/img/itf_ht/mn_top_bg.gif" align="right" bgcolor="ffffff" colspan="1"><a href="?mn=2" class="t_on"> Расписание занятий </a></td><td background="lib/img/itf_ht/mn_top_bg_btn.gif" align="right" bgcolor="ffffff"><img src="lib/img/itf_ht/mn_top_btn_r.gif"></td><td background="lib/img/itf_ht/mn_top_bg_btn.gif" align="right" bgcolor="ffffff"><a href="?mn=3" class="t_on">Расписание преподавателей</a></td><td background="lib/img/itf_ht/mn_top_bg_btn.gif" align="right" bgcolor="ffffff"><img src="lib/img/itf_ht/mn_top_btn_l.gif"><a href="?mnt=4"></a></td><td background="lib/img/itf_ht/mn_top_bg_btn.gif" align="right" bgcolor="ffffff"><img src="lib/img/itf_ht/mn_top_btn_r.gif"></td><td background="lib/img/itf_ht/mn_top_bg_btn.gif" align="right" bgcolor="ffffff"><a href="?mn=4" class="t_on"></a><a href="https://lk.ks.psuti.ru/std">Личный кабинет студента</a></td><td background="lib/img/itf_ht/mn_top_bg_btn.gif" align="right" bgcolor="ffffff"><img src="lib/img/itf_ht/mn_top_btn_l.gif"></td></tr></tbody></table></td></tr></tbody></table><center><a href="https://lk.ks.psuti.ru/lib/doc/lk_ks_psuti_manual.pdf" target="blank" class="t_green_14"><b><u><i>Инструкция: "Доступ в личный кабинет студента"</i></u></b></a><br>
|
||||
<a href="https://disk.yandex.ru/d/ioxTvdQXkPpU5w" target="_blank"><u><i>Расписание занятий на 1 семестр 2023-2024 уч. год (версия для печати)</i></u></a><br>
|
||||
<a href="https://disk.yandex.ru/i/WyzJ4DVeq5e_Vg" target="_blank"><u><i>График учебного процесса на 2023/2024 уч. год (версия для печати)</i></u></a><br>
|
||||
</center><br>
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" bgcolor="3481A6">
|
||||
<tbody><tr><td height="20" bgcolor="dddddd" align="center" colspan="7"><h7>Расписание занятий</h7></td></tr>
|
||||
<tr><td bgcolor="3481A6" align="center" colspan="7" background="lib/img/itf_ht/hr_b_01.gif"><img src="lib/img/itf_ht/hr_b_01.gif"></td></tr><tr><td height="10" bgcolor="ffffff" colspan="7" align="center">с 25.09.2023 по 01.10.2023
|
||||
</td></tr>
|
||||
<tr><td bgcolor="3481A6" align="center" colspan="7" background="lib/img/itf_ht/bg_h2_03.gif"><table border="0" cellpadding="0" cellspacing="0" bgcolor="ffffff">
|
||||
<tbody><tr><td width="1" valign="bottom" background="lib/img/itf_ht/bg_h2_03.gif"><a href="?mn=2&obj=146&wk=194"><img src="lib/img/itf_ht/frw_l.gif"></a></td><td height="20" bgcolor="3481A6" align="center" background="lib/img/itf_ht/bg_h2_03.gif"><a href="?mn=2&obj=146&wk=194"><h3>предыдущая неделя</h3></a></td><td width="1" valign="bottom" background="lib/img/itf_ht/bg_h2_03.gif"> </td><td width="1" valign="bottom" background="lib/img/itf_ht/bg_h2_03.gif"><img src="lib/img/itf_ht/rbl04.gif"></td><td width="1" valign="bottom" background="lib/img/itf_ht/bg_h2_03.gif"> </td><td width="1" valign="bottom" background="lib/img/itf_ht/bg_h2_03.gif"><img src="lib/img/itf_ht/rbl04.gif"></td><td width="1" valign="bottom" background="lib/img/itf_ht/bg_h2_03.gif"> </td><td height="20" bgcolor="3481A6" align="center" background="lib/img/itf_ht/bg_h2_03.gif"><a href="?mn=2&obj=146&wk=196"><h3>следующая неделя</h3></a></td><td width="1" valign="bottom" background="lib/img/itf_ht/bg_h2_03.gif"><a href="?mn=2&obj=146&wk=196"><img src="lib/img/itf_ht/frw_r.gif"></a></td></tr></tbody></table></td></tr>
|
||||
</tbody></table><table border="0" cellpadding="1" cellspacing="1" width="100%" bgcolor="3481A6">
|
||||
<tbody><tr><td height="20" bgcolor="ffffff" colspan="7"> <b>ПС-7</b></td></tr>
|
||||
<tr><td height="5" bgcolor="ffffff" colspan="7"></td></tr>
|
||||
<tr><td height="20" bgcolor="C0D8E3" colspan="7"><table border="0" cellpadding="0" cellspacing="0" bgcolor="ffffff">
|
||||
<tbody><tr><td bgcolor="3481A6"><h3>Понедельник 25.09.2023 / 4 неделя</h3></td><td bgcolor="C0D8E3"><img src="lib/img/itf_ht/days_bg.gif"></td></tr></tbody></table></td></tr>
|
||||
<tr align="center"><td bgcolor="ffffff"><b>№ пары</b></td>
|
||||
<td bgcolor="ffffff"><b>Время занятий</b></td>
|
||||
<td bgcolor="ffffff"><b>Способ</b></td>
|
||||
<td bgcolor="ffffff"><b>Дисциплина, преподаватель</b></td>
|
||||
<td bgcolor="ffffff"><b>Тема занятия</b></td>
|
||||
<td bgcolor="ffffff"><b>Ресурс</b></td>
|
||||
<td bgcolor="ffffff"><b>Задание для выполнения</b></td>
|
||||
</tr>
|
||||
<tr align="center"><td bgcolor="ffffff">1</td>
|
||||
<td bgcolor="ffffff">08:00 – 09:30
|
||||
</td>
|
||||
<td bgcolor="ffffff">Самостоятельная работа</td>
|
||||
<td bgcolor="ffffff">Физика<br>Кусаева Зарина Владимировна<font class="t_ur2"><br>Л. Толстого, 23<br>Кабинет: 410-2</font></td>
|
||||
<td bgcolor="ffffff">Силы в природе.</td>
|
||||
<td bgcolor="ffffff"><b><a href="https://cloud.mail.ru/public/eDk2/eV51SaUoU" target="blank">Лекция Задачи</a></b><br></td>
|
||||
<td bgcolor="ffffff">Сделать конспект по теме: выписать определения, формулы. Самостоятельно решить задачи.<br></td></tr>
|
||||
<tr align="center"><td bgcolor="ffffff">2</td>
|
||||
<td bgcolor="ffffff">09:40 – 11:10
|
||||
</td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff">Математика<br>Амукова Светлана Николаевна<font class="t_ur2"><br>Л. Толстого, 23<br>Кабинет: 410-2</font></td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff"><br></td></tr>
|
||||
<tr align="center"><td bgcolor="ffffff">3</td>
|
||||
<td bgcolor="ffffff">11:40 – 13:10
|
||||
</td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff">Математика<br>Амукова Светлана Николаевна<font class="t_ur2"><br>Л. Толстого, 23<br>Кабинет: 410-2</font></td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff"><br></td></tr>
|
||||
<tr align="center"><td bgcolor="ffffff">4</td>
|
||||
<td bgcolor="ffffff">13:20 – 14:50
|
||||
</td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff">Химия<br>Тарасова Таисия Евгеньевна<font class="t_ur2"><br>Л. Толстого, 23<br>Кабинет: 410-2</font></td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff"><br></td></tr>
|
||||
<tr><td height="5" bgcolor="ffffff" colspan="7"></td></tr>
|
||||
<tr><td height="20" bgcolor="C0D8E3" colspan="7"><table border="0" cellpadding="0" cellspacing="0" bgcolor="ffffff">
|
||||
<tbody><tr><td bgcolor="3481A6"><h3>Вторник 26.09.2023 / 4 неделя</h3></td><td bgcolor="C0D8E3"><img src="lib/img/itf_ht/days_bg.gif"></td></tr></tbody></table></td></tr>
|
||||
<tr align="center"><td bgcolor="ffffff"><b>№ пары</b></td>
|
||||
<td bgcolor="ffffff"><b>Время занятий</b></td>
|
||||
<td bgcolor="ffffff"><b>Способ</b></td>
|
||||
<td bgcolor="ffffff"><b>Дисциплина, преподаватель</b></td>
|
||||
<td bgcolor="ffffff"><b>Тема занятия</b></td>
|
||||
<td bgcolor="ffffff"><b>Ресурс</b></td>
|
||||
<td bgcolor="ffffff"><b>Задание для выполнения</b></td>
|
||||
</tr>
|
||||
<tr align="center"><td bgcolor="ffffff">3</td>
|
||||
<td bgcolor="ffffff">11:40 – 13:10
|
||||
</td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff">История<br>Арефьев Андрей Андреевич<font class="t_green_10"><br>Московское шоссе, 120<br>Кабинет: 401</font></td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff"><br></td></tr>
|
||||
<tr align="center"><td bgcolor="ffffbb">4</td>
|
||||
<td bgcolor="ffffbb">13:20 – 14:50
|
||||
<br><a class="t_zm">дистанционно</a></td>
|
||||
<td bgcolor="ffffbb"></td>
|
||||
<td bgcolor="ffffbb">Физическая культура<br> </td>
|
||||
<td bgcolor="ffffbb"></td>
|
||||
<td bgcolor="ffffbb"></td>
|
||||
<td bgcolor="ffffbb"><br></td></tr>
|
||||
<tr align="center"><td bgcolor="ffffff">5</td>
|
||||
<td bgcolor="ffffff">15:10 – 16:40
|
||||
</td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff">Информатика<br>Ларионова Софья Николаевна<font class="t_green_10"><br>Московское шоссе, 120<br>Кабинет: 208</font></td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff"><br></td></tr>
|
||||
<tr><td height="5" bgcolor="ffffff" colspan="7"></td></tr>
|
||||
<tr><td height="20" bgcolor="C0D8E3" colspan="7"><table border="0" cellpadding="0" cellspacing="0" bgcolor="ffffff">
|
||||
<tbody><tr><td bgcolor="3481A6"><h3>Среда 27.09.2023 / 4 неделя</h3></td><td bgcolor="C0D8E3"><img src="lib/img/itf_ht/days_bg.gif"></td></tr></tbody></table></td></tr>
|
||||
<tr align="center"><td bgcolor="ffffff"><b>№ пары</b></td>
|
||||
<td bgcolor="ffffff"><b>Время занятий</b></td>
|
||||
<td bgcolor="ffffff"><b>Способ</b></td>
|
||||
<td bgcolor="ffffff"><b>Дисциплина, преподаватель</b></td>
|
||||
<td bgcolor="ffffff"><b>Тема занятия</b></td>
|
||||
<td bgcolor="ffffff"><b>Ресурс</b></td>
|
||||
<td bgcolor="ffffff"><b>Задание для выполнения</b></td>
|
||||
</tr>
|
||||
<tr align="center"><td bgcolor="ffffff">3</td>
|
||||
<td bgcolor="ffffff">11:40 – 13:10
|
||||
</td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff">Физика<br>Кусаева Зарина Владимировна<font class="t_green_10"><br>Московское шоссе, 120<br>Кабинет: 310</font></td>
|
||||
<td bgcolor="ffffff">Импульс. Закон сохранения импульса.</td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff">Проверка конспекта и задач<br></td></tr>
|
||||
<tr align="center"><td bgcolor="ffffbb">4</td>
|
||||
<td bgcolor="ffffbb">13:20 – 14:50
|
||||
<br><a class="t_zm">перенос с 02.10.23</a></td>
|
||||
<td bgcolor="ffffbb"></td>
|
||||
<td bgcolor="ffffbb">Физика<br>Кусаева Зарина Владимировна<font class="t_green_10"><br>Московское шоссе, 120<br>Кабинет: 310</font></td>
|
||||
<td bgcolor="ffffbb">Закон сохранения энергии</td>
|
||||
<td bgcolor="ffffbb"></td>
|
||||
<td bgcolor="ffffbb"><br></td></tr>
|
||||
<tr align="center"><td bgcolor="ffffff">5</td>
|
||||
<td bgcolor="ffffff">15:10 – 16:40
|
||||
</td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff">Иностранный язык<br>Карпеева Александра Сергеевна<font class="t_green_10"><br>Московское шоссе, 120<br>Кабинет: 234</font></td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff"><br></td></tr>
|
||||
<tr align="center"><td bgcolor="ffffbb">6</td>
|
||||
<td bgcolor="ffffbb">16:50 – 18:20
|
||||
<br><a class="t_zm">дистанционно</a></td>
|
||||
<td bgcolor="ffffbb"></td>
|
||||
<td bgcolor="ffffbb">География<br> </td>
|
||||
<td bgcolor="ffffbb"></td>
|
||||
<td bgcolor="ffffbb"></td>
|
||||
<td bgcolor="ffffbb"><br></td></tr>
|
||||
<tr><td height="5" bgcolor="ffffff" colspan="7"></td></tr>
|
||||
<tr><td height="20" bgcolor="C0D8E3" colspan="7"><table border="0" cellpadding="0" cellspacing="0" bgcolor="ffffff">
|
||||
<tbody><tr><td bgcolor="3481A6"><h3>Четверг 28.09.2023 / 4 неделя</h3></td><td bgcolor="C0D8E3"><img src="lib/img/itf_ht/days_bg.gif"></td></tr></tbody></table></td></tr>
|
||||
<tr align="center"><td bgcolor="ffffff"><b>№ пары</b></td>
|
||||
<td bgcolor="ffffff"><b>Время занятий</b></td>
|
||||
<td bgcolor="ffffff"><b>Способ</b></td>
|
||||
<td bgcolor="ffffff"><b>Дисциплина, преподаватель</b></td>
|
||||
<td bgcolor="ffffff"><b>Тема занятия</b></td>
|
||||
<td bgcolor="ffffff"><b>Ресурс</b></td>
|
||||
<td bgcolor="ffffff"><b>Задание для выполнения</b></td>
|
||||
</tr>
|
||||
<tr align="center"><td bgcolor="ffffbb">2</td>
|
||||
<td bgcolor="ffffbb">09:40 – 11:10
|
||||
<br><a class="t_zm">дистанционно</a></td>
|
||||
<td bgcolor="ffffbb"></td>
|
||||
<td bgcolor="ffffbb">География<br> </td>
|
||||
<td bgcolor="ffffbb"></td>
|
||||
<td bgcolor="ffffbb"></td>
|
||||
<td bgcolor="ffffbb"><br></td></tr>
|
||||
<tr align="center"><td bgcolor="ffffff">3</td>
|
||||
<td bgcolor="ffffff">11:40 – 13:10
|
||||
</td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff">Математика<br>Амукова Светлана Николаевна<font class="t_ur2"><br>Л. Толстого, 23<br>Кабинет: 410-2</font></td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff"><br></td></tr>
|
||||
<tr align="center"><td bgcolor="ffffff">4</td>
|
||||
<td bgcolor="ffffff">13:20 – 14:50
|
||||
</td>
|
||||
<td bgcolor="ffffff">Очно</td>
|
||||
<td bgcolor="ffffff">Русский язык<br>Назарова Елена Федоровна<font class="t_ur2"><br>Л. Толстого, 23<br>Кабинет: 410-2</font></td>
|
||||
<td bgcolor="ffffff">Признаки заимствованного слова. Практическая работа 2</td>
|
||||
<td bgcolor="ffffff"><b><a href="https://cloud.mail.ru/public/7mwL/Ui6a7ftsP" target="blank">Практическая работа 2</a></b><br></td>
|
||||
<td bgcolor="ffffff"><br></td></tr>
|
||||
<tr align="center"><td bgcolor="ffffff">5</td>
|
||||
<td bgcolor="ffffff">15:10 – 16:40
|
||||
</td>
|
||||
<td bgcolor="ffffff">Очно</td>
|
||||
<td bgcolor="ffffff">Литература<br>Назарова Елена Федоровна<font class="t_ur2"><br>Л. Толстого, 23<br>Кабинет: 410-2</font></td>
|
||||
<td bgcolor="ffffff">Роман И. А. Гончарова "Обломов"</td>
|
||||
<td bgcolor="ffffff"><b><a href="https://cloud.mail.ru/public/5f3Y/vYGed416U" target="blank">лекция</a></b><br></td>
|
||||
<td bgcolor="ffffff">пересказ лекции, ответить на вопросы викторины<br></td></tr>
|
||||
<tr><td height="5" bgcolor="ffffff" colspan="7"></td></tr>
|
||||
<tr><td height="20" bgcolor="C0D8E3" colspan="7"><table border="0" cellpadding="0" cellspacing="0" bgcolor="ffffff">
|
||||
<tbody><tr><td bgcolor="3481A6"><h3>Пятница 29.09.2023 / 5 неделя</h3></td><td bgcolor="C0D8E3"><img src="lib/img/itf_ht/days_bg.gif"></td></tr></tbody></table></td></tr>
|
||||
<tr align="center"><td bgcolor="ffffff"><b>№ пары</b></td>
|
||||
<td bgcolor="ffffff"><b>Время занятий</b></td>
|
||||
<td bgcolor="ffffff"><b>Способ</b></td>
|
||||
<td bgcolor="ffffff"><b>Дисциплина, преподаватель</b></td>
|
||||
<td bgcolor="ffffff"><b>Тема занятия</b></td>
|
||||
<td bgcolor="ffffff"><b>Ресурс</b></td>
|
||||
<td bgcolor="ffffff"><b>Задание для выполнения</b></td>
|
||||
</tr>
|
||||
<tr align="center"><td bgcolor="ffffff">2</td>
|
||||
<td bgcolor="ffffff">09:40 – 11:10
|
||||
</td>
|
||||
<td bgcolor="ffffff">Очно</td>
|
||||
<td bgcolor="ffffff">Русский язык<br>Назарова Елена Федоровна<font class="t_green_10"><br>Московское шоссе, 120<br>Кабинет: 324</font></td>
|
||||
<td bgcolor="ffffff">Язык как система</td>
|
||||
<td bgcolor="ffffff"><b><a href="https://cloud.mail.ru/public/EBc5/MYaGgXqKe" target="blank">презентация</a></b><br></td>
|
||||
<td bgcolor="ffffff">изучить презентацию, выучить определения<br></td></tr>
|
||||
<tr align="center"><td bgcolor="ffffff">3</td>
|
||||
<td bgcolor="ffffff">11:40 – 13:10
|
||||
</td>
|
||||
<td bgcolor="ffffff">Очно</td>
|
||||
<td bgcolor="ffffff">Литература<br>Назарова Елена Федоровна<font class="t_green_10"><br>Московское шоссе, 120<br>Кабинет: 321</font></td>
|
||||
<td bgcolor="ffffff">"Илья Ильич Обломов как временной тип и одна из граней национального характера"</td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff">написать сочинение на тему "Что во мне есть от Обломова"<br></td></tr>
|
||||
<tr><td height="5" bgcolor="ffffff" colspan="7"></td></tr>
|
||||
<tr><td height="20" bgcolor="C0D8E3" colspan="7"><table border="0" cellpadding="0" cellspacing="0" bgcolor="ffffff">
|
||||
<tbody><tr><td bgcolor="3481A6"><h3>Суббота 30.09.2023 / 5 неделя</h3></td><td bgcolor="C0D8E3"><img src="lib/img/itf_ht/days_bg.gif"></td></tr></tbody></table></td></tr>
|
||||
<tr align="center"><td bgcolor="ffffff"><b>№ пары</b></td>
|
||||
<td bgcolor="ffffff"><b>Время занятий</b></td>
|
||||
<td bgcolor="ffffff"><b>Способ</b></td>
|
||||
<td bgcolor="ffffff"><b>Дисциплина, преподаватель</b></td>
|
||||
<td bgcolor="ffffff"><b>Тема занятия</b></td>
|
||||
<td bgcolor="ffffff"><b>Ресурс</b></td>
|
||||
<td bgcolor="ffffff"><b>Задание для выполнения</b></td>
|
||||
</tr>
|
||||
<tr align="center"><td bgcolor="ffffff">1</td>
|
||||
<td bgcolor="ffffff">08:00 – 09:30
|
||||
</td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff">Основы безопасности жизнедеятельности<br>Корнилова Светлана Александровна<font class="t_green_10"><br>Московское шоссе, 120<br>Кабинет: 110</font></td>
|
||||
<td bgcolor="ffffff">Пр№1 Обеспечение личной безопасности на дорогах</td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff">оформить отчет<br></td></tr>
|
||||
<tr align="center"><td bgcolor="ffffff">2</td>
|
||||
<td bgcolor="ffffff">09:40 – 11:10
|
||||
</td>
|
||||
<td bgcolor="ffffff">Очно</td>
|
||||
<td bgcolor="ffffff">Литература<br>Назарова Елена Федоровна<font class="t_green_10"><br>Московское шоссе, 120<br>Кабинет: 321</font></td>
|
||||
<td bgcolor="ffffff">Новый герой, «отрицающий все», в романе И. С. Тургенева (1818- 1883) «Отцы и дети»</td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff">читать роман "Отцы и дети"<br></td></tr>
|
||||
<tr align="center"><td bgcolor="ffffff">3</td>
|
||||
<td bgcolor="ffffff">11:30 – 13:00
|
||||
</td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff"><i>Свободное время</i><br> </td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff"></td>
|
||||
<td bgcolor="ffffff"><br></td></tr>
|
||||
|
||||
<tr><td height="5" bgcolor="ffffff" colspan="7"></td></tr>
|
||||
</tbody></table><table border="0" cellpadding="0" cellspacing="0" width="100%" bgcolor="3481A6">
|
||||
<tbody><tr><td bgcolor="3481A6" align="center" colspan="7" background="lib/img/itf_ht/bg_h2_03.gif"><table border="0" cellpadding="0" cellspacing="0" bgcolor="ffffff">
|
||||
<tbody><tr><td width="1" valign="bottom" background="lib/img/itf_ht/bg_h2_03.gif"><a href="?mn=2&obj=146&wk=194"><img src="lib/img/itf_ht/frw_l.gif"></a></td><td height="20" bgcolor="3481A6" align="center" background="lib/img/itf_ht/bg_h2_03.gif"><a href="?mn=2&obj=146&wk=194"><h3>предыдущая неделя</h3></a></td><td width="1" valign="bottom" background="lib/img/itf_ht/bg_h2_03.gif"> </td><td width="1" valign="bottom" background="lib/img/itf_ht/bg_h2_03.gif"><img src="lib/img/itf_ht/rbl04.gif"></td><td width="1" valign="bottom" background="lib/img/itf_ht/bg_h2_03.gif"> </td><td width="1" valign="bottom" background="lib/img/itf_ht/bg_h2_03.gif"><img src="lib/img/itf_ht/rbl04.gif"></td><td width="1" valign="bottom" background="lib/img/itf_ht/bg_h2_03.gif"> </td><td height="20" bgcolor="3481A6" align="center" background="lib/img/itf_ht/bg_h2_03.gif"><a href="?mn=2&obj=146&wk=196"><h3>следующая неделя</h3></a></td><td width="1" valign="bottom" background="lib/img/itf_ht/bg_h2_03.gif"><a href="?mn=2&obj=146&wk=196"><img src="lib/img/itf_ht/frw_r.gif"></a></td></tr></tbody></table></td></tr>
|
||||
</tbody></table><br><table border="0" cellpadding="0" cellspacing="0" width="100%" bgcolor="ffffff">
|
||||
<tbody><tr><td height="20" bgcolor="dddddd" align="center" colspan="8"><h7>Дневное отделение:</h7></td></tr>
|
||||
<tr><td bgcolor="3481A6" align="center" colspan="8" background="lib/img/itf_ht/hr_b_01.gif"><img src="lib/img/itf_ht/hr_b_01.gif"></td></tr><tr><td height="20" bgcolor="3481A6" align="center" colspan="8" background="lib/img/itf_ht/bg_h2_03.gif"><h3>Выберите группу:</h3></td></tr>
|
||||
<tr><td bgcolor="3481A6" align="center" colspan="8" background="lib/img/itf_ht/hr_b_03.gif"><img src="lib/img/itf_ht/hr_b_03.gif"></td></tr><tr><td bgcolor="3481A6" valign="bottom" colspan="2"><img src="lib/img/itf_ht/rtl02.gif"></td><td bgcolor="3481A6" valign="bottom" colspan="2"><img src="lib/img/itf_ht/rtl02.gif"></td><td bgcolor="3481A6" valign="bottom" colspan="2"><img src="lib/img/itf_ht/rtl02.gif"></td><td bgcolor="3481A6" valign="bottom" colspan="2"><img src="lib/img/itf_ht/rtl02.gif"></td></tr><tr><td width="1" valign="bottom"><img src="lib/img/itf_ht/rbl04.gif"></td><td height="20" width="25%" bgcolor="3481A6" align="center" background="lib/img/itf_ht/bg_h2_02.gif"><h3>Первый курс:</h3></td><td width="1" valign="bottom"><img src="lib/img/itf_ht/rbl04.gif"></td><td height="20" width="25%" bgcolor="3481A6" align="center" background="lib/img/itf_ht/bg_h2_02.gif"><h3>Второй курс:</h3></td><td width="1" valign="bottom"><img src="lib/img/itf_ht/rbl04.gif"></td><td height="20" width="25%" bgcolor="3481A6" align="center" background="lib/img/itf_ht/bg_h2_02.gif"><h3>Третий курс:</h3></td><td width="1" valign="bottom"><img src="lib/img/itf_ht/rbl04.gif"></td><td height="20" width="25%" bgcolor="3481A6" align="center" background="lib/img/itf_ht/bg_h2_02.gif"><h3>Четвертый курс:</h3></td></tr>
|
||||
<tr><td width="1" valign="bottom"></td><td width="25%" bgcolor="ffffff" align="center" valign="top"><table border="0" cellpadding="0" cellspacing="0" width="100%"><tbody><tr><td><table border="0" cellpadding="1" cellspacing="1" width="100%" bgcolor="3481A6">
|
||||
<tbody><tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=144"><b>ИБ-5</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=145"><b>ИБ-6</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=152"><b>ИКС-6</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=153"><b>ИКС-7к</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=150"><b>ИСПВ-6</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=151"><b>ИСПВ-7</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=161"><b>ИСПВ-8к</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=154"><b>ИСПИ-5</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=155"><b>ИСПИ-6к</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=156"><b>ИСПП-22</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=157"><b>ИСПП-23</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=158"><b>ИСПП-24к</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=159"><b>ИСПП-25к</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=160"><b>ИСПП-26к</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=163"><b>ИСПП-27к</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=146"><b>ПС-7</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=162"><b>СБ-1к</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=147"><b>ССА-10</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=148"><b>ССА-11</b></a> </td></tr>
|
||||
</tbody></table></td></tr><tr><td align="right" colspan="8" background="lib/img/itf_ht/hr_b_04.gif"><img src="lib/img/itf_ht/hr_b_05.gif"></td></tr></tbody></table></td>
|
||||
<td width="1" valign="bottom"></td><td width="25%" bgcolor="ffffff" align="center" valign="top"><table border="0" cellpadding="0" cellspacing="0" width="100%"><tbody><tr><td><table border="0" cellpadding="1" cellspacing="1" width="100%" bgcolor="3481A6">
|
||||
<tbody><tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=123"><b>ИБ-3</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=138"><b>ИБ-4к</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=124"><b>ИКС-4</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=126"><b>ИКС-5к</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=127"><b>ИСПВ-4</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=128"><b>ИСПВ-5к</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=129"><b>ИСПИ-3</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=130"><b>ИСПИ-4к</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=131"><b>ИСПП-16</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=132"><b>ИСПП-17</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=133"><b>ИСПП-18к</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=134"><b>ИСПП-19к</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=140"><b>ИСПП-20к</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=141"><b>ИСПП-21к</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=135"><b>ПС-6</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=136"><b>ССА-7</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=137"><b>ССА-8к</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=149"><b>ССА-9к</b></a> </td></tr>
|
||||
</tbody></table></td></tr><tr><td align="right" colspan="8" background="lib/img/itf_ht/hr_b_04.gif"><img src="lib/img/itf_ht/hr_b_05.gif"></td></tr></tbody></table></td>
|
||||
<td width="1" valign="bottom"></td><td width="25%" bgcolor="ffffff" align="center" valign="top"><table border="0" cellpadding="0" cellspacing="0" width="100%"><tbody><tr><td><table border="0" cellpadding="1" cellspacing="1" width="100%" bgcolor="3481A6">
|
||||
<tbody><tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=114"><b>ИБ-1к</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=115"><b>ИБ-2к</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=117"><b>ИКС-2</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=121"><b>ИКС-3к</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=111"><b>ИСПВ-1</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=112"><b>ИСПВ-2к</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=113"><b>ИСПВ-3к</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=109"><b>ИСПИ-1</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=110"><b>ИСПИ-2к</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=105"><b>ИСПП-11</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=106"><b>ИСПП-12</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=107"><b>ИСПП-13к</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=108"><b>ИСПП-14к</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=122"><b>ИСПП-15к</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=116"><b>ПС-5</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=102"><b>ССА-4</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=103"><b>ССА-5</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=104"><b>ССА-6к</b></a> </td></tr>
|
||||
</tbody></table></td></tr><tr><td align="right" colspan="8" background="lib/img/itf_ht/hr_b_04.gif"><img src="lib/img/itf_ht/hr_b_05.gif"></td></tr></tbody></table></td>
|
||||
<td width="1" valign="bottom"></td><td width="25%" bgcolor="ffffff" align="center" valign="top"><table border="0" cellpadding="0" cellspacing="0" width="100%"><tbody><tr><td><table border="0" cellpadding="1" cellspacing="1" width="100%" bgcolor="3481A6">
|
||||
<tbody><tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=101"><b>ИКС-1</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=67"><b>ИС-21</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=68"><b>ИС-22</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=70"><b>ИСПП-5</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=71"><b>ИСПП-6</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=72"><b>ИСПП-7к</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=73"><b>ИСПП-8к</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=74"><b>ИСПП-9к</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=75"><b>МТС-78</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=76"><b>ПКС-33</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=77"><b>ПКС-34</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=78"><b>ПКС-35к</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=80"><b>СК-69</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=81"><b>ССА-1к</b></a> </td></tr>
|
||||
<tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=100"><b>ССА-3к</b></a> </td></tr>
|
||||
</tbody></table></td></tr><tr><td align="right" colspan="8" background="lib/img/itf_ht/hr_b_04.gif"><img src="lib/img/itf_ht/hr_b_05.gif"></td></tr></tbody></table></td>
|
||||
</tr>
|
||||
</tbody></table><br><table border="0" cellpadding="0" cellspacing="0" width="100%" bgcolor="ffffff">
|
||||
<tbody><tr><td height="20" bgcolor="dddddd" align="center" colspan="12"><h7>Заочное отделение:</h7></td></tr>
|
||||
<tr><td bgcolor="3481A6" align="center" colspan="12" background="lib/img/itf_ht/hr_b_01.gif"><img src="lib/img/itf_ht/hr_b_01.gif"></td></tr><tr><td height="20" bgcolor="3481A6" align="center" colspan="12" background="lib/img/itf_ht/bg_h2_03.gif"><h3>Выберите группу:</h3></td></tr>
|
||||
<tr><td bgcolor="3481A6" align="center" colspan="12" background="lib/img/itf_ht/hr_b_03.gif"><img src="lib/img/itf_ht/hr_b_03.gif"></td></tr><tr><td bgcolor="3481A6" valign="bottom" colspan="2"><img src="lib/img/itf_ht/rtl02.gif"></td><td bgcolor="3481A6" valign="bottom" colspan="2"><img src="lib/img/itf_ht/rtl02.gif"></td><td bgcolor="3481A6" valign="bottom" colspan="2"><img src="lib/img/itf_ht/rtl02.gif"></td><td bgcolor="3481A6" valign="bottom" colspan="2"><img src="lib/img/itf_ht/rtl02.gif"></td><td bgcolor="3481A6" valign="bottom" colspan="2"><img src="lib/img/itf_ht/rtl02.gif"></td></tr><tr><td width="1" valign="bottom"><img src="lib/img/itf_ht/rbl04.gif"></td><td height="20" width="20%" bgcolor="3481A6" align="center" background="lib/img/itf_ht/bg_h2_02.gif"><h3>Первый курс:</h3></td><td width="1" valign="bottom"><img src="lib/img/itf_ht/rbl04.gif"></td>
|
||||
<td height="20" width="20%" bgcolor="3481A6" align="center" background="lib/img/itf_ht/bg_h2_02.gif"><h3>Второй курс:</h3></td><td width="1" valign="bottom"><img src="lib/img/itf_ht/rbl04.gif"></td>
|
||||
<td height="20" width="20%" bgcolor="3481A6" align="center" background="lib/img/itf_ht/bg_h2_02.gif"><h3>Третий курс:</h3></td><td width="1" valign="bottom"><img src="lib/img/itf_ht/rbl04.gif"></td>
|
||||
<td height="20" width="20%" bgcolor="3481A6" align="center" background="lib/img/itf_ht/bg_h2_02.gif"><h3>Четвертый курс:</h3></td><td width="1" valign="bottom"><img src="lib/img/itf_ht/rbl04.gif"></td>
|
||||
<td height="20" width="20%" bgcolor="3481A6" align="center" background="lib/img/itf_ht/bg_h2_02.gif"><h3>Пятый курс:</h3></td></tr>
|
||||
<tr><td width="1" valign="bottom"></td><td width="20%" bgcolor="ffffff" align="center" valign="top"><table border="0" cellpadding="0" cellspacing="0" width="100%"><tbody><tr><td><table border="0" cellpadding="1" cellspacing="1" width="100%" bgcolor="3481A6">
|
||||
<tbody><tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=166"><b>(з/о)иксс-23</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=165"><b>(з/о)ПС-23к</b></a> </td></tr>
|
||||
</tbody></table></td></tr><tr><td align="right" colspan="8" background="lib/img/itf_ht/hr_b_04.gif"><img src="lib/img/itf_ht/hr_b_05.gif"></td></tr></tbody></table></td>
|
||||
<td width="1" valign="bottom"></td><td width="20%" bgcolor="ffffff" align="center" valign="top"><table border="0" cellpadding="0" cellspacing="0" width="100%"><tbody><tr><td><table border="0" cellpadding="1" cellspacing="1" width="100%" bgcolor="3481A6">
|
||||
<tbody><tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=142"><b>(з/о)ИКСС-22к</b></a> </td></tr>
|
||||
</tbody></table></td></tr><tr><td align="right" colspan="8" background="lib/img/itf_ht/hr_b_04.gif"><img src="lib/img/itf_ht/hr_b_05.gif"></td></tr></tbody></table></td>
|
||||
<td width="1" valign="bottom"></td><td width="20%" bgcolor="ffffff" align="center" valign="top"><table border="0" cellpadding="0" cellspacing="0" width="100%"><tbody><tr><td><table border="0" cellpadding="1" cellspacing="1" width="100%" bgcolor="3481A6">
|
||||
<tbody><tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=118"><b>(з/о)ИКСС-21к</b></a> </td></tr>
|
||||
<tr><td bgcolor="dddddd" align="center"><a href="?mn=2&obj=143"><b>(з/о)ПС-22к</b></a> </td></tr>
|
||||
</tbody></table></td></tr><tr><td align="right" colspan="8" background="lib/img/itf_ht/hr_b_04.gif"><img src="lib/img/itf_ht/hr_b_05.gif"></td></tr></tbody></table></td>
|
||||
<td width="1" valign="bottom"></td><td width="20%" bgcolor="ffffff" align="center" valign="top"><table border="0" cellpadding="0" cellspacing="0" width="100%"><tbody><tr><td><table border="0" cellpadding="1" cellspacing="1" width="100%" bgcolor="3481A6">
|
||||
<tbody><tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=87"><b>(з/о)ПКС-20</b></a> </td></tr>
|
||||
</tbody></table></td></tr><tr><td align="right" colspan="8" background="lib/img/itf_ht/hr_b_04.gif"><img src="lib/img/itf_ht/hr_b_05.gif"></td></tr></tbody></table></td>
|
||||
<td width="1" valign="bottom"></td><td width="20%" bgcolor="ffffff" align="center" valign="top"><table border="0" cellpadding="0" cellspacing="0" width="100%"><tbody><tr><td><table border="0" cellpadding="1" cellspacing="1" width="100%" bgcolor="3481A6">
|
||||
<tbody><tr><td bgcolor="eeeeee" align="center"><a href="?mn=2&obj=46"><b>(з/о)ПКС-19</b></a> </td></tr>
|
||||
</tbody></table></td></tr><tr><td align="right" colspan="8" background="lib/img/itf_ht/hr_b_04.gif"><img src="lib/img/itf_ht/hr_b_05.gif"></td></tr></tbody></table></td>
|
||||
</tr>
|
||||
</tbody></table><br>
|
||||
</body></html>`
|
||||
@@ -1,64 +0,0 @@
|
||||
import { Day } from '@/shared/model/day'
|
||||
import { parsePage } from '@/app/parser/schedule'
|
||||
import contentTypeParser from 'content-type'
|
||||
import { JSDOM } from 'jsdom'
|
||||
// import { content as mockContent } from './mock'
|
||||
import { reportParserError } from '@/app/logger'
|
||||
// import { groups } from '@/shared/data/groups'
|
||||
|
||||
// const fetchingGroups: {
|
||||
// [groupID: number]: boolean
|
||||
// } = Object.fromEntries(Object.values(groups).map(([gId]) => [gId, false]))
|
||||
|
||||
// const callbacks: {
|
||||
// [groupID: number]: Set<{ resolve: (days: Day[]) => void, reject: (e: unknown) => void }>
|
||||
// } = Object.fromEntries(Object.values(groups).map(([gId]) => [gId, new Set()]))
|
||||
|
||||
export async function getSchedule(groupID: number, groupName: string): Promise<Day[]> {
|
||||
// if (fetchingGroups[groupID]) {
|
||||
// return new Promise((resolve, reject) => {
|
||||
// callbacks[groupID].add({
|
||||
// resolve: (days: Day[]) => resolve(days),
|
||||
// reject
|
||||
// })
|
||||
// })
|
||||
// } else {
|
||||
// fetchingGroups[groupID] = true
|
||||
// }
|
||||
|
||||
// try {
|
||||
// const result = await parseSchedule(groupID, groupName)
|
||||
// fetchingGroups[groupID] = false
|
||||
// Array.from(callbacks[groupID].values()).forEach(({ resolve }) => resolve(result))
|
||||
// callbacks[groupID].clear()
|
||||
// return result
|
||||
// } catch(e) {
|
||||
// fetchingGroups[groupID] = false
|
||||
// console.log(Array.from(callbacks[groupID].values()).length)
|
||||
// Array.from(callbacks[groupID].values()).forEach(({ reject }) => reject(e))
|
||||
// callbacks[groupID].clear()
|
||||
// throw e
|
||||
// }
|
||||
}
|
||||
|
||||
export async function parseSchedule(groupID: number, groupName: string) {
|
||||
const page = await fetch(`${process.env.PROXY_URL ?? 'https://lk.ks.psuti.ru'}/?mn=2&obj=${groupID}`)
|
||||
// const page = { text: async () => mockContent, status: 200, headers: { get: (s: string) => s && 'text/html' } }
|
||||
const content = await page.text()
|
||||
const contentType = page.headers.get('content-type')
|
||||
if (page.status === 200 && contentType && contentTypeParser.parse(contentType).type === 'text/html') {
|
||||
try {
|
||||
const root = new JSDOM(content).window.document
|
||||
return parsePage(root, groupName)
|
||||
} catch (e) {
|
||||
console.error('Error while parsing lk.ks.psuti.ru')
|
||||
reportParserError(new Date().toISOString(), 'Не удалось сделать парсинг для группы', groupName)
|
||||
throw e
|
||||
}
|
||||
} else {
|
||||
console.error(page.status, contentType)
|
||||
console.error(content.length > 500 ? content.slice(0, 500 - 3) + '...' : content)
|
||||
reportParserError(new Date().toISOString(), 'Не удалось получить страницу для группы', groupName)
|
||||
throw new Error('Error while fetching lk.ks.psuti.ru')
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import { Day } from '@/shared/model/day'
|
||||
import { parsePage, ParseResult, WeekInfo } from '@/app/parser/schedule'
|
||||
import contentTypeParser from 'content-type'
|
||||
import { JSDOM } from 'jsdom'
|
||||
// import { content as mockContent } from './mock'
|
||||
import { reportParserError } from '@/app/logger'
|
||||
import { PROXY_URL } from '@/shared/constants/urls'
|
||||
|
||||
@@ -12,41 +11,62 @@ export type ScheduleResult = {
|
||||
availableWeeks?: WeekInfo[]
|
||||
}
|
||||
|
||||
// ПС-7: 146
|
||||
export async function getSchedule(groupID: number, groupName: string, wk?: number, parseWeekNavigation: boolean = true): Promise<ScheduleResult> {
|
||||
// Валидация параметров
|
||||
if (!Number.isInteger(groupID) || groupID <= 0) {
|
||||
throw new Error('Invalid groupID: must be a positive integer')
|
||||
}
|
||||
|
||||
if (wk !== undefined && (!Number.isInteger(wk) || wk <= 0 || !isFinite(wk))) {
|
||||
throw new Error('Invalid wk parameter: must be a positive integer')
|
||||
}
|
||||
|
||||
const url = `${PROXY_URL}/?mn=2&obj=${groupID}${wk ? `&wk=${wk}` : ''}`
|
||||
const page = await fetch(url)
|
||||
// const page = { text: async () => mockContent, status: 200, headers: { get: (s: string) => s && 'text/html' } }
|
||||
const content = await page.text()
|
||||
const contentType = page.headers.get('content-type')
|
||||
if (page.status === 200 && contentType && contentTypeParser.parse(contentType).type === 'text/html') {
|
||||
let dom: JSDOM | null = null
|
||||
try {
|
||||
dom = new JSDOM(content, { url })
|
||||
const root = dom.window.document
|
||||
const result = parsePage(root, groupName, url, parseWeekNavigation)
|
||||
const scheduleResult = {
|
||||
days: result.days,
|
||||
currentWk: result.currentWk || wk,
|
||||
availableWeeks: result.availableWeeks
|
||||
}
|
||||
// Явно очищаем JSDOM для освобождения памяти
|
||||
dom.window.close()
|
||||
dom = null
|
||||
return scheduleResult
|
||||
} catch(e) {
|
||||
// Очищаем JSDOM даже в случае ошибки
|
||||
if (dom) {
|
||||
|
||||
// Добавляем таймаут 30 секунд для fetch запроса
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), 30000) // 30 секунд
|
||||
|
||||
try {
|
||||
const page = await fetch(url, { signal: controller.signal })
|
||||
clearTimeout(timeoutId)
|
||||
const content = await page.text()
|
||||
const contentType = page.headers.get('content-type')
|
||||
if (page.status === 200 && contentType && contentTypeParser.parse(contentType).type === 'text/html') {
|
||||
let dom: JSDOM | null = null
|
||||
try {
|
||||
dom = new JSDOM(content, { url })
|
||||
const root = dom.window.document
|
||||
const result = parsePage(root, groupName, url, parseWeekNavigation)
|
||||
const scheduleResult = {
|
||||
days: result.days,
|
||||
currentWk: result.currentWk || wk,
|
||||
availableWeeks: result.availableWeeks
|
||||
}
|
||||
// Явно очищаем JSDOM для освобождения памяти
|
||||
dom.window.close()
|
||||
dom = null
|
||||
return scheduleResult
|
||||
} catch(e) {
|
||||
// Очищаем JSDOM даже в случае ошибки
|
||||
if (dom) {
|
||||
dom.window.close()
|
||||
}
|
||||
console.error(`Error while parsing ${PROXY_URL}`)
|
||||
reportParserError(new Date().toISOString(), 'Не удалось сделать парсинг для группы', groupName)
|
||||
throw e
|
||||
}
|
||||
console.error(`Error while parsing ${PROXY_URL}`)
|
||||
reportParserError(new Date().toISOString(), 'Не удалось сделать парсинг для группы', groupName)
|
||||
throw e
|
||||
} else {
|
||||
// Логируем только метаданные, без содержимого ответа
|
||||
console.error(`Failed to fetch schedule: status=${page.status}, contentType=${contentType}, contentLength=${content.length}`)
|
||||
reportParserError(new Date().toISOString(), 'Не удалось получить страницу для группы', groupName)
|
||||
throw new Error(`Error while fetching ${PROXY_URL}: status ${page.status}`)
|
||||
}
|
||||
} else {
|
||||
console.error(page.status, contentType)
|
||||
console.error(content.length > 500 ? content.slice(0, 500 - 3) + '...' : content)
|
||||
reportParserError(new Date().toISOString(), 'Не удалось получить страницу для группы', groupName)
|
||||
throw new Error(`Error while fetching ${PROXY_URL}`)
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId)
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
throw new Error(`Request timeout while fetching ${PROXY_URL}`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,10 @@ export async function getServerSideProps(context: GetServerSidePropsContext<{ gr
|
||||
const settings = loadSettings()
|
||||
const group = context.params?.group
|
||||
const wkParam = context.query.wk
|
||||
const wk = wkParam ? Number(wkParam) : undefined
|
||||
// Валидация wk параметра: проверка на валидное число (не NaN, не Infinity)
|
||||
const wk = wkParam && !isNaN(Number(wkParam)) && isFinite(Number(wkParam)) && Number.isInteger(Number(wkParam)) && Number(wkParam) > 0
|
||||
? Number(wkParam)
|
||||
: undefined
|
||||
|
||||
if (group && Object.hasOwn(groups, group) && group in groups) {
|
||||
let scheduleResult: ScheduleResult
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import '@/shared/styles/globals.css'
|
||||
import type { AppProps } from 'next/app'
|
||||
import { ThemeProvider } from '@/shared/providers/theme-provider'
|
||||
import { LoadingContextProvider, LoadingContext } from '@/shared/context/loading-context'
|
||||
import { LoadingOverlay } from '@/shared/ui/loading-overlay'
|
||||
import { LoadingContextProvider, LoadingContext, LoadingOverlay } from '@/shared/context/loading-context'
|
||||
import Head from 'next/head'
|
||||
import React from 'react'
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { loadGroups, GroupsData } from '@/shared/data/groups-loader'
|
||||
import { loadSettings, AppSettings } from '@/shared/data/settings-loader'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shadcn/ui/select'
|
||||
import { ToastContainer, Toast } from '@/shared/ui/toast'
|
||||
import Head from 'next/head'
|
||||
|
||||
type AdminPageProps = {
|
||||
@@ -34,6 +35,16 @@ export default function AdminPage({ groups: initialGroups, settings: initialSett
|
||||
const [showEditDialog, setShowEditDialog] = React.useState(false)
|
||||
const [showDeleteDialog, setShowDeleteDialog] = React.useState(false)
|
||||
const [groupToDelete, setGroupToDelete] = React.useState<string | null>(null)
|
||||
const [toasts, setToasts] = React.useState<Toast[]>([])
|
||||
|
||||
const showToast = (message: string, type: 'success' | 'error' = 'success') => {
|
||||
const id = Date.now().toString()
|
||||
setToasts((prev) => [...prev, { id, message, type }])
|
||||
}
|
||||
|
||||
const removeToast = (id: string) => {
|
||||
setToasts((prev) => prev.filter((toast) => toast.id !== id))
|
||||
}
|
||||
|
||||
// Форма добавления/редактирования
|
||||
const [formData, setFormData] = React.useState({
|
||||
@@ -131,15 +142,20 @@ export default function AdminPage({ groups: initialGroups, settings: initialSett
|
||||
if (res.ok && data.success) {
|
||||
// Обновляем состояние из ответа сервера (для синхронизации)
|
||||
setSettings(data.settings)
|
||||
showToast('Настройки успешно обновлены', 'success')
|
||||
} else {
|
||||
// Откатываем изменения при ошибке
|
||||
setSettings(previousSettings)
|
||||
setError(data.error || 'Ошибка при обновлении настроек')
|
||||
const errorMessage = data.error || 'Ошибка при обновлении настроек'
|
||||
setError(errorMessage)
|
||||
showToast(errorMessage, 'error')
|
||||
}
|
||||
} catch (err) {
|
||||
// Откатываем изменения при ошибке
|
||||
setSettings(previousSettings)
|
||||
setError('Ошибка соединения с сервером')
|
||||
const errorMessage = 'Ошибка соединения с сервером'
|
||||
setError(errorMessage)
|
||||
showToast(errorMessage, 'error')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,11 +182,16 @@ export default function AdminPage({ groups: initialGroups, settings: initialSett
|
||||
setGroups(data.groups)
|
||||
setShowAddDialog(false)
|
||||
setFormData({ id: '', parseId: '', name: '', course: '1' })
|
||||
showToast('Группа успешно добавлена', 'success')
|
||||
} else {
|
||||
setError(data.error || 'Ошибка при добавлении группы')
|
||||
const errorMessage = data.error || 'Ошибка при добавлении группы'
|
||||
setError(errorMessage)
|
||||
showToast(errorMessage, 'error')
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Ошибка соединения с сервером')
|
||||
const errorMessage = 'Ошибка соединения с сервером'
|
||||
setError(errorMessage)
|
||||
showToast(errorMessage, 'error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -201,11 +222,16 @@ export default function AdminPage({ groups: initialGroups, settings: initialSett
|
||||
setShowEditDialog(false)
|
||||
setEditingGroup(null)
|
||||
setFormData({ id: '', parseId: '', name: '', course: '1' })
|
||||
showToast('Группа успешно обновлена', 'success')
|
||||
} else {
|
||||
setError(data.error || 'Ошибка при редактировании группы')
|
||||
const errorMessage = data.error || 'Ошибка при редактировании группы'
|
||||
setError(errorMessage)
|
||||
showToast(errorMessage, 'error')
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Ошибка соединения с сервером')
|
||||
const errorMessage = 'Ошибка соединения с сервером'
|
||||
setError(errorMessage)
|
||||
showToast(errorMessage, 'error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -228,11 +254,16 @@ export default function AdminPage({ groups: initialGroups, settings: initialSett
|
||||
setGroups(data.groups)
|
||||
setShowDeleteDialog(false)
|
||||
setGroupToDelete(null)
|
||||
showToast('Группа успешно удалена', 'success')
|
||||
} else {
|
||||
setError(data.error || 'Ошибка при удалении группы')
|
||||
const errorMessage = data.error || 'Ошибка при удалении группы'
|
||||
setError(errorMessage)
|
||||
showToast(errorMessage, 'error')
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Ошибка соединения с сервером')
|
||||
const errorMessage = 'Ошибка соединения с сервером'
|
||||
setError(errorMessage)
|
||||
showToast(errorMessage, 'error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -586,6 +617,9 @@ export default function AdminPage({ groups: initialGroups, settings: initialSett
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Toast уведомления */}
|
||||
<ToastContainer toasts={toasts} onClose={removeToast} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,3 +21,4 @@ export default function handler(
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,79 @@ type ResponseData = {
|
||||
error?: string
|
||||
}
|
||||
|
||||
// Rate limiting: 5 попыток в 15 минут
|
||||
const MAX_ATTEMPTS = 5
|
||||
const WINDOW_MS = 15 * 60 * 1000 // 15 минут
|
||||
|
||||
interface RateLimitEntry {
|
||||
attempts: number
|
||||
resetTime: number
|
||||
}
|
||||
|
||||
const rateLimitMap = new Map<string, RateLimitEntry>()
|
||||
|
||||
function getClientIP(req: NextApiRequest): string {
|
||||
// Получаем IP адрес клиента
|
||||
const forwarded = req.headers['x-forwarded-for']
|
||||
const realIP = req.headers['x-real-ip']
|
||||
const remoteAddress = req.socket.remoteAddress
|
||||
|
||||
if (typeof forwarded === 'string') {
|
||||
return forwarded.split(',')[0].trim()
|
||||
}
|
||||
if (typeof realIP === 'string') {
|
||||
return realIP
|
||||
}
|
||||
if (remoteAddress) {
|
||||
return remoteAddress
|
||||
}
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
function checkRateLimit(ip: string): boolean {
|
||||
const now = Date.now()
|
||||
const entry = rateLimitMap.get(ip)
|
||||
|
||||
// Очищаем старые записи
|
||||
if (entry && now > entry.resetTime) {
|
||||
rateLimitMap.delete(ip)
|
||||
}
|
||||
|
||||
const currentEntry = rateLimitMap.get(ip)
|
||||
|
||||
if (!currentEntry) {
|
||||
// Первая попытка
|
||||
rateLimitMap.set(ip, {
|
||||
attempts: 1,
|
||||
resetTime: now + WINDOW_MS
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
if (currentEntry.attempts >= MAX_ATTEMPTS) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Увеличиваем счетчик попыток
|
||||
currentEntry.attempts++
|
||||
return true
|
||||
}
|
||||
|
||||
function recordFailedAttempt(ip: string): void {
|
||||
const entry = rateLimitMap.get(ip)
|
||||
if (entry) {
|
||||
// Попытка уже засчитана в checkRateLimit
|
||||
return
|
||||
}
|
||||
|
||||
// Если записи нет, создаем новую
|
||||
const now = Date.now()
|
||||
rateLimitMap.set(ip, {
|
||||
attempts: 1,
|
||||
resetTime: now + WINDOW_MS
|
||||
})
|
||||
}
|
||||
|
||||
export default function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<ResponseData>
|
||||
@@ -15,6 +88,19 @@ export default function handler(
|
||||
return
|
||||
}
|
||||
|
||||
const clientIP = getClientIP(req)
|
||||
|
||||
// Проверяем rate limit
|
||||
if (!checkRateLimit(clientIP)) {
|
||||
const entry = rateLimitMap.get(clientIP)
|
||||
const retryAfter = entry ? Math.ceil((entry.resetTime - Date.now()) / 1000) : WINDOW_MS / 1000
|
||||
res.status(429).json({
|
||||
error: 'Too many login attempts. Please try again later.',
|
||||
retryAfter
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const { password } = req.body
|
||||
|
||||
if (!password || typeof password !== 'string') {
|
||||
@@ -23,9 +109,13 @@ export default function handler(
|
||||
}
|
||||
|
||||
if (verifyPassword(password)) {
|
||||
// Успешный вход - сбрасываем rate limit
|
||||
rateLimitMap.delete(clientIP)
|
||||
setSessionCookie(res)
|
||||
res.status(200).json({ success: true })
|
||||
} else {
|
||||
// Неудачная попытка - записываем
|
||||
recordFailedAttempt(clientIP)
|
||||
res.status(401).json({ error: 'Invalid password' })
|
||||
}
|
||||
}
|
||||
@@ -33,3 +123,4 @@ export default function handler(
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -21,3 +21,4 @@ export default function handler(
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Spinner } from '@/shared/ui/spinner'
|
||||
import { cn } from '@/shared/utils'
|
||||
|
||||
interface LoadingContextType {
|
||||
isLoading: boolean
|
||||
@@ -44,3 +46,82 @@ export function LoadingContextProvider({ children }: React.PropsWithChildren) {
|
||||
)
|
||||
}
|
||||
|
||||
const loadingMessages = [
|
||||
'Вайбкодим…',
|
||||
'Отменяем пары…',
|
||||
'Объезжаем пробки…',
|
||||
'Ищем замены…',
|
||||
'Ждем выходных…',
|
||||
]
|
||||
|
||||
interface LoadingOverlayProps {
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
export function LoadingOverlay({ isLoading }: LoadingOverlayProps) {
|
||||
const [currentMessage, setCurrentMessage] = React.useState<string>('')
|
||||
const [messageOpacity, setMessageOpacity] = React.useState(0)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isLoading) {
|
||||
setCurrentMessage('')
|
||||
setMessageOpacity(0)
|
||||
return
|
||||
}
|
||||
|
||||
// Выбираем случайное сообщение при старте загрузки
|
||||
const getRandomMessage = () => {
|
||||
const randomIndex = Math.floor(Math.random() * loadingMessages.length)
|
||||
return loadingMessages[randomIndex]
|
||||
}
|
||||
|
||||
// Устанавливаем первое сообщение
|
||||
setCurrentMessage(getRandomMessage())
|
||||
setMessageOpacity(1)
|
||||
|
||||
// Меняем сообщение каждые 2 секунды
|
||||
const interval = setInterval(() => {
|
||||
// Fade out
|
||||
setMessageOpacity(0)
|
||||
|
||||
// После fade out меняем сообщение и fade in
|
||||
setTimeout(() => {
|
||||
setCurrentMessage(getRandomMessage())
|
||||
setMessageOpacity(1)
|
||||
}, 300) // Длительность fade анимации
|
||||
}, 2000)
|
||||
|
||||
return () => {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, [isLoading])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 flex items-center justify-center',
|
||||
'bg-background/80 backdrop-blur-md',
|
||||
'transition-opacity duration-300',
|
||||
isLoading ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
||||
)}
|
||||
aria-label="Загрузка"
|
||||
role="status"
|
||||
aria-hidden={!isLoading}
|
||||
>
|
||||
{isLoading && (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="w-16 h-16">
|
||||
<Spinner size="large" />
|
||||
</div>
|
||||
<div
|
||||
className="min-h-[1.5rem] text-center transition-opacity duration-300"
|
||||
style={{ opacity: messageOpacity }}
|
||||
>
|
||||
{currentMessage}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Spinner } from './spinner'
|
||||
import { cn } from '@/shared/utils'
|
||||
|
||||
interface LoadingOverlayProps {
|
||||
isLoading: boolean
|
||||
}
|
||||
|
||||
export function LoadingOverlay({ isLoading }: LoadingOverlayProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 flex items-center justify-center',
|
||||
'bg-background/80 backdrop-blur-md',
|
||||
'transition-opacity duration-300',
|
||||
isLoading ? 'opacity-100' : 'opacity-0 pointer-events-none'
|
||||
)}
|
||||
aria-label="Загрузка"
|
||||
role="status"
|
||||
aria-hidden={!isLoading}
|
||||
>
|
||||
{isLoading && (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="w-16 h-16">
|
||||
<Spinner size="large" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
86
src/shared/ui/toast.tsx
Normal file
86
src/shared/ui/toast.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import React from 'react'
|
||||
import { cn } from '@/shared/utils'
|
||||
import { CheckCircle2, XCircle, X } from 'lucide-react'
|
||||
|
||||
export type ToastType = 'success' | 'error'
|
||||
|
||||
export interface Toast {
|
||||
id: string
|
||||
message: string
|
||||
type: ToastType
|
||||
}
|
||||
|
||||
interface ToastProps {
|
||||
toast: Toast
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function ToastComponent({ toast, onClose }: ToastProps) {
|
||||
const [isClosing, setIsClosing] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
handleClose()
|
||||
}, 3000) // Автоматически закрывается через 3 секунды
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}, [])
|
||||
|
||||
const handleClose = () => {
|
||||
setIsClosing(true)
|
||||
// Ждем завершения анимации перед удалением из DOM
|
||||
setTimeout(() => {
|
||||
onClose()
|
||||
}, 300) // Длительность анимации исчезновения
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'fixed bottom-4 left-1/2 -translate-x-1/2 z-50',
|
||||
'flex items-center gap-3 px-4 py-3 rounded-lg shadow-lg',
|
||||
'border min-w-[300px] max-w-[500px]',
|
||||
'transition-all duration-300 ease-in-out',
|
||||
isClosing
|
||||
? 'opacity-0 translate-y-2 scale-95'
|
||||
: 'opacity-100 translate-y-0 scale-100 animate-in slide-in-from-bottom-5 fade-in-0',
|
||||
toast.type === 'success'
|
||||
? 'bg-background border-green-500/50 text-foreground'
|
||||
: 'bg-background border-destructive/50 text-foreground'
|
||||
)}
|
||||
role="alert"
|
||||
>
|
||||
{toast.type === 'success' ? (
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500 flex-shrink-0" />
|
||||
) : (
|
||||
<XCircle className="h-5 w-5 text-destructive flex-shrink-0" />
|
||||
)}
|
||||
<p className="flex-1 text-sm font-medium">{toast.message}</p>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="flex-shrink-0 rounded-sm opacity-70 hover:opacity-100 transition-opacity focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 min-w-[24px] min-h-[24px] flex items-center justify-center"
|
||||
aria-label="Закрыть уведомление"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface ToastContainerProps {
|
||||
toasts: Toast[]
|
||||
onClose: (id: string) => void
|
||||
}
|
||||
|
||||
export function ToastContainer({ toasts, onClose }: ToastContainerProps) {
|
||||
if (toasts.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-4 left-1/2 -translate-x-1/2 z-50 flex flex-col gap-2 items-center">
|
||||
{toasts.map((toast) => (
|
||||
<ToastComponent key={toast.id} toast={toast} onClose={() => onClose(toast.id)} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,11 +2,28 @@ import { NextApiRequest, NextApiResponse } from 'next'
|
||||
import crypto from 'crypto'
|
||||
|
||||
const SESSION_COOKIE_NAME = 'admin_session'
|
||||
const SESSION_SECRET = process.env.ADMIN_SESSION_SECRET || 'change-me-in-production'
|
||||
const SESSION_SECRET = process.env.ADMIN_SESSION_SECRET
|
||||
const SESSION_DURATION = 1000 * 60 * 60 * 24 // 24 часа
|
||||
|
||||
// Получаем секрет сессии с учетом окружения
|
||||
function getSessionSecret(): string {
|
||||
if (SESSION_SECRET) {
|
||||
return SESSION_SECRET
|
||||
}
|
||||
|
||||
// В production требуем явную установку
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
throw new Error('ADMIN_SESSION_SECRET must be set in production environment')
|
||||
}
|
||||
|
||||
// В development используем дефолтный секрет с предупреждением
|
||||
console.warn('ADMIN_SESSION_SECRET is not set. Using default secret for development. This is not secure for production!')
|
||||
return 'change-me-in-production'
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет пароль администратора
|
||||
* Использует timing-safe сравнение для защиты от timing attacks
|
||||
*/
|
||||
export function verifyPassword(password: string): boolean {
|
||||
const adminPassword = process.env.ADMIN_PASSWORD
|
||||
@@ -14,17 +31,31 @@ export function verifyPassword(password: string): boolean {
|
||||
console.error('ADMIN_PASSWORD is not set')
|
||||
return false
|
||||
}
|
||||
return password === adminPassword
|
||||
|
||||
// Используем timing-safe сравнение для защиты от timing attacks
|
||||
if (password.length !== adminPassword.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
const passwordBuffer = Buffer.from(password, 'utf8')
|
||||
const adminPasswordBuffer = Buffer.from(adminPassword, 'utf8')
|
||||
return crypto.timingSafeEqual(passwordBuffer, adminPasswordBuffer)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создает сессионный токен
|
||||
*/
|
||||
function createSessionToken(): string {
|
||||
const secret = getSessionSecret()
|
||||
|
||||
const randomBytes = crypto.randomBytes(32).toString('hex')
|
||||
const timestamp = Date.now().toString()
|
||||
const data = `${randomBytes}:${timestamp}`
|
||||
const hash = crypto.createHmac('sha256', SESSION_SECRET).update(data).digest('hex')
|
||||
const hash = crypto.createHmac('sha256', secret).update(data).digest('hex')
|
||||
return `${data}:${hash}`
|
||||
}
|
||||
|
||||
@@ -33,12 +64,14 @@ function createSessionToken(): string {
|
||||
*/
|
||||
function verifySessionToken(token: string): boolean {
|
||||
try {
|
||||
const secret = getSessionSecret()
|
||||
|
||||
const parts = token.split(':')
|
||||
if (parts.length !== 3) return false
|
||||
|
||||
const [randomBytes, timestamp, hash] = parts
|
||||
const data = `${randomBytes}:${timestamp}`
|
||||
const expectedHash = crypto.createHmac('sha256', SESSION_SECRET).update(data).digest('hex')
|
||||
const expectedHash = crypto.createHmac('sha256', secret).update(data).digest('hex')
|
||||
|
||||
if (hash !== expectedHash) return false
|
||||
|
||||
@@ -70,8 +103,14 @@ export function checkAuth(req: NextApiRequest): boolean {
|
||||
const cookieHeader = req.headers.cookie
|
||||
if (!cookieHeader) return false
|
||||
|
||||
// Улучшенный парсинг cookies для корректной обработки значений с '='
|
||||
const cookies = cookieHeader.split(';').reduce((acc, cookie) => {
|
||||
const [key, value] = cookie.trim().split('=')
|
||||
const trimmed = cookie.trim()
|
||||
const equalIndex = trimmed.indexOf('=')
|
||||
if (equalIndex === -1) return acc
|
||||
|
||||
const key = trimmed.substring(0, equalIndex)
|
||||
const value = trimmed.substring(equalIndex + 1)
|
||||
acc[key] = value
|
||||
return acc
|
||||
}, {} as Record<string, string>)
|
||||
|
||||
Reference in New Issue
Block a user