diff --git a/app/database/sqlite.database.js b/app/database/sqlite.database.js new file mode 100644 index 0000000..c5443da --- /dev/null +++ b/app/database/sqlite.database.js @@ -0,0 +1,15 @@ +const Database = require("better-sqlite3"); +const logger = require("../utils/logger.util"); +const db_conf = require("../config/db.conf"); + +var db; + +try { + logger.info("Connecting to Database..."); + db = new Database(db_conf.path, { fileMustExist: true }); + logger.info("Connected to Database."); +} catch (error) { + logger.error(error.message); +} + +module.exports = db; \ No newline at end of file diff --git a/app/routes/private_routes.js b/app/routes/private_routes.js new file mode 100644 index 0000000..c92eff9 --- /dev/null +++ b/app/routes/private_routes.js @@ -0,0 +1,292 @@ +const db = require("../database/sqlite.database.js"); +const logger = require("../utils/logger.util.js"); +const Database = require("better-sqlite3"); +const path = require("path"); +const fs = require("fs"); +const app_conf = require("../config/app.conf.js"); +const db_conf = require("../config/db.conf.js"); + +const express = require('express'); +const router = express.Router(); + +function errorPageRenderer(res, emoji, message) { + res.render("error", { + page_title: app_conf.name, + css_path: path.join(app_conf.paths.css.web, "stylesheet.css"), + error_emoji: emoji, + error_message: message + }) +} + +router.delete("/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(); + } +}); + +router.post("/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(); +}); + +router.get("/download", async (req, res) => { + res.download(db_conf.path); +}); + +router.post("/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(); + } +}); + +router.post("/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(); + } +}); + +router.get("/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.paths.css.web, "stylesheet.css"), + js_path: path.join(app_conf.paths.js.web, "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); + } +}); + +router.get("/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.paths.css.web, "stylesheet.css"), + js_path: path.join(app_conf.paths.js.web, "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."); + } +}); + +router.get("", 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.paths.css.web, "stylesheet.css"), + js_path: path.join(app_conf.paths.js.web, "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); + } +}); + +module.exports = router; \ No newline at end of file diff --git a/app/routes/public_routes.js b/app/routes/public_routes.js new file mode 100644 index 0000000..4bd8cbc --- /dev/null +++ b/app/routes/public_routes.js @@ -0,0 +1,22 @@ +const path = require("path"); +const app_conf = require("../config/app.conf.js"); + +const express = require('express'); +const router = express.Router(); + +router.use(app_conf.paths.css.web, express.static(path.join(path.dirname(require.main.filename), app_conf.paths.css.src))); +router.use(app_conf.paths.js.web, express.static(path.join(path.dirname(require.main.filename), app_conf.paths.js.src))); + +/* + * Main endpoint - might implement authentication step here... + */ +router.get("/", (req, res) => { + res.render("auth", { + page_title: app_conf.name, + css_path: path.join(app_conf.paths.css.web, "stylesheet.css"), + protocol: app_conf.protocol, + host: app_conf.host + }); +}); + +module.exports = router; \ No newline at end of file diff --git a/app/samples/app.conf.sample b/app/samples/app.conf.sample deleted file mode 100644 index 4cb0088..0000000 --- a/app/samples/app.conf.sample +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - name: "CRTelemetry", - port: 80, - username: "admin", - password: "password", - logger: { - level: "info", - error_path: "/app/logs/error.log", - full_path: "/app/logs/combined.log" - }, - timestamp_format: "HH:mm:ss DD-MM-YYYY", - views_directory: "/app/views/", - temp_directory: "/app/temp/", - web_paths: { - css: "/app/src/css", - js: "/app/src/javascript" - } -}; \ No newline at end of file diff --git a/app/utils/logger.util.js b/app/utils/logger.util.js new file mode 100644 index 0000000..e828584 --- /dev/null +++ b/app/utils/logger.util.js @@ -0,0 +1,24 @@ +const winston = require("winston"); +const app_conf = require("../config/app.conf"); +const path = require("path"); + +/* + * 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(path.dirname(require.main.filename), app_conf.logger.error_path), level: "error", }), + new winston.transports.File({ filename: path.join(path.dirname(require.main.filename), app_conf.logger.full_path) }), + new winston.transports.Console() + ], +}); + +module.exports = logger; \ No newline at end of file diff --git a/app/views/auth.ejs b/app/views/auth.ejs new file mode 100644 index 0000000..bbd239e --- /dev/null +++ b/app/views/auth.ejs @@ -0,0 +1,31 @@ + + + + + + <%= page_title %> + + + + + + + + + +
+ +
+
+
+ + Enter +
+
+ + + \ No newline at end of file diff --git a/samples/app.conf.sample b/samples/app.conf.sample new file mode 100644 index 0000000..a5d999e --- /dev/null +++ b/samples/app.conf.sample @@ -0,0 +1,27 @@ +module.exports = { + name: "CRTelemetry", + host: "localhost", + protocol: "http", + port: 80, + username: "admin", + password: "password", + token_ttl: 1800, + logger: { + level: "info", + error_path: "/app/logs/error.log", + full_path: "/app/logs/combined.log" + }, + timestamp_format: "HH:mm:ss DD-MM-YYYY", + views_directory: "/app/views", + temp_directory: "/app/temp", + paths: { + css: { + web: "/css", + src: "/src/css" + }, + js: { + web: "/javascript", + src: "/src/javascript" + } + } +}; \ No newline at end of file diff --git a/app/samples/db.conf.sample b/samples/db.conf.sample similarity index 100% rename from app/samples/db.conf.sample rename to samples/db.conf.sample diff --git a/server.js b/server.js index 222ab54..bb94ecc 100644 --- a/server.js +++ b/server.js @@ -1,346 +1,37 @@ 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 public_routes = require('./app/routes/public_routes'); +const private_routes = require('./app/routes/private_routes'); + +const logger = require("./app/utils/logger.util.js"); +const db = require("./app/database/sqlite.database.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({ + +app.use("/", public_routes); + +app.use("/app", 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); - } -}) +}), private_routes); /* * Initialise ExpressJS @@ -350,14 +41,7 @@ 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."); diff --git a/app/src/css/stylesheet.css b/src/css/stylesheet.css similarity index 100% rename from app/src/css/stylesheet.css rename to src/css/stylesheet.css diff --git a/app/src/javascript/add.js b/src/javascript/add.js similarity index 100% rename from app/src/javascript/add.js rename to src/javascript/add.js diff --git a/app/src/javascript/edit.js b/src/javascript/edit.js similarity index 100% rename from app/src/javascript/edit.js rename to src/javascript/edit.js diff --git a/app/src/javascript/index.js b/src/javascript/index.js similarity index 94% rename from app/src/javascript/index.js rename to src/javascript/index.js index 9b8e368..889e85e 100644 --- a/app/src/javascript/index.js +++ b/src/javascript/index.js @@ -49,7 +49,7 @@ setEventListeners(download, () => { }); setEventListeners(logout, () => { - window.location.href = `${window.location.protocol}//log:out@${window.location.host}`; + window.location.href = `${window.location.protocol}//logout@${window.location.host}/`; }); for (let i = 1; i < sql_rows.length; i++) {