397 lines
9.6 KiB
TypeScript
397 lines
9.6 KiB
TypeScript
|
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<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/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 moved to shelf");
|
||
|
}
|
||
|
} 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}`);
|
||
|
});
|