From 74893c38ebd6b68681195116779242b0c02882c4 Mon Sep 17 00:00:00 2001 From: Martin Berka Date: Tue, 3 Oct 2023 22:52:09 +0200 Subject: [PATCH] =?UTF-8?q?Refaktor,=20rozd=C4=9Blen=C3=AD=20api,=20zp?= =?UTF-8?q?=C5=99ehledn=C4=9Bn=C3=AD=20k=C3=B3du?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/Api.ts | 121 ---------- client/src/App.tsx | 4 +- client/src/Login.tsx | 2 +- client/src/api/Api.ts | 57 +++++ client/src/api/FoodApi.ts | 19 ++ client/src/api/PizzaDayApi.ts | 44 ++++ client/src/api/VotingApi.ts | 12 + client/src/components/Header.tsx | 3 +- client/src/components/PizzaOrderList.tsx | 5 +- server/src/index.ts | 277 ++--------------------- server/src/routes/foodRoutes.ts | 113 +++++++++ server/src/routes/pizzaDayRoutes.ts | 109 +++++++++ server/src/routes/votingRoutes.ts | 27 +++ server/src/utils.ts | 43 ++++ server/src/websocket.ts | 28 +++ 15 files changed, 473 insertions(+), 391 deletions(-) delete mode 100644 client/src/Api.ts create mode 100644 client/src/api/Api.ts create mode 100644 client/src/api/FoodApi.ts create mode 100644 client/src/api/PizzaDayApi.ts create mode 100644 client/src/api/VotingApi.ts create mode 100644 server/src/routes/foodRoutes.ts create mode 100644 server/src/routes/pizzaDayRoutes.ts create mode 100644 server/src/routes/votingRoutes.ts create mode 100644 server/src/websocket.ts diff --git a/client/src/Api.ts b/client/src/Api.ts deleted file mode 100644 index c8f3a97..0000000 --- a/client/src/Api.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { toast } from "react-toastify"; -import { FeatureRequest, 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()}`); - try { - const response = await fetch(getBaseUrl() + url, config); - if (!response.ok) { - 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 = { - get: (url: string) => request(url), - post: (url: string, body: TBody) => request(url, { method: 'POST', body, headers: { 'Content-Type': 'application/json' } }), -} - -export const getQrUrl = (login: string) => { - return `${getBaseUrl()}/api/qr?login=${login}`; -} - -export const getData = async (dayIndex?: number) => { - let url = '/api/data'; - if (dayIndex != null) { - url += '?dayIndex=' + dayIndex; - } - return await api.get(url); -} -export const createPizzaDay = async () => { - return await api.post('/api/createPizzaDay', undefined); -} - -export const deletePizzaDay = async () => { - return await api.post('/api/deletePizzaDay', undefined); -} - -export const lockPizzaDay = async () => { - return await api.post('/api/lockPizzaDay', undefined); -} - -export const unlockPizzaDay = async () => { - return await api.post('/api/unlockPizzaDay', undefined); -} - -export const finishOrder = async () => { - return await api.post('/api/finishOrder', undefined); -} - -export const finishDelivery = async (bankAccount?: string, bankAccountHolder?: string) => { - return await api.post('/api/finishDelivery', JSON.stringify({ bankAccount, bankAccountHolder })); -} - -export const addChoice = async (locationIndex: number, foodIndex?: number, dayIndex?: number) => { - return await api.post('/api/addChoice', JSON.stringify({ locationIndex, foodIndex, dayIndex })); -} - -export const removeChoices = async (locationIndex: number, dayIndex?: number) => { - return await api.post('/api/removeChoices', JSON.stringify({ locationIndex, dayIndex })); -} - -export const removeChoice = async (locationIndex: number, foodIndex: number, dayIndex?: number) => { - return await api.post('/api/removeChoice', JSON.stringify({ locationIndex, foodIndex, dayIndex })); -} - -export const addPizza = async (pizzaIndex: number, pizzaSizeIndex: number) => { - return await api.post('/api/addPizza', JSON.stringify({ pizzaIndex, pizzaSizeIndex })); -} - -export const removePizza = async (pizzaOrder: PizzaOrder) => { - return await api.post('/api/removePizza', JSON.stringify({ pizzaOrder })); -} - -export const updatePizzaDayNote = async (note?: string) => { - return await api.post('/api/updatePizzaDayNote', JSON.stringify({ note })); -} - -export const login = async (login?: string) => { - return await api.post('/api/login', JSON.stringify({ login })); -} - -export const changeDepartureTime = async (time: string, dayIndex?: number) => { - return await api.post('/api/changeDepartureTime', JSON.stringify({ time, dayIndex })); -} - -export const updatePizzaFee = async (login: string, text?: string, price?: number) => { - return await api.post('/api/updatePizzaFee', JSON.stringify({ login, text, price })); -} - -export const getFeatureVotes = async () => { - return await api.get('/api/getFeatureVotes'); -} - -export const updateFeatureVote = async (option: FeatureRequest, active: boolean) => { - return await api.post('/api/updateFeatureVote', JSON.stringify({ option, active })); -} \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx index 6a79fbc..51dd614 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,7 +1,7 @@ import React, { useContext, useEffect, useMemo, useRef, useState, useCallback } from 'react'; import 'bootstrap/dist/css/bootstrap.min.css'; import { EVENT_DISCONNECT, EVENT_MESSAGE, SocketContext } from './context/socket'; -import { addChoice, addPizza, changeDepartureTime, createPizzaDay, deletePizzaDay, errorHandler, finishDelivery, finishOrder, getData, getQrUrl, lockPizzaDay, removeChoice, removeChoices, removePizza, unlockPizzaDay, updatePizzaDayNote } from './Api'; +import { addPizza, createPizzaDay, deletePizzaDay, finishDelivery, finishOrder, lockPizzaDay, removePizza, unlockPizzaDay, updatePizzaDayNote } from './api/PizzaDayApi'; import { useAuth } from './context/auth'; import Login from './Login'; import { Alert, Button, Col, Form, Row, Table } from 'react-bootstrap'; @@ -18,6 +18,8 @@ import { ClientData, Restaurants, Food, Order, Locations, PizzaOrder, PizzaDaySt import Footer from './components/Footer'; import { faChainBroken, faChevronLeft, faChevronRight, faGear, faSatelliteDish, faSearch } from '@fortawesome/free-solid-svg-icons'; import Loader from './components/Loader'; +import { getData, errorHandler, getQrUrl } from './api/Api'; +import { addChoice, removeChoices, removeChoice, changeDepartureTime } from './api/FoodApi'; const EVENT_CONNECT = "connect" diff --git a/client/src/Login.tsx b/client/src/Login.tsx index d1556e6..59e1241 100644 --- a/client/src/Login.tsx +++ b/client/src/Login.tsx @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useRef } from 'react'; import { Button } from 'react-bootstrap'; import { useAuth } from './context/auth'; -import { login } from './Api'; +import { login } from './api/Api'; import './Login.css'; /** diff --git a/client/src/api/Api.ts b/client/src/api/Api.ts new file mode 100644 index 0000000..11de2c3 --- /dev/null +++ b/client/src/api/Api.ts @@ -0,0 +1,57 @@ +import { toast } from "react-toastify"; +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()}`); + try { + const response = await fetch(getBaseUrl() + url, config); + if (!response.ok) { + 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); + } +} + +export const api = { + get: (url: string) => request(url), + post: (url: string, body: TBody) => request(url, { method: 'POST', body, headers: { 'Content-Type': 'application/json' } }), +} + +export const getQrUrl = (login: string) => { + return `${getBaseUrl()}/api/qr?login=${login}`; +} + +export const getData = async (dayIndex?: number) => { + let url = '/api/data'; + if (dayIndex != null) { + url += '?dayIndex=' + dayIndex; + } + return await api.get(url); +} + +export const login = async (login?: string) => { + return await api.post('/api/login', JSON.stringify({ login })); +} diff --git a/client/src/api/FoodApi.ts b/client/src/api/FoodApi.ts new file mode 100644 index 0000000..a35bf0a --- /dev/null +++ b/client/src/api/FoodApi.ts @@ -0,0 +1,19 @@ +import { api } from "./Api"; + +const FOOD_API_PREFIX = '/api/food'; + +export const addChoice = async (locationIndex: number, foodIndex?: number, dayIndex?: number) => { + return await api.post(`${FOOD_API_PREFIX}/addChoice`, JSON.stringify({ locationIndex, foodIndex, dayIndex })); +} + +export const removeChoices = async (locationIndex: number, dayIndex?: number) => { + return await api.post(`${FOOD_API_PREFIX}/removeChoices`, JSON.stringify({ locationIndex, dayIndex })); +} + +export const removeChoice = async (locationIndex: number, foodIndex: number, dayIndex?: number) => { + return await api.post(`${FOOD_API_PREFIX}/removeChoice`, JSON.stringify({ locationIndex, foodIndex, dayIndex })); +} + +export const changeDepartureTime = async (time: string, dayIndex?: number) => { + return await api.post(`${FOOD_API_PREFIX}/changeDepartureTime`, JSON.stringify({ time, dayIndex })); +} diff --git a/client/src/api/PizzaDayApi.ts b/client/src/api/PizzaDayApi.ts new file mode 100644 index 0000000..fffdcfb --- /dev/null +++ b/client/src/api/PizzaDayApi.ts @@ -0,0 +1,44 @@ +import { PizzaOrder } from "../types"; +import { api } from "./Api"; + +const PIZZADAY_API_PREFIX = '/api/pizzaDay'; + +export const createPizzaDay = async () => { + return await api.post(`${PIZZADAY_API_PREFIX}/create`, undefined); +} + +export const deletePizzaDay = async () => { + return await api.post(`${PIZZADAY_API_PREFIX}/delete`, undefined); +} + +export const lockPizzaDay = async () => { + return await api.post(`${PIZZADAY_API_PREFIX}/lock`, undefined); +} + +export const unlockPizzaDay = async () => { + return await api.post(`${PIZZADAY_API_PREFIX}/unlock`, undefined); +} + +export const finishOrder = async () => { + return await api.post(`${PIZZADAY_API_PREFIX}/finishOrder`, undefined); +} + +export const finishDelivery = async (bankAccount?: string, bankAccountHolder?: string) => { + return await api.post(`${PIZZADAY_API_PREFIX}/finishDelivery`, JSON.stringify({ bankAccount, bankAccountHolder })); +} + +export const addPizza = async (pizzaIndex: number, pizzaSizeIndex: number) => { + return await api.post(`${PIZZADAY_API_PREFIX}/add`, JSON.stringify({ pizzaIndex, pizzaSizeIndex })); +} + +export const removePizza = async (pizzaOrder: PizzaOrder) => { + return await api.post(`${PIZZADAY_API_PREFIX}/remove`, JSON.stringify({ pizzaOrder })); +} + +export const updatePizzaDayNote = async (note?: string) => { + return await api.post(`${PIZZADAY_API_PREFIX}/updatePizzaDayNote`, JSON.stringify({ note })); +} + +export const updatePizzaFee = async (login: string, text?: string, price?: number) => { + return await api.post(`${PIZZADAY_API_PREFIX}/updatePizzaFee`, JSON.stringify({ login, text, price })); +} diff --git a/client/src/api/VotingApi.ts b/client/src/api/VotingApi.ts new file mode 100644 index 0000000..e215546 --- /dev/null +++ b/client/src/api/VotingApi.ts @@ -0,0 +1,12 @@ +import { FeatureRequest } from "../types"; +import { api } from "./Api"; + +const VOTING_API_PREFIX = '/api/voting'; + +export const getFeatureVotes = async () => { + return await api.get(`${VOTING_API_PREFIX}/getVotes`); +} + +export const updateFeatureVote = async (option: FeatureRequest, active: boolean) => { + return await api.post(`${VOTING_API_PREFIX}/updateVote`, JSON.stringify({ option, active })); +} \ No newline at end of file diff --git a/client/src/components/Header.tsx b/client/src/components/Header.tsx index d3be02a..e941782 100644 --- a/client/src/components/Header.tsx +++ b/client/src/components/Header.tsx @@ -5,7 +5,8 @@ import BankAccountModal from "./modals/BankAccountModal"; import { useBank } from "../context/bank"; import FeaturesVotingModal from "./modals/FeaturesVotingModal"; import { FeatureRequest } from "../types"; -import { errorHandler, getFeatureVotes, updateFeatureVote } from "../Api"; +import { errorHandler } from "../api/Api"; +import { getFeatureVotes, updateFeatureVote } from "../api/VotingApi"; export default function Header() { diff --git a/client/src/components/PizzaOrderList.tsx b/client/src/components/PizzaOrderList.tsx index d14c039..80d8769 100644 --- a/client/src/components/PizzaOrderList.tsx +++ b/client/src/components/PizzaOrderList.tsx @@ -1,8 +1,7 @@ import { Table } from "react-bootstrap"; -import { useAuth } from "../context/auth"; import { Order, PizzaDayState, PizzaOrder } from "../types"; -import { updatePizzaFee } from "../Api"; import PizzaOrderRow from "./PizzaOrderRow"; +import { updatePizzaFee } from "../api/PizzaDayApi"; type Props = { state: PizzaDayState, @@ -12,8 +11,6 @@ type Props = { } export default function PizzaOrderList({ state, orders, onDelete, creator }: Props) { - const auth = useAuth(); - const saveFees = async (customer: string, text?: string, price?: number) => { await updatePizzaFee(customer, text, price); } diff --git a/server/src/index.ts b/server/src/index.ts index 18bbc6b..f53edf3 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,15 +1,16 @@ import express from "express"; -import { Server } from "socket.io"; import bodyParser from "body-parser"; import cors from 'cors'; -import { addChoice, getData, getDateForWeekIndex, getToday, removeChoice, removeChoices, updateDepartureTime } from "./service"; -import { addPizzaOrder, createPizzaDay, deletePizzaDay, finishPizzaDelivery, finishPizzaOrder, getPizzaList, lockPizzaDay, removePizzaOrder, unlockPizzaDay, updatePizzaDayNote, updatePizzaFee } from "./pizza"; +import { getData, getDateForWeekIndex } from "./service"; import dotenv from 'dotenv'; import path from 'path'; import { getQr } from "./qr"; -import { generateToken, getLogin, getTrusted, verify } from "./auth"; -import { InsufficientPermissions, getDayOfWeekIndex } from "./utils"; -import { getUserVotes, updateFeatureVote } from "./voting"; +import { generateToken, verify } from "./auth"; +import { InsufficientPermissions } from "./utils"; +import { initWebsocket } from "./websocket"; +import pizzaDayRoutes from "./routes/pizzaDayRoutes"; +import foodRoutes from "./routes/foodRoutes"; +import votingRoutes from "./routes/votingRoutes"; const ENVIRONMENT = process.env.NODE_ENV || 'production'; dotenv.config({ path: path.resolve(__dirname, `./.env.${ENVIRONMENT}`) }); @@ -21,59 +22,15 @@ if (!process.env.JWT_SECRET) { const app = express(); const server = require("http").createServer(app); -const io = new Server(server, { - cors: { - origin: "*", - }, -}); +initWebsocket(server); // Body-parser middleware for parsing JSON app.use(bodyParser.json()); -// app.use(express.json()); app.use(cors({ origin: '*' })); -// app.use((req, res, next) => { -// console.log("--- Request ---") -// console.log(req.url); -// console.log(req.baseUrl); -// console.log(req.originalUrl); -// console.log(req.path); -// next(); -// }); - -app.use(express.static('public')) - -const parseToken = (req: any) => { - if (req?.headers?.authorization) { - return req.headers.authorization.split(' ')[1]; - } -} - -/** - * 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: any) => { - if (req.body.dayIndex == null) { - throw Error(`Nebyl předán index dne v týdnu.`); - } - const todayDayIndex = getDayOfWeekIndex(getToday()); - const dayIndex = parseInt(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; -} - // ----------- Metody nevyžadující token -------------- app.get("/api/whoami", (req, res) => { @@ -113,7 +70,7 @@ app.get("/api/qr", (req, res) => { // ---------------------------------------------------- /** Middleware ověřující JWT token */ -app.use((req, res, next) => { +app.use("/api/", (req, res, next) => { const userHeader = req.header('remote-user'); const nameHeader = req.header('remote-name'); const emailHeader = req.header('remote-email'); @@ -143,205 +100,11 @@ app.get("/api/data", async (req, res) => { res.status(200).json(await getData(date)); }); -/** Založí pizza day pro aktuální den, za předpokladu že dosud neexistuje. */ -app.post("/api/createPizzaDay", async (req, res) => { - const login = getLogin(parseToken(req)); - const data = await createPizzaDay(login); - res.status(200).json(data); - io.emit("message", data); -}); - -/** Smaže pizza day pro aktuální den, za předpokladu že existuje. */ -app.post("/api/deletePizzaDay", async (req, res) => { - const login = getLogin(parseToken(req)); - const data = await deletePizzaDay(login); - io.emit("message", data); -}); - -app.post("/api/addPizza", async (req, res) => { - const login = getLogin(parseToken(req)); - if (isNaN(req.body?.pizzaIndex)) { - throw Error("Nebyl předán index pizzy"); - } - const pizzaIndex = req.body.pizzaIndex; - if (isNaN(req.body?.pizzaSizeIndex)) { - throw Error("Nebyl předán index velikosti pizzy"); - } - const pizzaSizeIndex = req.body.pizzaSizeIndex; - let pizzy = await getPizzaList(); - if (!pizzy) { - throw Error("Selhalo získání seznamu dostupných pizz."); - } - if (!pizzy[pizzaIndex]) { - throw Error("Neplatný index pizzy: " + pizzaIndex); - } - if (!pizzy[pizzaIndex].sizes[pizzaSizeIndex]) { - throw Error("Neplatný index velikosti pizzy: " + pizzaSizeIndex); - } - const data = await addPizzaOrder(login, pizzy[pizzaIndex], pizzy[pizzaIndex].sizes[pizzaSizeIndex]); - io.emit("message", data); - res.status(200).json({}); -}); - -app.post("/api/removePizza", async (req, res) => { - const login = getLogin(parseToken(req)); - if (!req.body?.pizzaOrder) { - throw Error("Nebyla předána objednávka"); - } - const data = await removePizzaOrder(login, req.body?.pizzaOrder); - io.emit("message", data); - res.status(200).json({}); -}); - -app.post("/api/lockPizzaDay", async (req, res) => { - const login = getLogin(parseToken(req)); - const data = await lockPizzaDay(login); - io.emit("message", data); - res.status(200).json({}); -}); - -app.post("/api/unlockPizzaDay", async (req, res) => { - const login = getLogin(parseToken(req)); - const data = await unlockPizzaDay(login); - io.emit("message", data); - res.status(200).json({}); -}); - -app.post("/api/finishOrder", async (req, res) => { - const login = getLogin(parseToken(req)); - const data = await finishPizzaOrder(login); - io.emit("message", data); - res.status(200).json({}); -}); - -app.post("/api/finishDelivery", async (req, res) => { - const login = getLogin(parseToken(req)); - const data = await finishPizzaDelivery(login, req.body.bankAccount, req.body.bankAccountHolder); - io.emit("message", data); - res.status(200).json({}); -}); - -app.post("/api/addChoice", async (req, res, next) => { - const login = getLogin(parseToken(req)); - const trusted = getTrusted(parseToken(req)); - if (req.body.locationIndex > -1) { - 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.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, 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.locationIndex, date); - io.emit("message", data); - res.status(200).json(data); - } catch (e: any) { next(e) } -}); - -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; - 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.locationIndex, req.body.foodIndex, date); - io.emit("message", data); - res.status(200).json(data); - } catch (e: any) { next(e) } -}); - -app.post("/api/updatePizzaDayNote", async (req, res) => { - const login = getLogin(parseToken(req)); - if (req.body.note && req.body.note.length > 100) { - throw Error("Poznámka může mít maximálně 100 znaků"); - } - const data = await updatePizzaDayNote(login, req.body.note); - io.emit("message", data); - res.status(200).json(data); -}); - -app.post("/api/changeDepartureTime", async (req, 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); - io.emit("message", data); - res.status(200).json(data); - } catch (e: any) { next(e) } -}); - -app.post("/api/updatePizzaFee", async (req, res, next) => { - const login = getLogin(parseToken(req)); - if (!req.body.login) { - return res.status(400).json({ error: "Nebyl předán login cílového uživatele" }); - } - try { - const data = await updatePizzaFee(login, req.body.login, req.body.text, req.body.price); - io.emit("message", data); - res.status(200).json(data); - } catch (e: any) { next(e) } -}); - -app.get("/api/getFeatureVotes", async (req, res) => { - const login = getLogin(parseToken(req)); - const data = await getUserVotes(login); - res.status(200).json(data); -}); - -app.post("/api/updateFeatureVote", async (req, res, next) => { - const login = getLogin(parseToken(req)); - if (req.body?.option == null || req.body?.active == null) { - res.status(400).json({ error: "Chybné parametry volání" }); - } - try { - const data = await updateFeatureVote(login, req.body.option, req.body.active); - io.emit("message", data); - res.status(200).json(data); - } catch (e: any) { next(e) } -}); +// Ostatní routes +app.use("/api/pizzaDay", pizzaDayRoutes); +app.use("/api/food", foodRoutes); +app.use("/api/voting", votingRoutes); +app.use(express.static('public')) // Middleware pro zpracování chyb app.use((err: any, req: any, res: any, next: any) => { @@ -353,18 +116,6 @@ app.use((err: any, req: any, res: any, next: any) => { next(); }); -io.on("connection", (socket) => { - console.log(`New client connected: ${socket.id}`); - - socket.on("message", (message) => { - io.emit("message", message); - }); - - socket.on("disconnect", () => { - console.log(`Client disconnected: ${socket.id}`); - }); -}); - const PORT = process.env.PORT || 3001; const HOST = process.env.HOST || '0.0.0.0'; diff --git a/server/src/routes/foodRoutes.ts b/server/src/routes/foodRoutes.ts new file mode 100644 index 0000000..2767773 --- /dev/null +++ b/server/src/routes/foodRoutes.ts @@ -0,0 +1,113 @@ +import express from "express"; +import { getLogin, getTrusted } from "../auth"; +import { getDateForWeekIndex, addChoice, removeChoices, removeChoice, updateDepartureTime, getToday } from "../service"; +import { getDayOfWeekIndex, parseToken } from "../utils"; +import { getWebsocket } from "../websocket"; + +/** + * 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: any) => { + if (req.body.dayIndex == null) { + throw Error(`Nebyl předán index dne v týdnu.`); + } + const todayDayIndex = getDayOfWeekIndex(getToday()); + const dayIndex = parseInt(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, res, next) => { + const login = getLogin(parseToken(req)); + const trusted = getTrusted(parseToken(req)); + if (req.body.locationIndex > -1) { + 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.locationIndex, req.body.foodIndex, date); + getWebsocket().emit("message", data); + return res.status(200).json(data); + } catch (e: any) { next(e) } + } + return res.status(400); // TODO přidat popis chyby +}); + +router.post("/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; + 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.locationIndex, date); + getWebsocket().emit("message", data); + res.status(200).json(data); + } catch (e: any) { next(e) } +}); + +router.post("/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; + 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.locationIndex, req.body.foodIndex, date); + getWebsocket().emit("message", data); + res.status(200).json(data); + } catch (e: any) { next(e) } +}); + +router.post("/changeDepartureTime", async (req, 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) } +}); + +export default router; \ No newline at end of file diff --git a/server/src/routes/pizzaDayRoutes.ts b/server/src/routes/pizzaDayRoutes.ts new file mode 100644 index 0000000..fe67598 --- /dev/null +++ b/server/src/routes/pizzaDayRoutes.ts @@ -0,0 +1,109 @@ +import express from "express"; +import { getLogin } from "../auth"; +import { createPizzaDay, deletePizzaDay, getPizzaList, addPizzaOrder, removePizzaOrder, lockPizzaDay, unlockPizzaDay, finishPizzaOrder, finishPizzaDelivery, updatePizzaDayNote, updatePizzaFee } from "../pizza"; +import { parseToken } from "../utils"; +import { getWebsocket } from "../websocket"; + +const router = express.Router(); + +/** Založí pizza day pro aktuální den, za předpokladu že dosud neexistuje. */ +router.post("/create", async (req, res) => { + const login = getLogin(parseToken(req)); + const data = await createPizzaDay(login); + res.status(200).json(data); + getWebsocket().emit("message", data); +}); + +/** Smaže pizza day pro aktuální den, za předpokladu že existuje. */ +router.post("/delete", async (req, res) => { + const login = getLogin(parseToken(req)); + const data = await deletePizzaDay(login); + getWebsocket().emit("message", data); +}); + +router.post("/add", async (req, res) => { + const login = getLogin(parseToken(req)); + if (isNaN(req.body?.pizzaIndex)) { + throw Error("Nebyl předán index pizzy"); + } + const pizzaIndex = req.body.pizzaIndex; + if (isNaN(req.body?.pizzaSizeIndex)) { + throw Error("Nebyl předán index velikosti pizzy"); + } + const pizzaSizeIndex = req.body.pizzaSizeIndex; + let pizzy = await getPizzaList(); + if (!pizzy) { + throw Error("Selhalo získání seznamu dostupných pizz."); + } + if (!pizzy[pizzaIndex]) { + throw Error("Neplatný index pizzy: " + pizzaIndex); + } + if (!pizzy[pizzaIndex].sizes[pizzaSizeIndex]) { + throw Error("Neplatný index velikosti pizzy: " + pizzaSizeIndex); + } + const data = await addPizzaOrder(login, pizzy[pizzaIndex], pizzy[pizzaIndex].sizes[pizzaSizeIndex]); + getWebsocket().emit("message", data); + res.status(200).json({}); +}); + +router.post("/remove", async (req, res) => { + const login = getLogin(parseToken(req)); + if (!req.body?.pizzaOrder) { + throw Error("Nebyla předána objednávka"); + } + const data = await removePizzaOrder(login, req.body?.pizzaOrder); + getWebsocket().emit("message", data); + res.status(200).json({}); +}); + +router.post("/lock", async (req, res) => { + const login = getLogin(parseToken(req)); + const data = await lockPizzaDay(login); + getWebsocket().emit("message", data); + res.status(200).json({}); +}); + +router.post("/unlock", async (req, res) => { + const login = getLogin(parseToken(req)); + const data = await unlockPizzaDay(login); + getWebsocket().emit("message", data); + res.status(200).json({}); +}); + +router.post("/finishOrder", async (req, res) => { + const login = getLogin(parseToken(req)); + const data = await finishPizzaOrder(login); + getWebsocket().emit("message", data); + res.status(200).json({}); +}); + +router.post("/finishDelivery", async (req, res) => { + const login = getLogin(parseToken(req)); + const data = await finishPizzaDelivery(login, req.body.bankAccount, req.body.bankAccountHolder); + getWebsocket().emit("message", data); + res.status(200).json({}); +}); + +router.post("/updatePizzaDayNote", async (req, res) => { + const login = getLogin(parseToken(req)); + if (req.body.note && req.body.note.length > 100) { + throw Error("Poznámka může mít maximálně 100 znaků"); + } + const data = await updatePizzaDayNote(login, req.body.note); + getWebsocket().emit("message", data); + res.status(200).json(data); +}); + +router.post("/updatePizzaFee", async (req, res, next) => { + const login = getLogin(parseToken(req)); + if (!req.body.login) { + return res.status(400).json({ error: "Nebyl předán login cílového uživatele" }); + } + try { + const data = await updatePizzaFee(login, req.body.login, req.body.text, req.body.price); + getWebsocket().emit("message", data); + res.status(200).json(data); + } catch (e: any) { next(e) } +}); + +export default router; \ No newline at end of file diff --git a/server/src/routes/votingRoutes.ts b/server/src/routes/votingRoutes.ts new file mode 100644 index 0000000..2bdeafd --- /dev/null +++ b/server/src/routes/votingRoutes.ts @@ -0,0 +1,27 @@ +import express from "express"; +import { getLogin } from "../auth"; +import { parseToken } from "../utils"; +import { getUserVotes, updateFeatureVote } from "../voting"; +import { getWebsocket } from "../websocket"; + +const router = express.Router(); + +router.get("/getVotes", async (req, res) => { + const login = getLogin(parseToken(req)); + const data = await getUserVotes(login); + res.status(200).json(data); +}); + +router.post("/updateVote", async (req, res, next) => { + const login = getLogin(parseToken(req)); + if (req.body?.option == null || req.body?.active == null) { + res.status(400).json({ error: "Chybné parametry volání" }); + } + try { + const data = await updateFeatureVote(login, req.body.option, req.body.active); + getWebsocket().emit("message", data); + res.status(200).json(data); + } catch (e: any) { next(e) } +}); + +export default router; \ No newline at end of file diff --git a/server/src/utils.ts b/server/src/utils.ts index c5a3bd6..1dbd885 100644 --- a/server/src/utils.ts +++ b/server/src/utils.ts @@ -39,5 +39,48 @@ export function getIsWeekend(date: Date) { return index == 5 || index == 6; } +/** + * Vrátí JWT token z hlaviček, pokud ho obsahují. + * + * @param req request + * @returns token, pokud ho hlavičky requestu obsahují + */ +export const parseToken = (req: any) => { + if (req?.headers?.authorization) { + return req.headers.authorization.split(' ')[1]; + } +} + +/** + * Ověří přítomnost (not null) předaných parametrů v URL query. + * V případě nepřítomnosti kteréhokoli parametru vyhodí chybu. + * + * @param req request + * @param paramNames pole názvů požadovaných parametrů + */ +export const checkQueryParams = (req: any, paramNames: string[]) => { + for (const name of paramNames) { + if (req.query[name] == null) { + throw Error(`Nebyl předán parametr '${name}' v query požadavku`); + } + } +} + +/** + * Ověří přítomnost (not null) předaných parametrů v těle requestu. + * V případě nepřítomnosti kteréhokoli parametru vyhodí chybu. + * + * @param req request + * @param paramNames pole názvů požadovaných parametrů + */ +export const checkBodyParams = (req: any, paramNames: string[]) => { + for (const name of paramNames) { + if (req.body[name] == null) { + throw Error(`Nebyl předán parametr '${name}' v těle požadavku`); + } + } +} + + // TODO umístit do samostatného souboru export class InsufficientPermissions extends Error { } \ No newline at end of file diff --git a/server/src/websocket.ts b/server/src/websocket.ts new file mode 100644 index 0000000..a98e22d --- /dev/null +++ b/server/src/websocket.ts @@ -0,0 +1,28 @@ +import { Server } from "socket.io"; +import { DefaultEventsMap } from "socket.io/dist/typed-events"; + +let io: Server; + +export const initWebsocket = (server: any) => { + io = new Server(server, { + cors: { + origin: "*", + }, + }); + io.on("connection", (socket) => { + console.log(`New client connected: ${socket.id}`); + + socket.on("message", (message) => { + io.emit("message", message); + }); + + socket.on("disconnect", () => { + console.log(`Client disconnected: ${socket.id}`); + }); + }); + return io; +} + +export const getWebsocket = () => { + return io; +} \ No newline at end of file