Secure Exam System: Anti-Cheat Protection & Backend Validation#3
Secure Exam System: Anti-Cheat Protection & Backend Validation#3alexbrestpl wants to merge 23 commits intomainfrom
Conversation
The ON CONFLICT DO UPDATE statement was calculating error_rate using old column values (before increment), causing statistics to be off by one measurement. Fixed by computing error_rate with incremented values: - error_rate = CAST(total_wrong + ? AS REAL) / (total_shown + 1) * 100 Tested with multiple answers on same questions: - Q5: 3 shown, 3 wrong → 100% ✅ - Q10: 3 shown, 2 wrong → 66.67% ✅ 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
Issues fixed: 1. skipQuestion() was not calling logBackendAnswer() 2. Skipped questions were not added to topWrongQuestions 3. This caused mismatch between wrongAnswers count and actual answers rows in DB 4. Telegram stats never showed skipped questions Changes: - app.js: skipQuestion() now treats skip as wrong answer - Gets current question - Adds to topWrongQuestions - Calls logBackendAnswer(questionId, false) - backend/database.js: Fix error_rate calculation for INSERT - Added error_rate to INSERT VALUES - First answer now correctly shows 100% or 0% error_rate - Previously showed 0% for all first answers Tested: - Q5 (correct): error_rate=0.0% ✅ - Q12 (skipped): logged as wrong, error_rate=100.0% ✅ - Q25 (wrong): error_rate=100.0% ✅ - Telegram notification includes skipped questions ✅ 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
Critical security issue:
- README instructed to serve repository root via Nginx
- This made .env publicly downloadable at https://domain/.env
- Telegram bot token would be immediately compromised
Changes:
1. Added security section in README with warnings
2. Updated Nginx config with deny rules:
- Block all dotfiles: location ~ /\.
- Block sensitive paths: .env, .git, node_modules, backend/*
3. Added .htaccess for Apache users with same protections
4. Added verification steps to check after deployment
Nginx rules added:
- location ~ /\. { deny all; return 404; }
- location ~* ^/(\.env|\.git|node_modules|backend/) { deny all; }
Apache .htaccess added:
- Denies access to .env and all dotfiles
- Blocks backend/ and node_modules/ directories
- Additional protection for sensitive file extensions
Verification:
curl https://domain/.env → should return 404/403
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
…ection Major Changes: Backend Security (database.js, server.js): - Migrate questions from JSON to SQLite (396 questions) - Questions stored in protected backend/data/, not accessible via web - Remove correct answer flags from API responses - Implement session token authentication for all protected endpoints - Add GET /api/session/:id/next - load one question at a time - Add POST /api/session/:id/submit-answer - server-side answer validation - Add POST /api/session/:id/focus-switch - log suspicious activity - Track session state: question_ids, current_question_index, focus_switches Frontend Security (app.js, security.js, style.css): - Remove questions_data.json from frontend (moved to backend) - Load questions one-by-one via protected API - localStorage stores ONLY sessionId/Token, NOT questions or answers - Implement anti-cheat protection module (security.js): * Block copy-paste (Ctrl+C, Ctrl+V, Ctrl+X) * Disable right-click context menu * Prevent text selection (user-select: none) * Detect DevTools opening (F12, Ctrl+Shift+I/J/C) * Monitor focus/tab switches with visual warnings - Add semi-transparent UUID watermark over exam screen - Log all suspicious activity to backend Migration & Tooling: - Add migrate-database.js - SQLite schema migration script - Add migrate-questions.js - JSON to SQLite data migration - Preserves all question data except correct answer flags in public API - Correct answers stored securely in separate column Testing: - API endpoints tested and working - 396 questions migrated successfully - Session token validation working - Frontend successfully integrated with secure API Security Benefits: - Correct answers never sent to client - Impossible to view answers in DevTools/localStorage - Exam integrity maintained through server-side validation - Suspicious activity (focus switches, DevTools) logged - User identification via persistent UUID watermark 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
The frontend is served by Express on port 3000, but localhost:3000 wasn't in the allowedOrigins list, causing CORS errors when frontend tried to fetch from the API. Fixed by adding: - http://localhost:3000 - http://127.0.0.1:3000 to the development allowedOrigins array. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Problem: When completing a test, the result screen showed 0 for all counters, but displayed correct values after page refresh. Root cause: Race condition when user clicks Exit button while checkAnswer() is still processing asynchronously. Local counters (correctAnswersCount, wrongAnswersCount) weren't updated yet. Solution: 1. In showResults(), fetch actual counters from server via new endpoint GET /api/stats/session/:id before displaying results 2. Added 100ms delay in Exit button handler to let pending operations complete 3. Server-side counters are always accurate since answers are logged to DB This ensures results are always correct regardless of timing or how user exits the test (Exit button vs automatic completion). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Allows users to click Exit button to view current progress/statistics, then return to continue the test from where they left off. Session only ends when all questions completed or user explicitly goes home/restarts. - Added continueTestBtn to result screen - Modified showResults() to accept forceEnd parameter - Session stays active for intermediate result views - Continue button shown only when questions remain - Updated restartTest() to force end session before restarting - Updated goToStart() to properly end session before going home 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Remove duplicate /api prefix in session stats endpoint call. The apiRequest function already includes /api in the base URL. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Prevent accidental session termination by disabling header click when user is on question screen. Header remains clickable on all other screens (home, results, info) for navigation. - Added logic to showScreen() to toggle header interactivity - cursor: pointer disabled on questionScreen - pointerEvents: none prevents clicks during test - Users must use explicit "Exit" button to leave test 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Created favicon featuring Class 4 hazardous materials sign: - Diamond shape with black border - Upper half white with black flame symbol - Lower half red - Number "4" at bottom - Based on real hazardous cargo warning signage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Implemented interactive Telegram bot using long polling to respond to commands from authorized users. Features: - Long polling mechanism for receiving Telegram updates - /difficult command - shows top 10 most difficult questions - Authorization check (only TELEGRAM_CHAT_ID can use commands) - Graceful start/stop of polling with server lifecycle - New database function getDifficultQuestions(limit) Technical details: - Uses getUpdates API with 30s timeout - Polling interval: 2 seconds - Proper error handling and logging - Unauthorized access attempts are logged but ignored Example response format: ❗️ Топ-10 самых сложных вопросов: 1. Вопрос #142 📊 Ошибок: 65.2% (показан 23 раз) ... 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Reorganized project structure for better maintainability: Frontend: - Moved static assets to frontend/public/ - Moved source files to frontend/src/ - Updated all resource paths in index.html Backend: - Moved core files to backend/src/ - Organized config files in src/config/ - Organized services in src/services/ - Moved migration scripts to scripts/ - Updated all import paths and dotenv config - Removed unnecessary .htaccess file Root level: - Added package.json with npm workspaces - Added Docker support (docker-compose.yml, .dockerignore) - Added comprehensive documentation (DATABASE.md, API.md, TELEGRAM.md) - Added setup automation script (setup.sh) - Updated .gitignore for better coverage All file moves preserve git history via git mv. Server tested and confirmed working on port 3000. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Move path module import before dotenv.config() and use path.join() to properly resolve .env file location from backend/src/ directory. This fixes Telegram bot configuration not being loaded properly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Added automatic hiding of DevTools detection warning message after 5 seconds with fade-out animation, similar to focus switch warnings. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Added PM2 + Nginx deployment setup with complete documentation: Deployment Files: - ecosystem.config.js: PM2 process manager configuration - nginx.conf.example: Nginx reverse proxy config for subdomain - deploy.sh: Automated deployment script with git pull and PM2 restart - docs/DEPLOYMENT.md: Complete VPS deployment guide Documentation: - Step-by-step VPS setup instructions - DNS and subdomain configuration - SSL setup with Let's Encrypt - Firewall configuration (UFW) - Monitoring and troubleshooting - Backup strategies - Security best practices Updates: - README.md: Updated structure section, added deployment quick start - .gitignore: Added PM2 logs, backups, and pid files Features: - One-command deployment with deploy.sh - Automatic SSL with certbot - Production-ready PM2 configuration with auto-restart - Nginx optimization for static files - Security headers and access controls 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Fixed 'Cannot GET /' error by adding explicit route that serves index.html for all non-API requests. Also fixed PM2 ecosystem.config.js to use fork mode instead of cluster mode to prevent EADDRINUSE errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Modified wildcard route to skip static file requests (js, css, images) and only serve index.html for non-static routes. This fixes MIME type errors where CSS/JS files were being served as HTML. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Changed image paths from 'img/' to 'public/img/' to reflect new directory structure after frontend reorganization. Fixes 404 errors for question images. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Added purple gradient background with hover effects and shadow for the exit button (#exitBtn). Random text generation preserved. Style features: - Purple gradient (667eea → 764ba2) - Smooth hover animation with reverse gradient - Enhanced shadow effects - Active state feedback 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Changed exit button gradient from purple to elegant dark blue: - Base: #2c3e50 → #3498db (dark slate to blue) - Hover: lighter shade with enhanced glow - Maintains smooth animations and shadow effects 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| app.post('/api/session-end', async (req, res) => { | ||
| try { | ||
| const { sessionId, correctAnswers, wrongAnswers, topWrongQuestions } = req.body; | ||
|
|
||
| if (!sessionId || correctAnswers === undefined || wrongAnswers === undefined) { |
There was a problem hiding this comment.
Require session token when ending sessions
The new /api/session-end handler closes sessions without checking a session token; it only reads sessionId and proceeds. Because other endpoints protect with X-Session-Token, an unauthenticated caller who can guess an id can now finalize any session and trigger Telegram notifications, bypassing the anti-cheat controls for active sessions.
Useful? React with 👍 / 👎.
| // Завершаем сессию в БД | ||
| db.endSession(sessionId, correctAnswers, wrongAnswers); |
There was a problem hiding this comment.
Server trusts client-supplied scores when closing session
Session completion writes correctAnswers/wrongAnswers from the client directly into the database, even though every answer is already logged server-side. A tampered request (or a mismatched local counter) can therefore save arbitrary percentages and corrupt statistics/Telegram reports; the backend should derive results from the stored answers instead of trusting client totals.
Useful? React with 👍 / 👎.
| const dbPath = path.join(__dirname, '..', 'config', 'statistics.db'); | ||
| const db = new Database(dbPath); |
There was a problem hiding this comment.
migrate-database targets non-existent DB path
The migration script opens ../config/statistics.db, but the database file now lives under src/config/statistics.db. Running this script will throw ENOENT: no such file or directory, open .../backend/config/statistics.db, preventing migrations from running at all.
Useful? React with 👍 / 👎.
| - ./backend:/app | ||
| - /app/node_modules | ||
| - ./backend/statistics.db:/app/statistics.db |
There was a problem hiding this comment.
Docker volume no longer matches database location
docker-compose still mounts ./backend/statistics.db into the container, but the app now reads its database from src/config/statistics.db. The mounted file is unused and the actual DB inside /app/src/config will be ephemeral, so data will be recreated on every container restart instead of being persisted.
Useful? React with 👍 / 👎.
| // Отправляем на сервер: пропуск = неправильный ответ (выбираем первый ID как "неправильный") | ||
| await submitAnswerToServer(currentQuestion.question_number, currentQuestion.answers[0].id); |
There was a problem hiding this comment.
Skipping a question can be recorded as correct
When skipping, the client always posts currentQuestion.answers[0].id to the backend. If the first answer happens to be the correct one (common in the migrated dataset), the backend logs a correct response while the UI increments wrongAnswersCount and marks the question wrong, leaving server-side stats and Telegram results inconsistent.
Useful? React with 👍 / 👎.
* feat: Add detailed question display on statistics page - Enhanced /api/stats endpoint to include full question data - Statistics page now shows complete question text and all answers - Correct answers highlighted with green border - Added question cards with clean borders and spacing - Removed document links and hover effects for cleaner UI 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: Add statistics button to main page and update configuration - Added statistics mode card to main page with 3-card responsive layout - Increased container width to 1000px to accommodate 3 cards - Added responsive grid that stacks cards vertically on screens ≤800px - Updated PM2 port configuration to 3001 - Temporarily disabled shuffle checkbox in training mode 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Alex Leoniuk <alex.brest.by@gmail.com> Co-authored-by: Claude <noreply@anthropic.com>
Summary
This PR introduces a comprehensive secure exam system with anti-cheat features, Telegram bot integration for statistics, and complete monorepo restructuring:
🔐 Secure Exam System
🤖 Telegram Bot Integration
/difficultcommand to view top-10 hardest questions based on error rates📁 Monorepo Restructuring
public/for static assets,src/for source codesrc/config/,src/services/,scripts/📊 Database & Statistics
🎨 Frontend Improvements
Test Plan
/difficultcommand (requires TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID in .env)🤖 Generated with Claude Code