Refaktor, rozdělení api, zpřehlednění kódu

This commit is contained in:
2023-10-03 22:52:09 +02:00
parent 829197e17a
commit 74893c38eb
15 changed files with 473 additions and 391 deletions

View File

@@ -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';

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 { }

28
server/src/websocket.ts Normal file
View File

@@ -0,0 +1,28 @@
import { Server } from "socket.io";
import { DefaultEventsMap } from "socket.io/dist/typed-events";
let io: Server<DefaultEventsMap, DefaultEventsMap, DefaultEventsMap, any>;
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;
}