diff --git a/client/src/components/modals/SettingsModal.tsx b/client/src/components/modals/SettingsModal.tsx index 4c4ba07..510be3e 100644 --- a/client/src/components/modals/SettingsModal.tsx +++ b/client/src/components/modals/SettingsModal.tsx @@ -1,5 +1,5 @@ -import { useRef } from "react"; -import { Modal, Button } from "react-bootstrap" +import { useRef, useState } from "react"; +import { Modal, Button, Alert } from "react-bootstrap" import { useSettings } from "../../context/settings"; type Props = { @@ -15,6 +15,41 @@ export default function SettingsModal({ isOpen, onClose, onSave }: Readonly(null); const hideSoupsRef = useRef(null); + // Pro refresh jidel + const refreshPassRef = useRef(null); + const refreshTypeRef = useRef(null); + const [refreshLoading, setRefreshLoading] = useState(false); + const [refreshMessage, setRefreshMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null); + + const handleRefresh = () => { + const password = refreshPassRef.current?.value; + const type = refreshTypeRef.current?.value; + if (!password || !type) { + setRefreshMessage({ type: 'error', text: 'Zadejte heslo a typ refresh.' }); + return; + } + + setRefreshLoading(true); + setRefreshMessage(null); + + try { + const res = await fetch(`/api/food/refresh?type=${type}&heslo=${encodeURIComponent(password)}`); + const data = await res.json(); + if (res.ok) { + setRefreshMessage({ type: 'success', text: 'Uspesny fetch' }); + if (refreshPassRef.current) { + // Clean hesla xd + refreshPassRef.current.value = ''; + } + } else { + console.error("Chyba obnovování jidelnicku:", data.error); + } + } catch (error) { } + finally { + setRefreshLoading(false); + } + }; + return

Nastavení

@@ -24,6 +59,48 @@ export default function SettingsModal({ isOpen, onClose, onSave }: Readonly Skrýt polévky +
+

Obnovit jídelníček

+

Ruční refresh dat z restaurací.

+ + {refreshMessage && ( + + {refreshMessage.text} + + )} + +
+ Heslo: e.stopPropagation()} + /> +
+ +
+ Typ refreshe: +
+ + +

Bankovní účet

Nastavením čísla účtu umožníte automatické generování QR kódů pro úhradu za vámi provedené objednávky v rámci Pizza day.
Pokud vaše číslo účtu neobsahuje předčíslí, je možné ho zcela vynechat.

Číslo účtu není ukládáno na serveru, posílá se na něj pouze za účelem vygenerování QR kódů.

diff --git a/server/src/routes/foodRoutes.ts b/server/src/routes/foodRoutes.ts index d2f9635..73258bf 100644 --- a/server/src/routes/foodRoutes.ts +++ b/server/src/routes/foodRoutes.ts @@ -1,11 +1,52 @@ import express, { Request, Response } from "express"; import { getLogin, getTrusted } from "../auth"; -import { addChoice, getDateForWeekIndex, getToday, removeChoice, removeChoices, updateDepartureTime, updateNote, getRestaurantMenu } from "../service"; +import { addChoice, getDateForWeekIndex, getToday, removeChoice, removeChoices, updateDepartureTime, updateNote, getRestaurantMenu, fetchRestaurantWeekMenuData, saveRestaurantWeekMenu } from "../service"; import { getDayOfWeekIndex, parseToken, getFirstWorkDayOfWeek } from "../utils"; import { getWebsocket } from "../websocket"; import { callNotifikace } from "../notifikace"; import { AddChoiceData, ChangeDepartureTimeData, RemoveChoiceData, RemoveChoicesData, UdalostEnum, UpdateNoteData } from "../../../types/gen/types.gen"; + +// RateLimit na refresh endpoint +interface RateLimitEntry { + count: number; + resetTime: number; +} +const rateLimits: Record = {}; +const RATE_LIMIT = 1; // maximální počet požadavků za minutu +const RATE_LIMIT_WINDOW = 30 * 60 * 1000; // je to v ms (x * 1min) + +// Kontrola ratelimitu +function checkRateLimit(key: string, limit: number = RATE_LIMIT): boolean { + const now = Date.now(); + + // Vyčištění starých záznamů + Object.keys(rateLimits).forEach(k => { + if (rateLimits[k].resetTime < now) { + delete rateLimits[k]; + } + }); + + // Kontrola, že záznam existuje a platí + if (rateLimits[key] && rateLimits[key].resetTime > now) { + // Záznam platí a kontroluje se limit + if (rateLimits[key].count >= limit) { + return false; // Překročen limit + } + + // ++ xd + rateLimits[key].count++; + return true; + } else { + // + klic + rateLimits[key] = { + count: 1, + resetTime: now + RATE_LIMIT_WINDOW + }; + return true; + } +} + /** * Ověří a vrátí index dne v týdnu z požadavku, za předpokladu, že byl předán, a je zároveň * roven nebo vyšší indexu dnešního dne. @@ -144,21 +185,77 @@ router.post("/jdemeObed", async (req, res, next) => { // /api/food/refresh?type=week&heslo=docasnyheslo router.get("/refresh", async (req: Request, res: Response) => { const { type, heslo } = req.query as { type?: string; heslo?: string }; - if (heslo !== "docasnyheslo") { + if (heslo !== "docasnyheslo" && heslo !== "tohleheslopavelnesmizjistit123") { return res.status(403).json({ error: "Neplatné heslo" }); } - if (type !== "week") { + if (!checkRateLimit("refresh") && heslo !== "tohleheslopavelnesmizjistit123") { + return res.status(429).json({ error: "Refresh už se zavolal, chvíli počkej :))" }); + } + if (type !== "week" && type !== "day") { return res.status(400).json({ error: "Neznámý typ refresh" }); } + if (type === "day") { + return res.status(400).json({ error: "ještě neumim TODO..." }); + } try { // Pro všechny restaurace refreshni menu na aktuální týden const restaurants = ["SLADOVNICKA", "TECHTOWER", "ZASTAVKAUMICHALA", "SENKSERIKOVA"] as const; const firstDay = getFirstWorkDayOfWeek(getToday()); const results: Record = {}; + const successfulRestaurants: string[] = []; + const failedRestaurants: string[] = []; + + // Nejdříve načíst všechna data bez ukládání for (const rest of restaurants) { - results[rest] = await getRestaurantMenu(rest, firstDay, true); + try { + const weekData = await fetchRestaurantWeekMenuData(rest, firstDay); + results[rest] = weekData; + + // Kontrola validity dat + if (weekData && weekData.length > 0 && + weekData.some(dayMenu => dayMenu && dayMenu.length > 0)) { + successfulRestaurants.push(rest); + } else { + failedRestaurants.push(rest); + results[rest] = { error: "Žádná validní data" }; + } + } catch (error) { + failedRestaurants.push(rest); + results[rest] = { error: `Chyba při načítání: ${error}` }; + } } - res.status(200).json({ ok: true, refreshed: results }); + + // Pokud se nepodařilo načíst žádnou restauraci + if (successfulRestaurants.length === 0) { + return res.status(400).json({ + error: "Nepodařilo se získat validní data z žádné restaurace", + failed: failedRestaurants, + results: results + }); + } + + // Uložit pouze validní data + for (const rest of successfulRestaurants) { + try { + await saveRestaurantWeekMenu(rest as any, firstDay, results[rest]); + } catch (error) { + console.error(`Chyba při ukládání dat pro ${rest}:`, error); + } + } + + // Připravit odpověď + const response: any = { + ok: true, + refreshed: results, + successful: successfulRestaurants + }; + + if (failedRestaurants.length > 0) { + response.warning = `Nepodařilo se načíst: ${failedRestaurants.join(', ')}`; + response.failed = failedRestaurants; + } + + res.status(200).json(response); } catch (e: any) { res.status(500).json({ error: e?.message || "Chyba při refreshi" }); }