fix:
- Fix TypeScript type errors in api-wrapper.ts (ApiResponse type)
- Fix backward compatibility in database.ts getSettings() for missing fields
- Fix default value for weekNavigationEnabled (changed from true to false)
- Fix API routes error handling with unified wrapper
- Fix duplicate toggle switch code in admin.tsx (6 instances)
- Fix inconsistent authentication check in API routes (unified with withAuth)
- Fix error message text in loading-context.tsx (improved user experience)
add:
- Add database.ts: SQLite database layer with better-sqlite3 for persistent storage
* Groups management (CRUD operations)
* Settings management with caching
* Admin password hashing with bcrypt
* Automatic database initialization and migration
- Add api-wrapper.ts utility for unified API route handling
* withAuth wrapper for protected routes
* withMethods wrapper for public routes
* Consistent error handling and method validation
- Add validation.ts utility with centralized validation functions
* validateCourse - course validation (1-5)
* validateGroupId - group ID format validation
* validatePassword - password strength validation
- Add showAddGroupButton setting to control visibility of 'Add Group' button on homepage
- Add toggle switch component in admin.tsx for reusable UI (replaces 6 duplicate instances)
- Add CourseSelect component in admin.tsx for reusable course selection
- Add DialogFooterButtons component in admin.tsx for reusable dialog footer
- Add unified loadData function in admin.tsx to reduce code duplication
- Add change-password.ts API endpoint for admin password management
- Add logs.ts API endpoint for viewing error logs in admin panel
- Add logErrorToFile function in logger.ts for persistent error logging
- Add comprehensive error logging in schedule.ts (parsing, fetch, timeout, network errors)
- Add comprehensive project structure documentation in README.md
- Add architecture and code organization section in README.md
- Add database information section in README.md
- Add SQLite and bcrypt to tech stack documentation
- Add better-sqlite3 and bcrypt dependencies to package.json
- Add .gitignore rules for error.log and database files (data/, *.db, *.db-shm, *.db-wal)
refactor:
- Refactor admin.tsx: extract reusable components (toggle, select, dialog footer)
- Refactor API routes to use withAuth wrapper for consistent authentication
- Refactor API routes to use validation utilities instead of inline validation
- Refactor groups.ts and groups.json: move to old/data/ directory (deprecated, now using SQLite)
- Refactor settings-loader.ts: migrate from JSON to SQLite database
- Refactor groups-loader.ts: migrate from JSON to SQLite database
- Refactor database.ts: improve backward compatibility for settings migration
- Refactor admin.tsx: unify data loading functions (loadGroupsList, loadSettingsList)
- Refactor index.tsx: add showAddGroupButton prop and conditional rendering
- Refactor API routes: consistent error handling and method validation
- Refactor README.md: update tech stack, project structure, and admin panel documentation
- Refactor auth.ts: improve session management and cookie handling
- Refactor schedule.ts: improve error handling with detailed logging and error types
- Refactor logger.ts: add file-based error logging functionality
- Refactor loading-context.tsx: improve error message clarity
remove:
- Remove hello.ts test API endpoint
- Remove groups.ts and groups.json (moved to old/data/, replaced by SQLite)
update:
- Update .gitignore to exclude old data files, database files, and error logs
- Update package.json: add better-sqlite3, bcrypt and their type definitions
- Update README.md with new features, architecture, and database information
- Update all API routes to use new wrapper system
- Update admin panel with new settings and improved UI
- Update sitemap.xml with cache usage comment
129 lines
3.0 KiB
TypeScript
129 lines
3.0 KiB
TypeScript
import type { NextApiRequest, NextApiResponse } from 'next'
|
|
import { verifyPassword, setSessionCookie } from '@/shared/utils/auth'
|
|
|
|
type ResponseData = {
|
|
success?: boolean
|
|
error?: string
|
|
retryAfter?: number
|
|
}
|
|
|
|
// 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 async function handler(
|
|
req: NextApiRequest,
|
|
res: NextApiResponse<ResponseData>
|
|
) {
|
|
if (req.method !== 'POST') {
|
|
res.status(405).json({ error: 'Method not allowed' })
|
|
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') {
|
|
res.status(400).json({ error: 'Password is required' })
|
|
return
|
|
}
|
|
|
|
const isValid = await verifyPassword(password)
|
|
if (isValid) {
|
|
// Успешный вход - сбрасываем rate limit
|
|
rateLimitMap.delete(clientIP)
|
|
setSessionCookie(res)
|
|
res.status(200).json({ success: true })
|
|
} else {
|
|
// Неудачная попытка - записываем
|
|
recordFailedAttempt(clientIP)
|
|
res.status(401).json({ error: 'Invalid password' })
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|