ff-admin-server/src/helpers/settingsHelper.ts

288 lines
9.4 KiB
TypeScript

import { SettingString, settingsType, SettingTopic, SettingTypeAtom, SettingValueMapping } from "../type/settingTypes";
import { CodingHelper } from "./codingHelper";
import SettingCommandHandler from "../command/management/setting/settingCommandHandler";
import SettingService from "../service/management/settingService";
import { APPLICATION_SECRET } from "../env.defaults";
import {
BooleanConverter,
EmailConverter,
LongStringConverter,
MsConverter,
NumberConverter,
StringConverter,
TypeConverter,
UrlConverter,
} from "./convertHelper";
import cloneDeep from "lodash.clonedeep";
import { rejects } from "assert";
import InternalException from "../exceptions/internalException";
import MailHelper from "./mailHelper";
export default abstract class SettingHelper {
private static settings: { [key in SettingString]?: string } = {};
private static listeners: Map<SettingString, Array<(newValue: any, oldValue: any) => void>> = new Map();
private static topicListeners: Map<SettingTopic, Array<() => void>> = new Map();
private static readonly converters: Record<SettingTypeAtom, TypeConverter<any>> = {
longstring: new LongStringConverter(),
string: new StringConverter(),
url: new UrlConverter(),
number: new NumberConverter(),
boolean: new BooleanConverter(),
ms: new MsConverter(),
email: new EmailConverter(),
};
public static getAllSettings(): { [key in SettingString]: SettingValueMapping[key] } {
return Object.keys(settingsType).reduce((acc, key) => {
const typedKey = key as SettingString;
//@ts-expect-error
acc[typedKey] = this.getSetting(typedKey);
return acc;
}, {} as { [key in SettingString]: SettingValueMapping[key] });
}
/**
* Returns the value of a setting with the correct type based on the key
* @param key The key of the setting
* @returns The typed value of the setting
*/
public static getSetting<K extends SettingString>(key: K): SettingValueMapping[K] {
const settingType = settingsType[key];
const rawValue = this.settings[key] ?? String(settingType.default ?? "");
if (Array.isArray(settingType.type)) {
return rawValue as unknown as SettingValueMapping[K];
}
let processedValue = rawValue;
if (typeof settingType.type === "string" && settingType.type.includes("/crypt")) {
processedValue = CodingHelper.decrypt(APPLICATION_SECRET, processedValue);
}
const baseType =
typeof settingType.type === "string"
? (settingType.type.split("/")[0] as SettingTypeAtom)
: (settingType.type as SettingTypeAtom);
return this.converters[baseType].fromString(processedValue) as unknown as SettingValueMapping[K];
}
/**
* Sets a setting
* undefined value leads to reset of key
* @param key The key of the setting
* @param value The value to set
*/
public static async setSetting<K extends SettingString>(key: K, value: SettingValueMapping[K]): Promise<void> {
if (value === undefined || value === null) {
if (key != "mail.password") this.resetSetting(key);
return;
}
const stringValue = String(value);
const settingType = settingsType[key];
this.validateSetting(key, stringValue);
const oldValue = cloneDeep(this.settings[key]);
let newValue = stringValue;
if (typeof settingType.type === "string" && settingType.type.includes("/crypt")) {
newValue = CodingHelper.encrypt(APPLICATION_SECRET, stringValue);
}
this.settings[key] = stringValue;
const [topic, settingKey] = key.split(".") as [SettingTopic, string];
await SettingCommandHandler.create({
topic,
key: settingKey,
value: newValue,
});
this.notifyListeners(key, newValue, oldValue);
}
/**
* Resets a setting to its default value
* @param key The key of the setting
*/
public static async resetSetting(key: SettingString): Promise<void> {
if (this.getSetting(key) == String(settingsType[key].default ?? "")) return;
const oldValue = this.getSetting(key);
const settingType = settingsType[key];
this.settings[key] = String(settingType.default ?? "");
const [topic, settingKey] = key.split(".") as [SettingTopic, string];
await SettingCommandHandler.delete({
topic,
key: settingKey,
});
const newValue = this.getSetting(key);
this.notifyListeners(key, newValue, oldValue);
}
public static async configure(): Promise<void> {
console.log("Configuring Settings");
const settings = await SettingService.getSettings();
for (const element of settings) {
const ref = `${element.topic}.${element.key}` as SettingString;
this.settings[ref] = element.value;
try {
this.validateSetting(ref);
} catch (error) {
console.warn(`Invalid setting ${ref}: ${error.message}`);
}
}
}
public static async checkMail<K extends SettingString>(
setting: Array<{ key: K; value: SettingValueMapping[K] }>
): Promise<void> {
return new Promise(async (resolve, reject) => {
if (setting.some((t) => t.key == "mail.email" && t.value != undefined)) {
let emailValue = setting.find((t) => t.key == "mail.email").value as string;
let checkMail = await MailHelper.checkMail(emailValue);
if (!checkMail) {
return reject("mail");
}
}
if (setting.some((t) => t.key.startsWith("mail"))) {
let checkConfig = await MailHelper.verifyTransport({
user:
(setting.find((t) => t.key == "mail.username").value as string) ??
SettingHelper.getSetting("mail.username"),
password:
(setting.find((t) => t.key == "mail.password").value as string) ??
SettingHelper.getSetting("mail.password"),
host: (setting.find((t) => t.key == "mail.host").value as string) ?? SettingHelper.getSetting("mail.host"),
port: (setting.find((t) => t.key == "mail.port").value as number) ?? SettingHelper.getSetting("mail.port"),
secure:
(setting.find((t) => t.key == "mail.secure").value as boolean) ?? SettingHelper.getSetting("mail.secure"),
});
if (!checkConfig) {
return reject("Config is not valid");
}
}
resolve();
});
}
/**
* Validates a setting
* @param key The key of the setting
* @param value Optional value to validate
*/
private static validateSetting(key: SettingString, value?: string): void {
const settingType = settingsType[key];
const valueToCheck = value ?? this.settings[key] ?? String(settingType.default ?? "");
if (Array.isArray(settingType.type)) {
return;
}
const baseType =
typeof settingType.type === "string"
? (settingType.type.split("/")[0] as SettingTypeAtom)
: (settingType.type as SettingTypeAtom);
if (!this.converters[baseType].validate(valueToCheck)) {
throw new Error(`Invalid value for ${key} of type ${baseType}`);
}
if (baseType === "number" && settingType.min !== undefined) {
const numValue = Number(valueToCheck);
if (numValue < settingType.min) {
throw new Error(`${key} must be at least ${settingType.min}`);
}
}
}
/**
* Registers a listener for changes to a specific setting
* @param key The setting to monitor
* @param callback Function to be called when changes occur
*/
public static onSettingChanged<K extends SettingString>(
key: K,
callback: (newValue: SettingValueMapping[K], oldValue: SettingValueMapping[K]) => void
): void {
if (!this.listeners.has(key)) {
this.listeners.set(key, []);
}
this.listeners.get(key)!.push(callback);
}
/**
* Registers a listener for changes to a specific setting
* @param key The setting to monitor
* @param callback Function to be called when changes occur
*/
public static onSettingTopicChanged<K extends SettingTopic>(key: K, callback: () => void): void {
if (!this.topicListeners.has(key)) {
this.topicListeners.set(key, []);
}
this.topicListeners.get(key)!.push(callback);
}
/**
* Removes a registered listener
* @param key The setting
* @param callback The callback to remove
*/
public static removeSettingListener<K extends SettingString>(
key: K,
callback: (newValue: SettingValueMapping[K], oldValue: SettingValueMapping[K]) => void
): void {
if (!this.listeners.has(key)) return;
const callbacks = this.listeners.get(key)!;
const index = callbacks.indexOf(callback);
if (index !== -1) {
callbacks.splice(index, 1);
}
if (callbacks.length === 0) {
this.listeners.delete(key);
}
}
/**
* Notifies all registered listeners about changes
* @param key The changed setting
* @param newValue The new value
* @param oldValue The old value
*/
private static notifyListeners(key: SettingString, newValue: any, oldValue: any): void {
if (!this.listeners.has(key)) return;
const callbacks = this.listeners.get(key)!;
for (const callback of callbacks) {
try {
callback(newValue, oldValue);
} catch (error) {
console.error(`Error in setting listener for ${key}:`, error);
}
}
const topicCallbacks = this.topicListeners.get(key.split(".")[0] as SettingTopic)!;
for (const callback of topicCallbacks) {
try {
callback();
} catch (error) {
console.error(`Error in setting listener for ${key.split(".")[0]}:`, error);
}
}
}
}