Files
kspguti-schedule/src/pages/api/admin/login.ts
kilyabin e46a2419c3 refactor: optimize project structure, migrate to SQLite, and add new features
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
2025-12-03 21:44:07 +04:00

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' })
}
}