diff --git a/.env.example b/.env.example index 0d9bf9d..c1fe1dc 100644 --- a/.env.example +++ b/.env.example @@ -17,25 +17,10 @@ DB_PASSWORD = database_password ## BSP für sqlite DB_HOST = filename.db +## Dev only SERVER_PORT = portnumber -JWT_SECRET = ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 # besitzt default -JWT_EXPIRATION = [0-9]*(y|d|h|m|s) # default ist 15m -REFRESH_EXPIRATION = [0-9]*(y|d|h|m|s) # default ist 1d -PWA_REFRESH_EXPIRATION = [0-9]*(y|d|h|m|s) # default ist 5d - -MAIL_USERNAME = mail_username -MAIL_PASSWORD = mail_password -MAIL_HOST = mail_hoststring -MAIL_PORT = mail_portnumber # default ist 587 -MAIL_SECURE = (true|false) # true für port 465, false für anders gewählten port - -CLUB_NAME = clubname #default FF Admin -CLUB_WEBSITE = https://my-club-website-url #optional, muss aber mit http:// oder https:// beginnen - -BACKUP_INTERVAL = number of days (min 1) # default 1 -BACKUP_COPIES = number of parallel copies # default 7 -BACKUP_AUTO_RESTORE = (true|false) # default ist true +APPLICATION_SECRET = mysecret USE_SECURITY_STRICT_LIMIT = (true|false) # default ist true SECURITY_STRICT_LIMIT_WINDOW = [0-9]*(y|d|h|m|s) # default ist 15m diff --git a/package-lock.json b/package-lock.json index 901f487..a7c3021 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "AGPL-3.0-only", "dependencies": { "cors": "^2.8.5", + "crypto": "^1.0.1", "dotenv": "^16.4.5", "express": "^5.1.0", "express-rate-limit": "^7.5.0", @@ -1522,6 +1523,13 @@ "node": ">= 8" } }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", + "license": "ISC" + }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", diff --git a/package.json b/package.json index 357a7de..9f7d24c 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "start_ts": "ts-node src/index.ts", "typeorm": "typeorm-ts-node-commonjs", "migrate": "set DBMODE=migration && npx typeorm-ts-node-commonjs migration:generate ./src/migrations/%npm_config_name% -d ./src/data-source.ts", + "migrate-empty": "set DBMODE=migration && npx typeorm-ts-node-commonjs migration:create ./src/migrations/%npm_config_name%", "synchronize-database": "set DBMODE=update-database && npx typeorm-ts-node-commonjs schema:sync -d ./src/data-source.ts", "update-database": "set DBMODE=update-database && npx typeorm-ts-node-commonjs migration:run -d ./src/data-source.ts", "revert-database": "set DBMODE=update-database && npx typeorm-ts-node-commonjs migration:revert -d ./src/data-source.ts", @@ -25,6 +26,7 @@ "license": "AGPL-3.0-only", "dependencies": { "cors": "^2.8.5", + "crypto": "^1.0.1", "dotenv": "^16.4.5", "express": "^5.1.0", "express-rate-limit": "^7.5.0", diff --git a/src/command/refreshCommandHandler.ts b/src/command/refreshCommandHandler.ts index df6a8ea..959996a 100644 --- a/src/command/refreshCommandHandler.ts +++ b/src/command/refreshCommandHandler.ts @@ -1,10 +1,8 @@ import { dataSource } from "../data-source"; import { refresh } from "../entity/refresh"; -import { PWA_REFRESH_EXPIRATION, REFRESH_EXPIRATION } from "../env.defaults"; import DatabaseActionException from "../exceptions/databaseActionException"; -import InternalException from "../exceptions/internalException"; +import SettingHelper from "../helpers/settingsHelper"; import { StringHelper } from "../helpers/stringHelper"; -import UserService from "../service/management/userService"; import { CreateRefreshCommand, DeleteRefreshCommand } from "./refreshCommand"; import ms from "ms"; @@ -25,8 +23,8 @@ export default abstract class RefreshCommandHandler { token: refreshToken, userId: createRefresh.userId, expiry: createRefresh.isFromPwa - ? new Date(Date.now() + ms(PWA_REFRESH_EXPIRATION)) - : new Date(Date.now() + ms(REFRESH_EXPIRATION)), + ? new Date(Date.now() + ms(SettingHelper.getSetting("session.pwa_refresh_expiration") as ms.StringValue)) + : new Date(Date.now() + ms(SettingHelper.getSetting("session.refresh_expiration") as ms.StringValue)), }) .execute() .then((result) => { diff --git a/src/command/settingCommand.ts b/src/command/settingCommand.ts new file mode 100644 index 0000000..e0c9d84 --- /dev/null +++ b/src/command/settingCommand.ts @@ -0,0 +1,10 @@ +export interface CreateOrUpdateSettingCommand { + topic: string; + key: string; + value: string; +} + +export interface DeleteSettingCommand { + topic: string; + key: string; +} diff --git a/src/command/settingCommandHandler.ts b/src/command/settingCommandHandler.ts new file mode 100644 index 0000000..148bdef --- /dev/null +++ b/src/command/settingCommandHandler.ts @@ -0,0 +1,53 @@ +import { dataSource } from "../data-source"; +import { setting } from "../entity/setting"; +import DatabaseActionException from "../exceptions/databaseActionException"; +import { StringHelper } from "../helpers/stringHelper"; +import { CreateOrUpdateSettingCommand, DeleteSettingCommand } from "./settingCommand"; + +export default abstract class SettingCommandHandler { + /** + * @description create setting + * @param {CreateOrUpdateSettingCommand} createSetting + * @returns {Promise} + */ + static async create(createSetting: CreateOrUpdateSettingCommand): Promise { + const token = StringHelper.random(32); + + return await dataSource + .createQueryBuilder() + .insert() + .into(setting) + .values({ + topic: createSetting.topic, + key: createSetting.key, + value: createSetting.value, + }) + .orUpdate(["value"], ["topic", "key"]) + .execute() + .then((result) => { + return token; + }) + .catch((err) => { + throw new DatabaseActionException("CREATE OR UPDATE", "setting", err); + }); + } + + /** + * @description delete setting by topic and key + * @param {DeleteRefreshCommand} deleteSetting + * @returns {Promise} + */ + static async delete(deleteSetting: DeleteSettingCommand): Promise { + return await dataSource + .createQueryBuilder() + .delete() + .from(setting) + .where("setting.topic = :topic", { topic: deleteSetting.topic }) + .andWhere("setting.key = :key", { key: deleteSetting.key }) + .execute() + .then((res) => {}) + .catch((err) => { + throw new DatabaseActionException("DELETE", "setting", err); + }); + } +} diff --git a/src/controller/admin/management/userController.ts b/src/controller/admin/management/userController.ts index ba2a134..7da2460 100644 --- a/src/controller/admin/management/userController.ts +++ b/src/controller/admin/management/userController.ts @@ -11,10 +11,10 @@ import { } from "../../../command/management/user/userCommand"; import UserCommandHandler from "../../../command/management/user/userCommandHandler"; import MailHelper from "../../../helpers/mailHelper"; -import { CLUB_NAME } from "../../../env.defaults"; import { UpdateUserPermissionsCommand } from "../../../command/management/user/userPermissionCommand"; import UserPermissionCommandHandler from "../../../command/management/user/userPermissionCommandHandler"; import BadRequestException from "../../../exceptions/badRequestException"; +import SettingHelper from "../../../helpers/settingsHelper"; /** * @description get All users @@ -157,7 +157,7 @@ export async function deleteUser(req: Request, res: Response): Promise { // sendmail await MailHelper.sendMail( mail, - `Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`, + `Email Bestätigung für Mitglieder Admin-Portal von ${SettingHelper.getSetting("club.name")}`, `Ihr Nutzerkonto des Adminportals wurde erfolgreich gelöscht.` ); } catch (error) {} diff --git a/src/controller/admin/management/webapiController.ts b/src/controller/admin/management/webapiController.ts index 80df60a..38d40cd 100644 --- a/src/controller/admin/management/webapiController.ts +++ b/src/controller/admin/management/webapiController.ts @@ -12,8 +12,8 @@ import WebapiCommandHandler from "../../../command/management/webapi/webapiComma import { UpdateWebapiPermissionsCommand } from "../../../command/management/webapi/webapiPermissionCommand"; import WebapiPermissionCommandHandler from "../../../command/management/webapi/webapiPermissionCommandHandler"; import { JWTHelper } from "../../../helpers/jwtHelper"; -import { CLUB_NAME } from "../../../env.defaults"; import { StringHelper } from "../../../helpers/stringHelper"; +import SettingHelper from "../../../helpers/settingsHelper"; /** * @description get All apis @@ -78,7 +78,7 @@ export async function createWebapi(req: Request, res: Response): Promise { let token = await JWTHelper.create( { - iss: CLUB_NAME, + iss: SettingHelper.getSetting("club.name") as string, sub: "api_token_retrieve", aud: StringHelper.random(32), }, diff --git a/src/controller/inviteController.ts b/src/controller/inviteController.ts index 14e346c..769260f 100644 --- a/src/controller/inviteController.ts +++ b/src/controller/inviteController.ts @@ -1,6 +1,5 @@ import { Request, Response } from "express"; import { JWTHelper } from "../helpers/jwtHelper"; -import { JWTToken } from "../type/jwtTypes"; import InternalException from "../exceptions/internalException"; import RefreshCommandHandler from "../command/refreshCommandHandler"; import { CreateRefreshCommand } from "../command/refreshCommand"; @@ -15,10 +14,8 @@ import MailHelper from "../helpers/mailHelper"; import InviteService from "../service/management/inviteService"; import UserService from "../service/management/userService"; import CustomRequestException from "../exceptions/customRequestException"; -import { CLUB_NAME } from "../env.defaults"; -import { CreateUserPermissionCommand } from "../command/management/user/userPermissionCommand"; -import UserPermissionCommandHandler from "../command/management/user/userPermissionCommandHandler"; import InviteFactory from "../factory/admin/management/invite"; +import SettingHelper from "../helpers/settingsHelper"; /** * @description get all invites @@ -59,7 +56,7 @@ export async function inviteUser(req: Request, res: Response, isInvite: boolean throw new CustomRequestException(409, "Username and Mail are already in use"); } - var secret = speakeasy.generateSecret({ length: 20, name: `FF Admin ${CLUB_NAME}` }); + var secret = speakeasy.generateSecret({ length: 20, name: `FF Admin ${SettingHelper.getSetting("club.name")}` }); let createInvite: CreateInviteCommand = { username: username, @@ -73,7 +70,7 @@ export async function inviteUser(req: Request, res: Response, isInvite: boolean // sendmail await MailHelper.sendMail( mail, - `Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`, + `Email Bestätigung für Mitglieder Admin-Portal von ${SettingHelper.getSetting("club.name")}`, `Öffne folgenden Link: ${origin}/${isInvite ? "invite" : "setup"}/verify?mail=${mail}&token=${token}` ); @@ -92,7 +89,7 @@ export async function verifyInvite(req: Request, res: Response): Promise { let { secret, username } = await InviteService.getByMailAndToken(mail, token); - const url = `otpauth://totp/FF Admin ${CLUB_NAME}?secret=${secret}`; + const url = `otpauth://totp/FF Admin ${SettingHelper.getSetting("club.name")}?secret=${secret}`; QRCode.toDataURL(url) .then((result) => { diff --git a/src/controller/resetController.ts b/src/controller/resetController.ts index fc0dc3e..3592639 100644 --- a/src/controller/resetController.ts +++ b/src/controller/resetController.ts @@ -1,6 +1,5 @@ import { Request, Response } from "express"; import { JWTHelper } from "../helpers/jwtHelper"; -import { JWTToken } from "../type/jwtTypes"; import InternalException from "../exceptions/internalException"; import RefreshCommandHandler from "../command/refreshCommandHandler"; import { CreateRefreshCommand } from "../command/refreshCommand"; @@ -12,12 +11,9 @@ import ResetCommandHandler from "../command/resetCommandHandler"; import MailHelper from "../helpers/mailHelper"; import ResetService from "../service/resetService"; import UserService from "../service/management/userService"; -import { CLUB_NAME } from "../env.defaults"; -import PermissionHelper from "../helpers/permissionHelper"; -import RolePermissionService from "../service/management/rolePermissionService"; -import UserPermissionService from "../service/management/userPermissionService"; import { UpdateUserSecretCommand } from "../command/management/user/userCommand"; import UserCommandHandler from "../command/management/user/userCommandHandler"; +import SettingHelper from "../helpers/settingsHelper"; /** * @description request totp reset @@ -31,7 +27,7 @@ export async function startReset(req: Request, res: Response): Promise { let { mail } = await UserService.getByUsername(username); - var secret = speakeasy.generateSecret({ length: 20, name: `FF Admin ${CLUB_NAME}` }); + var secret = speakeasy.generateSecret({ length: 20, name: `FF Admin ${SettingHelper.getSetting("club.name")}` }); let createReset: CreateResetCommand = { username: username, @@ -43,7 +39,7 @@ export async function startReset(req: Request, res: Response): Promise { // sendmail await MailHelper.sendMail( mail, - `Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`, + `Email Bestätigung für Mitglieder Admin-Portal von ${SettingHelper.getSetting("club.name")}`, `Öffne folgenden Link: ${origin}/reset/reset?mail=${mail}&token=${token}` ); @@ -62,7 +58,7 @@ export async function verifyReset(req: Request, res: Response): Promise { let { secret } = await ResetService.getByMailAndToken(mail, token); - const url = `otpauth://totp/FF Admin ${CLUB_NAME}?secret=${secret}`; + const url = `otpauth://totp/FF Admin ${SettingHelper.getSetting("club.name")}?secret=${secret}`; QRCode.toDataURL(url) .then((result) => { diff --git a/src/controller/userController.ts b/src/controller/userController.ts index 1e764f1..8827dc9 100644 --- a/src/controller/userController.ts +++ b/src/controller/userController.ts @@ -2,12 +2,12 @@ import { Request, Response } from "express"; import speakeasy from "speakeasy"; import QRCode from "qrcode"; import InternalException from "../exceptions/internalException"; -import { CLUB_NAME } from "../env.defaults"; import UserService from "../service/management/userService"; import UserFactory from "../factory/admin/management/user"; import { TransferUserOwnerCommand, UpdateUserCommand } from "../command/management/user/userCommand"; import UserCommandHandler from "../command/management/user/userCommandHandler"; import ForbiddenRequestException from "../exceptions/forbiddenRequestException"; +import SettingHelper from "../helpers/settingsHelper"; /** * @description get my by id @@ -33,7 +33,7 @@ export async function getMyTotp(req: Request, res: Response): Promise { let { secret } = await UserService.getById(userId); - const url = `otpauth://totp/FF Admin ${CLUB_NAME}?secret=${secret}`; + const url = `otpauth://totp/FF Admin ${SettingHelper.getSetting("club.name")}?secret=${secret}`; QRCode.toDataURL(url) .then((result) => { diff --git a/src/data-source.ts b/src/data-source.ts index d7be30d..d94d77f 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -1,7 +1,6 @@ import "dotenv/config"; import "reflect-metadata"; import { DataSource } from "typeorm"; -import { SettingHelper } from "./helpers/settingsHelper"; import { user } from "./entity/management/user"; import { refresh } from "./entity/refresh"; @@ -51,14 +50,17 @@ import { TemplatesAndProtocolSort1742549956787 } from "./migrations/174254995678 import { QueryToUUID1742922178643 } from "./migrations/1742922178643-queryToUUID"; import { NewsletterColumnType1744351418751 } from "./migrations/1744351418751-newsletterColumnType"; import { QueryUpdatedAt1744795756230 } from "./migrations/1744795756230-QueryUpdatedAt"; +import { setting } from "./entity/setting"; +import { SettingsFromEnv1745059495808 } from "./migrations/1745059495808-settingsFromEnv"; +import { DB_HOST, DB_NAME, DB_PASSWORD, DB_PORT, DB_TYPE, DB_USERNAME } from "./env.defaults"; const dataSource = new DataSource({ - type: SettingHelper.getEnvSetting("database.type") as any, - host: SettingHelper.getEnvSetting("database.host"), - port: Number(SettingHelper.getEnvSetting("database.port")), - username: SettingHelper.getEnvSetting("database.username"), - password: SettingHelper.getEnvSetting("database.password"), - database: SettingHelper.getEnvSetting("database.name"), + type: DB_TYPE as any, + host: DB_HOST, + port: DB_PORT, + username: DB_USERNAME, + password: DB_PASSWORD, + database: DB_NAME, synchronize: false, logging: process.env.NODE_ENV ? true : ["schema", "error", "warn", "log", "migration"], bigNumberStrings: false, @@ -103,6 +105,7 @@ const dataSource = new DataSource({ membershipView, webapi, webapiPermission, + setting, ], migrations: [ BackupAndResetDatabase1738166124200, @@ -111,6 +114,7 @@ const dataSource = new DataSource({ QueryToUUID1742922178643, NewsletterColumnType1744351418751, QueryUpdatedAt1744795756230, + SettingsFromEnv1745059495808, ], migrationsRun: true, migrationsTransactionMode: "each", diff --git a/src/entity/management/settings.ts b/src/entity/setting.ts similarity index 77% rename from src/entity/management/settings.ts rename to src/entity/setting.ts index 0057cbc..99d3cda 100644 --- a/src/entity/management/settings.ts +++ b/src/entity/setting.ts @@ -1,13 +1,13 @@ import { Column, Entity, PrimaryColumn } from "typeorm"; @Entity() -export class user { +export class setting { @PrimaryColumn({ type: "varchar", length: 255 }) topic: string; @PrimaryColumn({ type: "varchar", length: 255 }) key: string; - @Column({ type: "varchar", length: 255 }) + @Column({ type: "text" }) value: string; } diff --git a/src/env.defaults.ts b/src/env.defaults.ts index 97a3928..f7bc48d 100644 --- a/src/env.defaults.ts +++ b/src/env.defaults.ts @@ -2,22 +2,16 @@ import "dotenv/config"; import ms from "ms"; import ip from "ip"; -export const JWT_SECRET = process.env.JWT_SECRET ?? "my_jwt_secret_string_ilughfnadiuhgq§$IUZGFVRweiouarbt1oub3h5q4a"; -export const JWT_EXPIRATION = (process.env.JWT_EXPIRATION ?? "15m") as ms.StringValue; -export const REFRESH_EXPIRATION = (process.env.REFRESH_EXPIRATION ?? "1d") as ms.StringValue; -export const PWA_REFRESH_EXPIRATION = (process.env.PWA_REFRESH_EXPIRATION ?? "5d") as ms.StringValue; +export const DB_TYPE = process.env.DB_TYPE ?? "mysql"; +export const DB_HOST = process.env.DB_HOST ?? ""; +export const DB_PORT = Number(process.env.DB_PORT ?? 3306); +export const DB_NAME = process.env.DB_NAME ?? ""; +export const DB_USERNAME = process.env.DB_USERNAME ?? ""; +export const DB_PASSWORD = process.env.DB_PASSWORD ?? ""; -export const MAIL_USERNAME = process.env.MAIL_USERNAME ?? ""; -export const MAIL_PASSWORD = process.env.MAIL_PASSWORD ?? ""; -export const MAIL_HOST = process.env.MAIL_HOST ?? ""; -export const MAIL_PORT = Number(process.env.MAIL_PORT ?? "587"); -export const MAIL_SECURE = process.env.MAIL_SECURE ?? "false"; +export const SERVER_PORT = Number(process.env.SERVER_PORT ?? 5000); -export const CLUB_NAME = process.env.CLUB_NAME ?? "FF Admin"; -export const CLUB_WEBSITE = process.env.CLUB_WEBSITE ?? ""; - -export const BACKUP_INTERVAL = Number(process.env.BACKUP_INTERVAL ?? "1"); -export const BACKUP_COPIES = Number(process.env.BACKUP_COPIES ?? "7"); +export const APPLICATION_SECRET = process.env.APPLICATION_SECRET; export const USE_SECURITY_STRICT_LIMIT = process.env.USE_SECURITY_STRICT_LIMIT ?? "true"; export const SECURITY_STRICT_LIMIT_WINDOW = (process.env.SECURITY_STRICT_LIMIT_WINDOW ?? "15m") as ms.StringValue; @@ -45,25 +39,17 @@ export const TRUST_PROXY = ((): Array | string | boolean | number | null })(); export function configCheck() { - if (JWT_SECRET == "" || typeof JWT_SECRET != "string") throw new Error("set valid value to JWT_SECRET"); - checkMS(JWT_EXPIRATION, "JWT_EXPIRATION"); - checkMS(REFRESH_EXPIRATION, "REFRESH_EXPIRATION"); - checkMS(PWA_REFRESH_EXPIRATION, "PWA_REFRESH_EXPIRATION"); + if (DB_TYPE != "mysql" && DB_TYPE != "sqlite" && DB_TYPE != "postgres") + throw new Error("set valid value to DB_TYPE (mysql|sqlite|postgres)"); + if ((DB_HOST == "" || typeof DB_HOST != "string") && DB_TYPE != "sqlite") + throw new Error("set valid value to DB_HOST"); + if (DB_NAME == "" || typeof DB_NAME != "string") throw new Error("set valid value to DB_NAME"); + if ((DB_USERNAME == "" || typeof DB_USERNAME != "string") && DB_TYPE != "sqlite") + throw new Error("set valid value to DB_USERNAME"); + if ((DB_PASSWORD == "" || typeof DB_PASSWORD != "string") && DB_TYPE != "sqlite") + throw new Error("set valid value to DB_PASSWORD"); - if (MAIL_USERNAME == "" || typeof MAIL_USERNAME != "string") throw new Error("set valid value to MAIL_USERNAME"); - if (MAIL_PASSWORD == "" || typeof MAIL_PASSWORD != "string") throw new Error("set valid value to MAIL_PASSWORD"); - if (MAIL_HOST == "" || typeof MAIL_HOST != "string") throw new Error("set valid value to MAIL_HOST"); - if (isNaN(MAIL_PORT)) throw new Error("set valid numeric value to MAIL_PORT"); - if (MAIL_SECURE != "true" && MAIL_SECURE != "false") throw new Error("set 'true' or 'false' to MAIL_SECURE"); - - if ( - CLUB_WEBSITE != "" && - !/^(http(s):\/\/.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/.test(CLUB_WEBSITE) - ) - throw new Error("CLUB_WEBSITE is not valid url"); - - if (BACKUP_INTERVAL < 1) throw new Error("BACKUP_INTERVAL has to be at least 1"); - if (BACKUP_COPIES < 1) throw new Error("BACKUP_COPIES has to be at least 1"); + if (isNaN(SERVER_PORT)) throw new Error("set valid numeric value to SERVER_PORT"); if (USE_SECURITY_STRICT_LIMIT != "true" && USE_SECURITY_STRICT_LIMIT != "false") throw new Error("set 'true' or 'false' to USE_SECURITY_STRICT_LIMIT"); diff --git a/src/factory/admin/club/member/dateMappingHelper.ts b/src/factory/admin/club/member/dateMappingHelper.ts index e4b6a2c..8fdd072 100644 --- a/src/factory/admin/club/member/dateMappingHelper.ts +++ b/src/factory/admin/club/member/dateMappingHelper.ts @@ -1,8 +1,8 @@ -import { SettingHelper } from "../../../../helpers/settingsHelper"; +import { DB_TYPE } from "../../../../env.defaults"; export default abstract class DateMappingHelper { static mapDate(entry: any) { - switch (SettingHelper.getEnvSetting("database.type")) { + switch (DB_TYPE) { case "postgres": return `${entry?.years ?? 0} years ${entry?.months ?? 0} months ${entry?.days ?? 0} days`; case "mysql": diff --git a/src/helpers/backupHelper.ts b/src/helpers/backupHelper.ts index 319a298..76f7693 100644 --- a/src/helpers/backupHelper.ts +++ b/src/helpers/backupHelper.ts @@ -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({}); } } diff --git a/src/helpers/calendarHelper.ts b/src/helpers/calendarHelper.ts index 7418089..fb60496 100644 --- a/src/helpers/calendarHelper.ts +++ b/src/helpers/calendarHelper.ts @@ -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): { 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", diff --git a/src/helpers/codingHelper.ts b/src/helpers/codingHelper.ts new file mode 100644 index 0000000..e6a79f9 --- /dev/null +++ b/src/helpers/codingHelper.ts @@ -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 ""; + } + } +} diff --git a/src/helpers/jwtHelper.ts b/src/helpers/jwtHelper.ts index 5708ab8..066bb8b 100644 --- a/src/helpers/jwtHelper.ts +++ b/src/helpers/jwtHelper.ts @@ -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 { return new Promise((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((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(); diff --git a/src/helpers/mailHelper.ts b/src/helpers/mailHelper.ts index ab44a26..1bf8634 100644 --- a/src/helpers/mailHelper.ts +++ b/src/helpers/mailHelper.ts @@ -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, diff --git a/src/helpers/newsletterHelper.ts b/src/helpers/newsletterHelper.ts index 32cdd16..c09da94 100644 --- a/src/helpers/newsletterHelper.ts +++ b/src/helpers/newsletterHelper.ts @@ -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, diff --git a/src/helpers/settingsHelper.ts b/src/helpers/settingsHelper.ts index d176cc2..3652bce 100644 --- a/src/helpers/settingsHelper.ts +++ b/src/helpers/settingsHelper.ts @@ -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; + } } diff --git a/src/index.ts b/src/index.ts index 973f7bf..4aa928c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,9 +1,7 @@ import "dotenv/config"; import "./handlebars.config"; import express from "express"; -import { SettingHelper } from "./helpers/settingsHelper"; -SettingHelper.configurEnv(); import { configCheck } from "./env.defaults"; configCheck(); @@ -23,13 +21,15 @@ declare global { import { dataSource } from "./data-source"; import BackupHelper from "./helpers/backupHelper"; +import SettingHelper from "./helpers/settingsHelper"; dataSource.initialize().then(async () => { if (await dataSource.createQueryRunner().hasTable("user")) { await BackupHelper.autoRestoreBackup().catch((err) => { console.log(`${new Date().toISOString()}: failed auto-restoring database`, err); }); } - SettingHelper.configure(); + await SettingHelper.configure(); + MailHelper.createTransport(); }); const app = express(); @@ -43,6 +43,7 @@ app.listen(process.env.NODE_ENV ? process.env.SERVER_PORT ?? 5000 : 5000, () => import schedule from "node-schedule"; import RefreshCommandHandler from "./command/refreshCommandHandler"; +import MailHelper from "./helpers/mailHelper"; const job = schedule.scheduleJob("0 0 * * *", async () => { console.log(`${new Date().toISOString()}: running Cron`); await RefreshCommandHandler.deleteExpired(); diff --git a/src/migrations/1738166124200-BackupAndResetDatabase.ts b/src/migrations/1738166124200-BackupAndResetDatabase.ts index 30595da..f0656ba 100644 --- a/src/migrations/1738166124200-BackupAndResetDatabase.ts +++ b/src/migrations/1738166124200-BackupAndResetDatabase.ts @@ -2,16 +2,13 @@ import { MigrationInterface, QueryRunner, Table } from "typeorm"; import BackupHelper from "../helpers/backupHelper"; import { getDefaultByORM, getTypeByORM, isIncrementPrimary } from "./ormHelper"; import InternalException from "../exceptions/internalException"; -import { SettingHelper } from "../helpers/settingsHelper"; +import { DB_TYPE } from "../env.defaults"; export class BackupAndResetDatabase1738166124200 implements MigrationInterface { name = "BackupAndResetDatabase1738166124200"; public async up(queryRunner: QueryRunner): Promise { - let query = - SettingHelper.getEnvSetting("database.type") == "postgres" - ? "SELECT name FROM migrations" - : "SELECT `name` FROM `migrations`"; + let query = DB_TYPE == "postgres" ? "SELECT name FROM migrations" : "SELECT `name` FROM `migrations`"; let migrations = await queryRunner.query(query); if ( (await queryRunner.hasTable("user")) && diff --git a/src/migrations/1738166167472-CreateSchema.ts b/src/migrations/1738166167472-CreateSchema.ts index c9a4c8d..df9c6fd 100644 --- a/src/migrations/1738166167472-CreateSchema.ts +++ b/src/migrations/1738166167472-CreateSchema.ts @@ -54,7 +54,7 @@ import { newsletter_recipients_table, newsletter_table, } from "./baseSchemaTables/newsletter"; -import { SettingHelper } from "../helpers/settingsHelper"; +import { DB_TYPE } from "../env.defaults"; export class CreateSchema1738166167472 implements MigrationInterface { name = "CreateSchema1738166167472"; @@ -84,7 +84,6 @@ export class CreateSchema1738166167472 implements MigrationInterface { await queryRunner.createTable(member_executive_positions_table, true, true, true); await queryRunner.createTable(member_qualifications_table, true, true, true); - const DB_TYPE = SettingHelper.getEnvSetting("database.type"); if (DB_TYPE == "postgres") await queryRunner.createView(member_view_postgres, true); else if (DB_TYPE == "mysql") await queryRunner.createView(member_view_mysql, true); else if (DB_TYPE == "sqlite") await queryRunner.createView(member_view_sqlite, true); diff --git a/src/migrations/1745059495808-settingsFromEnv.ts b/src/migrations/1745059495808-settingsFromEnv.ts new file mode 100644 index 0000000..6cc8f56 --- /dev/null +++ b/src/migrations/1745059495808-settingsFromEnv.ts @@ -0,0 +1,30 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; +import { setting_table } from "./baseSchemaTables/admin"; +import { envSettingsType } from "../type/settingTypes"; +import SettingHelper from "../helpers/settingsHelper"; + +export class SettingsFromEnv1745059495808 implements MigrationInterface { + name = "SettingsFromEnv1745059495808"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable(setting_table, true, true, true); + + //transfer settings of env to database + await SettingHelper.setSetting("club.name", process.env.CLUB_NAME); + await SettingHelper.setSetting("club.website", process.env.CLUB_WEBSITE); + await SettingHelper.setSetting("session.jwt_expiration", process.env.JWT_EXPIRATION); + await SettingHelper.setSetting("session.refresh_expiration", process.env.REFRESH_EXPIRATION); + await SettingHelper.setSetting("session.pwa_refresh_expiration", process.env.PWA_REFRESH_EXPIRATION); + await SettingHelper.setSetting("mail.username", process.env.MAIL_USERNAME); + await SettingHelper.setSetting("mail.password", process.env.MAIL_PASSWORD); + await SettingHelper.setSetting("mail.host", process.env.MAIL_HOST); + await SettingHelper.setSetting("mail.port", process.env.MAIL_PORT); + await SettingHelper.setSetting("mail.secure", process.env.MAIL_SECURE); + await SettingHelper.setSetting("backup.interval", process.env.BACKUP_INTERVAL); + await SettingHelper.setSetting("backup.copies", process.env.BACKUP_COPIES); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable(setting_table.name, true, true, true); + } +} diff --git a/src/migrations/baseSchemaTables/admin.ts b/src/migrations/baseSchemaTables/admin.ts index c3eb94b..6ece9ad 100644 --- a/src/migrations/baseSchemaTables/admin.ts +++ b/src/migrations/baseSchemaTables/admin.ts @@ -148,3 +148,12 @@ export const reset_table = new Table({ { name: "secret", ...getTypeByORM("varchar") }, ], }); + +export const setting_table = new Table({ + name: "setting", + columns: [ + { name: "topic", ...getTypeByORM("varchar"), isPrimary: true }, + { name: "key", ...getTypeByORM("varchar"), isPrimary: true }, + { name: "value", ...getTypeByORM("text") }, + ], +}); diff --git a/src/migrations/ormHelper.ts b/src/migrations/ormHelper.ts index 099e006..8ebe5c9 100644 --- a/src/migrations/ormHelper.ts +++ b/src/migrations/ormHelper.ts @@ -1,4 +1,4 @@ -import { SettingHelper } from "../helpers/settingsHelper"; +import { DB_TYPE } from "../env.defaults"; export type ORMType = "int" | "bigint" | "boolean" | "date" | "datetime" | "time" | "text" | "varchar" | "uuid"; export type ORMDefault = "currentTimestamp" | "string" | "boolean" | "number" | "null"; @@ -15,7 +15,7 @@ export type Primary = { }; export function getTypeByORM(type: ORMType, nullable: boolean = false, length: number = 255): ColumnConfig { - const dbType = SettingHelper.getEnvSetting("database.type"); + const dbType = DB_TYPE; const typeMap: Record> = { mysql: { @@ -65,7 +65,7 @@ export function getTypeByORM(type: ORMType, nullable: boolean = false, length: n } export function getDefaultByORM(type: ORMDefault, data?: string | number | boolean): T { - const dbType = SettingHelper.getEnvSetting("database.type"); + const dbType = DB_TYPE; const typeMap: Record> = { mysql: { diff --git a/src/service/club/member/memberService.ts b/src/service/club/member/memberService.ts index eb791b3..cba1e47 100644 --- a/src/service/club/member/memberService.ts +++ b/src/service/club/member/memberService.ts @@ -1,11 +1,9 @@ import { Brackets, Like, SelectQueryBuilder } from "typeorm"; import { dataSource } from "../../../data-source"; import { member } from "../../../entity/club/member/member"; -import { membership } from "../../../entity/club/member/membership"; import DatabaseActionException from "../../../exceptions/databaseActionException"; -import InternalException from "../../../exceptions/internalException"; import { memberView } from "../../../views/memberView"; -import { SettingHelper } from "../../../helpers/settingsHelper"; +import { DB_TYPE } from "../../../env.defaults"; export default abstract class MemberService { /** @@ -169,7 +167,7 @@ export default abstract class MemberService { "member.firstMembershipEntry", "member.memberships", "membership_first", - SettingHelper.getEnvSetting("database.type") == "postgres" + DB_TYPE == "postgres" ? 'membership_first.memberId = member.id AND membership_first.start = (SELECT MIN("m_first"."start") FROM "membership" "m_first" WHERE "m_first"."memberId" = "member"."id")' : "membership_first.memberId = member.id AND membership_first.start = (SELECT MIN(m_first.start) FROM membership m_first WHERE m_first.memberId = member.id)" ) @@ -177,7 +175,7 @@ export default abstract class MemberService { "member.lastMembershipEntry", "member.memberships", "membership_last", - SettingHelper.getEnvSetting("database.type") == "postgres" + DB_TYPE == "postgres" ? 'membership_last.memberId = member.id AND membership_last.start = (SELECT MAX("m_last"."start") FROM "membership" "m_last" WHERE "m_last"."memberId" = "member"."id")' : "membership_last.memberId = member.id AND membership_last.start = (SELECT MAX(m_last.start) FROM membership m_last WHERE m_last.memberId = member.id)" ) diff --git a/src/service/settingService.ts b/src/service/settingService.ts new file mode 100644 index 0000000..d5f7701 --- /dev/null +++ b/src/service/settingService.ts @@ -0,0 +1,43 @@ +import { dataSource } from "../data-source"; +import { setting } from "../entity/setting"; +import InternalException from "../exceptions/internalException"; +import { SettingString } from "../type/settingTypes"; + +export default abstract class SettingService { + /** + * @description get settings + * @returns {Promise} + */ + static async getSettings(): Promise { + return await dataSource + .getRepository(setting) + .createQueryBuilder("setting") + .getMany() + .then((res) => { + return res; + }) + .catch((err) => { + throw new InternalException("setting not found", err); + }); + } + + /** + * @description get setting + * @param token SettingString + * @returns {Promise} + */ + static async getBySettingString(key: SettingString): Promise { + return await dataSource + .getRepository(setting) + .createQueryBuilder("setting") + .where("setting.topic = :topic", { topic: key.split(".")[0] }) + .andWhere("setting.key >= :key", { key: key.split(".")[1] }) + .getOneOrFail() + .then((res) => { + return res; + }) + .catch((err) => { + throw new InternalException("setting not found", err); + }); + } +} diff --git a/src/type/settingTypes.ts b/src/type/settingTypes.ts index 6c7935c..f1ada64 100644 --- a/src/type/settingTypes.ts +++ b/src/type/settingTypes.ts @@ -10,7 +10,6 @@ export type SettingString = | "club.website" | "app.custom_login_message" | "app.show_link_to_calendar" - | "session.jwt_secret" | "session.jwt_expiration" | "session.refresh_expiration" | "session.pwa_refresh_expiration" @@ -20,23 +19,17 @@ export type SettingString = | "mail.port" | "mail.secure" | "backup.interval" - | "backup.copies" - | "security.strict_limit" - | "security.strict_limit_window" - | "security.strict_limit_request_count" - | "security.limit" - | "security.limit_window" - | "security.limit_request_count" - | "security.trust_proxy"; + | "backup.copies"; export type SettingTypeAtom = "longstring" | "string" | "ms" | "number" | "boolean" | "url"; export type SettingType = SettingTypeAtom | `${SettingTypeAtom}/crypt` | `${SettingTypeAtom}/rand`; export const settingsType: { [key in SettingString]: { - type: SettingType | SettingType[]; + type: SettingType | SettingTypeAtom[]; default?: string | number | boolean | ms.StringValue; optional?: boolean; + min?: number; }; } = { "club.name": { type: "string", default: "FF Admin" }, @@ -45,7 +38,6 @@ export const settingsType: { "club.website": { type: "url", optional: true }, "app.custom_login_message": { type: "string", optional: true }, "app.show_link_to_calendar": { type: "boolean", default: true }, - "session.jwt_secret": { type: "longstring/rand", default: StringHelper.random(64) }, "session.jwt_expiration": { type: "ms", default: "15m" }, "session.refresh_expiration": { type: "ms", default: "1d" }, "session.pwa_refresh_expiration": { type: "ms", default: "5d" }, @@ -54,15 +46,8 @@ export const settingsType: { "mail.host": { type: "url", optional: false }, "mail.port": { type: "number", default: 587 }, "mail.secure": { type: "boolean", default: false }, - "backup.interval": { type: "number", default: 1 }, - "backup.copies": { type: "number", default: 7 }, - "security.strict_limit": { type: "boolean", default: true }, - "security.strict_limit_window": { type: "ms", default: "15m" }, - "security.strict_limit_request_count": { type: "number", default: 15 }, - "security.limit": { type: "boolean", default: true }, - "security.limit_window": { type: "ms", default: "1m" }, - "security.limit_request_count": { type: "number", default: 500 }, - "security.trust_proxy": { type: ["boolean", "number"], optional: true }, + "backup.interval": { type: "number", default: 1, min: 1 }, + "backup.copies": { type: "number", default: 7, min: 1 }, }; /** ENV Settings */ @@ -72,12 +57,20 @@ export type EnvSettingString = | "database.port" | "database.name" | "database.username" - | "database.password"; + | "database.password" + | "application.secret" + | "security.strict_limit" + | "security.strict_limit_window" + | "security.strict_limit_request_count" + | "security.limit" + | "security.limit_window" + | "security.limit_request_count" + | "security.trust_proxy"; export const envSettingsType: { [key in EnvSettingString]: { type: SettingType | SettingType[]; - default?: string; + default?: string | number | boolean; }; } = { "database.type": { type: "string", default: "postgres" }, @@ -86,4 +79,12 @@ export const envSettingsType: { "database.name": { type: "string" }, "database.username": { type: "string" }, "database.password": { type: "string" }, + "application.secret": { type: "string" }, + "security.strict_limit": { type: "boolean", default: true }, + "security.strict_limit_window": { type: "ms", default: "15m" }, + "security.strict_limit_request_count": { type: "number", default: 15 }, + "security.limit": { type: "boolean", default: true }, + "security.limit_window": { type: "ms", default: "1m" }, + "security.limit_request_count": { type: "number", default: 500 }, + "security.trust_proxy": { type: ["boolean", "number", "string"] }, }; diff --git a/src/views/memberExecutivePositionView.ts b/src/views/memberExecutivePositionView.ts index fb85148..8b8f328 100644 --- a/src/views/memberExecutivePositionView.ts +++ b/src/views/memberExecutivePositionView.ts @@ -1,11 +1,10 @@ import { DataSource, ViewColumn, ViewEntity } from "typeorm"; import { memberExecutivePositions } from "../entity/club/member/memberExecutivePositions"; -import { SettingHelper } from "../helpers/settingsHelper"; +import { DB_TYPE } from "../env.defaults"; let durationInDays: string; let durationInYears: string; let exactDuration: string; -const DB_TYPE = SettingHelper.getEnvSetting("database.type"); if (DB_TYPE == "postgres") { durationInDays = `SUM(COALESCE("memberExecutivePositions"."end", CURRENT_DATE) - "memberExecutivePositions"."start")`; durationInYears = `SUM(EXTRACT(YEAR FROM AGE(COALESCE("memberExecutivePositions"."end", CURRENT_DATE), "memberExecutivePositions"."start")))`; diff --git a/src/views/memberQualificationsView.ts b/src/views/memberQualificationsView.ts index 0d7c05b..6f88f39 100644 --- a/src/views/memberQualificationsView.ts +++ b/src/views/memberQualificationsView.ts @@ -1,11 +1,10 @@ import { DataSource, ViewColumn, ViewEntity } from "typeorm"; import { memberQualifications } from "../entity/club/member/memberQualifications"; -import { SettingHelper } from "../helpers/settingsHelper"; +import { DB_TYPE } from "../env.defaults"; let durationInDays: string; let durationInYears: string; let exactDuration: string; -const DB_TYPE = SettingHelper.getEnvSetting("database.type"); if (DB_TYPE == "postgres") { durationInDays = `SUM(COALESCE("memberQualifications"."end", CURRENT_DATE) - "memberQualifications"."start") `; durationInYears = `SUM(EXTRACT(YEAR FROM AGE(COALESCE("memberQualifications"."end", CURRENT_DATE), "memberQualifications"."start")))`; diff --git a/src/views/memberView.ts b/src/views/memberView.ts index 748d07e..194bade 100644 --- a/src/views/memberView.ts +++ b/src/views/memberView.ts @@ -1,11 +1,10 @@ import { DataSource, ViewColumn, ViewEntity } from "typeorm"; import { member } from "../entity/club/member/member"; -import { SettingHelper } from "../helpers/settingsHelper"; +import { DB_TYPE } from "../env.defaults"; let todayAge: string; let ageThisYear: string; let exactAge: string; -const DB_TYPE = SettingHelper.getEnvSetting("database.type"); if (DB_TYPE == "postgres") { todayAge = `DATE_PART('year', AGE(CURRENT_DATE, member.birthdate))`; ageThisYear = `EXTRACT(YEAR FROM CURRENT_DATE) - EXTRACT(YEAR FROM member.birthdate)`; diff --git a/src/views/membershipsView.ts b/src/views/membershipsView.ts index 4e21191..fc38c23 100644 --- a/src/views/membershipsView.ts +++ b/src/views/membershipsView.ts @@ -1,11 +1,10 @@ import { DataSource, ViewColumn, ViewEntity } from "typeorm"; import { membership } from "../entity/club/member/membership"; -import { SettingHelper } from "../helpers/settingsHelper"; +import { DB_TYPE } from "../env.defaults"; let durationInDays: string; let durationInYears: string; let exactDuration: string; -const DB_TYPE = SettingHelper.getEnvSetting("database.type"); if (DB_TYPE == "postgres") { durationInDays = `SUM(COALESCE("membership"."end", CURRENT_DATE) - "membership"."start") `; durationInYears = `SUM(EXTRACT(YEAR FROM AGE(COALESCE("membership"."end", CURRENT_DATE), "membership"."start")))`;