This commit is contained in:
Andras Schmelczer 2026-06-06 19:37:55 +01:00
parent 52eb07a993
commit 0fde080ab8
8 changed files with 555 additions and 376 deletions

View file

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="hu">
<head>
<meta charset="UTF-8" />

156
backend/server.test.js Normal file
View file

@ -0,0 +1,156 @@
const { test, before, after, beforeEach } = require("node:test");
const assert = require("node:assert");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
// Point the server at a throwaway data dir before importing it, so tests never
// touch real question data.
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "fizika-test-"));
const dataPath = path.join(tmpDir, "fizika.json");
const picsPath = path.join(tmpDir, "pics");
fs.mkdirSync(picsPath, { recursive: true });
process.env.DATA_PATH = dataPath;
process.env.PICS_PATH = picsPath;
const { app, isUnsafeFilename } = require("./server");
const seedQuestions = [
{
id: 1,
source: "2016/m1/1",
description: "q1",
a: "a",
b: "b",
c: "c",
d: "d",
correct: 1,
type: "mk",
image: null,
},
{
id: 2,
source: "2016/m1/2",
description: "q2",
a: "a",
b: "b",
c: "c",
d: "d",
correct: 2,
type: "md",
image: null,
},
];
let server;
let baseUrl;
const api = (route, options) => fetch(`${baseUrl}${route}`, options);
const sendJson = (route, method, body) =>
api(route, {
method,
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
before(async () => {
server = app.listen(0);
await new Promise((resolve) => server.once("listening", resolve));
baseUrl = `http://127.0.0.1:${server.address().port}`;
});
after(() => {
server.close();
fs.rmSync(tmpDir, { recursive: true, force: true });
});
beforeEach(() => {
fs.writeFileSync(dataPath, JSON.stringify(seedQuestions, null, 2));
});
test("GET /api/fizika returns every question", async () => {
const res = await api("/api/fizika");
assert.equal(res.status, 200);
const body = await res.json();
assert.equal(body.length, 2);
});
test("POST creates a question with an incremented id", async () => {
const res = await sendJson("/api/admin/questions", "POST", {
source: "2020/1/1",
description: "new",
a: "a",
b: "b",
c: "c",
d: "d",
correct: 3,
type: "h",
image: null,
});
assert.equal(res.status, 201);
const created = await res.json();
assert.equal(created.id, 3);
const all = await (await api("/api/fizika")).json();
assert.equal(all.length, 3);
});
test("PUT updates an existing question", async () => {
const res = await sendJson("/api/admin/questions/1", "PUT", {
description: "updated",
});
assert.equal(res.status, 200);
const updated = await res.json();
assert.equal(updated.description, "updated");
// unchanged fields are preserved
assert.equal(updated.source, "2016/m1/1");
});
test("PUT returns 404 for a missing question", async () => {
const res = await sendJson("/api/admin/questions/999", "PUT", { a: "x" });
assert.equal(res.status, 404);
});
test("DELETE removes a question", async () => {
const res = await api("/api/admin/questions/1", { method: "DELETE" });
assert.equal(res.status, 200);
const all = await (await api("/api/fizika")).json();
assert.equal(all.length, 1);
assert.equal(all[0].id, 2);
});
test("DELETE returns 404 for a missing question", async () => {
const res = await api("/api/admin/questions/999", { method: "DELETE" });
assert.equal(res.status, 404);
});
test("DELETE image rejects an unsafe filename", async () => {
// Percent-encoded so the path survives client-side URL normalization and
// reaches the route with a separator the guard must reject.
const res = await api(`/api/admin/images/${encodeURIComponent("a\\b")}`, {
method: "DELETE",
});
assert.equal(res.status, 400);
});
test("DELETE image returns 404 for a missing but valid filename", async () => {
const res = await api("/api/admin/images/nope.png", { method: "DELETE" });
assert.equal(res.status, 404);
});
test("unknown routes return 404 JSON", async () => {
const res = await api("/api/does-not-exist");
assert.equal(res.status, 404);
const body = await res.json();
assert.equal(body.error, "Not found");
});
test("isUnsafeFilename blocks traversal but allows plain names", () => {
for (const bad of ["../etc", "a/b", "a\\b", "..", ".", "/abs.png", ""]) {
assert.equal(isUnsafeFilename(bad), true, `${bad} should be unsafe`);
}
for (const ok of ["pic.png", "image_1.jpg", "2016-m1-1.png"]) {
assert.equal(isUnsafeFilename(ok), false, `${ok} should be safe`);
}
});

View file

@ -12,7 +12,9 @@
border-style: solid;
border-width: 3px;
border-color: #003366;
box-shadow: 0 8px 16px 0 rgba(0,0,0,0.15), 0 6px 20px 0 rgba(0,0,0,0.15);
box-shadow:
0 8px 16px 0 rgba(0, 0, 0, 0.15),
0 6px 20px 0 rgba(0, 0, 0, 0.15);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
@ -28,4 +30,3 @@
margin: 0.15em;
}
}

View file

@ -3,7 +3,9 @@
padding: 2em;
text-align: left;
margin: 2em 0em 2em 0em;
box-shadow: 0 8px 16px 0 rgba(0,0,0,0.5), 0 6px 20px 0 rgba(0,0,0,0.09);
box-shadow:
0 8px 16px 0 rgba(0, 0, 0, 0.5),
0 6px 20px 0 rgba(0, 0, 0, 0.09);
height: auto;
width: auto;
overflow: auto;
@ -14,7 +16,7 @@ h2 {
font-size: 2em;
margin: auto;
text-align: center;
color: #0A0F14;
color: #0a0f14;
}
pre {
text-align: justify;

View file

@ -19,16 +19,17 @@
text-shadow: none;
}
h1:hover {
text-shadow: 0.3px 0.3px #002D5B,
text-shadow:
0.3px 0.3px #002d5b,
0.6px 0.6px #002851,
1px 1px #002347,
1.3px 1.3px #001E3D,
1.3px 1.3px #001e3d,
1.6px 1.6px #001933,
2px 2px #001326,
2.3px 2.3px #000E1C,
2.6px 2.6px #000A14,
3px 3px #00070F,
3.3px 3.3px #00050A,
2.3px 2.3px #000e1c,
2.6px 2.6px #000a14,
3px 3px #00070f,
3.3px 3.3px #00050a,
3.6px 3.6px #000205,
4px 4px #000000;
}
@ -75,7 +76,7 @@ li {
}
li:hover {
cursor: pointer;
text-shadow: 1px 1px 1px #0A0F14;
text-shadow: 1px 1px 1px #0a0f14;
}
#wrapper {
width: 75%;
@ -85,7 +86,8 @@ li:hover {
#bfooldal {
font-weight: 700;
}
.szoveg, #buttonwrapper {
.szoveg,
#buttonwrapper {
width: 80%;
margin: auto;
}
@ -96,7 +98,7 @@ p {
font-family: "Open sans", sans-serif;
font-weight: 300;
font-size: 1.3em;
color: #0A0F14;
color: #0a0f14;
text-align: justify;
margin: auto;
padding: 0.5em 0;
@ -107,7 +109,10 @@ p:last-of-type {
.buttonwrapper {
text-align: center;
}
#teszt, #eredmenyek, #kereses, #temakor {
#teszt,
#eredmenyek,
#kereses,
#temakor {
display: none;
}
#temak {
@ -145,10 +150,12 @@ p:last-of-type {
background-color: white;
width: 63%;
}
.f2006, .f2016 {
.f2006,
.f2016 {
display: none;
}
#load, #loadingGif {
#load,
#loadingGif {
display: none;
}
#loading {
@ -169,7 +176,8 @@ p:last-of-type {
font-size: 3em;
text-align: center;
}
#state, #state2 {
#state,
#state2 {
font-weight: 300;
font-size: 2em;
text-align: center;
@ -183,7 +191,7 @@ select {
border: 2px solid #ccc;
border-radius: 4px;
font-size: 1.25em;
color: #0A0F14;
color: #0a0f14;
width: 26%;
padding: 12px 20px 12px 40px;
background-color: #fff;
@ -194,7 +202,8 @@ table {
margin: auto;
width: 100%;
}
th, td {
th,
td {
text-align: left;
padding: 0.5em;
}
@ -265,4 +274,3 @@ h4 {
margin-left: 0;
}
}

View file

@ -2,43 +2,43 @@
display: none;
}
input[type=radio] {
input[type="radio"] {
cursor: pointer;
}
input[type=radio] {
input[type="radio"] {
width: 28px;
margin: 0;
padding: 0;
opacity: 0;
}
input[type=radio] + label {
input[type="radio"] + label {
display: inline;
font-family: "Open Sans", sans-serif;
font-weight: 300;
font-size: 1.3em;
margin-left: -28px;
padding-left: 28px;
background: url('../auxIMG/unchecked.svg') no-repeat 0 50%;
background: url("../auxIMG/unchecked.svg") no-repeat 0 50%;
line-height: 35px;
background-size: 28px 28px;
margin-bottom: 6.25em;
}
input[type=radio]:checked + label {
background: url('../auxIMG/checked.svg') no-repeat 0 50%;
input[type="radio"]:checked + label {
background: url("../auxIMG/checked.svg") no-repeat 0 50%;
background-size: 28px 28px;
}
@media only screen and (max-width: 1000px) {
input[type=radio] {
input[type="radio"] {
width: 60px;
height: 60px;
}
input[type=radio] + label {
input[type="radio"] + label {
margin-left: -60px;
padding-left: 60px;
background-size: 60px 60px;
}
input[type=radio]:checked + label {
input[type="radio"]:checked + label {
background-size: 60px 60px;
}
}

View file

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="hu-HU">
<head>
<meta charset="UTF-8" />
@ -92,10 +92,14 @@
<p>
A Tesztek fülre kattintva juthatsz el a gyakorló feladatlapokig,
amiket <b>a te beállításaid alapján</b> állít össze az alkalmazás. Ezt
egy <b>659 kérdést tartalmazó adatbázisból</b> viszi véghez, melyet az
előző <b>több mint 10 év</b> érettségijeinek feladatai alkotnak. Ebben
a menüpontban nem csak témakör, hanem év alapján is
<b>rákereshetsz a feladatokra</b>.
egy
<b
><span id="questionCount">682</span> kérdést tartalmazó
adatbázisból</b
>
viszi véghez, melyet az előző <b>több mint 10 év</b> érettségijeinek
feladatai alkotnak. Ebben a menüpontban nem csak témakör, hanem év
alapján is <b>rákereshetsz a feladatokra</b>.
</p>
<p>
A teszteken elért pontjaidat, és azokkal kapcsolatos egyéb infókat, az

View file

@ -29,10 +29,13 @@ async function ajaxLoad(type) {
try {
if (type == 1) {
var source =
"^" + $("#evszam").val() + $("#honap").val() + $("#feladat").val() + "$";
"^" +
$("#evszam").val() +
$("#honap").val() +
$("#feladat").val() +
"$";
for (var i = 0; i <= 3; i++) {
source = source.replace("all", ".*");
console.log(source);
}
result = await loadQuestions(true, undefined, source, 1000000);
} else if (type == 2) {
@ -56,7 +59,7 @@ async function ajaxLoad(type) {
"v",
],
undefined,
15
15,
);
} else {
var NOQ = $("#numberof").val() ? $("#numberof").val() : 15;
@ -109,7 +112,7 @@ async function ajaxLoad(type) {
</div>
`);
$("#state").html("Hiba a feladatok betöltésekor");
console.error('Quiz loading error:', error);
console.error("Quiz loading error:", error);
}
}
@ -121,11 +124,11 @@ function showCorrect(id, correctAns) {
id +
".rad" +
correctAns +
"').css('background-color', '#C6FF8C');"
"').css('background-color', '#C6FF8C');",
);
$("#state").html("Helyes válaszok bejelölve!");
$("#state2").html(
"(Ellenőrző mód, az itteni eredményeid nem kerülnek elmentésre, a módból való kilépéshez tölts be egy új tesztsort!)"
"(Ellenőrző mód, az itteni eredményeid nem kerülnek elmentésre, a módból való kilépéshez tölts be egy új tesztsort!)",
);
}
@ -140,7 +143,6 @@ function teszt(id, correctAns) {
} else {
$(div).animate({ backgroundColor: "#FF808C" }, 1100);
}
console.log(currentPoints, totalPoints, correctAnswersGiven);
if (currentPoints >= totalPoints) {
var percentage = (correctAnswersGiven / totalPoints) * 100;
percentage = Math.round(percentage * 100) / 100;
@ -151,30 +153,34 @@ function teszt(id, correctAns) {
var datum = new Date();
var ido = Math.round(timer / 60);
eval(
"localStorage.teszt" + numberOfPreviousTests + " = '" + percentage + "'"
"localStorage.teszt" +
numberOfPreviousTests +
" = '" +
percentage +
"'",
);
eval(
"localStorage.teszt" +
numberOfPreviousTests +
"date = '" +
datum.toLocaleDateString() +
"'"
"'",
);
eval(
"localStorage.teszt" + numberOfPreviousTests + "time = '" + ido + "'"
"localStorage.teszt" + numberOfPreviousTests + "time = '" + ido + "'",
);
eval(
"localStorage.teszt" +
numberOfPreviousTests +
"total = '" +
totalPoints +
"'"
"'",
);
startTimer = 0;
timer = 0;
$("#state2").show();
$("#state2").html(
"Eredményed mentésre került! Ellenőrző módba belépve az eredményeid nem kerülnek tárolásra. A módból való kilépéshez tölts be egy új tesztsort!"
"Eredményed mentésre került! Ellenőrző módba belépve az eredményeid nem kerülnek tárolásra. A módból való kilépéshez tölts be egy új tesztsort!",
);
eredmeny();
reviewMode = 1;
@ -189,7 +195,7 @@ function scrollTo(where) {
{
scrollTop: $(where).offset().top - 100,
},
1000
1000,
);
}
@ -208,7 +214,7 @@ function eredmeny() {
if (isLocal) {
if (typeof localStorage.teszt1 !== "undefined") {
$("#tablazat").html(
'<table id="ered"><tr><th></th><th>Dátum</th><th>Időtartam</th><th>Eredmény</th><th>Pontszám</th></tr></table>'
'<table id="ered"><tr><th></th><th>Dátum</th><th>Időtartam</th><th>Eredmény</th><th>Pontszám</th></tr></table>',
);
for (var i = 1; i < numberOfPreviousTests; i++) {
var localString = "localStorage.teszt" + i;
@ -232,7 +238,7 @@ function eredmeny() {
Math.round((eval(localString) * eval(totalString)) / 100) +
"/" +
eval(totalString) +
" pont</td></tr>"
" pont</td></tr>",
);
}
} else {
@ -240,7 +246,7 @@ function eredmeny() {
}
} else {
$("#tablazat").html(
"<h2>Sajnos a böngésződ nem támogatja ezt a funkciót, tölts le egy modernebbet vagy jelentkezz be!</h2>"
"<h2>Sajnos a böngésződ nem támogatja ezt a funkciót, tölts le egy modernebbet vagy jelentkezz be!</h2>",
);
}
}
@ -261,11 +267,11 @@ $(document).ready(function () {
eredmeny();
// Initialize year dropdown with dynamic years from question data
if (typeof initializeYearDropdown === 'function') {
if (typeof initializeYearDropdown === "function") {
initializeYearDropdown().then(() => {
// Initialize month dropdown for default "all" year selection
if (typeof initializeMonthDropdown === 'function') {
initializeMonthDropdown('all/');
if (typeof initializeMonthDropdown === "function") {
initializeMonthDropdown("all/");
}
});
}
@ -330,7 +336,7 @@ $(document).ready(function () {
const selectedYear = $("#evszam").val();
// Initialize month dropdown dynamically based on selected year
if (typeof initializeMonthDropdown === 'function') {
if (typeof initializeMonthDropdown === "function") {
initializeMonthDropdown(selectedYear);
} else {
// Fallback to original logic if dynamic function not available
@ -390,7 +396,7 @@ $(document).ready(function () {
{
scrollTop: $("#teszt").offset().top,
},
3000
3000,
);
});
});
@ -419,28 +425,28 @@ $(document).ready(function () {
Math.max(
Math.min(
parseInt(g.pos * (g.end[0] - g.start[0]) + g.start[0]),
255
255,
),
0
0,
),
Math.max(
Math.min(
parseInt(g.pos * (g.end[1] - g.start[1]) + g.start[1]),
255
255,
),
0
0,
),
Math.max(
Math.min(
parseInt(g.pos * (g.end[2] - g.start[2]) + g.start[2]),
255
255,
),
0
0,
),
].join(",") +
")";
};
}
},
);
function b(f) {
var e;
@ -448,15 +454,17 @@ $(document).ready(function () {
return f;
}
if (
(e = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(
f
(e =
/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(
f,
))
) {
return [parseInt(e[1]), parseInt(e[2]), parseInt(e[3])];
}
if (
(e = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(
f
(e =
/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(
f,
))
) {
return [