Рефакторинг: улучшение системы аутентификации и 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:
kilyabin
2025-11-28 00:29:46 +04:00
parent 24bb531dfb
commit 9df04745df
17 changed files with 511 additions and 117 deletions

View File

@@ -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

View File

@@ -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'

View File

@@ -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} />
</>
)
}

View File

@@ -21,3 +21,4 @@ export default function handler(

View File

@@ -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(

View File

@@ -21,3 +21,4 @@ export default function handler(