From bc181defa871c3e4319515ed489d5da8120c545d Mon Sep 17 00:00:00 2001 From: Martin Berka Date: Mon, 18 Sep 2023 22:38:04 +0200 Subject: [PATCH] =?UTF-8?q?Zpracov=C3=A1n=C3=AD=20chyb=20z=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/Api.ts | 27 +++++++++++++++++++++--- client/src/App.tsx | 16 ++++++-------- server/src/index.ts | 44 ++++++++++++++++++++++++++------------ server/src/service.ts | 49 +++++++++++++++++++++++++++---------------- server/src/utils.ts | 5 ++++- 5 files changed, 96 insertions(+), 45 deletions(-) diff --git a/client/src/Api.ts b/client/src/Api.ts index 48071a8..4d8be45 100644 --- a/client/src/Api.ts +++ b/client/src/Api.ts @@ -1,18 +1,39 @@ +import { toast } from "react-toastify"; import { PizzaOrder } from "./types"; import { getBaseUrl, getToken } from "./Utils"; +/** + * Wrapper pro volání API, u kterých chceme automaticky zobrazit toaster s chybou ze serveru. + * + * @param apiFunction volaná API funkce + */ +export function errorHandler(apiFunction: () => Promise): Promise { + return new Promise((resolve, reject) => { + apiFunction().then((result) => { + resolve(result); + }).catch(e => { + toast.error(e.message, { theme: "colored" }); + }); + }); +} + async function request( url: string, config: RequestInit = {} ): Promise { config.headers = config?.headers ? new Headers(config.headers) : new Headers(); config.headers.set("Authorization", `Bearer ${getToken()}`); - return fetch(getBaseUrl() + url, config).then(response => { + try { + const response = await fetch(getBaseUrl() + url, config); if (!response.ok) { - throw new Error(response.statusText); + const json = await response.json(); + // Vyhodíme samotnou hlášku z odpovědi, odchytí si jí errorHandler + throw new Error(json.error); } return response.json() as TResponse; - }); + } catch (e) { + return Promise.reject(e); + } } const api = { diff --git a/client/src/App.tsx b/client/src/App.tsx index 60b275f..80826db 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,7 +1,7 @@ import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'; import 'bootstrap/dist/css/bootstrap.min.css'; import { EVENT_DISCONNECT, EVENT_MESSAGE, SocketContext } from './context/socket'; -import { addChoice, addPizza, changeDepartureTime, createPizzaDay, deletePizzaDay, finishDelivery, finishOrder, getData, getQrUrl, lockPizzaDay, removeChoice, removeChoices, removePizza, unlockPizzaDay, updateNote } from './Api'; +import { addChoice, addPizza, changeDepartureTime, createPizzaDay, deletePizzaDay, errorHandler, finishDelivery, finishOrder, getData, getQrUrl, lockPizzaDay, removeChoice, removeChoices, removePizza, unlockPizzaDay, updateNote } from './Api'; import { useAuth } from './context/auth'; import Login from './Login'; import { Alert, Button, Col, Form, Row, Table } from 'react-bootstrap'; @@ -165,7 +165,7 @@ function App() { const doAddChoice = async (event: React.ChangeEvent) => { const index = Object.keys(Locations).indexOf(event.target.value as unknown as Locations); if (auth?.login) { - await addChoice(index, undefined, dayIndex); + await errorHandler(() => addChoice(index, undefined, dayIndex)); if (foodChoiceRef.current?.value) { foodChoiceRef.current.value = ""; } @@ -177,14 +177,14 @@ function App() { const restaurantKey = choiceRef.current.value; if (auth?.login) { const locationIndex = Object.keys(Locations).indexOf(restaurantKey as unknown as Locations); - await addChoice(locationIndex, Number(event.target.value), dayIndex); + await errorHandler(() => addChoice(locationIndex, Number(event.target.value), dayIndex)); } } } const doRemoveChoices = async (locationKey: string) => { if (auth?.login) { - await removeChoices(Number(locationKey), dayIndex); + await errorHandler(() => removeChoices(Number(locationKey), dayIndex)); // Vyresetujeme výběr, aby bylo jasné pro který případně vybíráme jídlo if (choiceRef?.current?.value) { choiceRef.current.value = ""; @@ -197,7 +197,7 @@ function App() { const doRemoveFoodChoice = async (locationKey: string, foodIndex: number) => { if (auth?.login) { - await removeChoice(Number(locationKey), foodIndex, dayIndex); + await errorHandler(() => removeChoice(Number(locationKey), foodIndex, dayIndex)); if (choiceRef?.current?.value) { choiceRef.current.value = ""; } @@ -358,11 +358,7 @@ function App() { Poslední změny:
    -
  • Možnost náhledu na celý týden a výběru na následující dny v týdnu
  • -
      -
    • Pizza day je možno založit pouze pro aktuální den
    • -
    • Ne, šipky na klávesnici zatím nefungují
    • -
    +
  • Ochrana proti některým Stánkovinám
{dayIndex != null && diff --git a/server/src/index.ts b/server/src/index.ts index 581dabb..afda2e1 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -7,7 +7,7 @@ import dotenv from 'dotenv'; import path from 'path'; import { getQr } from "./qr"; import { generateToken, getLogin, getTrusted, verify } from "./auth"; -import { getDayOfWeekIndex } from "./utils"; +import { InsufficientPermissions, getDayOfWeekIndex } from "./utils"; const ENVIRONMENT = process.env.NODE_ENV || 'production'; dotenv.config({ path: path.resolve(__dirname, `./.env.${ENVIRONMENT}`) }); @@ -219,7 +219,7 @@ app.post("/api/finishDelivery", async (req, res) => { res.status(200).json({}); }); -app.post("/api/addChoice", async (req, res) => { +app.post("/api/addChoice", async (req, res, next) => { const login = getLogin(parseToken(req)); const trusted = getTrusted(parseToken(req)); if (req.body.locationIndex > -1) { @@ -233,15 +233,18 @@ app.post("/api/addChoice", async (req, res) => { } date = getDateForWeekIndex(dayIndex); } - const data = await addChoice(login, trusted, req.body.locationIndex, req.body.foodIndex, date); - io.emit("message", data); - return res.status(200).json(data); + try { + const data = await addChoice(login, trusted, req.body.locationIndex, req.body.foodIndex, date); + io.emit("message", data); + return res.status(200).json(data); + } catch (e: any) { next(e) } } return res.status(400); // TODO přidat popis chyby }); -app.post("/api/removeChoices", async (req, res) => { +app.post("/api/removeChoices", async (req, res, next) => { const login = getLogin(parseToken(req)); + const trusted = getTrusted(parseToken(req)); let date = undefined; if (req.body.dayIndex != null) { let dayIndex; @@ -252,13 +255,16 @@ app.post("/api/removeChoices", async (req, res) => { } date = getDateForWeekIndex(dayIndex); } - const data = await removeChoices(login, req.body.locationIndex, date); - io.emit("message", data); - res.status(200).json(data); + try { + const data = await removeChoices(login, trusted, req.body.locationIndex, date); + io.emit("message", data); + res.status(200).json(data); + } catch (e: any) { next(e) } }); -app.post("/api/removeChoice", async (req, res) => { +app.post("/api/removeChoice", async (req, res, next) => { const login = getLogin(parseToken(req)); + const trusted = getTrusted(parseToken(req)); let date = undefined; if (req.body.dayIndex != null) { let dayIndex; @@ -269,9 +275,11 @@ app.post("/api/removeChoice", async (req, res) => { } date = getDateForWeekIndex(dayIndex); } - const data = await removeChoice(login, req.body.locationIndex, req.body.foodIndex, date); - io.emit("message", data); - res.status(200).json(data); + try { + const data = await removeChoice(login, trusted, req.body.locationIndex, req.body.foodIndex, date); + io.emit("message", data); + res.status(200).json(data); + } catch (e: any) { next(e) } }); // TODO přejmenovat, ať je jasné, že to patří k Pizza day @@ -302,6 +310,16 @@ app.post("/api/changeDepartureTime", async (req, res) => { res.status(200).json(data); }); +// Middleware pro zpracování chyb +app.use((err: any, req: any, res: any, next: any) => { + if (err instanceof InsufficientPermissions) { + res.status(403).send({ error: err.message }) + } else { + res.status(500).send({ error: err.message }) + } + next(); +}); + io.on("connection", (socket) => { console.log(`New client connected: ${socket.id}`); diff --git a/server/src/service.ts b/server/src/service.ts index 3dbdd89..8885569 100644 --- a/server/src/service.ts +++ b/server/src/service.ts @@ -1,7 +1,7 @@ -import { formatDate, getDayOfWeekIndex, getHumanDate, getHumanTime, getIsWeekend } from "./utils"; +import { InsufficientPermissions, formatDate, getDayOfWeekIndex, getHumanDate, getHumanTime, getIsWeekend } from "./utils"; import { callNotifikace } from "./notifikace"; import { generateQr } from "./qr"; -import { ClientData, PizzaDayState, UdalostEnum, Pizza, PizzaSize, Order, PizzaOrder, Locations, Restaurants, Food, Menu } from "../../types"; +import { ClientData, PizzaDayState, UdalostEnum, Pizza, PizzaSize, Order, PizzaOrder, Locations, Restaurants, Menu } from "../../types"; import getStorage from "./storage"; import { getMenuSladovnicka, getMenuTechTower, getMenuUMotliku } from "./restaurants"; import { downloadPizzy } from "./chefie"; @@ -345,14 +345,15 @@ export async function initIfNeeded(date?: Date) { * Odstraní kompletně volbu uživatele (včetně případných podřízených jídel). * * @param login login uživatele + * @param trusted příznak, zda se jedná o ověřeného uživatele * @param location vybrané "umístění" * @param date datum, ke kterému se volba vztahuje * @returns */ -export async function removeChoices(login: string, location: Locations, date?: Date) { +export async function removeChoices(login: string, trusted: boolean, location: Locations, date?: Date) { const selectedDay = formatDate(date ?? getToday()); let data: ClientData = await storage.getData(selectedDay); - // TODO zajistit, že neověřený uživatel se stejným loginem nemůže mazat volby ověřeného + validateTrusted(data, login, trusted); if (location in data.choices) { if (login in data.choices[location]) { delete data.choices[location][login] @@ -370,15 +371,16 @@ export async function removeChoices(login: string, location: Locations, date?: D * Neodstraňuje volbu samotnou, k tomu slouží {@link removeChoices}. * * @param login login uživatele + * @param trusted příznak, zda se jedná o ověřeného uživatele * @param location vybrané "umístění" * @param foodIndex index jídla v jídelním lístku daného umístění, pokud existuje * @param date datum, ke kterému se volba vztahuje * @returns */ -export async function removeChoice(login: string, location: Locations, foodIndex: number, date?: Date) { +export async function removeChoice(login: string, trusted: boolean, location: Locations, foodIndex: number, date?: Date) { const selectedDay = formatDate(date ?? getToday()); let data: ClientData = await storage.getData(selectedDay); - // TODO řešit ověření uživatele + validateTrusted(data, login, trusted); if (location in data.choices) { if (login in data.choices[location]) { const index = data.choices[location][login].options.indexOf(foodIndex); @@ -411,20 +413,13 @@ async function removeChoiceIfPresent(login: string, date: string) { } /** - * Přidá volbu uživatele. + * Ověří, zda se neověřený uživatel nepokouší přepsat údaje ověřeného a případně vyhodí chybu. * - * @param login login uživatele - * @param location vybrané "umístění" - * @param foodIndex volitelný index jídla v daném umístění + * @param data aktuální klientská data + * @param login přihlašovací jméno uživatele * @param trusted příznak, zda se jedná o ověřeného uživatele - * @param date datum, ke kterému se volba vztahuje - * @returns aktuální data */ -export async function addChoice(login: string, trusted: boolean, location: Locations, foodIndex?: number, date?: Date) { - await initIfNeeded(); - const selectedDate = formatDate(date ?? getToday()); - let data: ClientData = await storage.getData(selectedDate); - // Ověření, že se neověřený užívatel nepokouší přepsat údaje ověřeného +function validateTrusted(data: ClientData, login: string, trusted: boolean) { const locations = Object.values(data?.choices); let found = false; if (!trusted) { @@ -435,8 +430,26 @@ export async function addChoice(login: string, trusted: boolean, location: Locat } } if (!trusted && found) { - throw Error("Nelze změnit volbu ověřeného uživatele"); + throw new InsufficientPermissions("Nelze změnit volbu ověřeného uživatele"); } +} + +/** + * Přidá volbu uživatele. + * + * @param login login uživatele + * @param trusted příznak, zda se jedná o ověřeného uživatele + * @param location vybrané "umístění" + * @param foodIndex volitelný index jídla v daném umístění + * @param trusted příznak, zda se jedná o ověřeného uživatele + * @param date datum, ke kterému se volba vztahuje + * @returns aktuální data + */ +export async function addChoice(login: string, trusted: boolean, location: Locations, foodIndex?: number, date?: Date) { + await initIfNeeded(); + const selectedDate = formatDate(date ?? getToday()); + let data: ClientData = await storage.getData(selectedDate); + validateTrusted(data, login, trusted); // Pokud měníme pouze lokaci, mažeme případné předchozí if (foodIndex == null) { data = await removeChoiceIfPresent(login, selectedDate); diff --git a/server/src/utils.ts b/server/src/utils.ts index 6e7893e..c5a3bd6 100644 --- a/server/src/utils.ts +++ b/server/src/utils.ts @@ -37,4 +37,7 @@ export const getDayOfWeekIndex = (date: Date) => { export function getIsWeekend(date: Date) { const index = getDayOfWeekIndex(date); return index == 5 || index == 6; -} \ No newline at end of file +} + +// TODO umístit do samostatného souboru +export class InsufficientPermissions extends Error { } \ No newline at end of file