init + api use

This commit is contained in:
Primakov Alexandr Alexandrovich
2025-11-03 17:59:08 +03:00
commit e777b57991
52 changed files with 20725 additions and 0 deletions

173
stubs/api/README.md Normal file
View File

@@ -0,0 +1,173 @@
# Challenge Admin API Stubs
Стабовый API сервер для разработки и тестирования админской панели Challenge Service.
## 📁 Структура
```
stubs/api/
├── data/ # JSON файлы с тестовыми данными
│ ├── tasks.json # Задания (5 шт.)
│ ├── chains.json # Цепочки (3 шт.)
│ ├── users.json # Пользователи (8 шт.)
│ ├── submissions.json # Попытки (8 шт.)
│ └── stats.json # Системная статистика
├── index.js # API роуты
└── README.md # Эта документация
```
## 🔧 Реализованные endpoints
### Tasks (Задания)
- `GET /api/challenge/tasks` - список всех заданий
- `GET /api/challenge/task/:id` - одно задание
- `POST /api/challenge/task` - создать задание
- `PUT /api/challenge/task/:id` - обновить задание
- `DELETE /api/challenge/task/:id` - удалить задание
### Chains (Цепочки)
- `GET /api/challenge/chains` - список всех цепочек
- `GET /api/challenge/chain/:id` - одна цепочка
- `POST /api/challenge/chain` - создать цепочку
- `PUT /api/challenge/chain/:id` - обновить цепочку
- `DELETE /api/challenge/chain/:id` - удалить цепочку
### Users (Пользователи)
- `GET /api/challenge/users` - список всех пользователей
### Statistics (Статистика)
- `GET /api/challenge/stats` - общая системная статистика
- `GET /api/challenge/user/:userId/stats` - статистика пользователя (генерируется динамически)
### Submissions (Попытки)
- `GET /api/challenge/submissions` - все попытки
- `GET /api/challenge/user/:userId/submissions?taskId=xxx` - попытки пользователя (с опциональной фильтрацией по заданию)
## 📝 Формат ответов
Все ответы возвращаются в формате:
### Успешный ответ
```json
{
"error": null,
"data": <данные>
}
```
### Ошибка
```json
{
"error": {
"message": "Описание ошибки"
},
"data": null
}
```
## 💾 In-memory хранилище
Стабовый сервер использует **in-memory хранилище**:
- JSON файлы загружаются в память при первом запросе
- Все изменения (CREATE/UPDATE/DELETE) сохраняются только в памяти
- При перезапуске сервера все изменения сбрасываются
- Оригинальные JSON файлы не изменяются
## 🎯 Особенности
### 1. Автоматическое обновление статистики
При создании/удалении задания или цепочки автоматически обновляется системная статистика.
### 2. Динамическая генерация статистики пользователей
Endpoint `/api/challenge/user/:userId/stats` генерирует статистику на лету на основе:
- Попыток пользователя (submissions)
- Доступных цепочек
- Статуса заданий
### 3. Populate для цепочек
При создании/обновлении цепочки задания автоматически populated из списка заданий.
### 4. Валидация
Стабовый сервер включает базовую валидацию:
- Проверка обязательных полей
- Проверка существования ресурсов
- Возврат корректных HTTP статусов (404, 400)
## 📊 Тестовые данные
### Задания (5 шт.)
1. **Реализовать сортировку массива** - с hiddenInstructions о сложности O(n log n)
2. **Создать REST API endpoint** - с требованием пагинации
3. **Компонент React формы** - с валидацией
4. **SQL запрос с JOIN** - без hiddenInstructions
5. **Валидация формы** - с проверкой edge cases
### Цепочки (3 шт.)
1. **Основы JavaScript** - 2 задания
2. **React разработка** - 1 задание
3. **Backend разработка** - 2 задания
### Пользователи (8 шт.)
- alex_student, maria_dev, ivan_coder, olga_js
- dmitry_react, anna_frontend, sergey_backend, elena_fullstack
### Попытки (8 шт.)
Различные статусы:
- **accepted** (5) - принятые решения
- **needs_revision** (3) - требующие доработки
- Включают реалистичный feedback от LLM
## 🔄 Примеры запросов
### Создать задание
```bash
POST /api/challenge/task
Content-Type: application/json
{
"title": "Новое задание",
"description": "# Описание\n\nТекст задания...",
"hiddenInstructions": "Проверь алгоритм..."
}
```
### Создать цепочку
```bash
POST /api/challenge/chain
Content-Type: application/json
{
"name": "Моя цепочка",
"tasks": ["507f1f77bcf86cd799439011", "507f1f77bcf86cd799439012"]
}
```
### Получить статистику пользователя
```bash
GET /api/challenge/user/user001/stats
```
Ответ будет содержать динамически вычисленную статистику на основе всех попыток пользователя.
## ⚙️ Настройка задержки
По умолчанию все запросы имеют задержку 300ms для имитации сетевых запросов. Изменить можно в `index.js`:
```javascript
const timer = (time = 300) => (req, res, next) => setTimeout(next, time);
```
## 🚀 Использование
Стабы автоматически подключаются при запуске dev сервера:
```bash
npm start
```
Сервер будет доступен на `http://localhost:8099`
---
**Примечание:** Этот стабовый API предназначен только для разработки. В production окружении используйте реальный Challenge Service API.

View File

@@ -0,0 +1,70 @@
[
{
"_id": "607f1f77bcf86cd799439021",
"id": "607f1f77bcf86cd799439021",
"name": "Основы JavaScript",
"tasks": [
{
"_id": "507f1f77bcf86cd799439011",
"id": "507f1f77bcf86cd799439011",
"title": "Реализовать сортировку массива",
"description": "# Задание: Сортировка массива\n\nНапишите функцию `sortArray(arr)`, которая сортирует массив чисел по возрастанию.",
"createdAt": "2024-11-01T10:00:00.000Z",
"updatedAt": "2024-11-01T10:00:00.000Z"
},
{
"_id": "507f1f77bcf86cd799439015",
"id": "507f1f77bcf86cd799439015",
"title": "Валидация формы",
"description": "# Задание: Валидация email\n\nНапишите функцию для валидации email адреса.",
"createdAt": "2024-11-05T11:00:00.000Z",
"updatedAt": "2024-11-05T11:00:00.000Z"
}
],
"createdAt": "2024-11-01T09:00:00.000Z",
"updatedAt": "2024-11-05T12:00:00.000Z"
},
{
"_id": "607f1f77bcf86cd799439022",
"id": "607f1f77bcf86cd799439022",
"name": "React разработка",
"tasks": [
{
"_id": "507f1f77bcf86cd799439013",
"id": "507f1f77bcf86cd799439013",
"title": "Компонент React формы",
"description": "# Задание: Форма регистрации\n\nСоздайте компонент React для формы регистрации.",
"createdAt": "2024-11-03T09:15:00.000Z",
"updatedAt": "2024-11-03T09:15:00.000Z"
}
],
"createdAt": "2024-11-03T08:00:00.000Z",
"updatedAt": "2024-11-03T09:30:00.000Z"
},
{
"_id": "607f1f77bcf86cd799439023",
"id": "607f1f77bcf86cd799439023",
"name": "Backend разработка",
"tasks": [
{
"_id": "507f1f77bcf86cd799439012",
"id": "507f1f77bcf86cd799439012",
"title": "Создать REST API endpoint",
"description": "# Задание: REST API для пользователей\n\nСоздайте REST API endpoint для получения списка пользователей.",
"createdAt": "2024-11-02T12:30:00.000Z",
"updatedAt": "2024-11-02T12:30:00.000Z"
},
{
"_id": "507f1f77bcf86cd799439014",
"id": "507f1f77bcf86cd799439014",
"title": "SQL запрос с JOIN",
"description": "# Задание: SQL запрос\n\nНапишите SQL запрос для выборки всех заказов пользователя.",
"createdAt": "2024-11-04T14:20:00.000Z",
"updatedAt": "2024-11-04T14:20:00.000Z"
}
],
"createdAt": "2024-11-02T11:00:00.000Z",
"updatedAt": "2024-11-04T15:00:00.000Z"
}
]

21
stubs/api/data/stats.json Normal file
View File

@@ -0,0 +1,21 @@
{
"users": 8,
"tasks": 5,
"chains": 3,
"submissions": {
"total": 8,
"accepted": 5,
"rejected": 3,
"pending": 0,
"inProgress": 0
},
"averageCheckTimeMs": 3275,
"queue": {
"queueLength": 0,
"waiting": 0,
"inProgress": 0,
"maxConcurrency": 5,
"currentlyProcessing": 0
}
}

View File

@@ -0,0 +1,203 @@
[
{
"_id": "sub001",
"id": "sub001",
"user": {
"_id": "user001",
"id": "user001",
"nickname": "alex_student",
"createdAt": "2024-10-15T08:30:00.000Z"
},
"task": {
"_id": "507f1f77bcf86cd799439011",
"id": "507f1f77bcf86cd799439011",
"title": "Реализовать сортировку массива",
"description": "# Задание: Сортировка массива\n\nНапишите функцию `sortArray(arr)`, которая сортирует массив чисел по возрастанию.",
"createdAt": "2024-11-01T10:00:00.000Z",
"updatedAt": "2024-11-01T10:00:00.000Z"
},
"result": "function sortArray(arr) {\n return arr.sort((a, b) => a - b);\n}",
"status": "needs_revision",
"queueId": "q001",
"feedback": "Ваше решение изменяет исходный массив. Необходимо создать копию массива перед сортировкой. Используйте spread оператор или Array.from().",
"submittedAt": "2024-11-01T15:30:00.000Z",
"checkedAt": "2024-11-01T15:30:03.500Z",
"attemptNumber": 1
},
{
"_id": "sub002",
"id": "sub002",
"user": {
"_id": "user001",
"id": "user001",
"nickname": "alex_student",
"createdAt": "2024-10-15T08:30:00.000Z"
},
"task": {
"_id": "507f1f77bcf86cd799439011",
"id": "507f1f77bcf86cd799439011",
"title": "Реализовать сортировку массива",
"description": "# Задание: Сортировка массива",
"createdAt": "2024-11-01T10:00:00.000Z",
"updatedAt": "2024-11-01T10:00:00.000Z"
},
"result": "function sortArray(arr) {\n return [...arr].sort((a, b) => a - b);\n}",
"status": "accepted",
"queueId": "q002",
"feedback": "Отлично! Ваше решение корректно создаёт копию массива и сортирует её. Сложность O(n log n) соответствует требованиям.",
"submittedAt": "2024-11-01T15:45:00.000Z",
"checkedAt": "2024-11-01T15:45:02.800Z",
"attemptNumber": 2
},
{
"_id": "sub003",
"id": "sub003",
"user": {
"_id": "user002",
"id": "user002",
"nickname": "maria_dev",
"createdAt": "2024-10-16T10:15:00.000Z"
},
"task": {
"_id": "507f1f77bcf86cd799439013",
"id": "507f1f77bcf86cd799439013",
"title": "Компонент React формы",
"description": "# Задание: Форма регистрации",
"createdAt": "2024-11-03T09:15:00.000Z",
"updatedAt": "2024-11-03T09:15:00.000Z"
},
"result": "import React, { useState } from 'react';\n\nfunction RegistrationForm() {\n const [email, setEmail] = useState('');\n const [password, setPassword] = useState('');\n const [confirmPassword, setConfirmPassword] = useState('');\n const [errors, setErrors] = useState({});\n\n const handleSubmit = (e) => {\n e.preventDefault();\n const newErrors = {};\n if (!email.includes('@')) newErrors.email = 'Invalid email';\n if (password.length < 6) newErrors.password = 'Too short';\n if (password !== confirmPassword) newErrors.confirmPassword = 'Passwords do not match';\n \n if (Object.keys(newErrors).length === 0) {\n console.log('Form submitted');\n } else {\n setErrors(newErrors);\n }\n };\n\n return (\n <form onSubmit={handleSubmit}>\n <input value={email} onChange={e => setEmail(e.target.value)} />\n {errors.email && <span>{errors.email}</span>}\n <input type=\"password\" value={password} onChange={e => setPassword(e.target.value)} />\n {errors.password && <span>{errors.password}</span>}\n <input type=\"password\" value={confirmPassword} onChange={e => setConfirmPassword(e.target.value)} />\n {errors.confirmPassword && <span>{errors.confirmPassword}</span>}\n <button type=\"submit\">Register</button>\n </form>\n );\n}",
"status": "accepted",
"queueId": "q003",
"feedback": "Превосходно! Использованы controlled components, есть валидация, обработка ошибок и правильное управление state. Всё соответствует требованиям.",
"submittedAt": "2024-11-03T16:20:00.000Z",
"checkedAt": "2024-11-03T16:20:04.200Z",
"attemptNumber": 1
},
{
"_id": "sub004",
"id": "sub004",
"user": {
"_id": "user003",
"id": "user003",
"nickname": "ivan_coder",
"createdAt": "2024-10-17T14:20:00.000Z"
},
"task": {
"_id": "507f1f77bcf86cd799439012",
"id": "507f1f77bcf86cd799439012",
"title": "Создать REST API endpoint",
"description": "# Задание: REST API для пользователей",
"createdAt": "2024-11-02T12:30:00.000Z",
"updatedAt": "2024-11-02T12:30:00.000Z"
},
"result": "app.get('/api/users', async (req, res) => {\n const users = await User.find();\n res.json({ users });\n});",
"status": "needs_revision",
"queueId": "q004",
"feedback": "В решении отсутствует пагинация, обработка ошибок и валидация параметров. Необходимо добавить параметры page и limit, обернуть код в try-catch и валидировать входные данные.",
"submittedAt": "2024-11-02T17:00:00.000Z",
"checkedAt": "2024-11-02T17:00:03.100Z",
"attemptNumber": 1
},
{
"_id": "sub005",
"id": "sub005",
"user": {
"_id": "user004",
"id": "user004",
"nickname": "olga_js",
"createdAt": "2024-10-18T09:00:00.000Z"
},
"task": {
"_id": "507f1f77bcf86cd799439015",
"id": "507f1f77bcf86cd799439015",
"title": "Валидация формы",
"description": "# Задание: Валидация email",
"createdAt": "2024-11-05T11:00:00.000Z",
"updatedAt": "2024-11-05T11:00:00.000Z"
},
"result": "function validateEmail(email) {\n if (!email || email.trim() === '') return false;\n const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n return emailRegex.test(email);\n}",
"status": "accepted",
"queueId": "q005",
"feedback": "Отлично! Функция обрабатывает все edge cases: пустая строка, отсутствие @, отсутствие домена. Regex валидация корректная.",
"submittedAt": "2024-11-05T14:10:00.000Z",
"checkedAt": "2024-11-05T14:10:02.500Z",
"attemptNumber": 1
},
{
"_id": "sub006",
"id": "sub006",
"user": {
"_id": "user005",
"id": "user005",
"nickname": "dmitry_react",
"createdAt": "2024-10-20T11:45:00.000Z"
},
"task": {
"_id": "507f1f77bcf86cd799439011",
"id": "507f1f77bcf86cd799439011",
"title": "Реализовать сортировку массива",
"description": "# Задание: Сортировка массива",
"createdAt": "2024-11-01T10:00:00.000Z",
"updatedAt": "2024-11-01T10:00:00.000Z"
},
"result": "function sortArray(arr) {\n const result = [];\n for (let i = 0; i < arr.length; i++) {\n for (let j = i + 1; j < arr.length; j++) {\n if (arr[i] > arr[j]) {\n [arr[i], arr[j]] = [arr[j], arr[i]];\n }\n }\n result.push(arr[i]);\n }\n return result;\n}",
"status": "needs_revision",
"queueId": "q006",
"feedback": "Ваше решение использует bubble sort с сложностью O(n²), что не соответствует требованиям. Необходимо использовать алгоритм с сложностью O(n log n), например, встроенный метод sort().",
"submittedAt": "2024-11-01T18:30:00.000Z",
"checkedAt": "2024-11-01T18:30:03.900Z",
"attemptNumber": 1
},
{
"_id": "sub007",
"id": "sub007",
"user": {
"_id": "user006",
"id": "user006",
"nickname": "anna_frontend",
"createdAt": "2024-10-22T16:30:00.000Z"
},
"task": {
"_id": "507f1f77bcf86cd799439014",
"id": "507f1f77bcf86cd799439014",
"title": "SQL запрос с JOIN",
"description": "# Задание: SQL запрос",
"createdAt": "2024-11-04T14:20:00.000Z",
"updatedAt": "2024-11-04T14:20:00.000Z"
},
"result": "SELECT users.name, users.email, orders.id as order_id, orders.created_at,\n products.name as product_name, products.price, order_items.quantity\nFROM users\nINNER JOIN orders ON users.id = orders.user_id\nINNER JOIN order_items ON orders.id = order_items.order_id\nINNER JOIN products ON order_items.product_id = products.id\nWHERE orders.status = 'active'\nORDER BY orders.created_at DESC;",
"status": "accepted",
"queueId": "q007",
"feedback": "Отличный запрос! Использованы правильные JOIN'ы, добавлена фильтрация по активным заказам и сортировка по дате. Всё соответствует требованиям.",
"submittedAt": "2024-11-04T20:15:00.000Z",
"checkedAt": "2024-11-04T20:15:02.700Z",
"attemptNumber": 1
},
{
"_id": "sub008",
"id": "sub008",
"user": {
"_id": "user007",
"id": "user007",
"nickname": "sergey_backend",
"createdAt": "2024-10-25T13:00:00.000Z"
},
"task": {
"_id": "507f1f77bcf86cd799439012",
"id": "507f1f77bcf86cd799439012",
"title": "Создать REST API endpoint",
"description": "# Задание: REST API для пользователей",
"createdAt": "2024-11-02T12:30:00.000Z",
"updatedAt": "2024-11-02T12:30:00.000Z"
},
"result": "app.get('/api/users', async (req, res) => {\n try {\n const page = parseInt(req.query.page) || 1;\n const limit = parseInt(req.query.limit) || 10;\n \n if (page < 1 || limit < 1 || limit > 100) {\n return res.status(400).json({ error: 'Invalid pagination parameters' });\n }\n \n const skip = (page - 1) * limit;\n const users = await User.find().skip(skip).limit(limit);\n const total = await User.countDocuments();\n \n res.json({\n users,\n total,\n page,\n limit\n });\n } catch (error) {\n res.status(500).json({ error: 'Internal server error' });\n }\n});",
"status": "accepted",
"queueId": "q008",
"feedback": "Превосходная работа! Есть пагинация, валидация параметров, обработка ошибок. Код чистый и следует best practices.",
"submittedAt": "2024-11-02T21:30:00.000Z",
"checkedAt": "2024-11-02T21:30:04.100Z",
"attemptNumber": 1
}
]

72
stubs/api/data/tasks.json Normal file
View File

@@ -0,0 +1,72 @@
[
{
"_id": "507f1f77bcf86cd799439011",
"id": "507f1f77bcf86cd799439011",
"title": "Реализовать сортировку массива",
"description": "# Задание: Сортировка массива\n\nНапишите функцию `sortArray(arr)`, которая сортирует массив чисел по возрастанию.\n\n## Требования:\n\n- Функция должна принимать массив чисел\n- Возвращать отсортированный массив\n- Не изменять исходный массив\n\n## Пример:\n\n```javascript\nconst arr = [5, 2, 8, 1, 9];\nconst sorted = sortArray(arr);\nconsole.log(sorted); // [1, 2, 5, 8, 9]\n```",
"hiddenInstructions": "Проверь, чтобы сложность алгоритма была не хуже O(n log n). Не принимай bubble sort или простые O(n²) решения. Убедись, что исходный массив не изменяется.",
"creator": {
"sub": "teacher-123",
"preferred_username": "ivanov_teacher",
"email": "ivanov@example.com"
},
"createdAt": "2024-11-01T10:00:00.000Z",
"updatedAt": "2024-11-01T10:00:00.000Z"
},
{
"_id": "507f1f77bcf86cd799439012",
"id": "507f1f77bcf86cd799439012",
"title": "Создать REST API endpoint",
"description": "# Задание: REST API для пользователей\n\nСоздайте REST API endpoint для получения списка пользователей.\n\n## Требования:\n\n- Метод: GET\n- Путь: /api/users\n- Должна быть пагинация\n- Обработка ошибок\n- Валидация параметров\n\n## Пример ответа:\n\n```json\n{\n \"users\": [...],\n \"total\": 100,\n \"page\": 1,\n \"limit\": 10\n}\n```",
"hiddenInstructions": "Обязательна пагинация, обработка ошибок и валидация параметров. Если чего-то не хватает - укажи в feedback.",
"creator": {
"sub": "teacher-123",
"preferred_username": "ivanov_teacher",
"email": "ivanov@example.com"
},
"createdAt": "2024-11-02T12:30:00.000Z",
"updatedAt": "2024-11-02T12:30:00.000Z"
},
{
"_id": "507f1f77bcf86cd799439013",
"id": "507f1f77bcf86cd799439013",
"title": "Компонент React формы",
"description": "# Задание: Форма регистрации\n\nСоздайте компонент React для формы регистрации.\n\n## Требования:\n\n- Поля: email, password, confirmPassword\n- Валидация на стороне клиента\n- Использование controlled components\n- Обработка submit\n\n## Бонус:\n\n- TypeScript типы\n- Показ ошибок валидации",
"hiddenInstructions": "Обязательна валидация на стороне клиента, использование controlled components, и правильное управление state. Если используются uncontrolled components - отправь на доработку.",
"creator": {
"sub": "teacher-456",
"preferred_username": "petrova_teacher",
"email": "petrova@example.com"
},
"createdAt": "2024-11-03T09:15:00.000Z",
"updatedAt": "2024-11-03T09:15:00.000Z"
},
{
"_id": "507f1f77bcf86cd799439014",
"id": "507f1f77bcf86cd799439014",
"title": "SQL запрос с JOIN",
"description": "# Задание: SQL запрос\n\nНапишите SQL запрос для выборки всех заказов пользователя вместе с информацией о товарах.\n\n## Структура таблиц:\n\n- users (id, name, email)\n- orders (id, user_id, created_at)\n- order_items (id, order_id, product_id, quantity)\n- products (id, name, price)\n\n## Требования:\n\n- Использовать JOIN\n- Отсортировать по дате создания заказа\n- Показать только активные заказы",
"creator": {
"sub": "teacher-123",
"preferred_username": "ivanov_teacher",
"email": "ivanov@example.com"
},
"createdAt": "2024-11-04T14:20:00.000Z",
"updatedAt": "2024-11-04T14:20:00.000Z"
},
{
"_id": "507f1f77bcf86cd799439015",
"id": "507f1f77bcf86cd799439015",
"title": "Валидация формы",
"description": "# Задание: Валидация email\n\nНапишите функцию для валидации email адреса.\n\n## Требования:\n\n- Проверка формата email\n- Возвращает true/false\n- Обработка edge cases\n\n## Примеры:\n\n```javascript\nvalidateEmail('test@example.com') // true\nvalidateEmail('invalid-email') // false\nvalidateEmail('') // false\n```",
"hiddenInstructions": "Проверь, что функция обрабатывает edge cases: пустая строка, нет @, нет домена, множественные @. Если не все случаи покрыты - отправь на доработку.",
"creator": {
"sub": "teacher-456",
"preferred_username": "petrova_teacher",
"email": "petrova@example.com"
},
"createdAt": "2024-11-05T11:00:00.000Z",
"updatedAt": "2024-11-05T11:00:00.000Z"
}
]

51
stubs/api/data/users.json Normal file
View File

@@ -0,0 +1,51 @@
[
{
"_id": "user001",
"id": "user001",
"nickname": "alex_student",
"createdAt": "2024-10-15T08:30:00.000Z"
},
{
"_id": "user002",
"id": "user002",
"nickname": "maria_dev",
"createdAt": "2024-10-16T10:15:00.000Z"
},
{
"_id": "user003",
"id": "user003",
"nickname": "ivan_coder",
"createdAt": "2024-10-17T14:20:00.000Z"
},
{
"_id": "user004",
"id": "user004",
"nickname": "olga_js",
"createdAt": "2024-10-18T09:00:00.000Z"
},
{
"_id": "user005",
"id": "user005",
"nickname": "dmitry_react",
"createdAt": "2024-10-20T11:45:00.000Z"
},
{
"_id": "user006",
"id": "user006",
"nickname": "anna_frontend",
"createdAt": "2024-10-22T16:30:00.000Z"
},
{
"_id": "user007",
"id": "user007",
"nickname": "sergey_backend",
"createdAt": "2024-10-25T13:00:00.000Z"
},
{
"_id": "user008",
"id": "user008",
"nickname": "elena_fullstack",
"createdAt": "2024-10-28T10:00:00.000Z"
}
]

391
stubs/api/index.js Normal file
View File

@@ -0,0 +1,391 @@
const router = require('express').Router();
const path = require('path');
const fs = require('fs');
const timer = (time = 300) => (req, res, next) => setTimeout(next, time);
// Helper functions
const loadJSON = (filename) => {
const filePath = path.join(__dirname, 'data', filename);
const data = fs.readFileSync(filePath, 'utf8');
return JSON.parse(data);
};
const respond = (res, data) => {
res.json({ error: null, data });
};
const respondError = (res, message, statusCode = 400) => {
res.status(statusCode).json({
error: { message },
data: null
});
};
// In-memory storage (resets on server restart)
let tasksCache = null;
let chainsCache = null;
let usersCache = null;
let submissionsCache = null;
let statsCache = null;
const getTasks = () => {
if (!tasksCache) tasksCache = loadJSON('tasks.json');
return tasksCache;
};
const getChains = () => {
if (!chainsCache) chainsCache = loadJSON('chains.json');
return chainsCache;
};
const getUsers = () => {
if (!usersCache) usersCache = loadJSON('users.json');
return usersCache;
};
const getSubmissions = () => {
if (!submissionsCache) submissionsCache = loadJSON('submissions.json');
return submissionsCache;
};
const getStats = () => {
if (!statsCache) statsCache = loadJSON('stats.json');
return statsCache;
};
router.use(timer());
// ============= TASKS =============
// GET /api/challenge/tasks
router.get('/challenge/tasks', (req, res) => {
const tasks = getTasks();
respond(res, tasks);
});
// GET /api/challenge/task/:id
router.get('/challenge/task/:id', (req, res) => {
const tasks = getTasks();
const task = tasks.find(t => t.id === req.params.id);
if (!task) {
return respondError(res, 'Task not found', 404);
}
respond(res, task);
});
// POST /api/challenge/task
router.post('/challenge/task', (req, res) => {
const { title, description, hiddenInstructions } = req.body;
if (!title || !description) {
return respondError(res, 'Title and description are required');
}
const tasks = getTasks();
const newTask = {
_id: `task_${Date.now()}`,
id: `task_${Date.now()}`,
title,
description,
hiddenInstructions: hiddenInstructions || undefined,
creator: {
sub: 'teacher-123',
preferred_username: 'current_teacher',
email: 'teacher@example.com'
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
tasks.push(newTask);
// Update stats
const stats = getStats();
stats.tasks = tasks.length;
respond(res, newTask);
});
// PUT /api/challenge/task/:id
router.put('/challenge/task/:id', (req, res) => {
const tasks = getTasks();
const taskIndex = tasks.findIndex(t => t.id === req.params.id);
if (taskIndex === -1) {
return respondError(res, 'Task not found', 404);
}
const { title, description, hiddenInstructions } = req.body;
const task = tasks[taskIndex];
if (title) task.title = title;
if (description) task.description = description;
if (hiddenInstructions !== undefined) {
task.hiddenInstructions = hiddenInstructions || undefined;
}
task.updatedAt = new Date().toISOString();
respond(res, task);
});
// DELETE /api/challenge/task/:id
router.delete('/challenge/task/:id', (req, res) => {
const tasks = getTasks();
const taskIndex = tasks.findIndex(t => t.id === req.params.id);
if (taskIndex === -1) {
return respondError(res, 'Task not found', 404);
}
tasks.splice(taskIndex, 1);
// Update stats
const stats = getStats();
stats.tasks = tasks.length;
respond(res, { success: true });
});
// ============= CHAINS =============
// GET /api/challenge/chains
router.get('/challenge/chains', (req, res) => {
const chains = getChains();
respond(res, chains);
});
// GET /api/challenge/chain/:id
router.get('/challenge/chain/:id', (req, res) => {
const chains = getChains();
const chain = chains.find(c => c.id === req.params.id);
if (!chain) {
return respondError(res, 'Chain not found', 404);
}
respond(res, chain);
});
// POST /api/challenge/chain
router.post('/challenge/chain', (req, res) => {
const { name, tasks } = req.body;
if (!name || !tasks || !Array.isArray(tasks)) {
return respondError(res, 'Name and tasks array are required');
}
const chains = getChains();
const allTasks = getTasks();
// Populate tasks
const populatedTasks = tasks.map(taskId => {
const task = allTasks.find(t => t.id === taskId);
return task ? {
_id: task._id,
id: task.id,
title: task.title,
description: task.description,
createdAt: task.createdAt,
updatedAt: task.updatedAt
} : null;
}).filter(t => t !== null);
const newChain = {
_id: `chain_${Date.now()}`,
id: `chain_${Date.now()}`,
name,
tasks: populatedTasks,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
chains.push(newChain);
// Update stats
const stats = getStats();
stats.chains = chains.length;
respond(res, newChain);
});
// PUT /api/challenge/chain/:id
router.put('/challenge/chain/:id', (req, res) => {
const chains = getChains();
const chainIndex = chains.findIndex(c => c.id === req.params.id);
if (chainIndex === -1) {
return respondError(res, 'Chain not found', 404);
}
const { name, tasks } = req.body;
const chain = chains[chainIndex];
if (name) chain.name = name;
if (tasks && Array.isArray(tasks)) {
const allTasks = getTasks();
const populatedTasks = tasks.map(taskId => {
const task = allTasks.find(t => t.id === taskId);
return task ? {
_id: task._id,
id: task.id,
title: task.title,
description: task.description,
createdAt: task.createdAt,
updatedAt: task.updatedAt
} : null;
}).filter(t => t !== null);
chain.tasks = populatedTasks;
}
chain.updatedAt = new Date().toISOString();
respond(res, chain);
});
// DELETE /api/challenge/chain/:id
router.delete('/challenge/chain/:id', (req, res) => {
const chains = getChains();
const chainIndex = chains.findIndex(c => c.id === req.params.id);
if (chainIndex === -1) {
return respondError(res, 'Chain not found', 404);
}
chains.splice(chainIndex, 1);
// Update stats
const stats = getStats();
stats.chains = chains.length;
respond(res, { success: true });
});
// ============= USERS =============
// GET /api/challenge/users
router.get('/challenge/users', (req, res) => {
const users = getUsers();
respond(res, users);
});
// ============= STATS =============
// GET /api/challenge/stats
router.get('/challenge/stats', (req, res) => {
const stats = getStats();
respond(res, stats);
});
// GET /api/challenge/user/:userId/stats
router.get('/challenge/user/:userId/stats', (req, res) => {
const users = getUsers();
const submissions = getSubmissions();
const chains = getChains();
const user = users.find(u => u.id === req.params.userId);
if (!user) {
return respondError(res, 'User not found', 404);
}
const userSubmissions = submissions.filter(s => s.user.id === req.params.userId);
// Calculate stats
const completedTasks = new Set();
const taskStats = {};
userSubmissions.forEach(sub => {
const taskId = sub.task.id;
if (!taskStats[taskId]) {
taskStats[taskId] = {
taskId: taskId,
taskTitle: sub.task.title,
attempts: [],
totalAttempts: 0,
status: 'not_attempted',
lastAttemptAt: null
};
}
taskStats[taskId].attempts.push({
attemptNumber: sub.attemptNumber,
status: sub.status,
submittedAt: sub.submittedAt,
checkedAt: sub.checkedAt,
feedback: sub.feedback
});
taskStats[taskId].totalAttempts++;
taskStats[taskId].status = sub.status;
taskStats[taskId].lastAttemptAt = sub.submittedAt;
if (sub.status === 'accepted') {
completedTasks.add(taskId);
}
});
const taskStatsArray = Object.values(taskStats);
// Chain stats
const chainStats = chains.map(chain => {
const completedInChain = chain.tasks.filter(t => completedTasks.has(t.id)).length;
return {
chainId: chain.id,
chainName: chain.name,
totalTasks: chain.tasks.length,
completedTasks: completedInChain,
progress: chain.tasks.length > 0 ? (completedInChain / chain.tasks.length * 100) : 0
};
});
const totalCheckTime = userSubmissions
.filter(s => s.checkedAt)
.reduce((sum, s) => {
const submitted = new Date(s.submittedAt).getTime();
const checked = new Date(s.checkedAt).getTime();
return sum + (checked - submitted);
}, 0);
const userStats = {
totalTasksAttempted: taskStatsArray.length,
completedTasks: completedTasks.size,
inProgressTasks: taskStatsArray.filter(t => t.status === 'in_progress').length,
needsRevisionTasks: taskStatsArray.filter(t => t.status === 'needs_revision').length,
totalSubmissions: userSubmissions.length,
averageCheckTimeMs: userSubmissions.length > 0 ? totalCheckTime / userSubmissions.length : 0,
taskStats: taskStatsArray,
chainStats: chainStats
};
respond(res, userStats);
});
// ============= SUBMISSIONS =============
// GET /api/challenge/submissions
router.get('/challenge/submissions', (req, res) => {
const submissions = getSubmissions();
respond(res, submissions);
});
// GET /api/challenge/user/:userId/submissions
router.get('/challenge/user/:userId/submissions', (req, res) => {
const submissions = getSubmissions();
const taskId = req.query.taskId;
let filtered = submissions.filter(s => s.user.id === req.params.userId);
if (taskId) {
filtered = filtered.filter(s => s.task.id === taskId);
}
respond(res, filtered);
});
module.exports = router;