├── uploads
└── .gitkeep
├── controllers
├── quizController_mysql.js
├── analyticsController.js
└── achievementController.js
├── client
├── public
│ ├── favicon.png
│ ├── web_logo.png
│ └── vite.svg
├── postcss.config.js
├── src
│ ├── lib
│ │ └── utils.js
│ ├── main.jsx
│ ├── components
│ │ ├── Layout.jsx
│ │ ├── ProtectedRoute.jsx
│ │ ├── ui
│ │ │ ├── label.jsx
│ │ │ ├── input.jsx
│ │ │ ├── progress.jsx
│ │ │ ├── badge.jsx
│ │ │ ├── password-input.jsx
│ │ │ ├── card.jsx
│ │ │ ├── button.jsx
│ │ │ └── dialog.jsx
│ │ └── FunctionalBugs
│ │ │ └── GuestLoginModal.jsx
│ ├── App.css
│ ├── services
│ │ └── categoryService.js
│ ├── index.css
│ ├── context
│ │ └── AuthContext.jsx
│ ├── assets
│ │ └── react.svg
│ └── pages
│ │ └── Register.jsx
├── .env.production
├── .gitignore
├── index.html
├── vite.config.js
├── eslint.config.js
├── README.md
├── tailwind.config.js
└── package.json
├── client_backup
├── public
│ ├── favicon.png
│ ├── web_logo.png
│ └── vite.svg
├── postcss.config.js
├── src
│ ├── lib
│ │ └── utils.js
│ ├── main.jsx
│ ├── components
│ │ ├── Layout.jsx
│ │ ├── ProtectedRoute.jsx
│ │ ├── ui
│ │ │ ├── label.jsx
│ │ │ ├── progress.jsx
│ │ │ ├── input.jsx
│ │ │ ├── badge.jsx
│ │ │ ├── card.jsx
│ │ │ ├── button.jsx
│ │ │ └── dialog.jsx
│ │ └── FunctionalBugs
│ │ │ └── GuestLoginModal.jsx
│ ├── App.css
│ ├── services
│ │ ├── categoryService.js
│ │ └── api.js
│ ├── index.css
│ ├── context
│ │ └── AuthContext.jsx
│ ├── assets
│ │ └── react.svg
│ ├── pages
│ │ ├── Register.jsx
│ │ └── Login.jsx
│ └── App.jsx
├── vite.config.js
├── .gitignore
├── index.html
├── eslint.config.js
├── package.json
└── tailwind.config.js
├── routes
├── analytics.js
├── achievements.js
├── leaderboard.js
├── progress.js
├── quiz.js
├── questions.js
├── arenaAuth.js
├── admin.js
├── functionalBugs.js
├── questionUpload.js
└── auth.js
├── utils
├── errorResponse.js
├── helpers.js
└── validators.js
├── middleware
├── validation.js
├── rateLimiter.js
├── errorHandler.js
├── auth.js
└── arenaAuth.js
├── models
├── mysql
│ ├── BugTestingTip.js
│ ├── QuestionTag.js
│ ├── BugPreventionTip.js
│ ├── QuizAnswer.js
│ ├── BugHint.js
│ ├── BugStep.js
│ ├── QuestionOption.js
│ ├── QuestionOptionTranslation.js
│ ├── AchievementTranslation.js
│ ├── SiteVisit.js
│ ├── RecentActivity.js
│ ├── UserAchievement.js
│ ├── QuizQuestion.js
│ ├── QuestionTranslation.js
│ ├── DifficultyProgress.js
│ ├── Progress.js
│ ├── CategoryProgress.js
│ ├── Achievement.js
│ ├── ArenaUser.js
│ ├── FunctionalBug.js
│ ├── Question.js
│ ├── Quiz.js
│ └── User.js
├── Achievement.js
├── ArenaUser.js
├── FunctionalBugStats.js
├── UserFunctionalBugProgress.js
├── Quiz.js
├── Progress.js
├── FunctionalBug.js
├── Question.js
└── User.js
├── tests
├── setup.js
├── questions.test.js
├── auth.test.js
└── quiz.test.js
├── scripts
├── reseedFunctionalBugs.js
├── testSmtp.js
└── createDatabase.js
├── config
├── database.js
├── mysqlDatabase.js
└── mysql.config.js
├── .env.example
├── package.json
├── seedAchievements.js
├── .gitignore
└── server.js
/uploads/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/controllers/quizController_mysql.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rhydwharn/qaarena/HEAD/client/public/favicon.png
--------------------------------------------------------------------------------
/client/public/web_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rhydwharn/qaarena/HEAD/client/public/web_logo.png
--------------------------------------------------------------------------------
/client_backup/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rhydwharn/qaarena/HEAD/client_backup/public/favicon.png
--------------------------------------------------------------------------------
/client_backup/public/web_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rhydwharn/qaarena/HEAD/client_backup/public/web_logo.png
--------------------------------------------------------------------------------
/client/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/client_backup/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | '@tailwindcss/postcss': {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/client/src/lib/utils.js:
--------------------------------------------------------------------------------
1 | import { clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs) {
5 | return twMerge(clsx(inputs))
6 | }
--------------------------------------------------------------------------------
/client/.env.production:
--------------------------------------------------------------------------------
1 | # Production Environment Variables
2 | # Replace with your actual production backend URL
3 | VITE_API_URL=https://qaarena.online/api
4 | # NODE_ENV=production
5 |
--------------------------------------------------------------------------------
/client_backup/src/lib/utils.js:
--------------------------------------------------------------------------------
1 | import { clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs) {
5 | return twMerge(clsx(inputs))
6 | }
--------------------------------------------------------------------------------
/client_backup/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vite.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/client/src/main.jsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import './index.css'
4 | import App from './App.jsx'
5 |
6 | createRoot(document.getElementById('root')).render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/client_backup/src/main.jsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import './index.css'
4 | import App from './App.jsx'
5 |
6 | createRoot(document.getElementById('root')).render(
7 |
8 |
9 | ,
10 | )
11 |
--------------------------------------------------------------------------------
/client/src/components/Layout.jsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from 'react-router-dom';
2 | import Navbar from './Navbar';
3 |
4 | export default function Layout() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/client_backup/src/components/Layout.jsx:
--------------------------------------------------------------------------------
1 | import { Outlet } from 'react-router-dom';
2 | import Navbar from './Navbar';
3 |
4 | export default function Layout() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 | );
13 | }
14 |
--------------------------------------------------------------------------------
/routes/analytics.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const { recordVisit } = require('../controllers/analyticsController');
4 | const { optionalAuth } = require('../middleware/auth');
5 |
6 | // POST /api/analytics/visit
7 | router.post('/visit', optionalAuth, recordVisit);
8 |
9 | module.exports = router;
10 |
--------------------------------------------------------------------------------
/client_backup/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/utils/errorResponse.js:
--------------------------------------------------------------------------------
1 | // utils/errorResponse.js
2 | class ErrorResponse extends Error {
3 | constructor(message, statusCode) {
4 | super(message);
5 | this.statusCode = statusCode;
6 | this.isOperational = true; // This is to distinguish operational errors from programming errors
7 |
8 | Error.captureStackTrace(this, this.constructor);
9 | }
10 | }
11 |
12 | module.exports = ErrorResponse;
--------------------------------------------------------------------------------
/client_backup/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | client
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | .env.production
26 | .env.development
27 | .env.local
28 | dist
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | QA ARENA | The QA One-Stop Platform
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/middleware/validation.js:
--------------------------------------------------------------------------------
1 | const { validationResult } = require('express-validator');
2 |
3 | exports.validate = (req, res, next) => {
4 | const errors = validationResult(req);
5 |
6 | if (!errors.isEmpty()) {
7 | return res.status(400).json({
8 | status: 'error',
9 | message: 'Validation failed',
10 | errors: errors.array().map(err => ({
11 | field: err.path,
12 | message: err.msg
13 | }))
14 | });
15 | }
16 |
17 | next();
18 | };
--------------------------------------------------------------------------------
/routes/achievements.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const {
4 | getAllAchievements,
5 | getUserAchievements,
6 | checkAchievements
7 | } = require('../controllers/achievementController');
8 | const { protect } = require('../middleware/auth');
9 |
10 | router.get('/', protect, getAllAchievements);
11 | router.get('/user', protect, getUserAchievements);
12 | router.post('/check', protect, checkAchievements);
13 |
14 | module.exports = router;
--------------------------------------------------------------------------------
/routes/leaderboard.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const {
4 | getGlobalLeaderboard,
5 | getCategoryLeaderboard,
6 | getUserRank
7 | } = require('../controllers/leaderboardController');
8 | const { protect, optionalAuth } = require('../middleware/auth');
9 |
10 | router.get('/global', optionalAuth, getGlobalLeaderboard);
11 | router.get('/category/:category', optionalAuth, getCategoryLeaderboard);
12 | router.get('/rank', protect, getUserRank);
13 |
14 | module.exports = router;
--------------------------------------------------------------------------------
/client/src/components/ProtectedRoute.jsx:
--------------------------------------------------------------------------------
1 | import { Navigate } from 'react-router-dom';
2 | import { useAuth } from '../context/AuthContext';
3 |
4 | export default function ProtectedRoute({ children }) {
5 | const { user, loading } = useAuth();
6 |
7 | if (loading) {
8 | return (
9 |
12 | );
13 | }
14 |
15 | if (!user) {
16 | return ;
17 | }
18 |
19 | return children;
20 | }
21 |
--------------------------------------------------------------------------------
/client_backup/src/components/ProtectedRoute.jsx:
--------------------------------------------------------------------------------
1 | import { Navigate } from 'react-router-dom';
2 | import { useAuth } from '../context/AuthContext';
3 |
4 | export default function ProtectedRoute({ children }) {
5 | const { user, loading } = useAuth();
6 |
7 | if (loading) {
8 | return (
9 |
12 | );
13 | }
14 |
15 | if (!user) {
16 | return ;
17 | }
18 |
19 | return children;
20 | }
21 |
--------------------------------------------------------------------------------
/routes/progress.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const {
4 | getProgress,
5 | getCategoryProgress,
6 | getWeakAreas,
7 | getStudyStreak,
8 | getRecentActivity
9 | } = require('../controllers/progressController');
10 | const { protect } = require('../middleware/auth');
11 |
12 | router.get('/', protect, getProgress);
13 | router.get('/categories', protect, getCategoryProgress);
14 | router.get('/weak-areas', protect, getWeakAreas);
15 | router.get('/streak', protect, getStudyStreak);
16 | router.get('/activity', protect, getRecentActivity);
17 |
18 | module.exports = router;
--------------------------------------------------------------------------------
/client/src/components/ui/label.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { cva } from "class-variance-authority"
4 | import { cn } from "../../lib/utils"
5 |
6 | const labelVariants = cva(
7 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
8 | )
9 |
10 | const Label = React.forwardRef(({ className, ...props }, ref) => (
11 |
16 | ))
17 | Label.displayName = LabelPrimitive.Root.displayName
18 |
19 | export { Label }
20 |
--------------------------------------------------------------------------------
/client_backup/src/components/ui/label.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { cva } from "class-variance-authority"
4 | import { cn } from "../../lib/utils"
5 |
6 | const labelVariants = cva(
7 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
8 | )
9 |
10 | const Label = React.forwardRef(({ className, ...props }, ref) => (
11 |
16 | ))
17 | Label.displayName = LabelPrimitive.Root.displayName
18 |
19 | export { Label }
20 |
--------------------------------------------------------------------------------
/client/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 |
4 | export default defineConfig(() => ({
5 | plugins: [react()],
6 | // Use root base in both dev and build so the app works at https://qaarena.online/
7 | base: '/',
8 | server: {
9 | port: 5173,
10 | strictPort: true,
11 | proxy: {
12 | '/api': {
13 | // In development, proxy API calls to the local backend
14 | target: 'http://localhost:5001',
15 | changeOrigin: true,
16 | secure: false,
17 | // Keep /api prefix as-is (matches backend routes: /api/*)
18 | }
19 | }
20 | }
21 | }));
--------------------------------------------------------------------------------
/routes/quiz.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const {
4 | startQuiz,
5 | answerQuestion,
6 | completeQuiz,
7 | getQuiz,
8 | getUserQuizzes,
9 | getInProgressQuiz
10 | } = require('../controllers/quizController');
11 | const { protect } = require('../middleware/auth');
12 | const { quizLimiter } = require('../middleware/rateLimiter');
13 |
14 | router.post('/start', protect, startQuiz);
15 | router.post('/answer', protect, quizLimiter, answerQuestion);
16 | router.post('/:id/complete', protect, completeQuiz);
17 | router.get('/in-progress', protect, getInProgressQuiz);
18 | router.get('/:id', protect, getQuiz);
19 | router.get('/user/history', protect, getUserQuizzes);
20 |
21 | module.exports = router;
--------------------------------------------------------------------------------
/client/src/components/ui/input.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cn } from "../../lib/utils"
3 |
4 | const Input = React.forwardRef(({ className, type, ...props }, ref) => {
5 | return (
6 |
15 | )
16 | })
17 | Input.displayName = "Input"
18 |
19 | export { Input }
20 |
--------------------------------------------------------------------------------
/client/src/components/ui/progress.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ProgressPrimitive from "@radix-ui/react-progress"
3 | import { cn } from "../../lib/utils"
4 |
5 | const Progress = React.forwardRef(({ className, value, ...props }, ref) => (
6 |
14 |
18 |
19 | ))
20 | Progress.displayName = ProgressPrimitive.Root.displayName
21 |
22 | export { Progress }
23 |
--------------------------------------------------------------------------------
/client_backup/src/components/ui/progress.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ProgressPrimitive from "@radix-ui/react-progress"
3 | import { cn } from "../../lib/utils"
4 |
5 | const Progress = React.forwardRef(({ className, value, ...props }, ref) => (
6 |
14 |
18 |
19 | ))
20 | Progress.displayName = ProgressPrimitive.Root.displayName
21 |
22 | export { Progress }
23 |
--------------------------------------------------------------------------------
/routes/questions.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const {
4 | getQuestions,
5 | getQuestion,
6 | createQuestion,
7 | updateQuestion,
8 | deleteQuestion,
9 | voteQuestion,
10 | flagQuestion
11 | } = require('../controllers/questionController');
12 | const { protect, authorize, optionalAuth } = require('../middleware/auth');
13 |
14 | router.get('/', optionalAuth, getQuestions);
15 | router.get('/:id', getQuestion);
16 | router.post('/', protect, authorize('admin', 'moderator'), createQuestion);
17 | router.put('/:id', protect, updateQuestion);
18 | router.delete('/:id', protect, deleteQuestion);
19 | router.post('/:id/vote', protect, voteQuestion);
20 | router.post('/:id/flag', protect, flagQuestion);
21 |
22 | module.exports = router;
--------------------------------------------------------------------------------
/client_backup/src/components/ui/input.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cn } from "../../lib/utils"
3 |
4 | const Input = React.forwardRef(({ className, type, ...props }, ref) => {
5 | return (
6 |
15 | )
16 | })
17 | Input.displayName = "Input"
18 |
19 | export { Input }
20 |
--------------------------------------------------------------------------------
/routes/arenaAuth.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const arenaAuthController = require('../controllers/arenaAuthController');
4 | const { verifyUser } = require('../middleware/arenaAuth');
5 |
6 | // POST /api/arena-auth/signup
7 | router.post('/signup', arenaAuthController.signup);
8 |
9 | // POST /api/arena-auth/verify-otp
10 | router.post('/verify-otp', arenaAuthController.verifyOTP);
11 |
12 | // POST /api/arena-auth/verify-token
13 | router.post('/verify-token', arenaAuthController.verifyToken);
14 |
15 | // POST /api/arena-auth/signin
16 | router.post('/signin', arenaAuthController.signin);
17 |
18 | // GET /api/arena-auth/verify-user - Check if token is valid
19 | router.get('/verify-user', verifyUser);
20 |
21 | module.exports = router;
22 |
--------------------------------------------------------------------------------
/client/src/App.css:
--------------------------------------------------------------------------------
1 | #root {
2 | max-width: 1280px;
3 | margin: 0 auto;
4 | padding: 2rem;
5 | text-align: center;
6 | }
7 |
8 | .logo {
9 | height: 6em;
10 | padding: 1.5em;
11 | will-change: filter;
12 | transition: filter 300ms;
13 | }
14 | .logo:hover {
15 | filter: drop-shadow(0 0 2em #646cffaa);
16 | }
17 | .logo.react:hover {
18 | filter: drop-shadow(0 0 2em #61dafbaa);
19 | }
20 |
21 | @keyframes logo-spin {
22 | from {
23 | transform: rotate(0deg);
24 | }
25 | to {
26 | transform: rotate(360deg);
27 | }
28 | }
29 |
30 | @media (prefers-reduced-motion: no-preference) {
31 | a:nth-of-type(2) .logo {
32 | animation: logo-spin infinite 20s linear;
33 | }
34 | }
35 |
36 | .card {
37 | padding: 2em;
38 | }
39 |
40 | .read-the-docs {
41 | color: #888;
42 | }
43 |
--------------------------------------------------------------------------------
/client_backup/src/App.css:
--------------------------------------------------------------------------------
1 | #root {
2 | max-width: 1280px;
3 | margin: 0 auto;
4 | padding: 2rem;
5 | text-align: center;
6 | }
7 |
8 | .logo {
9 | height: 6em;
10 | padding: 1.5em;
11 | will-change: filter;
12 | transition: filter 300ms;
13 | }
14 | .logo:hover {
15 | filter: drop-shadow(0 0 2em #646cffaa);
16 | }
17 | .logo.react:hover {
18 | filter: drop-shadow(0 0 2em #61dafbaa);
19 | }
20 |
21 | @keyframes logo-spin {
22 | from {
23 | transform: rotate(0deg);
24 | }
25 | to {
26 | transform: rotate(360deg);
27 | }
28 | }
29 |
30 | @media (prefers-reduced-motion: no-preference) {
31 | a:nth-of-type(2) .logo {
32 | animation: logo-spin infinite 20s linear;
33 | }
34 | }
35 |
36 | .card {
37 | padding: 2em;
38 | }
39 |
40 | .read-the-docs {
41 | color: #888;
42 | }
43 |
--------------------------------------------------------------------------------
/models/mysql/BugTestingTip.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../../config/mysqlDatabase');
3 |
4 | const BugTestingTip = sequelize.define('BugTestingTip', {
5 | id: {
6 | type: DataTypes.INTEGER,
7 | primaryKey: true,
8 | autoIncrement: true
9 | },
10 | bugId: {
11 | type: DataTypes.INTEGER,
12 | allowNull: false,
13 | field: 'bug_id',
14 | references: {
15 | model: 'functional_bugs',
16 | key: 'id'
17 | }
18 | },
19 | tipText: {
20 | type: DataTypes.TEXT,
21 | allowNull: false,
22 | field: 'tip_text'
23 | }
24 | }, {
25 | tableName: 'bug_testing_tips',
26 | timestamps: false,
27 | underscored: true,
28 | indexes: [
29 | { fields: ['bug_id'] }
30 | ]
31 | });
32 |
33 | module.exports = BugTestingTip;
34 |
--------------------------------------------------------------------------------
/models/mysql/QuestionTag.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../../config/mysqlDatabase');
3 |
4 | const QuestionTag = sequelize.define('QuestionTag', {
5 | id: {
6 | type: DataTypes.INTEGER,
7 | primaryKey: true,
8 | autoIncrement: true
9 | },
10 | questionId: {
11 | type: DataTypes.INTEGER,
12 | allowNull: false,
13 | field: 'question_id',
14 | references: {
15 | model: 'questions',
16 | key: 'id'
17 | }
18 | },
19 | tag: {
20 | type: DataTypes.STRING(50),
21 | allowNull: false
22 | }
23 | }, {
24 | tableName: 'question_tags',
25 | timestamps: false,
26 | underscored: true,
27 | indexes: [
28 | { fields: ['question_id'] },
29 | { fields: ['tag'] }
30 | ]
31 | });
32 |
33 | module.exports = QuestionTag;
34 |
--------------------------------------------------------------------------------
/models/mysql/BugPreventionTip.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../../config/mysqlDatabase');
3 |
4 | const BugPreventionTip = sequelize.define('BugPreventionTip', {
5 | id: {
6 | type: DataTypes.INTEGER,
7 | primaryKey: true,
8 | autoIncrement: true
9 | },
10 | bugId: {
11 | type: DataTypes.INTEGER,
12 | allowNull: false,
13 | field: 'bug_id',
14 | references: {
15 | model: 'functional_bugs',
16 | key: 'id'
17 | }
18 | },
19 | tipText: {
20 | type: DataTypes.TEXT,
21 | allowNull: false,
22 | field: 'tip_text'
23 | }
24 | }, {
25 | tableName: 'bug_prevention_tips',
26 | timestamps: false,
27 | underscored: true,
28 | indexes: [
29 | { fields: ['bug_id'] }
30 | ]
31 | });
32 |
33 | module.exports = BugPreventionTip;
34 |
--------------------------------------------------------------------------------
/models/mysql/QuizAnswer.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../../config/mysqlDatabase');
3 |
4 | const QuizAnswer = sequelize.define('QuizAnswer', {
5 | id: {
6 | type: DataTypes.INTEGER,
7 | primaryKey: true,
8 | autoIncrement: true
9 | },
10 | quizQuestionId: {
11 | type: DataTypes.INTEGER,
12 | allowNull: false,
13 | field: 'quiz_question_id',
14 | references: {
15 | model: 'quiz_questions',
16 | key: 'id'
17 | }
18 | },
19 | optionIndex: {
20 | type: DataTypes.INTEGER,
21 | allowNull: false,
22 | field: 'option_index'
23 | }
24 | }, {
25 | tableName: 'quiz_answers',
26 | timestamps: false,
27 | underscored: true,
28 | indexes: [
29 | { fields: ['quiz_question_id'] }
30 | ]
31 | });
32 |
33 | module.exports = QuizAnswer;
34 |
--------------------------------------------------------------------------------
/client/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import { defineConfig, globalIgnores } from 'eslint/config'
6 |
7 | export default defineConfig([
8 | globalIgnores(['dist']),
9 | {
10 | files: ['**/*.{js,jsx}'],
11 | extends: [
12 | js.configs.recommended,
13 | reactHooks.configs['recommended-latest'],
14 | reactRefresh.configs.vite,
15 | ],
16 | languageOptions: {
17 | ecmaVersion: 2020,
18 | globals: globals.browser,
19 | parserOptions: {
20 | ecmaVersion: 'latest',
21 | ecmaFeatures: { jsx: true },
22 | sourceType: 'module',
23 | },
24 | },
25 | rules: {
26 | 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27 | },
28 | },
29 | ])
30 |
--------------------------------------------------------------------------------
/client_backup/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import { defineConfig, globalIgnores } from 'eslint/config'
6 |
7 | export default defineConfig([
8 | globalIgnores(['dist']),
9 | {
10 | files: ['**/*.{js,jsx}'],
11 | extends: [
12 | js.configs.recommended,
13 | reactHooks.configs['recommended-latest'],
14 | reactRefresh.configs.vite,
15 | ],
16 | languageOptions: {
17 | ecmaVersion: 2020,
18 | globals: globals.browser,
19 | parserOptions: {
20 | ecmaVersion: 'latest',
21 | ecmaFeatures: { jsx: true },
22 | sourceType: 'module',
23 | },
24 | },
25 | rules: {
26 | 'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
27 | },
28 | },
29 | ])
30 |
--------------------------------------------------------------------------------
/models/mysql/BugHint.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../../config/mysqlDatabase');
3 |
4 | const BugHint = sequelize.define('BugHint', {
5 | id: {
6 | type: DataTypes.INTEGER,
7 | primaryKey: true,
8 | autoIncrement: true
9 | },
10 | bugId: {
11 | type: DataTypes.INTEGER,
12 | allowNull: false,
13 | field: 'bug_id',
14 | references: {
15 | model: 'functional_bugs',
16 | key: 'id'
17 | }
18 | },
19 | hintNumber: {
20 | type: DataTypes.INTEGER,
21 | allowNull: false,
22 | field: 'hint_number'
23 | },
24 | hintText: {
25 | type: DataTypes.TEXT,
26 | allowNull: false,
27 | field: 'hint_text'
28 | }
29 | }, {
30 | tableName: 'bug_hints',
31 | timestamps: false,
32 | underscored: true,
33 | indexes: [
34 | { fields: ['bug_id'] }
35 | ]
36 | });
37 |
38 | module.exports = BugHint;
39 |
--------------------------------------------------------------------------------
/models/mysql/BugStep.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../../config/mysqlDatabase');
3 |
4 | const BugStep = sequelize.define('BugStep', {
5 | id: {
6 | type: DataTypes.INTEGER,
7 | primaryKey: true,
8 | autoIncrement: true
9 | },
10 | bugId: {
11 | type: DataTypes.INTEGER,
12 | allowNull: false,
13 | field: 'bug_id',
14 | references: {
15 | model: 'functional_bugs',
16 | key: 'id'
17 | }
18 | },
19 | stepNumber: {
20 | type: DataTypes.INTEGER,
21 | allowNull: false,
22 | field: 'step_number'
23 | },
24 | stepDescription: {
25 | type: DataTypes.TEXT,
26 | allowNull: false,
27 | field: 'step_description'
28 | }
29 | }, {
30 | tableName: 'bug_steps',
31 | timestamps: false,
32 | underscored: true,
33 | indexes: [
34 | { fields: ['bug_id'] }
35 | ]
36 | });
37 |
38 | module.exports = BugStep;
39 |
--------------------------------------------------------------------------------
/models/mysql/QuestionOption.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../../config/mysqlDatabase');
3 |
4 | const QuestionOption = sequelize.define('QuestionOption', {
5 | id: {
6 | type: DataTypes.INTEGER,
7 | primaryKey: true,
8 | autoIncrement: true
9 | },
10 | questionId: {
11 | type: DataTypes.INTEGER,
12 | allowNull: false,
13 | field: 'question_id',
14 | references: {
15 | model: 'questions',
16 | key: 'id'
17 | }
18 | },
19 | optionIndex: {
20 | type: DataTypes.INTEGER,
21 | allowNull: false,
22 | field: 'option_index'
23 | },
24 | isCorrect: {
25 | type: DataTypes.BOOLEAN,
26 | allowNull: false,
27 | field: 'is_correct'
28 | }
29 | }, {
30 | tableName: 'question_options',
31 | timestamps: false,
32 | underscored: true,
33 | indexes: [
34 | { fields: ['question_id'] }
35 | ]
36 | });
37 |
38 | module.exports = QuestionOption;
39 |
--------------------------------------------------------------------------------
/tests/setup.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | // Set test environment BEFORE any imports
4 | process.env.NODE_ENV = 'test';
5 | process.env.JWT_SECRET = process.env.JWT_SECRET || 'test_jwt_secret_key';
6 | process.env.MONGODB_URI = 'mongodb://localhost:27017/istqb_test';
7 |
8 | beforeAll(async () => {
9 | // Disconnect any existing connections
10 | if (mongoose.connection.readyState !== 0) {
11 | await mongoose.disconnect();
12 | }
13 |
14 | const mongoUri = 'mongodb://localhost:27017/istqb_test';
15 | await mongoose.connect(mongoUri);
16 | });
17 |
18 | afterAll(async () => {
19 | try {
20 | await mongoose.connection.dropDatabase();
21 | await mongoose.disconnect();
22 | } catch (error) {
23 | console.error('Error in afterAll:', error);
24 | }
25 | });
26 |
27 | afterEach(async () => {
28 | const collections = mongoose.connection.collections;
29 | for (const key in collections) {
30 | await collections[key].deleteMany({});
31 | }
32 | });
--------------------------------------------------------------------------------
/models/Achievement.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const achievementSchema = new mongoose.Schema({
4 | name: { type: Map, of: String, required: true },
5 | description: { type: Map, of: String, required: true },
6 | icon: { type: String, required: true },
7 | type: {
8 | type: String,
9 | enum: ['quiz', 'streak', 'score', 'category', 'special'],
10 | required: true
11 | },
12 | criteria: {
13 | metric: { type: String, required: true },
14 | threshold: { type: Number, required: true },
15 | category: String
16 | },
17 | rarity: {
18 | type: String,
19 | enum: ['common', 'rare', 'epic', 'legendary'],
20 | default: 'common'
21 | },
22 | points: { type: Number, default: 10 },
23 | isActive: { type: Boolean, default: true },
24 | unlockedBy: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
25 | createdAt: { type: Date, default: Date.now }
26 | }, { timestamps: true });
27 |
28 | module.exports = mongoose.model('Achievement', achievementSchema);
--------------------------------------------------------------------------------
/routes/admin.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const {
4 | getStats,
5 | getAllUsers,
6 | updateUserRole,
7 | deactivateUser,
8 | getFlaggedQuestions,
9 | reviewQuestion,
10 | createAchievement,
11 | updateAchievement,
12 | deleteAchievement,
13 | getSiteVisitStats
14 | } = require('../controllers/adminController');
15 | const { protect, authorize } = require('../middleware/auth');
16 |
17 | router.use(protect);
18 | router.use(authorize('admin'));
19 |
20 | router.get('/stats', getStats);
21 | router.get('/users', getAllUsers);
22 | router.put('/users/:id/role', updateUserRole);
23 | router.put('/users/:id/deactivate', deactivateUser);
24 | router.get('/questions/flagged', getFlaggedQuestions);
25 | router.put('/questions/:id/review', reviewQuestion);
26 | router.post('/achievements', createAchievement);
27 | router.put('/achievements/:id', updateAchievement);
28 | router.delete('/achievements/:id', deleteAchievement);
29 | router.get('/site-visits', getSiteVisitStats);
30 |
31 | module.exports = router;
--------------------------------------------------------------------------------
/models/mysql/QuestionOptionTranslation.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../../config/mysqlDatabase');
3 |
4 | const QuestionOptionTranslation = sequelize.define('QuestionOptionTranslation', {
5 | id: {
6 | type: DataTypes.INTEGER,
7 | primaryKey: true,
8 | autoIncrement: true
9 | },
10 | optionId: {
11 | type: DataTypes.INTEGER,
12 | allowNull: false,
13 | field: 'option_id',
14 | references: {
15 | model: 'question_options',
16 | key: 'id'
17 | }
18 | },
19 | language: {
20 | type: DataTypes.STRING(10),
21 | allowNull: false
22 | },
23 | optionText: {
24 | type: DataTypes.TEXT,
25 | allowNull: false,
26 | field: 'option_text'
27 | }
28 | }, {
29 | tableName: 'question_option_translations',
30 | timestamps: false,
31 | underscored: true,
32 | indexes: [
33 | {
34 | unique: true,
35 | fields: ['option_id', 'language'],
36 | name: 'unique_option_language'
37 | }
38 | ]
39 | });
40 |
41 | module.exports = QuestionOptionTranslation;
42 |
--------------------------------------------------------------------------------
/client/src/services/categoryService.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5001/api';
4 |
5 | // Get all unique categories from the database
6 | export const getCategories = async () => {
7 | try {
8 | const response = await axios.get(`${API_URL}/questions-upload/categories`);
9 | return response.data.data;
10 | } catch (error) {
11 | console.error('Error fetching categories:', error);
12 | // Return default categories as fallback
13 | return [
14 | 'fundamentals',
15 | 'testing-throughout-sdlc',
16 | 'static-testing',
17 | 'test-techniques',
18 | 'test-management',
19 | 'tool-support',
20 | 'agile-testing',
21 | 'test-automation'
22 | ];
23 | }
24 | };
25 |
26 | // Format categories for select dropdowns
27 | export const formatCategoriesForSelect = (categories) => {
28 | return categories.map(cat => ({
29 | value: cat,
30 | label: cat.split('-').map(word =>
31 | word.charAt(0).toUpperCase() + word.slice(1)
32 | ).join(' ')
33 | }));
34 | };
35 |
--------------------------------------------------------------------------------
/client_backup/src/services/categoryService.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5001/api';
4 |
5 | // Get all unique categories from the database
6 | export const getCategories = async () => {
7 | try {
8 | const response = await axios.get(`${API_URL}/questions-upload/categories`);
9 | return response.data.data;
10 | } catch (error) {
11 | console.error('Error fetching categories:', error);
12 | // Return default categories as fallback
13 | return [
14 | 'fundamentals',
15 | 'testing-throughout-sdlc',
16 | 'static-testing',
17 | 'test-techniques',
18 | 'test-management',
19 | 'tool-support',
20 | 'agile-testing',
21 | 'test-automation'
22 | ];
23 | }
24 | };
25 |
26 | // Format categories for select dropdowns
27 | export const formatCategoriesForSelect = (categories) => {
28 | return categories.map(cat => ({
29 | value: cat,
30 | label: cat.split('-').map(word =>
31 | word.charAt(0).toUpperCase() + word.slice(1)
32 | ).join(' ')
33 | }));
34 | };
35 |
--------------------------------------------------------------------------------
/models/mysql/AchievementTranslation.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../../config/mysqlDatabase');
3 |
4 | const AchievementTranslation = sequelize.define('AchievementTranslation', {
5 | id: {
6 | type: DataTypes.INTEGER,
7 | primaryKey: true,
8 | autoIncrement: true
9 | },
10 | achievementId: {
11 | type: DataTypes.INTEGER,
12 | allowNull: false,
13 | field: 'achievement_id',
14 | references: {
15 | model: 'achievements',
16 | key: 'id'
17 | }
18 | },
19 | language: {
20 | type: DataTypes.STRING(10),
21 | allowNull: false
22 | },
23 | name: {
24 | type: DataTypes.STRING(255),
25 | allowNull: false
26 | },
27 | description: {
28 | type: DataTypes.TEXT,
29 | allowNull: false
30 | }
31 | }, {
32 | tableName: 'achievement_translations',
33 | timestamps: false,
34 | underscored: true,
35 | indexes: [
36 | {
37 | unique: true,
38 | fields: ['achievement_id', 'language'],
39 | name: 'unique_achievement_language'
40 | }
41 | ]
42 | });
43 |
44 | module.exports = AchievementTranslation;
45 |
--------------------------------------------------------------------------------
/client/src/components/ui/badge.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva } from "class-variance-authority"
3 |
4 | import { cn } from "../../lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | function Badge({
27 | className,
28 | variant,
29 | ...props
30 | }) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/models/ArenaUser.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const ArenaUserSchema = new mongoose.Schema({
4 | firstName: {
5 | type: String,
6 | required: [true, 'First name is required'],
7 | trim: true
8 | },
9 | lastName: {
10 | type: String,
11 | required: [true, 'Last name is required'],
12 | trim: true
13 | },
14 | email: {
15 | type: String,
16 | required: [true, 'Email is required'],
17 | unique: true,
18 | lowercase: true,
19 | trim: true,
20 | match: [/^\S+@\S+\.\S+$/, 'Please provide a valid email']
21 | },
22 | password: {
23 | type: String,
24 | required: [true, 'Password is required'],
25 | minlength: 6
26 | },
27 | authMode: {
28 | type: String,
29 | enum: ['otp', 'token'],
30 | required: true,
31 | default: 'otp'
32 | },
33 | isVerified: {
34 | type: Boolean,
35 | default: false
36 | },
37 | verificationOTP: String,
38 | verificationToken: String,
39 | verificationExpiry: Date,
40 | createdAt: {
41 | type: Date,
42 | default: Date.now
43 | }
44 | });
45 |
46 | module.exports = mongoose.model('ArenaUser', ArenaUserSchema);
47 |
--------------------------------------------------------------------------------
/client_backup/src/components/ui/badge.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva } from "class-variance-authority"
3 |
4 | import { cn } from "../../lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | function Badge({
27 | className,
28 | variant,
29 | ...props
30 | }) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/models/mysql/SiteVisit.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../../config/mysqlDatabase');
3 |
4 | const SiteVisit = sequelize.define('SiteVisit', {
5 | id: {
6 | type: DataTypes.INTEGER,
7 | primaryKey: true,
8 | autoIncrement: true
9 | },
10 | visitedAt: {
11 | type: DataTypes.DATE,
12 | allowNull: false,
13 | defaultValue: DataTypes.NOW,
14 | field: 'visited_at'
15 | },
16 | path: {
17 | type: DataTypes.STRING(255),
18 | allowNull: true
19 | },
20 | ipHash: {
21 | type: DataTypes.STRING(64),
22 | allowNull: true,
23 | field: 'ip_hash'
24 | },
25 | userAgent: {
26 | type: DataTypes.STRING(255),
27 | allowNull: true,
28 | field: 'user_agent'
29 | },
30 | userId: {
31 | type: DataTypes.INTEGER,
32 | allowNull: true,
33 | field: 'user_id',
34 | references: {
35 | model: 'users',
36 | key: 'id'
37 | }
38 | }
39 | }, {
40 | tableName: 'site_visits',
41 | timestamps: false,
42 | underscored: true,
43 | indexes: [
44 | { fields: ['visited_at'] },
45 | { fields: ['path'] }
46 | ]
47 | });
48 |
49 | module.exports = SiteVisit;
50 |
--------------------------------------------------------------------------------
/models/mysql/RecentActivity.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../../config/mysqlDatabase');
3 |
4 | const RecentActivity = sequelize.define('RecentActivity', {
5 | id: {
6 | type: DataTypes.INTEGER,
7 | primaryKey: true,
8 | autoIncrement: true
9 | },
10 | progressId: {
11 | type: DataTypes.INTEGER,
12 | allowNull: false,
13 | field: 'progress_id',
14 | references: {
15 | model: 'progress',
16 | key: 'id'
17 | }
18 | },
19 | activityDate: {
20 | type: DataTypes.DATEONLY,
21 | allowNull: false,
22 | field: 'activity_date'
23 | },
24 | questionsAnswered: {
25 | type: DataTypes.INTEGER,
26 | defaultValue: 0,
27 | field: 'questions_answered'
28 | },
29 | score: {
30 | type: DataTypes.DECIMAL(5, 2),
31 | defaultValue: 0
32 | },
33 | timeSpent: {
34 | type: DataTypes.INTEGER,
35 | defaultValue: 0,
36 | field: 'time_spent'
37 | }
38 | }, {
39 | tableName: 'recent_activity',
40 | timestamps: false,
41 | underscored: true,
42 | indexes: [
43 | { fields: ['progress_id', 'activity_date'] }
44 | ]
45 | });
46 |
47 | module.exports = RecentActivity;
48 |
--------------------------------------------------------------------------------
/routes/functionalBugs.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const {
4 | getAllBugs,
5 | getBugById,
6 | startBugScenario,
7 | getHint,
8 | submitAnswer,
9 | getUserProgress,
10 | getLeaderboard,
11 | getBugStats,
12 | createBug,
13 | updateBug,
14 | deleteBug
15 | } = require('../controllers/functionalBugController');
16 | const { protect, authorize } = require('../middleware/auth');
17 |
18 | // Public routes
19 | router.get('/', getAllBugs);
20 | router.get('/leaderboard', getLeaderboard);
21 | router.get('/:bugId', getBugById);
22 | router.get('/:bugId/stats', getBugStats);
23 |
24 | // Protected routes (require authentication)
25 | router.post('/:bugId/start', protect, startBugScenario);
26 | router.post('/:bugId/hint', protect, getHint);
27 | router.post('/:bugId/submit', protect, submitAnswer);
28 | router.get('/user/progress', protect, getUserProgress);
29 |
30 | // Admin routes
31 | router.post('/', protect, authorize('admin', 'moderator'), createBug);
32 | router.put('/:bugId', protect, authorize('admin', 'moderator'), updateBug);
33 | router.delete('/:bugId', protect, authorize('admin'), deleteBug);
34 |
35 | module.exports = router;
36 |
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # React + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
10 | ## React Compiler
11 |
12 | The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
13 |
14 | ## Expanding the ESLint configuration
15 |
16 | If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
17 |
--------------------------------------------------------------------------------
/scripts/reseedFunctionalBugs.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const mongoose = require('mongoose');
3 | const FunctionalBug = require('../models/FunctionalBug');
4 | const FunctionalBugStats = require('../models/FunctionalBugStats');
5 |
6 | const connectDB = async () => {
7 | try {
8 | await mongoose.connect(process.env.MONGODB_URI);
9 | console.log('✅ MongoDB Connected');
10 | } catch (error) {
11 | console.error('❌ MongoDB Connection Error:', error.message);
12 | process.exit(1);
13 | }
14 | };
15 |
16 | const reseedBugs = async () => {
17 | try {
18 | await connectDB();
19 |
20 | console.log('🗑️ Deleting existing functional bugs...');
21 | const deletedBugs = await FunctionalBug.deleteMany({});
22 | const deletedStats = await FunctionalBugStats.deleteMany({});
23 | console.log(`✅ Deleted ${deletedBugs.deletedCount} bugs and ${deletedStats.deletedCount} stats`);
24 |
25 | console.log('\n📦 Now run: node scripts/seedFunctionalBugs.js');
26 | console.log(' to seed the new bugs with simulators!\n');
27 |
28 | process.exit(0);
29 | } catch (error) {
30 | console.error('❌ Error:', error);
31 | process.exit(1);
32 | }
33 | };
34 |
35 | reseedBugs();
36 |
--------------------------------------------------------------------------------
/models/mysql/UserAchievement.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../../config/mysqlDatabase');
3 |
4 | const UserAchievement = sequelize.define('UserAchievement', {
5 | id: {
6 | type: DataTypes.INTEGER,
7 | primaryKey: true,
8 | autoIncrement: true
9 | },
10 | userId: {
11 | type: DataTypes.INTEGER,
12 | allowNull: false,
13 | field: 'user_id',
14 | references: {
15 | model: 'users',
16 | key: 'id'
17 | }
18 | },
19 | achievementId: {
20 | type: DataTypes.INTEGER,
21 | allowNull: false,
22 | field: 'achievement_id',
23 | references: {
24 | model: 'achievements',
25 | key: 'id'
26 | }
27 | },
28 | unlockedAt: {
29 | type: DataTypes.DATE,
30 | defaultValue: DataTypes.NOW,
31 | field: 'unlocked_at'
32 | }
33 | }, {
34 | tableName: 'user_achievements',
35 | timestamps: false,
36 | underscored: true,
37 | indexes: [
38 | {
39 | unique: true,
40 | fields: ['user_id', 'achievement_id'],
41 | name: 'unique_user_achievement'
42 | },
43 | { fields: ['user_id'] },
44 | { fields: ['achievement_id'] }
45 | ]
46 | });
47 |
48 | module.exports = UserAchievement;
49 |
--------------------------------------------------------------------------------
/client/src/components/ui/password-input.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Eye, EyeOff } from "lucide-react";
3 | import { Input } from "./input";
4 | import { Button } from "./button";
5 | import { cn } from "../../lib/utils";
6 |
7 | export const PasswordInput = React.forwardRef(
8 | ({ className, toggleAriaLabel = "Toggle password visibility", ...props }, ref) => {
9 | const [visible, setVisible] = React.useState(false);
10 |
11 | const type = visible ? "text" : "password";
12 |
13 | return (
14 |
15 |
21 |
31 |
32 | );
33 | }
34 | );
35 |
36 | PasswordInput.displayName = "PasswordInput";
37 |
--------------------------------------------------------------------------------
/scripts/testSmtp.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const nodemailer = require('nodemailer');
3 |
4 | async function main() {
5 | console.log('Using SMTP config:');
6 | console.log({
7 | host: process.env.SMTP_HOST,
8 | port: process.env.SMTP_PORT,
9 | secure: process.env.SMTP_SECURE === 'true',
10 | user: process.env.SMTP_USER,
11 | from: process.env.SMTP_FROM,
12 | });
13 |
14 | const transporter = nodemailer.createTransport({
15 | host: process.env.SMTP_HOST,
16 | port: Number(process.env.SMTP_PORT) || 587,
17 | secure: process.env.SMTP_SECURE === 'true', // false for STARTTLS
18 | auth: {
19 | user: process.env.SMTP_USER,
20 | pass: process.env.SMTP_PASS,
21 | },
22 | });
23 |
24 | const info = await transporter.sendMail({
25 | from: process.env.SMTP_FROM,
26 | to: process.env.SMTP_TEST_TO || process.env.SMTP_USER, // set a test recipient
27 | subject: 'SMTP Test from QA Arena backend',
28 | text: 'This is a test email confirming SMTP credentials work.',
29 | });
30 |
31 | console.log('Message sent:', info.messageId);
32 | }
33 |
34 | main().catch((err) => {
35 | console.error('SMTP test failed:', err.message);
36 | console.error(err);
37 | process.exit(1);
38 | });
--------------------------------------------------------------------------------
/models/mysql/QuizQuestion.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../../config/mysqlDatabase');
3 |
4 | const QuizQuestion = sequelize.define('QuizQuestion', {
5 | id: {
6 | type: DataTypes.INTEGER,
7 | primaryKey: true,
8 | autoIncrement: true
9 | },
10 | quizId: {
11 | type: DataTypes.INTEGER,
12 | allowNull: false,
13 | field: 'quiz_id',
14 | references: {
15 | model: 'quizzes',
16 | key: 'id'
17 | }
18 | },
19 | questionId: {
20 | type: DataTypes.INTEGER,
21 | allowNull: false,
22 | field: 'question_id',
23 | references: {
24 | model: 'questions',
25 | key: 'id'
26 | }
27 | },
28 | isCorrect: {
29 | type: DataTypes.BOOLEAN,
30 | allowNull: true,
31 | field: 'is_correct'
32 | },
33 | timeSpent: {
34 | type: DataTypes.INTEGER,
35 | defaultValue: 0,
36 | field: 'time_spent'
37 | },
38 | answeredAt: {
39 | type: DataTypes.DATE,
40 | allowNull: true,
41 | field: 'answered_at'
42 | }
43 | }, {
44 | tableName: 'quiz_questions',
45 | timestamps: false,
46 | underscored: true,
47 | indexes: [
48 | { fields: ['quiz_id'] },
49 | { fields: ['question_id'] }
50 | ]
51 | });
52 |
53 | module.exports = QuizQuestion;
54 |
--------------------------------------------------------------------------------
/config/database.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | /**
4 | * Legacy MongoDB connection helper.
5 | *
6 | * The main application now uses MySQL via Sequelize. This function is kept
7 | * only so older scripts or deployments that still import `config/database`
8 | * do not crash when `MONGODB_URI` is missing.
9 | *
10 | * Behaviour:
11 | * - If `DISABLE_MONGO` is set to `true` (string) OR `MONGODB_URI` is absent,
12 | * this becomes a no-op and simply logs that Mongo is skipped.
13 | * - Otherwise, it attempts a normal mongoose connection.
14 | */
15 | const connectDB = async () => {
16 | const disableMongo = String(process.env.DISABLE_MONGO || '').toLowerCase() === 'true';
17 | const uri = process.env.MONGODB_URI;
18 |
19 | if (disableMongo || !uri) {
20 | console.log('ℹ️ MongoDB connection skipped (legacy). Using MySQL only.');
21 | return null;
22 | }
23 |
24 | try {
25 | const conn = await mongoose.connect(uri);
26 | console.log(`✅ MongoDB Connected (legacy): ${conn.connection.host}`);
27 | return conn;
28 | } catch (error) {
29 | console.error(`❌ MongoDB Connection Error (legacy): ${error.message}`);
30 | // Do not crash the process in MySQL-only mode; just log the failure.
31 | return null;
32 | }
33 | };
34 |
35 | module.exports = connectDB;
--------------------------------------------------------------------------------
/models/mysql/QuestionTranslation.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../../config/mysqlDatabase');
3 |
4 | const QuestionTranslation = sequelize.define('QuestionTranslation', {
5 | id: {
6 | type: DataTypes.INTEGER,
7 | primaryKey: true,
8 | autoIncrement: true
9 | },
10 | questionId: {
11 | type: DataTypes.INTEGER,
12 | allowNull: false,
13 | field: 'question_id',
14 | references: {
15 | model: 'questions',
16 | key: 'id'
17 | }
18 | },
19 | language: {
20 | type: DataTypes.STRING(10),
21 | allowNull: false,
22 | validate: {
23 | notEmpty: true
24 | }
25 | },
26 | questionText: {
27 | type: DataTypes.TEXT,
28 | allowNull: false,
29 | field: 'question_text',
30 | validate: {
31 | notEmpty: {
32 | msg: 'Question text is required'
33 | }
34 | }
35 | },
36 | explanation: {
37 | type: DataTypes.TEXT,
38 | allowNull: true
39 | }
40 | }, {
41 | tableName: 'question_translations',
42 | timestamps: false,
43 | underscored: true,
44 | indexes: [
45 | {
46 | unique: true,
47 | fields: ['question_id', 'language'],
48 | name: 'unique_question_language'
49 | }
50 | ]
51 | });
52 |
53 | module.exports = QuestionTranslation;
54 |
--------------------------------------------------------------------------------
/models/mysql/DifficultyProgress.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../../config/mysqlDatabase');
3 |
4 | const DifficultyProgress = sequelize.define('DifficultyProgress', {
5 | id: {
6 | type: DataTypes.INTEGER,
7 | primaryKey: true,
8 | autoIncrement: true
9 | },
10 | progressId: {
11 | type: DataTypes.INTEGER,
12 | allowNull: false,
13 | field: 'progress_id',
14 | references: {
15 | model: 'progress',
16 | key: 'id'
17 | }
18 | },
19 | difficulty: {
20 | type: DataTypes.STRING(50),
21 | allowNull: false
22 | },
23 | questionsAttempted: {
24 | type: DataTypes.INTEGER,
25 | defaultValue: 0,
26 | field: 'questions_attempted'
27 | },
28 | questionsCorrect: {
29 | type: DataTypes.INTEGER,
30 | defaultValue: 0,
31 | field: 'questions_correct'
32 | },
33 | averageScore: {
34 | type: DataTypes.DECIMAL(5, 2),
35 | defaultValue: 0,
36 | field: 'average_score'
37 | }
38 | }, {
39 | tableName: 'difficulty_progress',
40 | timestamps: false,
41 | underscored: true,
42 | indexes: [
43 | {
44 | unique: true,
45 | fields: ['progress_id', 'difficulty'],
46 | name: 'unique_progress_difficulty'
47 | }
48 | ]
49 | });
50 |
51 | module.exports = DifficultyProgress;
52 |
--------------------------------------------------------------------------------
/utils/helpers.js:
--------------------------------------------------------------------------------
1 | exports.shuffleArray = (array) => {
2 | const shuffled = [...array];
3 | for (let i = shuffled.length - 1; i > 0; i--) {
4 | const j = Math.floor(Math.random() * (i + 1));
5 | [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
6 | }
7 | return shuffled;
8 | };
9 |
10 | exports.calculatePercentage = (correct, total) => {
11 | if (total === 0) return 0;
12 | return Math.round((correct / total) * 100);
13 | };
14 |
15 | exports.formatDuration = (seconds) => {
16 | const hours = Math.floor(seconds / 3600);
17 | const minutes = Math.floor((seconds % 3600) / 60);
18 | const secs = seconds % 60;
19 |
20 | if (hours > 0) {
21 | return `${hours}h ${minutes}m ${secs}s`;
22 | } else if (minutes > 0) {
23 | return `${minutes}m ${secs}s`;
24 | }
25 | return `${secs}s`;
26 | };
27 |
28 | exports.getDateRange = (period) => {
29 | const now = new Date();
30 | const ranges = {
31 | today: new Date(now.setHours(0, 0, 0, 0)),
32 | week: new Date(now.setDate(now.getDate() - 7)),
33 | month: new Date(now.setMonth(now.getMonth() - 1)),
34 | year: new Date(now.setFullYear(now.getFullYear() - 1))
35 | };
36 | return ranges[period] || null;
37 | };
38 |
39 | exports.sanitizeUser = (user) => {
40 | const { password, ...sanitized } = user.toObject();
41 | return sanitized;
42 | };
--------------------------------------------------------------------------------
/utils/validators.js:
--------------------------------------------------------------------------------
1 | const { body } = require('express-validator');
2 |
3 | exports.questionValidation = [
4 | body('questionText').notEmpty().withMessage('Question text is required'),
5 | body('type').isIn(['single-choice', 'multiple-choice', 'true-false']).withMessage('Invalid question type'),
6 | body('options').isArray({ min: 2 }).withMessage('At least 2 options are required'),
7 | body('category').notEmpty().withMessage('Category is required'),
8 | body('difficulty').optional().isIn(['foundation', 'advanced', 'expert']).withMessage('Invalid difficulty')
9 | ];
10 |
11 | exports.quizValidation = [
12 | body('mode').isIn(['practice', 'exam', 'timed', 'category']).withMessage('Invalid quiz mode'),
13 | body('numberOfQuestions').optional().isInt({ min: 1, max: 100 }).withMessage('Number of questions must be between 1 and 100')
14 | ];
15 |
16 | exports.achievementValidation = [
17 | body('name').notEmpty().withMessage('Achievement name is required'),
18 | body('description').notEmpty().withMessage('Achievement description is required'),
19 | body('type').isIn(['quiz', 'streak', 'score', 'category', 'special']).withMessage('Invalid achievement type'),
20 | body('criteria.metric').notEmpty().withMessage('Achievement metric is required'),
21 | body('criteria.threshold').isNumeric().withMessage('Threshold must be a number')
22 | ];
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | NODE_ENV=development
2 | PORT=2001
3 |
4 | # Database
5 | # MONGODB_URI=mongodb://localhost:27017/istqb_practice
6 | # For MongoDB Atlas: mongodb+srv://username:password@cluster.mongodb.net/istqb_practice
7 | MONGODB_URI=mongodb+srv://istqb_opensource:cSAib3OUQhW3dA5B@istqbpractice.el6nw8z.mongodb.net/?appName=istqbpractice
8 |
9 | # JWT Secret
10 | JWT_SECRET=your_super_secret_jwt_key_change_this_in_production
11 | # JWT_EXPIRE: Token expiration time
12 | # Recommended: 7d (7 days) for good balance between security and UX
13 | # Options: 1h, 24h, 7d, 30d
14 | JWT_EXPIRE=7d
15 |
16 | # CORS
17 | CORS_ORIGIN=http://localhost:5173
18 |
19 | # Rate Limiting
20 | RATE_LIMIT_WINDOW_MS=9000
21 | RATE_LIMIT_MAX_REQUESTS=100
22 |
23 | # Email Configuration (Ethereal Email for Testing)
24 | # Run: node scripts/setupEtherealEmail.js to generate credentials
25 | SMTP_HOST=smtp.ethereal.email
26 | SMTP_PORT=587
27 | SMTP_SECURE=false
28 | SMTP_USER=your-ethereal-username@ethereal.email
29 | SMTP_PASS=your-ethereal-password
30 | SMTP_FROM="Test Automation Arena "
31 |
32 | # Frontend URL (for email verification links)
33 | FRONTEND_URL=http://localhost:5173
34 |
35 | # Seed accounts
36 | ADMIN_EMAIL=admin@istqb.com
37 | ADMIN_PASSWORD=Admin123!
38 | TEST_USER_USERNAME=testuser
39 | TEST_USER_EMAIL=test@example.com
40 | TEST_USER_PASSWORD=Test123!
--------------------------------------------------------------------------------
/models/mysql/Progress.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../../config/mysqlDatabase');
3 |
4 | const Progress = sequelize.define('Progress', {
5 | id: {
6 | type: DataTypes.INTEGER,
7 | primaryKey: true,
8 | autoIncrement: true
9 | },
10 | userId: {
11 | type: DataTypes.INTEGER,
12 | allowNull: false,
13 | unique: true,
14 | field: 'user_id',
15 | references: {
16 | model: 'users',
17 | key: 'id'
18 | }
19 | },
20 |
21 | // Study Streak
22 | currentStreak: {
23 | type: DataTypes.INTEGER,
24 | defaultValue: 0,
25 | field: 'current_streak'
26 | },
27 | longestStreak: {
28 | type: DataTypes.INTEGER,
29 | defaultValue: 0,
30 | field: 'longest_streak'
31 | },
32 | lastStudyDate: {
33 | type: DataTypes.DATEONLY,
34 | allowNull: true,
35 | field: 'last_study_date'
36 | },
37 |
38 | totalTimeSpent: {
39 | type: DataTypes.INTEGER,
40 | defaultValue: 0,
41 | field: 'total_time_spent'
42 | },
43 | lastUpdated: {
44 | type: DataTypes.DATE,
45 | defaultValue: DataTypes.NOW,
46 | field: 'last_updated'
47 | }
48 | }, {
49 | tableName: 'progress',
50 | timestamps: true,
51 | underscored: true,
52 | indexes: [
53 | { fields: ['user_id'], unique: true }
54 | ]
55 | });
56 |
57 | module.exports = Progress;
58 |
--------------------------------------------------------------------------------
/config/mysqlDatabase.js:
--------------------------------------------------------------------------------
1 | const { Sequelize } = require('sequelize');
2 | const config = require('./mysql.config');
3 |
4 | const env = process.env.NODE_ENV || 'development';
5 | const dbConfig = config[env];
6 |
7 | // Create Sequelize instance
8 | const sequelize = new Sequelize(
9 | dbConfig.database,
10 | dbConfig.username,
11 | dbConfig.password,
12 | {
13 | host: dbConfig.host,
14 | port: dbConfig.port,
15 | dialect: dbConfig.dialect,
16 | logging: dbConfig.logging,
17 | pool: dbConfig.pool,
18 | define: dbConfig.define
19 | }
20 | );
21 |
22 | // Test connection
23 | const testConnection = async () => {
24 | try {
25 | await sequelize.authenticate();
26 | console.log('✅ MySQL Connection has been established successfully.');
27 | return true;
28 | } catch (error) {
29 | console.error('❌ Unable to connect to MySQL database:', error.message);
30 | return false;
31 | }
32 | };
33 |
34 | // Sync database (create tables if they don't exist)
35 | const syncDatabase = async (options = {}) => {
36 | try {
37 | await sequelize.sync(options);
38 | console.log('✅ MySQL Database synchronized successfully.');
39 | } catch (error) {
40 | console.error('❌ Error synchronizing MySQL database:', error.message);
41 | throw error;
42 | }
43 | };
44 |
45 | module.exports = {
46 | sequelize,
47 | testConnection,
48 | syncDatabase,
49 | Sequelize
50 | };
51 |
--------------------------------------------------------------------------------
/models/mysql/CategoryProgress.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../../config/mysqlDatabase');
3 |
4 | const CategoryProgress = sequelize.define('CategoryProgress', {
5 | id: {
6 | type: DataTypes.INTEGER,
7 | primaryKey: true,
8 | autoIncrement: true
9 | },
10 | progressId: {
11 | type: DataTypes.INTEGER,
12 | allowNull: false,
13 | field: 'progress_id',
14 | references: {
15 | model: 'progress',
16 | key: 'id'
17 | }
18 | },
19 | category: {
20 | type: DataTypes.STRING(100),
21 | allowNull: false
22 | },
23 | questionsAttempted: {
24 | type: DataTypes.INTEGER,
25 | defaultValue: 0,
26 | field: 'questions_attempted'
27 | },
28 | questionsCorrect: {
29 | type: DataTypes.INTEGER,
30 | defaultValue: 0,
31 | field: 'questions_correct'
32 | },
33 | averageScore: {
34 | type: DataTypes.DECIMAL(5, 2),
35 | defaultValue: 0,
36 | field: 'average_score'
37 | },
38 | lastAttempted: {
39 | type: DataTypes.DATE,
40 | allowNull: true,
41 | field: 'last_attempted'
42 | }
43 | }, {
44 | tableName: 'category_progress',
45 | timestamps: false,
46 | underscored: true,
47 | indexes: [
48 | {
49 | unique: true,
50 | fields: ['progress_id', 'category'],
51 | name: 'unique_progress_category'
52 | }
53 | ]
54 | });
55 |
56 | module.exports = CategoryProgress;
57 |
--------------------------------------------------------------------------------
/client/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client_backup/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/models/mysql/Achievement.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../../config/mysqlDatabase');
3 |
4 | const Achievement = sequelize.define('Achievement', {
5 | id: {
6 | type: DataTypes.INTEGER,
7 | primaryKey: true,
8 | autoIncrement: true
9 | },
10 | icon: {
11 | type: DataTypes.STRING(255),
12 | allowNull: false
13 | },
14 | type: {
15 | type: DataTypes.ENUM('quiz', 'streak', 'score', 'category', 'special'),
16 | allowNull: false
17 | },
18 |
19 | // Criteria
20 | criteriaMetric: {
21 | type: DataTypes.STRING(100),
22 | allowNull: false,
23 | field: 'criteria_metric'
24 | },
25 | criteriaThreshold: {
26 | type: DataTypes.INTEGER,
27 | allowNull: false,
28 | field: 'criteria_threshold'
29 | },
30 | criteriaCategory: {
31 | type: DataTypes.STRING(100),
32 | allowNull: true,
33 | field: 'criteria_category'
34 | },
35 |
36 | rarity: {
37 | type: DataTypes.ENUM('common', 'rare', 'epic', 'legendary'),
38 | defaultValue: 'common'
39 | },
40 | points: {
41 | type: DataTypes.INTEGER,
42 | defaultValue: 10
43 | },
44 | isActive: {
45 | type: DataTypes.BOOLEAN,
46 | defaultValue: true,
47 | field: 'is_active'
48 | }
49 | }, {
50 | tableName: 'achievements',
51 | timestamps: true,
52 | underscored: true,
53 | indexes: [
54 | { fields: ['type'] },
55 | { fields: ['is_active'] }
56 | ]
57 | });
58 |
59 | module.exports = Achievement;
60 |
--------------------------------------------------------------------------------
/client_backup/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint .",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "@radix-ui/react-avatar": "^1.1.10",
14 | "@radix-ui/react-dialog": "^1.1.15",
15 | "@radix-ui/react-dropdown-menu": "^2.1.16",
16 | "@radix-ui/react-label": "^2.1.7",
17 | "@radix-ui/react-progress": "^1.1.7",
18 | "@radix-ui/react-select": "^2.2.6",
19 | "@radix-ui/react-slot": "^1.2.3",
20 | "@radix-ui/react-tabs": "^1.1.13",
21 | "@radix-ui/react-toast": "^1.2.15",
22 | "axios": "^1.13.1",
23 | "class-variance-authority": "^0.7.1",
24 | "clsx": "^2.1.1",
25 | "lucide-react": "^0.552.0",
26 | "prop-types": "^15.8.1",
27 | "react": "^19.1.1",
28 | "react-dom": "^19.1.1",
29 | "react-router-dom": "^7.9.5",
30 | "tailwind-merge": "^3.3.1",
31 | "tailwindcss-animate": "^1.0.7"
32 | },
33 | "devDependencies": {
34 | "@eslint/js": "^9.36.0",
35 | "@tailwindcss/postcss": "^4.1.17",
36 | "@types/react": "^19.1.16",
37 | "@types/react-dom": "^19.1.9",
38 | "@vitejs/plugin-react": "^5.0.4",
39 | "autoprefixer": "^10.4.21",
40 | "eslint": "^9.36.0",
41 | "eslint-plugin-react-hooks": "^5.2.0",
42 | "eslint-plugin-react-refresh": "^0.4.22",
43 | "globals": "^16.4.0",
44 | "postcss": "^8.5.6",
45 | "tailwindcss": "^4.1.16",
46 | "vite": "^6.0.0"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/routes/questionUpload.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const multer = require('multer');
3 | const {
4 | uploadQuestionsFromExcel,
5 | downloadTemplate,
6 | getUploadStats,
7 | getCategories
8 | } = require('../controllers/questionUploadController');
9 | const { protect, authorize } = require('../middleware/auth');
10 |
11 | const router = express.Router();
12 |
13 | // Configure multer for memory storage
14 | const storage = multer.memoryStorage();
15 | const upload = multer({
16 | storage,
17 | limits: {
18 | fileSize: 10 * 1024 * 1024 // 10MB limit
19 | },
20 | fileFilter: (req, file, cb) => {
21 | // Accept only Excel files
22 | if (
23 | file.mimetype === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
24 | file.mimetype === 'application/vnd.ms-excel' ||
25 | file.originalname.endsWith('.xlsx') ||
26 | file.originalname.endsWith('.xls')
27 | ) {
28 | cb(null, true);
29 | } else {
30 | cb(new Error('Only Excel files (.xlsx, .xls) are allowed'), false);
31 | }
32 | }
33 | });
34 |
35 | // Public routes
36 | router.get('/categories', getCategories);
37 |
38 | // Protected routes (admin only)
39 | router.use(protect);
40 | router.use(authorize('admin'));
41 |
42 | // Upload questions from Excel
43 | router.post('/upload', upload.single('file'), uploadQuestionsFromExcel);
44 |
45 | // Download template
46 | router.get('/template', downloadTemplate);
47 |
48 | // Get upload statistics
49 | router.get('/stats', getUploadStats);
50 |
51 | module.exports = router;
52 |
--------------------------------------------------------------------------------
/models/FunctionalBugStats.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const functionalBugStatsSchema = new mongoose.Schema({
4 | bugId: {
5 | type: String,
6 | required: true,
7 | unique: true
8 | },
9 | totalAttempts: {
10 | type: Number,
11 | default: 0
12 | },
13 | totalCompletions: {
14 | type: Number,
15 | default: 0
16 | },
17 | correctIdentifications: {
18 | type: Number,
19 | default: 0
20 | },
21 | averageTimeSpent: {
22 | type: Number,
23 | default: 0
24 | },
25 | averageHintsUsed: {
26 | type: Number,
27 | default: 0
28 | },
29 | successRate: {
30 | type: Number,
31 | default: 0
32 | },
33 | updatedAt: {
34 | type: Date,
35 | default: Date.now
36 | }
37 | });
38 |
39 | // Method to update stats
40 | functionalBugStatsSchema.methods.updateStats = async function(isCorrect, timeSpent, hintsUsed) {
41 | this.totalAttempts += 1;
42 | this.totalCompletions += 1;
43 |
44 | if (isCorrect) {
45 | this.correctIdentifications += 1;
46 | }
47 |
48 | // Update averages
49 | this.averageTimeSpent = ((this.averageTimeSpent * (this.totalCompletions - 1)) + timeSpent) / this.totalCompletions;
50 | this.averageHintsUsed = ((this.averageHintsUsed * (this.totalCompletions - 1)) + hintsUsed) / this.totalCompletions;
51 | this.successRate = (this.correctIdentifications / this.totalCompletions) * 100;
52 | this.updatedAt = Date.now();
53 |
54 | await this.save();
55 | };
56 |
57 | module.exports = mongoose.model('FunctionalBugStats', functionalBugStatsSchema);
58 |
--------------------------------------------------------------------------------
/client/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | darkMode: ["class"],
4 | content: [
5 | "./index.html",
6 | "./src/**/*.{js,ts,jsx,tsx}",
7 | ],
8 | theme: {
9 | extend: {
10 | colors: {
11 | border: "hsl(var(--border))",
12 | input: "hsl(var(--input))",
13 | ring: "hsl(var(--ring))",
14 | background: "hsl(var(--background))",
15 | foreground: "hsl(var(--foreground))",
16 | primary: {
17 | DEFAULT: "hsl(var(--primary))",
18 | foreground: "hsl(var(--primary-foreground))",
19 | },
20 | secondary: {
21 | DEFAULT: "hsl(var(--secondary))",
22 | foreground: "hsl(var(--secondary-foreground))",
23 | },
24 | destructive: {
25 | DEFAULT: "hsl(var(--destructive))",
26 | foreground: "hsl(var(--destructive-foreground))",
27 | },
28 | muted: {
29 | DEFAULT: "hsl(var(--muted))",
30 | foreground: "hsl(var(--muted-foreground))",
31 | },
32 | accent: {
33 | DEFAULT: "hsl(var(--accent))",
34 | foreground: "hsl(var(--accent-foreground))",
35 | },
36 | card: {
37 | DEFAULT: "hsl(var(--card))",
38 | foreground: "hsl(var(--card-foreground))",
39 | },
40 | },
41 | borderRadius: {
42 | lg: "var(--radius)",
43 | md: "calc(var(--radius) - 2px)",
44 | sm: "calc(var(--radius) - 4px)",
45 | },
46 | },
47 | },
48 | plugins: [require("tailwindcss-animate")],
49 | }
--------------------------------------------------------------------------------
/client_backup/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | darkMode: ["class"],
4 | content: [
5 | "./index.html",
6 | "./src/**/*.{js,ts,jsx,tsx}",
7 | ],
8 | theme: {
9 | extend: {
10 | colors: {
11 | border: "hsl(var(--border))",
12 | input: "hsl(var(--input))",
13 | ring: "hsl(var(--ring))",
14 | background: "hsl(var(--background))",
15 | foreground: "hsl(var(--foreground))",
16 | primary: {
17 | DEFAULT: "hsl(var(--primary))",
18 | foreground: "hsl(var(--primary-foreground))",
19 | },
20 | secondary: {
21 | DEFAULT: "hsl(var(--secondary))",
22 | foreground: "hsl(var(--secondary-foreground))",
23 | },
24 | destructive: {
25 | DEFAULT: "hsl(var(--destructive))",
26 | foreground: "hsl(var(--destructive-foreground))",
27 | },
28 | muted: {
29 | DEFAULT: "hsl(var(--muted))",
30 | foreground: "hsl(var(--muted-foreground))",
31 | },
32 | accent: {
33 | DEFAULT: "hsl(var(--accent))",
34 | foreground: "hsl(var(--accent-foreground))",
35 | },
36 | card: {
37 | DEFAULT: "hsl(var(--card))",
38 | foreground: "hsl(var(--card-foreground))",
39 | },
40 | },
41 | borderRadius: {
42 | lg: "var(--radius)",
43 | md: "calc(var(--radius) - 2px)",
44 | sm: "calc(var(--radius) - 4px)",
45 | },
46 | },
47 | },
48 | plugins: [require("tailwindcss-animate")],
49 | }
--------------------------------------------------------------------------------
/routes/auth.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const { body } = require('express-validator');
4 | const { register, login, getMe, updateProfile, changePassword, refreshToken, extendToken } = require('../controllers/authController');
5 | const { protect } = require('../middleware/auth');
6 | const { validate } = require('../middleware/validation');
7 | const { authLimiter } = require('../middleware/rateLimiter');
8 |
9 | router.post('/register', authLimiter, [
10 | body('username').trim().isLength({ min: 3, max: 30 }).withMessage('Username must be 3-30 characters'),
11 | body('email').isEmail().normalizeEmail().withMessage('Valid email is required'),
12 | body('password').isLength({ min: 6 }).withMessage('Password must be at least 6 characters'),
13 | validate
14 | ], register);
15 |
16 | router.post('/login', authLimiter, [
17 | body('email').isEmail().normalizeEmail().withMessage('Valid email is required'),
18 | body('password').notEmpty().withMessage('Password is required'),
19 | validate
20 | ], login);
21 |
22 | router.get('/me', protect, getMe);
23 | router.put('/profile', protect, updateProfile);
24 | router.put('/change-password', protect, [
25 | body('currentPassword').notEmpty().withMessage('Current password is required'),
26 | body('newPassword').isLength({ min: 6 }).withMessage('New password must be at least 6 characters'),
27 | validate
28 | ], changePassword);
29 |
30 | // Token management routes
31 | router.post('/refresh', protect, refreshToken);
32 | router.post('/extend', protect, extendToken);
33 |
34 | module.exports = router;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "istqb-practice-backend",
3 | "version": "0.1.0",
4 | "description": "Backend API for ISTQB Practice Q&A Platform",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "node server.js",
8 | "dev": "nodemon server.js",
9 | "test": "jest --coverage --detectOpenHandles",
10 | "test:watch": "source /home/korrektt/nodevenv/qaarena/24/bin/activate && cd /home/korrektt/qaarena",
11 | "seed": "node scripts/seedDatabase.js",
12 | "setup-email": "node scripts/setupEtherealEmail.js"
13 | },
14 | "keywords": [
15 | "istqb",
16 | "testing",
17 | "certification",
18 | "quiz",
19 | "education"
20 | ],
21 | "author": "ISTQB Community",
22 | "license": "MIT",
23 | "dependencies": {
24 | "axios": "^1.13.2",
25 | "bcryptjs": "^2.4.3",
26 | "compression": "^1.7.4",
27 | "cors": "^2.8.5",
28 | "dotenv": "^16.3.1",
29 | "express": "^4.18.2",
30 | "express-rate-limit": "^7.1.5",
31 | "express-validator": "^7.0.1",
32 | "helmet": "^7.1.0",
33 | "jsonwebtoken": "^9.0.2",
34 | "mongoose": "^8.0.3",
35 | "morgan": "^1.10.0",
36 | "multer": "^2.0.2",
37 | "mysql2": "^3.15.3",
38 | "nodemailer": "^7.0.11",
39 | "sequelize": "^6.37.7",
40 | "sequelize-cli": "^6.6.3",
41 | "xlsx": "^0.18.5"
42 | },
43 | "devDependencies": {
44 | "jest": "^29.7.0",
45 | "nodemon": "^3.0.2",
46 | "supertest": "^6.3.3"
47 | },
48 | "jest": {
49 | "testEnvironment": "node",
50 | "coveragePathIgnorePatterns": [
51 | "/node_modules/"
52 | ],
53 | "testTimeout": 30000
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "preview": "vite preview",
10 | "build:prod": "vite build --mode production",
11 | "preview:prod": "vite preview --port 4173"
12 | },
13 | "dependencies": {
14 | "@radix-ui/react-avatar": "^1.1.10",
15 | "@radix-ui/react-dialog": "^1.1.15",
16 | "@radix-ui/react-dropdown-menu": "^2.1.16",
17 | "@radix-ui/react-label": "^2.1.7",
18 | "@radix-ui/react-progress": "^1.1.7",
19 | "@radix-ui/react-select": "^2.2.6",
20 | "@radix-ui/react-slot": "^1.2.3",
21 | "@radix-ui/react-tabs": "^1.1.13",
22 | "@radix-ui/react-toast": "^1.2.15",
23 | "@tanstack/react-query": "^5.90.12",
24 | "axios": "^1.13.1",
25 | "class-variance-authority": "^0.7.1",
26 | "clsx": "^2.1.1",
27 | "lucide-react": "^0.552.0",
28 | "prop-types": "^15.8.1",
29 | "react": "^19.1.1",
30 | "react-dom": "^19.1.1",
31 | "react-router-dom": "^7.9.5",
32 | "tailwind-merge": "^3.3.1",
33 | "tailwindcss-animate": "^1.0.7"
34 | },
35 | "devDependencies": {
36 | "@eslint/js": "^9.36.0",
37 | "@types/react": "^19.1.16",
38 | "@types/react-dom": "^19.1.9",
39 | "@vitejs/plugin-react": "^5.0.4",
40 | "autoprefixer": "^10.4.21",
41 | "eslint": "^9.36.0",
42 | "eslint-plugin-react-hooks": "^5.2.0",
43 | "eslint-plugin-react-refresh": "^0.4.22",
44 | "globals": "^16.4.0",
45 | "postcss": "^8.5.6",
46 | "tailwindcss": "^3.4.18",
47 | "vite": "^7.1.7"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/middleware/rateLimiter.js:
--------------------------------------------------------------------------------
1 | const rateLimit = require('express-rate-limit');
2 |
3 | const isProd = process.env.NODE_ENV === 'production';
4 |
5 | exports.apiLimiter = rateLimit({
6 | windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) || 15 * 60 * 1000,
7 | max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS, 10) || 100,
8 | message: {
9 | status: 'error',
10 | message: 'Too many requests from this IP, please try again later'
11 | },
12 | standardHeaders: true,
13 | legacyHeaders: false,
14 | });
15 |
16 | // Auth limiter configuration (can be tuned/disabled via env)
17 | const rawAuthWindow = parseInt(process.env.AUTH_RATE_LIMIT_WINDOW_MS, 10);
18 | const rawAuthMax = parseInt(process.env.AUTH_RATE_LIMIT_MAX_REQUESTS, 10);
19 | const authLimitDisabled = process.env.AUTH_RATE_LIMIT_DISABLED === 'true';
20 |
21 | const authWindowMs = Number.isNaN(rawAuthWindow)
22 | ? (isProd ? 15 * 60 * 1000 : 60 * 1000)
23 | : rawAuthWindow;
24 |
25 | const authMax = Number.isNaN(rawAuthMax)
26 | ? (isProd ? 5 : 1000)
27 | : rawAuthMax;
28 |
29 | exports.authLimiter = authLimitDisabled
30 | ? (req, res, next) => next()
31 | : rateLimit({
32 | windowMs: authWindowMs,
33 | max: authMax,
34 | message: {
35 | status: 'error',
36 | message: 'Too many authentication attempts, please try again later'
37 | },
38 | skipSuccessfulRequests: true,
39 | });
40 |
41 | exports.quizLimiter = rateLimit({
42 | windowMs: 60 * 1000,
43 | max: 100, // Increased for testing
44 | message: {
45 | status: 'error',
46 | message: 'Too many quiz submissions, please slow down'
47 | },
48 | });
--------------------------------------------------------------------------------
/config/mysql.config.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 |
3 | module.exports = {
4 | development: {
5 | username: process.env.MYSQL_USER || 'root',
6 | password: process.env.MYSQL_PASSWORD || '',
7 | database: process.env.MYSQL_DATABASE || 'qa_arena',
8 | host: process.env.MYSQL_HOST || 'localhost',
9 | port: process.env.MYSQL_PORT || 3306,
10 | dialect: 'mysql',
11 | logging: console.log,
12 | pool: {
13 | max: 10,
14 | min: 0,
15 | acquire: 60000,
16 | idle: 10000,
17 | evict: 10000,
18 | handleDisconnects: true
19 | },
20 | define: {
21 | timestamps: true,
22 | underscored: true,
23 | freezeTableName: true
24 | }
25 | },
26 | test: {
27 | username: process.env.MYSQL_USER || 'root',
28 | password: process.env.MYSQL_PASSWORD || '',
29 | database: process.env.MYSQL_TEST_DATABASE || 'qa_arena_test',
30 | host: process.env.MYSQL_HOST || 'localhost',
31 | port: process.env.MYSQL_PORT || 3306,
32 | dialect: 'mysql',
33 | logging: false,
34 | pool: {
35 | max: 5,
36 | min: 0,
37 | acquire: 30000,
38 | idle: 10000
39 | }
40 | },
41 | production: {
42 | username: process.env.MYSQL_USER,
43 | password: process.env.MYSQL_PASSWORD,
44 | database: process.env.MYSQL_DATABASE,
45 | host: process.env.MYSQL_HOST,
46 | port: process.env.MYSQL_PORT || 3306,
47 | dialect: 'mysql',
48 | logging: false,
49 | pool: {
50 | max: 10,
51 | min: 2,
52 | acquire: 30000,
53 | idle: 10000
54 | },
55 | define: {
56 | timestamps: true,
57 | underscored: true,
58 | freezeTableName: true
59 | }
60 | }
61 | };
62 |
--------------------------------------------------------------------------------
/models/UserFunctionalBugProgress.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const userFunctionalBugProgressSchema = new mongoose.Schema({
4 | user: {
5 | type: mongoose.Schema.Types.ObjectId,
6 | ref: 'User',
7 | required: true
8 | },
9 | bugId: {
10 | type: String,
11 | required: true
12 | },
13 | domain: {
14 | type: String,
15 | required: true,
16 | enum: ['fintech', 'ecommerce', 'ordering', 'grading']
17 | },
18 | difficulty: {
19 | type: String,
20 | enum: ['beginner', 'intermediate', 'advanced']
21 | },
22 | attempts: {
23 | type: Number,
24 | default: 0
25 | },
26 | completed: {
27 | type: Boolean,
28 | default: false
29 | },
30 | pointsEarned: {
31 | type: Number,
32 | default: 0
33 | },
34 | timeSpent: {
35 | type: Number,
36 | default: 0
37 | // in seconds
38 | },
39 | hintsUsed: {
40 | type: Number,
41 | default: 0
42 | },
43 | identifiedCorrectly: {
44 | type: Boolean,
45 | default: false
46 | },
47 | userAnswer: {
48 | bugType: String,
49 | description: String,
50 | confidence: Number,
51 | submittedAt: Date
52 | },
53 | startedAt: {
54 | type: Date,
55 | default: Date.now
56 | },
57 | completedAt: Date,
58 | createdAt: {
59 | type: Date,
60 | default: Date.now
61 | }
62 | });
63 |
64 | // Compound index for efficient queries and uniqueness
65 | userFunctionalBugProgressSchema.index({ user: 1, bugId: 1 }, { unique: true });
66 | userFunctionalBugProgressSchema.index({ user: 1, domain: 1 });
67 | userFunctionalBugProgressSchema.index({ user: 1, completed: 1 });
68 |
69 | module.exports = mongoose.model('UserFunctionalBugProgress', userFunctionalBugProgressSchema);
70 |
--------------------------------------------------------------------------------
/client/src/components/ui/card.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cn } from "../../lib/utils"
3 |
4 | const Card = React.forwardRef(({ className, ...props }, ref) => (
5 |
13 | ))
14 | Card.displayName = "Card"
15 |
16 | const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
17 |
22 | ))
23 | CardHeader.displayName = "CardHeader"
24 |
25 | const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
26 |
34 | ))
35 | CardTitle.displayName = "CardTitle"
36 |
37 | const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
38 |
43 | ))
44 | CardDescription.displayName = "CardDescription"
45 |
46 | const CardContent = React.forwardRef(({ className, ...props }, ref) => (
47 |
48 | ))
49 | CardContent.displayName = "CardContent"
50 |
51 | const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
52 |
57 | ))
58 | CardFooter.displayName = "CardFooter"
59 |
60 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
61 |
--------------------------------------------------------------------------------
/client/src/components/ui/button.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva } from "class-variance-authority"
4 | import { cn } from "../../lib/utils"
5 |
6 | const buttonVariants = cva(
7 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
12 | destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
13 | outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
14 | secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | ghost: "hover:bg-accent hover:text-accent-foreground",
16 | link: "text-primary underline-offset-4 hover:underline",
17 | },
18 | size: {
19 | default: "h-10 px-4 py-2",
20 | sm: "h-9 rounded-md px-3",
21 | lg: "h-11 rounded-md px-8",
22 | icon: "h-10 w-10",
23 | },
24 | },
25 | defaultVariants: {
26 | variant: "default",
27 | size: "default",
28 | },
29 | }
30 | )
31 |
32 | const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
33 | const Comp = asChild ? Slot : "button"
34 | return (
35 |
40 | )
41 | })
42 | Button.displayName = "Button"
43 |
44 | export { Button, buttonVariants }
45 |
--------------------------------------------------------------------------------
/client_backup/src/components/ui/card.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cn } from "../../lib/utils"
3 |
4 | const Card = React.forwardRef(({ className, ...props }, ref) => (
5 |
13 | ))
14 | Card.displayName = "Card"
15 |
16 | const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
17 |
22 | ))
23 | CardHeader.displayName = "CardHeader"
24 |
25 | const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
26 |
34 | ))
35 | CardTitle.displayName = "CardTitle"
36 |
37 | const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
38 |
43 | ))
44 | CardDescription.displayName = "CardDescription"
45 |
46 | const CardContent = React.forwardRef(({ className, ...props }, ref) => (
47 |
48 | ))
49 | CardContent.displayName = "CardContent"
50 |
51 | const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
52 |
57 | ))
58 | CardFooter.displayName = "CardFooter"
59 |
60 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
61 |
--------------------------------------------------------------------------------
/client_backup/src/components/ui/button.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva } from "class-variance-authority"
4 | import { cn } from "../../lib/utils"
5 |
6 | const buttonVariants = cva(
7 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
12 | destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
13 | outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
14 | secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | ghost: "hover:bg-accent hover:text-accent-foreground",
16 | link: "text-primary underline-offset-4 hover:underline",
17 | },
18 | size: {
19 | default: "h-10 px-4 py-2",
20 | sm: "h-9 rounded-md px-3",
21 | lg: "h-11 rounded-md px-8",
22 | icon: "h-10 w-10",
23 | },
24 | },
25 | defaultVariants: {
26 | variant: "default",
27 | size: "default",
28 | },
29 | }
30 | )
31 |
32 | const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
33 | const Comp = asChild ? Slot : "button"
34 | return (
35 |
40 | )
41 | })
42 | Button.displayName = "Button"
43 |
44 | export { Button, buttonVariants }
45 |
--------------------------------------------------------------------------------
/middleware/errorHandler.js:
--------------------------------------------------------------------------------
1 | const ErrorResponse = require('../utils/errorResponse');
2 |
3 | const errorHandler = (err, req, res, next) => {
4 | let error = { ...err };
5 | error.message = err.message;
6 |
7 | // Log to console for dev
8 | console.error('Error:', err);
9 |
10 | // Mongoose bad ObjectId
11 | if (err.name === 'CastError') {
12 | const message = `Resource not found with id of ${err.value}`;
13 | error = new ErrorResponse(message, 404);
14 | }
15 |
16 | // Mongoose duplicate key
17 | if (err.code === 11000) {
18 | const field = Object.keys(err.keyValue)[0];
19 | const value = err.keyValue[field];
20 | const message = `Duplicate field value: ${field} '${value}' already exists. Please use another value.`;
21 | error = new ErrorResponse(message, 400);
22 | }
23 |
24 | // Mongoose validation error
25 | if (err.name === 'ValidationError') {
26 | const messages = Object.values(err.errors).map(val => val.message);
27 | error = new ErrorResponse(messages.join(', '), 400);
28 | }
29 |
30 | // JWT errors
31 | if (err.name === 'JsonWebTokenError') {
32 | const message = 'Not authorized, token failed';
33 | error = new ErrorResponse(message, 401);
34 | }
35 |
36 | // JWT expired
37 | if (err.name === 'TokenExpiredError') {
38 | const message = 'Your session has expired. Please log in again.';
39 | error = new ErrorResponse(message, 401);
40 | }
41 |
42 | res.status(error.statusCode || 500).json({
43 | success: false,
44 | message: error.message || 'An unexpected error occurred. Please try again later.',
45 | // Only include stack trace in development mode (not in test or production)
46 | ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
47 | });
48 | };
49 |
50 | module.exports = errorHandler;
--------------------------------------------------------------------------------
/seedAchievements.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const Achievement = require('./models/Achievement');
3 | const path = require('path');
4 | require('dotenv').config({ path: path.join(__dirname, '.env') });
5 |
6 | const achievements = [
7 | {
8 | name: { en: 'First Steps' },
9 | description: { en: 'Complete your first quiz' },
10 | icon: 'trophy',
11 | type: 'quiz',
12 | criteria: { metric: 'totalQuizzes', threshold: 1 },
13 | rarity: 'common',
14 | points: 10
15 | },
16 | {
17 | name: { en: 'Getting Started' },
18 | description: { en: 'Complete 5 quizzes' },
19 | icon: 'star',
20 | type: 'quiz',
21 | criteria: { metric: 'totalQuizzes', threshold: 5 },
22 | rarity: 'common',
23 | points: 25
24 | },
25 | {
26 | name: { en: 'Point Collector' },
27 | description: { en: 'Earn a total of 300 points' },
28 | icon: 'award',
29 | type: 'score',
30 | criteria: { metric: 'totalScore', threshold: 300 },
31 | rarity: 'rare',
32 | points: 60
33 | }
34 | ];
35 |
36 | async function seedAchievements() {
37 | try {
38 | const mongoUri = process.env.MONGODB_URI || 'mongodb://localhost:27017/istqb_practice';
39 | console.log('Connecting to MongoDB...');
40 | await mongoose.connect(mongoUri);
41 | console.log('✅ MongoDB Connected');
42 |
43 | // Clear existing achievements
44 | await Achievement.deleteMany({});
45 | console.log('🗑️ Cleared existing achievements');
46 |
47 | // Insert new achievements
48 | await Achievement.insertMany(achievements);
49 | console.log(`✅ Added ${achievements.length} achievements`);
50 |
51 | console.log('\n📋 Achievements created:');
52 | achievements.forEach(a => {
53 | console.log(` - ${a.name.en}: ${a.description.en}`);
54 | });
55 |
56 | process.exit(0);
57 | } catch (error) {
58 | console.error('❌ Error:', error);
59 | process.exit(1);
60 | }
61 | }
62 |
63 | seedAchievements();
64 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 84% 4.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 222.2 84% 4.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 222.2 84% 4.9%;
13 | --primary: 173 80% 40%;
14 | --primary-foreground: 0 0% 100%;
15 | --secondary: 210 40% 96.1%;
16 | --secondary-foreground: 222.2 47.4% 11.2%;
17 | --muted: 210 40% 96.1%;
18 | --muted-foreground: 215.4 16.3% 46.9%;
19 | --accent: 210 40% 96.1%;
20 | --accent-foreground: 222.2 47.4% 11.2%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 210 40% 98%;
23 | --border: 214.3 31.8% 91.4%;
24 | --input: 214.3 31.8% 91.4%;
25 | --ring: 173 80% 40%;
26 | --radius: 0.5rem;
27 | }
28 |
29 | .dark {
30 | --background: 222.2 84% 4.9%;
31 | --foreground: 210 40% 98%;
32 | --card: 222.2 84% 4.9%;
33 | --card-foreground: 210 40% 98%;
34 | --popover: 222.2 84% 4.9%;
35 | --popover-foreground: 210 40% 98%;
36 | --primary: 173 80% 50%;
37 | --primary-foreground: 0 0% 100%;
38 | --secondary: 217.2 32.6% 17.5%;
39 | --secondary-foreground: 210 40% 98%;
40 | --muted: 217.2 32.6% 17.5%;
41 | --muted-foreground: 215 20.2% 65.1%;
42 | --accent: 217.2 32.6% 17.5%;
43 | --accent-foreground: 210 40% 98%;
44 | --destructive: 0 62.8% 30.6%;
45 | --destructive-foreground: 210 40% 98%;
46 | --border: 217.2 32.6% 17.5%;
47 | --input: 217.2 32.6% 17.5%;
48 | --ring: 173 80% 50%;
49 | }
50 | }
51 |
52 | @layer base {
53 | * {
54 | @apply border-border;
55 | }
56 | body {
57 | @apply bg-background text-foreground;
58 | }
59 |
60 | /* Prevent horizontal scrolling */
61 | html, body {
62 | overflow-x: hidden;
63 | max-width: 100vw;
64 | }
65 |
66 | /* Ensure all containers respect viewport width */
67 | * {
68 | box-sizing: border-box;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/client_backup/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 84% 4.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 222.2 84% 4.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 222.2 84% 4.9%;
13 | --primary: 173 80% 40%;
14 | --primary-foreground: 0 0% 100%;
15 | --secondary: 210 40% 96.1%;
16 | --secondary-foreground: 222.2 47.4% 11.2%;
17 | --muted: 210 40% 96.1%;
18 | --muted-foreground: 215.4 16.3% 46.9%;
19 | --accent: 210 40% 96.1%;
20 | --accent-foreground: 222.2 47.4% 11.2%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 210 40% 98%;
23 | --border: 214.3 31.8% 91.4%;
24 | --input: 214.3 31.8% 91.4%;
25 | --ring: 173 80% 40%;
26 | --radius: 0.5rem;
27 | }
28 |
29 | .dark {
30 | --background: 222.2 84% 4.9%;
31 | --foreground: 210 40% 98%;
32 | --card: 222.2 84% 4.9%;
33 | --card-foreground: 210 40% 98%;
34 | --popover: 222.2 84% 4.9%;
35 | --popover-foreground: 210 40% 98%;
36 | --primary: 173 80% 50%;
37 | --primary-foreground: 0 0% 100%;
38 | --secondary: 217.2 32.6% 17.5%;
39 | --secondary-foreground: 210 40% 98%;
40 | --muted: 217.2 32.6% 17.5%;
41 | --muted-foreground: 215 20.2% 65.1%;
42 | --accent: 217.2 32.6% 17.5%;
43 | --accent-foreground: 210 40% 98%;
44 | --destructive: 0 62.8% 30.6%;
45 | --destructive-foreground: 210 40% 98%;
46 | --border: 217.2 32.6% 17.5%;
47 | --input: 217.2 32.6% 17.5%;
48 | --ring: 173 80% 50%;
49 | }
50 | }
51 |
52 | @layer base {
53 | * {
54 | @apply border-border;
55 | }
56 | body {
57 | @apply bg-background text-foreground;
58 | }
59 |
60 | /* Prevent horizontal scrolling */
61 | html, body {
62 | overflow-x: hidden;
63 | max-width: 100vw;
64 | }
65 |
66 | /* Ensure all containers respect viewport width */
67 | * {
68 | box-sizing: border-box;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/middleware/auth.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 | // MySQL Model
3 | const { User } = require('../models/mysql');
4 |
5 | exports.protect = async (req, res, next) => {
6 | try {
7 | let token;
8 | if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
9 | token = req.headers.authorization.split(' ')[1];
10 | }
11 |
12 | if (!token) {
13 | return res.status(401).json({ status: 'error', message: 'Not authorized to access this route' });
14 | }
15 |
16 | try {
17 | const decoded = jwt.verify(token, process.env.JWT_SECRET);
18 | req.user = await User.findByPk(decoded.id);
19 |
20 | if (!req.user) {
21 | return res.status(401).json({ status: 'error', message: 'User not found' });
22 | }
23 |
24 | if (!req.user.isActive) {
25 | return res.status(401).json({ status: 'error', message: 'User account is deactivated' });
26 | }
27 |
28 | next();
29 | } catch (err) {
30 | return res.status(401).json({ status: 'error', message: 'Token is invalid or expired' });
31 | }
32 | } catch (error) {
33 | next(error);
34 | }
35 | };
36 |
37 | exports.authorize = (...roles) => {
38 | return (req, res, next) => {
39 | if (!roles.includes(req.user.role)) {
40 | return res.status(403).json({
41 | status: 'error',
42 | message: `User role '${req.user.role}' is not authorized to access this route`
43 | });
44 | }
45 | next();
46 | };
47 | };
48 |
49 | exports.optionalAuth = async (req, res, next) => {
50 | try {
51 | let token;
52 | if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
53 | token = req.headers.authorization.split(' ')[1];
54 | }
55 |
56 | if (token) {
57 | try {
58 | const decoded = jwt.verify(token, process.env.JWT_SECRET);
59 | req.user = await User.findByPk(decoded.id);
60 | } catch (err) {
61 | req.user = null;
62 | }
63 | }
64 | next();
65 | } catch (error) {
66 | next(error);
67 | }
68 | };
--------------------------------------------------------------------------------
/models/Quiz.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const quizSchema = new mongoose.Schema({
4 | user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
5 | mode: {
6 | type: String,
7 | enum: ['practice', 'exam', 'timed', 'category'],
8 | required: true
9 | },
10 | questions: [{
11 | question: { type: mongoose.Schema.Types.ObjectId, ref: 'Question', required: true },
12 | userAnswer: [{ type: Number }],
13 | isCorrect: Boolean,
14 | timeSpent: { type: Number, default: 0 },
15 | answeredAt: Date
16 | }],
17 | settings: {
18 | language: { type: String, default: 'en' },
19 | category: String,
20 | difficulty: String,
21 | numberOfQuestions: Number,
22 | timeLimit: Number,
23 | randomOrder: { type: Boolean, default: true }
24 | },
25 | score: {
26 | correct: { type: Number, default: 0 },
27 | incorrect: { type: Number, default: 0 },
28 | unanswered: { type: Number, default: 0 },
29 | percentage: { type: Number, default: 0 },
30 | totalPoints: { type: Number, default: 0 }
31 | },
32 | status: {
33 | type: String,
34 | enum: ['in-progress', 'completed', 'abandoned'],
35 | default: 'in-progress'
36 | },
37 | startedAt: { type: Date, default: Date.now },
38 | completedAt: Date,
39 | totalTime: { type: Number, default: 0 }
40 | }, { timestamps: true });
41 |
42 | quizSchema.index({ user: 1, status: 1 });
43 | quizSchema.index({ createdAt: -1 });
44 |
45 | quizSchema.pre('save', function(next) {
46 | if (this.status === 'completed') {
47 | const totalQuestions = this.questions.length;
48 | this.score.correct = this.questions.filter(q => q.isCorrect === true).length;
49 | this.score.incorrect = this.questions.filter(q => q.isCorrect === false).length;
50 | this.score.unanswered = this.questions.filter(q => q.isCorrect === undefined).length;
51 | if (totalQuestions > 0) {
52 | this.score.percentage = Math.round((this.score.correct / totalQuestions) * 100);
53 | }
54 | }
55 | next();
56 | });
57 |
58 | module.exports = mongoose.model('Quiz', quizSchema);
--------------------------------------------------------------------------------
/models/Progress.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const progressSchema = new mongoose.Schema({
4 | user: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, unique: true },
5 | categoryProgress: [{
6 | category: { type: String, required: true },
7 | questionsAttempted: { type: Number, default: 0 },
8 | questionsCorrect: { type: Number, default: 0 },
9 | averageScore: { type: Number, default: 0 },
10 | lastAttempted: Date
11 | }],
12 | difficultyProgress: [{
13 | difficulty: { type: String, required: true },
14 | questionsAttempted: { type: Number, default: 0 },
15 | questionsCorrect: { type: Number, default: 0 },
16 | averageScore: { type: Number, default: 0 }
17 | }],
18 | weakAreas: [{
19 | category: String,
20 | successRate: Number,
21 | needsImprovement: Boolean
22 | }],
23 | strongAreas: [{
24 | category: String,
25 | successRate: Number
26 | }],
27 | studyStreak: {
28 | current: { type: Number, default: 0 },
29 | longest: { type: Number, default: 0 },
30 | lastStudyDate: Date
31 | },
32 | milestones: [{
33 | type: { type: String, required: true },
34 | achieved: { type: Boolean, default: false },
35 | achievedAt: Date,
36 | value: Number
37 | }],
38 | recentActivity: [{
39 | date: { type: Date, default: Date.now },
40 | questionsAnswered: Number,
41 | score: Number,
42 | timeSpent: Number
43 | }],
44 | totalTimeSpent: { type: Number, default: 0 },
45 | lastUpdated: { type: Date, default: Date.now }
46 | }, { timestamps: true });
47 |
48 | progressSchema.methods.updateAreas = function() {
49 | this.weakAreas = this.categoryProgress
50 | .filter(cp => cp.averageScore < 60 && cp.questionsAttempted >= 5)
51 | .map(cp => ({ category: cp.category, successRate: cp.averageScore, needsImprovement: true }))
52 | .sort((a, b) => a.successRate - b.successRate);
53 |
54 | this.strongAreas = this.categoryProgress
55 | .filter(cp => cp.averageScore >= 80 && cp.questionsAttempted >= 5)
56 | .map(cp => ({ category: cp.category, successRate: cp.averageScore }))
57 | .sort((a, b) => b.successRate - a.successRate);
58 | };
59 |
60 | module.exports = mongoose.model('Progress', progressSchema);
--------------------------------------------------------------------------------
/models/FunctionalBug.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const functionalBugSchema = new mongoose.Schema({
4 | bugId: {
5 | type: String,
6 | required: true,
7 | unique: true,
8 | // Format: FB001, FB002, etc.
9 | },
10 | domain: {
11 | type: String,
12 | required: true,
13 | enum: ['fintech', 'ecommerce', 'ordering', 'grading', 'authentication']
14 | },
15 | title: {
16 | type: String,
17 | required: true
18 | },
19 | difficulty: {
20 | type: String,
21 | required: true,
22 | enum: ['beginner', 'intermediate', 'advanced']
23 | },
24 | severity: {
25 | type: String,
26 | required: true,
27 | enum: ['low', 'medium', 'high', 'critical']
28 | },
29 | category: {
30 | type: String,
31 | required: true,
32 | // Business Logic, Calculation Error, Validation Error, etc.
33 | },
34 | scenario: {
35 | description: String,
36 | steps: [String],
37 | initialState: mongoose.Schema.Types.Mixed,
38 | // Initial state for the simulator
39 | },
40 | expected: {
41 | type: String,
42 | required: true
43 | },
44 | actual: {
45 | type: String,
46 | required: true
47 | },
48 | bugType: {
49 | type: String,
50 | required: true
51 | },
52 | rootCause: {
53 | type: String,
54 | required: true
55 | },
56 | fix: {
57 | type: String,
58 | required: true
59 | },
60 | preventionTips: [String],
61 | testingTips: [String],
62 | points: {
63 | type: Number,
64 | default: 100
65 | },
66 | hints: [String],
67 | isActive: {
68 | type: Boolean,
69 | default: true
70 | },
71 | createdBy: {
72 | type: mongoose.Schema.Types.ObjectId,
73 | ref: 'User'
74 | },
75 | createdAt: {
76 | type: Date,
77 | default: Date.now
78 | },
79 | updatedAt: {
80 | type: Date,
81 | default: Date.now
82 | }
83 | });
84 |
85 | // Index for efficient queries
86 | functionalBugSchema.index({ domain: 1, difficulty: 1 });
87 | functionalBugSchema.index({ isActive: 1 });
88 | functionalBugSchema.index({ bugId: 1 });
89 |
90 | // Update timestamp on save
91 | functionalBugSchema.pre('save', function(next) {
92 | this.updatedAt = Date.now();
93 | next();
94 | });
95 |
96 | module.exports = mongoose.model('FunctionalBug', functionalBugSchema);
97 |
--------------------------------------------------------------------------------
/models/Question.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 |
3 | const questionSchema = new mongoose.Schema({
4 | questionText: {
5 | type: Map,
6 | of: String,
7 | required: [true, 'Question text is required']
8 | },
9 | type: {
10 | type: String,
11 | enum: ['single-choice', 'multiple-choice', 'true-false'],
12 | required: [true, 'Question type is required']
13 | },
14 | options: [{
15 | text: { type: Map, of: String, required: true },
16 | isCorrect: { type: Boolean, required: true }
17 | }],
18 | explanation: { type: Map, of: String },
19 | category: {
20 | type: String,
21 | required: [true, 'Category is required'],
22 | lowercase: true,
23 | trim: true
24 | },
25 | difficulty: {
26 | type: String,
27 | enum: ['foundation', 'advanced', 'expert'],
28 | default: 'foundation'
29 | },
30 | syllabus: { type: String, default: 'ISTQB-CTFL-2018' },
31 | tags: [String],
32 | points: { type: Number, default: 1, min: 1, max: 10 },
33 | statistics: {
34 | timesAnswered: { type: Number, default: 0 },
35 | timesCorrect: { type: Number, default: 0 },
36 | averageTime: { type: Number, default: 0 }
37 | },
38 | status: {
39 | type: String,
40 | enum: ['draft', 'published', 'archived', 'flagged'],
41 | default: 'published'
42 | },
43 | createdBy: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
44 | contributors: [{
45 | user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
46 | contribution: String,
47 | date: { type: Date, default: Date.now }
48 | }],
49 | flags: [{
50 | user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
51 | reason: String,
52 | date: { type: Date, default: Date.now }
53 | }],
54 | votes: {
55 | upvotes: { type: Number, default: 0 },
56 | downvotes: { type: Number, default: 0 }
57 | }
58 | }, { timestamps: true });
59 |
60 | questionSchema.index({ category: 1, difficulty: 1, status: 1 });
61 | questionSchema.index({ createdBy: 1 });
62 | questionSchema.index({ tags: 1 });
63 |
64 | questionSchema.virtual('successRate').get(function() {
65 | if (this.statistics.timesAnswered === 0) return 0;
66 | return Math.round((this.statistics.timesCorrect / this.statistics.timesAnswered) * 100);
67 | });
68 |
69 | module.exports = mongoose.model('Question', questionSchema);
--------------------------------------------------------------------------------
/scripts/createDatabase.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const mysql = require('mysql2/promise');
3 |
4 | async function createDatabase() {
5 | console.log('🔧 Creating MySQL Database...\n');
6 |
7 | const config = {
8 | host: process.env.MYSQL_HOST || 'localhost',
9 | port: process.env.MYSQL_PORT || 3306,
10 | user: process.env.MYSQL_USER || 'root',
11 | password: process.env.MYSQL_PASSWORD || ''
12 | };
13 |
14 | const dbName = process.env.MYSQL_DATABASE || 'qa_arena';
15 |
16 | try {
17 | console.log('Step 1: Connecting to MySQL server...');
18 | console.log(` Host: ${config.host}`);
19 | console.log(` Port: ${config.port}`);
20 | console.log(` User: ${config.user}\n`);
21 |
22 | // Connect without specifying database
23 | const connection = await mysql.createConnection(config);
24 |
25 | console.log('✅ Connected to MySQL server!\n');
26 |
27 | // Create database if it doesn't exist
28 | console.log(`Step 2: Creating database '${dbName}'...`);
29 | await connection.query(`CREATE DATABASE IF NOT EXISTS \`${dbName}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
30 |
31 | console.log(`✅ Database '${dbName}' created successfully!\n`);
32 |
33 | // Show databases
34 | console.log('Step 3: Listing databases...');
35 | const [databases] = await connection.query('SHOW DATABASES');
36 | console.log('\nAvailable databases:');
37 | databases.forEach((db, index) => {
38 | const dbName = db.Database;
39 | console.log(` ${index + 1}. ${dbName}`);
40 | });
41 |
42 | await connection.end();
43 |
44 | console.log('\n🎉 Database setup complete!\n');
45 | console.log('Next step: Run the connection test');
46 | console.log(' node scripts/testMySQLConnection.js\n');
47 |
48 | } catch (error) {
49 | console.error('\n❌ Error:', error.message);
50 |
51 | if (error.code === 'ECONNREFUSED') {
52 | console.error('\n💡 MySQL server is not running. Please start it:');
53 | console.error(' Mac: brew services start mysql');
54 | console.error(' Or: mysql.server start\n');
55 | } else if (error.code === 'ER_ACCESS_DENIED_ERROR') {
56 | console.error('\n💡 Access denied. Check your credentials in .env file\n');
57 | }
58 |
59 | process.exit(1);
60 | }
61 | }
62 |
63 | createDatabase();
64 |
--------------------------------------------------------------------------------
/models/mysql/ArenaUser.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../../config/mysqlDatabase');
3 |
4 | const ArenaUser = sequelize.define('ArenaUser', {
5 | id: {
6 | type: DataTypes.INTEGER,
7 | primaryKey: true,
8 | autoIncrement: true
9 | },
10 | firstName: {
11 | type: DataTypes.STRING(100),
12 | allowNull: false,
13 | field: 'first_name',
14 | validate: {
15 | notEmpty: {
16 | msg: 'First name is required'
17 | }
18 | }
19 | },
20 | lastName: {
21 | type: DataTypes.STRING(100),
22 | allowNull: false,
23 | field: 'last_name',
24 | validate: {
25 | notEmpty: {
26 | msg: 'Last name is required'
27 | }
28 | }
29 | },
30 | email: {
31 | type: DataTypes.STRING(255),
32 | allowNull: false,
33 | unique: true,
34 | validate: {
35 | isEmail: {
36 | msg: 'Please provide a valid email'
37 | },
38 | notEmpty: {
39 | msg: 'Email is required'
40 | }
41 | },
42 | set(value) {
43 | this.setDataValue('email', value.toLowerCase().trim());
44 | }
45 | },
46 | password: {
47 | type: DataTypes.STRING(255),
48 | allowNull: false,
49 | validate: {
50 | len: {
51 | args: [6, 255],
52 | msg: 'Password must be at least 6 characters'
53 | },
54 | notEmpty: {
55 | msg: 'Password is required'
56 | }
57 | }
58 | },
59 | authMode: {
60 | type: DataTypes.ENUM('otp', 'token'),
61 | defaultValue: 'otp',
62 | field: 'auth_mode'
63 | },
64 | isVerified: {
65 | type: DataTypes.BOOLEAN,
66 | defaultValue: false,
67 | field: 'is_verified'
68 | },
69 | verificationOtp: {
70 | type: DataTypes.STRING(10),
71 | allowNull: true,
72 | field: 'verification_otp'
73 | },
74 | verificationToken: {
75 | type: DataTypes.STRING(255),
76 | allowNull: true,
77 | field: 'verification_token'
78 | },
79 | verificationExpiry: {
80 | type: DataTypes.DATE,
81 | allowNull: true,
82 | field: 'verification_expiry'
83 | }
84 | }, {
85 | tableName: 'arena_users',
86 | timestamps: true,
87 | underscored: true,
88 | createdAt: 'created_at',
89 | updatedAt: false,
90 | indexes: [
91 | { fields: ['email'], unique: true },
92 | { fields: ['is_verified'] }
93 | ]
94 | });
95 |
96 | module.exports = ArenaUser;
97 |
--------------------------------------------------------------------------------
/controllers/analyticsController.js:
--------------------------------------------------------------------------------
1 | const crypto = require('crypto');
2 | const { Op } = require('sequelize');
3 | const { SiteVisit } = require('../models/mysql');
4 |
5 | // Public endpoint to record a visit
6 | exports.recordVisit = async (req, res, next) => {
7 | try {
8 | const path = req.body.path || req.originalUrl;
9 | const rawIp =
10 | req.headers['x-forwarded-for']?.split(',')[0]?.trim() ||
11 | req.ip ||
12 | req.connection?.remoteAddress ||
13 | null;
14 |
15 | const ipHash = rawIp
16 | ? crypto.createHash('sha256').update(rawIp).digest('hex')
17 | : null;
18 |
19 | await SiteVisit.create({
20 | path,
21 | ipHash,
22 | userAgent: (req.headers['user-agent'] || '').slice(0, 255),
23 | userId: req.user?.id || null,
24 | });
25 |
26 | return res.status(204).end();
27 | } catch (error) {
28 | return next(error);
29 | }
30 | };
31 |
32 | // Admin-only: aggregated visit stats
33 | exports.getVisitStats = async (req, res, next) => {
34 | try {
35 | const { from, to, groupBy = 'day' } = req.query;
36 |
37 | let format;
38 | switch (groupBy) {
39 | case 'hour':
40 | format = '%Y-%m-%d %H:00:00';
41 | break;
42 | case 'month':
43 | format = '%Y-%m-01';
44 | break;
45 | case 'year':
46 | format = '%Y-01-01';
47 | break;
48 | default:
49 | format = '%Y-%m-%d';
50 | }
51 |
52 | const where = {};
53 | if (from) {
54 | where.visitedAt = { [Op.gte]: new Date(from) };
55 | }
56 | if (to) {
57 | where.visitedAt = {
58 | ...(where.visitedAt || {}),
59 | [Op.lte]: new Date(to),
60 | };
61 | }
62 |
63 | const buckets = await SiteVisit.findAll({
64 | where,
65 | attributes: [
66 | [SiteVisit.sequelize.fn('DATE_FORMAT', SiteVisit.sequelize.col('visited_at'), format), 'bucket'],
67 | [SiteVisit.sequelize.fn('COUNT', SiteVisit.sequelize.col('*')), 'count'],
68 | ],
69 | group: ['bucket'],
70 | order: [[SiteVisit.sequelize.literal('bucket'), 'ASC']],
71 | raw: true,
72 | });
73 |
74 | const total = await SiteVisit.count({ where });
75 |
76 | return res.status(200).json({
77 | status: 'success',
78 | data: {
79 | total,
80 | buckets,
81 | },
82 | });
83 | } catch (error) {
84 | return next(error);
85 | }
86 | };
87 |
--------------------------------------------------------------------------------
/models/User.js:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | const bcrypt = require('bcryptjs');
3 |
4 | const userSchema = new mongoose.Schema({
5 | username: {
6 | type: String,
7 | required: [true, 'Username is required'],
8 | unique: true,
9 | trim: true,
10 | minlength: [3, 'Username must be at least 3 characters'],
11 | maxlength: [30, 'Username cannot exceed 30 characters']
12 | },
13 | email: {
14 | type: String,
15 | required: [true, 'Email is required'],
16 | unique: true,
17 | lowercase: true,
18 | trim: true,
19 | match: [/^\S+@\S+\.\S+$/, 'Please provide a valid email']
20 | },
21 | password: {
22 | type: String,
23 | required: [true, 'Password is required'],
24 | minlength: [6, 'Password must be at least 6 characters'],
25 | select: false
26 | },
27 | role: {
28 | type: String,
29 | enum: ['user', 'admin', 'moderator'],
30 | default: 'user'
31 | },
32 | profile: {
33 | firstName: String,
34 | lastName: String,
35 | country: String,
36 | preferredLanguage: { type: String, default: 'en' },
37 | avatar: String
38 | },
39 | stats: {
40 | totalQuizzes: { type: Number, default: 0 },
41 | totalQuestions: { type: Number, default: 0 },
42 | correctAnswers: { type: Number, default: 0 },
43 | totalScore: { type: Number, default: 0 },
44 | averageScore: { type: Number, default: 0 },
45 | streak: { type: Number, default: 0 },
46 | lastActiveDate: Date
47 | },
48 | achievements: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Achievement' }],
49 | isActive: { type: Boolean, default: true },
50 | isEmailVerified: { type: Boolean, default: false },
51 | lastLogin: Date,
52 | createdAt: { type: Date, default: Date.now }
53 | }, { timestamps: true });
54 |
55 | userSchema.pre('save', async function(next) {
56 | if (!this.isModified('password')) return next();
57 | const salt = await bcrypt.genSalt(10);
58 | this.password = await bcrypt.hash(this.password, salt);
59 | next();
60 | });
61 |
62 | userSchema.methods.comparePassword = async function(candidatePassword) {
63 | return await bcrypt.compare(candidatePassword, this.password);
64 | };
65 |
66 | userSchema.methods.updateAverageScore = function() {
67 | if (this.stats.totalQuestions > 0) {
68 | this.stats.averageScore = Math.round((this.stats.correctAnswers / this.stats.totalQuestions) * 100);
69 | }
70 | };
71 |
72 | module.exports = mongoose.model('User', userSchema);
--------------------------------------------------------------------------------
/middleware/arenaAuth.js:
--------------------------------------------------------------------------------
1 | const jwt = require('jsonwebtoken');
2 |
3 | /**
4 | * Middleware to verify JWT token for Arena Auth Simulator
5 | */
6 | exports.verifyArenaToken = (req, res, next) => {
7 | try {
8 | // Get token from header
9 | const token = req.header('Authorization')?.replace('Bearer ', '') ||
10 | req.header('x-auth-token');
11 |
12 | if (!token) {
13 | return res.status(401).json({
14 | success: false,
15 | message: 'No token provided',
16 | expired: false
17 | });
18 | }
19 |
20 | // Verify token
21 | const decoded = jwt.verify(token, process.env.JWT_SECRET || 'arena-secret-key');
22 |
23 | // Attach user info to request
24 | req.user = decoded;
25 | next();
26 | } catch (error) {
27 | // Check if token is expired
28 | if (error.name === 'TokenExpiredError') {
29 | return res.status(401).json({
30 | success: false,
31 | message: 'Token has expired. Please sign in again.',
32 | expired: true
33 | });
34 | }
35 |
36 | // Invalid token
37 | return res.status(401).json({
38 | success: false,
39 | message: 'Invalid token',
40 | expired: false
41 | });
42 | }
43 | };
44 |
45 | /**
46 | * Verify token and return user info (for dashboard access)
47 | */
48 | exports.verifyUser = async (req, res) => {
49 | try {
50 | const token = req.header('Authorization')?.replace('Bearer ', '') ||
51 | req.header('x-auth-token');
52 |
53 | if (!token) {
54 | return res.status(401).json({
55 | success: false,
56 | message: 'No token provided',
57 | expired: false
58 | });
59 | }
60 |
61 | // Verify token
62 | const decoded = jwt.verify(token, process.env.JWT_SECRET || 'arena-secret-key');
63 |
64 | res.json({
65 | success: true,
66 | user: {
67 | id: decoded.userId,
68 | email: decoded.email
69 | }
70 | });
71 | } catch (error) {
72 | // Check if token is expired
73 | if (error.name === 'TokenExpiredError') {
74 | return res.status(401).json({
75 | success: false,
76 | message: 'Token has expired. Please sign in again.',
77 | expired: true
78 | });
79 | }
80 |
81 | // Invalid token
82 | return res.status(401).json({
83 | success: false,
84 | message: 'Invalid token',
85 | expired: false
86 | });
87 | }
88 | };
89 |
--------------------------------------------------------------------------------
/client/src/context/AuthContext.jsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useState, useEffect } from 'react';
2 | import { authAPI } from '../services/api';
3 |
4 | const AuthContext = createContext(null);
5 |
6 | export const AuthProvider = ({ children }) => {
7 | const [user, setUser] = useState(null);
8 | const [loading, setLoading] = useState(true);
9 |
10 | useEffect(() => {
11 | const token = localStorage.getItem('token');
12 | if (token) {
13 | loadUser();
14 | } else {
15 | setLoading(false);
16 | }
17 | }, []);
18 |
19 | const loadUser = async () => {
20 | try {
21 | const response = await authAPI.getMe();
22 | setUser(response.data.data.user);
23 | } catch (error) {
24 | localStorage.removeItem('token');
25 | localStorage.removeItem('user');
26 | } finally {
27 | setLoading(false);
28 | }
29 | };
30 |
31 | const login = async (email, password) => {
32 | const response = await authAPI.login({ email, password });
33 | const { token, user } = response.data.data;
34 | localStorage.setItem('token', token);
35 | localStorage.setItem('user', JSON.stringify(user));
36 | setUser(user);
37 | return response.data;
38 | };
39 |
40 | const register = async (userData) => {
41 | const response = await authAPI.register(userData);
42 | const { token, user } = response.data.data;
43 | localStorage.setItem('token', token);
44 | localStorage.setItem('user', JSON.stringify(user));
45 | setUser(user);
46 | return response.data;
47 | };
48 |
49 | const logout = () => {
50 | // Clear main app tokens
51 | localStorage.removeItem('token');
52 | localStorage.removeItem('user');
53 |
54 | // Clear simulator tokens
55 | localStorage.removeItem('arena_sim_token');
56 | localStorage.removeItem('arena_sim_user');
57 |
58 | // Clear any other arena-related tokens
59 | localStorage.removeItem('arena_auth_token');
60 | localStorage.removeItem('arena_user');
61 |
62 | // Clear user state
63 | setUser(null);
64 |
65 | console.log('✅ Logout complete - all session data cleared');
66 | };
67 |
68 | return (
69 |
70 | {children}
71 |
72 | );
73 | };
74 |
75 | export const useAuth = () => {
76 | const context = useContext(AuthContext);
77 | if (!context) {
78 | throw new Error('useAuth must be used within AuthProvider');
79 | }
80 | return context;
81 | };
82 |
--------------------------------------------------------------------------------
/client_backup/src/context/AuthContext.jsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useState, useEffect } from 'react';
2 | import { authAPI } from '../services/api';
3 |
4 | const AuthContext = createContext(null);
5 |
6 | export const AuthProvider = ({ children }) => {
7 | const [user, setUser] = useState(null);
8 | const [loading, setLoading] = useState(true);
9 |
10 | useEffect(() => {
11 | const token = localStorage.getItem('token');
12 | if (token) {
13 | loadUser();
14 | } else {
15 | setLoading(false);
16 | }
17 | }, []);
18 |
19 | const loadUser = async () => {
20 | try {
21 | const response = await authAPI.getMe();
22 | setUser(response.data.data.user);
23 | } catch (error) {
24 | localStorage.removeItem('token');
25 | localStorage.removeItem('user');
26 | } finally {
27 | setLoading(false);
28 | }
29 | };
30 |
31 | const login = async (email, password) => {
32 | const response = await authAPI.login({ email, password });
33 | const { token, user } = response.data.data;
34 | localStorage.setItem('token', token);
35 | localStorage.setItem('user', JSON.stringify(user));
36 | setUser(user);
37 | return response.data;
38 | };
39 |
40 | const register = async (userData) => {
41 | const response = await authAPI.register(userData);
42 | const { token, user } = response.data.data;
43 | localStorage.setItem('token', token);
44 | localStorage.setItem('user', JSON.stringify(user));
45 | setUser(user);
46 | return response.data;
47 | };
48 |
49 | const logout = () => {
50 | // Clear main app tokens
51 | localStorage.removeItem('token');
52 | localStorage.removeItem('user');
53 |
54 | // Clear simulator tokens
55 | localStorage.removeItem('arena_sim_token');
56 | localStorage.removeItem('arena_sim_user');
57 |
58 | // Clear any other arena-related tokens
59 | localStorage.removeItem('arena_auth_token');
60 | localStorage.removeItem('arena_user');
61 |
62 | // Clear user state
63 | setUser(null);
64 |
65 | console.log('✅ Logout complete - all session data cleared');
66 | };
67 |
68 | return (
69 |
70 | {children}
71 |
72 | );
73 | };
74 |
75 | export const useAuth = () => {
76 | const context = useContext(AuthContext);
77 | if (!context) {
78 | throw new Error('useAuth must be used within AuthProvider');
79 | }
80 | return context;
81 | };
82 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 |
9 | # OS
10 | .DS_Store
11 | Thumbs.db
12 |
13 | # Node modules
14 | node_modules/
15 | client/node_modules/
16 | client/client/node_modules/
17 |
18 | # Build outputs
19 | build/
20 | dist/
21 | client/build/
22 | client/dist/
23 |
24 | # Coverage and test
25 | coverage/
26 | *.lcov
27 |
28 | # Env files (sensitive)
29 | .env
30 | .env.local
31 | .env.*.local
32 | client/.env
33 | client/.env.local
34 | client/client/.env
35 | client/client/.env.local
36 |
37 | # Allow production env files (non-sensitive)
38 | !.env.production
39 | !.env.example
40 | !client/.env.production
41 | !client/client/.env.production
42 |
43 | # Local data
44 | *.sqlite
45 | *.db
46 |
47 | # Cache
48 | .cache/
49 | .next/
50 | .vite/
51 | client/.vite/
52 |
53 | # IDE
54 | .vscode/
55 | .idea/
56 | *.swp
57 |
58 | # Misc
59 | *.local
60 | *.pid
61 | *.seed
62 |
63 | # Generated assets
64 | public/*.map
65 | client/public/*.map
66 | node_modules/
67 | package-lock.json
68 | yarn.lock
69 | .env
70 | .env.local
71 | .env.*.local
72 | logs/
73 | *.log
74 | npm-debug.log*
75 | pids/
76 | *.pid
77 | coverage/
78 | .nyc_output/
79 | .vscode/
80 | .idea/
81 | *.swp
82 | .DS_Store
83 | dist/
84 | build/
85 | uploads/*
86 | !uploads/.gitkeep
87 | tmp/
88 | temp/
89 |
90 | # Word temporary files
91 | ~$*
92 |
93 | # Archived documentation (consolidated into main docs)
94 | docs/archive/
95 |
96 | # Old test files
97 | QA_Unplugged_Haven_Test_Cases.xlsx
98 |
99 | # Temporary fix/setup documentation (use main README instead)
100 | FIX_*.md
101 | PRODUCTION_*.md
102 | *-fix.sh
103 | *-setup.sh
104 | quick-*.sh
105 | deploy-to-*.sh
106 |
107 | AUTH_SIMULATOR_BACKEND_GUIDE.md
108 | DATA_CY_LOCATORS.md
109 | deploy-prepare.sh
110 | DOCUMENTATION_CLEANUP_SUMMARY.md
111 | functional-bugs-data.json
112 | SOFTWARE_REQUIREMENTS_DOCUMENT.md
113 | TEST_AUTOMATION_ARENA.md
114 | ARENA_AUTH_IMPLEMENTATION_COMPLETE.md
115 | ARENA_AUTH_SETUP_COMPLETE.md
116 | AUTH_SIMULATOR_BACKEND_GUIDE.md
117 | DATA_CY_LOCATORS.md
118 | ETHEREAL_EMAIL_SETUP.md
119 | GMAIL_SETUP_GUIDE.md
120 | SENDGRID_SETUP_GUIDE.md
121 | FIX_*.md
122 | scripts/setupEtherealEmail.js
123 | scripts/testEmail.js
124 | TOKEN_EXPIRATION_IMPLEMENTATION.md
125 | DEMO_VS_REAL_MODE_IMPLEMENTATION.md
126 | LOGIN_PAGE_NAVIGATION_UPDATE.md
127 | LOGOUT_COMPLETE_SESSION_CLEANUP.md
128 | LANDING_PAGE_LOGGED_IN_UX.md
129 | LEADERBOARD_TIE_RANKING_FIX.md
130 | migrateCoreDataToMysql.js
131 | migrateFunctionalBugsToMysql.js
132 | migrateUsersToMysql.js
--------------------------------------------------------------------------------
/models/mysql/FunctionalBug.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../../config/mysqlDatabase');
3 |
4 | const FunctionalBug = sequelize.define('FunctionalBug', {
5 | id: {
6 | type: DataTypes.INTEGER,
7 | primaryKey: true,
8 | autoIncrement: true
9 | },
10 | bugId: {
11 | type: DataTypes.STRING(20),
12 | allowNull: false,
13 | unique: true,
14 | field: 'bug_id'
15 | },
16 | domain: {
17 | type: DataTypes.ENUM('fintech', 'ecommerce', 'ordering', 'grading', 'authentication'),
18 | allowNull: false
19 | },
20 | title: {
21 | type: DataTypes.STRING(255),
22 | allowNull: false
23 | },
24 | difficulty: {
25 | type: DataTypes.ENUM('beginner', 'intermediate', 'advanced'),
26 | allowNull: false
27 | },
28 | severity: {
29 | type: DataTypes.ENUM('low', 'medium', 'high', 'critical'),
30 | allowNull: false
31 | },
32 | category: {
33 | type: DataTypes.STRING(100),
34 | allowNull: false
35 | },
36 |
37 | // Scenario
38 | scenarioDescription: {
39 | type: DataTypes.TEXT,
40 | allowNull: true,
41 | field: 'scenario_description'
42 | },
43 | initialState: {
44 | type: DataTypes.JSON,
45 | allowNull: true,
46 | field: 'initial_state'
47 | },
48 |
49 | expectedResult: {
50 | type: DataTypes.TEXT,
51 | allowNull: false,
52 | field: 'expected_result'
53 | },
54 | actualResult: {
55 | type: DataTypes.TEXT,
56 | allowNull: false,
57 | field: 'actual_result'
58 | },
59 | bugType: {
60 | type: DataTypes.STRING(100),
61 | allowNull: false,
62 | field: 'bug_type'
63 | },
64 | rootCause: {
65 | type: DataTypes.TEXT,
66 | allowNull: false,
67 | field: 'root_cause'
68 | },
69 | fixDescription: {
70 | type: DataTypes.TEXT,
71 | allowNull: false,
72 | field: 'fix_description'
73 | },
74 |
75 | points: {
76 | type: DataTypes.INTEGER,
77 | defaultValue: 100
78 | },
79 | isActive: {
80 | type: DataTypes.BOOLEAN,
81 | defaultValue: true,
82 | field: 'is_active'
83 | },
84 | createdBy: {
85 | type: DataTypes.INTEGER,
86 | allowNull: true,
87 | field: 'created_by',
88 | references: {
89 | model: 'users',
90 | key: 'id'
91 | }
92 | }
93 | }, {
94 | tableName: 'functional_bugs',
95 | timestamps: true,
96 | underscored: true,
97 | indexes: [
98 | { fields: ['bug_id'], unique: true },
99 | { fields: ['domain', 'difficulty'] },
100 | { fields: ['is_active'] }
101 | ]
102 | });
103 |
104 | module.exports = FunctionalBug;
105 |
--------------------------------------------------------------------------------
/models/mysql/Question.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../../config/mysqlDatabase');
3 |
4 | const Question = sequelize.define('Question', {
5 | id: {
6 | type: DataTypes.INTEGER,
7 | primaryKey: true,
8 | autoIncrement: true
9 | },
10 | type: {
11 | type: DataTypes.ENUM('single-choice', 'multiple-choice', 'true-false'),
12 | allowNull: false,
13 | validate: {
14 | notEmpty: {
15 | msg: 'Question type is required'
16 | }
17 | }
18 | },
19 | category: {
20 | type: DataTypes.STRING(100),
21 | allowNull: false,
22 | validate: {
23 | notEmpty: {
24 | msg: 'Category is required'
25 | }
26 | },
27 | set(value) {
28 | this.setDataValue('category', value.toLowerCase().trim());
29 | }
30 | },
31 | difficulty: {
32 | type: DataTypes.ENUM('foundation', 'advanced', 'expert'),
33 | defaultValue: 'foundation'
34 | },
35 | syllabus: {
36 | type: DataTypes.STRING(100),
37 | defaultValue: 'ISTQB-CTFL-2018'
38 | },
39 | points: {
40 | type: DataTypes.INTEGER,
41 | defaultValue: 1,
42 | validate: {
43 | min: 1,
44 | max: 10
45 | }
46 | },
47 |
48 | // Statistics
49 | timesAnswered: {
50 | type: DataTypes.INTEGER,
51 | defaultValue: 0,
52 | field: 'times_answered'
53 | },
54 | timesCorrect: {
55 | type: DataTypes.INTEGER,
56 | defaultValue: 0,
57 | field: 'times_correct'
58 | },
59 | averageTime: {
60 | type: DataTypes.DECIMAL(10, 2),
61 | defaultValue: 0,
62 | field: 'average_time'
63 | },
64 |
65 | // Status
66 | status: {
67 | type: DataTypes.ENUM('draft', 'published', 'archived', 'flagged'),
68 | defaultValue: 'published'
69 | },
70 |
71 | // Votes
72 | upvotes: {
73 | type: DataTypes.INTEGER,
74 | defaultValue: 0
75 | },
76 | downvotes: {
77 | type: DataTypes.INTEGER,
78 | defaultValue: 0
79 | },
80 |
81 | // Foreign key
82 | createdBy: {
83 | type: DataTypes.INTEGER,
84 | allowNull: false,
85 | field: 'created_by',
86 | references: {
87 | model: 'users',
88 | key: 'id'
89 | }
90 | }
91 | }, {
92 | tableName: 'questions',
93 | timestamps: true,
94 | underscored: true,
95 | indexes: [
96 | { fields: ['category', 'difficulty'] },
97 | { fields: ['status'] },
98 | { fields: ['created_by'] }
99 | ]
100 | });
101 |
102 | // Virtual field for success rate
103 | Question.prototype.getSuccessRate = function() {
104 | if (this.timesAnswered === 0) return 0;
105 | return Math.round((this.timesCorrect / this.timesAnswered) * 100);
106 | };
107 |
108 | module.exports = Question;
109 |
--------------------------------------------------------------------------------
/models/mysql/Quiz.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const { sequelize } = require('../../config/mysqlDatabase');
3 |
4 | const Quiz = sequelize.define('Quiz', {
5 | id: {
6 | type: DataTypes.INTEGER,
7 | primaryKey: true,
8 | autoIncrement: true
9 | },
10 | userId: {
11 | type: DataTypes.INTEGER,
12 | allowNull: false,
13 | field: 'user_id',
14 | references: {
15 | model: 'users',
16 | key: 'id'
17 | }
18 | },
19 | mode: {
20 | type: DataTypes.ENUM('practice', 'exam', 'timed', 'category'),
21 | allowNull: false
22 | },
23 |
24 | // Settings
25 | language: {
26 | type: DataTypes.STRING(10),
27 | defaultValue: 'en'
28 | },
29 | category: {
30 | type: DataTypes.STRING(100),
31 | allowNull: true
32 | },
33 | difficulty: {
34 | type: DataTypes.STRING(50),
35 | allowNull: true
36 | },
37 | numberOfQuestions: {
38 | type: DataTypes.INTEGER,
39 | allowNull: true,
40 | field: 'number_of_questions'
41 | },
42 | timeLimit: {
43 | type: DataTypes.INTEGER,
44 | allowNull: true,
45 | field: 'time_limit'
46 | },
47 | randomOrder: {
48 | type: DataTypes.BOOLEAN,
49 | defaultValue: true,
50 | field: 'random_order'
51 | },
52 |
53 | // Score
54 | correctCount: {
55 | type: DataTypes.INTEGER,
56 | defaultValue: 0,
57 | field: 'correct_count'
58 | },
59 | incorrectCount: {
60 | type: DataTypes.INTEGER,
61 | defaultValue: 0,
62 | field: 'incorrect_count'
63 | },
64 | unansweredCount: {
65 | type: DataTypes.INTEGER,
66 | defaultValue: 0,
67 | field: 'unanswered_count'
68 | },
69 | percentage: {
70 | type: DataTypes.DECIMAL(5, 2),
71 | defaultValue: 0
72 | },
73 | totalPoints: {
74 | type: DataTypes.INTEGER,
75 | defaultValue: 0,
76 | field: 'total_points'
77 | },
78 |
79 | // Status
80 | status: {
81 | type: DataTypes.ENUM('in-progress', 'completed', 'abandoned'),
82 | defaultValue: 'in-progress'
83 | },
84 |
85 | startedAt: {
86 | type: DataTypes.DATE,
87 | defaultValue: DataTypes.NOW,
88 | field: 'started_at'
89 | },
90 | completedAt: {
91 | type: DataTypes.DATE,
92 | allowNull: true,
93 | field: 'completed_at'
94 | },
95 | totalTime: {
96 | type: DataTypes.INTEGER,
97 | defaultValue: 0,
98 | field: 'total_time'
99 | }
100 | }, {
101 | tableName: 'quizzes',
102 | timestamps: true,
103 | underscored: true,
104 | indexes: [
105 | { fields: ['user_id', 'status'] },
106 | { fields: ['created_at'] }
107 | ],
108 | hooks: {
109 | beforeUpdate: (quiz) => {
110 | if (quiz.status === 'completed' && quiz.changed('status')) {
111 | quiz.completedAt = new Date();
112 | }
113 | }
114 | }
115 | });
116 |
117 | module.exports = Quiz;
118 |
--------------------------------------------------------------------------------
/server.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const express = require('express');
3 | const cors = require('cors');
4 | const helmet = require('helmet');
5 | const morgan = require('morgan');
6 | const compression = require('compression');
7 | const { testConnection } = require('./config/mysqlDatabase');
8 | const errorHandler = require('./middleware/errorHandler');
9 |
10 | const authRoutes = require('./routes/auth');
11 | const questionRoutes = require('./routes/questions');
12 | const quizRoutes = require('./routes/quiz');
13 | const progressRoutes = require('./routes/progress');
14 | const leaderboardRoutes = require('./routes/leaderboard');
15 | const achievementRoutes = require('./routes/achievements');
16 | const adminRoutes = require('./routes/admin');
17 | const functionalBugRoutes = require('./routes/functionalBugs');
18 | const questionUploadRoutes = require('./routes/questionUpload');
19 | const arenaAuthRoutes = require('./routes/arenaAuth');
20 | const analyticsRoutes = require('./routes/analytics');
21 |
22 | const app = express();
23 |
24 | // Only connect to DB if not in test mode (tests handle their own connection)
25 | if (process.env.NODE_ENV !== 'test') {
26 | testConnection();
27 | }
28 |
29 | app.use(helmet());
30 | app.use(cors({
31 | origin: process.env.CORS_ORIGIN || 'http://localhost:5173',
32 | credentials: true
33 | }));
34 | app.use(compression());
35 | app.use(morgan('dev'));
36 | app.use(express.json());
37 | app.use(express.urlencoded({ extended: true }));
38 |
39 | // Support both with and without /qaarena prefix
40 | const basePath = process.env.BASE_PATH || '';
41 |
42 | app.get(`${basePath}/api/health`, (req, res) => {
43 | res.status(200).json({
44 | status: 'success',
45 | message: 'ISTQB Practice API is running',
46 | timestamp: new Date().toISOString()
47 | });
48 | });
49 |
50 | app.use(`${basePath}/api/auth`, authRoutes);
51 | app.use(`${basePath}/api/questions`, questionRoutes);
52 | app.use(`${basePath}/api/quiz`, quizRoutes);
53 | app.use(`${basePath}/api/progress`, progressRoutes);
54 | app.use(`${basePath}/api/leaderboard`, leaderboardRoutes);
55 | app.use(`${basePath}/api/achievements`, achievementRoutes);
56 | app.use(`${basePath}/api/admin`, adminRoutes);
57 | app.use(`${basePath}/api/functional-bugs`, functionalBugRoutes);
58 | app.use(`${basePath}/api/questions-upload`, questionUploadRoutes);
59 | app.use(`${basePath}/api/arena-auth`, arenaAuthRoutes);
60 | app.use(`${basePath}/api/analytics`, analyticsRoutes);
61 |
62 | app.use((req, res) => {
63 | res.status(404).json({
64 | status: 'error',
65 | message: 'Route not found'
66 | });
67 | });
68 |
69 | app.use(errorHandler);
70 |
71 | const PORT = process.env.PORT || 5001;
72 |
73 | // Only start server if not in test mode
74 | if (process.env.NODE_ENV !== 'test') {
75 | const server = app.listen(PORT, () => {
76 | console.log(`🚀 Server running in ${process.env.NODE_ENV} mode on port ${PORT}`);
77 | });
78 |
79 | // Handle port already in use error
80 | server.on('error', (err) => {
81 | if (err.code === 'EADDRINUSE') {
82 | console.error(`❌ Port ${PORT} is already in use. Please kill the existing process or use a different port.`);
83 | console.error(` Run: lsof -ti:${PORT} | xargs kill -9`);
84 | process.exit(1);
85 | } else {
86 | console.error('Server error:', err);
87 | process.exit(1);
88 | }
89 | });
90 |
91 | process.on('unhandledRejection', (err) => {
92 | console.error('Unhandled Rejection:', err);
93 | server.close(() => process.exit(1));
94 | });
95 | }
96 |
97 | module.exports = app;
--------------------------------------------------------------------------------
/client/src/components/ui/dialog.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as DialogPrimitive from "@radix-ui/react-dialog"
3 | import { X } from "lucide-react"
4 |
5 | import { cn } from "../../lib/utils"
6 |
7 | const Dialog = DialogPrimitive.Root
8 |
9 | const DialogTrigger = DialogPrimitive.Trigger
10 |
11 | const DialogPortal = DialogPrimitive.Portal
12 |
13 | const DialogClose = DialogPrimitive.Close
14 |
15 | const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
16 |
24 | ))
25 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
26 |
27 | const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
28 |
29 |
30 |
38 | {children}
39 |
40 |
41 | Close
42 |
43 |
44 |
45 | ))
46 | DialogContent.displayName = DialogPrimitive.Content.displayName
47 |
48 | const DialogHeader = ({
49 | className,
50 | ...props
51 | }) => (
52 |
59 | )
60 | DialogHeader.displayName = "DialogHeader"
61 |
62 | const DialogFooter = ({
63 | className,
64 | ...props
65 | }) => (
66 |
73 | )
74 | DialogFooter.displayName = "DialogFooter"
75 |
76 | const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (
77 |
85 | ))
86 | DialogTitle.displayName = DialogPrimitive.Title.displayName
87 |
88 | const DialogDescription = React.forwardRef(({ className, ...props }, ref) => (
89 |
94 | ))
95 | DialogDescription.displayName = DialogPrimitive.Description.displayName
96 |
97 | export {
98 | Dialog,
99 | DialogPortal,
100 | DialogOverlay,
101 | DialogClose,
102 | DialogTrigger,
103 | DialogContent,
104 | DialogHeader,
105 | DialogFooter,
106 | DialogTitle,
107 | DialogDescription,
108 | }
109 |
--------------------------------------------------------------------------------
/client_backup/src/components/ui/dialog.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as DialogPrimitive from "@radix-ui/react-dialog"
3 | import { X } from "lucide-react"
4 |
5 | import { cn } from "../../lib/utils"
6 |
7 | const Dialog = DialogPrimitive.Root
8 |
9 | const DialogTrigger = DialogPrimitive.Trigger
10 |
11 | const DialogPortal = DialogPrimitive.Portal
12 |
13 | const DialogClose = DialogPrimitive.Close
14 |
15 | const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
16 |
24 | ))
25 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
26 |
27 | const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
28 |
29 |
30 |
38 | {children}
39 |
40 |
41 | Close
42 |
43 |
44 |
45 | ))
46 | DialogContent.displayName = DialogPrimitive.Content.displayName
47 |
48 | const DialogHeader = ({
49 | className,
50 | ...props
51 | }) => (
52 |
59 | )
60 | DialogHeader.displayName = "DialogHeader"
61 |
62 | const DialogFooter = ({
63 | className,
64 | ...props
65 | }) => (
66 |
73 | )
74 | DialogFooter.displayName = "DialogFooter"
75 |
76 | const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (
77 |
85 | ))
86 | DialogTitle.displayName = DialogPrimitive.Title.displayName
87 |
88 | const DialogDescription = React.forwardRef(({ className, ...props }, ref) => (
89 |
94 | ))
95 | DialogDescription.displayName = DialogPrimitive.Description.displayName
96 |
97 | export {
98 | Dialog,
99 | DialogPortal,
100 | DialogOverlay,
101 | DialogClose,
102 | DialogTrigger,
103 | DialogContent,
104 | DialogHeader,
105 | DialogFooter,
106 | DialogTitle,
107 | DialogDescription,
108 | }
109 |
--------------------------------------------------------------------------------
/client/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client_backup/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/questions.test.js:
--------------------------------------------------------------------------------
1 | require('./setup');
2 | const request = require('supertest');
3 | const app = require('../server');
4 | const User = require('../models/User');
5 | const Question = require('../models/Question');
6 |
7 | describe('Question Tests', () => {
8 | let adminToken;
9 | let adminUser;
10 | let userToken;
11 | let regularUser;
12 |
13 | beforeEach(async () => {
14 |
15 | regularUser = await User.create({
16 | username: 'user',
17 | email: 'user@example.com',
18 | password: 'User123!',
19 | role: 'user'
20 | });
21 |
22 |
23 |
24 | const userRes = await request(app)
25 | .post('/api/auth/login')
26 | .send({ email: 'user@example.com', password: 'User123!' });
27 | userToken = userRes.body.data.token;
28 | });
29 |
30 | describe('POST /api/questions', () => {
31 | it('should create question as admin', async () => {
32 | const res = await request(app)
33 | .post('/api/questions')
34 | .set('Authorization', `Bearer ${adminToken}`)
35 | .send({
36 | questionText: { en: 'What is testing?' },
37 | type: 'single-choice',
38 | options: [
39 | { text: { en: 'Option 1' }, isCorrect: true },
40 | { text: { en: 'Option 2' }, isCorrect: false }
41 | ],
42 | category: 'fundamentals',
43 | difficulty: 'foundation'
44 | });
45 |
46 | expect(res.statusCode).toBe(201);
47 | expect(res.body.status).toBe('success');
48 | expect(res.body.data.question).toHaveProperty('_id');
49 | });
50 |
51 | it('should not create question as regular user', async () => {
52 | const res = await request(app)
53 | .post('/api/questions')
54 | .set('Authorization', `Bearer ${userToken}`)
55 | .send({
56 | questionText: { en: 'What is testing?' },
57 | type: 'single-choice',
58 | options: [
59 | { text: { en: 'Option 1' }, isCorrect: true },
60 | { text: { en: 'Option 2' }, isCorrect: false }
61 | ],
62 | category: 'fundamentals'
63 | });
64 |
65 | expect(res.statusCode).toBe(403);
66 | expect(res.body.status).toBe('error');
67 | });
68 | });
69 |
70 | describe('GET /api/questions', () => {
71 | beforeEach(async () => {
72 | await Question.create({
73 | questionText: new Map([['en', 'Test Question 1']]),
74 | type: 'single-choice',
75 | options: [
76 | { text: new Map([['en', 'Option 1']]), isCorrect: true },
77 | { text: new Map([['en', 'Option 2']]), isCorrect: false }
78 | ],
79 | category: 'fundamentals',
80 | difficulty: 'foundation',
81 | createdBy: adminUser._id,
82 | status: 'published'
83 | });
84 |
85 | await Question.create({
86 | questionText: new Map([['en', 'Test Question 2']]),
87 | type: 'true-false',
88 | options: [
89 | { text: new Map([['en', 'True']]), isCorrect: true },
90 | { text: new Map([['en', 'False']]), isCorrect: false }
91 | ],
92 | category: 'test-techniques',
93 | difficulty: 'advanced',
94 | createdBy: adminUser._id,
95 | status: 'published'
96 | });
97 | });
98 |
99 | it('should get all published questions', async () => {
100 | const res = await request(app).get('/api/questions');
101 |
102 | expect(res.statusCode).toBe(200);
103 | expect(res.body.status).toBe('success');
104 | expect(res.body.data.questions).toHaveLength(2);
105 | });
106 |
107 | it('should filter questions by category', async () => {
108 | const res = await request(app).get('/api/questions?category=fundamentals');
109 |
110 | expect(res.statusCode).toBe(200);
111 | expect(res.body.data.questions).toHaveLength(1);
112 | expect(res.body.data.questions[0].category).toBe('fundamentals');
113 | });
114 |
115 | it('should filter questions by difficulty', async () => {
116 | const res = await request(app).get('/api/questions?difficulty=advanced');
117 |
118 | expect(res.statusCode).toBe(200);
119 | expect(res.body.data.questions).toHaveLength(1);
120 | expect(res.body.data.questions[0].difficulty).toBe('advanced');
121 | });
122 | });
123 | });
--------------------------------------------------------------------------------
/models/mysql/User.js:
--------------------------------------------------------------------------------
1 | const { DataTypes } = require('sequelize');
2 | const bcrypt = require('bcryptjs');
3 | const { sequelize } = require('../../config/mysqlDatabase');
4 |
5 | const User = sequelize.define('User', {
6 | id: {
7 | type: DataTypes.INTEGER,
8 | primaryKey: true,
9 | autoIncrement: true
10 | },
11 | username: {
12 | type: DataTypes.STRING(30),
13 | allowNull: false,
14 | unique: true,
15 | validate: {
16 | len: {
17 | args: [3, 30],
18 | msg: 'Username must be between 3 and 30 characters'
19 | },
20 | notEmpty: {
21 | msg: 'Username is required'
22 | }
23 | }
24 | },
25 | email: {
26 | type: DataTypes.STRING(255),
27 | allowNull: false,
28 | unique: true,
29 | validate: {
30 | isEmail: {
31 | msg: 'Please provide a valid email'
32 | },
33 | notEmpty: {
34 | msg: 'Email is required'
35 | }
36 | },
37 | set(value) {
38 | this.setDataValue('email', value.toLowerCase().trim());
39 | }
40 | },
41 | password: {
42 | type: DataTypes.STRING(255),
43 | allowNull: false,
44 | validate: {
45 | len: {
46 | args: [6, 255],
47 | msg: 'Password must be at least 6 characters'
48 | },
49 | notEmpty: {
50 | msg: 'Password is required'
51 | }
52 | }
53 | },
54 | role: {
55 | type: DataTypes.ENUM('user', 'admin', 'moderator'),
56 | defaultValue: 'user'
57 | },
58 |
59 | // Profile fields
60 | firstName: {
61 | type: DataTypes.STRING(100),
62 | allowNull: true,
63 | field: 'first_name'
64 | },
65 | lastName: {
66 | type: DataTypes.STRING(100),
67 | allowNull: true,
68 | field: 'last_name'
69 | },
70 | country: {
71 | type: DataTypes.STRING(100),
72 | allowNull: true
73 | },
74 | preferredLanguage: {
75 | type: DataTypes.STRING(10),
76 | defaultValue: 'en',
77 | field: 'preferred_language'
78 | },
79 | avatar: {
80 | type: DataTypes.STRING(255),
81 | allowNull: true
82 | },
83 |
84 | // Stats fields
85 | totalQuizzes: {
86 | type: DataTypes.INTEGER,
87 | defaultValue: 0,
88 | field: 'total_quizzes'
89 | },
90 | totalQuestions: {
91 | type: DataTypes.INTEGER,
92 | defaultValue: 0,
93 | field: 'total_questions'
94 | },
95 | correctAnswers: {
96 | type: DataTypes.INTEGER,
97 | defaultValue: 0,
98 | field: 'correct_answers'
99 | },
100 | totalScore: {
101 | type: DataTypes.INTEGER,
102 | defaultValue: 0,
103 | field: 'total_score'
104 | },
105 | averageScore: {
106 | type: DataTypes.DECIMAL(5, 2),
107 | defaultValue: 0,
108 | field: 'average_score'
109 | },
110 | streak: {
111 | type: DataTypes.INTEGER,
112 | defaultValue: 0
113 | },
114 | lastActiveDate: {
115 | type: DataTypes.DATE,
116 | allowNull: true,
117 | field: 'last_active_date'
118 | },
119 |
120 | // Status fields
121 | isActive: {
122 | type: DataTypes.BOOLEAN,
123 | defaultValue: true,
124 | field: 'is_active'
125 | },
126 | isEmailVerified: {
127 | type: DataTypes.BOOLEAN,
128 | defaultValue: false,
129 | field: 'is_email_verified'
130 | },
131 | lastLogin: {
132 | type: DataTypes.DATE,
133 | allowNull: true,
134 | field: 'last_login'
135 | }
136 | }, {
137 | tableName: 'users',
138 | timestamps: true,
139 | underscored: true,
140 | indexes: [
141 | { fields: ['username'] },
142 | { fields: ['email'] },
143 | { fields: ['role'] }
144 | ],
145 | hooks: {
146 | beforeCreate: async (user) => {
147 | if (user.password) {
148 | const salt = await bcrypt.genSalt(10);
149 | user.password = await bcrypt.hash(user.password, salt);
150 | }
151 | },
152 | beforeUpdate: async (user) => {
153 | if (user.changed('password')) {
154 | const salt = await bcrypt.genSalt(10);
155 | user.password = await bcrypt.hash(user.password, salt);
156 | }
157 | }
158 | }
159 | });
160 |
161 | // Instance methods
162 | User.prototype.comparePassword = async function(candidatePassword) {
163 | return await bcrypt.compare(candidatePassword, this.password);
164 | };
165 |
166 | User.prototype.updateAverageScore = function() {
167 | if (this.totalQuestions > 0) {
168 | this.averageScore = Math.round((this.correctAnswers / this.totalQuestions) * 100);
169 | }
170 | };
171 |
172 | // Exclude password from JSON responses
173 | User.prototype.toJSON = function() {
174 | const values = Object.assign({}, this.get());
175 | delete values.password;
176 | return values;
177 | };
178 |
179 | module.exports = User;
180 |
--------------------------------------------------------------------------------
/tests/auth.test.js:
--------------------------------------------------------------------------------
1 | require('./setup');
2 | const request = require('supertest');
3 | const app = require('../server');
4 | const User = require('../models/User');
5 | const Progress = require('../models/Progress');
6 |
7 | describe('Authentication Tests', () => {
8 | describe('POST /api/auth/register', () => {
9 | it('should register a new user', async () => {
10 | const res = await request(app)
11 | .post('/api/auth/register')
12 | .send({
13 | username: 'testuser',
14 | email: 'test@example.com',
15 | password: 'Test123!',
16 | firstName: 'Test',
17 | lastName: 'User'
18 | });
19 |
20 | expect(res.statusCode).toBe(201);
21 | expect(res.body.status).toBe('success');
22 | expect(res.body.data).toHaveProperty('token');
23 | expect(res.body.data.user).toHaveProperty('username', 'testuser');
24 |
25 | const user = await User.findOne({ email: 'test@example.com' });
26 | expect(user).toBeTruthy();
27 |
28 | const progress = await Progress.findOne({ user: user._id });
29 | expect(progress).toBeTruthy();
30 | });
31 |
32 | it('should not register user with duplicate email', async () => {
33 | await User.create({
34 | username: 'existing',
35 | email: 'test@example.com',
36 | password: 'Test123!'
37 | });
38 |
39 | const res = await request(app)
40 | .post('/api/auth/register')
41 | .send({
42 | username: 'newuser',
43 | email: 'test@example.com',
44 | password: 'Test123!'
45 | });
46 |
47 | expect(res.statusCode).toBe(400);
48 | expect(res.body.status).toBe('error');
49 | });
50 |
51 | it('should validate required fields', async () => {
52 | const res = await request(app)
53 | .post('/api/auth/register')
54 | .send({
55 | username: 'te',
56 | email: 'invalid-email',
57 | password: '123'
58 | });
59 |
60 | expect(res.statusCode).toBe(400);
61 | expect(res.body.status).toBe('error');
62 | });
63 | });
64 |
65 | describe('POST /api/auth/login', () => {
66 | beforeEach(async () => {
67 | await User.create({
68 | username: 'testuser',
69 | email: 'test@example.com',
70 | password: 'Test123!'
71 | });
72 | });
73 |
74 | it('should login with valid credentials', async () => {
75 | const res = await request(app)
76 | .post('/api/auth/login')
77 | .send({
78 | email: 'test@example.com',
79 | password: 'Test123!'
80 | });
81 |
82 | expect(res.statusCode).toBe(200);
83 | expect(res.body.status).toBe('success');
84 | expect(res.body.data).toHaveProperty('token');
85 | expect(res.body.data.user).toHaveProperty('email', 'test@example.com');
86 | });
87 |
88 | it('should not login with invalid password', async () => {
89 | const res = await request(app)
90 | .post('/api/auth/login')
91 | .send({
92 | email: 'test@example.com',
93 | password: 'WrongPassword'
94 | });
95 |
96 | expect(res.statusCode).toBe(401);
97 | expect(res.body.status).toBe('error');
98 | });
99 |
100 | it('should not login with non-existent email', async () => {
101 | const res = await request(app)
102 | .post('/api/auth/login')
103 | .send({
104 | email: 'nonexistent@example.com',
105 | password: 'Test123!'
106 | });
107 |
108 | expect(res.statusCode).toBe(401);
109 | expect(res.body.status).toBe('error');
110 | });
111 | });
112 |
113 | describe('GET /api/auth/me', () => {
114 | let token;
115 | let user;
116 |
117 | beforeEach(async () => {
118 | user = await User.create({
119 | username: 'testuser',
120 | email: 'test@example.com',
121 | password: 'Test123!'
122 | });
123 |
124 | const res = await request(app)
125 | .post('/api/auth/login')
126 | .send({
127 | email: 'test@example.com',
128 | password: 'Test123!'
129 | });
130 |
131 | token = res.body.data.token;
132 | });
133 |
134 | it('should get current user with valid token', async () => {
135 | const res = await request(app)
136 | .get('/api/auth/me')
137 | .set('Authorization', `Bearer ${token}`);
138 |
139 | expect(res.statusCode).toBe(200);
140 | expect(res.body.status).toBe('success');
141 | expect(res.body.data.user).toHaveProperty('email', 'test@example.com');
142 | });
143 |
144 | it('should not get user without token', async () => {
145 | const res = await request(app).get('/api/auth/me');
146 |
147 | expect(res.statusCode).toBe(401);
148 | expect(res.body.status).toBe('error');
149 | });
150 | });
151 | });
--------------------------------------------------------------------------------
/controllers/achievementController.js:
--------------------------------------------------------------------------------
1 | // MySQL Models
2 | const { Achievement, AchievementTranslation, UserAchievement, User } = require('../models/mysql');
3 | const { Op } = require('sequelize');
4 |
5 | exports.getAllAchievements = async (req, res, next) => {
6 | try {
7 | const achievements = await Achievement.findAll({
8 | where: { isActive: true },
9 | include: [{ model: AchievementTranslation, as: 'translations' }]
10 | });
11 |
12 | const userAchievements = await UserAchievement.findAll({
13 | where: { userId: req.user.id },
14 | attributes: ['achievementId']
15 | });
16 | const unlockedIds = userAchievements.map(ua => ua.achievementId);
17 |
18 | const achievementsWithStatus = achievements.map(achievement => ({
19 | id: achievement.id,
20 | name: achievement.translations?.find(t => t.language === 'en')?.name || '',
21 | description: achievement.translations?.find(t => t.language === 'en')?.description || '',
22 | icon: achievement.icon,
23 | type: achievement.type,
24 | criteriaMetric: achievement.criteriaMetric,
25 | criteriaThreshold: achievement.criteriaThreshold,
26 | rarity: achievement.rarity,
27 | points: achievement.points,
28 | unlocked: unlockedIds.includes(achievement.id)
29 | }));
30 |
31 | res.status(200).json({
32 | status: 'success',
33 | data: { achievements: achievementsWithStatus }
34 | });
35 | } catch (error) {
36 | next(error);
37 | }
38 | };
39 |
40 | exports.getUserAchievements = async (req, res, next) => {
41 | try {
42 | const userAchievements = await UserAchievement.findAll({
43 | where: { userId: req.user.id },
44 | include: [{
45 | model: Achievement,
46 | as: 'achievement',
47 | include: [{ model: AchievementTranslation, as: 'translations' }]
48 | }]
49 | });
50 |
51 | const achievements = userAchievements.map(ua => ({
52 | id: ua.achievement.id,
53 | name: ua.achievement.translations?.find(t => t.language === 'en')?.name || '',
54 | description: ua.achievement.translations?.find(t => t.language === 'en')?.description || '',
55 | icon: ua.achievement.icon,
56 | type: ua.achievement.type,
57 | rarity: ua.achievement.rarity,
58 | points: ua.achievement.points,
59 | unlockedAt: ua.unlockedAt
60 | }));
61 |
62 | res.status(200).json({
63 | status: 'success',
64 | data: { achievements }
65 | });
66 | } catch (error) {
67 | next(error);
68 | }
69 | };
70 |
71 | exports.checkAchievements = async (req, res, next) => {
72 | try {
73 | const user = await User.findByPk(req.user.id);
74 | const achievements = await Achievement.findAll({
75 | where: { isActive: true },
76 | include: [{ model: AchievementTranslation, as: 'translations' }]
77 | });
78 |
79 | const userAchievements = await UserAchievement.findAll({
80 | where: { userId: req.user.id },
81 | attributes: ['achievementId']
82 | });
83 | const unlockedIds = userAchievements.map(ua => ua.achievementId);
84 |
85 | const newAchievements = [];
86 |
87 | for (const achievement of achievements) {
88 | const alreadyUnlocked = unlockedIds.includes(achievement.id);
89 |
90 | if (!alreadyUnlocked) {
91 | let unlocked = false;
92 |
93 | switch (achievement.criteriaMetric) {
94 | case 'totalQuizzes':
95 | unlocked = user.totalQuizzes >= achievement.criteriaThreshold;
96 | break;
97 | case 'totalScore':
98 | unlocked = user.totalScore >= achievement.criteriaThreshold;
99 | break;
100 | case 'averageScore':
101 | unlocked = parseFloat(user.averageScore) >= achievement.criteriaThreshold;
102 | break;
103 | case 'streak':
104 | unlocked = user.streak >= achievement.criteriaThreshold;
105 | break;
106 | case 'correctAnswers':
107 | unlocked = user.correctAnswers >= achievement.criteriaThreshold;
108 | break;
109 | }
110 |
111 | if (unlocked) {
112 | await UserAchievement.create({
113 | userId: user.id,
114 | achievementId: achievement.id,
115 | unlockedAt: new Date()
116 | });
117 | newAchievements.push({
118 | id: achievement.id,
119 | name: achievement.translations?.find(t => t.language === 'en')?.name || '',
120 | description: achievement.translations?.find(t => t.language === 'en')?.description || '',
121 | icon: achievement.icon,
122 | points: achievement.points
123 | });
124 | }
125 | }
126 | }
127 |
128 | res.status(200).json({
129 | status: 'success',
130 | data: { newAchievements }
131 | });
132 | } catch (error) {
133 | next(error);
134 | }
135 | };
--------------------------------------------------------------------------------
/tests/quiz.test.js:
--------------------------------------------------------------------------------
1 | require('./setup');
2 | const request = require('supertest');
3 | const app = require('../server');
4 | const User = require('../models/User');
5 | const Question = require('../models/Question');
6 | const Quiz = require('../models/Quiz');
7 | const Progress = require('../models/Progress');
8 |
9 | describe('Quiz Tests', () => {
10 | let token;
11 | let user;
12 | let question;
13 |
14 | beforeEach(async () => {
15 | user = await User.create({
16 | username: 'testuser',
17 | email: 'test@example.com',
18 | password: 'Test123!'
19 | });
20 |
21 | await Progress.create({ user: user._id });
22 |
23 | const res = await request(app)
24 | .post('/api/auth/login')
25 | .send({ email: 'test@example.com', password: 'Test123!' });
26 | token = res.body.data.token;
27 |
28 | question = await Question.create({
29 | questionText: new Map([['en', 'Test Question']]),
30 | type: 'single-choice',
31 | options: [
32 | { text: new Map([['en', 'Correct Answer']]), isCorrect: true },
33 | { text: new Map([['en', 'Wrong Answer']]), isCorrect: false }
34 | ],
35 | category: 'fundamentals',
36 | difficulty: 'foundation',
37 | createdBy: user._id,
38 | status: 'published'
39 | });
40 | });
41 |
42 | describe('POST /api/quiz/start', () => {
43 | it('should start a new quiz', async () => {
44 | const res = await request(app)
45 | .post('/api/quiz/start')
46 | .set('Authorization', `Bearer ${token}`)
47 | .send({
48 | mode: 'practice',
49 | category: 'fundamentals',
50 | numberOfQuestions: 1
51 | });
52 |
53 | expect(res.statusCode).toBe(201);
54 | expect(res.body.status).toBe('success');
55 | expect(res.body.data.quiz).toHaveProperty('_id');
56 | expect(res.body.data.quiz.questions).toHaveLength(1);
57 | expect(res.body.data.quiz.status).toBe('in-progress');
58 | });
59 |
60 | it('should return error when no questions match criteria', async () => {
61 | const res = await request(app)
62 | .post('/api/quiz/start')
63 | .set('Authorization', `Bearer ${token}`)
64 | .send({
65 | mode: 'practice',
66 | category: 'non-existent-category',
67 | numberOfQuestions: 10
68 | });
69 |
70 | expect(res.statusCode).toBe(404);
71 | expect(res.body.status).toBe('error');
72 | });
73 | });
74 |
75 | describe('POST /api/quiz/answer', () => {
76 | let quiz;
77 |
78 | beforeEach(async () => {
79 | quiz = await Quiz.create({
80 | user: user._id,
81 | mode: 'practice',
82 | questions: [{ question: question._id }],
83 | settings: { language: 'en' }
84 | });
85 | });
86 |
87 | it('should answer a question correctly', async () => {
88 | const res = await request(app)
89 | .post('/api/quiz/answer')
90 | .set('Authorization', `Bearer ${token}`)
91 | .send({
92 | quizId: quiz._id,
93 | questionId: question._id,
94 | answer: 0,
95 | timeSpent: 30
96 | });
97 |
98 | expect(res.statusCode).toBe(200);
99 | expect(res.body.status).toBe('success');
100 | expect(res.body.data.isCorrect).toBe(true);
101 | });
102 |
103 | it('should answer a question incorrectly', async () => {
104 | const res = await request(app)
105 | .post('/api/quiz/answer')
106 | .set('Authorization', `Bearer ${token}`)
107 | .send({
108 | quizId: quiz._id,
109 | questionId: question._id,
110 | answer: 1,
111 | timeSpent: 25
112 | });
113 |
114 | expect(res.statusCode).toBe(200);
115 | expect(res.body.status).toBe('success');
116 | expect(res.body.data.isCorrect).toBe(false);
117 | });
118 | });
119 |
120 | describe('POST /api/quiz/:id/complete', () => {
121 | let quiz;
122 |
123 | beforeEach(async () => {
124 | quiz = await Quiz.create({
125 | user: user._id,
126 | mode: 'practice',
127 | questions: [
128 | {
129 | question: question._id,
130 | userAnswer: [0],
131 | isCorrect: true,
132 | timeSpent: 30
133 | }
134 | ],
135 | settings: { language: 'en', category: 'fundamentals' }
136 | });
137 | });
138 |
139 | it('should complete a quiz and update stats', async () => {
140 | const res = await request(app)
141 | .post(`/api/quiz/${quiz._id}/complete`)
142 | .set('Authorization', `Bearer ${token}`);
143 |
144 | expect(res.statusCode).toBe(200);
145 | expect(res.body.status).toBe('success');
146 | expect(res.body.data.quiz.status).toBe('completed');
147 | expect(res.body.data.quiz.score.correct).toBe(1);
148 |
149 | const updatedUser = await User.findById(user._id);
150 | expect(updatedUser.stats.totalQuizzes).toBe(1);
151 | expect(updatedUser.stats.correctAnswers).toBe(1);
152 | });
153 | });
154 | });
--------------------------------------------------------------------------------
/client_backup/src/services/api.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | const API_URL = import.meta.env.VITE_API_URL || import.meta.env.VITE_API_BASE || 'http://localhost:5001/api';
4 |
5 | const api = axios.create({
6 | baseURL: API_URL,
7 | headers: {
8 | 'Content-Type': 'application/json',
9 | },
10 | });
11 |
12 | // Request interceptor to add token
13 | api.interceptors.request.use(
14 | (config) => {
15 | const token = localStorage.getItem('token');
16 | if (token) {
17 | config.headers.Authorization = `Bearer ${token}`;
18 | }
19 | return config;
20 | },
21 | (error) => Promise.reject(error)
22 | );
23 |
24 | // Response interceptor for error handling
25 | api.interceptors.response.use(
26 | (response) => response,
27 | (error) => {
28 | if (error.response?.status === 401) {
29 | // Only redirect to login if we're on a protected route
30 | const publicRoutes = ['/', '/login', '/register', '/bug-hunting', '/functional-bug-hunting', '/events'];
31 | const currentPath = window.location.pathname;
32 | const isPublicRoute = publicRoutes.some(route => currentPath.startsWith(route));
33 |
34 | if (!isPublicRoute) {
35 | localStorage.removeItem('token');
36 | localStorage.removeItem('user');
37 | window.location.href = '/login';
38 | }
39 | }
40 | return Promise.reject(error);
41 | }
42 | );
43 |
44 | // Auth API
45 | export const authAPI = {
46 | register: (data) => api.post('/auth/register', data),
47 | login: (data) => api.post('/auth/login', data),
48 | getMe: () => api.get('/auth/me'),
49 | updateProfile: (data) => api.put('/auth/profile', data),
50 | changePassword: (data) => api.put('/auth/change-password', data),
51 | };
52 |
53 | // Questions API
54 | export const questionsAPI = {
55 | getAll: (params) => api.get('/questions', { params }),
56 | getById: (id) => api.get(`/questions/${id}`),
57 | create: (data) => api.post('/questions', data),
58 | update: (id, data) => api.put(`/questions/${id}`, data),
59 | delete: (id) => api.delete(`/questions/${id}`),
60 | vote: (id, voteType) => api.post(`/questions/${id}/vote`, { voteType }),
61 | flag: (id, reason) => api.post(`/questions/${id}/flag`, { reason }),
62 | };
63 |
64 | // Quiz API
65 | export const quizAPI = {
66 | start: (data) => api.post('/quiz/start', data),
67 | answer: (data) => api.post('/quiz/answer', data),
68 | complete: (id) => api.post(`/quiz/${id}/complete`),
69 | getById: (id) => api.get(`/quiz/${id}`),
70 | getUserQuizzes: (params) => api.get('/quiz/user/history', { params }),
71 | getInProgress: () => api.get('/quiz/in-progress'),
72 | };
73 |
74 | // Progress API
75 | export const progressAPI = {
76 | get: () => api.get('/progress'),
77 | getCategories: () => api.get('/progress/categories'),
78 | getWeakAreas: () => api.get('/progress/weak-areas'),
79 | getStreak: () => api.get('/progress/streak'),
80 | getActivity: () => api.get('/progress/activity'),
81 | };
82 |
83 | // Leaderboard API
84 | export const leaderboardAPI = {
85 | getGlobal: (params) => api.get('/leaderboard/global', { params }),
86 | getCategory: (category, params) => api.get(`/leaderboard/category/${category}`, { params }),
87 | getUserRank: () => api.get('/leaderboard/rank'),
88 | };
89 |
90 | // Achievements API
91 | export const achievementsAPI = {
92 | getAll: () => api.get('/achievements'),
93 | getUserAchievements: () => api.get('/achievements/user'),
94 | check: () => api.post('/achievements/check'),
95 | };
96 |
97 | // Admin API
98 | export const adminAPI = {
99 | getStats: () => api.get('/admin/stats'),
100 | getUsers: (params) => api.get('/admin/users', { params }),
101 | updateUserRole: (id, role) => api.put(`/admin/users/${id}/role`, { role }),
102 | deactivateUser: (id) => api.put(`/admin/users/${id}/deactivate`),
103 | getFlaggedQuestions: () => api.get('/admin/questions/flagged'),
104 | reviewQuestion: (id, status) => api.put(`/admin/questions/${id}/review`, { status }),
105 | createAchievement: (data) => api.post('/admin/achievements', data),
106 | updateAchievement: (id, data) => api.put(`/admin/achievements/${id}`, data),
107 | deleteAchievement: (id) => api.delete(`/admin/achievements/${id}`),
108 | };
109 |
110 | // Functional Bugs API
111 | export const functionalBugsAPI = {
112 | getAll: (params) => api.get('/functional-bugs', { params }),
113 | getById: (bugId) => api.get(`/functional-bugs/${bugId}`),
114 | start: (bugId) => api.post(`/functional-bugs/${bugId}/start`),
115 | getHint: (bugId) => api.post(`/functional-bugs/${bugId}/hint`),
116 | submit: (bugId, data) => api.post(`/functional-bugs/${bugId}/submit`, data),
117 | getUserProgress: (params) => api.get('/functional-bugs/user/progress', { params }),
118 | getLeaderboard: (params) => api.get('/functional-bugs/leaderboard', { params }),
119 | getStats: (bugId) => api.get(`/functional-bugs/${bugId}/stats`),
120 | create: (data) => api.post('/functional-bugs', data),
121 | update: (bugId, data) => api.put(`/functional-bugs/${bugId}`, data),
122 | delete: (bugId) => api.delete(`/functional-bugs/${bugId}`),
123 | };
124 |
125 | export default api;
--------------------------------------------------------------------------------
/client/src/components/FunctionalBugs/GuestLoginModal.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import { X, Trophy, User, LogIn, UserPlus } from 'lucide-react';
4 |
5 | const GuestLoginModal = ({ isOpen, onClose, onContinueAsGuest }) => {
6 | const navigate = useNavigate();
7 |
8 | if (!isOpen) return null;
9 |
10 | const handleLogin = () => {
11 | navigate('/login');
12 | };
13 |
14 | const handleRegister = () => {
15 | navigate('/register');
16 | };
17 |
18 | return (
19 |
20 |
21 | {/* Header */}
22 |
23 |
29 |
30 |
31 |
Earn Points & Track Progress!
32 |
33 |
34 | Create an account to save your progress and compete on the leaderboard
35 |
36 |
37 |
38 | {/* Content */}
39 |
40 | {/* Benefits Section */}
41 |
42 |
43 |
44 | With an Account:
45 |
46 |
47 | -
48 | ✓
49 | Earn points for every bug you find
50 |
51 | -
52 | ✓
53 | Track your progress across all bugs
54 |
55 | -
56 | ✓
57 | Compete on the leaderboard
58 |
59 | -
60 | ✓
61 | Save your achievements
62 |
63 | -
64 | ✓
65 | Access your stats anytime
66 |
67 |
68 |
69 |
70 | {/* Action Buttons */}
71 |
72 |
79 |
80 |
87 |
88 |
89 |
92 |
93 | or
94 |
95 |
96 |
97 |
104 |
105 |
106 | {/* Guest Info */}
107 |
108 |
109 | Note: Guest progress is saved only for this session and won't earn points or appear on the leaderboard.
110 |
111 |
112 |
113 |
114 |
115 | );
116 | };
117 |
118 | export default GuestLoginModal;
119 |
--------------------------------------------------------------------------------
/client_backup/src/components/FunctionalBugs/GuestLoginModal.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import { X, Trophy, User, LogIn, UserPlus } from 'lucide-react';
4 |
5 | const GuestLoginModal = ({ isOpen, onClose, onContinueAsGuest }) => {
6 | const navigate = useNavigate();
7 |
8 | if (!isOpen) return null;
9 |
10 | const handleLogin = () => {
11 | navigate('/login');
12 | };
13 |
14 | const handleRegister = () => {
15 | navigate('/register');
16 | };
17 |
18 | return (
19 |
20 |
21 | {/* Header */}
22 |
23 |
29 |
30 |
31 |
Earn Points & Track Progress!
32 |
33 |
34 | Create an account to save your progress and compete on the leaderboard
35 |
36 |
37 |
38 | {/* Content */}
39 |
40 | {/* Benefits Section */}
41 |
42 |
43 |
44 | With an Account:
45 |
46 |
47 | -
48 | ✓
49 | Earn points for every bug you find
50 |
51 | -
52 | ✓
53 | Track your progress across all bugs
54 |
55 | -
56 | ✓
57 | Compete on the leaderboard
58 |
59 | -
60 | ✓
61 | Save your achievements
62 |
63 | -
64 | ✓
65 | Access your stats anytime
66 |
67 |
68 |
69 |
70 | {/* Action Buttons */}
71 |
72 |
79 |
80 |
87 |
88 |
89 |
92 |
93 | or
94 |
95 |
96 |
97 |
104 |
105 |
106 | {/* Guest Info */}
107 |
108 |
109 | Note: Guest progress is saved only for this session and won't earn points or appear on the leaderboard.
110 |
111 |
112 |
113 |
114 |
115 | );
116 | };
117 |
118 | export default GuestLoginModal;
119 |
--------------------------------------------------------------------------------
/client/src/pages/Register.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { useNavigate, Link } from 'react-router-dom';
3 | import { useAuth } from '../context/AuthContext';
4 | import { Button } from '../components/ui/button';
5 | import { Input } from '../components/ui/input';
6 | import { PasswordInput } from '../components/ui/password-input';
7 | import { Label } from '../components/ui/label';
8 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card';
9 | import { BookOpen } from 'lucide-react';
10 |
11 | export default function Register() {
12 | const [formData, setFormData] = useState({
13 | username: '',
14 | email: '',
15 | password: '',
16 | confirmPassword: '',
17 | });
18 | const [error, setError] = useState('');
19 | const [loading, setLoading] = useState(false);
20 | const { register } = useAuth();
21 | const navigate = useNavigate();
22 |
23 | const handleChange = (e) => {
24 | setFormData({ ...formData, [e.target.name]: e.target.value });
25 | };
26 |
27 | const handleSubmit = async (e) => {
28 | e.preventDefault();
29 | setError('');
30 |
31 | if (formData.password !== formData.confirmPassword) {
32 | setError('Passwords do not match');
33 | return;
34 | }
35 |
36 | if (formData.password.length < 6) {
37 | setError('Password must be at least 6 characters');
38 | return;
39 | }
40 |
41 | setLoading(true);
42 |
43 | try {
44 | await register({
45 | username: formData.username,
46 | email: formData.email,
47 | password: formData.password,
48 | });
49 | navigate('/dashboard');
50 | } catch (err) {
51 | setError(err.response?.data?.message || 'Registration failed. Please try again.');
52 | } finally {
53 | setLoading(false);
54 | }
55 | };
56 |
57 | return (
58 |
59 |
60 |
61 |
66 | Create Account
67 | Sign up to start practicing ISTQB questions
68 |
69 |
70 |
129 |
130 | Already have an account?
131 |
132 | Sign in
133 |
134 |
135 |
136 |
137 |
138 | );
139 | }
140 |
--------------------------------------------------------------------------------
/client_backup/src/pages/Register.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { useNavigate, Link } from 'react-router-dom';
3 | import { useAuth } from '../context/AuthContext';
4 | import { Button } from '../components/ui/button';
5 | import { Input } from '../components/ui/input';
6 | import { Label } from '../components/ui/label';
7 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card';
8 | import { BookOpen } from 'lucide-react';
9 |
10 | export default function Register() {
11 | const [formData, setFormData] = useState({
12 | username: '',
13 | email: '',
14 | password: '',
15 | confirmPassword: '',
16 | });
17 | const [error, setError] = useState('');
18 | const [loading, setLoading] = useState(false);
19 | const { register } = useAuth();
20 | const navigate = useNavigate();
21 |
22 | const handleChange = (e) => {
23 | setFormData({ ...formData, [e.target.name]: e.target.value });
24 | };
25 |
26 | const handleSubmit = async (e) => {
27 | e.preventDefault();
28 | setError('');
29 |
30 | if (formData.password !== formData.confirmPassword) {
31 | setError('Passwords do not match');
32 | return;
33 | }
34 |
35 | if (formData.password.length < 6) {
36 | setError('Password must be at least 6 characters');
37 | return;
38 | }
39 |
40 | setLoading(true);
41 |
42 | try {
43 | await register({
44 | username: formData.username,
45 | email: formData.email,
46 | password: formData.password,
47 | });
48 | navigate('/dashboard');
49 | } catch (err) {
50 | setError(err.response?.data?.message || 'Registration failed. Please try again.');
51 | } finally {
52 | setLoading(false);
53 | }
54 | };
55 |
56 | return (
57 |
58 |
59 |
60 |
65 | Create Account
66 | Sign up to start practicing ISTQB questions
67 |
68 |
69 |
130 |
131 | Already have an account?
132 |
133 | Sign in
134 |
135 |
136 |
137 |
138 |
139 | );
140 | }
141 |
--------------------------------------------------------------------------------
/client_backup/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
2 | import { lazy, Suspense } from 'react';
3 | import { AuthProvider, useAuth } from './context/AuthContext';
4 | import ProtectedRoute from './components/ProtectedRoute';
5 | import Layout from './components/Layout';
6 | import Landing from './pages/Landing';
7 | import Login from './pages/Login';
8 | import Register from './pages/Register';
9 | import Dashboard from './pages/Dashboard';
10 | import Questions from './pages/Questions';
11 | import Quiz from './pages/Quiz';
12 | import Progress from './pages/Progress';
13 | import Leaderboard from './pages/Leaderboard';
14 | import Achievements from './pages/Achievements';
15 | import Events from './pages/Events';
16 | import BugHuntingHub from './pages/BugHuntingHub';
17 | import BugHunting from './pages/BugHunting';
18 | import BugScenario from './pages/BugScenario';
19 | import FunctionalBugHunting from './pages/FunctionalBugHunting';
20 | import FunctionalBugScenario from './pages/FunctionalBugScenario';
21 | import QuestionUpload from './pages/QuestionUpload';
22 |
23 | // Test Automation Arena imports
24 | import ArenaLanding from './pages/automation-arena/ArenaLanding';
25 | import ArenaSignUp from './pages/automation-arena/ArenaSignUp';
26 | import ArenaSignIn from './pages/automation-arena/ArenaSignIn';
27 | import ArenaDashboard from './pages/automation-arena/ArenaDashboard';
28 | import EcommerceSimulator from './pages/automation-arena/simulators/EcommerceSimulator';
29 | import SchoolManagementSimulator from './pages/automation-arena/simulators/SchoolManagementSimulator';
30 | import ATMSimulator from './pages/automation-arena/simulators/ATMSimulator';
31 | import FundsTransferSimulator from './pages/automation-arena/simulators/FundsTransferSimulator';
32 | import AuthSimulator from './pages/automation-arena/simulators/AuthSimulator';
33 |
34 | // Lazy load Admin page
35 | const Admin = lazy(() => import('./pages/Admin'));
36 |
37 | // Admin route protection
38 | function AdminRoute({ children }) {
39 | const { user } = useAuth();
40 |
41 | if (!user) {
42 | return ;
43 | }
44 |
45 | if (user.role !== 'admin') {
46 | return ;
47 | }
48 |
49 | return children;
50 | }
51 |
52 | function App() {
53 | return (
54 |
55 |
56 |
57 | } />
58 | } />
59 | } />
60 | } />
61 |
62 | {/* Test Automation Arena - Public Routes */}
63 | } />
64 | } />
65 | } />
66 | } />
67 | } />
68 | } />
69 | } />
70 | } />
71 | } />
72 |
73 | {/* Public Bug Hunting Routes with Navbar */}
74 | }>
75 | } />
76 | } />
77 | } />
78 | } />
79 | } />
80 |
81 |
82 | {/* Protected Routes */}
83 |
86 |
87 |
88 | }
89 | >
90 | } />
91 | } />
92 | } />
93 | } />
94 | } />
95 | } />
96 |
100 |
102 |
103 |
104 | }>
105 |
106 |
107 |
108 | }
109 | />
110 |
114 |
115 |
116 | }
117 | />
118 |
119 |
120 |
121 |
122 | );
123 | }
124 |
125 | export default App;
126 |
--------------------------------------------------------------------------------
/client_backup/src/pages/Login.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { useNavigate, Link } from 'react-router-dom';
3 | import { useAuth } from '../context/AuthContext';
4 | import { Button } from '../components/ui/button';
5 | import { Input } from '../components/ui/input';
6 | import { Label } from '../components/ui/label';
7 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card';
8 | import { BookOpen, AlertCircle, Home, ArrowLeft } from 'lucide-react';
9 |
10 | export default function Login() {
11 | const [email, setEmail] = useState('');
12 | const [password, setPassword] = useState('');
13 | const [error, setError] = useState('');
14 | const [loading, setLoading] = useState(false);
15 | const { login } = useAuth();
16 | const navigate = useNavigate();
17 |
18 | const handleSubmit = async (e) => {
19 | e.preventDefault();
20 | setError('');
21 | setLoading(true);
22 |
23 | try {
24 | await login(email, password);
25 | navigate('/dashboard');
26 | } catch (err) {
27 | setError(err.response?.data?.message || 'Login failed. Please try again.');
28 | } finally {
29 | setLoading(false);
30 | }
31 | };
32 |
33 | return (
34 |
35 | {/* Navigation Bar */}
36 |
59 |
60 | {/* Login Form */}
61 |
62 |
63 |
64 | {/* Back to Home Button */}
65 |
66 |
67 |
71 |
72 |
73 |
78 | Welcome Back
79 | Sign in to your QA ARENA account
80 |
81 |
82 |
117 |
118 | Don't have an account?
119 |
120 | Sign up
121 |
122 |
123 | {/*
124 |
Demo Credentials:
125 |
User: test@example.com / Test123!
126 |
*/}
127 |
128 |
129 |
130 |
131 | );
132 | }
133 |
--------------------------------------------------------------------------------