Some checks are pending
ci/woodpecker/push/workflow Pipeline is pending
fix: oprava kopírování changelogů do Docker image fix: oprava kopírování changelogů do Docker image fix: oprava
198 lines
7.2 KiB
TypeScript
198 lines
7.2 KiB
TypeScript
import express from "express";
|
|
import bodyParser from "body-parser";
|
|
import cors from 'cors';
|
|
import { getData, getDateForWeekIndex, getToday } from "./service";
|
|
import dotenv from 'dotenv';
|
|
import path from 'path';
|
|
import { getQr } from "./qr";
|
|
import { generateToken, getLogin, verify } from "./auth";
|
|
import { getIsWeekend, InsufficientPermissions, PizzaDayConflictError, parseToken } from "./utils";
|
|
import { getPendingQrs } from "./pizza";
|
|
import { initWebsocket } from "./websocket";
|
|
import { startReminderScheduler } from "./pushReminder";
|
|
import pizzaDayRoutes from "./routes/pizzaDayRoutes";
|
|
import foodRoutes, { refreshMetoda } from "./routes/foodRoutes";
|
|
import votingRoutes from "./routes/votingRoutes";
|
|
import easterEggRoutes from "./routes/easterEggRoutes";
|
|
import statsRoutes from "./routes/statsRoutes";
|
|
import notificationRoutes from "./routes/notificationRoutes";
|
|
import qrRoutes from "./routes/qrRoutes";
|
|
import devRoutes from "./routes/devRoutes";
|
|
import changelogRoutes from "./routes/changelogRoutes";
|
|
|
|
const ENVIRONMENT = process.env.NODE_ENV ?? 'production';
|
|
dotenv.config({ path: path.resolve(__dirname, `../.env.${ENVIRONMENT}`) });
|
|
|
|
// Validace nastavení JWT tokenu - nemá bez něj smysl vůbec povolit server spustit
|
|
if (!process.env.JWT_SECRET) {
|
|
throw Error("Není vyplněna proměnná prostředí JWT_SECRET");
|
|
}
|
|
|
|
const app = express();
|
|
const server = require("http").createServer(app);
|
|
initWebsocket(server);
|
|
|
|
// Body-parser middleware for parsing JSON
|
|
app.use(bodyParser.json());
|
|
|
|
app.use(cors({
|
|
origin: '*'
|
|
}));
|
|
|
|
// Zapínatelný login přes hlavičky - pokud je zapnutý nepovolí "basicauth"
|
|
const HTTP_REMOTE_USER_ENABLED = process.env.HTTP_REMOTE_USER_ENABLED === 'true' || false;
|
|
const HTTP_REMOTE_USER_HEADER_NAME = process.env.HTTP_REMOTE_USER_HEADER_NAME ?? 'remote-user';
|
|
if (HTTP_REMOTE_USER_ENABLED) {
|
|
if (!process.env.HTTP_REMOTE_TRUSTED_IPS) {
|
|
throw new Error('Je zapnutý login z hlaviček, ale není nastaven rozsah adres ze kterých hlavička může přijít.');
|
|
}
|
|
const HTTP_REMOTE_TRUSTED_IPS = process.env.HTTP_REMOTE_TRUSTED_IPS.split(',').map(ip => ip.trim());
|
|
//TODO: nevim jak udelat console.log pouze pro "debug"
|
|
//console.log("Budu věřit hlavičkám z: " + HTTP_REMOTE_TRUSTED_IPS);
|
|
app.set('trust proxy', HTTP_REMOTE_TRUSTED_IPS);
|
|
console.log('Zapnutý login přes hlavičky z proxy.');
|
|
}
|
|
|
|
|
|
// ----------- Metody nevyžadující token --------------
|
|
|
|
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));
|
|
})
|
|
|
|
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) {
|
|
res.status(200).json(generateToken(Buffer.from(remoteUser, 'latin1').toString(), true));
|
|
} else {
|
|
throw Error("Je zapnuto přihlášení přes hlavičky, ale nepřišla hlavička nebo ??");
|
|
}
|
|
} else {
|
|
// Klasická autentizace loginem
|
|
if (!req.body?.login || req.body.login.trim().length === 0) {
|
|
throw Error("Nebyl předán login");
|
|
}
|
|
// TODO zavést podmínky pro délku loginu (min i max)
|
|
res.status(200).json(generateToken(req.body.login, false));
|
|
}
|
|
});
|
|
|
|
// TODO dočasné řešení - QR se zobrazuje přes <img>, nemáme sem jak dostat token
|
|
app.get("/api/qr", (req, res) => {
|
|
if (!req.query?.login) {
|
|
throw Error("Nebyl předán login");
|
|
}
|
|
const img = getQr(req.query.login as string);
|
|
res.writeHead(200, {
|
|
'Content-Type': 'image/png',
|
|
'Content-Length': img.length
|
|
});
|
|
res.end(img);
|
|
});
|
|
|
|
// ----------------------------------------------------
|
|
|
|
// Přeskočení auth pro refresh dat xd
|
|
app.use("/api/food/refresh", refreshMetoda);
|
|
|
|
/** Middleware ověřující JWT token */
|
|
app.use("/api/", (req, res, next) => {
|
|
if (HTTP_REMOTE_USER_ENABLED) {
|
|
// 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: %s.", remoteName);
|
|
}
|
|
}
|
|
}
|
|
if (!req.headers.authorization) {
|
|
return res.status(401).json({ error: 'Nebyl předán autentizační token' });
|
|
}
|
|
const token = req.headers.authorization.split(' ')[1];
|
|
if (!verify(token)) {
|
|
return res.status(403).json({ error: 'Neplatný autentizační token' });
|
|
}
|
|
next();
|
|
});
|
|
|
|
/** Vrátí data pro aktuální den. */
|
|
app.get("/api/data", async (req, res) => {
|
|
let date = undefined;
|
|
if (req.query.dayIndex != null && typeof req.query.dayIndex === 'string') {
|
|
const index = parseInt(req.query.dayIndex);
|
|
if (!isNaN(index)) {
|
|
date = getDateForWeekIndex(parseInt(req.query.dayIndex));
|
|
}
|
|
} else if (getIsWeekend(getToday())) {
|
|
// Na víkendu zobrazíme pátek místo hlášky "Užívejte víkend"
|
|
date = getDateForWeekIndex(4);
|
|
}
|
|
const data = await getData(date);
|
|
// Připojíme nevyřízené QR kódy pro přihlášeného uživatele
|
|
try {
|
|
const login = getLogin(parseToken(req));
|
|
const pendingQrs = await getPendingQrs(login);
|
|
if (pendingQrs.length > 0) {
|
|
data.pendingQrs = pendingQrs;
|
|
}
|
|
} catch {
|
|
// Token nemusí být validní, ignorujeme
|
|
}
|
|
res.status(200).json(data);
|
|
});
|
|
|
|
// Ostatní routes
|
|
app.use("/api/pizzaDay", pizzaDayRoutes);
|
|
app.use("/api/food", foodRoutes);
|
|
app.use("/api/voting", votingRoutes);
|
|
app.use("/api/easterEggs", easterEggRoutes);
|
|
app.use("/api/stats", statsRoutes);
|
|
app.use("/api/notifications", notificationRoutes);
|
|
app.use("/api/qr", qrRoutes);
|
|
app.use("/api/dev", devRoutes);
|
|
app.use("/api/changelogs", changelogRoutes);
|
|
|
|
app.use('/stats', express.static('public'));
|
|
app.use(express.static('public'));
|
|
|
|
// 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 if (err instanceof PizzaDayConflictError) {
|
|
res.status(409).send({ error: err.message })
|
|
} else {
|
|
res.status(500).send({ error: err.message })
|
|
}
|
|
next();
|
|
});
|
|
|
|
const PORT = process.env.PORT ?? 3001;
|
|
const HOST = process.env.HOST ?? '0.0.0.0';
|
|
|
|
server.listen(PORT, () => {
|
|
console.log(`Server listening on ${HOST}, port ${PORT}`);
|
|
startReminderScheduler();
|
|
});
|
|
|
|
// Umožníme vypnutí serveru přes SIGINT, jinak Docker čeká než ho sestřelí
|
|
process.on('SIGINT', function () {
|
|
console.log("\nSIGINT (Ctrl-C), vypínám server");
|
|
process.exit(0);
|
|
}); |