diff --git a/src/data-source.ts b/src/data-source.ts index 37f957d..d7be30d 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -1,7 +1,7 @@ import "dotenv/config"; import "reflect-metadata"; import { DataSource } from "typeorm"; -import { DB_HOST, DB_USERNAME, DB_PASSWORD, DB_NAME, DB_TYPE, DB_PORT } from "./env.defaults"; +import { SettingHelper } from "./helpers/settingsHelper"; import { user } from "./entity/management/user"; import { refresh } from "./entity/refresh"; @@ -53,12 +53,12 @@ import { NewsletterColumnType1744351418751 } from "./migrations/1744351418751-ne import { QueryUpdatedAt1744795756230 } from "./migrations/1744795756230-QueryUpdatedAt"; const dataSource = new DataSource({ - type: DB_TYPE as any, - host: DB_HOST, - port: DB_PORT, - username: DB_USERNAME, - password: DB_PASSWORD, - database: DB_NAME, + 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"), synchronize: false, logging: process.env.NODE_ENV ? true : ["schema", "error", "warn", "log", "migration"], bigNumberStrings: false, diff --git a/src/entity/management/settings.ts b/src/entity/management/settings.ts new file mode 100644 index 0000000..0057cbc --- /dev/null +++ b/src/entity/management/settings.ts @@ -0,0 +1,13 @@ +import { Column, Entity, PrimaryColumn } from "typeorm"; + +@Entity() +export class user { + @PrimaryColumn({ type: "varchar", length: 255 }) + topic: string; + + @PrimaryColumn({ type: "varchar", length: 255 }) + key: string; + + @Column({ type: "varchar", length: 255 }) + value: string; +} diff --git a/src/env.defaults.ts b/src/env.defaults.ts index 739fd87..97a3928 100644 --- a/src/env.defaults.ts +++ b/src/env.defaults.ts @@ -2,15 +2,6 @@ import "dotenv/config"; import ms from "ms"; import ip from "ip"; -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 SERVER_PORT = Number(process.env.SERVER_PORT ?? 5000); - 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; @@ -27,7 +18,6 @@ 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 BACKUP_AUTO_RESTORE = process.env.BACKUP_AUTO_RESTORE ?? "true"; 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; @@ -55,18 +45,6 @@ export const TRUST_PROXY = ((): Array | string | boolean | number | null })(); export function configCheck() { - 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 (isNaN(SERVER_PORT)) throw new Error("set valid numeric value to SERVER_PORT"); - 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"); @@ -84,8 +62,6 @@ export function configCheck() { ) throw new Error("CLUB_WEBSITE is not valid url"); - if (BACKUP_AUTO_RESTORE != "true" && BACKUP_AUTO_RESTORE != "false") - throw new Error("set 'true' or 'false' to BACKUP_AUTO_RESTORE"); 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"); diff --git a/src/factory/admin/club/member/dateMappingHelper.ts b/src/factory/admin/club/member/dateMappingHelper.ts index 8fdd072..e4b6a2c 100644 --- a/src/factory/admin/club/member/dateMappingHelper.ts +++ b/src/factory/admin/club/member/dateMappingHelper.ts @@ -1,8 +1,8 @@ -import { DB_TYPE } from "../../../../env.defaults"; +import { SettingHelper } from "../../../../helpers/settingsHelper"; export default abstract class DateMappingHelper { static mapDate(entry: any) { - switch (DB_TYPE) { + switch (SettingHelper.getEnvSetting("database.type")) { case "postgres": return `${entry?.years ?? 0} years ${entry?.months ?? 0} months ${entry?.days ?? 0} days`; case "mysql": diff --git a/src/helpers/settingsHelper.ts b/src/helpers/settingsHelper.ts new file mode 100644 index 0000000..d176cc2 --- /dev/null +++ b/src/helpers/settingsHelper.ts @@ -0,0 +1,57 @@ +import ms from "ms"; +import { EnvSettingString, envSettingsType, SettingString, settingsType } from "../type/settingTypes"; + +export 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 ?? ""; + } + + public static getEnvSetting(key: EnvSettingString): string { + let settingType = envSettingsType[key]; + return this.envSettings[key] ?? settingType.default ?? ""; + } + + public static async configure() {} + + 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(); + } + + 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)"); + if ( + this.checkIfEmptyOrNotString(this.envSettings["database.host"]) && + this.envSettings["database.type"] != "sqlite" + ) + throw new Error("set valid value to DB_HOST"); + if ( + this.checkIfEmptyOrNotString(this.envSettings["database.username"]) && + this.envSettings["database.type"] != "sqlite" + ) + throw new Error("set valid value to DB_USERNAME"); + if ( + this.checkIfEmptyOrNotString(this.envSettings["database.password"]) && + this.envSettings["database.type"] != "sqlite" + ) + throw new Error("set valid value to DB_PASSWORD"); + } + + private static checkIfEmptyOrNotString(val: any) { + return typeof val != "string" || val == ""; + } +} diff --git a/src/index.ts b/src/index.ts index a282863..973f7bf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,10 @@ import "dotenv/config"; import "./handlebars.config"; import express from "express"; +import { SettingHelper } from "./helpers/settingsHelper"; -import { BACKUP_AUTO_RESTORE, configCheck, SERVER_PORT } from "./env.defaults"; +SettingHelper.configurEnv(); +import { configCheck } from "./env.defaults"; configCheck(); import { PermissionObject } from "./type/permissionTypes"; @@ -22,18 +24,21 @@ declare global { import { dataSource } from "./data-source"; import BackupHelper from "./helpers/backupHelper"; dataSource.initialize().then(async () => { - if ((BACKUP_AUTO_RESTORE as "true" | "false") == "true" && (await dataSource.createQueryRunner().hasTable("user"))) { + if (await dataSource.createQueryRunner().hasTable("user")) { await BackupHelper.autoRestoreBackup().catch((err) => { console.log(`${new Date().toISOString()}: failed auto-restoring database`, err); }); } + SettingHelper.configure(); }); const app = express(); import router from "./routes/index"; router(app); -app.listen(process.env.NODE_ENV ? SERVER_PORT : 5000, () => { - console.log(`${new Date().toISOString()}: listening on port ${process.env.NODE_ENV ? SERVER_PORT : 5000}`); +app.listen(process.env.NODE_ENV ? process.env.SERVER_PORT ?? 5000 : 5000, () => { + console.log( + `${new Date().toISOString()}: listening on port ${process.env.NODE_ENV ? process.env.SERVER_PORT ?? 5000 : 5000}` + ); }); import schedule from "node-schedule"; diff --git a/src/migrations/1738166124200-BackupAndResetDatabase.ts b/src/migrations/1738166124200-BackupAndResetDatabase.ts index f0656ba..30595da 100644 --- a/src/migrations/1738166124200-BackupAndResetDatabase.ts +++ b/src/migrations/1738166124200-BackupAndResetDatabase.ts @@ -2,13 +2,16 @@ import { MigrationInterface, QueryRunner, Table } from "typeorm"; import BackupHelper from "../helpers/backupHelper"; import { getDefaultByORM, getTypeByORM, isIncrementPrimary } from "./ormHelper"; import InternalException from "../exceptions/internalException"; -import { DB_TYPE } from "../env.defaults"; +import { SettingHelper } from "../helpers/settingsHelper"; export class BackupAndResetDatabase1738166124200 implements MigrationInterface { name = "BackupAndResetDatabase1738166124200"; public async up(queryRunner: QueryRunner): Promise { - let query = DB_TYPE == "postgres" ? "SELECT name FROM migrations" : "SELECT `name` FROM `migrations`"; + let query = + SettingHelper.getEnvSetting("database.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 df9c6fd..c9a4c8d 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 { DB_TYPE } from "../env.defaults"; +import { SettingHelper } from "../helpers/settingsHelper"; export class CreateSchema1738166167472 implements MigrationInterface { name = "CreateSchema1738166167472"; @@ -84,6 +84,7 @@ 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/ormHelper.ts b/src/migrations/ormHelper.ts index 20a57ad..099e006 100644 --- a/src/migrations/ormHelper.ts +++ b/src/migrations/ormHelper.ts @@ -1,3 +1,5 @@ +import { SettingHelper } from "../helpers/settingsHelper"; + export type ORMType = "int" | "bigint" | "boolean" | "date" | "datetime" | "time" | "text" | "varchar" | "uuid"; export type ORMDefault = "currentTimestamp" | "string" | "boolean" | "number" | "null"; export type ColumnConfig = { @@ -13,7 +15,7 @@ export type Primary = { }; export function getTypeByORM(type: ORMType, nullable: boolean = false, length: number = 255): ColumnConfig { - const dbType = process.env.DB_TYPE; + const dbType = SettingHelper.getEnvSetting("database.type"); const typeMap: Record> = { mysql: { @@ -63,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 = process.env.DB_TYPE; + const dbType = SettingHelper.getEnvSetting("database.type"); const typeMap: Record> = { mysql: { diff --git a/src/service/club/member/memberService.ts b/src/service/club/member/memberService.ts index 3eae4e6..eb791b3 100644 --- a/src/service/club/member/memberService.ts +++ b/src/service/club/member/memberService.ts @@ -5,7 +5,7 @@ import { membership } from "../../../entity/club/member/membership"; import DatabaseActionException from "../../../exceptions/databaseActionException"; import InternalException from "../../../exceptions/internalException"; import { memberView } from "../../../views/memberView"; -import { DB_TYPE } from "../../../env.defaults"; +import { SettingHelper } from "../../../helpers/settingsHelper"; export default abstract class MemberService { /** @@ -169,7 +169,7 @@ export default abstract class MemberService { "member.firstMembershipEntry", "member.memberships", "membership_first", - DB_TYPE == "postgres" + SettingHelper.getEnvSetting("database.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 +177,7 @@ export default abstract class MemberService { "member.lastMembershipEntry", "member.memberships", "membership_last", - DB_TYPE == "postgres" + SettingHelper.getEnvSetting("database.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/type/settingTypes.ts b/src/type/settingTypes.ts new file mode 100644 index 0000000..6c7935c --- /dev/null +++ b/src/type/settingTypes.ts @@ -0,0 +1,89 @@ +import ms from "ms"; +import { StringHelper } from "../helpers/stringHelper"; + +export type SettingTopic = "club" | "app" | "session" | "mail" | "backup" | "security"; + +export type SettingString = + | "club.name" + | "club.imprint" + | "club.privacy" + | "club.website" + | "app.custom_login_message" + | "app.show_link_to_calendar" + | "session.jwt_secret" + | "session.jwt_expiration" + | "session.refresh_expiration" + | "session.pwa_refresh_expiration" + | "mail.username" + | "mail.password" + | "mail.host" + | "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"; + +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[]; + default?: string | number | boolean | ms.StringValue; + optional?: boolean; + }; +} = { + "club.name": { type: "string", default: "FF Admin" }, + "club.imprint": { type: "url", optional: true }, + "club.privacy": { type: "url", optional: true }, + "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" }, + "mail.username": { type: "string", optional: false }, + "mail.password": { type: "string/crypt", optional: false }, + "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 }, +}; + +/** ENV Settings */ +export type EnvSettingString = + | "database.type" + | "database.host" + | "database.port" + | "database.name" + | "database.username" + | "database.password"; + +export const envSettingsType: { + [key in EnvSettingString]: { + type: SettingType | SettingType[]; + default?: string; + }; +} = { + "database.type": { type: "string", default: "postgres" }, + "database.host": { type: "string" }, + "database.port": { type: "string", default: "5432" }, + "database.name": { type: "string" }, + "database.username": { type: "string" }, + "database.password": { type: "string" }, +}; diff --git a/src/views/memberExecutivePositionView.ts b/src/views/memberExecutivePositionView.ts index 8b8f328..fb85148 100644 --- a/src/views/memberExecutivePositionView.ts +++ b/src/views/memberExecutivePositionView.ts @@ -1,10 +1,11 @@ import { DataSource, ViewColumn, ViewEntity } from "typeorm"; import { memberExecutivePositions } from "../entity/club/member/memberExecutivePositions"; -import { DB_TYPE } from "../env.defaults"; +import { SettingHelper } from "../helpers/settingsHelper"; 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 6f88f39..0d7c05b 100644 --- a/src/views/memberQualificationsView.ts +++ b/src/views/memberQualificationsView.ts @@ -1,10 +1,11 @@ import { DataSource, ViewColumn, ViewEntity } from "typeorm"; import { memberQualifications } from "../entity/club/member/memberQualifications"; -import { DB_TYPE } from "../env.defaults"; +import { SettingHelper } from "../helpers/settingsHelper"; 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 194bade..748d07e 100644 --- a/src/views/memberView.ts +++ b/src/views/memberView.ts @@ -1,10 +1,11 @@ import { DataSource, ViewColumn, ViewEntity } from "typeorm"; import { member } from "../entity/club/member/member"; -import { DB_TYPE } from "../env.defaults"; +import { SettingHelper } from "../helpers/settingsHelper"; 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 fc38c23..4e21191 100644 --- a/src/views/membershipsView.ts +++ b/src/views/membershipsView.ts @@ -1,10 +1,11 @@ import { DataSource, ViewColumn, ViewEntity } from "typeorm"; import { membership } from "../entity/club/member/membership"; -import { DB_TYPE } from "../env.defaults"; +import { SettingHelper } from "../helpers/settingsHelper"; 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")))`;