320 lines
11 KiB
JavaScript
320 lines
11 KiB
JavaScript
const http = require("http");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const mammoth = require("mammoth");
|
|
const pdfParse = require("pdf-parse");
|
|
const XLSX = require("xlsx");
|
|
const formidable = require("formidable");
|
|
|
|
const PORT = process.env.PORT || 3000;
|
|
const DATA_FILE = path.join(__dirname, "data.json");
|
|
const UPLOADS_DIR = path.join(__dirname, "uploads");
|
|
|
|
if (!fs.existsSync(UPLOADS_DIR)) fs.mkdirSync(UPLOADS_DIR);
|
|
if (!fs.existsSync(DATA_FILE)) {
|
|
fs.writeFileSync(DATA_FILE, JSON.stringify({ tests: [], videos: [], users: {}, results: [] }));
|
|
}
|
|
|
|
function loadData() {
|
|
try { return JSON.parse(fs.readFileSync(DATA_FILE, "utf8")); } catch { return { tests: [], videos: [], users: {}, results: [] }; }
|
|
}
|
|
function saveData(data) {
|
|
fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
|
|
}
|
|
|
|
function body(req) {
|
|
return new Promise((resolve) => {
|
|
let s = "";
|
|
req.on("data", (c) => (s += c));
|
|
req.on("end", () => { try { resolve(JSON.parse(s || "{}")); } catch { resolve({}); } });
|
|
});
|
|
}
|
|
|
|
async function parseTestFile(filepath, filename) {
|
|
const ext = path.extname(filename).toLowerCase();
|
|
let questions = [];
|
|
|
|
if (ext === ".docx") {
|
|
const result = await mammoth.extractRawText({ path: filepath });
|
|
const lines = result.value.split("\n").filter(l => l.trim());
|
|
let currentQ = null;
|
|
let options = [];
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (/^\d+[\.\)]/.test(trimmed)) {
|
|
if (currentQ && options.length >= 4) {
|
|
questions.push({ q: currentQ, options, correct: 0 });
|
|
}
|
|
currentQ = trimmed.replace(/^\d+[\.\)]\s*/, "");
|
|
options = [];
|
|
} else if (/^[A-ЯA-Z]\)/i.test(trimmed)) {
|
|
options.push(trimmed.replace(/^[A-ЯA-Z]\)\s*/, ""));
|
|
}
|
|
}
|
|
if (currentQ && options.length >= 4) {
|
|
questions.push({ q: currentQ, options, correct: 0 });
|
|
}
|
|
}
|
|
else if (ext === ".pdf") {
|
|
const data = await pdfParse(fs.readFileSync(filepath));
|
|
const text = data.text;
|
|
const lines = text.split("\n").filter(l => l.trim());
|
|
let currentQ = null;
|
|
let options = [];
|
|
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (/^\d+[\.\)]/.test(trimmed)) {
|
|
if (currentQ && options.length >= 4) {
|
|
questions.push({ q: currentQ, options, correct: 0 });
|
|
}
|
|
currentQ = trimmed.replace(/^\d+[\.\)]\s*/, "");
|
|
options = [];
|
|
} else if (/^[A-ЯA-Z]\)/i.test(trimmed)) {
|
|
options.push(trimmed.replace(/^[A-ЯA-Z]\)\s*/, ""));
|
|
}
|
|
}
|
|
if (currentQ && options.length >= 4) {
|
|
questions.push({ q: currentQ, options, correct: 0 });
|
|
}
|
|
}
|
|
else if (ext === ".xlsx" || ext === ".xls") {
|
|
const wb = XLSX.readFile(filepath);
|
|
const sheet = wb.Sheets[wb.SheetNames[0]];
|
|
const rows = XLSX.utils.sheet_to_json(sheet, { header: 1 });
|
|
|
|
for (const row of rows) {
|
|
if (row.length >= 6 && row[0]) {
|
|
questions.push({
|
|
q: String(row[0]),
|
|
options: [String(row[1]||""), String(row[2]||""), String(row[3]||""), String(row[4]||"")],
|
|
correct: parseInt(row[5]) || 0
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return questions;
|
|
}
|
|
|
|
http.createServer(async (req, res) => {
|
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
|
|
if (req.method === "OPTIONS") {
|
|
res.writeHead(200);
|
|
return res.end();
|
|
}
|
|
|
|
// API: Get all tests
|
|
if (req.method === "GET" && req.url === "/api/tests") {
|
|
const data = loadData();
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
return res.end(JSON.stringify(data.tests));
|
|
}
|
|
|
|
// API: Upload test file
|
|
if (req.method === "POST" && req.url === "/api/upload-test") {
|
|
const form = formidable({ uploadDir: UPLOADS_DIR, keepExtensions: true });
|
|
form.parse(req, async (err, fields, files) => {
|
|
if (err) {
|
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
return res.end(JSON.stringify({ error: err.message }));
|
|
}
|
|
|
|
const file = files.file && files.file[0];
|
|
if (!file) {
|
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
return res.end(JSON.stringify({ error: "Файл табылмады" }));
|
|
}
|
|
|
|
try {
|
|
const questions = await parseTestFile(file.filepath, file.originalFilename);
|
|
if (questions.length === 0) {
|
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
return res.end(JSON.stringify({ error: "Сұрақтар табылмады. Форматты тексеріңіз." }));
|
|
}
|
|
|
|
const data = loadData();
|
|
const newTest = {
|
|
id: Date.now(),
|
|
title: fields.title || path.basename(file.originalFilename, path.extname(file.originalFilename)),
|
|
topicId: parseInt(fields.topicId) || 1,
|
|
questions,
|
|
createdAt: new Date().toISOString()
|
|
};
|
|
data.tests.push(newTest);
|
|
saveData(data);
|
|
|
|
fs.unlinkSync(file.filepath);
|
|
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
return res.end(JSON.stringify({ ok: true, count: questions.length, test: newTest }));
|
|
} catch (e) {
|
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
return res.end(JSON.stringify({ error: e.message }));
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
// API: Upload video
|
|
if (req.method === "POST" && req.url === "/api/upload-video") {
|
|
const form = formidable({ uploadDir: UPLOADS_DIR, keepExtensions: true });
|
|
form.parse(req, (err, fields, files) => {
|
|
if (err) {
|
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
return res.end(JSON.stringify({ error: err.message }));
|
|
}
|
|
|
|
const file = files.file && files.file[0];
|
|
if (!file) {
|
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
return res.end(JSON.stringify({ error: "Файл табылмады" }));
|
|
}
|
|
|
|
const data = loadData();
|
|
const newVideo = {
|
|
id: Date.now(),
|
|
title: fields.title || path.basename(file.originalFilename, path.extname(file.originalFilename)),
|
|
topic: fields.topic || "Жалпы",
|
|
filename: file.newFilename,
|
|
originalName: file.originalFilename,
|
|
size: file.size,
|
|
createdAt: new Date().toISOString()
|
|
};
|
|
data.videos.push(newVideo);
|
|
saveData(data);
|
|
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
return res.end(JSON.stringify({ ok: true, video: newVideo }));
|
|
});
|
|
return;
|
|
}
|
|
|
|
// API: Get all videos
|
|
if (req.method === "GET" && req.url === "/api/videos") {
|
|
const data = loadData();
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
return res.end(JSON.stringify(data.videos));
|
|
}
|
|
|
|
// API: Get video file
|
|
if (req.method === "GET" && req.url.startsWith("/api/video/")) {
|
|
const filename = req.url.split("/")[3];
|
|
const videopath = path.join(UPLOADS_DIR, filename);
|
|
if (fs.existsSync(videopath)) {
|
|
res.writeHead(200, { "Content-Type": "video/mp4" });
|
|
return fs.createReadStream(videopath).pipe(res);
|
|
}
|
|
res.writeHead(404);
|
|
return res.end("Not found");
|
|
}
|
|
|
|
// API: Save result
|
|
if (req.method === "POST" && req.url === "/api/result") {
|
|
const result = await body(req);
|
|
const data = loadData();
|
|
data.results.push(result);
|
|
|
|
// Update user stats in data.json
|
|
if (!data.users[result.username]) {
|
|
data.users[result.username] = { tests: [], totalTime: 0 };
|
|
}
|
|
data.users[result.username].tests.push({
|
|
test: result.test,
|
|
percent: result.percent,
|
|
date: result.date
|
|
});
|
|
data.users[result.username].totalTime += result.time;
|
|
saveData(data);
|
|
|
|
// Also update in localStorage format for students
|
|
try {
|
|
const studentsFile = path.join(__dirname, 'students.json');
|
|
let students = {};
|
|
if (fs.existsSync(studentsFile)) {
|
|
students = JSON.parse(fs.readFileSync(studentsFile, 'utf8'));
|
|
}
|
|
if (students[result.username]) {
|
|
if (!students[result.username].tests) students[result.username].tests = [];
|
|
students[result.username].tests.push({
|
|
test: result.test,
|
|
percent: result.percent,
|
|
date: result.date
|
|
});
|
|
students[result.username].totalTime = (students[result.username].totalTime || 0) + result.time;
|
|
fs.writeFileSync(studentsFile, JSON.stringify(students, null, 2));
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to update students.json:', e);
|
|
}
|
|
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
return res.end(JSON.stringify({ ok: true }));
|
|
}
|
|
|
|
// API: Get user stats
|
|
if (req.method === "GET" && req.url.startsWith("/api/user/")) {
|
|
const username = req.url.split("/")[3];
|
|
const data = loadData();
|
|
const user = data.users[username] || { tests: [], totalTime: 0 };
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
return res.end(JSON.stringify(user));
|
|
}
|
|
|
|
// API: Get all students (admin only)
|
|
if (req.method === "GET" && req.url === "/api/students") {
|
|
try {
|
|
const studentsFile = path.join(__dirname, 'students.json');
|
|
let students = {};
|
|
if (fs.existsSync(studentsFile)) {
|
|
students = JSON.parse(fs.readFileSync(studentsFile, 'utf8'));
|
|
}
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
return res.end(JSON.stringify(students));
|
|
} catch (e) {
|
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
return res.end(JSON.stringify({ error: e.message }));
|
|
}
|
|
}
|
|
|
|
// API: Ban/unban student
|
|
if (req.method === "POST" && req.url === "/api/ban-student") {
|
|
const { phone, banned } = await body(req);
|
|
try {
|
|
const studentsFile = path.join(__dirname, 'students.json');
|
|
let students = {};
|
|
if (fs.existsSync(studentsFile)) {
|
|
students = JSON.parse(fs.readFileSync(studentsFile, 'utf8'));
|
|
}
|
|
if (students[phone]) {
|
|
students[phone].banned = banned;
|
|
fs.writeFileSync(studentsFile, JSON.stringify(students, null, 2));
|
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
return res.end(JSON.stringify({ ok: true }));
|
|
}
|
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
return res.end(JSON.stringify({ error: 'Оқушы табылмады' }));
|
|
} catch (e) {
|
|
res.writeHead(500, { "Content-Type": "application/json" });
|
|
return res.end(JSON.stringify({ error: e.message }));
|
|
}
|
|
}
|
|
|
|
// Static files
|
|
let file = req.url === "/" ? "/index.html" : req.url.split("?")[0];
|
|
const full = path.join(__dirname, file);
|
|
if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) {
|
|
const ext = path.extname(full);
|
|
const type = ext === ".css" ? "text/css" : ext === ".js" ? "application/javascript" : ext === ".mp4" ? "video/mp4" : "text/html";
|
|
res.writeHead(200, { "Content-Type": type + "; charset=utf-8" });
|
|
return res.end(fs.readFileSync(full));
|
|
}
|
|
|
|
res.writeHead(404);
|
|
res.end("Not found");
|
|
}).listen(PORT, () => console.log("Сервер запущен на порту " + PORT));
|