7 Commits

Author SHA1 Message Date
67758d91cf Refresh menu, první část 2025-04-15 19:51:41 +02:00
49b8ab5c13 Update server/src/index.ts
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
delete req.headers["cookie"]
2025-04-11 12:06:52 +02:00
9a05ef1fe6 Update server/src/index.ts
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
vic logov
2025-04-11 12:01:58 +02:00
0bfea3765f properta pro logovani headeru
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-04-11 10:42:27 +02:00
962fbe2947 fix hardcoded header name xd
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-04-11 10:06:35 +02:00
d6d6ebb682 Aktualizace posledních změn
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful
2025-03-21 00:24:26 +01:00
5bb7de58e7 Odebrání zimní atmosféry 2025-03-21 00:24:17 +01:00
9 changed files with 139 additions and 23 deletions

View File

@@ -410,12 +410,12 @@ function App() {
<div className='wrapper'>
{data.isWeekend ? <h4>Užívejte víkend :)</h4> : <>
<Alert variant={'primary'}>
<img alt="" src='hat.png' style={{ position: "absolute", width: "70px", rotate: "-45deg", left: -40, top: -58 }} />
<img alt="" src='snowman.png' style={{ position: "absolute", height: "110px", right: 10, top: 5 }} />
{/* <img alt="" src='hat.png' style={{ position: "absolute", width: "70px", rotate: "-45deg", left: -40, top: -58 }} />
<img alt="" src='snowman.png' style={{ position: "absolute", height: "110px", right: 10, top: 5 }} /> */}
Poslední změny:
<ul>
<li>Možnost výběru restaurace a jídel kliknutím v tabulce</li>
<li><Link to={STATS_URL}>Statistiky</Link></li>
<li>Migrace na generované <Link target='_blank' to="https://www.openapis.org">OpenAPI</Link></li>
<li>Odebrání zimní atmosféry</li>
</ul>
</Alert>
{dayIndex != null &&

View File

@@ -1,6 +1,6 @@
import { Routes, Route } from "react-router-dom";
import { ProvideSettings } from "./context/settings";
import Snowfall from "react-snowfall";
// import Snowfall from "react-snowfall";
import { ToastContainer } from "react-toastify";
import { SocketContext, socket } from "./context/socket";
import StatsPage from "./pages/StatsPage";
@@ -16,12 +16,12 @@ export default function AppRoutes() {
<ProvideSettings>
<SocketContext.Provider value={socket}>
<>
<Snowfall style={{
{/* <Snowfall style={{
zIndex: 2,
position: 'fixed',
width: '100vw',
height: '100vh'
}} />
}} /> */}
<App />
</>
<ToastContainer />

View File

@@ -7,7 +7,8 @@ import FeaturesVotingModal from "./modals/FeaturesVotingModal";
import PizzaCalculatorModal from "./modals/PizzaCalculatorModal";
import { useNavigate } from "react-router";
import { STATS_URL } from "../AppRoutes";
import { FeatureRequest, getVotes, updateVote } from "../../../types";
import { FeatureRequest, getVotes, refreshMenu, Restaurant, updateVote } from "../../../types";
import RefreshMenuModal from "./modals/RefreshMenuModal";
export default function Header() {
const auth = useAuth();
@@ -16,6 +17,7 @@ export default function Header() {
const [settingsModalOpen, setSettingsModalOpen] = useState<boolean>(false);
const [votingModalOpen, setVotingModalOpen] = useState<boolean>(false);
const [pizzaModalOpen, setPizzaModalOpen] = useState<boolean>(false);
const [refreshModalOpen, setRefreshModalOpen] = useState<boolean>(false);
const [featureVotes, setFeatureVotes] = useState<FeatureRequest[] | undefined>([]);
useEffect(() => {
@@ -38,6 +40,10 @@ export default function Header() {
setPizzaModalOpen(false);
}
const closeRefreshModal = () => {
setRefreshModalOpen(false);
}
const isValidInteger = (str: string) => {
str = str.trim();
if (!str) {
@@ -107,6 +113,12 @@ export default function Header() {
setFeatureVotes(votes);
}
const handleRefreshMenu = async (restaurants: Restaurant[]) => {
if (restaurants.length > 0) {
await refreshMenu({ body: restaurants });
}
}
return <Navbar variant='dark' expand="lg">
<Navbar.Brand href="/">Luncher</Navbar.Brand>
<Navbar.Toggle aria-controls="basic-navbar-nav" />
@@ -117,6 +129,7 @@ export default function Header() {
<NavDropdown.Item onClick={() => setVotingModalOpen(true)}>Hlasovat o nových funkcích</NavDropdown.Item>
<NavDropdown.Item onClick={() => setPizzaModalOpen(true)}>Pizza kalkulačka</NavDropdown.Item>
<NavDropdown.Item onClick={() => navigate(STATS_URL)}>Statistiky</NavDropdown.Item>
<NavDropdown.Item onClick={() => setRefreshModalOpen(true)}>Přenačíst menu</NavDropdown.Item>
<NavDropdown.Divider />
<NavDropdown.Item onClick={auth?.logout}>Odhlásit se</NavDropdown.Item>
</NavDropdown>
@@ -125,5 +138,6 @@ export default function Header() {
<SettingsModal isOpen={settingsModalOpen} onClose={closeSettingsModal} onSave={saveSettings} />
<FeaturesVotingModal isOpen={votingModalOpen} onClose={closeVotingModal} onChange={saveFeatureVote} initialValues={featureVotes} />
<PizzaCalculatorModal isOpen={pizzaModalOpen} onClose={closePizzaModal} />
<RefreshMenuModal isOpen={refreshModalOpen} onClose={closeRefreshModal} onSubmit={handleRefreshMenu} />
</Navbar>
}

View File

@@ -0,0 +1,53 @@
import { Modal, Button, Form } from "react-bootstrap"
import { Restaurant } from "../../../../types";
import { getRestaurantName } from "../../enums";
import { useState } from "react";
type Props = {
isOpen: boolean,
onClose: () => void,
onSubmit: (restaurants: Restaurant[]) => void,
}
/** Modální dialog pro přenačtení menu jednotlivých podniků. */
export default function RefreshMenuModal({ isOpen, onClose, onSubmit }: Readonly<Props>) {
const [restaurants, setRestaurants] = useState<Restaurant[]>([]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.currentTarget.checked) {
setRestaurants([...restaurants, e.currentTarget.value as Restaurant]);
} else {
setRestaurants(restaurants.filter(restaurant => restaurant !== e.currentTarget.value as Restaurant));
}
}
return <Modal show={isOpen} onHide={onClose} size="lg">
<Modal.Header closeButton>
<Modal.Title>
Vyberte podniky k přenačtení menu
<p style={{ fontSize: '12px' }}>Menu lze přenačíst nejdříve 15 minut od poslední aktualizace</p>
</Modal.Title>
</Modal.Header>
<Modal.Body>
{(Object.keys(Restaurant) as Array<keyof typeof Restaurant>).map(key => {
return <Form.Check
key={key}
type='checkbox'
id={key}
label={getRestaurantName(key as Restaurant)}
onChange={handleChange}
value={key}
/>
})}
</Modal.Body>
<Modal.Footer>
<Button variant="primary" onClick={() => onSubmit(restaurants)} disabled={restaurants.length === 0}>
Přenačíst
</Button>
<Button variant="secondary" onClick={onClose}>
Zrušit
</Button>
</Modal.Footer>
</Modal>
}

View File

@@ -54,6 +54,10 @@ app.get("/api/whoami", (req, res) => {
if (!HTTP_REMOTE_USER_ENABLED) {
res.status(403).json({ error: 'Není zapnuté přihlášení z hlaviček' });
}
if(process.env.ENABLE_HEADERS_LOGGING === 'yes'){
delete req.headers["cookie"]
console.log(req.headers)
}
res.send(req.header(HTTP_REMOTE_USER_HEADER_NAME));
})
@@ -61,11 +65,11 @@ app.post("/api/login", (req, res) => {
if (HTTP_REMOTE_USER_ENABLED) { // je rovno app.enabled('trust proxy')
// Autentizace pomocí trusted headers
const remoteUser = req.header(HTTP_REMOTE_USER_HEADER_NAME);
const remoteName = req.header('remote-name');
if (remoteUser && remoteUser.length > 0 && remoteName && remoteName.length > 0) {
res.status(200).json(generateToken(Buffer.from(remoteName, 'latin1').toString(), true));
//const remoteName = req.header('remote-name');
if (remoteUser && remoteUser.length > 0 ) {
res.status(200).json(generateToken(Buffer.from(remoteUser, 'latin1').toString(), true));
} else {
throw Error("Tohle nema nastat nekdo neco dela spatne.");
throw Error("Je zapnuto přihlášení přes hlavičky, ale nepřišla hlavička nebo ??");
}
} else {
// Klasická autentizace loginem
@@ -95,13 +99,16 @@ app.get("/api/qr", (req, res) => {
/** Middleware ověřující JWT token */
app.use("/api/", (req, res, next) => {
if (HTTP_REMOTE_USER_ENABLED) {
const userHeader = req.header(HTTP_REMOTE_USER_HEADER_NAME);
const nameHeader = req.header('remote-name');
const emailHeader = req.header('remote-email');
if (userHeader !== undefined && nameHeader !== undefined) {
const remoteName = Buffer.from(nameHeader, 'latin1').toString();
// Autentizace pomocí trusted headers
const remoteUser = req.header(HTTP_REMOTE_USER_HEADER_NAME);
if(process.env.ENABLE_HEADERS_LOGGING === 'yes'){
delete req.headers["cookie"]
console.log(req.headers)
}
if (remoteUser && remoteUser.length > 0) {
const remoteName = Buffer.from(remoteUser, 'latin1').toString();
if (ENVIRONMENT !== "production") {
console.log("Tvuj username, name a email: %s, %s, %s.", userHeader, remoteName, emailHeader);
console.log("Tvuj username: %s.", remoteName);
}
}
}

View File

@@ -1,11 +1,14 @@
import express, { Request } from "express";
import { getLogin, getTrusted } from "../auth";
import { addChoice, getDateForWeekIndex, getToday, removeChoice, removeChoices, updateDepartureTime, updateNote } from "../service";
import { addChoice, getDateForWeekIndex, getRestaurantMenu, getToday, removeChoice, removeChoices, updateDepartureTime, updateNote } from "../service";
import { getDayOfWeekIndex, parseToken } from "../utils";
import { getWebsocket } from "../websocket";
import { callNotifikace } from "../notifikace";
import { AddChoiceData, ChangeDepartureTimeData, RemoveChoiceData, RemoveChoicesData, UdalostEnum, UpdateNoteData } from "../../../types/gen/types.gen";
/** Po jak dlouhé době (v minutách) lze provést nové načtení menu. */
const MENU_REFRESH_INTERVAL = 15;
/**
* 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.
@@ -141,4 +144,25 @@ router.post("/jdemeObed", async (req, res, next) => {
} catch (e: any) { next(e) }
});
router.post("/refreshMenu", async (req, res, next) => {
if (!req.body || !Array.isArray(req.body)) {
return res.status(400).json({ error: "Neplatný požadavek" });
}
try {
const now = new Date();
for (const restaurant of req.body) {
// TODO tohle je technicky špatně, protože pokud aktuálně jídla načtená nejsou, tak je toto volání načte a následně je to načte znovu kvůli force!
const menu = await getRestaurantMenu(restaurant);
if (menu.lastUpdate != null) {
const minutes = (now.getTime() - menu.lastUpdate) / 1000 / 60;
if (minutes < MENU_REFRESH_INTERVAL) {
throw Error(`Podnik ${restaurant} byl přenačtený před ${Math.round(minutes)} minutami. Nové přenačtení lze provést nejdříve za ${Math.round(MENU_REFRESH_INTERVAL - minutes)} minut.`);
}
}
await getRestaurantMenu(restaurant, undefined, true);
}
res.status(200).json({});
} catch (e: any) { next(e) }
});
export default router;

View File

@@ -78,13 +78,13 @@ async function getMenu(date: Date): Promise<WeekMenu | undefined> {
// TODO přesun do restaurants.ts
/**
* Vrátí menu dané restaurace pro předaný den.
* Pokud neexistuje, provede stažení menu pro příslušný týden a uložení do DB.
* Pokud neexistuje nebo je nastaven příznak force, provede stažení menu pro příslušný týden a uložení do DB.
*
* @param restaurant restaurace
* @param date datum, ke kterému získat menu
* @param mock příznak, zda chceme pouze mock data
* @param force Příznak, zda znovu získat aktuální menu i v případě, že je již načteno. Pokud není předán, provede se načtení pouze v případě, že menu aktuálně nemáme. Pokud je true, provede nové načtení. Pokud je false, neprovede se nové načtení ani v případě, že menu aktuálně nemáme.
*/
export async function getRestaurantMenu(restaurant: Restaurant, date?: Date): Promise<RestaurantDayMenu> {
export async function getRestaurantMenu(restaurant: Restaurant, date?: Date, force?: boolean): Promise<RestaurantDayMenu> {
const usedDate = date ?? getToday();
const dayOfWeekIndex = getDayOfWeekIndex(usedDate);
const now = new Date().getTime();
@@ -112,7 +112,8 @@ export async function getRestaurantMenu(restaurant: Restaurant, date?: Date): Pr
};
}
}
if (!weekMenu[dayOfWeekIndex][restaurant]?.food?.length) {
if ((!weekMenu[dayOfWeekIndex][restaurant]?.food?.length && force === undefined) || force) {
const firstDay = getFirstWorkDayOfWeek(usedDate);
const mock = process.env.MOCK_DATA === 'true';
switch (restaurant) {

View File

@@ -26,6 +26,8 @@ paths:
$ref: "./paths/food/changeDepartureTime.yml"
/food/jdemeObed:
$ref: "./paths/food/jdemeObed.yml"
/food/refreshMenu:
$ref: "./paths/food/refreshMenu.yml"
# Pizza day (/api/pizzaDay)
/pizzaDay/create:

View File

@@ -0,0 +1,15 @@
post:
operationId: refreshMenu
summary: Přenačtení menu vybraných podniků
requestBody:
required: true
content:
application/json:
schema:
type: array
items:
$ref: "../../schemas/_index.yml#/Restaurant"
responses:
"200":
description: Menu bylo přenačteno