Initial commit
This commit is contained in:
		
							
								
								
									
										18
									
								
								src/handlers/config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/handlers/config.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
import ConfigManager from '../libs/configManager.js';
 | 
			
		||||
import __path from './path.js';
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import log from './log.js';
 | 
			
		||||
 | 
			
		||||
// 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,
 | 
			
		||||
	http_domain: 'example.org',
 | 
			
		||||
	http_enable_hsts: false,
 | 
			
		||||
	devmode: true
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
!config.global.devmode && log.core.error('devmode active! Do NOT use this in prod!');
 | 
			
		||||
 | 
			
		||||
export default config;
 | 
			
		||||
							
								
								
									
										40
									
								
								src/handlers/db.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/handlers/db.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
import { PrismaClient, Prisma } from '@prisma/client'; // Database
 | 
			
		||||
import { Response } from 'express';
 | 
			
		||||
import config from './config.js';
 | 
			
		||||
import log from './log.js';
 | 
			
		||||
 | 
			
		||||
// TODO: Add errorhandling with some sort of message.
 | 
			
		||||
const prisma = new PrismaClient({
 | 
			
		||||
	datasources: {
 | 
			
		||||
		db: {
 | 
			
		||||
			url: config.global.db_connection_string
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// FIXME: any
 | 
			
		||||
export function handlePrismaError(errorObj: any, res: Response, source: string) {
 | 
			
		||||
	log.db.error(source, errorObj);
 | 
			
		||||
	if (errorObj instanceof Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
		switch (errorObj.code) {
 | 
			
		||||
 | 
			
		||||
			// P2002 -> "Unique constraint failed on the {constraint}"
 | 
			
		||||
			case 'P2002':
 | 
			
		||||
				res.status(409).json({ status: 'ERROR', errorcode: 'DB_ERROR', message: 'The object needs to be unique', meta: errorObj.meta });
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			// P2003 -> "Foreign key constraint failed on the field: {field_name}"
 | 
			
		||||
			case 'P2003':
 | 
			
		||||
				res.status(404).json({ status: 'ERROR', errorcode: 'DB_ERROR', message: 'Relation object does not exist', meta: errorObj.meta });
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			default:
 | 
			
		||||
				res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', message: 'An error occurred during the database operation' });
 | 
			
		||||
				break;
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		res.status(500).json({ status: 'ERROR', errorcode: 'DB_ERROR', message: 'If you can read this something went terribly wrong!' });
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default prisma;
 | 
			
		||||
							
								
								
									
										52
									
								
								src/handlers/log.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/handlers/log.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
import { Logger,ISettingsParam } from "tslog";
 | 
			
		||||
 | 
			
		||||
function loggerConfig(name: string): ISettingsParam<unknown> {
 | 
			
		||||
	return {
 | 
			
		||||
		type: "pretty", // pretty, json, hidden
 | 
			
		||||
		name: name,
 | 
			
		||||
		hideLogPositionForProduction: true,
 | 
			
		||||
		prettyLogTemplate: "{{dateIsoStr}} {{logLevelName}} {{nameWithDelimiterPrefix}} "
 | 
			
		||||
	
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type log = {
 | 
			
		||||
	core: Logger<unknown>
 | 
			
		||||
	db: Logger<unknown>
 | 
			
		||||
	web: Logger<unknown>
 | 
			
		||||
	S3: Logger<unknown>
 | 
			
		||||
	auth: Logger<unknown>
 | 
			
		||||
	api?: Logger<unknown>
 | 
			
		||||
	frontend?: Logger<unknown>
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// FIXME: any type
 | 
			
		||||
let log: log = {
 | 
			
		||||
	core: new Logger(loggerConfig("Core")),
 | 
			
		||||
	db: new Logger(loggerConfig("DB")),
 | 
			
		||||
	web: new Logger(loggerConfig("Web")),
 | 
			
		||||
	S3: new Logger(loggerConfig("S3")),
 | 
			
		||||
	auth: new Logger(loggerConfig("Auth")),
 | 
			
		||||
//	helper: new Logger(loggerConfig("HELPER")),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
log["api"] = log.web.getSubLogger({ name: "API" });
 | 
			
		||||
log["frontend"] = log.web.getSubLogger({ name: "Frontend" });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// log.core.silly("Hello from core");
 | 
			
		||||
//log.api.trace("Hello from api");
 | 
			
		||||
//log.frontend.trace("Hello from frontend");
 | 
			
		||||
// 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
									
								
							
							
						
						
									
										4
									
								
								src/handlers/path.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
// Return the app directory as an absolute path
 | 
			
		||||
const __path = process.argv[1];
 | 
			
		||||
 | 
			
		||||
export default __path;
 | 
			
		||||
							
								
								
									
										22
									
								
								src/helpers/prisma_helpers.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/helpers/prisma_helpers.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
/**
 | 
			
		||||
 * A function to create a sortBy compatible object from a string
 | 
			
		||||
 *
 | 
			
		||||
 * @export
 | 
			
		||||
 * @param {string} SortField
 | 
			
		||||
 * @param {string} Order
 | 
			
		||||
 * @returns {object}
 | 
			
		||||
 */
 | 
			
		||||
export function parseDynamicSortBy(SortField: string, Order: string) {
 | 
			
		||||
	return JSON.parse(`{ "${SortField}": "${Order}" }`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Function to parse a string into a number or return undefined if it is not a number
 | 
			
		||||
 * Deprecated since all empty strings in bodys are now undefined. This happens in api/v1 router
 | 
			
		||||
 * @export
 | 
			
		||||
 * @param {string || any} data
 | 
			
		||||
 * @returns {object}
 | 
			
		||||
 */
 | 
			
		||||
export function parseIntOrUndefined(data: any) {
 | 
			
		||||
	return isNaN(parseInt(data)) ? undefined : parseInt(data);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										109
									
								
								src/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,109 @@
 | 
			
		||||
// MARK: Imports
 | 
			
		||||
import path from 'node:path';
 | 
			
		||||
import __path from './handlers/path.js';
 | 
			
		||||
import log from './handlers/log.js';
 | 
			
		||||
import db from './handlers/db.js';
 | 
			
		||||
import config from './handlers/config.js';
 | 
			
		||||
 | 
			
		||||
// Express & more
 | 
			
		||||
import express from 'express';
 | 
			
		||||
import cors from 'cors';
 | 
			
		||||
import helmet from 'helmet';
 | 
			
		||||
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 ChildProcess from 'child_process';
 | 
			
		||||
 | 
			
		||||
import routes from './routes/index.js';
 | 
			
		||||
 | 
			
		||||
import fs from 'node:fs';
 | 
			
		||||
 | 
			
		||||
log.core.trace('Running from path: ' + __path);
 | 
			
		||||
 | 
			
		||||
// MARK: Express
 | 
			
		||||
const app = express();
 | 
			
		||||
 | 
			
		||||
// Versioning
 | 
			
		||||
try {
 | 
			
		||||
	const rawPkg = fs.readFileSync('package.json', 'utf8');
 | 
			
		||||
	const pkgJson = JSON.parse(rawPkg);
 | 
			
		||||
	app.locals.version = pkgJson.version;
 | 
			
		||||
} catch (error) {
 | 
			
		||||
	log.core.error('Failed to get version from package.json.');
 | 
			
		||||
	app.locals.version = '0.0.0';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
try {
 | 
			
		||||
	app.locals.versionRevLong = ChildProcess.execSync('git rev-parse HEAD').toString().trim();
 | 
			
		||||
	app.locals.versionRev = app.locals.versionRevLong.substring(0, 7);
 | 
			
		||||
} catch (error) {
 | 
			
		||||
	log.core.error('Failed to get git revision hash.');
 | 
			
		||||
	app.locals.versionRev = '0';
 | 
			
		||||
	app.locals.versionRevLong = '0';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
try {
 | 
			
		||||
	app.locals.versionRevLatest = ChildProcess.execSync('git ls-remote --refs -q').toString().trim().split('\t')[0];
 | 
			
		||||
} catch (error) {
 | 
			
		||||
	log.core.error('Failed to get latest git revision hash.');
 | 
			
		||||
	app.locals.versionRevLatest = '0';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
app.locals.versionUpdateAvailable = false;
 | 
			
		||||
if (app.locals.versionRevLong === app.locals.versionRevLatest) {
 | 
			
		||||
	log.core.info(`Running Latest Version (${app.locals.versionRevLong}; ${app.locals.version})`);
 | 
			
		||||
} else {
 | 
			
		||||
	log.core.info(`Running Version: ${app.locals.versionRevLong}; ${app.locals.version} (Latest: ${app.locals.versionRevLatest})`);
 | 
			
		||||
	app.locals.versionUpdateAvailable = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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); // helmet does this too. But not in devmode
 | 
			
		||||
if (!config.global.devmode) {
 | 
			
		||||
	app.use(
 | 
			
		||||
		helmet({
 | 
			
		||||
			strictTransportSecurity: config.global.http_enable_hsts,
 | 
			
		||||
			contentSecurityPolicy: {
 | 
			
		||||
				useDefaults: false,
 | 
			
		||||
				directives: {
 | 
			
		||||
					defaultSrc: ["'self'"],
 | 
			
		||||
					scriptSrc: ["'self'", config.global.http_domain],
 | 
			
		||||
					objectSrc: ["'none'"],
 | 
			
		||||
					upgradeInsecureRequests: config.global.devmode ? null : []
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	); // Add headers
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
app.use(fileUpload());
 | 
			
		||||
app.use(bodyParser.urlencoded({ extended: false }));
 | 
			
		||||
app.use(bodyParser.json());
 | 
			
		||||
 | 
			
		||||
app.use(routes);
 | 
			
		||||
 | 
			
		||||
// TODO: Remove hardcoded http
 | 
			
		||||
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
									
								
							
							
						
						
									
										140
									
								
								src/libs/configManager.ts
									
									
									
									
									
										Normal 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);
 | 
			
		||||
*/
 | 
			
		||||
							
								
								
									
										11
									
								
								src/routes/api/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/routes/api/index.ts
									
									
									
									
									
										Normal 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;
 | 
			
		||||
							
								
								
									
										51
									
								
								src/routes/api/v1/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/routes/api/v1/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
import express from 'express';
 | 
			
		||||
import passport from 'passport';
 | 
			
		||||
 | 
			
		||||
// Route imports
 | 
			
		||||
import testRoute from './test.js';
 | 
			
		||||
import versionRoute from './version.js'
 | 
			
		||||
 | 
			
		||||
import userRoute from './user.js';
 | 
			
		||||
import userRoute_schema from './user_schema.js';
 | 
			
		||||
 | 
			
		||||
// import content_route from './content.js';
 | 
			
		||||
// import content_schema from './content_schema.js';
 | 
			
		||||
 | 
			
		||||
// import * as content_s3_sub_route from './content_s3_sub.js';
 | 
			
		||||
// import * as content_s3_sub_schema from './content_s3_sub_schema.js';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// Router base is '/api/v1'
 | 
			
		||||
const Router = express.Router({ strict: false });
 | 
			
		||||
 | 
			
		||||
// All empty strings are undefined (not null!) values (body)
 | 
			
		||||
Router.use('*', function (req, res, next) {
 | 
			
		||||
	for (let key in req.body) {
 | 
			
		||||
		if (req.body[key] === '') {
 | 
			
		||||
			req.body[key] = undefined;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	next();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// All api routes lowercase! Yea I know but when strict: true it matters.
 | 
			
		||||
Router.route('/user').get(userRoute.get).post(userRoute.post).patch(userRoute.patch).delete(userRoute.del);
 | 
			
		||||
Router.route('/user/describe').get(userRoute_schema);
 | 
			
		||||
 | 
			
		||||
// Router.route('/content').get(content_route.get).post(content_route.post).patch(content_route.patch).delete(content_route.del);
 | 
			
		||||
// Router.route('/content/describe').get(content_schema);
 | 
			
		||||
 | 
			
		||||
// Router.route('/content/downloadurl').get(content_s3_sub_route.get_downloadurl);
 | 
			
		||||
// Router.route('/content/uploadurl').get(content_s3_sub_route.get_uploadurl);
 | 
			
		||||
// Router.route('/content/downloadurl/describe').get(content_s3_sub_schema.get_describe_downloadurl);
 | 
			
		||||
// Router.route('/content/uploadurl/describe').get(content_s3_sub_schema.get_describe_uploadurl);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Router.route('/version').get(versionRoute.get);
 | 
			
		||||
//Router.use('/search', search_routes);
 | 
			
		||||
 | 
			
		||||
Router.route('/test').get(testRoute.get);
 | 
			
		||||
 | 
			
		||||
export default Router;
 | 
			
		||||
							
								
								
									
										7
									
								
								src/routes/api/v1/test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/routes/api/v1/test.ts
									
									
									
									
									
										Normal 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 };
 | 
			
		||||
							
								
								
									
										165
									
								
								src/routes/api/v1/user.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								src/routes/api/v1/user.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,165 @@
 | 
			
		||||
import { Request, Response } from 'express';
 | 
			
		||||
import db, { handlePrismaError } from '../../../handlers/db.js'; // Database
 | 
			
		||||
import log from '../../../handlers/log.js';
 | 
			
		||||
import { parseDynamicSortBy } from '../../../helpers/prisma_helpers.js';
 | 
			
		||||
import { schema_get, schema_post, schema_patch, schema_del } from './user_schema.js';
 | 
			
		||||
 | 
			
		||||
// MARK: GET user
 | 
			
		||||
async function get(req: Request, res: Response) {
 | 
			
		||||
	const { error, value } = schema_get.validate(req.query);
 | 
			
		||||
	if (error) {
 | 
			
		||||
		log.api?.debug('GET user Error:', req.query, value, error.details[0].message);
 | 
			
		||||
		res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message });
 | 
			
		||||
	} else {
 | 
			
		||||
		log.api?.debug('GET user Success:', req.query, value);
 | 
			
		||||
 | 
			
		||||
		if (value.search !== undefined || value.id !== undefined) {
 | 
			
		||||
			// if search or get by id
 | 
			
		||||
			await db
 | 
			
		||||
				.$transaction([
 | 
			
		||||
					// Same query for count and findMany
 | 
			
		||||
					db.user.count({
 | 
			
		||||
						where: {
 | 
			
		||||
							OR: [{ id: value.id }, { name: { search: value.search } }]
 | 
			
		||||
						},
 | 
			
		||||
						orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()),
 | 
			
		||||
						skip: value.skip,
 | 
			
		||||
						take: value.take
 | 
			
		||||
					}),
 | 
			
		||||
					db.user.findMany({
 | 
			
		||||
						where: {
 | 
			
		||||
							OR: [{ id: value.id }, { name: { search: value.search } }]
 | 
			
		||||
						},
 | 
			
		||||
						orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()),
 | 
			
		||||
						skip: value.skip,
 | 
			
		||||
						take: value.take
 | 
			
		||||
					})
 | 
			
		||||
				])
 | 
			
		||||
				.then(([count, result]) => {
 | 
			
		||||
					if (result.length !== 0) {
 | 
			
		||||
						result.forEach((element: { id: number; name: string; code: string | null | boolean }) => {
 | 
			
		||||
							// code-> true if code is set
 | 
			
		||||
							element.code = element.code !== null;
 | 
			
		||||
						});
 | 
			
		||||
						res.status(200).json({ count, result });
 | 
			
		||||
					} else {
 | 
			
		||||
						res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified object' });
 | 
			
		||||
					}
 | 
			
		||||
				})
 | 
			
		||||
				.catch((err) => {
 | 
			
		||||
					handlePrismaError(err, res, 'GET user');
 | 
			
		||||
				});
 | 
			
		||||
		} else {
 | 
			
		||||
			// get all
 | 
			
		||||
			await db
 | 
			
		||||
				.$transaction([
 | 
			
		||||
					// Same query for count and findMany
 | 
			
		||||
					db.user.count({
 | 
			
		||||
						orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()),
 | 
			
		||||
						skip: value.skip,
 | 
			
		||||
						take: value.take
 | 
			
		||||
					}),
 | 
			
		||||
					db.user.findMany({
 | 
			
		||||
						orderBy: parseDynamicSortBy(value.sort.toString(), value.order.toString()),
 | 
			
		||||
						skip: value.skip,
 | 
			
		||||
						take: value.take
 | 
			
		||||
					})
 | 
			
		||||
				])
 | 
			
		||||
				.then(([count, result]) => {
 | 
			
		||||
					if (result.length !== 0) {
 | 
			
		||||
						result.forEach((element: { id: number; name: string; code: string | null | boolean }) => {
 | 
			
		||||
							// code-> true if code is set
 | 
			
		||||
							element.code = element.code !== null;
 | 
			
		||||
						});
 | 
			
		||||
						res.status(200).json({ count, result });
 | 
			
		||||
					} else {
 | 
			
		||||
						res.status(404).json({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified object' });
 | 
			
		||||
					}
 | 
			
		||||
				})
 | 
			
		||||
				.catch((err) => {
 | 
			
		||||
					handlePrismaError(err, res, 'GET user');
 | 
			
		||||
				});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MARK: CREATE user
 | 
			
		||||
async function post(req: Request, res: Response) {
 | 
			
		||||
	const { error, value } = schema_post.validate(req.body);
 | 
			
		||||
	if (error) {
 | 
			
		||||
		log.api?.debug('POST user Error:', req.body, value, error.details[0].message);
 | 
			
		||||
		res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message });
 | 
			
		||||
	} else {
 | 
			
		||||
		log.api?.debug('POST user Success:', req.body, value);
 | 
			
		||||
		await db.user
 | 
			
		||||
			.create({
 | 
			
		||||
				data: {
 | 
			
		||||
					name: value.name,
 | 
			
		||||
					code: value.code
 | 
			
		||||
				},
 | 
			
		||||
				select: {
 | 
			
		||||
					id: true
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
			.then((result) => {
 | 
			
		||||
				res.status(201).json({ status: 'CREATED', message: 'Successfully created user', id: result.id });
 | 
			
		||||
			})
 | 
			
		||||
			.catch((err) => {
 | 
			
		||||
				handlePrismaError(err, res, 'POST user');
 | 
			
		||||
			});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MARK: UPDATE user
 | 
			
		||||
async function patch(req: Request, res: Response) {
 | 
			
		||||
	const { error, value } = schema_patch.validate(req.body);
 | 
			
		||||
	if (error) {
 | 
			
		||||
		log.api?.debug('PATCH user Error:', req.body, value, error.details[0].message);
 | 
			
		||||
		res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message });
 | 
			
		||||
	} else {
 | 
			
		||||
		log.api?.debug('PATCH user Success:', req.body, value);
 | 
			
		||||
		await db.user
 | 
			
		||||
			.update({
 | 
			
		||||
				where: {
 | 
			
		||||
					id: value.id
 | 
			
		||||
				},
 | 
			
		||||
				data: {
 | 
			
		||||
					name: value.name,
 | 
			
		||||
					code: value.code
 | 
			
		||||
				},
 | 
			
		||||
				select: {
 | 
			
		||||
					id: true
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
			.then((result) => {
 | 
			
		||||
				res.status(200).json({ status: 'UPDATED', message: 'Successfully updated user', id: result.id });
 | 
			
		||||
			})
 | 
			
		||||
			.catch((err) => {
 | 
			
		||||
				handlePrismaError(err, res, 'PATCH user');
 | 
			
		||||
			});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MARK: DELETE user
 | 
			
		||||
async function del(req: Request, res: Response) {
 | 
			
		||||
	const { error, value } = schema_del.validate(req.body);
 | 
			
		||||
	if (error) {
 | 
			
		||||
		log.api?.debug('DEL user Error:', req.body, value, error.details[0].message);
 | 
			
		||||
		res.status(400).json({ status: 'ERROR', errorcode: 'VALIDATION_ERROR', message: error.details[0].message });
 | 
			
		||||
	} else {
 | 
			
		||||
		log.api?.debug('DEL user Success:', req.body, value);
 | 
			
		||||
		await db.user
 | 
			
		||||
			.delete({
 | 
			
		||||
				where: {
 | 
			
		||||
					id: value.id
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
			.then((result) => {
 | 
			
		||||
				res.status(200).json({ status: 'DELETED', message: 'Successfully deleted user', id: result.id });
 | 
			
		||||
			}).catch((err) => {
 | 
			
		||||
				handlePrismaError(err, res, 'DEL user');
 | 
			
		||||
			});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default { get, post, patch, del };
 | 
			
		||||
							
								
								
									
										58
									
								
								src/routes/api/v1/user_schema.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/routes/api/v1/user_schema.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
import { Request, Response } from 'express';
 | 
			
		||||
import validator from 'joi'; // DOCS: https://joi.dev/api
 | 
			
		||||
import { Prisma } from '@prisma/client';
 | 
			
		||||
 | 
			
		||||
// MARK: GET user
 | 
			
		||||
const schema_get = validator
 | 
			
		||||
	.object({
 | 
			
		||||
		sort: validator
 | 
			
		||||
			.string()
 | 
			
		||||
			.valid(...Object.keys(Prisma.UserScalarFieldEnum))
 | 
			
		||||
			.default('id'),
 | 
			
		||||
 | 
			
		||||
		order: validator.string().valid('asc', 'desc').default('asc'),
 | 
			
		||||
		take: validator.number().min(1).max(512),
 | 
			
		||||
		skip: validator.number().min(0),
 | 
			
		||||
		// This regex ensures that the search string does not contain consecutive asterisks (**) and is at least 3 characters long.
 | 
			
		||||
		search: validator.string().min(3).max(20).regex(new RegExp('^(?!.*\\*{2,}).*$')),
 | 
			
		||||
		id: validator.number().positive().precision(0)
 | 
			
		||||
	})
 | 
			
		||||
	.nand('id', 'search'); // Allow id or search. not both.
 | 
			
		||||
 | 
			
		||||
// MARK: CREATE alertContact
 | 
			
		||||
const schema_post = validator.object({
 | 
			
		||||
	name: validator.string().min(1).max(32).required(),
 | 
			
		||||
	code: validator.string().min(4).max(4).trim().regex(new RegExp('/^[0-9]+$/'))
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// MARK: UPDATE alertContact
 | 
			
		||||
const schema_patch = validator
 | 
			
		||||
	.object({
 | 
			
		||||
		id: validator.number().positive().precision(0).required(),
 | 
			
		||||
		name: validator.string().min(1).max(32),
 | 
			
		||||
		code: validator.string().min(4).max(4).trim().regex(new RegExp('/^[0-9]+$/'))
 | 
			
		||||
	})
 | 
			
		||||
	.or('name', 'code');
 | 
			
		||||
 | 
			
		||||
// MARK: DELETE alertContact
 | 
			
		||||
const schema_del = validator.object({
 | 
			
		||||
	id: validator.number().positive().precision(0).required()
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Describe all schemas
 | 
			
		||||
const schema_get_desc = schema_get.describe();
 | 
			
		||||
const schema_post_desc = schema_post.describe();
 | 
			
		||||
const schema_patch_desc = schema_patch.describe();
 | 
			
		||||
const schema_del_desc = schema_del.describe();
 | 
			
		||||
 | 
			
		||||
// GET route
 | 
			
		||||
export default async function get(req: Request, res: Response) {
 | 
			
		||||
	res.status(200).json({
 | 
			
		||||
		GET: schema_get_desc,
 | 
			
		||||
		POST: schema_post_desc,
 | 
			
		||||
		PATCH: schema_patch_desc,
 | 
			
		||||
		DELETE: schema_del_desc
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { schema_get, schema_post, schema_patch, schema_del };
 | 
			
		||||
							
								
								
									
										9
									
								
								src/routes/api/v1/version.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/routes/api/v1/version.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
import { Request, Response } from 'express';
 | 
			
		||||
 | 
			
		||||
function get(req: Request, res: Response) {
 | 
			
		||||
	res.status(200).send({ version: '1.0.0', commit: req.app.locals.versionRev, updateAvailable: req.app.locals.versionUpdateAvailable });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default { get };
 | 
			
		||||
 | 
			
		||||
// TODO: FIXME!!!!!!
 | 
			
		||||
							
								
								
									
										7
									
								
								src/routes/frontend/contact.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/routes/frontend/contact.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
import express, { Request, Response } from 'express';
 | 
			
		||||
 | 
			
		||||
function get(req: Request, res: Response) {
 | 
			
		||||
	res.render("contacts")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default { get };
 | 
			
		||||
							
								
								
									
										7
									
								
								src/routes/frontend/dashboard.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/routes/frontend/dashboard.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
import express, { Request, Response } from 'express';
 | 
			
		||||
 | 
			
		||||
function get(req: Request, res: Response) {
 | 
			
		||||
	res.render("lockscreen", { message: "Hello world from eta!" })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default { get };
 | 
			
		||||
							
								
								
									
										25
									
								
								src/routes/frontend/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/routes/frontend/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
import express from 'express';
 | 
			
		||||
 | 
			
		||||
// Route imports
 | 
			
		||||
import dashboardRoute from './dashboard.js';
 | 
			
		||||
import testRoute from './test.js';
 | 
			
		||||
import contactRoute from './contact.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);
 | 
			
		||||
Router.route('/dbTest').get(testRoute.get);
 | 
			
		||||
Router.route('/contact').get(contactRoute.get);
 | 
			
		||||
 | 
			
		||||
export default Router;
 | 
			
		||||
							
								
								
									
										7
									
								
								src/routes/frontend/test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/routes/frontend/test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
import express, { Request, Response } from 'express';
 | 
			
		||||
 | 
			
		||||
function get(req: Request, res: Response) {
 | 
			
		||||
	res.render("test", { message: "Hello world from eta!" })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default { get };
 | 
			
		||||
							
								
								
									
										34
									
								
								src/routes/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/routes/index.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
import express from 'express';
 | 
			
		||||
import path from 'node:path';
 | 
			
		||||
import __path from "../handlers/path.js";
 | 
			
		||||
import log from "../handlers/log.js";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// Route imports
 | 
			
		||||
import frontend_routes from './frontend/index.js';
 | 
			
		||||
import api_routes from './api/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')));
 | 
			
		||||
Router.use('/libs/bootstrap-icons', express.static(path.join(__path, 'node_modules', 'bootstrap-icons')));
 | 
			
		||||
 | 
			
		||||
// Other routers
 | 
			
		||||
Router.use('/api', api_routes);
 | 
			
		||||
Router.use('/', 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({ status: 'ERROR', errorcode: 'NOT_FOUND', message: 'Could not find specified page' });
 | 
			
		||||
	} else {
 | 
			
		||||
		res.status(404).render('errors/404', { url: req.originalUrl });
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export default Router;
 | 
			
		||||
		Reference in New Issue
	
	Block a user