n39librarian/middleware/index.ts

423 lines
10 KiB
TypeScript
Raw Permalink Normal View History

import express, { Request, Response, Application } from "express";
import dotenv from "dotenv";
import { createPool } from "mariadb";
import { Book, MoveShelfAction } 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";
import { moveShelf } from "./queries/moveShelf";
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<any, any, any, any>,
res: Response,
next: () => Promise<void>
) => {
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<undefined, undefined, { password: string }>,
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<Book, "isbn" | "title" | "shelf" | "published">
>,
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<undefined, undefined, Pick<Book, "uuid">>,
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/shelf/move",
async (
req: Request<undefined, undefined, MoveShelfAction>,
res: Response
) => {
await isAuthenticated(req, res, async () => {
const { target } = req.body;
try {
const containsThread = checkForThreads([target], res);
if (containsThread) {
return;
}
const conn = await pool.getConnection();
await conn.query(moveShelf(req.body));
await conn.end();
res.status(200).send(`Books moved to shelf ${target}.`);
} catch (error) {
res.status(500).send("Internal Server Error: " + error);
}
});
}
);
app.post(
"/api/books/edit",
async (
req: Request<
undefined,
undefined,
Pick<Book, "uuid" | "isbn" | "title" | "published" | "shelf">
>,
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 edited.");
}
} catch (error) {
res.status(500).send("Internal Server Error: " + error);
}
});
}
);
app.post(
"/api/books/checkout",
async (
req: Request<
undefined,
undefined,
Pick<Book, "uuid" | "checkoutBy" | "contact">
>,
res: Response<string>
) => {
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<Partial<Book>, "isbn" | "title" | "uuid">
>,
res: Response<Book[] | string>
) => {
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<undefined, undefined, null>,
res: Response<Book[] | string>
) => {
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<Book, "uuid" | "checkoutBy" | "contact">
>,
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<Book[]>(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}`);
});