Skip to content

Secure Exam System: Anti-Cheat Protection & Backend Validation#3

Open
alexbrestpl wants to merge 23 commits intomainfrom
feature/secure-exam-system
Open

Secure Exam System: Anti-Cheat Protection & Backend Validation#3
alexbrestpl wants to merge 23 commits intomainfrom
feature/secure-exam-system

Conversation

@alexbrestpl
Copy link
Copy Markdown
Owner

@alexbrestpl alexbrestpl commented Nov 14, 2025

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

  • Anti-cheat protection with focus detection and tab switching monitoring
  • Session-based authentication with cryptographic tokens
  • Continue test functionality allowing users to resume interrupted sessions
  • Real-time answer validation and statistics tracking

🤖 Telegram Bot Integration

  • /difficult command to view top-10 hardest questions based on error rates
  • Automatic session completion notifications
  • Owner-only access with security logging
  • Long polling implementation

📁 Monorepo Restructuring

  • Organized frontend structure: public/ for static assets, src/ for source code
  • Organized backend structure: src/config/, src/services/, scripts/
  • Root-level npm workspaces configuration
  • Docker support with docker-compose.yml
  • Comprehensive documentation (DATABASE.md, API.md, TELEGRAM.md)
  • Setup automation script

📊 Database & Statistics

  • SQLite database with 5 tables: questions, users, sessions, answers, questions_stats
  • Migration scripts for database schema and question data
  • Error rate calculation and difficulty analysis
  • Session history tracking

🎨 Frontend Improvements

  • Enhanced UI with modal dialogs for intermediate results
  • Favicon with hazard class 4 warning sign
  • Security module for anti-cheat features
  • Improved result display and navigation

Test Plan

  • Server starts successfully on port 3000
  • Test mode prevents tab switching and tracks focus changes
  • Training mode allows free navigation
  • Session tokens are validated correctly
  • Continue test functionality works after interruption
  • Telegram bot responds to /difficult command (requires TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID in .env)
  • Statistics are calculated correctly (error rates, percentages)
  • Migration scripts populate database correctly
  • Docker containers build and run successfully
  • All API endpoints return correct responses
  • Frontend resources load from new paths (public/, src/)

🤖 Generated with Claude Code

alexbrestby and others added 22 commits November 13, 2025 18:28
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>
@alexbrestpl
Copy link
Copy Markdown
Owner Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +149 to +153
app.post('/api/session-end', async (req, res) => {
try {
const { sessionId, correctAnswers, wrongAnswers, topWrongQuestions } = req.body;

if (!sessionId || correctAnswers === undefined || wrongAnswers === undefined) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +157 to +158
// Завершаем сессию в БД
db.endSession(sessionId, correctAnswers, wrongAnswers);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +9 to +10
const dbPath = path.join(__dirname, '..', 'config', 'statistics.db');
const db = new Database(dbPath);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +15 to +17
- ./backend:/app
- /app/node_modules
- ./backend/statistics.db:/app/statistics.db
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +423 to +424
// Отправляем на сервер: пропуск = неправильный ответ (выбираем первый ID как "неправильный")
await submitAnswerToServer(currentQuestion.question_number, currentQuestion.answers[0].id);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants