From 5f549258083517e633a95c61914788902b3b26de Mon Sep 17 00:00:00 2001 From: batmanisko Date: Thu, 24 Sep 2026 09:30:04 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20zp=C5=AFsob=20p=C5=99ihl=C3=A1=C5=A1en?= =?UTF-8?q?=C3=AD=20a=20spr=C3=A1va=20obchod=C5=AF=20podle=20konfigurace?= =?UTF-8?q?=20serveru?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 4 +- client/src/Login.css | 10 ++ client/src/Login.tsx | 108 ++++++++++++------ .../src/components/modals/StoreAdminModal.tsx | 21 +++- client/src/hooks/useGroupLimits.ts | 9 +- client/src/index.tsx | 14 ++- client/src/utils/appConfig.ts | 39 +++++-- e2e/tests/login.spec.ts | 35 +++--- server/src/index.ts | 9 +- types/paths/config/config.yml | 18 ++- 10 files changed, 185 insertions(+), 82 deletions(-) diff --git a/TODO.md b/TODO.md index a0c2c9d..93bcc46 100644 --- a/TODO.md +++ b/TODO.md @@ -5,8 +5,8 @@ - [ ] `easterEggRoutes.ts` — náhodně generované URL easter eggů jsou per-pod; URL funguje pouze na podu, který ji vygeneroval; zvážit deterministické seedy nebo sdílení přes Redis - [ ] `service.ts` — komplexní víceúrovňové funkce (`addChoice`, `removeChoiceIfPresent`) provádějí více po sobě jdoucích zápisů do stejného Redis klíče; pro plnou atomicitu je potřeba per-klíčový distribuovaný zámek (Redlock nebo `SET NX EX`) nebo sloučení logiky do jednoho `updateData` volání - [ ] HTTP_REMOTE_TRUSTED_IPS se nikde nevalidují, hlavičky jsou přijímány odkudkoli -- [ ] V případě zapnutí přihlašování přes trusted headers nefunguje standardní přihlášení (nevrátí žádnou odpověď) - - [ ] Nemělo by se jít dostat na přihlašovací formulář (měla by tam být nanejvýš hláška nebo přesměrování) +- [X] V případě zapnutí přihlašování přes trusted headers nefunguje standardní přihlášení (nevrátí žádnou odpověď) + - [X] Nemělo by se jít dostat na přihlašovací formulář (měla by tam být nanejvýš hláška nebo přesměrování) - [ ] Možnost úhrady celé útraty jednou osobou - Základní myšlenka: jedna osoba uhradí celou útratu (v zájmu rychlosti odbavení), ostatním se automaticky vygeneruje QR kód, kterým následně uhradí svoji část útraty - Obecně to bude problém např. pokud si někdo objedná něco navíc (pití apod.) diff --git a/client/src/Login.css b/client/src/Login.css index 0965166..a24e602 100644 --- a/client/src/Login.css +++ b/client/src/Login.css @@ -87,3 +87,13 @@ font-weight: 600; margin-top: 8px; } + +.login-card > .hint { + color: var(--luncher-text-muted); +} + +.login-error { + color: var(--luncher-danger); + line-height: 1.5; + margin: 0; +} diff --git a/client/src/Login.tsx b/client/src/Login.tsx index b5cc446..9818e0b 100644 --- a/client/src/Login.tsx +++ b/client/src/Login.tsx @@ -1,29 +1,37 @@ -import React, { useCallback, useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Button } from 'react-bootstrap'; import { useAuth } from './context/auth'; import { login } from '../../types'; +import { useAppConfig } from './hooks/useGroupLimits'; import './Login.css'; /** - * Formulář pro prvotní zadání přihlašovacího jména. + * Přihlášení. Podle konfigurace serveru buď přihlásí uživatele automaticky přes důvěryhodné + * hlavičky z proxy (bez formuláře), nebo zobrazí formulář pro zadání jména. Když se konfigurace + * nenačte, zkusí nejprve přihlášení „naprázdno" a jinak zobrazí formulář (původní chování). */ export default function Login() { const auth = useAuth(); + const { loaded, trustedHeaders } = useAppConfig(); const loginRef = useRef(null); + const [trustedError, setTrustedError] = useState(); + const [attempt, setAttempt] = useState(0); useEffect(() => { - if (auth && !auth.login) { - // Vyzkoušíme přihlášení "naprázdno", pokud projde, přihlásili nás trusted headers - login().then(response => { - const token = response.data; - if (token) { - auth?.setToken(token as unknown as string); // TODO vyřešit, API definice je špatně, je to skutečně string - } - }).catch(error => { - // nezajímá nás - }); - } - }, [auth]); + if (!auth || auth.login || !loaded || trustedHeaders === false) return; + // Přihlášení přes hlavičky z proxy (případně pokus „naprázdno", pokud nevíme, jak server přihlašuje) + setTrustedError(undefined); + login().then(response => { + const token = response.data; + if (token) { + auth.setToken(token as unknown as string); // TODO vyřešit, API definice je špatně, je to skutečně string + } else if (trustedHeaders) { + setTrustedError((response.error as any)?.error ?? 'Přihlášení přes proxy se nezdařilo.'); + } + }).catch(() => { + if (trustedHeaders) setTrustedError('Server je nedostupný. Zkuste to prosím za chvíli.'); + }); + }, [auth, loaded, trustedHeaders, attempt]); const doLogin = useCallback(async () => { const length = loginRef?.current?.value.length && loginRef.current.value.replaceAll(/\s/g, '').length @@ -35,36 +43,60 @@ export default function Login() { } }, [auth]); - if (!auth?.login) { + if (auth?.login) { + return
Neplatný stav
+ } + + const header = <> +

Luncher

+

Aplikace pro profesionální management obědů

+ ; + + // Přihlašuje proxy — formulář se jménem nedává smysl + if (!loaded || trustedHeaders) { return (
-

Luncher

-

Aplikace pro profesionální management obědů

-
-
- - { - if (event.key === 'Enter') { - doLogin() - } - }} - /> -

- Zadejte jméno nebo přezdívku, pod kterou vás kolegové snadno identifikují. - Jméno je možné kdykoli změnit. -

+ {header} + {trustedError ? ( +
+

{trustedError}

+
- -
+ ) : ( +

Přihlašuji…

+ )}
); } - return
Neplatný stav
+ + return ( +
+
+ {header} +
+
+ + { + if (event.key === 'Enter') { + doLogin() + } + }} + /> +

+ Zadejte jméno nebo přezdívku, pod kterou vás kolegové snadno identifikují. + Jméno je možné kdykoli změnit. +

+
+ +
+
+
+ ); } diff --git a/client/src/components/modals/StoreAdminModal.tsx b/client/src/components/modals/StoreAdminModal.tsx index e0e8b80..f9cf8a6 100644 --- a/client/src/components/modals/StoreAdminModal.tsx +++ b/client/src/components/modals/StoreAdminModal.tsx @@ -6,6 +6,7 @@ import { faArrowsRotate, faPen, faPlus, faUpRightFromSquare, faXmark } from "@fo import { addStore, deleteStore, updateStore, getStoreMenuStatus, refreshStoreMenu, Store, StoreMenuProvider, StoreScrapeInfo, StoreScrapeStatus } from "../../../../types"; import { getHumanDateTime } from "../../Utils"; import PriceHistory from "../PriceHistory"; +import { useAppConfig } from "../../hooks/useGroupLimits"; import { getStoreUrls, storeUrlLabel } from "../../utils/storeUrls"; type Props = { @@ -52,6 +53,9 @@ export default function StoreAdminModal({ isOpen, onClose, stores, onStoresChang const [heslo, setHeslo] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + // Server bez admin hesla — správa (přidání/úprava/odebrání/obnovení) nejde, zbývá jen přehled. + // undefined = konfigurace nenačtena, chováme se jako dřív (správa zapnutá). + const adminEnabled = useAppConfig().storeAdmin !== false; const [scrapeInfo, setScrapeInfo] = useState>({}); const [refreshing, setRefreshing] = useState(null); // Rozbalená historie cen („obchod|služba") @@ -209,6 +213,14 @@ export default function StoreAdminModal({ isOpen, onClose, stores, onStoresChang )} + {!adminEnabled && ( + + Správa obchodů není na tomto serveru zapnutá (chybí ADMIN_PASSWORD). + Seznam obchodů, stav nabídek a historii cen lze jen prohlížet. + + )} + + {adminEnabled && <> Admin heslo
+ }
Aktuální seznam
{stores.length === 0 ? ( @@ -307,7 +320,7 @@ export default function StoreAdminModal({ isOpen, onClose, stores, onStoresChang Menu {PROVIDER_LABELS[p.provider]}: {text} {/* Zakázané tlačítko nezobrazí tooltip — obalení do span ho zobrazí i bez hesla */} - @@ -330,7 +343,7 @@ export default function StoreAdminModal({ isOpen, onClose, stores, onStoresChang - + } {p.dishCount > 0 && (