split in env required and dynamic values
This commit is contained in:
parent
f32143b7ac
commit
730c25a9a1
35 changed files with 491 additions and 198 deletions
|
@ -4,9 +4,9 @@ import { EntityManager } from "typeorm";
|
|||
import uniqBy from "lodash.uniqby";
|
||||
import InternalException from "../exceptions/internalException";
|
||||
import UserService from "../service/management/userService";
|
||||
import { BACKUP_COPIES, BACKUP_INTERVAL } from "../env.defaults";
|
||||
import DatabaseActionException from "../exceptions/databaseActionException";
|
||||
import { availableTemplates } from "../type/templateTypes";
|
||||
import SettingHelper from "./settingsHelper";
|
||||
|
||||
export type BackupSection =
|
||||
| "member"
|
||||
|
@ -103,7 +103,7 @@ export default abstract class BackupHelper {
|
|||
let files = FileSystemHelper.getFilesInDirectory("backup", ".json");
|
||||
let sorted = files.sort((a, b) => new Date(b.split(".")[0]).getTime() - new Date(a.split(".")[0]).getTime());
|
||||
|
||||
const filesToDelete = sorted.slice(BACKUP_COPIES);
|
||||
const filesToDelete = sorted.slice(SettingHelper.getSetting("backup.copies") as number);
|
||||
for (const file of filesToDelete) {
|
||||
FileSystemHelper.deleteFile("backup", file);
|
||||
}
|
||||
|
@ -117,7 +117,7 @@ export default abstract class BackupHelper {
|
|||
let diffInMs = new Date().getTime() - lastBackup.getTime();
|
||||
let diffInDays = diffInMs / (1000 * 60 * 60 * 24);
|
||||
|
||||
if (diffInDays >= BACKUP_INTERVAL) {
|
||||
if (diffInDays >= (SettingHelper.getSetting("backup.interval") as number)) {
|
||||
await this.createBackup({});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { createEvents } from "ics";
|
||||
import { calendar } from "../entity/club/calendar";
|
||||
import moment from "moment";
|
||||
import { CLUB_NAME, CLUB_WEBSITE, MAIL_USERNAME } from "../env.defaults";
|
||||
import SettingHelper from "./settingsHelper";
|
||||
|
||||
export abstract class CalendarHelper {
|
||||
public static buildICS(entries: Array<calendar>): { error?: Error; value?: string } {
|
||||
|
@ -35,7 +35,10 @@ export abstract class CalendarHelper {
|
|||
description: i.content,
|
||||
location: i.location,
|
||||
categories: [i.type.type],
|
||||
organizer: { name: CLUB_NAME, email: MAIL_USERNAME },
|
||||
organizer: {
|
||||
name: SettingHelper.getSetting("club.name") as string,
|
||||
email: SettingHelper.getSetting("mail.username") as string,
|
||||
},
|
||||
created: moment(i.createdAt)
|
||||
.format("YYYY-M-D-H-m")
|
||||
.split("-")
|
||||
|
@ -46,7 +49,9 @@ export abstract class CalendarHelper {
|
|||
.map((a) => parseInt(a)) as [number, number, number, number, number],
|
||||
transp: "OPAQUE" as "OPAQUE",
|
||||
status: "CONFIRMED",
|
||||
...(CLUB_WEBSITE != "" ? { url: CLUB_WEBSITE } : {}),
|
||||
...(SettingHelper.getSetting("club.website") != ""
|
||||
? { url: SettingHelper.getSetting("club.website") as string }
|
||||
: {}),
|
||||
alarms: [
|
||||
{
|
||||
action: "display",
|
||||
|
|
86
src/helpers/codingHelper.ts
Normal file
86
src/helpers/codingHelper.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
import { createCipheriv, createDecipheriv, scryptSync, randomBytes } from "crypto";
|
||||
import { ValueTransformer } from "typeorm";
|
||||
|
||||
export abstract class CodingHelper {
|
||||
private static readonly algorithm = "aes-256-gcm";
|
||||
private static readonly ivLength = 16;
|
||||
private static readonly authTagLength = 16;
|
||||
|
||||
static entityBaseCoding(key: string = "", fallback: string = ""): ValueTransformer {
|
||||
return {
|
||||
from(val: string | null | undefined): string {
|
||||
if (!val) return fallback;
|
||||
try {
|
||||
return CodingHelper.decrypt(key, val) || fallback;
|
||||
} catch (error) {
|
||||
console.error("Decryption error:", error);
|
||||
return fallback;
|
||||
}
|
||||
},
|
||||
to(val: string | null | undefined): string {
|
||||
const valueToEncrypt = val || fallback;
|
||||
if (valueToEncrypt === "") return "";
|
||||
|
||||
try {
|
||||
return CodingHelper.encrypt(key, valueToEncrypt);
|
||||
} catch (error) {
|
||||
console.error("Encryption error:", error);
|
||||
return "";
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static encrypt(phrase: string, content: string): string {
|
||||
if (!content) return "";
|
||||
|
||||
// Generiere zufälligen IV für jede Verschlüsselung (sicherer als statischer IV)
|
||||
const iv = randomBytes(this.ivLength);
|
||||
const key = scryptSync(phrase, "salt", 32);
|
||||
|
||||
const cipher = createCipheriv(this.algorithm, Uint8Array.from(key), Uint8Array.from(iv));
|
||||
|
||||
// Verschlüssele den Inhalt
|
||||
let encrypted = cipher.update(content, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
|
||||
// Speichere das Auth-Tag für GCM (wichtig für die Entschlüsselung)
|
||||
const authTag = cipher.getAuthTag();
|
||||
|
||||
// Gib das Format: iv:verschlüsselter_text:authTag zurück
|
||||
return Buffer.concat([
|
||||
Uint8Array.from(iv),
|
||||
Uint8Array.from(Buffer.from(encrypted, "hex")),
|
||||
Uint8Array.from(authTag),
|
||||
]).toString("base64");
|
||||
}
|
||||
|
||||
public static decrypt(phrase: string, content: string): string {
|
||||
if (!content) return "";
|
||||
|
||||
try {
|
||||
// Dekodiere den Base64-String
|
||||
const buffer = Buffer.from(content, "base64");
|
||||
|
||||
// Extrahiere IV, verschlüsselten Text und Auth-Tag
|
||||
const iv = buffer.subarray(0, this.ivLength);
|
||||
const authTag = buffer.subarray(buffer.length - this.authTagLength);
|
||||
const encryptedText = buffer.subarray(this.ivLength, buffer.length - this.authTagLength).toString("hex");
|
||||
|
||||
const key = scryptSync(phrase, "salt", 32);
|
||||
|
||||
// Erstelle Decipher und setze Auth-Tag
|
||||
const decipher = createDecipheriv(this.algorithm, Uint8Array.from(key), Uint8Array.from(iv));
|
||||
decipher.setAuthTag(Uint8Array.from(authTag));
|
||||
|
||||
// Entschlüssele den Text
|
||||
let decrypted = decipher.update(encryptedText, "hex", "utf8");
|
||||
decrypted += decipher.final("utf8");
|
||||
|
||||
return decrypted;
|
||||
} catch (error) {
|
||||
console.error("Decryption failed:", error);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
import jwt from "jsonwebtoken";
|
||||
import { JWTData, JWTToken } from "../type/jwtTypes";
|
||||
import { JWT_SECRET, JWT_EXPIRATION } from "../env.defaults";
|
||||
import InternalException from "../exceptions/internalException";
|
||||
import RolePermissionService from "../service/management/rolePermissionService";
|
||||
import UserPermissionService from "../service/management/userPermissionService";
|
||||
|
@ -9,11 +8,13 @@ import PermissionHelper from "./permissionHelper";
|
|||
import WebapiService from "../service/management/webapiService";
|
||||
import WebapiPermissionService from "../service/management/webapiPermissionService";
|
||||
import ms from "ms";
|
||||
import SettingHelper from "./settingsHelper";
|
||||
import { APPLICATION_SECRET } from "../env.defaults";
|
||||
|
||||
export abstract class JWTHelper {
|
||||
static validate(token: string): Promise<string | jwt.JwtPayload> {
|
||||
return new Promise<string | jwt.JwtPayload>((resolve, reject) => {
|
||||
jwt.verify(token, JWT_SECRET, (err, decoded) => {
|
||||
jwt.verify(token, APPLICATION_SECRET, (err, decoded) => {
|
||||
if (err) reject(err.message);
|
||||
else resolve(decoded);
|
||||
});
|
||||
|
@ -27,9 +28,11 @@ export abstract class JWTHelper {
|
|||
return new Promise<string>((resolve, reject) => {
|
||||
jwt.sign(
|
||||
data,
|
||||
JWT_SECRET,
|
||||
APPLICATION_SECRET,
|
||||
{
|
||||
...(useExpiration ?? true ? { expiresIn: expOverwrite ?? JWT_EXPIRATION } : {}),
|
||||
...(useExpiration ?? true
|
||||
? { expiresIn: expOverwrite ?? (SettingHelper.getSetting("session.jwt_expiration") as ms.StringValue) }
|
||||
: {}),
|
||||
},
|
||||
(err, token) => {
|
||||
if (err) reject(err.message);
|
||||
|
@ -100,7 +103,8 @@ export abstract class JWTHelper {
|
|||
};
|
||||
|
||||
let overwriteExpiration =
|
||||
ms(JWT_EXPIRATION) < new Date().getTime() - new Date(expiration).getTime()
|
||||
ms(SettingHelper.getSetting("session.jwt_expiration") as ms.StringValue) <
|
||||
new Date().getTime() - new Date(expiration).getTime()
|
||||
? null
|
||||
: Date.now() - new Date(expiration).getTime();
|
||||
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
import { Transporter, createTransport, TransportOptions } from "nodemailer";
|
||||
import { CLUB_NAME, MAIL_HOST, MAIL_PASSWORD, MAIL_PORT, MAIL_SECURE, MAIL_USERNAME } from "../env.defaults";
|
||||
import { Attachment } from "nodemailer/lib/mailer";
|
||||
import SettingHelper from "./settingsHelper";
|
||||
|
||||
export default abstract class MailHelper {
|
||||
private static readonly transporter: Transporter = createTransport({
|
||||
host: MAIL_HOST,
|
||||
port: MAIL_PORT,
|
||||
secure: (MAIL_SECURE as "true" | "false") == "true",
|
||||
auth: {
|
||||
user: MAIL_USERNAME,
|
||||
pass: MAIL_PASSWORD,
|
||||
},
|
||||
} as TransportOptions);
|
||||
private static transporter: Transporter;
|
||||
|
||||
static createTransport() {
|
||||
this.transporter = createTransport({
|
||||
host: SettingHelper.getSetting("mail.host"),
|
||||
port: SettingHelper.getSetting("mail.port"),
|
||||
secure: SettingHelper.getSetting("mail.secure") as boolean,
|
||||
auth: {
|
||||
user: SettingHelper.getSetting("mail.username"),
|
||||
pass: SettingHelper.getSetting("mail.password"),
|
||||
},
|
||||
} as TransportOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @description send mail
|
||||
|
@ -29,7 +33,7 @@ export default abstract class MailHelper {
|
|||
return new Promise((resolve, reject) => {
|
||||
this.transporter
|
||||
.sendMail({
|
||||
from: `"${CLUB_NAME}" <${MAIL_USERNAME}>`,
|
||||
from: `"${SettingHelper.getSetting("club.name")}" <${SettingHelper.getSetting("mail.username")}>`,
|
||||
to: target,
|
||||
subject,
|
||||
text: content,
|
||||
|
|
|
@ -10,13 +10,13 @@ import { CalendarHelper } from "./calendarHelper";
|
|||
import DynamicQueryBuilder from "./dynamicQueryBuilder";
|
||||
import { FileSystemHelper } from "./fileSystemHelper";
|
||||
import MailHelper from "./mailHelper";
|
||||
import { CLUB_NAME } from "../env.defaults";
|
||||
import { TemplateHelper } from "./templateHelper";
|
||||
import { PdfExport } from "./pdfExport";
|
||||
import NewsletterConfigService from "../service/configuration/newsletterConfigService";
|
||||
import { NewsletterConfigEnum } from "../enums/newsletterConfigEnum";
|
||||
import InternalException from "../exceptions/internalException";
|
||||
import EventEmitter from "events";
|
||||
import SettingHelper from "./settingsHelper";
|
||||
|
||||
export interface NewsletterEventType {
|
||||
kind: "pdf" | "mail";
|
||||
|
@ -179,7 +179,7 @@ export abstract class NewsletterHelper {
|
|||
pdfRecipients.unshift({
|
||||
id: "0",
|
||||
firstname: "Alle Mitglieder",
|
||||
lastname: CLUB_NAME,
|
||||
lastname: SettingHelper.getSetting("club.name"),
|
||||
nameaffix: "",
|
||||
salutation: { salutation: "" },
|
||||
} as member);
|
||||
|
@ -221,11 +221,14 @@ export abstract class NewsletterHelper {
|
|||
const { body } = await TemplateHelper.renderFileForModule({
|
||||
module: "newsletter",
|
||||
bodyData: data,
|
||||
title: `Newsletter von ${CLUB_NAME}`,
|
||||
title: `Newsletter von ${SettingHelper.getSetting("club.name")}`,
|
||||
});
|
||||
await MailHelper.sendMail(rec.sendNewsletter.email, `Newsletter von ${CLUB_NAME}`, body, [
|
||||
{ filename: "events.ics", path: this.getICSFilePath(newsletter) },
|
||||
])
|
||||
await MailHelper.sendMail(
|
||||
rec.sendNewsletter.email,
|
||||
`Newsletter von ${SettingHelper.getSetting("club.name")}`,
|
||||
body,
|
||||
[{ filename: "events.ics", path: this.getICSFilePath(newsletter) }]
|
||||
)
|
||||
.then(() => {
|
||||
this.formatJobEmit(
|
||||
"progress",
|
||||
|
@ -286,7 +289,7 @@ export abstract class NewsletterHelper {
|
|||
|
||||
await PdfExport.renderFile({
|
||||
template: "newsletter",
|
||||
title: `Newsletter von ${CLUB_NAME}`,
|
||||
title: `Newsletter von ${SettingHelper.getSetting("club.name")}`,
|
||||
filename: `${rec.lastname}_${rec.firstname}_${rec.id}`.replaceAll(" ", "-"),
|
||||
folder: `newsletter/${newsletter.id}_${newsletter.title.replaceAll(" ", "")}`,
|
||||
data: data,
|
||||
|
|
|
@ -1,57 +1,135 @@
|
|||
import { SettingString, settingsType } from "../type/settingTypes";
|
||||
import ms from "ms";
|
||||
import { EnvSettingString, envSettingsType, SettingString, settingsType } from "../type/settingTypes";
|
||||
import { CodingHelper } from "./codingHelper";
|
||||
import SettingCommandHandler from "../command/settingCommandHandler";
|
||||
import SettingService from "../service/settingService";
|
||||
import { APPLICATION_SECRET } from "../env.defaults";
|
||||
|
||||
export abstract class SettingHelper {
|
||||
export default abstract class SettingHelper {
|
||||
private static settings: { [key in SettingString]?: string } = {};
|
||||
private static envSettings: { [key in EnvSettingString]?: string } = {};
|
||||
|
||||
public static getSetting(key: SettingString): string | number | boolean | ms.StringValue {
|
||||
let settingType = settingsType[key];
|
||||
return this.settings[key] ?? settingType.default ?? "";
|
||||
let setting = this.settings[key] ?? String(settingType.default ?? "");
|
||||
|
||||
if (Array.isArray(settingType.type)) {
|
||||
return setting;
|
||||
}
|
||||
|
||||
if (settingType.type.includes("/crypt")) {
|
||||
setting = CodingHelper.decrypt(APPLICATION_SECRET, String(setting));
|
||||
}
|
||||
|
||||
if (settingType.type.startsWith("string")) {
|
||||
return setting;
|
||||
}
|
||||
if (settingType.type.startsWith("ms")) {
|
||||
return setting as ms.StringValue;
|
||||
}
|
||||
if (settingType.type.startsWith("number")) {
|
||||
return Number(setting);
|
||||
}
|
||||
if (settingType.type.startsWith("boolean")) {
|
||||
return setting == "true";
|
||||
}
|
||||
return setting;
|
||||
}
|
||||
|
||||
public static getEnvSetting(key: EnvSettingString): string {
|
||||
let settingType = envSettingsType[key];
|
||||
return this.envSettings[key] ?? settingType.default ?? "";
|
||||
public static async setSetting(key: SettingString, value: string) {
|
||||
if (value == undefined || value == null) return;
|
||||
let settingType = settingsType[key];
|
||||
|
||||
let result = value;
|
||||
|
||||
this.checkSettings(key, result);
|
||||
|
||||
if (!Array.isArray(settingType.type) && settingType.type.includes("/crypt")) {
|
||||
result = CodingHelper.encrypt(APPLICATION_SECRET, value);
|
||||
}
|
||||
|
||||
await SettingCommandHandler.create({
|
||||
topic: key.split(".")[0],
|
||||
key: key.split(".")[1],
|
||||
value: result,
|
||||
});
|
||||
}
|
||||
|
||||
public static async configure() {}
|
||||
public static async resetSetting(key: SettingString) {
|
||||
let settingType = settingsType[key];
|
||||
this.settings[key] = String(settingType.default ?? "");
|
||||
|
||||
public static async configurEnv() {
|
||||
this.envSettings = {
|
||||
"database.type": process.env.DB_TYPE,
|
||||
"database.host": process.env.DB_HOST,
|
||||
"database.port": process.env.DB_PORT,
|
||||
"database.name": process.env.DB_NAME,
|
||||
"database.username": process.env.DB_USERNAME,
|
||||
"database.password": process.env.DB_PASSWORD,
|
||||
};
|
||||
this.checkEnvSettings();
|
||||
await SettingCommandHandler.delete({
|
||||
topic: key.split(".")[0],
|
||||
key: key.split(".")[1],
|
||||
});
|
||||
}
|
||||
|
||||
private static checkEnvSettings() {
|
||||
if (!["mysql", "sqlite", "postgres"].includes(this.envSettings["database.type"]))
|
||||
throw new Error("set valid value to DB_TYPE (mysql|sqlite|postgres)");
|
||||
if (this.checkIfEmptyOrNotString(this.envSettings["database.name"]))
|
||||
throw new Error("set valid value to DB_NAME (name of database or filepath for sqlite)");
|
||||
public static async configure() {
|
||||
console.log("Configured Settings");
|
||||
let settings = await SettingService.getSettings();
|
||||
|
||||
for (const element of settings) {
|
||||
let ref = `${element.topic}.${element.key}` as SettingString;
|
||||
this.settings[ref] = element.value;
|
||||
this.checkSettings(ref);
|
||||
}
|
||||
}
|
||||
|
||||
private static checkSettings(key: SettingString, value?: string) {
|
||||
let settingType = settingsType[key];
|
||||
|
||||
if (!value) {
|
||||
value = this.getSetting(key).toString();
|
||||
}
|
||||
|
||||
if (
|
||||
this.checkIfEmptyOrNotString(this.envSettings["database.host"]) &&
|
||||
this.envSettings["database.type"] != "sqlite"
|
||||
)
|
||||
throw new Error("set valid value to DB_HOST");
|
||||
!Array.isArray(settingType.type) &&
|
||||
settingType.type.startsWith("string") &&
|
||||
this.checkIfEmptyOrNotString(value)
|
||||
) {
|
||||
throw new Error(`set valid value to ${key}`);
|
||||
}
|
||||
if (
|
||||
this.checkIfEmptyOrNotString(this.envSettings["database.username"]) &&
|
||||
this.envSettings["database.type"] != "sqlite"
|
||||
)
|
||||
throw new Error("set valid value to DB_USERNAME");
|
||||
!Array.isArray(settingType.type) &&
|
||||
settingType.type.startsWith("ms") &&
|
||||
this.checkIfNotMS(value as ms.StringValue)
|
||||
) {
|
||||
throw new Error(`set valid ms value to ${key} -> [0-9]*(y|d|h|m|s)`);
|
||||
}
|
||||
if (!Array.isArray(settingType.type) && settingType.type.startsWith("number") && isNaN(Number(value))) {
|
||||
throw new Error(`set valid numeric value to ${key}`);
|
||||
}
|
||||
if (
|
||||
this.checkIfEmptyOrNotString(this.envSettings["database.password"]) &&
|
||||
this.envSettings["database.type"] != "sqlite"
|
||||
)
|
||||
throw new Error("set valid value to DB_PASSWORD");
|
||||
!Array.isArray(settingType.type) &&
|
||||
settingType.type.startsWith("number") &&
|
||||
settingType.min &&
|
||||
Number(value) < settingType.min
|
||||
) {
|
||||
throw new Error(`${key} has to be at least ${settingType.min}`);
|
||||
}
|
||||
if (
|
||||
!Array.isArray(settingType.type) &&
|
||||
settingType.type.startsWith("boolean") &&
|
||||
value != "true" &&
|
||||
value != "false"
|
||||
) {
|
||||
throw new Error(`"set 'true' or 'false' to ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
private static checkIfEmptyOrNotString(val: any) {
|
||||
return typeof val != "string" || val == "";
|
||||
}
|
||||
|
||||
private static checkIfNotMS(input: ms.StringValue): boolean {
|
||||
try {
|
||||
const result = ms(input);
|
||||
if (result === undefined) {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue