Initial commit

This commit is contained in:
2025-01-14 23:15:37 +01:00
commit fa06c402e6
31 changed files with 25972 additions and 0 deletions

33
src/handlers/config.ts Normal file
View File

@ -0,0 +1,33 @@
import ConfigManager from '../libs/configManager.js';
import __path from "./path.js";
import _ from 'lodash';
// Create a new config instance.
const config = new ConfigManager(__path + '/config.json', true, {
db_connection_string: 'mysql://USER:PASSWORD@HOST:3306/DATABASE',
http_listen_address: '0.0.0.0',
http_port: 3000,
debug: true,
auth: {
cookie_secret: 'gen',
cookie_secure: true,
local: {
active: true,
users: {}
},
oidc: {
active: false
}
}
});
// If no local User exists, create the default with a generated password
if (_.isEqual(config.global.auth.local.users, {})) {
config.global.auth.local.users = {
'administrator': 'gen',
};
config.save_config();
}
export default config;

12
src/handlers/db.ts Normal file
View File

@ -0,0 +1,12 @@
import { PrismaClient } from '@prisma/client'; // Database
import config from "./config.js";
// TODO: Add errorhandling with some sort of message.
export const prisma = new PrismaClient({
datasources: {
db: {
url: config.global.db_connection_string
}
}
});

34
src/handlers/log.ts Normal file
View File

@ -0,0 +1,34 @@
import { Logger } from "tslog";
const loggerConfig: any = {
type: "pretty", // pretty, json, hidden
name: "Core",
hideLogPositionForProduction: true,
prettyLogTemplate: "{{dateIsoStr}} {{logLevelName}} {{nameWithDelimiterPrefix}} "
}
const coreLogger = new Logger(loggerConfig);
export const log = {
core: coreLogger,
db: coreLogger.getSubLogger({ name: "DB" }),
web: coreLogger.getSubLogger({ name: "WEB" }),
auth: coreLogger.getSubLogger({ name: "AUTH" }),
helper: coreLogger.getSubLogger({ name: "HELPER" }),
};
// log.core.silly("Hello from core");
// log.core.trace("Hello from core");
// log.core.debug("Hello from core");
// log.core.info("Hello from core");
// log.core.warn("Hello from core");
// log.core.error("Hello from core");
// log.db.silly("Hello from db");
// log.db.trace("Hello from db");
// log.web.debug("Hello from db");
// log.auth.info("Hello from db");
// log.helper.warn("Hello from db");
// log.db.error("Hello from db");
// log.core.fatal(new Error("I am a pretty Error with a stacktrace."));
export default log;

4
src/handlers/path.ts Normal file
View File

@ -0,0 +1,4 @@
// Return the app directory as an absolute path
const __path = process.argv[1];
export default __path;

84
src/index.ts Normal file
View File

@ -0,0 +1,84 @@
// MARK: Imports
import path from 'node:path';
import __path from "./handlers/path.js";
import log from "./handlers/log.js";
import config from './handlers/config.js';
// Express & more
import express from 'express';
import cors from 'cors'
import session from 'express-session';
import fileUpload from 'express-fileupload';
import bodyParser, { Options } from 'body-parser';
import { Eta } from "eta";
import passport from 'passport';
import routes from './routes/index.js';
log.core.trace("Running from path: " + __path);
// MARK: Express
const app = express();
// TODO: Version check need to be rewritten.
//app.locals.versionRevLong = require('child_process').execSync('git rev-parse HEAD').toString().trim();
//app.locals.versionRev = require('child_process').execSync('git rev-parse --short HEAD').toString().trim();
//app.locals.versionRevLatest = require('child_process').execSync('git ls-remote --refs -q').toString().trim().split('\t')[0];
app.locals.versionRev = '0';
app.locals.versionRevLong = '0';
app.locals.versionRevLatest = '0';
if (app.locals.versionRevLong === app.locals.versionRevLatest) {
log.core.info(`Running Latest Version (${app.locals.versionRevLong})`);
} else {
log.core.info(`Running Version: ${app.locals.versionRevLong} (Latest: ${app.locals.versionRevLatest})`);
}
// ETA Init
const eta = new Eta({ views: path.join(__path, "views") })
app.engine("eta", buildEtaEngine())
app.set("view engine", "eta")
// MARK: Express Middleware & Config
app.set('x-powered-by', false);
app.use(fileUpload());
//app.use(cors());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
// Session store
// TODO: Move secret to config -> Autogenerate.
app.use(
session({
secret: config.global.auth.cookie_secret,
resave: false,
saveUninitialized: false,
cookie: { secure: config.global.auth.cookie_secure }
})
);
app.use(passport.authenticate('session'));
app.use(routes);
app.listen(config.global.http_port, config.global.http_listen_address, () => {
log.web.info(`Listening at http://${config.global.http_listen_address}:${config.global.http_port}`);
});
// MARK: Helper Functions
function buildEtaEngine() {
return (path:string, opts:Options, callback: CallableFunction) => {
try {
const fileContent = eta.readFile(path);
const renderedTemplate = eta.renderString(fileContent, opts);
callback(null, renderedTemplate);
} catch (error) {
callback(error);
}
};
}

140
src/libs/configManager.ts Normal file
View File

@ -0,0 +1,140 @@
import fs from 'node:fs';
import _ from 'lodash';
import { randomUUID, randomBytes } from 'crypto';
export type configObject = Record<any, any>;
/**
* This class is responsible to save/edit config files.
*
* @export
* @class config
* @typedef {config}
*/
export default class config {
#configPath: string;
//global = {[key: string] : string}
global: configObject;
replaceSecrets: boolean;
/**
* Creates an instance of config.
*
* @constructor
* @param {string} configPath Path to config file.
* @param {object} configPreset Default config object with default values.
*/
constructor(configPath: string, replaceSecrets: boolean, configPreset: object) {
this.#configPath = configPath;
this.global = configPreset;
this.replaceSecrets = replaceSecrets;
try {
// Read config
const data = fs.readFileSync(this.#configPath, 'utf8');
// Extend config with missing parameters from configPreset.
this.global = _.defaultsDeep(JSON.parse(data), this.global);
// Save config.
this.save_config();
} catch (err: any) {
// If file does not exist, create it.
if (err.code === 'ENOENT') {
console.log(`Config file does not exist. Creating it at ${this.#configPath} now.`);
this.save_config();
return;
}
console.error(`Could not read config file at ${this.#configPath} due to: ${err}`);
// Exit process.
process.exit(1);
}
}
/**
* Saves the jsonified config object to the config file.
*/
save_config() {
try {
// If enabled replace tokens defines as "gen" with random token
if (this.replaceSecrets) {
// Replace tokens with value "gen"
this.generate_secrets(this.global, 'gen')
}
fs.writeFileSync(this.#configPath, JSON.stringify(this.global, null, 8));
} catch (err) {
console.error(`Could not write config file at ${this.#configPath} due to: ${err}`);
return;
}
console.log(`Successfully written config file to ${this.#configPath}`);
}
/**
* Replaces each item matching the value of placeholder with a random UUID.
* Thanks to https://stackoverflow.com/questions/8085004/iterate-through-nested-javascript-objects
* @param {configObject} obj
*/
generate_secrets(obj: configObject, placeholder: string) {
const stack = [obj];
while (stack?.length > 0) {
const currentObj:any = stack.pop();
Object.keys(currentObj).forEach((key) => {
if (currentObj[key] === placeholder) {
console.log('Generating secret: ' + key);
currentObj[key] = randomBytes(48).toString('base64').replace(/\W/g, '');
}
if (typeof currentObj[key] === 'object' && currentObj[key] !== null) {
stack.push(currentObj[key]);
}
});
}
}
}
/*
**** Example ****
import ConfigHandlerNG from './assets/configHandlerNG.js';
// Create a new config instance.
export const config = new ConfigHandler(__path + '/config.json', true, {
test1: 't1',
test2: 't2',
test3: 'gen',
test4: 't4',
test5: 'gen',
testObj: {
local: {
active: true,
users: {
user1: 'gen',
user2: 'gen',
user3: 'gen',
user4: 'gen',
}
},
oidc: {
active: false
}
}
});
console.log('Base Config:');
console.log(config.global);
console.log('Add some new key to config and call save_config().');
config.global.NewKey = 'ThisIsANewKey!'
config.save_config()
console.log('This will add a new key with value gen, but gen gets replaced with a random UUID when save_config() is called.');
config.global.someSecret = 'gen'
config.save_config() // global.someSecret is getting replaced with some random UUID since it was set to 'gen'.
console.log('Complete Config:');
console.log(config.global);
*/

View File

@ -0,0 +1,25 @@
export function checkAuthentication(req: any, res: any, next: Function) {
next(); // FIXME: Auth bypass!!!
return; // FIXME: Auth bypass!!!
if (req.isAuthenticated()) {
//req.isAuthenticated() will return true if user is logged in
next();
} else {
res.redirect('/auth/login');
}
}
// const checkIsInRole = (...roles) => (req, res, next) => {
// if (!req.user) {
// return res.redirect('/login')
// }
// const hasRole = roles.find(role => req.user.role === role)
// if (!hasRole) {
// return res.redirect('/login')
// }
// return next()
// }

11
src/routes/api/index.ts Normal file
View File

@ -0,0 +1,11 @@
import express from 'express';
// Route imports
import v1_routes from './v1/index.js';
// Router base is '/api'
const Router = express.Router({ strict: false });
Router.use('/v1', v1_routes);
export default Router;

View File

@ -0,0 +1,40 @@
import express from 'express';
import passport from 'passport';
// Route imports
import testRoute from './test.js';
//import itemRoute from './items.js';
//import categoryRoute from './categories.js';
//import storageUnitRoute from './storageUnits.js';
//import storageLocationRoute from './storageLocations.js';
//import contactInfo from './contactInfo.js';
//import versionRoute from './version.js'
//import search_routes from './search/index.js';
// Router base is '/api/v1'
const Router = express.Router({ strict: false });
// All empty strings are null values.
Router.use('*', function (req, res, next) {
for (let key in req.body) {
if (req.body[key] === '') {
req.body[key] = null;
}
}
next();
});
//Router.route('/items').get(itemRoute.get).post(itemRoute.post).patch(itemRoute.patch).delete(itemRoute.del);
//Router.route('/categories').get(categoryRoute.get).post(categoryRoute.post).patch(categoryRoute.patch).delete(categoryRoute.del);
// TODO: Migrate routes to lowercase.
//Router.route('/storageUnits').get(storageUnitRoute.get).post(storageUnitRoute.post).patch(storageUnitRoute.patch).delete(storageUnitRoute.del);
//Router.route('/storageLocations').get(storageLocationRoute.get).post(storageLocationRoute.post).patch(storageLocationRoute.patch).delete(storageLocationRoute.del);
//Router.route('/contactInfo').get(contactInfo.get).post(contactInfo.post).patch(contactInfo.patch).delete(contactInfo.del);
//Router.route('/version').get(versionRoute.get);
//Router.use('/search', search_routes);
Router.route('/test').get(testRoute.get);
export default Router;

View File

@ -0,0 +1,7 @@
import express, { Request, Response } from 'express';
function get(req: Request, res: Response) {
res.status(200).send('API v1 Test successful!');
};
export default { get };

View File

@ -0,0 +1,7 @@
import express, { Request, Response } from 'express';
function get(req: Request, res: Response) {
res.render("index", { message: "Hello world from eta!" })
}
export default { get };

View File

@ -0,0 +1,24 @@
import express from 'express';
// Route imports
// import skuRoute from './:id.js';
// import skuRouteDash from './itemInfo.js'
// import testRoute from './test.js';
import dashboardRoute from './dashboard.js';
// import itemsRoute from './items.js';
// import manage_routes from './manage/index.js';
// Router base is '/'
const Router = express.Router({ strict: false });
// Router.route('/test').get(testRoute.get);
// Router.route('/items').get(itemsRoute.get);
// Router.route('/:id(\\w{8})').get(skuRoute.get);
// Router.route('/s/:id').get(skuRouteDash.get);
// Router.use('/manage', manage_routes);
Router.route('/').get(dashboardRoute.get);
export default Router;

37
src/routes/index.ts Normal file
View File

@ -0,0 +1,37 @@
import express from 'express';
import path from 'node:path';
import __path from "../handlers/path.js";
import log from "../handlers/log.js";
// Middleware Imports
import { checkAuthentication } from '../middlewares/auth.mw.js'
// Route imports
import frontend_routes from './frontend/index.js';
import api_routes from './api/index.js';
//import auth_routes from './auth/index.js';
const Router = express.Router({ strict: false });
// static / libs routes
Router.use('/static', express.static(__path + '/static'));
Router.use('/libs/bulma', express.static(path.join(__path, 'node_modules', 'bulma', 'css'))); // http://192.168.221.10:3000/libs/bulma/bulma.css
Router.use('/libs/jquery', express.static(path.join(__path, 'node_modules', 'jquery', 'dist')));
// Other routers
Router.use('/api', checkAuthentication, api_routes);
//Router.use('/auth', auth_routes);
Router.use('/', checkAuthentication, frontend_routes);
// Default route.
Router.all('*', function (req, res) {
// TODO: Respond based on content-type (with req.is('application/json'))
if (req.is('application/json')) {
res.status(404).json({ errorcode: 'NOT_FOUND', error: 'Not Found!' });
} else {
res.status(404).render('errors/404', { url: req.originalUrl });
}
});
export default Router;