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