import express, { Request, Response, Application } from "express"; import dotenv from "dotenv"; import { createPool } from "mariadb"; import { Book } from "./types/Book"; import { checkoutBook, findBook, getBook, listBooks, createBook, deleteBook, editBook, returnBook, } from "./queries"; import { v4 as uuidv4 } from "uuid"; import path from "path"; import session from "express-session"; import { censorBookData } from "./utils/censorBookData"; import * as fs from "fs"; import { checkForThreads } from "./utils/passesSQLInjectionCheck"; dotenv.config(); const app: Application = express(); const port = process.env.PORT || 8000; let adminKey = process.env.ADMIN_KEY; const pool = createPool({ host: process.env.DB_HOST ?? "localhost", port: process.env.DB_PORT ? Number(process.env.DB_PORT) : 3306, user: process.env.ADMIN_DB_USER ?? "admin", password: process.env.ADMIN_DB_PW ?? "AdminPassword", database: "librarian", connectionLimit: process.env.DB_CONNECTION_LIMIT ? Number(process.env.DB_CONNECTION_LIMIT) : 10, }); app.use(express.json()); app.use( session({ secret: adminKey ?? "superSecretKey", resave: false, saveUninitialized: true, }) ); const isAuthenticated = async ( req: Request, res: Response, next: () => Promise ) => { if (!req.session.authenticated) { res.status(403).send("unauthorized"); } else { await next(); } }; const stringifyResponse = (obj: unknown) => JSON.stringify(obj, (_, v) => (typeof v === "bigint" ? v.toString() : v)); app.use(express.static(path.join(__dirname, "frontend"))); app.get("/", function (req, res) { res.sendFile(path.join(__dirname, "frontend", "index.html")); }); app.get("/books", function (req, res) { res.sendFile(path.join(__dirname, "frontend", "index.html")); }); app.get("/books/*", function (req, res) { res.sendFile(path.join(__dirname, "frontend", "index.html")); }); app.post( "/api/authenticate", async ( req: Request, res: Response ) => { try { if (req.body.password === adminKey || req.session.authenticated) { req.session.authenticated = true; res.status(200).send("ok"); } else { res.status(403).send("unauthorized"); } } catch (error) { res.status(500).send("Internal Server Error: " + error); } } ); app.post( "/api/updateAdminKey", async ( req: Request< undefined, undefined, { oldPassword: string; newPassword: string } >, res: Response ) => { try { if (req.body.oldPassword === adminKey) { if (req.body.newPassword.length < 13) { res .status(400) .send("Admin key must contain at least 13 charakters."); return; } fs.readFile("./.env", "utf8", (err, file) => { if (err) return console.log(err); const replacedPw = file.replace( /ADMIN_KEY=.*/g, `ADMIN_KEY=${req.body.newPassword}` ); fs.writeFile("./.env", replacedPw, "utf8", function (err) { if (err) return console.log(err); }); }); adminKey = req.body.newPassword; res.status(200).send("ok"); } else { res.status(403).send("unauthorized"); } } catch (error) { res.status(500).send("Internal Server Error: " + error); } } ); app.post( "/api/books/create", async ( req: Request< undefined, undefined, Pick >, res: Response ) => { isAuthenticated(req, res, async () => { const { title, isbn, shelf, published } = req.body; try { const containsThread = checkForThreads( [title, isbn, shelf, published], res ); if (containsThread) { return; } const conn = await pool.getConnection(); const uuid = uuidv4(); await conn.query(createBook({ ...req.body, uuid })); const createdBook = await conn.query(getBook({ uuid })); await conn.end(); res.status(200).send(stringifyResponse(createdBook)); } catch (error) { res.status(500).send("Internal Server Error: " + error); } }); } ); app.post( "/api/books/delete", async ( req: Request>, res: Response ) => { await isAuthenticated(req, res, async () => { try { const containsThread = checkForThreads([req.body.uuid], res); if (containsThread) { return; } const conn = await pool.getConnection(); const foundBooks = await conn.query(getBook(req.body)); if (!foundBooks.length) { await conn.end(); res.status(404).send("Book not found"); } else { await conn.query(deleteBook(req.body)); await conn.end(); res.status(200).send("Book deleted"); } } catch (error) { res.status(500).send("Internal Server Error: " + error); } }); } ); app.post( "/api/books/edit", async ( req: Request< undefined, undefined, Pick >, res: Response ) => { await isAuthenticated(req, res, async () => { const { uuid, title, isbn, shelf, published } = req.body; try { const containsThread = checkForThreads( [uuid, title, isbn, shelf, published], res ); if (containsThread) { return; } const conn = await pool.getConnection(); const foundBooks = await conn.query(getBook(req.body)); if (!foundBooks.length) { await conn.end(); res.status(404).send("Book not found"); } else { await conn.query(editBook(req.body)); await conn.end(); res.status(200).send("Book moved to shelf"); } } catch (error) { res.status(500).send("Internal Server Error: " + error); } }); } ); app.post( "/api/books/checkout", async ( req: Request< undefined, undefined, Pick >, res: Response ) => { const { uuid, checkoutBy, contact } = req.body; try { const containsThread = checkForThreads([uuid, checkoutBy, contact], res); if (containsThread) { return; } const conn = await pool.getConnection(); const foundBooks = await conn.query(getBook(req.body)); if (!foundBooks.length) { await conn.end(); res.status(404).send(foundBooks); } else { await conn.query( checkoutBook({ ...req.body, lastCheckoutDate: Date.now() }) ); await conn.end(); res.status(200).send("Book checked out"); } } catch (error) { res.status(500).send("Internal Server Error: " + error); } } ); app.post( "/api/books/find", async ( req: Request< undefined, undefined, Pick, "isbn" | "title" | "uuid"> >, res: Response ) => { const { uuid, title, isbn } = req.body; try { const containsThread = checkForThreads([uuid, title, isbn], res); if (containsThread) { return; } if (!isbn && !title && !uuid) { res.status(400).send("No isbn or title provided"); return; } const conn = await pool.getConnection(); const foundBooks = await conn.query(findBook(req.body)); await conn.end(); if (!foundBooks.length) { res.status(404).send("Book not found"); return; } res .status(200) .send( stringifyResponse( censorBookData(foundBooks, !!req.session.authenticated) ) ); } catch (error) { res.status(500).send("Internal Server Error: " + error); } } ); app.get( "/api/books/list", async ( req: Request, res: Response ) => { try { const conn = await pool.getConnection(); const foundBooks = await conn.query(listBooks); await conn.end(); res .status(200) .send( stringifyResponse( censorBookData(foundBooks, !!req.session.authenticated) ) ); } catch (error) { res.status(500).send("Internal Server Error: " + error); } } ); app.post( "/api/books/return", async ( req: Request< undefined, undefined, Pick >, res: Response ) => { const { uuid, checkoutBy, contact } = req.body; try { const containsThread = checkForThreads([uuid, checkoutBy, contact], res); if (containsThread) { return; } const conn = await pool.getConnection(); const foundBooks = await conn.query(getBook(req.body)); if (!foundBooks.length) { await conn.end(); res.status(404).send("Book not found"); return; } if ( !req.session.authenticated && !( foundBooks[0].checkoutBy === req.body.checkoutBy && foundBooks[0].contact === req.body.contact ) ) { res.status(403).send("unauthorized"); return; } await conn.query(returnBook(req.body)); await conn.end(); res.status(200).send("Book returned"); } catch (error) { res.status(500).send("Internal Server Error: " + error); } } ); app.listen(port, async () => { console.log(`Server listening on port ${port}`); });