const express = require("express"); const Database = require("better-sqlite3"); const winston = require("winston"); const path = require("path"); const fileUpload = require("express-fileupload"); const basicAuth = require("express-basic-auth"); const fs = require("fs"); const app_conf = require("./app/config/app.conf.js"); const db_conf = require("./app/config/db.conf.js"); const app = express(); var db; var shutting_down = false; /* * Static middleware - serve css and js files to public */ app.use(app_conf.web_paths.css, express.static(path.join(__dirname, app_conf.web_paths.css))); app.use(app_conf.web_paths.js, express.static(path.join(__dirname, app_conf.web_paths.js))); app.use(express.json()); app.use(fileUpload({ limits: { fileSize: 102400 }, useTempFiles: true, tempFileDir: path.join(__dirname, app_conf.temp_directory) })); app.use(basicAuth({ users: { [app_conf.username]: app_conf.password }, challenge: true })); /* * Initialize console and file logger */ const logger = winston.createLogger({ levels: winston.config.syslog.levels, level: process.env.LOG_LEVEL || app_conf.logger.level, format: winston.format.combine( winston.format.timestamp({ format: app_conf.timestamp_format, }), winston.format.printf((info) => `[${info.timestamp}][${info.level}]: ${info.message}`) ), transports: [ new winston.transports.File({ filename: path.join(__dirname, app_conf.logger.error_path), level: "error", }), new winston.transports.File({ filename: path.join(__dirname, app_conf.logger.full_path) }), new winston.transports.Console() ], }); app.delete("/app/remove", async (req, res) => { if (req.query["element"]) { try { const remove = db.prepare(`DELETE FROM ${db_conf.table_name} WHERE Name='${req.query["element"]}';`); if (remove.run()["changes"]) { res.status(200).send(); } else { res.status(404).send(); } } catch (error) { res.status(500).send(); logger.error(error.message); } } else { res.status(400).send(); } }); app.post("/app/upload", async (req, res) => { if (!req.files || !req.files.file) { res.status(422).send(); } else { const uploadedFile = req.files.file; db.close(); fs.copyFile(uploadedFile.tempFilePath, db_conf.path, (error) => { if (error) { logger.error(error); } else { try { db = new Database(db_conf.path, { fileMustExist: true }); } catch (error) { logger.error(error); shutting_down = true; shutdown(); } } }); } res.status(201).send(); }); app.get("/app/download", async (req, res) => { res.download(db_conf.path); }); app.post("/app/add", async (req, res) => { if (req.query["element"] && req.body) { try { let keys = Object.keys(req.body); let values = `'${req.query["element"]}',`; for (const k of keys) { if (req.body[k] === "null") { values += `NULL,` } else { values += `'${req.body[k]}',`; } } if (values) { values = values.slice(0, -1); const update = db.prepare(`INSERT INTO ${db_conf.table_name} VALUES (${values});`); update.run(); res.status(200).send(); } else { res.status(400).send(); } } catch (error) { res.status(500).send(); logger.error(error.message); } } else { res.status(400).send(); } }); app.post("/app/edit", async (req, res) => { if (req.query["element"] && req.body) { try { let keys = Object.keys(req.body); let values = ""; for (const k of keys) { if (req.body[k] === "null") { values += `${k}=NULL,` } else { values += `${k}='${req.body[k]}',`; } } if (values) { values = values.slice(0, -1); const update = db.prepare(`UPDATE ${db_conf.table_name} SET ${values} WHERE Name='${req.query["element"]}'`); update.run(); res.status(200).send(); } else { res.status(400).send(); } } catch (error) { res.status(500).send(); logger.error(error.message); } } else { res.status(400).send(); } }); /* * Main endpoint - might implement authentication step here... */ app.get("/", (req, res) => { res.redirect("app/"); }); function errorPageRenderer(res, emoji, message) { res.render("error", { page_title: app_conf.name, css_path: path.join(app_conf.web_paths.css, "stylesheet.css"), error_emoji: emoji, error_message: message }) } app.get("/app/add", async (req, res) => { try { const select_get = db.prepare(`SELECT * FROM ${db_conf.table_name};`); const row = select_get.get(); const sql_header = Object.keys(row); var table_header = `${sql_header[0]}\n`; sql_header.splice(0, 1); if (row) { var column_options = {}; for (const key of sql_header) { table_header += `${key}\n`; const select_distinct = db.prepare(` SELECT DISTINCT ${key} FROM ${db_conf.table_name}; `); values = select_distinct.all(); for (v of values) { if (column_options[key] == null) { column_options[key] = Object.values(v); } else { column_options[key] = column_options[key].concat(Object.values(v)); } } } var table_data = ""; const sql_row_values = Object.values(row); table_data += `\n`; for (let i = 1; i < sql_row_values.length; i++) { table_data += `\n`; } table_data += ""; res.render("add", { page_title: app_conf.name, css_path: path.join(app_conf.web_paths.css, "stylesheet.css"), js_path: path.join(app_conf.web_paths.js, "add.js"), column_options: JSON.stringify(column_options), table_header: table_header, table_data: table_data }); } else { errorPageRenderer(res, "❓", "The database is empty."); } } catch (error) { logger.error(error.message); } }); /* * Edit endpoint - edits a specific row */ app.get("/app/edit", async (req, res) => { if (req.query["element"]) { try { const select_get = db.prepare(`SELECT * FROM ${db_conf.table_name} WHERE Name='${req.query["element"]}';`); const row = select_get.get(); if (row) { const sql_header = Object.keys(row); var table_header = `${sql_header[0]}\n`; sql_header.splice(0, 1); var column_options = {}; for (const key of sql_header) { table_header += `${key}\n`; const select_distinct = db.prepare(` SELECT DISTINCT ${key} FROM ${db_conf.table_name} WHERE ${key} IS NOT NULL AND ${key} NOT IN ( SELECT ${key} FROM ${db_conf.table_name} WHERE Name='${req.query["element"]}' AND ${key} IS NOT NULL); `); values = select_distinct.all(); for (v of values) { if (column_options[key] == null) { column_options[key] = Object.values(v); } else { column_options[key] = column_options[key].concat(Object.values(v)); } } } var table_data = ""; const sql_row_values = Object.values(row); table_data += `${sql_row_values[0]}\n`; sql_row_values.splice(0, 1); for (const v of sql_row_values) { table_data += `\n`; } table_data += ""; res.render("edit", { page_title: app_conf.name, css_path: path.join(app_conf.web_paths.css, "stylesheet.css"), js_path: path.join(app_conf.web_paths.js, "edit.js"), column_options: JSON.stringify(column_options), table_header: table_header, table_data: table_data }); } else { errorPageRenderer(res, "🔍❌", "Not found."); } } catch (error) { logger.error(error.message); } } else { errorPageRenderer(res, "✋⛔", "Bad request."); } }); /* * App endpoint - shows the database */ app.get("/app", async (req, res) => { logger.info(`${req.method}: "${req.url}" => ${req.get("User-Agent")}`); try { const select = db.prepare(`SELECT * FROM ${db_conf.table_name}`); const rows = select.all(); if (rows[0]) { const sql_header = Object.keys(rows[0]); var table_header = ""; for (const key of sql_header) { table_header += `${key}\n`; } var table_data = ""; for (const row of rows) { const sql_row_values = Object.values(row); table_data += ""; for (const v of sql_row_values) { table_data += `${v}\n`; } table_data += ""; } res.render("index", { page_title: app_conf.name, css_path: path.join(app_conf.web_paths.css, "stylesheet.css"), js_path: path.join(app_conf.web_paths.js, "index.js"), table_header: table_header, table_data: table_data }); } else { errorPageRenderer(res, "❓", "The database is empty."); } } catch (error) { errorPageRenderer(res, "🌋💥", "Internal server error. Check logs for more information."); logger.error(error.message); } }) /* * Initialise ExpressJS * Using ejs to pass variables from ExpressJS to HTML pages. */ app.set("view engine", "ejs"); app.set("views", path.join(__dirname, app_conf.views_directory)); const server = app.listen(app_conf.port, () => { logger.info("Starting Database Editor."); logger.info("Connecting to Database..."); try { db = new Database(db_conf.path, { fileMustExist: true }); logger.info("Connected to Database."); } catch (error) { logger.error(error.message); shutdown(); } fs.rm(path.join(__dirname, app_conf.temp_directory, "/."), { recursive: true }, (error) => { if (error == null) { logger.info("Removed temp directory."); } else if (error.code === "ENOENT") { logger.info("Temp directory not found."); } else { logger.error(error.message); } }); logger.info(`Listening to port ${app_conf.port}.`); }) /* * Handle Ctrl+C to shutdown gracefully */ process.on("SIGINT", () => { if (shutting_down) { logger.emerg("Shutting down forcefully."); process.exit(-1); } else { shutdown(); } }); /* * This function shuts the server down */ function shutdown() { shutting_down = true; logger.info("Closing Database connection."); try { db.close(); } catch (error) { logger.error(error.message); } server.close(() => { logger.info("Shutting down server."); }); }