improved Database & Models

This commit is contained in:
Kai Waggeling 2025-12-06 20:04:11 +01:00
commit 0bbe91bec3
18 changed files with 956 additions and 0 deletions

465
lib/models.mjs Normal file
View file

@ -0,0 +1,465 @@
import {
object,
array,
string,
number,
enums,
boolean,
literal,
optional,
dynamic,
nullable,
defaulted,
union,
size,
create,
assert,
define,
} from "superstruct";
import { Database } from "all.db";
import crypto from "node:crypto";
import {
ipv4,
ipv6
} from "cidr-block";
import path from "node:path";
import { EventEmitter } from "node:events";
class ObjectModel {
#modelName;
#modelStruct;
#modelStore;
#dynRef = {};
#hooks = new EventEmitter();
constructor(settings) {
this.#modelName = settings.name;
this.#modelStruct = settings.model;
this.#modelStore = new Database({
dataPath: path.join(process.cwd(), 'data/database', `${settings.name}.json`)
});
if (settings.hooks) {
Object.entries(settings.hooks).forEach(([eventName, eventFunction]) => {
this.on(eventName, eventFunction);
})
}
this.#dynRef.refOne = dynamic((objectId, context) => {
let permittedObjIds = Object.values(this.getAll()).map(item => item.id);
return enums(permittedObjIds);
});
this.#dynRef.refMany = dynamic((objectId, context) => {
let permittedObjIds = Object.values(this.getAll()).map(item => item.id);
return array(
enums(permittedObjIds)
);
});
if (settings.dynRef instanceof Object) {
this.#dynRef = Object.assign(this.#dynRef, settings.dynRef)
}
}
complete(data) {
try {
let result = create(data, this.#modelStruct);
return result;
} catch (error) {
throw new Error(`error completing ${this.#modelName}: ` + error.message);
}
}
validate(data) {
try {
assert(data, this.#modelStruct);
return data;
} catch (error) {
throw new Error(`error validating ${this.#modelName}: ` + error.message);
}
}
getAll() {
return this.#modelStore.getAll();
}
getById(objectId) {
return this.#modelStore.get(objectId);
}
create(objectData) {
// try {
const newData = this.validate(
this.complete(objectData)
);
if (this.#modelStore.exists(newData.id)) {
throw new Error(`failed create ${this.#modelName} with ID ${newData.id}. already existing in database.`);
}
this.#hooks.emit('beforeCreate', null, newData);
this.#modelStore.set(newData.id, newData);
this.#hooks.emit('afterCreate', newData);
return newData;
// } catch (error) {
// throw error;
// }
}
update(objectData) {
// try {
const newData = this.validate(
this.complete(objectData)
);
const oldData = this.getById(newData.id)
if (!oldData) {
throw new Error(`failed update ${this.#modelName} with ID ${newData.id}. not existing in database.`);
}
this.#hooks.emit('beforeUpdate', oldData, newData);
this.#modelStore.set(newData.id, newData);
this.#hooks.emit('afterUpdate', oldData, newData);
return newData;
// } catch (error) {
// throw error;
// }
}
delete(objectId) {
// try {
const oldData = this.getById(objectId);
if (!oldData) {
throw new Error(`failed delete ${this.#modelName} with ID ${newData.id}. not existing in database.`);
}
this.#hooks.emit('beforeDelete', oldData, null);
this.#modelStore.delete(oldData.id);
this.#hooks.emit('afterDelete', oldData, null);
return oldData;
// } catch (error) {
// throw error;
// }
}
has(objectId) {
return this.#modelStore.has(objectId);
}
get dynRef() {
return this.#dynRef;
}
on(eventName, eventFunction) {
this.#hooks.on(eventName, eventFunction);
}
}
// - --- --- --- --- --- --- --- --- --- --- --- ---
//
// Custom Checks
//
// - --- --- --- --- --- --- --- --- --- --- --- ---
const ipv4Address = define('ipv4Address', (value) => {
return ipv4.isValidAddress(value);
});
const ipv4Cidr = define('ipv4Cidr', (value) => {
console.log(value);
console.log(ipv4.isValidCIDR(value));
return ipv4.isValidCIDR(value);
});
// - --- --- --- --- --- --- --- --- --- --- --- ---
//
// Base Models
//
// - --- --- --- --- --- --- --- --- --- --- --- ---
// Authorisation Provider Model
export const authProvider = new ObjectModel({
name: 'authProvider',
model: object({
id: defaulted(string(), () => {
return crypto.randomUUID();
}),
name: string(),
login_text: string(),
type: enums(["oauth2"]),
settings: dynamic((value, context) => {
switch (value.type) {
case "oauth2":
return object({
client_id: string(),
client_secret: string(),
authorize_url: string(),
token_url: string(),
user_info_url: string(),
redirect_url: string(),
scope: array(string())
});
}
return object({});
})
})
});
// User Model
export const user = new ObjectModel({
name: 'user',
model: object({
id: defaulted(string(), () => {
return crypto.randomUUID();
}),
external_id: string(),
provider: string(),
displayName: string(),
email: string()
})
});
// User Group Model
export const userGroup = new ObjectModel({
name: 'userGroup',
model: object({
id: defaulted(string(), () => {
return crypto.randomUUID();
}),
name: string(),
comment: defaulted(string(), ''),
members: user.dynRef.refMany
})
});
// - --- --- --- --- --- --- --- --- --- --- --- ---
//
// Wireguard Models
//
// - --- --- --- --- --- --- --- --- --- --- --- ---
// Wireguard Interface Model
export const wireguardInterface = new ObjectModel({
name: 'wireguardInterface',
model: object({
id: defaulted(string(), () => {
return crypto.randomUUID();
}),
name: size(string(), 1, 128),
comment: defaulted(size(string(), 0, 256), ''),
enabled: defaulted(boolean(), true),
privateKey: string(),
publicKey: string(),
ifName: string(),
ifAddress: ipv4Cidr,
listenPort: number(),
endpoint: string(),
dnsServer: nullable(ipv4Address)
})
});
// Wireguard Peer Model
export const wireguardPeer = new ObjectModel({
name: 'wireguardPeer',
model: object({
id: defaulted(string(), () => {
return crypto.randomUUID();
}),
name: size(string(), 1, 128),
comment: defaulted(size(string(), 0, 256), ''),
interface: wireguardInterface.dynRef.refOne,
enabled: defaulted(boolean(), true),
privateKey: string(),
publicKey: string(),
presharedKey: string(),
ifAddress: ipv4Cidr,
keepalive: size(defaulted(number(), 30), 1, 300),
})
});
// - --- --- --- --- --- --- --- --- --- --- --- ---
//
// CMDB Models
//
// - --- --- --- --- --- --- --- --- --- --- --- ---
// Address Object Model
export const addressObject = new ObjectModel({
name: 'addressObject',
model: object({
id: defaulted(string(), () => {
return crypto.randomUUID();
}),
name: size(string(), 1, 128),
comment: defaulted(size(string(), 0, 256), ''),
elements: array(union([
object({
type: literal('ipmask'),
ipmask: ipv4Address
}),
object({
type: literal('fqdn'),
fqdn: string(),
result: nullable(defaulted(string(), null))
}),
object({
type: literal('peer'),
peer: wireguardPeer.dynRef.refOne
})
])),
members: dynamic((memberList, context) => {
// allow many values from list of Peer IDs
// - exclude objects that contain members (loop prevention)
// - exclude self (loop prevention)
let permittedAdressObjects = Object.values(addressObject.getAll())
.filter(object => object.members.length == 0)
.filter(object => object.id != context.id)
.map(item => item.id);
// return array from here to reduce Database reads
return array(enums(permittedAdressObjects));
}),
})
});
// Service Object Model
export const serviceObject = new ObjectModel({
name: 'serviceObject',
model: object({
id: defaulted(string(), () => {
return crypto.randomUUID();
}),
name: size(string(), 1, 128),
comment: defaulted(size(string(), 0, 256), ''),
elements: array(union([
object({
type: literal('tcp'),
port: number()
}),
object({
type: literal('udp'),
port: number()
}),
object({
type: literal('icmp'),
icmpType: enums([
"echo-reply",
"destination-unreachable",
"source-quench",
"redirect",
"echo-request",
"time-exceeded",
"parameter-problem",
"timestamp-request",
"timestamp-reply",
"info-request",
"info-reply",
"address-mask-request",
"address-mask-reply",
"router-advertisement",
"router-solicitation"
])
}),
object({
type: literal('esp'),
port: number()
})
]))
})
});
// - --- --- --- --- --- --- --- --- --- --- --- ---
//
// Firewall Policy Models
//
// - --- --- --- --- --- --- --- --- --- --- --- ---
export const accessPolicy = new ObjectModel({
name: 'accessPolicy',
model: object({
id: defaulted(string(), () => {
return crypto.randomUUID();
}),
name: size(string(), 1, 128),
comment: defaulted(size(string(), 0, 256), ''),
enabled: defaulted(boolean(), true),
srcAddr: addressObject.dynRef.refMany,
dstAddr: addressObject.dynRef.refMany,
services: serviceObject.dynRef.refMany,
})
});
export const natPolicy = new ObjectModel({
name: 'natPolicy',
model: object({
id: defaulted(string(), () => {
return crypto.randomUUID();
}),
name: size(string(), 1, 128),
comment: defaulted(size(string(), 0, 256), ''),
enabled: defaulted(boolean(), true),
natType: enums(['snat', 'dnat', ' masquerade']),
srcAddress: string(),
dstAddress: string(),
dnatPort: optional(number()), // Ziel Port für DNAT
})
});
// - --- --- --- --- --- --- --- --- --- --- --- ---
//
// Access Control Models
//
// - --- --- --- --- --- --- --- --- --- --- --- ---
export const configToken = new ObjectModel({
name: 'configToken',
model: object({
id: defaulted(string(), () => {
return crypto.randomUUID();
}),
token: defaulted(string(), () => {
return crypto.randomBytes(32).toString('hex');
}),
peer: string(),
name: string()
})
});
export const apiToken = new ObjectModel({
name: 'apiToken',
model: object({
id: defaulted(string(), () => {
return crypto.randomUUID();
}),
token: defaulted(string(), () => {
return crypto.randomBytes(32).toString('hex');
}),
name: string()
}),
hooks: {
beforeCreate: (oldData, newData) => {
console.log("before API Token created");
},
afterCreate: (oldData, newData) => {
console.log("after API Token created");
}
}
});
export default {
wireguardInterface,
wireguardPeer,
addressObject,
authProvider,
user,
userGroup,
accessPolicy,
natPolicy,
configToken,
apiToken
}