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
19
.env.example
19
.env.example
|
@ -17,25 +17,10 @@ DB_PASSWORD = database_password
|
||||||
## BSP für sqlite
|
## BSP für sqlite
|
||||||
DB_HOST = filename.db
|
DB_HOST = filename.db
|
||||||
|
|
||||||
|
## Dev only
|
||||||
SERVER_PORT = portnumber
|
SERVER_PORT = portnumber
|
||||||
|
|
||||||
JWT_SECRET = ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 # besitzt default
|
APPLICATION_SECRET = mysecret
|
||||||
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
|
|
||||||
|
|
||||||
USE_SECURITY_STRICT_LIMIT = (true|false) # default ist true
|
USE_SECURITY_STRICT_LIMIT = (true|false) # default ist true
|
||||||
SECURITY_STRICT_LIMIT_WINDOW = [0-9]*(y|d|h|m|s) # default ist 15m
|
SECURITY_STRICT_LIMIT_WINDOW = [0-9]*(y|d|h|m|s) # default ist 15m
|
||||||
|
|
8
package-lock.json
generated
8
package-lock.json
generated
|
@ -10,6 +10,7 @@
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-rate-limit": "^7.5.0",
|
"express-rate-limit": "^7.5.0",
|
||||||
|
@ -1522,6 +1523,13 @@
|
||||||
"node": ">= 8"
|
"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": {
|
"node_modules/data-uri-to-buffer": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
"start_ts": "ts-node src/index.ts",
|
"start_ts": "ts-node src/index.ts",
|
||||||
"typeorm": "typeorm-ts-node-commonjs",
|
"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": "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",
|
"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",
|
"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",
|
"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",
|
"license": "AGPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"express-rate-limit": "^7.5.0",
|
"express-rate-limit": "^7.5.0",
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import { dataSource } from "../data-source";
|
import { dataSource } from "../data-source";
|
||||||
import { refresh } from "../entity/refresh";
|
import { refresh } from "../entity/refresh";
|
||||||
import { PWA_REFRESH_EXPIRATION, REFRESH_EXPIRATION } from "../env.defaults";
|
|
||||||
import DatabaseActionException from "../exceptions/databaseActionException";
|
import DatabaseActionException from "../exceptions/databaseActionException";
|
||||||
import InternalException from "../exceptions/internalException";
|
import SettingHelper from "../helpers/settingsHelper";
|
||||||
import { StringHelper } from "../helpers/stringHelper";
|
import { StringHelper } from "../helpers/stringHelper";
|
||||||
import UserService from "../service/management/userService";
|
|
||||||
import { CreateRefreshCommand, DeleteRefreshCommand } from "./refreshCommand";
|
import { CreateRefreshCommand, DeleteRefreshCommand } from "./refreshCommand";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
|
|
||||||
|
@ -25,8 +23,8 @@ export default abstract class RefreshCommandHandler {
|
||||||
token: refreshToken,
|
token: refreshToken,
|
||||||
userId: createRefresh.userId,
|
userId: createRefresh.userId,
|
||||||
expiry: createRefresh.isFromPwa
|
expiry: createRefresh.isFromPwa
|
||||||
? new Date(Date.now() + ms(PWA_REFRESH_EXPIRATION))
|
? new Date(Date.now() + ms(SettingHelper.getSetting("session.pwa_refresh_expiration") as ms.StringValue))
|
||||||
: new Date(Date.now() + ms(REFRESH_EXPIRATION)),
|
: new Date(Date.now() + ms(SettingHelper.getSetting("session.refresh_expiration") as ms.StringValue)),
|
||||||
})
|
})
|
||||||
.execute()
|
.execute()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
|
10
src/command/settingCommand.ts
Normal file
10
src/command/settingCommand.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
export interface CreateOrUpdateSettingCommand {
|
||||||
|
topic: string;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeleteSettingCommand {
|
||||||
|
topic: string;
|
||||||
|
key: string;
|
||||||
|
}
|
53
src/command/settingCommandHandler.ts
Normal file
53
src/command/settingCommandHandler.ts
Normal file
|
@ -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<string>}
|
||||||
|
*/
|
||||||
|
static async create(createSetting: CreateOrUpdateSettingCommand): Promise<string> {
|
||||||
|
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<any>}
|
||||||
|
*/
|
||||||
|
static async delete(deleteSetting: DeleteSettingCommand): Promise<any> {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,10 +11,10 @@ import {
|
||||||
} from "../../../command/management/user/userCommand";
|
} from "../../../command/management/user/userCommand";
|
||||||
import UserCommandHandler from "../../../command/management/user/userCommandHandler";
|
import UserCommandHandler from "../../../command/management/user/userCommandHandler";
|
||||||
import MailHelper from "../../../helpers/mailHelper";
|
import MailHelper from "../../../helpers/mailHelper";
|
||||||
import { CLUB_NAME } from "../../../env.defaults";
|
|
||||||
import { UpdateUserPermissionsCommand } from "../../../command/management/user/userPermissionCommand";
|
import { UpdateUserPermissionsCommand } from "../../../command/management/user/userPermissionCommand";
|
||||||
import UserPermissionCommandHandler from "../../../command/management/user/userPermissionCommandHandler";
|
import UserPermissionCommandHandler from "../../../command/management/user/userPermissionCommandHandler";
|
||||||
import BadRequestException from "../../../exceptions/badRequestException";
|
import BadRequestException from "../../../exceptions/badRequestException";
|
||||||
|
import SettingHelper from "../../../helpers/settingsHelper";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description get All users
|
* @description get All users
|
||||||
|
@ -157,7 +157,7 @@ export async function deleteUser(req: Request, res: Response): Promise<any> {
|
||||||
// sendmail
|
// sendmail
|
||||||
await MailHelper.sendMail(
|
await MailHelper.sendMail(
|
||||||
mail,
|
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.`
|
`Ihr Nutzerkonto des Adminportals wurde erfolgreich gelöscht.`
|
||||||
);
|
);
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
|
|
|
@ -12,8 +12,8 @@ import WebapiCommandHandler from "../../../command/management/webapi/webapiComma
|
||||||
import { UpdateWebapiPermissionsCommand } from "../../../command/management/webapi/webapiPermissionCommand";
|
import { UpdateWebapiPermissionsCommand } from "../../../command/management/webapi/webapiPermissionCommand";
|
||||||
import WebapiPermissionCommandHandler from "../../../command/management/webapi/webapiPermissionCommandHandler";
|
import WebapiPermissionCommandHandler from "../../../command/management/webapi/webapiPermissionCommandHandler";
|
||||||
import { JWTHelper } from "../../../helpers/jwtHelper";
|
import { JWTHelper } from "../../../helpers/jwtHelper";
|
||||||
import { CLUB_NAME } from "../../../env.defaults";
|
|
||||||
import { StringHelper } from "../../../helpers/stringHelper";
|
import { StringHelper } from "../../../helpers/stringHelper";
|
||||||
|
import SettingHelper from "../../../helpers/settingsHelper";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description get All apis
|
* @description get All apis
|
||||||
|
@ -78,7 +78,7 @@ export async function createWebapi(req: Request, res: Response): Promise<any> {
|
||||||
|
|
||||||
let token = await JWTHelper.create(
|
let token = await JWTHelper.create(
|
||||||
{
|
{
|
||||||
iss: CLUB_NAME,
|
iss: SettingHelper.getSetting("club.name") as string,
|
||||||
sub: "api_token_retrieve",
|
sub: "api_token_retrieve",
|
||||||
aud: StringHelper.random(32),
|
aud: StringHelper.random(32),
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { JWTHelper } from "../helpers/jwtHelper";
|
import { JWTHelper } from "../helpers/jwtHelper";
|
||||||
import { JWTToken } from "../type/jwtTypes";
|
|
||||||
import InternalException from "../exceptions/internalException";
|
import InternalException from "../exceptions/internalException";
|
||||||
import RefreshCommandHandler from "../command/refreshCommandHandler";
|
import RefreshCommandHandler from "../command/refreshCommandHandler";
|
||||||
import { CreateRefreshCommand } from "../command/refreshCommand";
|
import { CreateRefreshCommand } from "../command/refreshCommand";
|
||||||
|
@ -15,10 +14,8 @@ import MailHelper from "../helpers/mailHelper";
|
||||||
import InviteService from "../service/management/inviteService";
|
import InviteService from "../service/management/inviteService";
|
||||||
import UserService from "../service/management/userService";
|
import UserService from "../service/management/userService";
|
||||||
import CustomRequestException from "../exceptions/customRequestException";
|
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 InviteFactory from "../factory/admin/management/invite";
|
||||||
|
import SettingHelper from "../helpers/settingsHelper";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description get all invites
|
* @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");
|
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 = {
|
let createInvite: CreateInviteCommand = {
|
||||||
username: username,
|
username: username,
|
||||||
|
@ -73,7 +70,7 @@ export async function inviteUser(req: Request, res: Response, isInvite: boolean
|
||||||
// sendmail
|
// sendmail
|
||||||
await MailHelper.sendMail(
|
await MailHelper.sendMail(
|
||||||
mail,
|
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}`
|
`Ö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<any> {
|
||||||
|
|
||||||
let { secret, username } = await InviteService.getByMailAndToken(mail, token);
|
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)
|
QRCode.toDataURL(url)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { JWTHelper } from "../helpers/jwtHelper";
|
import { JWTHelper } from "../helpers/jwtHelper";
|
||||||
import { JWTToken } from "../type/jwtTypes";
|
|
||||||
import InternalException from "../exceptions/internalException";
|
import InternalException from "../exceptions/internalException";
|
||||||
import RefreshCommandHandler from "../command/refreshCommandHandler";
|
import RefreshCommandHandler from "../command/refreshCommandHandler";
|
||||||
import { CreateRefreshCommand } from "../command/refreshCommand";
|
import { CreateRefreshCommand } from "../command/refreshCommand";
|
||||||
|
@ -12,12 +11,9 @@ import ResetCommandHandler from "../command/resetCommandHandler";
|
||||||
import MailHelper from "../helpers/mailHelper";
|
import MailHelper from "../helpers/mailHelper";
|
||||||
import ResetService from "../service/resetService";
|
import ResetService from "../service/resetService";
|
||||||
import UserService from "../service/management/userService";
|
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 { UpdateUserSecretCommand } from "../command/management/user/userCommand";
|
||||||
import UserCommandHandler from "../command/management/user/userCommandHandler";
|
import UserCommandHandler from "../command/management/user/userCommandHandler";
|
||||||
|
import SettingHelper from "../helpers/settingsHelper";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description request totp reset
|
* @description request totp reset
|
||||||
|
@ -31,7 +27,7 @@ export async function startReset(req: Request, res: Response): Promise<any> {
|
||||||
|
|
||||||
let { mail } = await UserService.getByUsername(username);
|
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 = {
|
let createReset: CreateResetCommand = {
|
||||||
username: username,
|
username: username,
|
||||||
|
@ -43,7 +39,7 @@ export async function startReset(req: Request, res: Response): Promise<any> {
|
||||||
// sendmail
|
// sendmail
|
||||||
await MailHelper.sendMail(
|
await MailHelper.sendMail(
|
||||||
mail,
|
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}`
|
`Öffne folgenden Link: ${origin}/reset/reset?mail=${mail}&token=${token}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -62,7 +58,7 @@ export async function verifyReset(req: Request, res: Response): Promise<any> {
|
||||||
|
|
||||||
let { secret } = await ResetService.getByMailAndToken(mail, token);
|
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)
|
QRCode.toDataURL(url)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
|
|
@ -2,12 +2,12 @@ import { Request, Response } from "express";
|
||||||
import speakeasy from "speakeasy";
|
import speakeasy from "speakeasy";
|
||||||
import QRCode from "qrcode";
|
import QRCode from "qrcode";
|
||||||
import InternalException from "../exceptions/internalException";
|
import InternalException from "../exceptions/internalException";
|
||||||
import { CLUB_NAME } from "../env.defaults";
|
|
||||||
import UserService from "../service/management/userService";
|
import UserService from "../service/management/userService";
|
||||||
import UserFactory from "../factory/admin/management/user";
|
import UserFactory from "../factory/admin/management/user";
|
||||||
import { TransferUserOwnerCommand, UpdateUserCommand } from "../command/management/user/userCommand";
|
import { TransferUserOwnerCommand, UpdateUserCommand } from "../command/management/user/userCommand";
|
||||||
import UserCommandHandler from "../command/management/user/userCommandHandler";
|
import UserCommandHandler from "../command/management/user/userCommandHandler";
|
||||||
import ForbiddenRequestException from "../exceptions/forbiddenRequestException";
|
import ForbiddenRequestException from "../exceptions/forbiddenRequestException";
|
||||||
|
import SettingHelper from "../helpers/settingsHelper";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description get my by id
|
* @description get my by id
|
||||||
|
@ -33,7 +33,7 @@ export async function getMyTotp(req: Request, res: Response): Promise<any> {
|
||||||
|
|
||||||
let { secret } = await UserService.getById(userId);
|
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)
|
QRCode.toDataURL(url)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import "reflect-metadata";
|
import "reflect-metadata";
|
||||||
import { DataSource } from "typeorm";
|
import { DataSource } from "typeorm";
|
||||||
import { SettingHelper } from "./helpers/settingsHelper";
|
|
||||||
|
|
||||||
import { user } from "./entity/management/user";
|
import { user } from "./entity/management/user";
|
||||||
import { refresh } from "./entity/refresh";
|
import { refresh } from "./entity/refresh";
|
||||||
|
@ -51,14 +50,17 @@ import { TemplatesAndProtocolSort1742549956787 } from "./migrations/174254995678
|
||||||
import { QueryToUUID1742922178643 } from "./migrations/1742922178643-queryToUUID";
|
import { QueryToUUID1742922178643 } from "./migrations/1742922178643-queryToUUID";
|
||||||
import { NewsletterColumnType1744351418751 } from "./migrations/1744351418751-newsletterColumnType";
|
import { NewsletterColumnType1744351418751 } from "./migrations/1744351418751-newsletterColumnType";
|
||||||
import { QueryUpdatedAt1744795756230 } from "./migrations/1744795756230-QueryUpdatedAt";
|
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({
|
const dataSource = new DataSource({
|
||||||
type: SettingHelper.getEnvSetting("database.type") as any,
|
type: DB_TYPE as any,
|
||||||
host: SettingHelper.getEnvSetting("database.host"),
|
host: DB_HOST,
|
||||||
port: Number(SettingHelper.getEnvSetting("database.port")),
|
port: DB_PORT,
|
||||||
username: SettingHelper.getEnvSetting("database.username"),
|
username: DB_USERNAME,
|
||||||
password: SettingHelper.getEnvSetting("database.password"),
|
password: DB_PASSWORD,
|
||||||
database: SettingHelper.getEnvSetting("database.name"),
|
database: DB_NAME,
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
logging: process.env.NODE_ENV ? true : ["schema", "error", "warn", "log", "migration"],
|
logging: process.env.NODE_ENV ? true : ["schema", "error", "warn", "log", "migration"],
|
||||||
bigNumberStrings: false,
|
bigNumberStrings: false,
|
||||||
|
@ -103,6 +105,7 @@ const dataSource = new DataSource({
|
||||||
membershipView,
|
membershipView,
|
||||||
webapi,
|
webapi,
|
||||||
webapiPermission,
|
webapiPermission,
|
||||||
|
setting,
|
||||||
],
|
],
|
||||||
migrations: [
|
migrations: [
|
||||||
BackupAndResetDatabase1738166124200,
|
BackupAndResetDatabase1738166124200,
|
||||||
|
@ -111,6 +114,7 @@ const dataSource = new DataSource({
|
||||||
QueryToUUID1742922178643,
|
QueryToUUID1742922178643,
|
||||||
NewsletterColumnType1744351418751,
|
NewsletterColumnType1744351418751,
|
||||||
QueryUpdatedAt1744795756230,
|
QueryUpdatedAt1744795756230,
|
||||||
|
SettingsFromEnv1745059495808,
|
||||||
],
|
],
|
||||||
migrationsRun: true,
|
migrationsRun: true,
|
||||||
migrationsTransactionMode: "each",
|
migrationsTransactionMode: "each",
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { Column, Entity, PrimaryColumn } from "typeorm";
|
import { Column, Entity, PrimaryColumn } from "typeorm";
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class user {
|
export class setting {
|
||||||
@PrimaryColumn({ type: "varchar", length: 255 })
|
@PrimaryColumn({ type: "varchar", length: 255 })
|
||||||
topic: string;
|
topic: string;
|
||||||
|
|
||||||
@PrimaryColumn({ type: "varchar", length: 255 })
|
@PrimaryColumn({ type: "varchar", length: 255 })
|
||||||
key: string;
|
key: string;
|
||||||
|
|
||||||
@Column({ type: "varchar", length: 255 })
|
@Column({ type: "text" })
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
|
@ -2,22 +2,16 @@ import "dotenv/config";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
import ip from "ip";
|
import ip from "ip";
|
||||||
|
|
||||||
export const JWT_SECRET = process.env.JWT_SECRET ?? "my_jwt_secret_string_ilughfnadiuhgq§$IUZGFVRweiouarbt1oub3h5q4a";
|
export const DB_TYPE = process.env.DB_TYPE ?? "mysql";
|
||||||
export const JWT_EXPIRATION = (process.env.JWT_EXPIRATION ?? "15m") as ms.StringValue;
|
export const DB_HOST = process.env.DB_HOST ?? "";
|
||||||
export const REFRESH_EXPIRATION = (process.env.REFRESH_EXPIRATION ?? "1d") as ms.StringValue;
|
export const DB_PORT = Number(process.env.DB_PORT ?? 3306);
|
||||||
export const PWA_REFRESH_EXPIRATION = (process.env.PWA_REFRESH_EXPIRATION ?? "5d") as ms.StringValue;
|
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 SERVER_PORT = Number(process.env.SERVER_PORT ?? 5000);
|
||||||
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 CLUB_NAME = process.env.CLUB_NAME ?? "FF Admin";
|
export const APPLICATION_SECRET = process.env.APPLICATION_SECRET;
|
||||||
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 USE_SECURITY_STRICT_LIMIT = process.env.USE_SECURITY_STRICT_LIMIT ?? "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;
|
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> | string | boolean | number | null
|
||||||
})();
|
})();
|
||||||
|
|
||||||
export function configCheck() {
|
export function configCheck() {
|
||||||
if (JWT_SECRET == "" || typeof JWT_SECRET != "string") throw new Error("set valid value to JWT_SECRET");
|
if (DB_TYPE != "mysql" && DB_TYPE != "sqlite" && DB_TYPE != "postgres")
|
||||||
checkMS(JWT_EXPIRATION, "JWT_EXPIRATION");
|
throw new Error("set valid value to DB_TYPE (mysql|sqlite|postgres)");
|
||||||
checkMS(REFRESH_EXPIRATION, "REFRESH_EXPIRATION");
|
if ((DB_HOST == "" || typeof DB_HOST != "string") && DB_TYPE != "sqlite")
|
||||||
checkMS(PWA_REFRESH_EXPIRATION, "PWA_REFRESH_EXPIRATION");
|
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 (isNaN(SERVER_PORT)) throw new Error("set valid numeric value to SERVER_PORT");
|
||||||
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 (USE_SECURITY_STRICT_LIMIT != "true" && USE_SECURITY_STRICT_LIMIT != "false")
|
if (USE_SECURITY_STRICT_LIMIT != "true" && USE_SECURITY_STRICT_LIMIT != "false")
|
||||||
throw new Error("set 'true' or 'false' to USE_SECURITY_STRICT_LIMIT");
|
throw new Error("set 'true' or 'false' to USE_SECURITY_STRICT_LIMIT");
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { SettingHelper } from "../../../../helpers/settingsHelper";
|
import { DB_TYPE } from "../../../../env.defaults";
|
||||||
|
|
||||||
export default abstract class DateMappingHelper {
|
export default abstract class DateMappingHelper {
|
||||||
static mapDate(entry: any) {
|
static mapDate(entry: any) {
|
||||||
switch (SettingHelper.getEnvSetting("database.type")) {
|
switch (DB_TYPE) {
|
||||||
case "postgres":
|
case "postgres":
|
||||||
return `${entry?.years ?? 0} years ${entry?.months ?? 0} months ${entry?.days ?? 0} days`;
|
return `${entry?.years ?? 0} years ${entry?.months ?? 0} months ${entry?.days ?? 0} days`;
|
||||||
case "mysql":
|
case "mysql":
|
||||||
|
|
|
@ -4,9 +4,9 @@ import { EntityManager } from "typeorm";
|
||||||
import uniqBy from "lodash.uniqby";
|
import uniqBy from "lodash.uniqby";
|
||||||
import InternalException from "../exceptions/internalException";
|
import InternalException from "../exceptions/internalException";
|
||||||
import UserService from "../service/management/userService";
|
import UserService from "../service/management/userService";
|
||||||
import { BACKUP_COPIES, BACKUP_INTERVAL } from "../env.defaults";
|
|
||||||
import DatabaseActionException from "../exceptions/databaseActionException";
|
import DatabaseActionException from "../exceptions/databaseActionException";
|
||||||
import { availableTemplates } from "../type/templateTypes";
|
import { availableTemplates } from "../type/templateTypes";
|
||||||
|
import SettingHelper from "./settingsHelper";
|
||||||
|
|
||||||
export type BackupSection =
|
export type BackupSection =
|
||||||
| "member"
|
| "member"
|
||||||
|
@ -103,7 +103,7 @@ export default abstract class BackupHelper {
|
||||||
let files = FileSystemHelper.getFilesInDirectory("backup", ".json");
|
let files = FileSystemHelper.getFilesInDirectory("backup", ".json");
|
||||||
let sorted = files.sort((a, b) => new Date(b.split(".")[0]).getTime() - new Date(a.split(".")[0]).getTime());
|
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) {
|
for (const file of filesToDelete) {
|
||||||
FileSystemHelper.deleteFile("backup", file);
|
FileSystemHelper.deleteFile("backup", file);
|
||||||
}
|
}
|
||||||
|
@ -117,7 +117,7 @@ export default abstract class BackupHelper {
|
||||||
let diffInMs = new Date().getTime() - lastBackup.getTime();
|
let diffInMs = new Date().getTime() - lastBackup.getTime();
|
||||||
let diffInDays = diffInMs / (1000 * 60 * 60 * 24);
|
let diffInDays = diffInMs / (1000 * 60 * 60 * 24);
|
||||||
|
|
||||||
if (diffInDays >= BACKUP_INTERVAL) {
|
if (diffInDays >= (SettingHelper.getSetting("backup.interval") as number)) {
|
||||||
await this.createBackup({});
|
await this.createBackup({});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { createEvents } from "ics";
|
import { createEvents } from "ics";
|
||||||
import { calendar } from "../entity/club/calendar";
|
import { calendar } from "../entity/club/calendar";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { CLUB_NAME, CLUB_WEBSITE, MAIL_USERNAME } from "../env.defaults";
|
import SettingHelper from "./settingsHelper";
|
||||||
|
|
||||||
export abstract class CalendarHelper {
|
export abstract class CalendarHelper {
|
||||||
public static buildICS(entries: Array<calendar>): { error?: Error; value?: string } {
|
public static buildICS(entries: Array<calendar>): { error?: Error; value?: string } {
|
||||||
|
@ -35,7 +35,10 @@ export abstract class CalendarHelper {
|
||||||
description: i.content,
|
description: i.content,
|
||||||
location: i.location,
|
location: i.location,
|
||||||
categories: [i.type.type],
|
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)
|
created: moment(i.createdAt)
|
||||||
.format("YYYY-M-D-H-m")
|
.format("YYYY-M-D-H-m")
|
||||||
.split("-")
|
.split("-")
|
||||||
|
@ -46,7 +49,9 @@ export abstract class CalendarHelper {
|
||||||
.map((a) => parseInt(a)) as [number, number, number, number, number],
|
.map((a) => parseInt(a)) as [number, number, number, number, number],
|
||||||
transp: "OPAQUE" as "OPAQUE",
|
transp: "OPAQUE" as "OPAQUE",
|
||||||
status: "CONFIRMED",
|
status: "CONFIRMED",
|
||||||
...(CLUB_WEBSITE != "" ? { url: CLUB_WEBSITE } : {}),
|
...(SettingHelper.getSetting("club.website") != ""
|
||||||
|
? { url: SettingHelper.getSetting("club.website") as string }
|
||||||
|
: {}),
|
||||||
alarms: [
|
alarms: [
|
||||||
{
|
{
|
||||||
action: "display",
|
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 jwt from "jsonwebtoken";
|
||||||
import { JWTData, JWTToken } from "../type/jwtTypes";
|
import { JWTData, JWTToken } from "../type/jwtTypes";
|
||||||
import { JWT_SECRET, JWT_EXPIRATION } from "../env.defaults";
|
|
||||||
import InternalException from "../exceptions/internalException";
|
import InternalException from "../exceptions/internalException";
|
||||||
import RolePermissionService from "../service/management/rolePermissionService";
|
import RolePermissionService from "../service/management/rolePermissionService";
|
||||||
import UserPermissionService from "../service/management/userPermissionService";
|
import UserPermissionService from "../service/management/userPermissionService";
|
||||||
|
@ -9,11 +8,13 @@ import PermissionHelper from "./permissionHelper";
|
||||||
import WebapiService from "../service/management/webapiService";
|
import WebapiService from "../service/management/webapiService";
|
||||||
import WebapiPermissionService from "../service/management/webapiPermissionService";
|
import WebapiPermissionService from "../service/management/webapiPermissionService";
|
||||||
import ms from "ms";
|
import ms from "ms";
|
||||||
|
import SettingHelper from "./settingsHelper";
|
||||||
|
import { APPLICATION_SECRET } from "../env.defaults";
|
||||||
|
|
||||||
export abstract class JWTHelper {
|
export abstract class JWTHelper {
|
||||||
static validate(token: string): Promise<string | jwt.JwtPayload> {
|
static validate(token: string): Promise<string | jwt.JwtPayload> {
|
||||||
return new Promise<string | jwt.JwtPayload>((resolve, reject) => {
|
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);
|
if (err) reject(err.message);
|
||||||
else resolve(decoded);
|
else resolve(decoded);
|
||||||
});
|
});
|
||||||
|
@ -27,9 +28,11 @@ export abstract class JWTHelper {
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
jwt.sign(
|
jwt.sign(
|
||||||
data,
|
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) => {
|
(err, token) => {
|
||||||
if (err) reject(err.message);
|
if (err) reject(err.message);
|
||||||
|
@ -100,7 +103,8 @@ export abstract class JWTHelper {
|
||||||
};
|
};
|
||||||
|
|
||||||
let overwriteExpiration =
|
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
|
? null
|
||||||
: Date.now() - new Date(expiration).getTime();
|
: Date.now() - new Date(expiration).getTime();
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,21 @@
|
||||||
import { Transporter, createTransport, TransportOptions } from "nodemailer";
|
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 { Attachment } from "nodemailer/lib/mailer";
|
||||||
|
import SettingHelper from "./settingsHelper";
|
||||||
|
|
||||||
export default abstract class MailHelper {
|
export default abstract class MailHelper {
|
||||||
private static readonly transporter: Transporter = createTransport({
|
private static transporter: Transporter;
|
||||||
host: MAIL_HOST,
|
|
||||||
port: MAIL_PORT,
|
static createTransport() {
|
||||||
secure: (MAIL_SECURE as "true" | "false") == "true",
|
this.transporter = createTransport({
|
||||||
auth: {
|
host: SettingHelper.getSetting("mail.host"),
|
||||||
user: MAIL_USERNAME,
|
port: SettingHelper.getSetting("mail.port"),
|
||||||
pass: MAIL_PASSWORD,
|
secure: SettingHelper.getSetting("mail.secure") as boolean,
|
||||||
},
|
auth: {
|
||||||
} as TransportOptions);
|
user: SettingHelper.getSetting("mail.username"),
|
||||||
|
pass: SettingHelper.getSetting("mail.password"),
|
||||||
|
},
|
||||||
|
} as TransportOptions);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description send mail
|
* @description send mail
|
||||||
|
@ -29,7 +33,7 @@ export default abstract class MailHelper {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.transporter
|
this.transporter
|
||||||
.sendMail({
|
.sendMail({
|
||||||
from: `"${CLUB_NAME}" <${MAIL_USERNAME}>`,
|
from: `"${SettingHelper.getSetting("club.name")}" <${SettingHelper.getSetting("mail.username")}>`,
|
||||||
to: target,
|
to: target,
|
||||||
subject,
|
subject,
|
||||||
text: content,
|
text: content,
|
||||||
|
|
|
@ -10,13 +10,13 @@ import { CalendarHelper } from "./calendarHelper";
|
||||||
import DynamicQueryBuilder from "./dynamicQueryBuilder";
|
import DynamicQueryBuilder from "./dynamicQueryBuilder";
|
||||||
import { FileSystemHelper } from "./fileSystemHelper";
|
import { FileSystemHelper } from "./fileSystemHelper";
|
||||||
import MailHelper from "./mailHelper";
|
import MailHelper from "./mailHelper";
|
||||||
import { CLUB_NAME } from "../env.defaults";
|
|
||||||
import { TemplateHelper } from "./templateHelper";
|
import { TemplateHelper } from "./templateHelper";
|
||||||
import { PdfExport } from "./pdfExport";
|
import { PdfExport } from "./pdfExport";
|
||||||
import NewsletterConfigService from "../service/configuration/newsletterConfigService";
|
import NewsletterConfigService from "../service/configuration/newsletterConfigService";
|
||||||
import { NewsletterConfigEnum } from "../enums/newsletterConfigEnum";
|
import { NewsletterConfigEnum } from "../enums/newsletterConfigEnum";
|
||||||
import InternalException from "../exceptions/internalException";
|
import InternalException from "../exceptions/internalException";
|
||||||
import EventEmitter from "events";
|
import EventEmitter from "events";
|
||||||
|
import SettingHelper from "./settingsHelper";
|
||||||
|
|
||||||
export interface NewsletterEventType {
|
export interface NewsletterEventType {
|
||||||
kind: "pdf" | "mail";
|
kind: "pdf" | "mail";
|
||||||
|
@ -179,7 +179,7 @@ export abstract class NewsletterHelper {
|
||||||
pdfRecipients.unshift({
|
pdfRecipients.unshift({
|
||||||
id: "0",
|
id: "0",
|
||||||
firstname: "Alle Mitglieder",
|
firstname: "Alle Mitglieder",
|
||||||
lastname: CLUB_NAME,
|
lastname: SettingHelper.getSetting("club.name"),
|
||||||
nameaffix: "",
|
nameaffix: "",
|
||||||
salutation: { salutation: "" },
|
salutation: { salutation: "" },
|
||||||
} as member);
|
} as member);
|
||||||
|
@ -221,11 +221,14 @@ export abstract class NewsletterHelper {
|
||||||
const { body } = await TemplateHelper.renderFileForModule({
|
const { body } = await TemplateHelper.renderFileForModule({
|
||||||
module: "newsletter",
|
module: "newsletter",
|
||||||
bodyData: data,
|
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, [
|
await MailHelper.sendMail(
|
||||||
{ filename: "events.ics", path: this.getICSFilePath(newsletter) },
|
rec.sendNewsletter.email,
|
||||||
])
|
`Newsletter von ${SettingHelper.getSetting("club.name")}`,
|
||||||
|
body,
|
||||||
|
[{ filename: "events.ics", path: this.getICSFilePath(newsletter) }]
|
||||||
|
)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.formatJobEmit(
|
this.formatJobEmit(
|
||||||
"progress",
|
"progress",
|
||||||
|
@ -286,7 +289,7 @@ export abstract class NewsletterHelper {
|
||||||
|
|
||||||
await PdfExport.renderFile({
|
await PdfExport.renderFile({
|
||||||
template: "newsletter",
|
template: "newsletter",
|
||||||
title: `Newsletter von ${CLUB_NAME}`,
|
title: `Newsletter von ${SettingHelper.getSetting("club.name")}`,
|
||||||
filename: `${rec.lastname}_${rec.firstname}_${rec.id}`.replaceAll(" ", "-"),
|
filename: `${rec.lastname}_${rec.firstname}_${rec.id}`.replaceAll(" ", "-"),
|
||||||
folder: `newsletter/${newsletter.id}_${newsletter.title.replaceAll(" ", "")}`,
|
folder: `newsletter/${newsletter.id}_${newsletter.title.replaceAll(" ", "")}`,
|
||||||
data: data,
|
data: data,
|
||||||
|
|
|
@ -1,57 +1,135 @@
|
||||||
|
import { SettingString, settingsType } from "../type/settingTypes";
|
||||||
import ms from "ms";
|
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 settings: { [key in SettingString]?: string } = {};
|
||||||
private static envSettings: { [key in EnvSettingString]?: string } = {};
|
|
||||||
|
|
||||||
public static getSetting(key: SettingString): string | number | boolean | ms.StringValue {
|
public static getSetting(key: SettingString): string | number | boolean | ms.StringValue {
|
||||||
let settingType = settingsType[key];
|
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 {
|
public static async setSetting(key: SettingString, value: string) {
|
||||||
let settingType = envSettingsType[key];
|
if (value == undefined || value == null) return;
|
||||||
return this.envSettings[key] ?? settingType.default ?? "";
|
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() {
|
await SettingCommandHandler.delete({
|
||||||
this.envSettings = {
|
topic: key.split(".")[0],
|
||||||
"database.type": process.env.DB_TYPE,
|
key: key.split(".")[1],
|
||||||
"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() {
|
public static async configure() {
|
||||||
if (!["mysql", "sqlite", "postgres"].includes(this.envSettings["database.type"]))
|
console.log("Configured Settings");
|
||||||
throw new Error("set valid value to DB_TYPE (mysql|sqlite|postgres)");
|
let settings = await SettingService.getSettings();
|
||||||
if (this.checkIfEmptyOrNotString(this.envSettings["database.name"]))
|
|
||||||
throw new Error("set valid value to DB_NAME (name of database or filepath for sqlite)");
|
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 (
|
if (
|
||||||
this.checkIfEmptyOrNotString(this.envSettings["database.host"]) &&
|
!Array.isArray(settingType.type) &&
|
||||||
this.envSettings["database.type"] != "sqlite"
|
settingType.type.startsWith("string") &&
|
||||||
)
|
this.checkIfEmptyOrNotString(value)
|
||||||
throw new Error("set valid value to DB_HOST");
|
) {
|
||||||
|
throw new Error(`set valid value to ${key}`);
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
this.checkIfEmptyOrNotString(this.envSettings["database.username"]) &&
|
!Array.isArray(settingType.type) &&
|
||||||
this.envSettings["database.type"] != "sqlite"
|
settingType.type.startsWith("ms") &&
|
||||||
)
|
this.checkIfNotMS(value as ms.StringValue)
|
||||||
throw new Error("set valid value to DB_USERNAME");
|
) {
|
||||||
|
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 (
|
if (
|
||||||
this.checkIfEmptyOrNotString(this.envSettings["database.password"]) &&
|
!Array.isArray(settingType.type) &&
|
||||||
this.envSettings["database.type"] != "sqlite"
|
settingType.type.startsWith("number") &&
|
||||||
)
|
settingType.min &&
|
||||||
throw new Error("set valid value to DB_PASSWORD");
|
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) {
|
private static checkIfEmptyOrNotString(val: any) {
|
||||||
return typeof val != "string" || val == "";
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import "./handlebars.config";
|
import "./handlebars.config";
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { SettingHelper } from "./helpers/settingsHelper";
|
|
||||||
|
|
||||||
SettingHelper.configurEnv();
|
|
||||||
import { configCheck } from "./env.defaults";
|
import { configCheck } from "./env.defaults";
|
||||||
configCheck();
|
configCheck();
|
||||||
|
|
||||||
|
@ -23,13 +21,15 @@ declare global {
|
||||||
|
|
||||||
import { dataSource } from "./data-source";
|
import { dataSource } from "./data-source";
|
||||||
import BackupHelper from "./helpers/backupHelper";
|
import BackupHelper from "./helpers/backupHelper";
|
||||||
|
import SettingHelper from "./helpers/settingsHelper";
|
||||||
dataSource.initialize().then(async () => {
|
dataSource.initialize().then(async () => {
|
||||||
if (await dataSource.createQueryRunner().hasTable("user")) {
|
if (await dataSource.createQueryRunner().hasTable("user")) {
|
||||||
await BackupHelper.autoRestoreBackup().catch((err) => {
|
await BackupHelper.autoRestoreBackup().catch((err) => {
|
||||||
console.log(`${new Date().toISOString()}: failed auto-restoring database`, err);
|
console.log(`${new Date().toISOString()}: failed auto-restoring database`, err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
SettingHelper.configure();
|
await SettingHelper.configure();
|
||||||
|
MailHelper.createTransport();
|
||||||
});
|
});
|
||||||
|
|
||||||
const app = express();
|
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 schedule from "node-schedule";
|
||||||
import RefreshCommandHandler from "./command/refreshCommandHandler";
|
import RefreshCommandHandler from "./command/refreshCommandHandler";
|
||||||
|
import MailHelper from "./helpers/mailHelper";
|
||||||
const job = schedule.scheduleJob("0 0 * * *", async () => {
|
const job = schedule.scheduleJob("0 0 * * *", async () => {
|
||||||
console.log(`${new Date().toISOString()}: running Cron`);
|
console.log(`${new Date().toISOString()}: running Cron`);
|
||||||
await RefreshCommandHandler.deleteExpired();
|
await RefreshCommandHandler.deleteExpired();
|
||||||
|
|
|
@ -2,16 +2,13 @@ import { MigrationInterface, QueryRunner, Table } from "typeorm";
|
||||||
import BackupHelper from "../helpers/backupHelper";
|
import BackupHelper from "../helpers/backupHelper";
|
||||||
import { getDefaultByORM, getTypeByORM, isIncrementPrimary } from "./ormHelper";
|
import { getDefaultByORM, getTypeByORM, isIncrementPrimary } from "./ormHelper";
|
||||||
import InternalException from "../exceptions/internalException";
|
import InternalException from "../exceptions/internalException";
|
||||||
import { SettingHelper } from "../helpers/settingsHelper";
|
import { DB_TYPE } from "../env.defaults";
|
||||||
|
|
||||||
export class BackupAndResetDatabase1738166124200 implements MigrationInterface {
|
export class BackupAndResetDatabase1738166124200 implements MigrationInterface {
|
||||||
name = "BackupAndResetDatabase1738166124200";
|
name = "BackupAndResetDatabase1738166124200";
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
let query =
|
let query = DB_TYPE == "postgres" ? "SELECT name FROM migrations" : "SELECT `name` FROM `migrations`";
|
||||||
SettingHelper.getEnvSetting("database.type") == "postgres"
|
|
||||||
? "SELECT name FROM migrations"
|
|
||||||
: "SELECT `name` FROM `migrations`";
|
|
||||||
let migrations = await queryRunner.query(query);
|
let migrations = await queryRunner.query(query);
|
||||||
if (
|
if (
|
||||||
(await queryRunner.hasTable("user")) &&
|
(await queryRunner.hasTable("user")) &&
|
||||||
|
|
|
@ -54,7 +54,7 @@ import {
|
||||||
newsletter_recipients_table,
|
newsletter_recipients_table,
|
||||||
newsletter_table,
|
newsletter_table,
|
||||||
} from "./baseSchemaTables/newsletter";
|
} from "./baseSchemaTables/newsletter";
|
||||||
import { SettingHelper } from "../helpers/settingsHelper";
|
import { DB_TYPE } from "../env.defaults";
|
||||||
|
|
||||||
export class CreateSchema1738166167472 implements MigrationInterface {
|
export class CreateSchema1738166167472 implements MigrationInterface {
|
||||||
name = "CreateSchema1738166167472";
|
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_executive_positions_table, true, true, true);
|
||||||
await queryRunner.createTable(member_qualifications_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);
|
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 == "mysql") await queryRunner.createView(member_view_mysql, true);
|
||||||
else if (DB_TYPE == "sqlite") await queryRunner.createView(member_view_sqlite, true);
|
else if (DB_TYPE == "sqlite") await queryRunner.createView(member_view_sqlite, true);
|
||||||
|
|
30
src/migrations/1745059495808-settingsFromEnv.ts
Normal file
30
src/migrations/1745059495808-settingsFromEnv.ts
Normal file
|
@ -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<void> {
|
||||||
|
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<void> {
|
||||||
|
await queryRunner.dropTable(setting_table.name, true, true, true);
|
||||||
|
}
|
||||||
|
}
|
|
@ -148,3 +148,12 @@ export const reset_table = new Table({
|
||||||
{ name: "secret", ...getTypeByORM("varchar") },
|
{ 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") },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
|
@ -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 ORMType = "int" | "bigint" | "boolean" | "date" | "datetime" | "time" | "text" | "varchar" | "uuid";
|
||||||
export type ORMDefault = "currentTimestamp" | "string" | "boolean" | "number" | "null";
|
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 {
|
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<string, Record<ORMType, string>> = {
|
const typeMap: Record<string, Record<ORMType, string>> = {
|
||||||
mysql: {
|
mysql: {
|
||||||
|
@ -65,7 +65,7 @@ export function getTypeByORM(type: ORMType, nullable: boolean = false, length: n
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDefaultByORM<T = string | null>(type: ORMDefault, data?: string | number | boolean): T {
|
export function getDefaultByORM<T = string | null>(type: ORMDefault, data?: string | number | boolean): T {
|
||||||
const dbType = SettingHelper.getEnvSetting("database.type");
|
const dbType = DB_TYPE;
|
||||||
|
|
||||||
const typeMap: Record<string, Record<ORMDefault, string | null>> = {
|
const typeMap: Record<string, Record<ORMDefault, string | null>> = {
|
||||||
mysql: {
|
mysql: {
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { Brackets, Like, SelectQueryBuilder } from "typeorm";
|
import { Brackets, Like, SelectQueryBuilder } from "typeorm";
|
||||||
import { dataSource } from "../../../data-source";
|
import { dataSource } from "../../../data-source";
|
||||||
import { member } from "../../../entity/club/member/member";
|
import { member } from "../../../entity/club/member/member";
|
||||||
import { membership } from "../../../entity/club/member/membership";
|
|
||||||
import DatabaseActionException from "../../../exceptions/databaseActionException";
|
import DatabaseActionException from "../../../exceptions/databaseActionException";
|
||||||
import InternalException from "../../../exceptions/internalException";
|
|
||||||
import { memberView } from "../../../views/memberView";
|
import { memberView } from "../../../views/memberView";
|
||||||
import { SettingHelper } from "../../../helpers/settingsHelper";
|
import { DB_TYPE } from "../../../env.defaults";
|
||||||
|
|
||||||
export default abstract class MemberService {
|
export default abstract class MemberService {
|
||||||
/**
|
/**
|
||||||
|
@ -169,7 +167,7 @@ export default abstract class MemberService {
|
||||||
"member.firstMembershipEntry",
|
"member.firstMembershipEntry",
|
||||||
"member.memberships",
|
"member.memberships",
|
||||||
"membership_first",
|
"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")'
|
||||||
: "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.lastMembershipEntry",
|
||||||
"member.memberships",
|
"member.memberships",
|
||||||
"membership_last",
|
"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")'
|
||||||
: "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)"
|
||||||
)
|
)
|
||||||
|
|
43
src/service/settingService.ts
Normal file
43
src/service/settingService.ts
Normal file
|
@ -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<setting[]>}
|
||||||
|
*/
|
||||||
|
static async getSettings(): Promise<setting[]> {
|
||||||
|
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<setting>}
|
||||||
|
*/
|
||||||
|
static async getBySettingString(key: SettingString): Promise<setting> {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,6 @@ export type SettingString =
|
||||||
| "club.website"
|
| "club.website"
|
||||||
| "app.custom_login_message"
|
| "app.custom_login_message"
|
||||||
| "app.show_link_to_calendar"
|
| "app.show_link_to_calendar"
|
||||||
| "session.jwt_secret"
|
|
||||||
| "session.jwt_expiration"
|
| "session.jwt_expiration"
|
||||||
| "session.refresh_expiration"
|
| "session.refresh_expiration"
|
||||||
| "session.pwa_refresh_expiration"
|
| "session.pwa_refresh_expiration"
|
||||||
|
@ -20,23 +19,17 @@ export type SettingString =
|
||||||
| "mail.port"
|
| "mail.port"
|
||||||
| "mail.secure"
|
| "mail.secure"
|
||||||
| "backup.interval"
|
| "backup.interval"
|
||||||
| "backup.copies"
|
| "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 SettingTypeAtom = "longstring" | "string" | "ms" | "number" | "boolean" | "url";
|
||||||
export type SettingType = SettingTypeAtom | `${SettingTypeAtom}/crypt` | `${SettingTypeAtom}/rand`;
|
export type SettingType = SettingTypeAtom | `${SettingTypeAtom}/crypt` | `${SettingTypeAtom}/rand`;
|
||||||
|
|
||||||
export const settingsType: {
|
export const settingsType: {
|
||||||
[key in SettingString]: {
|
[key in SettingString]: {
|
||||||
type: SettingType | SettingType[];
|
type: SettingType | SettingTypeAtom[];
|
||||||
default?: string | number | boolean | ms.StringValue;
|
default?: string | number | boolean | ms.StringValue;
|
||||||
optional?: boolean;
|
optional?: boolean;
|
||||||
|
min?: number;
|
||||||
};
|
};
|
||||||
} = {
|
} = {
|
||||||
"club.name": { type: "string", default: "FF Admin" },
|
"club.name": { type: "string", default: "FF Admin" },
|
||||||
|
@ -45,7 +38,6 @@ export const settingsType: {
|
||||||
"club.website": { type: "url", optional: true },
|
"club.website": { type: "url", optional: true },
|
||||||
"app.custom_login_message": { type: "string", optional: true },
|
"app.custom_login_message": { type: "string", optional: true },
|
||||||
"app.show_link_to_calendar": { type: "boolean", default: 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.jwt_expiration": { type: "ms", default: "15m" },
|
||||||
"session.refresh_expiration": { type: "ms", default: "1d" },
|
"session.refresh_expiration": { type: "ms", default: "1d" },
|
||||||
"session.pwa_refresh_expiration": { type: "ms", default: "5d" },
|
"session.pwa_refresh_expiration": { type: "ms", default: "5d" },
|
||||||
|
@ -54,15 +46,8 @@ export const settingsType: {
|
||||||
"mail.host": { type: "url", optional: false },
|
"mail.host": { type: "url", optional: false },
|
||||||
"mail.port": { type: "number", default: 587 },
|
"mail.port": { type: "number", default: 587 },
|
||||||
"mail.secure": { type: "boolean", default: false },
|
"mail.secure": { type: "boolean", default: false },
|
||||||
"backup.interval": { type: "number", default: 1 },
|
"backup.interval": { type: "number", default: 1, min: 1 },
|
||||||
"backup.copies": { type: "number", default: 7 },
|
"backup.copies": { type: "number", default: 7, min: 1 },
|
||||||
"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 */
|
/** ENV Settings */
|
||||||
|
@ -72,12 +57,20 @@ export type EnvSettingString =
|
||||||
| "database.port"
|
| "database.port"
|
||||||
| "database.name"
|
| "database.name"
|
||||||
| "database.username"
|
| "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: {
|
export const envSettingsType: {
|
||||||
[key in EnvSettingString]: {
|
[key in EnvSettingString]: {
|
||||||
type: SettingType | SettingType[];
|
type: SettingType | SettingType[];
|
||||||
default?: string;
|
default?: string | number | boolean;
|
||||||
};
|
};
|
||||||
} = {
|
} = {
|
||||||
"database.type": { type: "string", default: "postgres" },
|
"database.type": { type: "string", default: "postgres" },
|
||||||
|
@ -86,4 +79,12 @@ export const envSettingsType: {
|
||||||
"database.name": { type: "string" },
|
"database.name": { type: "string" },
|
||||||
"database.username": { type: "string" },
|
"database.username": { type: "string" },
|
||||||
"database.password": { 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"] },
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { DataSource, ViewColumn, ViewEntity } from "typeorm";
|
import { DataSource, ViewColumn, ViewEntity } from "typeorm";
|
||||||
import { memberExecutivePositions } from "../entity/club/member/memberExecutivePositions";
|
import { memberExecutivePositions } from "../entity/club/member/memberExecutivePositions";
|
||||||
import { SettingHelper } from "../helpers/settingsHelper";
|
import { DB_TYPE } from "../env.defaults";
|
||||||
|
|
||||||
let durationInDays: string;
|
let durationInDays: string;
|
||||||
let durationInYears: string;
|
let durationInYears: string;
|
||||||
let exactDuration: string;
|
let exactDuration: string;
|
||||||
const DB_TYPE = SettingHelper.getEnvSetting("database.type");
|
|
||||||
if (DB_TYPE == "postgres") {
|
if (DB_TYPE == "postgres") {
|
||||||
durationInDays = `SUM(COALESCE("memberExecutivePositions"."end", CURRENT_DATE) - "memberExecutivePositions"."start")`;
|
durationInDays = `SUM(COALESCE("memberExecutivePositions"."end", CURRENT_DATE) - "memberExecutivePositions"."start")`;
|
||||||
durationInYears = `SUM(EXTRACT(YEAR FROM AGE(COALESCE("memberExecutivePositions"."end", CURRENT_DATE), "memberExecutivePositions"."start")))`;
|
durationInYears = `SUM(EXTRACT(YEAR FROM AGE(COALESCE("memberExecutivePositions"."end", CURRENT_DATE), "memberExecutivePositions"."start")))`;
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { DataSource, ViewColumn, ViewEntity } from "typeorm";
|
import { DataSource, ViewColumn, ViewEntity } from "typeorm";
|
||||||
import { memberQualifications } from "../entity/club/member/memberQualifications";
|
import { memberQualifications } from "../entity/club/member/memberQualifications";
|
||||||
import { SettingHelper } from "../helpers/settingsHelper";
|
import { DB_TYPE } from "../env.defaults";
|
||||||
|
|
||||||
let durationInDays: string;
|
let durationInDays: string;
|
||||||
let durationInYears: string;
|
let durationInYears: string;
|
||||||
let exactDuration: string;
|
let exactDuration: string;
|
||||||
const DB_TYPE = SettingHelper.getEnvSetting("database.type");
|
|
||||||
if (DB_TYPE == "postgres") {
|
if (DB_TYPE == "postgres") {
|
||||||
durationInDays = `SUM(COALESCE("memberQualifications"."end", CURRENT_DATE) - "memberQualifications"."start") `;
|
durationInDays = `SUM(COALESCE("memberQualifications"."end", CURRENT_DATE) - "memberQualifications"."start") `;
|
||||||
durationInYears = `SUM(EXTRACT(YEAR FROM AGE(COALESCE("memberQualifications"."end", CURRENT_DATE), "memberQualifications"."start")))`;
|
durationInYears = `SUM(EXTRACT(YEAR FROM AGE(COALESCE("memberQualifications"."end", CURRENT_DATE), "memberQualifications"."start")))`;
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { DataSource, ViewColumn, ViewEntity } from "typeorm";
|
import { DataSource, ViewColumn, ViewEntity } from "typeorm";
|
||||||
import { member } from "../entity/club/member/member";
|
import { member } from "../entity/club/member/member";
|
||||||
import { SettingHelper } from "../helpers/settingsHelper";
|
import { DB_TYPE } from "../env.defaults";
|
||||||
|
|
||||||
let todayAge: string;
|
let todayAge: string;
|
||||||
let ageThisYear: string;
|
let ageThisYear: string;
|
||||||
let exactAge: string;
|
let exactAge: string;
|
||||||
const DB_TYPE = SettingHelper.getEnvSetting("database.type");
|
|
||||||
if (DB_TYPE == "postgres") {
|
if (DB_TYPE == "postgres") {
|
||||||
todayAge = `DATE_PART('year', AGE(CURRENT_DATE, member.birthdate))`;
|
todayAge = `DATE_PART('year', AGE(CURRENT_DATE, member.birthdate))`;
|
||||||
ageThisYear = `EXTRACT(YEAR FROM CURRENT_DATE) - EXTRACT(YEAR FROM member.birthdate)`;
|
ageThisYear = `EXTRACT(YEAR FROM CURRENT_DATE) - EXTRACT(YEAR FROM member.birthdate)`;
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { DataSource, ViewColumn, ViewEntity } from "typeorm";
|
import { DataSource, ViewColumn, ViewEntity } from "typeorm";
|
||||||
import { membership } from "../entity/club/member/membership";
|
import { membership } from "../entity/club/member/membership";
|
||||||
import { SettingHelper } from "../helpers/settingsHelper";
|
import { DB_TYPE } from "../env.defaults";
|
||||||
|
|
||||||
let durationInDays: string;
|
let durationInDays: string;
|
||||||
let durationInYears: string;
|
let durationInYears: string;
|
||||||
let exactDuration: string;
|
let exactDuration: string;
|
||||||
const DB_TYPE = SettingHelper.getEnvSetting("database.type");
|
|
||||||
if (DB_TYPE == "postgres") {
|
if (DB_TYPE == "postgres") {
|
||||||
durationInDays = `SUM(COALESCE("membership"."end", CURRENT_DATE) - "membership"."start") `;
|
durationInDays = `SUM(COALESCE("membership"."end", CURRENT_DATE) - "membership"."start") `;
|
||||||
durationInYears = `SUM(EXTRACT(YEAR FROM AGE(COALESCE("membership"."end", CURRENT_DATE), "membership"."start")))`;
|
durationInYears = `SUM(EXTRACT(YEAR FROM AGE(COALESCE("membership"."end", CURRENT_DATE), "membership"."start")))`;
|
||||||
|
|
Loading…
Add table
Reference in a new issue