Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(admin): better validation for uploaded games #162

Merged
merged 7 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
"Remove player's blindfold": "Remove player's blindfold",
"Blindfold player": "Blindfold player",
"Remove player from game": "Remove player from game",
"Game file error": "Game file error",
"errors": {
"room_not_found": "Room not found",
"host_quit": "Host quit the game",
Expand Down
1 change: 1 addition & 0 deletions public/locales/et/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
"Remove player's blindfold": "Eemalda mängija ekraani kate",
"Blindfold player": "Peitke mängija ekraan",
"Remove player from game": "Eemaldage mängija mängust",
"Game file error": "Küsimustiku vormingu viga",
"errors": {
"room_not_found": "Mängu ei leitud",
"host_quit": "Mängu juht lahkus mängust",
Expand Down
4 changes: 2 additions & 2 deletions src/components/Admin/AdminSettings/BuzzerSoundSettings.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import InfoTooltip from "@/components/ui/tooltip";
import { useTranslation } from "react-i18next";

function BuzzerSoundSettings({game, setGame, send}) {
function BuzzerSoundSettings({ game, setGame, send }) {
const { t } = useTranslation();

return (
Expand Down Expand Up @@ -51,4 +51,4 @@ function BuzzerSoundSettings({game, setGame, send}) {
);
}

export default BuzzerSoundSettings;
export default BuzzerSoundSettings;
4 changes: 2 additions & 2 deletions src/components/Admin/AdminSettings/FinalRoundTitleChanger.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { debounce } from "@/lib/utils";
import { useTranslation } from "react-i18next";

function FinalRoundTitleChanger({game, setGame, send}) {
function FinalRoundTitleChanger({ game, setGame, send }) {
const { t } = useTranslation();
return (
<div className="flex flex-row items-center space-x-5">
Expand All @@ -21,4 +21,4 @@ function FinalRoundTitleChanger({game, setGame, send}) {
);
}

export default FinalRoundTitleChanger;
export default FinalRoundTitleChanger;
4 changes: 2 additions & 2 deletions src/components/Admin/AdminSettings/HideGameQuestions.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useTranslation } from "react-i18next";

function HideGameQuestions({game, setGame, send}) {
function HideGameQuestions({ game, setGame, send }) {
const { t } = useTranslation();

return (
Expand Down Expand Up @@ -30,4 +30,4 @@ function HideGameQuestions({game, setGame, send}) {
);
}

export default HideGameQuestions;
export default HideGameQuestions;
6 changes: 3 additions & 3 deletions src/components/Admin/AdminSettings/index.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import "@/i18n/i18n";
import ThemeSwitcher from "@/components/Admin/ThemeSwitcher";
import HideGameQuestions from "./HideGameQuestions";
import FinalRoundTitleChanger from "./FinalRoundTitleChanger";
import BuzzerSoundSettings from "./BuzzerSoundSettings";
import FinalRoundTitleChanger from "./FinalRoundTitleChanger";
import HideGameQuestions from "./HideGameQuestions";

export default function AdminSettings({game, setGame, send}) {
export default function AdminSettings({ game, setGame, send }) {
return (
<div className="flex flex-col items-center">
<div className="grid grid-cols-2 gap-x-48 gap-y-10">
Expand Down
8 changes: 4 additions & 4 deletions src/components/AdminPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import "@/i18n/i18n";
import { Buffer } from "buffer";
import AdminSettings from "@/components/Admin/AdminSettings";
import CSVLoader from "@/components/Admin/CSVLoader";
import GameLoader from "@/components/Admin/GameLoader";
import Players from "@/components/Admin/Players";
import AdminSettings from "@/components/Admin/AdminSettings";
import BuzzerTable from "@/components/BuzzerTable";
import LanguageSwitcher from "@/components/LanguageSwitcher";
import { ERROR_CODES } from "@/i18n/errorCodes";
import { debounce } from "@/lib/utils";
import { FileUp } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { debounce } from "@/lib/utils";

function TitleMusic() {
const { i18n, t } = useTranslation();
Expand Down Expand Up @@ -581,9 +581,9 @@ export default function AdminPage(props) {
></input>
</div>
</div>
<p id="errorText" className="text-xl text-failure-700">
<div id="errorText" className="whitespace-pre-wrap text-xl text-failure-700">
{error.code ? t(error.code, { message: error.message }) : t(error)}
</p>
</div>
</div>
<hr className="my-12" />
{/* ADMIN CONTROLS */}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/utils/debounce.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ export function debounce(callback, wait = 400) {
callback.apply(this, args);
}, wait);
};
}
}
61 changes: 58 additions & 3 deletions src/lib/utils/files.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@ export function handleJsonFile(file, { setError, t, send }) {
reader.onload = function (evt) {
try {
let data = JSON.parse(evt.target.result);
let errors = validateGameData(data, { t });

if (errors.length > 0) {
setError(t("Game file error") + ":\n" + errors.join("\n"));
return;
}
console.debug(data);
// TODO some error checking for invalid game data
send({ action: "load_game", data: data });
} catch (e) {
console.error("Invalid JSON file");
setError(t("Invalid JSON file"));
console.error("Invalid JSON file", e);
setError(t(`Invalid JSON file: ${e}`));
}
};
reader.onerror = function (evt) {
Expand All @@ -36,6 +41,56 @@ export function handleCsvFile(file, { setError, t, setCsvFileUpload, setCsvFileU
};
}

export function validateGameData(game, { t }) {
let errors = [];
if (game.rounds.length == 0) {
errors.push(t("You need to create some rounds to save the game"));
}
game.rounds.forEach((r, index) => {
if (r.question === "") {
errors.push(
t("round number {{count, number}} has an empty question", {
count: index + 1,
})
);
}
if (r.multiply === "" || r.multiply === 0 || isNaN(r.multiply)) {
errors.push(
t("round number {{count, number}} has no point multipler", {
count: index + 1,
})
);
}
if (r.answers.length === 0) {
errors.push(
t("round number {{count, number}} has no answers", {
count: index + 1,
})
);
}
r.answers.forEach((a, aindex) => {
if (a.ans === "") {
errors.push(
t("round item {{count, number}} has empty answer at answer number {{answernum, number}}", {
count: index + 1,
answernum: aindex + 1,
})
);
}
if (a.pnt === 0 || a.pnt === "" || isNaN(a.pnt)) {
errors.push(
t("round item {{count, number}} has {{zero, number}} points answer number {{answernum, number}}", {
count: index + 1,
zero: 0,
answernum: aindex + 1,
})
);
}
});
});
return errors;
}

export function isValidFileType(file, allowedTypes) {
const fileName = file.name.toLowerCase();
const fileExtension = fileName.split(".").pop();
Expand Down
4 changes: 2 additions & 2 deletions src/lib/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { handleCsvFile, handleJsonFile, isValidFileType } from './files';
export { debounce } from './debounce';
export { handleCsvFile, handleJsonFile, isValidFileType, validateGameData } from "./files";
export { debounce } from "./debounce";
54 changes: 3 additions & 51 deletions src/pages/new.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
import "@/i18n/i18n";
import ThemeSwitcher from "@/components/Admin/ThemeSwitcher";
import LanguageSwitcher from "@/components/LanguageSwitcher";
import { validateGameData } from "@/lib/utils";

export default function CreateGame(props) {
const { t } = useTranslation();
Expand Down Expand Up @@ -363,61 +364,12 @@ export default function CreateGame(props) {
className="rounded-md bg-success-200 p-2 px-10 hover:shadow-md"
onClick={() => {
// ERROR checking
let error = [];
if (game.rounds.length == 0) {
error.push(t("You need to create some rounds to save the game"));
}
game.rounds.forEach((r, index) => {
if (r.question === "") {
error.push(
t("round number {{count, number}} has an empty question", {
count: index + 1,
})
);
}
if (r.multiply === "" || r.multiply === 0 || isNaN(r.multiply)) {
error.push(
t("round number {{count, number}} has no point multipler", {
count: index + 1,
})
);
}
if (r.answers.length === 0) {
error.push(
t("round number {{count, number}} has no answers", {
count: index + 1,
})
);
}
r.answers.forEach((a, aindex) => {
if (a.ans === "") {
error.push(
t("round item {{count, number}} has empty answer at answer number {{answernum, number}}", {
count: index + 1,
answernum: aindex + 1,
})
);
}
if (a.pnt === 0 || a.pnt === "" || isNaN(a.pnt)) {
error.push(
t(
"round item {{count, number}} has {{zero, number}} points answer number {{answernum, number}}",
{
count: index + 1,
zero: 0,
answernum: aindex + 1,
}
)
);
}
});
});

console.error(error);
let error = validateGameData(game, { t });
if (error.length === 0) {
setError("");
downloadToFile(JSON.stringify(game), `${t("new-cold-feud")}.json`, "text/json");
} else {
console.error(error);
setError(error.join(", "));
}
}}
Expand Down