import express, { Request, Response } from "express"; import { getLogin, getTrusted } from "../auth"; 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. * * @param req request * @returns index dne v týdnu */ const parseValidateFutureDayIndex = (req: Request<{}, any, AddChoiceData["body"] | UpdateNoteData["body"]>) => { if (req.body.dayIndex == null) { throw Error(`Nebyl předán index dne v týdnu.`); } const todayDayIndex = getDayOfWeekIndex(getToday()); const dayIndex = req.body.dayIndex; if (isNaN(dayIndex)) { throw Error(`Neplatný index dne v týdnu: ${req.body.dayIndex}`); } if (dayIndex < todayDayIndex) { throw Error(`Předaný index dne v týdnu (${dayIndex}) nesmí být nižší než dnešní den (${todayDayIndex})`); } return dayIndex; } const router = express.Router(); router.post("/addChoice", async (req: Request<{}, any, AddChoiceData["body"]>, res, next) => { const login = getLogin(parseToken(req)); const trusted = getTrusted(parseToken(req)); let date = undefined; if (req.body.dayIndex != null) { let dayIndex; try { dayIndex = parseValidateFutureDayIndex(req); } catch (e: any) { return res.status(400).json({ error: e.message }); } date = getDateForWeekIndex(dayIndex); } try { const data = await addChoice(login, trusted, req.body.locationKey, req.body.foodIndex, date); getWebsocket().emit("message", data); return res.status(200).json(data); } catch (e: any) { next(e) } }); router.post("/removeChoices", async (req: Request<{}, any, RemoveChoicesData["body"]>, res, next) => { const login = getLogin(parseToken(req)); const trusted = getTrusted(parseToken(req)); let date = undefined; if (req.body.dayIndex != null) { let dayIndex; try { dayIndex = parseValidateFutureDayIndex(req); } catch (e: any) { return res.status(400).json({ error: e.message }); } date = getDateForWeekIndex(dayIndex); } try { const data = await removeChoices(login, trusted, req.body.locationKey, date); getWebsocket().emit("message", data); res.status(200).json(data); } catch (e: any) { next(e) } }); router.post("/removeChoice", async (req: Request<{}, any, RemoveChoiceData["body"]>, res, next) => { const login = getLogin(parseToken(req)); const trusted = getTrusted(parseToken(req)); let date = undefined; if (req.body.dayIndex != null) { let dayIndex; try { dayIndex = parseValidateFutureDayIndex(req); } catch (e: any) { return res.status(400).json({ error: e.message }); } date = getDateForWeekIndex(dayIndex); } try { const data = await removeChoice(login, trusted, req.body.locationKey, req.body.foodIndex, date); getWebsocket().emit("message", data); res.status(200).json(data); } catch (e: any) { next(e) } }); router.post("/updateNote", async (req: Request<{}, any, UpdateNoteData["body"]>, res, next) => { const login = getLogin(parseToken(req)); const trusted = getTrusted(parseToken(req)); const note = req.body.note; try { if (note && note.length > 70) { throw Error("Poznámka může mít maximálně 70 znaků"); } let date = undefined; if (req.body.dayIndex != null) { let dayIndex; try { dayIndex = parseValidateFutureDayIndex(req); } catch (e: any) { return res.status(400).json({ error: e.message }); } date = getDateForWeekIndex(dayIndex); } const data = await updateNote(login, trusted, note, date); getWebsocket().emit("message", data); res.status(200).json(data); } catch (e: any) { next(e) } }); router.post("/changeDepartureTime", async (req: Request<{}, any, ChangeDepartureTimeData["body"]>, res, next) => { const login = getLogin(parseToken(req)); let date = undefined; if (req.body.dayIndex != null) { let dayIndex; try { dayIndex = parseValidateFutureDayIndex(req); } catch (e: any) { return res.status(400).json({ error: e.message }); } date = getDateForWeekIndex(dayIndex); } try { const data = await updateDepartureTime(login, req.body?.time, date); getWebsocket().emit("message", data); res.status(200).json(data); } catch (e: any) { next(e) } }); router.post("/jdemeObed", async (req, res, next) => { const login = getLogin(parseToken(req)); try { await callNotifikace({ input: { user: login, udalost: UdalostEnum.JDEME_NA_OBED }, gotify: false }) res.status(200).json({}); } catch (e: any) { next(e) } }); // /api/food/refresh?type=week&heslo=docasnyheslo export const refreshMetoda = async (req: Request, res: Response) => { const { type, heslo } = req.query as { type?: string; heslo?: string }; if (heslo !== "docasnyheslo" && heslo !== "tohleheslopavelnesmizjistit123") { return res.status(403).json({ error: "Neplatné heslo" }); } 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) { 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}` }; } } // 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" }); } } router.get("/refresh", refreshMetoda); export default router;