Compare commits

..

87 commits
v1.4.1 ... main

Author SHA1 Message Date
a59a47e663 1.7.0 2025-06-07 15:56:43 +02:00
4dcfe5febc Merge pull request 'minor v1.7.0' (#116) from develop into main
Reviewed-on: #116
2025-06-07 13:55:09 +00:00
3f54b5d243 Merge branch 'main' into develop 2025-06-07 13:52:59 +00:00
de6ed4fba8 update packages 2025-06-07 15:44:44 +02:00
fff9ebc4fa Merge pull request 'move-database-to-postgres-only' (#115) from move-database-to-postgres-only into develop
Reviewed-on: #115
2025-06-07 13:39:21 +00:00
dc43df8937 fix: spelling of invite orUpdate 2025-06-07 15:38:56 +02:00
feb03ba99d better insert template usage scopes 2025-06-07 15:38:34 +02:00
874b863b1e update package.json 2025-06-07 15:25:41 +02:00
97ffae009b migrate schema to postgres only 2025-06-07 15:20:20 +02:00
19457824ee 1.6.0 2025-06-06 09:36:53 +02:00
e4365135a1 Merge pull request 'minor v1.6.0' (#114) from develop into main
Reviewed-on: #114
2025-06-06 07:35:59 +00:00
c849b8eb18 fix: full fun of migrations from start 2025-06-06 09:33:06 +02:00
0582cb3af7 fix: database request ILIKE and demodata 2025-06-06 09:11:39 +02:00
1f8ec2eeed Merge branch 'main' into develop 2025-06-05 14:35:04 +00:00
db7a4fff03 enhance: case-insensitive search with ILike 2025-06-05 08:04:11 +02:00
2998943dfa Merge pull request 'feature/#103-member-extend-data' (#111) from feature/#103-member-extend-data into develop
Reviewed-on: #111
2025-06-03 13:33:05 +00:00
38bfa3dd75 member print 2025-06-03 15:32:41 +02:00
fded8a663a education controller, service, command 2025-06-03 15:20:46 +02:00
5368a96d0f fix: save false value to database 2025-05-31 07:32:25 +02:00
573f92d098 enhance: add membership total view 2025-05-30 15:13:38 +02:00
f6a0a61ed8 change: provide createdAt Date for newsletter 2025-05-29 11:21:08 +02:00
2357497d3a enhance: sort newsletter by creation date 2025-05-29 11:12:33 +02:00
4074617145 1.5.4 2025-05-22 10:03:56 +02:00
3a82c74829 Merge pull request 'patches v1.5.4' (#104) from fix/queryBuilder into main
Reviewed-on: #104
2025-05-22 08:03:29 +00:00
d7a0ee694f Merge branch 'fix/queryBuilder' into develop 2025-05-22 10:02:40 +02:00
d8376a4b7b hotfix: shorten table alias to keep it
max length of alias is 63
2025-05-22 10:01:27 +02:00
83250c1e62 change: pass fallback error message to client 2025-05-22 10:00:56 +02:00
a96c5266b8 fix: formatting of Postgres Interval Result 2025-05-22 10:00:28 +02:00
5187a2dba6 change: handlebar abstracts for date 2025-05-21 10:56:09 +02:00
496a60f2df enhance: enable deletion of protocol content 2025-05-21 10:33:05 +02:00
2076412c06 1.5.3 2025-05-19 13:26:42 +02:00
1a71d32b6d Merge pull request 'patches v1.5.3' (#101) from develop into main
Reviewed-on: #101
2025-05-19 11:25:54 +00:00
1a83e4939d Merge branch 'main' into develop 2025-05-17 05:36:53 +00:00
b778b6faa7 enhance: permission handling 2025-05-16 11:17:02 +02:00
1a66c9fac0 1.5.2 2025-05-08 08:22:35 +02:00
7db363e3de Merge pull request 'patches v1.5.2' (#100) from develop into main
Reviewed-on: #100
2025-05-08 06:22:12 +00:00
b8df454048 Merge branch 'main' into develop 2025-05-08 06:21:47 +00:00
0dbc726f64 fix: push settings val to listener 2025-05-08 08:16:12 +02:00
025cd555e9 fix: settings encryption 2025-05-08 08:16:07 +02:00
46bce99fd6 1.5.1 2025-05-07 09:29:46 +02:00
a36ebbd43c Merge pull request 'patches v1.5.1' (#99) from develop into main
Reviewed-on: #99
2025-05-07 07:28:59 +00:00
16ccd0d35f Merge branch 'main' into develop 2025-05-07 07:28:48 +00:00
9ba477a9b3 update: migration 2025-05-07 09:22:59 +02:00
69b447a2d8 fix: check of some permission returned always false 2025-05-07 09:05:55 +02:00
56484020d8 enhance: permission handling 2025-05-07 09:05:36 +02:00
9dd7686b67 change: request method for account credential change 2025-05-07 08:27:48 +02:00
8ead7386ca update: ReadMe 2025-05-07 08:27:42 +02:00
18fd56514d 1.5.0 2025-05-06 09:53:14 +02:00
75d0c1f524 Merge pull request 'minor v1.5.0' (#98) from develop into main
Reviewed-on: #98
2025-05-06 07:52:26 +00:00
7da497066f Merge branch 'main' into develop 2025-05-06 07:52:05 +00:00
676b7144bf update all packages 2025-05-06 09:30:54 +02:00
a64567ce4e Merge pull request 'feature/#70-static-user-login' (#97) from feature/#70-static-user-login into develop
Reviewed-on: #97
2025-05-06 07:20:15 +00:00
72552bdd83 enhance: update migration to work with newer model schema 2025-05-06 09:18:22 +02:00
cac784474c update migration to work with postgre 2025-05-06 09:17:55 +02:00
0ea12eaafc enable password on invite or reset 2025-05-06 08:37:56 +02:00
ddb460f8d0 enable switch to pw totp in account settings 2025-05-05 17:43:57 +02:00
be22c78372 login by password or totp 2025-05-05 14:21:13 +02:00
a476bf6823 migration change on default value and encrypted storage 2025-05-04 19:01:06 +02:00
03a5bb3592 change user model to login routine 2025-05-03 09:09:52 +02:00
c35b99e0c4 fix: migrations and discontinue sqlite 2025-05-03 09:08:15 +02:00
c4e547b288 Merge pull request 'feature/#71-settings-store' (#96) from feature/#71-settings-store into develop
Reviewed-on: #96
2025-05-01 15:48:30 +00:00
7dac58d958 remove unused code in settings helper 2025-05-01 17:47:14 +02:00
6c0ea0b11c change convert Helper Mail to validator package 2025-05-01 17:44:01 +02:00
0ea780dd51 change email validation to regex only 2025-05-01 17:36:48 +02:00
753cfdd5da Merge branch 'develop' into feature/#71-settings-store
# Conflicts:
#	src/data-source.ts
2025-04-30 12:30:55 +02:00
a6229bb77c enhance: provide latest inserted internal Id 2025-04-30 12:22:38 +02:00
41cb4eb569 check mail config on mail value change 2025-04-30 11:38:04 +02:00
5b3a72820a image upload and keep if not changed 2025-04-30 10:43:31 +02:00
be52a51055 bulk setting change and image update 2025-04-29 13:19:12 +02:00
964af82904 update to typesafe all-settings provider 2025-04-26 09:16:46 +02:00
99eafcb352 add settings to backup 2025-04-25 12:21:25 +02:00
ce9f621b8b add mail type and validation 2025-04-25 12:13:26 +02:00
2e3d0a755c setup routine 2025-04-25 08:18:49 +02:00
70edd165ee typesave set Setting 2025-04-25 08:07:53 +02:00
b4a7986c8a move pwa manifest to backend 2025-04-24 16:49:16 +02:00
7aa9038a61 provide setting endbpoints 2025-04-20 16:15:27 +02:00
a8edc19f34 optimize settings helper 2025-04-20 15:32:57 +02:00
730c25a9a1 split in env required and dynamic values 2025-04-19 16:51:37 +02:00
f32143b7ac SettingsHelper 2025-04-19 11:26:35 +02:00
a827185bf1 change: remove queryObj from newsletter 2025-04-19 10:04:28 +02:00
f850fc2526 Merge pull request 'feature/#93-newsletter-no-sendto-entry' (#94) from feature/#93-newsletter-no-sendto-entry into develop
Reviewed-on: #94
2025-04-19 07:43:53 +00:00
f1395357c5 add send none state to newsletter config 2025-04-19 09:42:11 +02:00
63f206cc6a do not print if config is null 2025-04-19 09:29:05 +02:00
fa5e778d0c 1.4.2 2025-04-18 11:10:45 +02:00
c2c782985c Merge pull request 'patches v1.4.2' (#91) from develop into main
Reviewed-on: #91
2025-04-18 09:10:13 +00:00
57848ff29b Merge branch 'main' into develop 2025-04-18 09:09:40 +00:00
3a956919b2 hotfix: template backup restore 2025-04-17 13:41:04 +02:00
122 changed files with 4385 additions and 1709 deletions

View file

@ -1,4 +1,4 @@
DB_TYPE = (mysql|sqlite|postgres) # default ist mysql
DB_TYPE = (mysql|postgres) # default ist mysql
## BSP für mysql
DB_PORT = 3306
@ -14,28 +14,10 @@ DB_NAME = database_name
DB_USERNAME = database_username
DB_PASSWORD = database_password
## BSP für sqlite
DB_HOST = filename.db
## Dev only
SERVER_PORT = portnumber
JWT_SECRET = ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 # besitzt default
JWT_EXPIRATION = [0-9]*(y|d|h|m|s) # default ist 15m
REFRESH_EXPIRATION = [0-9]*(y|d|h|m|s) # default ist 1d
PWA_REFRESH_EXPIRATION = [0-9]*(y|d|h|m|s) # default ist 5d
MAIL_USERNAME = mail_username
MAIL_PASSWORD = mail_password
MAIL_HOST = mail_hoststring
MAIL_PORT = mail_portnumber # default ist 587
MAIL_SECURE = (true|false) # true für port 465, false für anders gewählten port
CLUB_NAME = clubname #default FF Admin
CLUB_WEBSITE = https://my-club-website-url #optional, muss aber mit http:// oder https:// beginnen
BACKUP_INTERVAL = number of days (min 1) # default 1
BACKUP_COPIES = number of parallel copies # default 7
BACKUP_AUTO_RESTORE = (true|false) # default ist true
APPLICATION_SECRET = mysecret
USE_SECURITY_STRICT_LIMIT = (true|false) # default ist true
SECURITY_STRICT_LIMIT_WINDOW = [0-9]*(y|d|h|m|s) # default ist 15m

View file

@ -37,6 +37,7 @@ RUN mkdir -p /app/files
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
COPY --from=build /app/src/templates /app/src/templates
COPY --from=build /app/src/assets /app/src/assets
COPY --from=build /app/dist /app/dist
COPY --from=build /app/node_modules /app/node_modules
COPY --from=build /app/package.json /app/package.json

View file

@ -8,6 +8,8 @@ Dieses Projekt, `ff-admin-server`, ist das Backend zur Verwaltung von Mitglieder
Eine Demo zusammen mit der `ff-admin` finden Sie unter [https://admin-demo.ff-admin.de](https://admin-demo.ff-admin.de).
Das Handbuch zur Anwendung finden sie unter [https://ff-admin.de/ff-admin-handbook](https://ff-admin.de/ff-admin-handbook).
## Installation
Das Image exposed nur den Port 5000. Die Env-Variable SERVER_PORT kann nur im lokal ausführenden dev-Kontext verwendet werden.
@ -25,26 +27,13 @@ services:
container_name: ff_member_administration_server
restart: unless-stopped
environment:
- DB_TYPE=<mysql|sqlite|postgres> # default ist auf mysql gesetzt
- DB_TYPE=<mysql|postgres> # default ist auf mysql gesetzt
- DB_HOST=ff-db
- DB_PORT=<number> # default ist auf 3306 gesetzt
- DB_NAME=ffadmin
- DB_USERNAME=administration_backend
- DB_PASSWORD=<dbuserpasswd>
- JWT_SECRET=<tobemodified>
- JWT_EXPIRATION=<number[m|d] - bsp.:15m> # default ist auf 15m gesetzt
- REFRESH_EXPIRATION=<number[m|d] - bsp.:1d> # default ist auf 1d gesetzt
- PWA_REFRESH_EXPIRATION=<number[m|d] - bsp.:5d> # default ist auf 5d gesetzt
- MAIL_USERNAME=<mailadress|username>
- MAIL_PASSWORD=<password>
- MAIL_HOST=<url>
- MAIL_PORT=<port> # default ist auf 587 gesetzt
- MAIL_SECURE=<boolean> # default ist auf false gesetzt
- CLUB_NAME=<tobemodified> # default ist auf FF Admin gesetzt
- CLUB_WEBSITE=<tobemodified>
- BACKUP_INTERVAL=<number of days (min. 1)> # alle x Tage, sonst keine
- BACKUP_COPIES=<number of parallel copies> # Anzahl parallel bestehender Backups
- BACKUP_AUTO_RESTORE=<boolean> # default ist auf true gesetzt
- APPLICATION_SECRET=<tobemodified>
- USE_SECURITY_STRICT_LIMIT = (true|false) # default ist true
- SECURITY_STRICT_LIMIT_WINDOW = [0-9]*(y|d|h|m|s) # default ist 15
- SECURITY_STRICT_LIMIT_REQUEST_COUNT = strict_request_count # default ist 15
@ -91,8 +80,6 @@ networks:
Die Verwendung von postgres wird aufgrund des Verhaltens bei Datenbank-Update-Fehlern empfohlen.
Die Verwendung von SQLite wird nur für die Entwicklung oder lokale Tests empfohlen.
Führen Sie dann den folgenden Befehl im Verzeichnis der compose-Datei aus, um den Container zu starten:
```sh

1318
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,12 +1,13 @@
{
"name": "ff-admin-server",
"version": "1.4.1",
"version": "1.7.0",
"description": "Feuerwehr/Verein Mitgliederverwaltung Server",
"main": "dist/index.js",
"scripts": {
"start_ts": "ts-node src/index.ts",
"typeorm": "typeorm-ts-node-commonjs",
"migrate": "set DBMODE=migration && npx typeorm-ts-node-commonjs migration:generate ./src/migrations/%npm_config_name% -d ./src/data-source.ts",
"migrate-empty": "set DBMODE=migration && npx typeorm-ts-node-commonjs migration:create ./src/migrations/%npm_config_name%",
"synchronize-database": "set DBMODE=update-database && npx typeorm-ts-node-commonjs schema:sync -d ./src/data-source.ts",
"update-database": "set DBMODE=update-database && npx typeorm-ts-node-commonjs migration:run -d ./src/data-source.ts",
"revert-database": "set DBMODE=update-database && npx typeorm-ts-node-commonjs migration:revert -d ./src/data-source.ts",
@ -25,52 +26,56 @@
"license": "AGPL-3.0-only",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"crypto": "^1.0.1",
"dotenv": "^16.5.0",
"express": "^5.1.0",
"express-rate-limit": "^7.5.0",
"express-validator": "^7.2.1",
"handlebars": "^4.7.8",
"helmet": "^8.0.0",
"helmet": "^8.1.0",
"ics": "^3.8.1",
"ip": "^2.0.1",
"jsonwebtoken": "^9.0.2",
"lodash.clonedeep": "^4.5.0",
"lodash.uniqby": "^4.7.0",
"moment": "^2.30.1",
"morgan": "^1.10.0",
"ms": "^2.1.3",
"multer": "^1.4.5-lts.1",
"mysql": "^2.18.1",
"multer": "^2.0.1",
"node-schedule": "^2.1.1",
"nodemailer": "^6.10.1",
"nodemailer": "^7.0.3",
"pdf-lib": "^1.17.1",
"pg": "^8.13.1",
"puppeteer": "^24.6.1",
"pg": "^8.16.0",
"puppeteer": "^24.10.0",
"qrcode": "^1.5.4",
"reflect-metadata": "^0.2.2",
"rss-parser": "^3.13.0",
"socket.io": "^4.7.5",
"sharp": "^0.34.2",
"sharp-ico": "^0.1.5",
"socket.io": "^4.8.1",
"speakeasy": "^2.0.0",
"sqlite3": "^5.1.7",
"typeorm": "^0.3.20",
"uuid": "^11.1.0"
"typeorm": "^0.3.24",
"uuid": "^11.1.0",
"validator": "^13.15.15"
},
"devDependencies": {
"@types/cors": "^2.8.14",
"@types/express": "^5.0.1",
"@types/cors": "^2.8.19",
"@types/express": "^5.0.3",
"@types/ip": "^1.1.3",
"@types/jsonwebtoken": "^9.0.6",
"@types/jsonwebtoken": "^9.0.9",
"@types/lodash.clonedeep": "^4.5.9",
"@types/lodash.uniqby": "^4.7.9",
"@types/morgan": "^1.9.9",
"@types/morgan": "^1.9.10",
"@types/ms": "^2.1.0",
"@types/multer": "^1.4.12",
"@types/mysql": "^2.15.21",
"@types/node": "^22.14.1",
"@types/node-schedule": "^2.1.6",
"@types/nodemailer": "^6.4.14",
"@types/pg": "~8.11.12",
"@types/multer": "^1.4.13",
"@types/node": "^22.15.30",
"@types/node-schedule": "^2.1.7",
"@types/nodemailer": "^6.4.17",
"@types/pg": "~8.15.4",
"@types/qrcode": "~1.5.5",
"@types/speakeasy": "^2.0.10",
"@types/uuid": "^10.0.0",
"@types/validator": "^13.15.1",
"ts-node": "10.9.2",
"typescript": "^5.8.3"
}

BIN
src/assets/admin-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
src/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View file

@ -5,6 +5,7 @@ export interface CreateMemberCommand {
nameaffix: string;
birthdate: Date;
internalId?: string;
note?: string;
}
export interface UpdateMemberCommand {
@ -15,6 +16,7 @@ export interface UpdateMemberCommand {
nameaffix: string;
birthdate: Date;
internalId?: string;
note?: string;
}
export interface DeleteMemberCommand {

View file

@ -23,6 +23,7 @@ export default abstract class MemberCommandHandler {
nameaffix: createMember.nameaffix,
birthdate: createMember.birthdate,
internalId: createMember.internalId,
note: createMember.note,
})
.execute()
.then((result) => {
@ -49,6 +50,7 @@ export default abstract class MemberCommandHandler {
nameaffix: updateMember.nameaffix,
birthdate: updateMember.birthdate,
internalId: updateMember.internalId,
note: updateMember.note,
})
.where("id = :id", { id: updateMember.id })
.execute()

View file

@ -0,0 +1,23 @@
export interface CreateMemberEducationCommand {
start: Date;
end?: Date;
note?: string;
place?: string;
memberId: string;
educationId: number;
}
export interface UpdateMemberEducationCommand {
id: number;
start: Date;
end?: Date;
note?: string;
place?: string;
memberId: string;
educationId: number;
}
export interface DeleteMemberEducationCommand {
id: number;
memberId: string;
}

View file

@ -0,0 +1,82 @@
import { dataSource } from "../../../data-source";
import { memberEducations } from "../../../entity/club/member/memberEducations";
import DatabaseActionException from "../../../exceptions/databaseActionException";
import InternalException from "../../../exceptions/internalException";
import {
CreateMemberEducationCommand,
DeleteMemberEducationCommand,
UpdateMemberEducationCommand,
} from "./memberEducationCommand";
export default abstract class MemberEducationCommandHandler {
/**
* @description create memberEducation
* @param {CreateMemberEducationCommand} createMemberEducation
* @returns {Promise<number>}
*/
static async create(createMemberEducation: CreateMemberEducationCommand): Promise<number> {
return await dataSource
.createQueryBuilder()
.insert()
.into(memberEducations)
.values({
note: createMemberEducation.note,
place: createMemberEducation.place,
start: createMemberEducation.start,
end: createMemberEducation.end,
memberId: createMemberEducation.memberId,
educationId: createMemberEducation.educationId,
})
.execute()
.then((result) => {
return result.identifiers[0].id;
})
.catch((err) => {
throw new DatabaseActionException("CREATE", "memberEducation", err);
});
}
/**
* @description update memberEducation
* @param {UpdateMemberEducationCommand} updateMemberEducation
* @returns {Promise<void>}
*/
static async update(updateMemberEducation: UpdateMemberEducationCommand): Promise<void> {
return await dataSource
.createQueryBuilder()
.update(memberEducations)
.set({
note: updateMemberEducation.note,
start: updateMemberEducation.start,
end: updateMemberEducation.end,
place: updateMemberEducation.place,
educationId: updateMemberEducation.educationId,
})
.where("id = :id", { id: updateMemberEducation.id })
.andWhere("memberId = :memberId", { memberId: updateMemberEducation.memberId })
.execute()
.then(() => {})
.catch((err) => {
throw new DatabaseActionException("UPDATE", "memberEducation", err);
});
}
/**
* @description delete memberEducation
* @param {DeleteMemberEducationCommand} deleteMemberEducation
* @returns {Promise<void>}
*/
static async delete(deleteMemberEducation: DeleteMemberEducationCommand): Promise<void> {
return await dataSource
.createQueryBuilder()
.delete()
.from(memberEducations)
.where("id = :id", { id: deleteMemberEducation.id })
.andWhere("memberId = :memberId", { memberId: deleteMemberEducation.memberId })
.execute()
.then(() => {})
.catch((err) => {
throw new DatabaseActionException("DELETE", "memberEducation", err);
});
}
}

View file

@ -34,18 +34,32 @@ export default abstract class ProtocolAgendaCommandHandler {
/**
* @description sync protocolAgenda
* @param {number} protocolId
* @param {Array<SynchronizeProtocolAgendaCommand>} syncProtocolAgenda
* @returns {Promise<void>}
*/
static async sync(syncProtocolAgenda: Array<SynchronizeProtocolAgendaCommand>): Promise<void> {
static async sync(protocolId: number, syncProtocolAgenda: Array<SynchronizeProtocolAgendaCommand>): Promise<void> {
let currentAgenda = await ProtocolAgendaService.getAll(protocolId);
return await dataSource
.transaction(async (transactionalEntityManager) => {
let removed = currentAgenda.filter((ca) => !syncProtocolAgenda.some((spa) => spa.id == ca.id));
for (const agenda of syncProtocolAgenda) {
await transactionalEntityManager
.createQueryBuilder()
.update(protocolAgenda)
.set(agenda)
.where({ id: agenda.id })
.where({ id: agenda.id, protocolId })
.execute();
}
if (removed.length != 0) {
await transactionalEntityManager
.createQueryBuilder()
.delete()
.from(protocolAgenda)
.where("id IN (:...ids)", { ids: removed.map((m) => m.id) })
.andWhere({ protocolId })
.execute();
}
})

View file

@ -33,12 +33,19 @@ export default abstract class ProtocolDecisionCommandHandler {
}
/**
* @description sync protocolDecision
* @param {number} protocolId
* @param {Array<SynchronizeProtocolDecisionCommand>} syncProtocolDecisions
* @returns {Promise<void>}
*/
static async sync(syncProtocolDecisions: Array<SynchronizeProtocolDecisionCommand>): Promise<void> {
static async sync(
protocolId: number,
syncProtocolDecisions: Array<SynchronizeProtocolDecisionCommand>
): Promise<void> {
let currentDecision = await ProtocolDecisionService.getAll(protocolId);
return await dataSource
.transaction(async (transactionalEntityManager) => {
let removed = currentDecision.filter((ca) => !syncProtocolDecisions.some((spa) => spa.id == ca.id));
for (const decision of syncProtocolDecisions) {
await transactionalEntityManager
.createQueryBuilder()
@ -47,8 +54,17 @@ export default abstract class ProtocolDecisionCommandHandler {
.where({ id: decision.id })
.execute();
}
if (removed.length != 0) {
await transactionalEntityManager
.createQueryBuilder()
.delete()
.from(protocolDecision)
.where("id IN (:...ids)", { ids: removed.map((m) => m.id) })
.andWhere({ protocolId })
.execute();
}
})
.then(() => {})
.catch((err) => {
throw new DatabaseActionException("SYNC", "protocolDecision", err);
});

View file

@ -33,12 +33,16 @@ export default abstract class ProtocolVotingCommandHandler {
}
/**
* @description sync protocolVoting
* @param {number} protocolId
* @param {Array<SynchronizeProtocolVotingCommand>} syncProtocolVotings
* @returns {Promise<void>}
*/
static async sync(syncProtocolVotings: Array<SynchronizeProtocolVotingCommand>): Promise<void> {
static async sync(protocolId: number, syncProtocolVotings: Array<SynchronizeProtocolVotingCommand>): Promise<void> {
let currentVoting = await ProtocolVotingService.getAll(protocolId);
return await dataSource
.transaction(async (transactionalEntityManager) => {
let removed = currentVoting.filter((ca) => !syncProtocolVotings.some((spa) => spa.id == ca.id));
for (const voting of syncProtocolVotings) {
await transactionalEntityManager
.createQueryBuilder()
@ -47,8 +51,17 @@ export default abstract class ProtocolVotingCommandHandler {
.where({ id: voting.id })
.execute();
}
if (removed.length != 0) {
await transactionalEntityManager
.createQueryBuilder()
.delete()
.from(protocolVoting)
.where("id IN (:...ids)", { ids: removed.map((m) => m.id) })
.andWhere({ protocolId })
.execute();
}
})
.then(() => {})
.catch((err) => {
throw new DatabaseActionException("SYNC", "protocolVoting", err);
});

View file

@ -0,0 +1,14 @@
export interface CreateEducationCommand {
education: string;
description?: string;
}
export interface UpdateEducationCommand {
id: number;
education: string;
description?: string;
}
export interface DeleteEducationCommand {
id: number;
}

View file

@ -0,0 +1,68 @@
import { dataSource } from "../../../data-source";
import { education } from "../../../entity/configuration/education";
import DatabaseActionException from "../../../exceptions/databaseActionException";
import { CreateEducationCommand, DeleteEducationCommand, UpdateEducationCommand } from "./educationCommand";
export default abstract class EducationCommandHandler {
/**
* @description create education
* @param {CreateEducationCommand} createEducation
* @returns {Promise<number>}
*/
static async create(createEducation: CreateEducationCommand): Promise<number> {
return await dataSource
.createQueryBuilder()
.insert()
.into(education)
.values({
education: createEducation.education,
description: createEducation.description,
})
.execute()
.then((result) => {
return result.identifiers[0].id;
})
.catch((err) => {
throw new DatabaseActionException("CREATE", "education", err);
});
}
/**
* @description update education
* @param {UpdateEducationCommand} updateEducation
* @returns {Promise<void>}
*/
static async update(updateEducation: UpdateEducationCommand): Promise<void> {
return await dataSource
.createQueryBuilder()
.update(education)
.set({
education: updateEducation.education,
description: updateEducation.description,
})
.where("id = :id", { id: updateEducation.id })
.execute()
.then(() => {})
.catch((err) => {
throw new DatabaseActionException("UPDATE", "education", err);
});
}
/**
* @description delete education
* @param {DeleteEducationCommand} deleteEducation
* @returns {Promise<void>}
*/
static async delete(deleteEducation: DeleteEducationCommand): Promise<void> {
return await dataSource
.createQueryBuilder()
.delete()
.from(education)
.where("id = :id", { id: deleteEducation.id })
.execute()
.then(() => {})
.catch((err) => {
throw new DatabaseActionException("DELETE", "education", err);
});
}
}

View file

@ -1,8 +1,8 @@
import { NewsletterConfigType } from "../../../enums/newsletterConfigType";
import { NewsletterConfigEnum } from "../../../enums/newsletterConfigEnum";
export interface SetNewsletterConfigCommand {
comTypeId: number;
config: NewsletterConfigType;
config: NewsletterConfigEnum;
}
export interface DeleteNewsletterConfigCommand {

View file

@ -0,0 +1,10 @@
export interface CreateOrUpdateSettingCommand {
topic: string;
key: string;
value: string;
}
export interface DeleteSettingCommand {
topic: string;
key: string;
}

View file

@ -0,0 +1,50 @@
import { dataSource } from "../../../data-source";
import { setting } from "../../../entity/management/setting";
import DatabaseActionException from "../../../exceptions/databaseActionException";
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> {
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 createSetting.value;
})
.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);
});
}
}

View file

@ -26,7 +26,7 @@ export default abstract class InviteCommandHandler {
lastname: createInvite.lastname,
secret: createInvite.secret,
})
.orUpdate(["firstName", "lastName", "token", "secret"], ["mail"])
.orUpdate(["firstname", "lastname", "token", "secret"], ["mail"])
.execute()
.then((result) => {
return token;

View file

@ -1,3 +1,5 @@
import { LoginRoutineEnum } from "../../../enums/loginRoutineEnum";
export interface CreateUserCommand {
mail: string;
username: string;
@ -5,6 +7,7 @@ export interface CreateUserCommand {
lastname: string;
secret: string;
isOwner: boolean;
routine: LoginRoutineEnum;
}
export interface UpdateUserCommand {
@ -18,6 +21,7 @@ export interface UpdateUserCommand {
export interface UpdateUserSecretCommand {
id: string;
secret: string;
routine: LoginRoutineEnum;
}
export interface TransferUserOwnerCommand {

View file

@ -31,6 +31,7 @@ export default abstract class UserCommandHandler {
lastname: createUser.lastname,
secret: createUser.secret,
isOwner: createUser.isOwner,
routine: createUser.routine,
})
.execute()
.then((result) => {
@ -75,6 +76,7 @@ export default abstract class UserCommandHandler {
.update(user)
.set({
secret: updateUser.secret,
routine: updateUser.routine,
})
.where("id = :id", { id: updateUser.id })
.execute()

View file

@ -1,10 +1,8 @@
import { dataSource } from "../data-source";
import { refresh } from "../entity/refresh";
import { PWA_REFRESH_EXPIRATION, REFRESH_EXPIRATION } from "../env.defaults";
import DatabaseActionException from "../exceptions/databaseActionException";
import InternalException from "../exceptions/internalException";
import SettingHelper from "../helpers/settingsHelper";
import { StringHelper } from "../helpers/stringHelper";
import UserService from "../service/management/userService";
import { CreateRefreshCommand, DeleteRefreshCommand } from "./refreshCommand";
import ms from "ms";
@ -25,8 +23,8 @@ export default abstract class RefreshCommandHandler {
token: refreshToken,
userId: createRefresh.userId,
expiry: createRefresh.isFromPwa
? new Date(Date.now() + ms(PWA_REFRESH_EXPIRATION))
: new Date(Date.now() + ms(REFRESH_EXPIRATION)),
? new Date(Date.now() + ms(SettingHelper.getSetting("session.pwa_refresh_expiration")))
: new Date(Date.now() + ms(SettingHelper.getSetting("session.refresh_expiration"))),
})
.execute()
.then((result) => {

View file

@ -49,6 +49,14 @@ import {
import CommunicationCommandHandler from "../../../command/club/member/communicationCommandHandler";
import { PdfExport } from "../../../helpers/pdfExport";
import { PermissionModule } from "../../../type/permissionTypes";
import MemberEducationFactory from "../../../factory/admin/club/member/memberEducation";
import MemberEducationService from "../../../service/club/member/memberEducationService";
import {
CreateMemberEducationCommand,
DeleteMemberEducationCommand,
UpdateMemberEducationCommand,
} from "../../../command/club/member/memberEducationCommand";
import MemberEducationCommandHandler from "../../../command/club/member/memberEducationCommandHandler";
/**
* @description get all members
@ -92,6 +100,18 @@ export async function getMembersByIds(req: Request, res: Response): Promise<any>
});
}
/**
* @description get member latest inserted InternalId
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getMemberLastInternalId(req: Request, res: Response): Promise<any> {
let latest = await MemberService.getLatestInternalId();
res.send(latest);
}
/**
* @description get member by id
* @param req {Request} Express req object
@ -132,6 +152,7 @@ export async function getMemberPrintoutById(req: Request, res: Response): Promis
let qualifications = await MemberQualificationService.getAll(memberId);
let positions = await MemberExecutivePositionService.getAll(memberId);
let communications = await CommunicationService.getAll(memberId);
let educations = await MemberEducationService.getAll(memberId);
let pdf = await PdfExport.renderFile({
title: "Mitglieder-Ausdruck",
@ -145,6 +166,7 @@ export async function getMemberPrintoutById(req: Request, res: Response): Promis
qualifications,
positions,
communications,
educations,
},
});
@ -183,6 +205,19 @@ export async function getMembershipStatisticsById(req: Request, res: Response):
res.json(MembershipFactory.mapToBaseStatistics(member));
}
/**
* @description get member total statistics by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getMembershipTotalStatisticsById(req: Request, res: Response): Promise<any> {
const memberId = req.params.memberId;
let member = await MembershipService.getTotalStatisticsById(memberId);
res.json(MembershipFactory.mapToSingleTotalStatistic(member));
}
/**
* @description get membership by member and record
* @param req {Request} Express req object
@ -251,6 +286,33 @@ export async function getQualificationByMemberAndRecord(req: Request, res: Respo
res.json(MemberQualificationFactory.mapToSingle(qualification));
}
/**
* @description get educations by member
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getEducationsByMember(req: Request, res: Response): Promise<any> {
const memberId = req.params.memberId;
let educations = await MemberEducationService.getAll(memberId);
res.json(MemberEducationFactory.mapToBase(educations));
}
/**
* @description get education by member and record
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getEducationByMemberAndRecord(req: Request, res: Response): Promise<any> {
const memberId = req.params.memberId;
const recordId = parseInt(req.params.id);
let education = await MemberEducationService.getById(memberId, recordId);
res.json(MemberEducationFactory.mapToSingle(education));
}
/**
* @description get executive positions by member
* @param req {Request} Express req object
@ -318,6 +380,7 @@ export async function createMember(req: Request, res: Response): Promise<any> {
const nameaffix = req.body.nameaffix;
const birthdate = req.body.birthdate;
const internalId = req.body.internalId || null;
const note = req.body.note || null;
let createMember: CreateMemberCommand = {
salutationId,
@ -326,6 +389,7 @@ export async function createMember(req: Request, res: Response): Promise<any> {
nameaffix,
birthdate,
internalId,
note,
};
let memberId = await MemberCommandHandler.create(createMember);
@ -401,6 +465,33 @@ export async function addQualificationToMember(req: Request, res: Response): Pro
res.sendStatus(204);
}
/**
* @description add education to member
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function addEducationToMember(req: Request, res: Response): Promise<any> {
const memberId = req.params.memberId;
const note = req.body.note;
const place = req.body.place;
const start = req.body.start;
const end = req.body.end || null;
const educationId = req.body.educationId;
let createMemberEducation: CreateMemberEducationCommand = {
note,
start,
end,
place,
memberId,
educationId,
};
await MemberEducationCommandHandler.create(createMemberEducation);
res.sendStatus(204);
}
/**
* @description add executive positions to member
* @param req {Request} Express req object
@ -479,6 +570,7 @@ export async function updateMemberById(req: Request, res: Response): Promise<any
const nameaffix = req.body.nameaffix;
const birthdate = req.body.birthdate;
const internalId = req.body.internalId || null;
const note = req.body.note || null;
let updateMember: UpdateMemberCommand = {
id: memberId,
@ -488,6 +580,7 @@ export async function updateMemberById(req: Request, res: Response): Promise<any
nameaffix,
birthdate,
internalId,
note,
};
await MemberCommandHandler.update(updateMember);
@ -577,6 +670,35 @@ export async function updateQualificationOfMember(req: Request, res: Response):
res.sendStatus(204);
}
/**
* @description update education of member
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function updateEducationOfMember(req: Request, res: Response): Promise<any> {
const memberId = req.params.memberId;
const recordId = parseInt(req.params.recordId);
const start = req.body.start;
const end = req.body.end || null;
const note = req.body.note;
const place = req.body.place;
const educationId = req.body.educationId;
let updateMemberEducation: UpdateMemberEducationCommand = {
id: recordId,
start,
end,
note,
place,
memberId,
educationId,
};
await MemberEducationCommandHandler.update(updateMemberEducation);
res.sendStatus(204);
}
/**
* @description update executive position of member
* @param req {Request} Express req object
@ -717,6 +839,25 @@ export async function deleteQualificationOfMember(req: Request, res: Response):
res.sendStatus(204);
}
/**
* @description delete education from member
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function deleteEducationsOfMember(req: Request, res: Response): Promise<any> {
const memberId = req.params.memberId;
const recordId = parseInt(req.params.recordId);
let deleteMemberEducation: DeleteMemberEducationCommand = {
id: recordId,
memberId,
};
await MemberEducationCommandHandler.delete(deleteMemberEducation);
res.sendStatus(204);
}
/**
* @description delete executive position from member
* @param req {Request} Express req object

View file

@ -249,12 +249,7 @@ export async function createProtocolPrintoutById(req: Request, res: Response): P
title: protocol.title,
summary: protocol.summary,
iteration: iteration + 1,
date: new Date(protocol.date).toLocaleDateString("de-DE", {
weekday: "long",
day: "2-digit",
month: "2-digit",
year: "numeric",
}),
date: protocol.date,
start: protocol.starttime,
end: protocol.endtime,
agenda: agenda.sort((a, b) => a.sort - b.sort),
@ -324,7 +319,7 @@ export async function synchronizeProtocolAgendaById(req: Request, res: Response)
protocolId,
})
);
await ProtocolAgendaCommandHandler.sync(syncAgenda);
await ProtocolAgendaCommandHandler.sync(protocolId, syncAgenda);
res.sendStatus(204);
}
@ -348,7 +343,7 @@ export async function synchronizeProtocolDecisonsById(req: Request, res: Respons
protocolId,
})
);
await ProtocolDecisionCommandHandler.sync(syncDecision);
await ProtocolDecisionCommandHandler.sync(protocolId, syncDecision);
res.sendStatus(204);
}
@ -375,7 +370,7 @@ export async function synchronizeProtocolVotingsById(req: Request, res: Response
protocolId,
})
);
await ProtocolVotingCommandHandler.sync(syncVoting);
await ProtocolVotingCommandHandler.sync(protocolId, syncVoting);
res.sendStatus(204);
}

View file

@ -0,0 +1,91 @@
import { Request, Response } from "express";
import EducationService from "../../../service/configuration/education";
import EducationFactory from "../../../factory/admin/configuration/education";
import {
CreateEducationCommand,
DeleteEducationCommand,
UpdateEducationCommand,
} from "../../../command/configuration/education/educationCommand";
import EducationCommandHandler from "../../../command/configuration/education/educationCommandHandler";
/**
* @description get all educations
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getAllEducations(req: Request, res: Response): Promise<any> {
let educations = await EducationService.getAll();
res.json(EducationFactory.mapToBase(educations));
}
/**
* @description get education by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getEducationById(req: Request, res: Response): Promise<any> {
const id = parseInt(req.params.id);
let education = await EducationService.getById(id);
res.json(EducationFactory.mapToSingle(education));
}
/**
* @description create new education
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function createEducation(req: Request, res: Response): Promise<any> {
const education = req.body.education;
const description = req.body.description;
let createEducation: CreateEducationCommand = {
education: education,
description: description,
};
await EducationCommandHandler.create(createEducation);
res.sendStatus(204);
}
/**
* @description update education
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function updateEducation(req: Request, res: Response): Promise<any> {
const id = parseInt(req.params.id);
const education = req.body.education;
const description = req.body.description;
let updateEducation: UpdateEducationCommand = {
id: id,
education: education,
description: description,
};
await EducationCommandHandler.update(updateEducation);
res.sendStatus(204);
}
/**
* @description delete education
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function deleteEducation(req: Request, res: Response): Promise<any> {
const id = parseInt(req.params.id);
let deleteEducation: DeleteEducationCommand = {
id: id,
};
await EducationCommandHandler.delete(deleteEducation);
res.sendStatus(204);
}

View file

@ -0,0 +1,116 @@
import { Request, Response } from "express";
import SettingHelper from "../../../helpers/settingsHelper";
import { SettingString, SettingValueMapping } from "../../../type/settingTypes";
import MailHelper from "../../../helpers/mailHelper";
import InternalException from "../../../exceptions/internalException";
/**
* @description get All settings
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getSettings(req: Request, res: Response): Promise<any> {
res.json({ ...SettingHelper.getAllSettings(), ["mail.password"]: undefined });
}
/**
* @description get setting
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getSetting(req: Request, res: Response): Promise<any> {
let setting = req.params.setting as SettingString;
let value = SettingHelper.getSetting(setting);
if (setting == "mail.password") {
value = undefined;
}
res.send(value);
}
/**
* @description set setting
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function setSetting(req: Request, res: Response): Promise<any> {
let setting = req.body.setting as SettingString;
let value = req.body.value as string;
await SettingHelper.checkMail([{ key: setting, value }]).catch((err) => {
if (err == "mail") {
throw new InternalException("Mail is not valid");
} else {
throw new InternalException("Config is not valid");
}
});
await SettingHelper.setSetting(setting, value);
res.sendStatus(204);
}
/**
* @description set settings
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function setSettings<K extends SettingString>(req: Request, res: Response): Promise<any> {
let setting = req.body as Array<{ key: K; value: SettingValueMapping[K] }>;
await SettingHelper.checkMail(setting).catch((err) => {
if (err == "mail") {
throw new InternalException("Mail is not valid");
} else {
throw new InternalException("Config is not valid");
}
});
for (let entry of setting) {
await SettingHelper.setSetting(entry.key, entry.value);
}
res.sendStatus(204);
}
/**
* @description set setting
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function setImages(req: Request, res: Response): Promise<any> {
if (req.files && !Array.isArray(req.files) && req.files.icon) {
await SettingHelper.setSetting("club.icon", "configured");
} else if (req.body["club.icon"] != "keep") {
await SettingHelper.resetSetting("club.icon");
}
if (req.files && !Array.isArray(req.files) && req.files.logo) {
await SettingHelper.setSetting("club.logo", "configured");
} else if (req.body["club.logo"] != "keep") {
await SettingHelper.resetSetting("club.logo");
}
res.sendStatus(204);
}
/**
* @description reset setting
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function resetSetting(req: Request, res: Response): Promise<any> {
let setting = req.params.setting as SettingString;
await SettingHelper.resetSetting(setting);
res.sendStatus(204);
}

View file

@ -11,10 +11,10 @@ import {
} from "../../../command/management/user/userCommand";
import UserCommandHandler from "../../../command/management/user/userCommandHandler";
import MailHelper from "../../../helpers/mailHelper";
import { CLUB_NAME } from "../../../env.defaults";
import { UpdateUserPermissionsCommand } from "../../../command/management/user/userPermissionCommand";
import UserPermissionCommandHandler from "../../../command/management/user/userPermissionCommandHandler";
import BadRequestException from "../../../exceptions/badRequestException";
import SettingHelper from "../../../helpers/settingsHelper";
/**
* @description get All users
@ -157,7 +157,7 @@ export async function deleteUser(req: Request, res: Response): Promise<any> {
// sendmail
await MailHelper.sendMail(
mail,
`Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`,
`Email Bestätigung für Mitglieder Admin-Portal von ${SettingHelper.getSetting("club.name")}`,
`Ihr Nutzerkonto des Adminportals wurde erfolgreich gelöscht.`
);
} catch (error) {}

View file

@ -12,8 +12,8 @@ import WebapiCommandHandler from "../../../command/management/webapi/webapiComma
import { UpdateWebapiPermissionsCommand } from "../../../command/management/webapi/webapiPermissionCommand";
import WebapiPermissionCommandHandler from "../../../command/management/webapi/webapiPermissionCommandHandler";
import { JWTHelper } from "../../../helpers/jwtHelper";
import { CLUB_NAME } from "../../../env.defaults";
import { StringHelper } from "../../../helpers/stringHelper";
import SettingHelper from "../../../helpers/settingsHelper";
/**
* @description get All apis
@ -78,7 +78,7 @@ export async function createWebapi(req: Request, res: Response): Promise<any> {
let token = await JWTHelper.create(
{
iss: CLUB_NAME,
iss: SettingHelper.getSetting("club.name"),
sub: "api_token_retrieve",
aud: StringHelper.random(32),
},

View file

@ -8,6 +8,25 @@ import UserService from "../service/management/userService";
import speakeasy from "speakeasy";
import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
import RefreshService from "../service/refreshService";
import { LoginRoutineEnum } from "../enums/loginRoutineEnum";
/**
* @description Check authentication status by token
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function kickof(req: Request, res: Response): Promise<any> {
let username = req.body.username;
let { routine } = await UserService.getByUsername(username).catch(() => {
throw new UnauthorizedRequestException("Username not found");
});
res.json({
routine,
});
}
/**
* @description Check authentication status by token
@ -17,19 +36,25 @@ import RefreshService from "../service/refreshService";
*/
export async function login(req: Request, res: Response): Promise<any> {
let username = req.body.username;
let totp = req.body.totp;
let passedSecret = req.body.secret;
let { id, secret } = await UserService.getByUsername(username);
let { id } = await UserService.getByUsername(username);
let { secret, routine } = await UserService.getUserSecretAndRoutine(id);
let valid = speakeasy.totp.verify({
secret: secret,
encoding: "base32",
token: totp,
window: 2,
});
let valid = false;
if (routine == LoginRoutineEnum.totp) {
valid = speakeasy.totp.verify({
secret: secret,
encoding: "base32",
token: passedSecret,
window: 2,
});
} else {
valid = passedSecret == secret;
}
if (!valid) {
throw new UnauthorizedRequestException("Token not valid or expired");
throw new UnauthorizedRequestException("Credentials not valid or expired");
}
let accessToken = await JWTHelper.buildToken(id);

View file

@ -1,6 +1,5 @@
import { Request, Response } from "express";
import { JWTHelper } from "../helpers/jwtHelper";
import { JWTToken } from "../type/jwtTypes";
import InternalException from "../exceptions/internalException";
import RefreshCommandHandler from "../command/refreshCommandHandler";
import { CreateRefreshCommand } from "../command/refreshCommand";
@ -15,10 +14,9 @@ import MailHelper from "../helpers/mailHelper";
import InviteService from "../service/management/inviteService";
import UserService from "../service/management/userService";
import CustomRequestException from "../exceptions/customRequestException";
import { CLUB_NAME } from "../env.defaults";
import { CreateUserPermissionCommand } from "../command/management/user/userPermissionCommand";
import UserPermissionCommandHandler from "../command/management/user/userPermissionCommandHandler";
import InviteFactory from "../factory/admin/management/invite";
import SettingHelper from "../helpers/settingsHelper";
import { LoginRoutineEnum } from "../enums/loginRoutineEnum";
/**
* @description get all invites
@ -38,7 +36,7 @@ export async function getInvites(req: Request, res: Response): Promise<any> {
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function inviteUser(req: Request, res: Response, isInvite: boolean = true): Promise<any> {
export async function inviteUser(req: Request, res: Response, isSetup: boolean = false): Promise<any> {
let origin = req.headers.origin;
let username = req.body.username;
let mail = req.body.mail;
@ -59,7 +57,7 @@ export async function inviteUser(req: Request, res: Response, isInvite: boolean
throw new CustomRequestException(409, "Username and Mail are already in use");
}
var secret = speakeasy.generateSecret({ length: 20, name: `FF Admin ${CLUB_NAME}` });
var secret = speakeasy.generateSecret({ length: 20, name: `FF Admin ${SettingHelper.getSetting("club.name")}` });
let createInvite: CreateInviteCommand = {
username: username,
@ -73,8 +71,8 @@ export async function inviteUser(req: Request, res: Response, isInvite: boolean
// sendmail
await MailHelper.sendMail(
mail,
`Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`,
`Öffne folgenden Link: ${origin}/${isInvite ? "invite" : "setup"}/verify?mail=${mail}&token=${token}`
`Email Bestätigung für Mitglieder Admin-Portal von ${SettingHelper.getSetting("club.name")}`,
`Öffne folgenden Link: ${origin}/${isSetup ? "setup" : "invite"}/verify?mail=${mail}&token=${token}`
);
res.sendStatus(204);
@ -92,7 +90,7 @@ export async function verifyInvite(req: Request, res: Response): Promise<any> {
let { secret, username } = await InviteService.getByMailAndToken(mail, token);
const url = `otpauth://totp/FF Admin ${CLUB_NAME}?secret=${secret}`;
const url = `otpauth://totp/FF Admin ${SettingHelper.getSetting("club.name")}?secret=${secret}`;
QRCode.toDataURL(url)
.then((result) => {
@ -115,20 +113,26 @@ export async function verifyInvite(req: Request, res: Response): Promise<any> {
*/
export async function finishInvite(req: Request, res: Response, grantAdmin: boolean = false): Promise<any> {
let mail = req.body.mail;
let routine = req.body.routine;
let token = req.body.token;
let totp = req.body.totp;
let passedSecret = req.body.secret;
let { secret, username, firstname, lastname } = await InviteService.getByMailAndToken(mail, token);
let valid = speakeasy.totp.verify({
secret: secret,
encoding: "base32",
token: totp,
window: 2,
});
let valid = false;
if (routine == LoginRoutineEnum.totp) {
valid = speakeasy.totp.verify({
secret: secret,
encoding: "base32",
token: passedSecret,
window: 2,
});
} else {
valid = passedSecret != "";
}
if (!valid) {
throw new UnauthorizedRequestException("Token not valid or expired");
throw new UnauthorizedRequestException("Credentials not valid or expired");
}
let createUser: CreateUserCommand = {
@ -136,8 +140,9 @@ export async function finishInvite(req: Request, res: Response, grantAdmin: bool
firstname: firstname,
lastname: lastname,
mail: mail,
secret: secret,
secret: routine == LoginRoutineEnum.totp ? secret : passedSecret,
isOwner: grantAdmin,
routine,
};
let id = await UserCommandHandler.create(createUser);

View file

@ -2,11 +2,13 @@ import { Request, Response } from "express";
import CalendarService from "../service/club/calendarService";
import CalendarTypeService from "../service/configuration/calendarTypeService";
import { calendar } from "../entity/club/calendar";
import { createEvents } from "ics";
import moment from "moment";
import InternalException from "../exceptions/internalException";
import CalendarFactory from "../factory/admin/club/calendar";
import { CalendarHelper } from "../helpers/calendarHelper";
import SettingHelper from "../helpers/settingsHelper";
import sharp from "sharp";
import ico from "sharp-ico";
import { FileSystemHelper } from "../helpers/fileSystemHelper";
/**
* @description get all calendar items by types or nscdr
@ -51,3 +53,155 @@ export async function getCalendarItemsByTypes(req: Request, res: Response): Prom
res.type("ics").send(value);
}
}
/**
* @description get configuration of UI
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getApplicationConfig(req: Request, res: Response): Promise<any> {
let config = {
"club.name": SettingHelper.getSetting("club.name"),
"club.imprint": SettingHelper.getSetting("club.imprint"),
"club.privacy": SettingHelper.getSetting("club.privacy"),
"club.website": SettingHelper.getSetting("club.website"),
"app.custom_login_message": SettingHelper.getSetting("app.custom_login_message"),
"app.show_link_to_calendar": SettingHelper.getSetting("app.show_link_to_calendar"),
};
res.json(config);
}
/**
* @description get application Manifest
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getApplicationManifest(req: Request, res: Response): Promise<any> {
const backendUrl = `${req.protocol}://${req.get("host")}`;
const frontenUrl = `${req.get("referer")}`;
const manifest = {
id: "ff_admin_webapp",
lang: "de",
name: SettingHelper.getSetting("club.name"),
short_name: SettingHelper.getSetting("club.name"),
theme_color: "#990b00",
display: "standalone",
orientation: "portrait-primary",
start_url: frontenUrl,
icons: [
{
src: `${backendUrl}/api/public/favicon.ico`,
sizes: "48x48",
type: "image/ico",
},
{
src: `${backendUrl}/api/public/icon.png?width=512&height=512`,
sizes: "512x512",
type: "image/png",
},
],
};
res.set({
"Access-Control-Allow-Origin": "*",
"Content-Type": "application/manifest+json",
});
res.json(manifest);
}
/**
* @description get application Logo
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getApplicationLogo(req: Request, res: Response): Promise<any> {
let setLogo = SettingHelper.getSetting("club.logo");
res.set({
"Access-Control-Allow-Origin": "*",
"Cross-Origin-Resource-Policy": "cross-origin",
"Cross-Origin-Embedder-Policy": "credentialless",
"Timing-Allow-Origin": "*",
});
if (setLogo != "" && FileSystemHelper.getFilesInDirectory("/app", ".png").includes("admin-icon.png")) {
res.sendFile(FileSystemHelper.formatPath("/app/admin-logo.png"));
} else {
res.sendFile(FileSystemHelper.readAssetFile("admin-logo.png", true));
}
}
/**
* @description get application Favicon
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getApplicationFavicon(req: Request, res: Response): Promise<any> {
let icon = FileSystemHelper.readAssetFile("icon.png", true);
let setLogo = SettingHelper.getSetting("club.icon");
if (setLogo != "" && FileSystemHelper.getFilesInDirectory("/app", ".png").includes("admin-icon.png")) {
icon = FileSystemHelper.formatPath("/app/admin-icon.png");
}
let image = await sharp(icon)
.resize(48, 48, {
fit: "inside",
})
.png()
.toBuffer();
let buffer = ico.encode([image]);
res.set({
"Access-Control-Allow-Origin": "*",
"Cross-Origin-Resource-Policy": "cross-origin",
"Cross-Origin-Embedder-Policy": "credentialless",
"Timing-Allow-Origin": "*",
"Content-Type": "image/x-icon",
});
res.send(buffer);
}
/**
* @description get application Icon
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getApplicationIcon(req: Request, res: Response): Promise<any> {
const width = parseInt((req.query.width as string) ?? "512");
const height = parseInt((req.query.height as string) ?? "512");
let icon = FileSystemHelper.readAssetFile("icon.png", true);
let setLogo = SettingHelper.getSetting("club.icon");
if (setLogo != "" && FileSystemHelper.getFilesInDirectory("/app", ".png").includes("admin-icon.png")) {
icon = FileSystemHelper.formatPath("/app/admin-icon.png");
}
let image = await sharp(icon)
.resize(width, height, {
fit: "inside",
})
.png()
.toBuffer();
res.set({
"Access-Control-Allow-Origin": "*",
"Cross-Origin-Resource-Policy": "cross-origin",
"Cross-Origin-Embedder-Policy": "credentialless",
"Timing-Allow-Origin": "*",
"Content-Type": "image/png",
});
res.send(image);
}

View file

@ -1,6 +1,5 @@
import { Request, Response } from "express";
import { JWTHelper } from "../helpers/jwtHelper";
import { JWTToken } from "../type/jwtTypes";
import InternalException from "../exceptions/internalException";
import RefreshCommandHandler from "../command/refreshCommandHandler";
import { CreateRefreshCommand } from "../command/refreshCommand";
@ -12,12 +11,10 @@ import ResetCommandHandler from "../command/resetCommandHandler";
import MailHelper from "../helpers/mailHelper";
import ResetService from "../service/resetService";
import UserService from "../service/management/userService";
import { CLUB_NAME } from "../env.defaults";
import PermissionHelper from "../helpers/permissionHelper";
import RolePermissionService from "../service/management/rolePermissionService";
import UserPermissionService from "../service/management/userPermissionService";
import { UpdateUserSecretCommand } from "../command/management/user/userCommand";
import UserCommandHandler from "../command/management/user/userCommandHandler";
import SettingHelper from "../helpers/settingsHelper";
import { LoginRoutineEnum } from "../enums/loginRoutineEnum";
/**
* @description request totp reset
@ -31,7 +28,7 @@ export async function startReset(req: Request, res: Response): Promise<any> {
let { mail } = await UserService.getByUsername(username);
var secret = speakeasy.generateSecret({ length: 20, name: `FF Admin ${CLUB_NAME}` });
var secret = speakeasy.generateSecret({ length: 20, name: `FF Admin ${SettingHelper.getSetting("club.name")}` });
let createReset: CreateResetCommand = {
username: username,
@ -43,7 +40,7 @@ export async function startReset(req: Request, res: Response): Promise<any> {
// sendmail
await MailHelper.sendMail(
mail,
`Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`,
`Email Bestätigung für Mitglieder Admin-Portal von ${SettingHelper.getSetting("club.name")}`,
`Öffne folgenden Link: ${origin}/reset/reset?mail=${mail}&token=${token}`
);
@ -62,7 +59,7 @@ export async function verifyReset(req: Request, res: Response): Promise<any> {
let { secret } = await ResetService.getByMailAndToken(mail, token);
const url = `otpauth://totp/FF Admin ${CLUB_NAME}?secret=${secret}`;
const url = `otpauth://totp/FF Admin ${SettingHelper.getSetting("club.name")}?secret=${secret}`;
QRCode.toDataURL(url)
.then((result) => {
@ -84,27 +81,34 @@ export async function verifyReset(req: Request, res: Response): Promise<any> {
*/
export async function finishReset(req: Request, res: Response): Promise<any> {
let mail = req.body.mail;
let routine = req.body.routine;
let token = req.body.token;
let totp = req.body.totp;
let passedSecret = req.body.secret;
let { secret, username } = await ResetService.getByMailAndToken(mail, token);
let valid = speakeasy.totp.verify({
secret: secret,
encoding: "base32",
token: totp,
window: 2,
});
let valid = false;
if (routine == LoginRoutineEnum.totp) {
valid = speakeasy.totp.verify({
secret: secret,
encoding: "base32",
token: passedSecret,
window: 2,
});
} else {
valid = passedSecret != "";
}
if (!valid) {
throw new UnauthorizedRequestException("Token not valid or expired");
throw new UnauthorizedRequestException("Credentials not valid or expired");
}
let { id } = await UserService.getByUsername(username);
let updateUserSecret: UpdateUserSecretCommand = {
id,
secret,
secret: routine == LoginRoutineEnum.totp ? secret : passedSecret,
routine,
};
await UserCommandHandler.updateSecret(updateUserSecret);

View file

@ -1,4 +1,7 @@
import { Request, Response } from "express";
import SettingHelper from "../helpers/settingsHelper";
import MailHelper from "../helpers/mailHelper";
import InternalException from "../exceptions/internalException";
/**
* @description Service is currently not configured
@ -9,3 +12,131 @@ import { Request, Response } from "express";
export async function isSetup(req: Request, res: Response): Promise<any> {
res.sendStatus(204);
}
/**
* @description set club identity
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function setClubIdentity(req: Request, res: Response): Promise<any> {
const name = req.body.name;
const imprint = req.body.imprint;
const privacy = req.body.privacy;
const website = req.body.website;
if (name) {
await SettingHelper.setSetting("club.name", name);
} else {
await SettingHelper.resetSetting("club.name");
}
if (imprint) {
await SettingHelper.setSetting("club.imprint", imprint);
} else {
await SettingHelper.resetSetting("club.imprint");
}
if (privacy) {
await SettingHelper.setSetting("club.privacy", privacy);
} else {
await SettingHelper.resetSetting("club.privacy");
}
if (website) {
await SettingHelper.setSetting("club.website", website);
} else {
await SettingHelper.resetSetting("club.website");
}
res.sendStatus(204);
}
/**
* @description set applucation icon and logo
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function uploadClubImages(req: Request, res: Response): Promise<any> {
if (req.files && !Array.isArray(req.files) && req.files.icon) {
await SettingHelper.setSetting("club.icon", "configured");
} else {
await SettingHelper.resetSetting("club.icon");
}
if (req.files && !Array.isArray(req.files) && req.files.logo) {
await SettingHelper.setSetting("club.logo", "configured");
} else {
await SettingHelper.resetSetting("club.logo");
}
res.sendStatus(204);
}
/**
* @description set app identity
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function setAppIdentity(req: Request, res: Response): Promise<any> {
const custom_login_message = req.body.custom_login_message;
const show_link_to_calendar = req.body.show_link_to_calendar;
if (custom_login_message) {
await SettingHelper.setSetting("app.custom_login_message", custom_login_message);
} else {
await SettingHelper.resetSetting("app.custom_login_message");
}
if (show_link_to_calendar == false || show_link_to_calendar == true) {
await SettingHelper.setSetting("app.show_link_to_calendar", show_link_to_calendar);
} else {
await SettingHelper.resetSetting("app.show_link_to_calendar");
}
res.sendStatus(204);
}
/**
* @description set app identity
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function setMailConfig(req: Request, res: Response): Promise<any> {
const mail = req.body.mail;
const username = req.body.username;
const password = req.body.password;
const host = req.body.host;
const port = req.body.port;
const secure = req.body.secure;
let checkMail = await MailHelper.checkMail(mail);
if (!checkMail) {
throw new InternalException("Mail is not valid");
}
let checkConfig = await MailHelper.verifyTransport({
user: username,
password,
host,
port,
secure,
});
if (!checkConfig) {
throw new InternalException("Config is not valid");
}
await SettingHelper.setSetting("mail.email", mail);
await SettingHelper.setSetting("mail.username", username);
await SettingHelper.setSetting("mail.password", password);
await SettingHelper.setSetting("mail.host", host);
await SettingHelper.setSetting("mail.port", port);
await SettingHelper.setSetting("mail.secure", secure);
res.sendStatus(204);
}

View file

@ -2,12 +2,17 @@ import { Request, Response } from "express";
import speakeasy from "speakeasy";
import QRCode from "qrcode";
import InternalException from "../exceptions/internalException";
import { CLUB_NAME } from "../env.defaults";
import UserService from "../service/management/userService";
import UserFactory from "../factory/admin/management/user";
import { TransferUserOwnerCommand, UpdateUserCommand } from "../command/management/user/userCommand";
import {
TransferUserOwnerCommand,
UpdateUserCommand,
UpdateUserSecretCommand,
} from "../command/management/user/userCommand";
import UserCommandHandler from "../command/management/user/userCommandHandler";
import ForbiddenRequestException from "../exceptions/forbiddenRequestException";
import SettingHelper from "../helpers/settingsHelper";
import { LoginRoutineEnum } from "../enums/loginRoutineEnum";
/**
* @description get my by id
@ -22,6 +27,21 @@ export async function getMeById(req: Request, res: Response): Promise<any> {
res.json(UserFactory.mapToSingle(user));
}
/**
* @description get my routine by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getMyRoutine(req: Request, res: Response): Promise<any> {
const id = req.userId;
let user = await UserService.getById(id);
res.json({
routine: user.routine,
});
}
/**
* @description get my totp
* @param req {Request} Express req object
@ -31,9 +51,9 @@ export async function getMeById(req: Request, res: Response): Promise<any> {
export async function getMyTotp(req: Request, res: Response): Promise<any> {
const userId = req.userId;
let { secret } = await UserService.getById(userId);
let { secret, routine } = await UserService.getUserSecretAndRoutine(userId);
const url = `otpauth://totp/FF Admin ${CLUB_NAME}?secret=${secret}`;
const url = `otpauth://totp/FF Admin ${SettingHelper.getSetting("club.name")}?secret=${secret}`;
QRCode.toDataURL(url)
.then((result) => {
@ -57,7 +77,12 @@ export async function verifyMyTotp(req: Request, res: Response): Promise<any> {
const userId = req.userId;
let totp = req.body.totp;
let { secret } = await UserService.getById(userId);
let { secret, routine } = await UserService.getUserSecretAndRoutine(userId);
if (routine != LoginRoutineEnum.totp) {
throw new ForbiddenRequestException("only allowed for totp login");
}
let valid = speakeasy.totp.verify({
secret: secret,
encoding: "base32",
@ -71,6 +96,106 @@ export async function verifyMyTotp(req: Request, res: Response): Promise<any> {
res.sendStatus(204);
}
/**
* @description change my password
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function changeMyPassword(req: Request, res: Response): Promise<any> {
const userId = req.userId;
let current = req.body.current;
let newpassword = req.body.newpassword;
let { secret, routine } = await UserService.getUserSecretAndRoutine(userId);
if (routine == LoginRoutineEnum.password && current != secret) {
throw new ForbiddenRequestException("passwords do not match");
}
let updateUser: UpdateUserSecretCommand = {
id: userId,
secret: newpassword,
routine: LoginRoutineEnum.password,
};
await UserCommandHandler.updateSecret(updateUser);
res.sendStatus(204);
}
/**
* @description get change to totp
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getChangeToTOTP(req: Request, res: Response): Promise<any> {
var secret = speakeasy.generateSecret({ length: 20, name: `FF Admin ${SettingHelper.getSetting("club.name")}` });
QRCode.toDataURL(secret.otpauth_url)
.then((result) => {
res.json({
dataUrl: result,
otp: secret.base32,
});
})
.catch((err) => {
throw new InternalException("QRCode not created", err);
});
}
/**
* @description change to totp
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function changeToTOTP(req: Request, res: Response): Promise<any> {
const userId = req.userId;
let otp = req.body.otp;
let totp = req.body.totp;
let valid = speakeasy.totp.verify({
secret: otp,
encoding: "base32",
token: totp,
window: 2,
});
if (!valid) {
throw new InternalException("Token not valid or expired");
}
let updateUser: UpdateUserSecretCommand = {
id: userId,
secret: otp,
routine: LoginRoutineEnum.totp,
};
await UserCommandHandler.updateSecret(updateUser);
res.sendStatus(204);
}
/**
* @description change to password
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function changeToPW(req: Request, res: Response): Promise<any> {
const userId = req.userId;
let newpassword = req.body.newpassword;
let updateUser: UpdateUserSecretCommand = {
id: userId,
secret: newpassword,
routine: LoginRoutineEnum.password,
};
await UserCommandHandler.updateSecret(updateUser);
res.sendStatus(204);
}
/**
* @description transferOwnership
* @param req {Request} Express req object

View file

@ -1,13 +1,5 @@
import { Request, Response } from "express";
import { JWTHelper } from "../helpers/jwtHelper";
import { JWTToken } from "../type/jwtTypes";
import InternalException from "../exceptions/internalException";
import RefreshCommandHandler from "../command/refreshCommandHandler";
import { CreateRefreshCommand, DeleteRefreshCommand } from "../command/refreshCommand";
import UserService from "../service/management/userService";
import speakeasy from "speakeasy";
import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
import RefreshService from "../service/refreshService";
import WebapiService from "../service/management/webapiService";
import ForbiddenRequestException from "../exceptions/forbiddenRequestException";
import WebapiCommandHandler from "../command/management/webapi/webapiCommandHandler";

View file

@ -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 { configCheck, DB_HOST, DB_NAME, DB_PASSWORD, DB_PORT, DB_USERNAME } from "./env.defaults";
import { user } from "./entity/management/user";
import { refresh } from "./entity/refresh";
@ -34,7 +34,7 @@ import { query } from "./entity/configuration/query";
import { memberView } from "./views/memberView";
import { memberExecutivePositionsView } from "./views/memberExecutivePositionView";
import { memberQualificationsView } from "./views/memberQualificationsView";
import { membershipView } from "./views/membershipsView";
import { membershipTotalView, membershipView } from "./views/membershipsView";
import { template } from "./entity/configuration/template";
import { templateUsage } from "./entity/configuration/templateUsage";
import { newsletter } from "./entity/club/newsletter/newsletter";
@ -44,16 +44,17 @@ import { newsletterConfig } from "./entity/configuration/newsletterConfig";
import { webapi } from "./entity/management/webapi";
import { webapiPermission } from "./entity/management/webapi_permission";
import { salutation } from "./entity/configuration/salutation";
import { setting } from "./entity/management/setting";
import { education } from "./entity/configuration/education";
import { memberEducations } from "./entity/club/member/memberEducations";
import { BackupAndResetDatabase1738166124200 } from "./migrations/1738166124200-BackupAndResetDatabase";
import { CreateSchema1738166167472 } from "./migrations/1738166167472-CreateSchema";
import { TemplatesAndProtocolSort1742549956787 } from "./migrations/1742549956787-templatesAndProtocolSort";
import { QueryToUUID1742922178643 } from "./migrations/1742922178643-queryToUUID";
import { NewsletterColumnType1744351418751 } from "./migrations/1744351418751-newsletterColumnType";
import { QueryUpdatedAt1744795756230 } from "./migrations/1744795756230-QueryUpdatedAt";
import { BackupAndResetDatabase1749296262915 } from "./migrations/1749296262915-BackupAndResetDatabase";
import { CreateSchema1749296280721 } from "./migrations/1749296280721-CreateSchema";
configCheck();
const dataSource = new DataSource({
type: DB_TYPE as any,
type: "postgres",
host: DB_HOST,
port: DB_PORT,
username: DB_USERNAME,
@ -61,7 +62,6 @@ const dataSource = new DataSource({
database: DB_NAME,
synchronize: false,
logging: process.env.NODE_ENV ? true : ["schema", "error", "warn", "log", "migration"],
bigNumberStrings: false,
entities: [
user,
refresh,
@ -75,6 +75,7 @@ const dataSource = new DataSource({
communicationType,
executivePosition,
membershipStatus,
education,
qualification,
salutation,
member,
@ -82,6 +83,7 @@ const dataSource = new DataSource({
memberExecutivePositions,
memberQualifications,
membership,
memberEducations,
protocol,
protocolAgenda,
protocolDecision,
@ -101,17 +103,12 @@ const dataSource = new DataSource({
memberExecutivePositionsView,
memberQualificationsView,
membershipView,
membershipTotalView,
webapi,
webapiPermission,
setting,
],
migrations: [
BackupAndResetDatabase1738166124200,
CreateSchema1738166167472,
TemplatesAndProtocolSort1742549956787,
QueryToUUID1742922178643,
NewsletterColumnType1744351418751,
QueryUpdatedAt1744795756230,
],
migrations: [BackupAndResetDatabase1749296262915, CreateSchema1749296280721],
migrationsRun: true,
migrationsTransactionMode: "each",
subscribers: [],

View file

@ -7,7 +7,7 @@ export const protocolDemoData: {
title: string;
summary: string;
iteration: number;
date: string;
date: Date;
start: string;
end: string;
agenda: Array<Partial<protocolAgenda>>;
@ -19,12 +19,7 @@ export const protocolDemoData: {
title: "Beispiel Protokoll Daten",
summary: "Zusammenfassung der Demodaten.",
iteration: 1,
date: new Date().toLocaleDateString("de-DE", {
weekday: "long",
day: "2-digit",
month: "2-digit",
year: "numeric",
}),
date: new Date(),
start: "19:00:00",
end: "21:00:00",
agenda: [

View file

@ -45,7 +45,7 @@ export class calendar {
@UpdateDateColumn()
updatedAt: Date;
@Column({ type: "varchar", nullable: true, default: null, unique: true })
@Column({ type: "varchar", length: "255", nullable: true, default: null, unique: true })
webpageId: string;
@ManyToOne(() => calendarType, (t) => t.calendar, {

View file

@ -1,4 +1,15 @@
import { Column, ColumnType, Entity, JoinColumn, ManyToOne, OneToMany, OneToOne, PrimaryColumn } from "typeorm";
import {
Column,
ColumnType,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
OneToMany,
OneToOne,
PrimaryColumn,
PrimaryGeneratedColumn,
} from "typeorm";
import { membership } from "./membership";
import { memberAwards } from "./memberAwards";
import { memberQualifications } from "./memberQualifications";
@ -6,10 +17,11 @@ import { memberExecutivePositions } from "./memberExecutivePositions";
import { communication } from "./communication";
import { salutation } from "../../configuration/salutation";
import { getTypeByORM } from "../../../migrations/ormHelper";
import { memberEducations } from "./memberEducations";
@Entity()
export class member {
@PrimaryColumn({ generated: "uuid", type: "varchar" })
@PrimaryGeneratedColumn("uuid")
id: string;
@Column({ type: "varchar", length: 255 })
@ -27,9 +39,15 @@ export class member {
@Column({ type: "varchar", length: 255, unique: true, nullable: true })
internalId?: string;
@Column({ type: "varchar", length: 255, nullable: true })
note?: string;
@Column()
salutationId: number;
@CreateDateColumn()
createdAt: Date;
@ManyToOne(() => salutation, (salutation) => salutation.members, {
nullable: false,
onDelete: "RESTRICT",
@ -53,6 +71,9 @@ export class member {
@OneToMany(() => memberQualifications, (qualifications) => qualifications.member, { cascade: ["insert"] })
qualifications: memberQualifications[];
@OneToMany(() => memberEducations, (educations) => educations.member, { cascade: ["insert"] })
educations: memberEducations[];
firstMembershipEntry?: membership;
lastMembershipEntry?: membership;
preferredCommunication?: Array<communication>;

View file

@ -0,0 +1,43 @@
import { Column, ColumnType, Entity, ManyToOne, PrimaryColumn } from "typeorm";
import { member } from "./member";
import { education } from "../../configuration/education";
import { getTypeByORM } from "../../../migrations/ormHelper";
@Entity()
export class memberEducations {
@PrimaryColumn({ generated: "increment", type: "int" })
id: number;
@Column({ type: getTypeByORM("date").type as ColumnType })
start: Date;
@Column({ type: getTypeByORM("date").type as ColumnType, nullable: true })
end?: Date;
@Column({ type: "varchar", length: 255, nullable: true })
note?: string;
@Column({ type: "varchar", length: 255, nullable: true })
place?: string;
@Column()
memberId: string;
@Column()
educationId: number;
@ManyToOne(() => member, (member) => member.awards, {
nullable: false,
onDelete: "CASCADE",
onUpdate: "RESTRICT",
})
member: member;
@ManyToOne(() => education, (education) => education.members, {
nullable: false,
onDelete: "RESTRICT",
onUpdate: "RESTRICT",
cascade: ["insert"],
})
education: education;
}

View file

@ -1,4 +1,4 @@
import { Column, Entity, ManyToOne, OneToMany, PrimaryColumn } from "typeorm";
import { Column, CreateDateColumn, Entity, ManyToOne, OneToMany, PrimaryColumn } from "typeorm";
import { newsletterDates } from "./newsletterDates";
import { newsletterRecipients } from "./newsletterRecipients";
import { query } from "../../configuration/query";
@ -14,6 +14,9 @@ export class newsletter {
@Column({ type: "varchar", length: 255, default: "" })
description: string;
@CreateDateColumn()
createdAt: Date;
@Column({ type: "text", default: "" })
newsletterTitle: string;

View file

@ -0,0 +1,17 @@
import { Column, Entity, OneToMany, PrimaryColumn } from "typeorm";
import { memberEducations } from "../club/member/memberEducations";
@Entity()
export class education {
@PrimaryColumn({ generated: "increment", type: "int" })
id: number;
@Column({ type: "varchar", length: 255, unique: true })
education: string;
@Column({ type: "varchar", length: 255, nullable: true })
description?: string;
@OneToMany(() => memberEducations, (memberEducations) => memberEducations.education)
members: memberEducations[];
}

View file

@ -1,5 +1,5 @@
import { Column, Entity, ManyToOne, PrimaryColumn } from "typeorm";
import { NewsletterConfigType } from "../../enums/newsletterConfigType";
import { NewsletterConfigEnum } from "../../enums/newsletterConfigEnum";
import { communicationType } from "./communicationType";
@Entity()
@ -11,15 +11,15 @@ export class newsletterConfig {
type: "varchar",
length: "255",
transformer: {
to(value: NewsletterConfigType) {
to(value: NewsletterConfigEnum) {
return value.toString();
},
from(value: string) {
return NewsletterConfigType[value as keyof typeof NewsletterConfigType];
return NewsletterConfigEnum[value as keyof typeof NewsletterConfigEnum];
},
},
})
config: NewsletterConfigType;
config: NewsletterConfigEnum;
@ManyToOne(() => communicationType, {
nullable: false,

View file

@ -1,8 +1,8 @@
import { Column, Entity, PrimaryColumn, UpdateDateColumn } from "typeorm";
import { Column, Entity, PrimaryColumn, PrimaryGeneratedColumn, UpdateDateColumn } from "typeorm";
@Entity()
export class query {
@PrimaryColumn({ generated: "uuid", type: "varchar" })
@PrimaryGeneratedColumn("uuid")
id: string;
@Column({ type: "varchar", length: 255, unique: true })

View file

@ -0,0 +1,13 @@
import { Column, Entity, PrimaryColumn } from "typeorm";
@Entity()
export class setting {
@PrimaryColumn({ type: "varchar", length: 255 })
topic: string;
@PrimaryColumn({ type: "varchar", length: 255 })
key: string;
@Column({ type: "text" })
value: string;
}

View file

@ -1,10 +1,13 @@
import { Column, Entity, JoinTable, ManyToMany, OneToMany, PrimaryColumn } from "typeorm";
import { Column, Entity, JoinTable, ManyToMany, OneToMany, PrimaryColumn, PrimaryGeneratedColumn } from "typeorm";
import { role } from "./role";
import { userPermission } from "./user_permission";
import { LoginRoutineEnum } from "../../enums/loginRoutineEnum";
import { CodingHelper } from "../../helpers/codingHelper";
import { APPLICATION_SECRET } from "../../env.defaults";
@Entity()
export class user {
@PrimaryColumn({ generated: "uuid", type: "varchar" })
@PrimaryGeneratedColumn("uuid")
id: string;
@Column({ type: "varchar", unique: true, length: 255 })
@ -19,11 +22,27 @@ export class user {
@Column({ type: "varchar", length: 255 })
lastname: string;
@Column({ type: "varchar", length: 255 })
@Column({
type: "text",
select: false,
transformer: CodingHelper.entityBaseCoding(APPLICATION_SECRET, "<self>"),
})
secret: string;
@Column({ type: "boolean", default: false })
static: boolean;
@Column({
type: "varchar",
length: "255",
default: LoginRoutineEnum.totp,
transformer: {
to(value: LoginRoutineEnum) {
return value.toString();
},
from(value: string) {
return LoginRoutineEnum[value as keyof typeof LoginRoutineEnum];
},
},
})
routine: LoginRoutineEnum;
@Column({ type: "boolean", default: false })
isOwner: boolean;

View file

@ -0,0 +1,4 @@
export enum LoginRoutineEnum {
password = "password", // login with self defined password
totp = "totp", // login with totp by auth apps
}

View file

@ -0,0 +1,5 @@
export enum NewsletterConfigEnum {
pdf = "pdf",
mail = "mail",
none = "none",
}

View file

@ -1,4 +0,0 @@
export enum NewsletterConfigType {
pdf = "pdf",
mail = "mail",
}

View file

@ -2,32 +2,15 @@ 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_PORT = Number(process.env.DB_PORT ?? 5432);
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;
export const PWA_REFRESH_EXPIRATION = (process.env.PWA_REFRESH_EXPIRATION ?? "5d") as ms.StringValue;
export const MAIL_USERNAME = process.env.MAIL_USERNAME ?? "";
export const MAIL_PASSWORD = process.env.MAIL_PASSWORD ?? "";
export const MAIL_HOST = process.env.MAIL_HOST ?? "";
export const MAIL_PORT = Number(process.env.MAIL_PORT ?? "587");
export const MAIL_SECURE = process.env.MAIL_SECURE ?? "false";
export const CLUB_NAME = process.env.CLUB_NAME ?? "FF Admin";
export const CLUB_WEBSITE = process.env.CLUB_WEBSITE ?? "";
export const BACKUP_INTERVAL = Number(process.env.BACKUP_INTERVAL ?? "1");
export const BACKUP_COPIES = Number(process.env.BACKUP_COPIES ?? "7");
export const BACKUP_AUTO_RESTORE = process.env.BACKUP_AUTO_RESTORE ?? "true";
export const APPLICATION_SECRET = process.env.APPLICATION_SECRET ?? "";
export const USE_SECURITY_STRICT_LIMIT = process.env.USE_SECURITY_STRICT_LIMIT ?? "true";
export const SECURITY_STRICT_LIMIT_WINDOW = (process.env.SECURITY_STRICT_LIMIT_WINDOW ?? "15m") as ms.StringValue;
@ -55,40 +38,15 @@ export const TRUST_PROXY = ((): Array<string> | 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_HOST == "" || typeof DB_HOST != "string") 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 (DB_USERNAME == "" || typeof DB_USERNAME != "string") throw new Error("set valid value to DB_USERNAME");
if (DB_PASSWORD == "" || typeof DB_PASSWORD != "string") throw new Error("set valid value to DB_PASSWORD");
if (APPLICATION_SECRET == "") throw new Error("set valid APPLICATION_SECRET");
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");
checkMS(PWA_REFRESH_EXPIRATION, "PWA_REFRESH_EXPIRATION");
if (MAIL_USERNAME == "" || typeof MAIL_USERNAME != "string") throw new Error("set valid value to MAIL_USERNAME");
if (MAIL_PASSWORD == "" || typeof MAIL_PASSWORD != "string") throw new Error("set valid value to MAIL_PASSWORD");
if (MAIL_HOST == "" || typeof MAIL_HOST != "string") throw new Error("set valid value to MAIL_HOST");
if (isNaN(MAIL_PORT)) throw new Error("set valid numeric value to MAIL_PORT");
if (MAIL_SECURE != "true" && MAIL_SECURE != "false") throw new Error("set 'true' or 'false' to MAIL_SECURE");
if (
CLUB_WEBSITE != "" &&
!/^(http(s):\/\/.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/.test(CLUB_WEBSITE)
)
throw new Error("CLUB_WEBSITE is not valid url");
if (BACKUP_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");
if (USE_SECURITY_STRICT_LIMIT != "true" && USE_SECURITY_STRICT_LIMIT != "false")
throw new Error("set 'true' or 'false' to USE_SECURITY_STRICT_LIMIT");
checkMS(SECURITY_STRICT_LIMIT_WINDOW, "SECURITY_STRICT_LIMIT_WINDOW");

View file

@ -2,7 +2,7 @@ import CustomRequestException from "./customRequestException";
export default class DatabaseActionException extends CustomRequestException {
constructor(action: string, table: string, err: any) {
let errstring = `${action} on ${table} with ${err?.code ?? "XX"} at ${err?.sqlMessage ?? "XX"}`;
let errstring = `${action} on ${table} with ${err?.code ?? "XX"} at ${err?.sqlMessage ?? err?.message ?? "XX"}`;
super(500, errstring, err);
}
}

View file

@ -1,14 +1,5 @@
import { DB_TYPE } from "../../../../env.defaults";
export default abstract class DateMappingHelper {
static mapDate(entry: any) {
switch (DB_TYPE) {
case "postgres":
return `${entry?.years ?? 0} years ${entry?.months ?? 0} months ${entry?.days ?? 0} days`;
case "mysql":
return entry.toString();
case "sqlite":
return entry;
}
return `${entry?.years ?? 0} years ${entry?.months ?? 0} months ${entry?.days ?? 0} days`;
}
}

View file

@ -20,7 +20,8 @@ export default abstract class MemberFactory {
lastname: record?.lastname,
nameaffix: record?.nameaffix,
birthdate: record?.birthdate,
internalId: record.internalId,
internalId: record?.internalId,
note: record?.note,
firstMembershipEntry: record?.firstMembershipEntry
? MembershipFactory.mapToSingle(record.firstMembershipEntry)
: null,

View file

@ -0,0 +1,30 @@
import { memberEducations } from "../../../../entity/club/member/memberEducations";
import { MemberEducationViewModel } from "../../../../viewmodel/admin/club/member/memberEducation.models";
export default abstract class MemberEducationFactory {
/**
* @description map record to memberEducation
* @param {memberEducation} record
* @returns {MemberEducationViewModel}
*/
public static mapToSingle(record: memberEducations): MemberEducationViewModel {
return {
id: record.id,
start: record.start,
end: record.end,
note: record.note,
place: record.place,
education: record.education.education,
educationId: record.education.id,
};
}
/**
* @description map records to memberEducation
* @param {Array<memberEducation>} records
* @returns {Array<MemberEducationViewModel>}
*/
public static mapToBase(records: Array<memberEducations>): Array<MemberEducationViewModel> {
return records.map((r) => this.mapToSingle(r));
}
}

View file

@ -1,9 +1,10 @@
import { membership } from "../../../../entity/club/member/membership";
import {
MembershipStatisticsViewModel,
MembershipTotalStatisticsViewModel,
MembershipViewModel,
} from "../../../../viewmodel/admin/club/member/membership.models";
import { membershipView } from "../../../../views/membershipsView";
import { membershipTotalView, membershipView } from "../../../../views/membershipsView";
import DateMappingHelper from "./dateMappingHelper";
export default abstract class MembershipFactory {
@ -53,6 +54,25 @@ export default abstract class MembershipFactory {
};
}
/**
* @description map view record to MembershipTotalStatisticsViewModel
* @param {membershipTotalView} record
* @returns {MembershipTotalStatisticsViewModel}
*/
public static mapToSingleTotalStatistic(record: membershipTotalView): MembershipTotalStatisticsViewModel {
return {
durationInDays: record.durationInDays,
durationInYears: record.durationInYears,
exactDuration: DateMappingHelper.mapDate(record.exactDuration),
memberId: record.memberId,
memberSalutation: record.memberSalutation,
memberFirstname: record.memberFirstname,
memberLastname: record.memberLastname,
memberNameaffix: record.memberNameaffix,
memberBirthdate: record.memberBirthdate,
};
}
/**
* @description map records to MembershipStatisticsViewModel
* @param {Array<membershipView>} records

View file

@ -18,7 +18,7 @@ export default abstract class NewsletterFactory {
newsletterSignatur: record.newsletterSignatur,
isSent: record.isSent,
recipientsByQueryId: record?.recipientsByQuery ? record.recipientsByQuery.id : null,
recipientsByQuery: record?.recipientsByQuery ? QueryStoreFactory.mapToSingle(record.recipientsByQuery) : null,
createdAt: record.createdAt,
};
}

View file

@ -0,0 +1,26 @@
import { education } from "../../../entity/configuration/education";
import { EducationViewModel } from "../../../viewmodel/admin/configuration/education.models";
export default abstract class EducationFactory {
/**
* @description map record to education
* @param {education} record
* @returns {AwardViewModel}
*/
public static mapToSingle(record: education): EducationViewModel {
return {
id: record.id,
education: record.education,
description: record.description,
};
}
/**
* @description map records to education
* @param {Array<education>} records
* @returns {Array<AwardViewModel>}
*/
public static mapToBase(records: Array<education>): Array<EducationViewModel> {
return records.map((r) => this.mapToSingle(r));
}
}

View file

@ -8,6 +8,14 @@ Handlebars.registerHelper("date", function (aString) {
});
});
Handlebars.registerHelper("weekdayDayMonth", function (aString) {
return new Date(aString).toLocaleDateString("de-DE", {
weekday: "long",
day: "2-digit",
month: "long",
});
});
Handlebars.registerHelper("longdate", function (aString) {
return new Date(aString).toLocaleDateString("de-DE", {
weekday: "long",
@ -27,6 +35,27 @@ Handlebars.registerHelper("datetime", function (aString) {
});
});
Handlebars.registerHelper("longdatetime", function (aString) {
return new Date(aString).toLocaleDateString("de-DE", {
day: "2-digit",
month: "long",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
});
Handlebars.registerHelper("longdatetimeWithWeekday", function (aString) {
return new Date(aString).toLocaleDateString("de-DE", {
weekday: "long",
day: "2-digit",
month: "long",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
});
Handlebars.registerHelper("json", function (context) {
return JSON.stringify(context);
});

View file

@ -4,9 +4,11 @@ import { EntityManager } from "typeorm";
import uniqBy from "lodash.uniqby";
import InternalException from "../exceptions/internalException";
import UserService from "../service/management/userService";
import { BACKUP_COPIES, BACKUP_INTERVAL } from "../env.defaults";
import DatabaseActionException from "../exceptions/databaseActionException";
import { availableTemplates } from "../type/templateTypes";
import SettingHelper from "./settingsHelper";
import { LoginRoutineEnum } from "../enums/loginRoutineEnum";
import { education } from "../entity/configuration/education";
export type BackupSection =
| "member"
@ -18,7 +20,8 @@ export type BackupSection =
| "query"
| "template"
| "user"
| "webapi";
| "webapi"
| "settings";
export type BackupSectionRefered = {
[key in BackupSection]?: Array<string>;
@ -42,6 +45,7 @@ export default abstract class BackupHelper {
{ type: "template", orderOnInsert: 2, orderOnClear: 1 }, // INSERT depends on member com
{ type: "user", orderOnInsert: 1, orderOnClear: 1 },
{ type: "webapi", orderOnInsert: 1, orderOnClear: 1 },
{ type: "settings", orderOnInsert: 1, orderOnClear: 1 },
];
private static readonly backupSectionRefered: BackupSectionRefered = {
@ -52,6 +56,7 @@ export default abstract class BackupHelper {
"member_executive_positions",
"membership",
"communication",
"member_educations",
],
memberBase: [
"award",
@ -60,6 +65,7 @@ export default abstract class BackupHelper {
"membership_status",
"communication_type",
"salutation",
"education",
],
protocol: [
"protocol",
@ -76,6 +82,7 @@ export default abstract class BackupHelper {
template: ["template", "template_usage"],
user: ["user", "user_permission", "role", "role_permission", "invite"],
webapi: ["webapi", "webapi_permission"],
settings: ["setting"],
};
private static transactionManager: EntityManager;
@ -103,7 +110,7 @@ export default abstract class BackupHelper {
let files = FileSystemHelper.getFilesInDirectory("backup", ".json");
let sorted = files.sort((a, b) => new Date(b.split(".")[0]).getTime() - new Date(a.split(".")[0]).getTime());
const filesToDelete = sorted.slice(BACKUP_COPIES);
const filesToDelete = sorted.slice(SettingHelper.getSetting("backup.copies"));
for (const file of filesToDelete) {
FileSystemHelper.deleteFile("backup", file);
}
@ -117,7 +124,7 @@ export default abstract class BackupHelper {
let diffInMs = new Date().getTime() - lastBackup.getTime();
let diffInDays = diffInMs / (1000 * 60 * 60 * 24);
if (diffInDays >= BACKUP_INTERVAL) {
if (diffInDays >= SettingHelper.getSetting("backup.interval")) {
await this.createBackup({});
}
}
@ -220,6 +227,8 @@ export default abstract class BackupHelper {
return await this.getUser(collectIds);
case "webapi":
return await this.getWebapi();
case "settings":
return await this.getSettings();
default:
return [];
}
@ -240,6 +249,8 @@ export default abstract class BackupHelper {
.leftJoin("positions.executivePosition", "executivePosition")
.leftJoin("member.qualifications", "qualifications")
.leftJoin("qualifications.qualification", "qualification")
.leftJoin("member.educations", "educations")
.leftJoin("educations.education", "education")
.select([
...(collectIds ? ["member.id"] : []),
"member.firstname",
@ -247,6 +258,7 @@ export default abstract class BackupHelper {
"member.nameaffix",
"member.birthdate",
"member.internalId",
"member.note",
])
.addSelect(["salutation.salutation"])
.addSelect([
@ -274,6 +286,14 @@ export default abstract class BackupHelper {
"qualification.qualification",
"qualification.description",
])
.addSelect([
"educations.start",
"educations.end",
"educations.place",
"educations.note",
"education.education",
"education.description",
])
.getMany();
}
private static async getMemberBase(): Promise<{ [key: string]: Array<any> }> {
@ -288,6 +308,7 @@ export default abstract class BackupHelper {
qualification: await dataSource
.getRepository("qualification")
.find({ select: { qualification: true, description: true } }),
education: await dataSource.getRepository("education").find({ select: { education: true, description: true } }),
};
}
private static async getProtocol(collectIds: boolean): Promise<Array<any>> {
@ -338,6 +359,7 @@ export default abstract class BackupHelper {
"newsletter.newsletterText",
"newsletter.newsletterSignatur",
"newsletter.isSent",
"newsletter.createdAt",
])
.addSelect(["dates.calendarId", "dates.diffTitle", "dates.diffDescription"])
.addSelect(["recipients.memberId"])
@ -435,6 +457,7 @@ export default abstract class BackupHelper {
"user.firstname",
"user.lastname",
"user.secret",
"user.routine",
"user.isOwner",
])
.addSelect(["permissions.permission"])
@ -460,6 +483,13 @@ export default abstract class BackupHelper {
.addSelect(["permissions.permission"])
.getMany();
}
private static async getSettings(): Promise<Array<any>> {
return await dataSource
.getRepository("setting")
.createQueryBuilder("setting")
.select(["setting.topic", "setting.key", "setting.value"])
.getMany();
}
private static async setSectionData(
section: BackupSection,
@ -476,6 +506,7 @@ export default abstract class BackupHelper {
if (section == "template" && !Array.isArray(data)) await this.setTemplate(data);
if (section == "user" && !Array.isArray(data)) await this.setUser(data);
if (section == "webapi" && Array.isArray(data)) await this.setWebapi(data);
if (section == "settings" && Array.isArray(data)) await this.setSettings(data);
}
private static async setMemberData(data: Array<any>): Promise<void> {
@ -519,6 +550,13 @@ export default abstract class BackupHelper {
.map((d) => ({ ...d, id: undefined })),
"qualification"
),
education: uniqBy(
data
.map((d) => (d.education ?? []).map((c: any) => c.education))
.flat()
.map((d) => ({ ...d, id: undefined })),
"education"
),
});
let salutation = await this.transactionManager.getRepository("salutation").find();
@ -526,6 +564,7 @@ export default abstract class BackupHelper {
let membership = await this.transactionManager.getRepository("membership_status").find();
let award = await this.transactionManager.getRepository("award").find();
let qualification = await this.transactionManager.getRepository("qualification").find();
let education = await this.transactionManager.getRepository("education").find();
let position = await this.transactionManager.getRepository("executive_position").find();
let dataWithMappedIds = data.map((d) => ({
...d,
@ -568,6 +607,13 @@ export default abstract class BackupHelper {
id: qualification.find((iq) => iq.qualification == q.qualification.qualification)?.id ?? undefined,
},
})),
educations: (d.educations ?? []).map((e: any) => ({
...e,
education: {
...e.education,
id: education.find((id) => id.education == e.education.education)?.id ?? undefined,
},
})),
}));
await this.transactionManager.getRepository("member").save(dataWithMappedIds);
}
@ -578,6 +624,7 @@ export default abstract class BackupHelper {
let award = await this.transactionManager.getRepository("award").find();
let qualification = await this.transactionManager.getRepository("qualification").find();
let position = await this.transactionManager.getRepository("executive_position").find();
let education = await this.transactionManager.getRepository("education").find();
await this.transactionManager
.createQueryBuilder()
@ -619,10 +666,19 @@ export default abstract class BackupHelper {
.insert()
.into("qualification")
.values(
(data?.["qualification"] ?? []).filter((d) => !qualification.map((q) => q.award).includes(d.qualification))
(data?.["qualification"] ?? []).filter(
(d) => !qualification.map((q) => q.qualification).includes(d.qualification)
)
)
.orIgnore()
.execute();
await this.transactionManager
.createQueryBuilder()
.insert()
.into("education")
.values((data?.["education"] ?? []).filter((d) => !education.map((q) => q.education).includes(d.education)))
.orIgnore()
.execute();
}
private static async setProtocol(data: Array<any>, collectedIds: boolean): Promise<void> {
let members = await this.transactionManager.getRepository("member").find();
@ -755,11 +811,11 @@ export default abstract class BackupHelper {
.filter((d) => availableTemplates.includes(d.scope))
.map((d) => ({
...d,
headerHeightId: templates.find((template) => template.template == d.headerHeight.template)?.id ?? null,
footerHeightId: templates.find((template) => template.template == d.footerHeight.template)?.id ?? null,
headerId: templates.find((template) => template.template == d.header.template)?.id ?? null,
bodyId: templates.find((template) => template.template == d.body.template)?.id ?? null,
footerId: templates.find((template) => template.template == d.footer.template)?.id ?? null,
headerHeightId: templates.find((template) => template.template == d.headerHeight)?.id ?? null,
footerHeightId: templates.find((template) => template.template == d.footerHeight)?.id ?? null,
headerId: templates.find((template) => template.template == d.header?.template)?.id ?? null,
bodyId: templates.find((template) => template.template == d.body?.template)?.id ?? null,
footerId: templates.find((template) => template.template == d.footer?.template)?.id ?? null,
}));
availableTemplates.forEach((at) => {
if (!dataWithMappedId.some((d) => d.scope == at)) {
@ -793,6 +849,7 @@ export default abstract class BackupHelper {
let roles = await this.transactionManager.getRepository("role").find();
let dataWithMappedIds = (data?.["user"] ?? []).map((u) => ({
...u,
routine: u.routine ?? LoginRoutineEnum.totp,
roles: u.roles.map((r: any) => ({
...r,
id: roles.find((role) => role.role == r.role)?.id ?? undefined,
@ -810,4 +867,7 @@ export default abstract class BackupHelper {
private static async setWebapi(data: Array<any>): Promise<void> {
await this.transactionManager.getRepository("webapi").save(data);
}
private static async setSettings(data: Array<any>): Promise<void> {
await this.transactionManager.getRepository("setting").save(data);
}
}

View file

@ -1,7 +1,7 @@
import { createEvents } from "ics";
import { calendar } from "../entity/club/calendar";
import moment from "moment";
import { CLUB_NAME, CLUB_WEBSITE, MAIL_USERNAME } from "../env.defaults";
import SettingHelper from "./settingsHelper";
export abstract class CalendarHelper {
public static buildICS(entries: Array<calendar>): { error?: Error; value?: string } {
@ -35,7 +35,10 @@ export abstract class CalendarHelper {
description: i.content,
location: i.location,
categories: [i.type.type],
organizer: { name: CLUB_NAME, email: MAIL_USERNAME },
organizer: {
name: SettingHelper.getSetting("club.name"),
email: SettingHelper.getSetting("mail.username"),
},
created: moment(i.createdAt)
.format("YYYY-M-D-H-m")
.split("-")
@ -46,7 +49,7 @@ export abstract class CalendarHelper {
.map((a) => parseInt(a)) as [number, number, number, number, number],
transp: "OPAQUE" as "OPAQUE",
status: "CONFIRMED",
...(CLUB_WEBSITE != "" ? { url: CLUB_WEBSITE } : {}),
...(SettingHelper.getSetting("club.website") != "" ? { url: SettingHelper.getSetting("club.website") } : {}),
alarms: [
{
action: "display",

View file

@ -0,0 +1,95 @@
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 || val == "") return fallback;
try {
return CodingHelper.decrypt(key, val, true);
} catch (error) {
console.error("Decryption error in database-read - can be ignored");
if (fallback == "<self>") return val;
else return fallback;
}
},
to(val: string | null | undefined): string {
const valueToEncrypt = val || fallback;
if (valueToEncrypt === "") return "";
try {
return CodingHelper.encrypt(key, valueToEncrypt, true);
} catch (error) {
console.error("Encryption error in database-read - can be ignored");
if (fallback == "<self>") return val;
return "";
}
},
};
}
public static encrypt(phrase: string, content: string, passError = false): string {
if (!content) return "";
try {
// 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");
} catch (error) {
if (passError) throw error;
console.error("Encryption failed:", error);
return "";
}
}
public static decrypt(phrase: string, content: string, passError = false): 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) {
if (passError) throw error;
console.error("Decryption failed:", error);
return "";
}
}
}

View file

@ -0,0 +1,83 @@
import ms from "ms";
import validator from "validator";
export abstract class TypeConverter<T> {
abstract fromString(value: string): T;
abstract toString(value: T): string;
abstract validate(value: string): boolean;
}
export abstract class StringTypeConverter extends TypeConverter<string> {
fromString(value: string): string {
return value;
}
toString(value: string): string {
return value;
}
validate(value: string): boolean {
return typeof value === "string";
}
}
export abstract class NumberTypeConverter extends TypeConverter<number> {
fromString(value: string): number {
return Number(value);
}
toString(value: number): string {
return String(value);
}
validate(value: string): boolean {
const num = Number(value);
return !isNaN(num);
}
}
export abstract class BooleanTypeConverter extends TypeConverter<boolean> {
fromString(value: string): boolean {
return value === "true";
}
toString(value: boolean): string {
return value ? "true" : "false";
}
validate(value: string): boolean {
return value === "true" || value === "false";
}
}
export abstract class MsTypeConverter extends TypeConverter<ms.StringValue> {
fromString(value: string): ms.StringValue {
return value as ms.StringValue;
}
toString(value: ms.StringValue): string {
return String(value);
}
validate(value: string): boolean {
try {
const result = ms(value as ms.StringValue);
return result !== undefined;
} catch (e) {
return false;
}
}
}
export abstract class EmailTypeConverter extends TypeConverter<string> {
fromString(value: string): string {
return value;
}
toString(value: string): string {
return value;
}
validate(value: string): boolean {
return validator.isEmail(value);
}
}
// Konkrete Implementierungen der Converter
export class StringConverter extends StringTypeConverter {}
export class LongStringConverter extends StringTypeConverter {}
export class UrlConverter extends StringTypeConverter {}
export class NumberConverter extends NumberTypeConverter {}
export class BooleanConverter extends BooleanTypeConverter {}
export class MsConverter extends MsTypeConverter {}
export class EmailConverter extends EmailTypeConverter {}

View file

@ -12,6 +12,16 @@ export abstract class DemoDataHelper {
return newsletterDemoData;
case "member":
return memberDemoData;
case "listprint":
return {
today: new Date(),
list: [memberDemoData.memberships],
};
case "listprint.member":
return {
today: new Date(),
list: [memberDemoData.member],
};
default:
return {};
}

View file

@ -1,4 +1,4 @@
import { Brackets, DataSource, NotBrackets, ObjectLiteral, SelectQueryBuilder, WhereExpressionBuilder } from "typeorm";
import { Brackets, NotBrackets, ObjectLiteral, SelectQueryBuilder, WhereExpressionBuilder } from "typeorm";
import { dataSource } from "../data-source";
import { ConditionStructure, DynamicQueryStructure, FieldType, QueryResult } from "../type/dynamicQueries";
import { TableMeta } from "../type/tableMeta";
@ -17,11 +17,13 @@ export default abstract class DynamicQueryBuilder {
"memberAwards",
"memberExecutivePositions",
"memberQualifications",
"memberEducations",
"membership",
"memberView",
"memberExecutivePositionsView",
"memberQualificationsView",
"membershipView",
"membershipTotalView",
];
public static getTableMeta(tableName: string): TableMeta {
@ -63,7 +65,7 @@ export default abstract class DynamicQueryBuilder {
count?: number;
noLimit?: boolean;
}): SelectQueryBuilder<ObjectLiteral> {
let affix = queryObj.id ?? StringHelper.random(10);
let affix = queryObj.id.replaceAll("-", "") ?? StringHelper.random(10);
let query = dataSource.getRepository(queryObj.table).createQueryBuilder(`${affix}_${queryObj.table}`);
this.buildDynamicQuery(query, queryObj, affix);
@ -116,7 +118,7 @@ export default abstract class DynamicQueryBuilder {
if (queryObject.join) {
for (const join of queryObject.join) {
let subaffix = join.id ?? StringHelper.random(10);
let subaffix = join.id.replaceAll("-", "") ?? StringHelper.random(10);
if (join.type == undefined) join.type = "defined";
if (join.type == "defined") {
query.innerJoin(`${alias}.${join.foreignColumn}`, `${subaffix}_${join.table}`);
@ -226,19 +228,19 @@ export default abstract class DynamicQueryBuilder {
query += ` IS NOT NULL`;
break;
case "contains":
query += ` LIKE :${parameterKey}`;
query += ` ILIKE :${parameterKey}`;
parameters[parameterKey] = `%${condition.value}%`;
break;
case "notContains":
query += ` NOT LIKE :${parameterKey}`;
query += ` NOT ILIKE :${parameterKey}`;
parameters[parameterKey] = `%${condition.value}%`;
break;
case "startsWith":
query += ` LIKE :${parameterKey}`;
query += ` ILIKE :${parameterKey}`;
parameters[parameterKey] = `${condition.value}%`;
break;
case "endsWith":
query += ` LIKE :${parameterKey}`;
query += ` ILIKE :${parameterKey}`;
parameters[parameterKey] = `%${condition.value}`;
break;
case "timespanEq":
@ -272,8 +274,15 @@ export default abstract class DynamicQueryBuilder {
});
});
results = tempResults;
} else if (value && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date)) {
const objResults = flatten(value as QueryResult, newKey);
} else if (
value &&
typeof value === "object" &&
!Array.isArray(value) &&
!(value instanceof Date) &&
!(value instanceof Buffer) &&
!Object.keys(value).every((k) => ["years", "months", "days"].includes(k))
) {
const objResults = flatten(value, newKey);
const tempResults: Array<{ [key: string]: FieldType }> = [];
results.forEach((res) => {
objResults.forEach((objRes) => {
@ -283,7 +292,24 @@ export default abstract class DynamicQueryBuilder {
results = tempResults;
} else {
results.forEach((res) => {
if (String(value) != "undefined") res[newKey] = String(value);
if (typeof value === "object" && value instanceof Date) {
res[newKey] = new Date(value).toISOString();
} else if (
typeof value === "object" &&
!Array.isArray(value) &&
!(value instanceof Buffer) &&
value !== null
) {
let string = "";
for (const key of Object.keys(value)) {
string += `${value[key]} ${key} `;
}
res[newKey] = string.trim();
// JSON.stringify(value).replace(/["\\{}]/g, "").replaceAll(",", ", ");
} else if (String(value) != "undefined") {
res[newKey] = value !== null ? String(value) : "";
}
});
}
}
@ -366,7 +392,7 @@ export default abstract class DynamicQueryBuilder {
stats: "error",
sql: error.sql,
code: error.code,
msg: error.sqlMessage,
msg: error.sqlMessage ?? error.message,
};
}
});
@ -375,7 +401,7 @@ export default abstract class DynamicQueryBuilder {
stats: "error",
sql: error.sql,
code: error.code,
msg: error.sqlMessage,
msg: error.sqlMessage ?? error.message,
};
}
} else {
@ -395,7 +421,7 @@ export default abstract class DynamicQueryBuilder {
stats: "error",
sql: error.sql,
code: error.code,
msg: error.sqlMessage,
msg: error.sqlMessage ?? error.message,
};
}
}

View file

@ -20,9 +20,20 @@ export abstract class FileSystemHelper {
return readFileSync(this.formatPath(...filePath), "base64");
}
static readRootFile(filePath: string) {
return readFileSync(this.normalizePath(process.cwd(), filePath), "utf8");
}
static readTemplateFile(filePath: string) {
this.createFolder(filePath);
return readFileSync(process.cwd() + filePath, "utf8");
return readFileSync(this.normalizePath(process.cwd(), "src", "templates", filePath), "utf8");
}
static readAssetFile(filePath: string, returnPath: boolean = false) {
let path = this.normalizePath(process.cwd(), "src", "assets", filePath);
if (returnPath) {
return path;
}
return readFileSync(path, "utf8");
}
static writeFile(filePath: string, filename: string, file: any) {

View file

@ -1,6 +1,5 @@
import jwt from "jsonwebtoken";
import { JWTData, JWTToken } from "../type/jwtTypes";
import { JWT_SECRET, JWT_EXPIRATION } from "../env.defaults";
import InternalException from "../exceptions/internalException";
import RolePermissionService from "../service/management/rolePermissionService";
import UserPermissionService from "../service/management/userPermissionService";
@ -9,11 +8,13 @@ import PermissionHelper from "./permissionHelper";
import WebapiService from "../service/management/webapiService";
import WebapiPermissionService from "../service/management/webapiPermissionService";
import ms from "ms";
import SettingHelper from "./settingsHelper";
import { APPLICATION_SECRET } from "../env.defaults";
export abstract class JWTHelper {
static validate(token: string): Promise<string | jwt.JwtPayload> {
return new Promise<string | jwt.JwtPayload>((resolve, reject) => {
jwt.verify(token, JWT_SECRET, (err, decoded) => {
jwt.verify(token, APPLICATION_SECRET, (err, decoded) => {
if (err) reject(err.message);
else resolve(decoded);
});
@ -27,9 +28,11 @@ export abstract class JWTHelper {
return new Promise<string>((resolve, reject) => {
jwt.sign(
data,
JWT_SECRET,
APPLICATION_SECRET,
{
...(useExpiration ?? true ? { expiresIn: expOverwrite ?? JWT_EXPIRATION } : {}),
...(useExpiration ?? true
? { expiresIn: expOverwrite ?? (SettingHelper.getSetting("session.jwt_expiration") as ms.StringValue) }
: {}),
},
(err, token) => {
if (err) reject(err.message);
@ -100,7 +103,8 @@ export abstract class JWTHelper {
};
let overwriteExpiration =
ms(JWT_EXPIRATION) < new Date().getTime() - new Date(expiration).getTime()
ms(SettingHelper.getSetting("session.jwt_expiration") as ms.StringValue) <
new Date().getTime() - new Date(expiration).getTime()
? null
: Date.now() - new Date(expiration).getTime();

View file

@ -1,17 +1,78 @@
import { Transporter, createTransport, TransportOptions } from "nodemailer";
import { CLUB_NAME, MAIL_HOST, MAIL_PASSWORD, MAIL_PORT, MAIL_SECURE, MAIL_USERNAME } from "../env.defaults";
import { Attachment } from "nodemailer/lib/mailer";
import SettingHelper from "./settingsHelper";
import validator from "validator";
export default abstract class MailHelper {
private static readonly transporter: Transporter = createTransport({
host: MAIL_HOST,
port: MAIL_PORT,
secure: (MAIL_SECURE as "true" | "false") == "true",
auth: {
user: MAIL_USERNAME,
pass: MAIL_PASSWORD,
},
} as TransportOptions);
private static transporter: Transporter;
static createTransport() {
this.transporter?.close();
this.transporter = createTransport({
host: SettingHelper.getSetting("mail.host"),
port: SettingHelper.getSetting("mail.port"),
secure: SettingHelper.getSetting("mail.secure"),
auth: {
user: SettingHelper.getSetting("mail.username"),
pass: SettingHelper.getSetting("mail.password"),
},
} as TransportOptions);
}
static async verifyTransport({
host,
port,
secure,
user,
password,
}: {
host: string;
port: number;
secure: boolean;
user: string;
password: string;
}): Promise<boolean> {
let transport = createTransport({
host,
port,
secure,
auth: { user, pass: password },
});
return await transport
.verify()
.then(() => {
return true;
})
.catch((err) => {
console.log(err);
return false;
})
.finally(() => {
try {
transport?.close();
} catch (error) {}
});
}
static async checkMail(mail: string): Promise<boolean> {
return validator.isEmail(mail);
// return await emailCheck(mail)
// .then((res) => {
// return res;
// })
// .catch((err) => {
// return false;
// });
}
static initialize() {
SettingHelper.onSettingTopicChanged("mail", () => {
this.createTransport();
});
this.createTransport();
}
/**
* @description send mail
@ -29,7 +90,7 @@ export default abstract class MailHelper {
return new Promise((resolve, reject) => {
this.transporter
.sendMail({
from: `"${CLUB_NAME}" <${MAIL_USERNAME}>`,
from: `"${SettingHelper.getSetting("club.name")}" <${SettingHelper.getSetting("mail.email")}>`,
to: target,
subject,
text: content,

View file

@ -10,13 +10,13 @@ import { CalendarHelper } from "./calendarHelper";
import DynamicQueryBuilder from "./dynamicQueryBuilder";
import { FileSystemHelper } from "./fileSystemHelper";
import MailHelper from "./mailHelper";
import { CLUB_NAME } from "../env.defaults";
import { TemplateHelper } from "./templateHelper";
import { PdfExport } from "./pdfExport";
import NewsletterConfigService from "../service/configuration/newsletterConfigService";
import { NewsletterConfigType } from "../enums/newsletterConfigType";
import { NewsletterConfigEnum } from "../enums/newsletterConfigEnum";
import InternalException from "../exceptions/internalException";
import EventEmitter from "events";
import SettingHelper from "./settingsHelper";
export interface NewsletterEventType {
kind: "pdf" | "mail";
@ -66,33 +66,7 @@ export abstract class NewsletterHelper {
title: d.diffTitle || d.calendar.title,
content: d.diffDescription || d.calendar.content,
starttime: d.calendar.starttime,
formattedStarttime: new Date(d.calendar.starttime).toLocaleDateString("de-DE", {
weekday: "long",
day: "2-digit",
month: "long",
}),
formattedFullStarttime: new Date(d.calendar.starttime).toLocaleDateString("de-DE", {
weekday: "long",
day: "2-digit",
month: "long",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
}),
endtime: d.calendar.endtime,
formattedEndtime: new Date(d.calendar.endtime).toLocaleDateString("de-DE", {
weekday: "long",
day: "2-digit",
month: "long",
}),
formattedFullEndtime: new Date(d.calendar.endtime).toLocaleDateString("de-DE", {
weekday: "long",
day: "2-digit",
month: "long",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
}),
location: d.calendar.location,
}))
.sort((a, b) => a.starttime.getTime() - b.starttime.getTime()),
@ -145,7 +119,7 @@ export abstract class NewsletterHelper {
return [];
} else {
let members = await MemberService.getAll({ noLimit: true, ids: queryMemberIds });
return members[0];
return members[0].filter((m) => m.sendNewsletter != null);
}
}
@ -154,14 +128,11 @@ export abstract class NewsletterHelper {
let recipients = await NewsletterRecipientsService.getAll(newsletterId);
let config = await NewsletterConfigService.getAll();
let allowedForMail = config.filter((c) => c.config == NewsletterConfigType.mail).map((c) => c.comTypeId);
let allowedForMail = config.filter((c) => c.config == NewsletterConfigEnum.mail).map((c) => c.comTypeId);
const members = await this.transformRecipientsToMembers(newsletter, recipients);
const mailRecipients = members.filter(
(m) =>
m.sendNewsletter != null &&
m.sendNewsletter?.email != null &&
allowedForMail.includes(m.sendNewsletter?.type?.id)
(m) => m.sendNewsletter?.email != "" && allowedForMail.includes(m.sendNewsletter?.type?.id)
);
return mailRecipients;
@ -172,17 +143,17 @@ export abstract class NewsletterHelper {
let recipients = await NewsletterRecipientsService.getAll(newsletterId);
let config = await NewsletterConfigService.getAll();
let notAllowedForPdf = config.filter((c) => c.config == NewsletterConfigType.mail).map((c) => c.comTypeId);
let notAllowedForPdf = config
.filter((c) => c.config == NewsletterConfigEnum.none || c.config == NewsletterConfigEnum.mail)
.map((c) => c.comTypeId);
const members = await this.transformRecipientsToMembers(newsletter, recipients);
const pdfRecipients = members.filter(
(m) => !notAllowedForPdf.includes(m.sendNewsletter?.type?.id) || m.sendNewsletter == null
);
const pdfRecipients = members.filter((m) => !notAllowedForPdf.includes(m.sendNewsletter?.type?.id));
pdfRecipients.unshift({
id: "0",
firstname: "Alle Mitglieder",
lastname: CLUB_NAME,
lastname: SettingHelper.getSetting("club.name"),
nameaffix: "",
salutation: { salutation: "" },
} as member);
@ -224,11 +195,14 @@ export abstract class NewsletterHelper {
const { body } = await TemplateHelper.renderFileForModule({
module: "newsletter",
bodyData: data,
title: `Newsletter von ${CLUB_NAME}`,
title: `Newsletter von ${SettingHelper.getSetting("club.name")}`,
});
await MailHelper.sendMail(rec.sendNewsletter.email, `Newsletter von ${CLUB_NAME}`, body, [
{ filename: "events.ics", path: this.getICSFilePath(newsletter) },
])
await MailHelper.sendMail(
rec.sendNewsletter.email,
`Newsletter von ${SettingHelper.getSetting("club.name")}`,
body,
[{ filename: "events.ics", path: this.getICSFilePath(newsletter) }]
)
.then(() => {
this.formatJobEmit(
"progress",
@ -278,7 +252,7 @@ export abstract class NewsletterHelper {
if (error) throw new InternalException("Failed Building ICS form Pdf", error);
this.saveIcsToFile(newsletter, value);
let printWithAdress = config.filter((c) => c.config == NewsletterConfigType.pdf).map((c) => c.comTypeId);
let printWithAdress = config.filter((c) => c.config == NewsletterConfigEnum.pdf).map((c) => c.comTypeId);
const pdfRecipients = await this.getPrintRecipients(newsletterId);
@ -289,7 +263,7 @@ export abstract class NewsletterHelper {
await PdfExport.renderFile({
template: "newsletter",
title: `Newsletter von ${CLUB_NAME}`,
title: `Newsletter von ${SettingHelper.getSetting("club.name")}`,
filename: `${rec.lastname}_${rec.firstname}_${rec.id}`.replaceAll(" ", "-"),
folder: `newsletter/${newsletter.id}_${newsletter.title.replaceAll(" ", "")}`,
data: data,

View file

@ -17,33 +17,31 @@ export default class PermissionHelper {
permissions: PermissionObject,
type: PermissionType | "admin",
section: PermissionSection,
module?: PermissionModule
module: PermissionModule
) {
if (type == "admin") return permissions?.admin ?? permissions?.adminByOwner ?? false;
if (permissions?.admin || permissions?.adminByOwner) return true;
if (
(!module &&
permissions[section] != undefined &&
(permissions[section]?.all == "*" || permissions[section]?.all?.includes(type))) ||
permissions[section]?.all == "*" ||
permissions[section]?.all?.includes(type)
permissions[section]?.all?.includes(type) ||
permissions[section]?.[module] == "*" ||
permissions[section]?.[module]?.includes(type)
)
return true;
if (module && (permissions[section]?.[module] == "*" || permissions[section]?.[module]?.includes(type)))
return true;
return false;
}
static canSome(
permissions: PermissionObject,
checks: Array<{
requiredPermissions: PermissionType | "admin";
requiredPermission: PermissionType | "admin";
section: PermissionSection;
module?: PermissionModule;
module: PermissionModule;
}>
) {
checks.reduce<boolean>((prev, curr) => {
return prev || this.can(permissions, curr.requiredPermissions, curr.section, curr.module);
return checks.reduce<boolean>((prev, curr) => {
return prev || this.can(permissions, curr.requiredPermission, curr.section, curr.module);
}, false);
}
@ -66,12 +64,29 @@ export default class PermissionHelper {
static canSomeSection(
permissions: PermissionObject,
checks: Array<{
requiredPermissions: PermissionType | "admin";
requiredPermission: PermissionType | "admin";
section: PermissionSection;
}>
): boolean {
return checks.reduce<boolean>((prev, curr) => {
return prev || this.can(permissions, curr.requiredPermissions, curr.section);
return prev || this.canSection(permissions, curr.requiredPermission, curr.section);
}, false);
}
static canAccessSection(permissions: PermissionObject, section: PermissionSection): boolean {
if (permissions?.admin || permissions?.adminByOwner) return true;
if (permissions[section] != undefined) return true;
return false;
}
static canAccessSomeSection(
permissions: PermissionObject,
checks: Array<{
section: PermissionSection;
}>
): boolean {
return checks.reduce<boolean>((prev, curr) => {
return prev || this.canAccessSection(permissions, curr.section);
}, false);
}
@ -83,7 +98,7 @@ export default class PermissionHelper {
static passCheckMiddleware(
requiredPermissions: PermissionType | "admin",
section: PermissionSection,
module?: PermissionModule
module: PermissionModule
): (req: Request, res: Response, next: Function) => void {
return (req: Request, res: Response, next: Function) => {
const permissions = req.permissions;
@ -99,9 +114,9 @@ export default class PermissionHelper {
static passCheckSomeMiddleware(
checks: Array<{
requiredPermissions: PermissionType | "admin";
requiredPermission: PermissionType | "admin";
section: PermissionSection;
module?: PermissionModule;
module: PermissionModule;
}>
): (req: Request, res: Response, next: Function) => void {
return (req: Request, res: Response, next: Function) => {
@ -111,9 +126,7 @@ export default class PermissionHelper {
if (isOwner || this.canSome(permissions, checks)) {
next();
} else {
let permissionsToPass = checks.reduce<string>((prev, curr) => {
return prev + (prev != " or " ? "" : "") + `${curr.section}.${curr.module}.${curr.requiredPermissions}`;
}, "");
let permissionsToPass = checks.map((c) => `${c.section}.${c.module}.${c.requiredPermission}`).join(" or ");
throw new ForbiddenRequestException(`missing permission for ${permissionsToPass}`);
}
};
@ -136,7 +149,7 @@ export default class PermissionHelper {
}
static sectionPassCheckSomeMiddleware(
checks: Array<{ requiredPermissions: PermissionType | "admin"; section: PermissionSection }>
checks: Array<{ requiredPermission: PermissionType | "admin"; section: PermissionSection }>
): (req: Request, res: Response, next: Function) => void {
return (req: Request, res: Response, next: Function) => {
const permissions = req.permissions;
@ -145,9 +158,38 @@ export default class PermissionHelper {
if (isOwner || this.canSomeSection(permissions, checks)) {
next();
} else {
let permissionsToPass = checks.reduce<string>((prev, curr) => {
return prev + (prev != " or " ? "" : "") + `${curr.section}.${curr.requiredPermissions}`;
}, "");
let permissionsToPass = checks.map((c) => `${c.section}.${c.requiredPermission}`).join(" or ");
throw new ForbiddenRequestException(`missing permission for ${permissionsToPass}`);
}
};
}
static sectionAccessPassCheckMiddleware(
section: PermissionSection
): (req: Request, res: Response, next: Function) => void {
return (req: Request, res: Response, next: Function) => {
const permissions = req.permissions;
const isOwner = req.isOwner;
if (isOwner || this.canAccessSection(permissions, section)) {
next();
} else {
throw new ForbiddenRequestException(`missing permission for ${section}.${module}`);
}
};
}
static sectionAccessPassCheckSomeMiddleware(
checks: Array<{ section: PermissionSection }>
): (req: Request, res: Response, next: Function) => void {
return (req: Request, res: Response, next: Function) => {
const permissions = req.permissions;
const isOwner = req.isOwner;
if (isOwner || this.canAccessSomeSection(permissions, checks)) {
next();
} else {
let permissionsToPass = checks.map((c) => `${c.section}`).join(" or ");
throw new ForbiddenRequestException(`missing permission for ${permissionsToPass}`);
}
};

View file

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

View file

@ -9,10 +9,10 @@ export abstract class TemplateHelper {
static getTemplateFromFile(template: string) {
let tmpFile;
try {
tmpFile = FileSystemHelper.readTemplateFile(`/src/templates/${template}.template.html`);
tmpFile = FileSystemHelper.readTemplateFile(`${template}.template.html`);
} catch (err) {
tmpFile = FileSystemHelper.readTemplateFile(
`/src/templates/${template.split(".")[template.split(".").length - 1]}.template.html`
`${template.split(".")[template.split(".").length - 1]}.template.html`
);
}
return tmpFile;

View file

@ -2,7 +2,7 @@ import "dotenv/config";
import "./handlebars.config";
import express from "express";
import { BACKUP_AUTO_RESTORE, configCheck, SERVER_PORT } from "./env.defaults";
import { configCheck } from "./env.defaults";
configCheck();
import { PermissionObject } from "./type/permissionTypes";
@ -21,23 +21,29 @@ declare global {
import { dataSource } from "./data-source";
import BackupHelper from "./helpers/backupHelper";
import SettingHelper from "./helpers/settingsHelper";
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);
});
}
await SettingHelper.configure();
MailHelper.initialize();
});
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";
import RefreshCommandHandler from "./command/refreshCommandHandler";
import MailHelper from "./helpers/mailHelper";
const job = schedule.scheduleJob("0 0 * * *", async () => {
console.log(`${new Date().toISOString()}: running Cron`);
await RefreshCommandHandler.deleteExpired();

35
src/middleware/multer.ts Normal file
View file

@ -0,0 +1,35 @@
import multer from "multer";
import { FileSystemHelper } from "../helpers/fileSystemHelper";
import path from "path";
import BadRequestException from "../exceptions/badRequestException";
export const clubImageStorage = multer.diskStorage({
destination: FileSystemHelper.formatPath("/app"),
filename: function (req, file, cb) {
const fileExtension = path.extname(file.originalname).toLowerCase();
if (file.fieldname === "icon") {
cb(null, "admin-icon" + fileExtension);
} else if (file.fieldname === "logo") {
cb(null, "admin-logo" + fileExtension);
} else {
cb(null, file.originalname);
}
},
});
export const clubImageMulter = multer({
storage: clubImageStorage,
fileFilter(req, file, cb) {
if (file.mimetype.startsWith("image/png")) {
cb(null, true);
} else {
cb(new BadRequestException("Wrong file format"));
}
},
});
export const clubImageUpload = clubImageMulter.fields([
{ name: "icon", maxCount: 1 },
{ name: "logo", maxCount: 1 },
]);

View file

@ -1,177 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
import {
invite_table,
refresh_table,
reset_table,
role_permission_table,
role_table,
user_permission_table,
user_roles_table,
user_table,
webapi_permission_table,
webapi_table,
} from "./baseSchemaTables/admin";
import { templateUsage } from "../entity/configuration/templateUsage";
import {
award_table,
communication_type_table,
executive_position_table,
member_awards_table,
member_communication_table,
member_executive_positions_table,
member_executive_positions_view_mysql,
member_executive_positions_view_postgres,
member_executive_positions_view_sqlite,
member_qualifications_table,
member_qualifications_view_mysql,
member_qualifications_view_postgres,
member_qualifications_view_sqlite,
member_table,
member_view_mysql,
member_view_postgres,
member_view_sqlite,
membership_status_table,
membership_table,
membership_view_mysql,
membership_view_postgres,
membership_view_sqlite,
qualification_table,
salutation_table,
} from "./baseSchemaTables/member";
import { query_table, template_table, template_usage_table } from "./baseSchemaTables/query_template";
import {
protocol_agenda_table,
protocol_decision_table,
protocol_presence_table,
protocol_printout_table,
protocol_table,
protocol_voting_table,
} from "./baseSchemaTables/protocol";
import { calendar_table, calendar_type_table } from "./baseSchemaTables/calendar";
import {
newsletter_config_table,
newsletter_dates_table,
newsletter_recipients_table,
newsletter_table,
} from "./baseSchemaTables/newsletter";
import { DB_TYPE } from "../env.defaults";
export class CreateSchema1738166167472 implements MigrationInterface {
name = "CreateSchema1738166167472";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(reset_table, true, true, true);
await queryRunner.createTable(invite_table, true, true, true);
await queryRunner.createTable(role_table, true, true, true);
await queryRunner.createTable(role_permission_table, true, true, true);
await queryRunner.createTable(user_table, true, true, true);
await queryRunner.createTable(user_roles_table, true, true, true);
await queryRunner.createTable(user_permission_table, true, true, true);
await queryRunner.createTable(refresh_table, true, true, true);
await queryRunner.createTable(webapi_table, true, true, true);
await queryRunner.createTable(webapi_permission_table, true, true, true);
await queryRunner.createTable(salutation_table, true, true, true);
await queryRunner.createTable(award_table, true, true, true);
await queryRunner.createTable(communication_type_table, true, true, true);
await queryRunner.createTable(membership_status_table, true, true, true);
await queryRunner.createTable(executive_position_table, true, true, true);
await queryRunner.createTable(qualification_table, true, true, true);
await queryRunner.createTable(member_table, true, true, true);
await queryRunner.createTable(member_awards_table, true, true, true);
await queryRunner.createTable(member_communication_table, true, true, true);
await queryRunner.createTable(membership_table, true, true, true);
await queryRunner.createTable(member_executive_positions_table, true, true, true);
await queryRunner.createTable(member_qualifications_table, true, true, 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 == "sqlite") await queryRunner.createView(member_view_sqlite, true);
if (DB_TYPE == "postgres") await queryRunner.createView(membership_view_postgres, true);
else if (DB_TYPE == "mysql") await queryRunner.createView(membership_view_mysql, true);
else if (DB_TYPE == "sqlite") await queryRunner.createView(membership_view_sqlite, true);
if (DB_TYPE == "postgres") await queryRunner.createView(member_qualifications_view_postgres, true);
else if (DB_TYPE == "mysql") await queryRunner.createView(member_qualifications_view_mysql, true);
else if (DB_TYPE == "sqlite") await queryRunner.createView(member_qualifications_view_sqlite, true);
if (DB_TYPE == "postgres") await queryRunner.createView(member_executive_positions_view_postgres, true);
else if (DB_TYPE == "mysql") await queryRunner.createView(member_executive_positions_view_mysql, true);
else if (DB_TYPE == "sqlite") await queryRunner.createView(member_executive_positions_view_sqlite, true);
await queryRunner.createTable(query_table, true, true, true);
await queryRunner.createTable(template_table, true, true, true);
await queryRunner.createTable(template_usage_table, true, true, true);
await queryRunner.manager
.createQueryBuilder()
.insert()
.into(templateUsage)
.values([{ scope: "newsletter" }, { scope: "protocol" }, { scope: "member.list" }])
.orIgnore()
.execute();
await queryRunner.createTable(protocol_table, true, true, true);
await queryRunner.createTable(protocol_agenda_table, true, true, true);
await queryRunner.createTable(protocol_decision_table, true, true, true);
await queryRunner.createTable(protocol_presence_table, true, true, true);
await queryRunner.createTable(protocol_voting_table, true, true, true);
await queryRunner.createTable(protocol_printout_table, true, true, true);
await queryRunner.createTable(calendar_type_table, true, true, true);
await queryRunner.createTable(calendar_table, true, true, true);
await queryRunner.createTable(newsletter_config_table, true, true, true);
await queryRunner.createTable(newsletter_table, true, true, true);
await queryRunner.createTable(newsletter_dates_table, true, true, true);
await queryRunner.createTable(newsletter_recipients_table, true, true, true);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable("newsletter_dates", true, true, true);
await queryRunner.dropTable("newsletter_recipients", true, true, true);
await queryRunner.dropTable("newsletter", true, true, true);
await queryRunner.dropTable("newsletter_config", true, true, true);
await queryRunner.dropTable("calendar", true, true, true);
await queryRunner.dropTable("calendar_type", true, true, true);
await queryRunner.dropTable("protocol_agenda", true, true, true);
await queryRunner.dropTable("protocol_decision", true, true, true);
await queryRunner.dropTable("protocol_presence", true, true, true);
await queryRunner.dropTable("protocol_voting", true, true, true);
await queryRunner.dropTable("protocol_printout", true, true, true);
await queryRunner.dropTable("protocol", true, true, true);
await queryRunner.dropTable("template_usage", true, true, true);
await queryRunner.dropTable("template", true, true, true);
await queryRunner.dropTable("query", true, true, true);
await queryRunner.dropView("member_view");
await queryRunner.dropView("membership_view");
await queryRunner.dropView("member_qualifications_view");
await queryRunner.dropView("member_executive_positions_view");
await queryRunner.dropTable("member_awards", true, true, true);
await queryRunner.dropTable("communication", true, true, true);
await queryRunner.dropTable("membership", true, true, true);
await queryRunner.dropTable("member_executive_positions", true, true, true);
await queryRunner.dropTable("member_qualifications", true, true, true);
await queryRunner.dropTable("member", true, true, true);
await queryRunner.dropTable("salutation", true, true, true);
await queryRunner.dropTable("award", true, true, true);
await queryRunner.dropTable("communication_type", true, true, true);
await queryRunner.dropTable("membership_status", true, true, true);
await queryRunner.dropTable("executive_position", true, true, true);
await queryRunner.dropTable("qualification", true, true, true);
await queryRunner.dropTable("webapi_permission", true, true, true);
await queryRunner.dropTable("webapi", true, true, true);
await queryRunner.dropTable("refresh", true, true, true);
await queryRunner.dropTable("user_permission", true, true, true);
await queryRunner.dropTable("user_roles", true, true, true);
await queryRunner.dropTable("user", true, true, true);
await queryRunner.dropTable("role_permission", true, true, true);
await queryRunner.dropTable("role", true, true, true);
await queryRunner.dropTable("invite", true, true, true);
await queryRunner.dropTable("reset", true, true, true);
}
}

View file

@ -1,55 +0,0 @@
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
import { templateUsage } from "../entity/configuration/templateUsage";
import { getTypeByORM, getDefaultByORM } from "./ormHelper";
export class TemplatesAndProtocolSort1742549956787 implements MigrationInterface {
name = "TemplatesAndProtocolSort1742549956787";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.manager
.createQueryBuilder()
.insert()
.into(templateUsage)
.values([{ scope: "member" }])
.orIgnore()
.execute();
await queryRunner.manager
.createQueryBuilder()
.delete()
.from(templateUsage)
.where({ scope: "member.list" })
.execute();
await queryRunner.addColumn(
"protocol_agenda",
new TableColumn({ name: "sort", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) })
);
await queryRunner.addColumn(
"protocol_decision",
new TableColumn({ name: "sort", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) })
);
await queryRunner.addColumn(
"protocol_voting",
new TableColumn({ name: "sort", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) })
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn("protocol_agenda", "sort");
await queryRunner.dropColumn("protocol_decision", "sort");
await queryRunner.dropColumn("protocol_voting", "sort");
await queryRunner.manager
.createQueryBuilder()
.insert()
.into(templateUsage)
.values([{ scope: "member.list" }])
.orIgnore()
.execute();
await queryRunner.manager.createQueryBuilder().delete().from(templateUsage).where({ scope: "member" }).execute();
}
}

View file

@ -1,94 +0,0 @@
import { MigrationInterface, QueryRunner, TableColumn, TableForeignKey } from "typeorm";
import { getTypeByORM, isIncrementPrimary, isUUIDPrimary } from "./ormHelper";
import { query } from "../entity/configuration/query";
export class QueryToUUID1742922178643 implements MigrationInterface {
name = "QueryToUUID1742922178643";
public async up(queryRunner: QueryRunner): Promise<void> {
const table = await queryRunner.getTable("newsletter");
const foreignKey = table.foreignKeys.find((fk) => fk.columnNames.indexOf("recipientsByQueryId") !== -1);
await queryRunner.dropForeignKey("newsletter", foreignKey);
const entries = await queryRunner.manager.getRepository(query).find({ select: { title: true, query: true } });
await queryRunner.clearTable("query");
await queryRunner.dropColumn("newsletter", "recipientsByQueryId");
await queryRunner.dropColumn("query", "id");
await queryRunner.addColumn(
"query",
new TableColumn({
name: "id",
...getTypeByORM("uuid"),
...isUUIDPrimary,
})
);
await queryRunner.addColumn(
"newsletter",
new TableColumn({
name: "recipientsByQueryId",
...getTypeByORM("uuid", true),
})
);
await queryRunner.manager.createQueryBuilder().insert().into("query").values(entries).execute();
await queryRunner.createForeignKey(
"newsletter",
new TableForeignKey({
columnNames: ["recipientsByQueryId"],
referencedColumnNames: ["id"],
referencedTableName: "query",
onDelete: "CASCADE",
onUpdate: "RESTRICT",
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const table = await queryRunner.getTable("newsletter");
const foreignKey = table.foreignKeys.find((fk) => fk.columnNames.indexOf("recipientsByQueryId") !== -1);
await queryRunner.dropForeignKey("newsletter", foreignKey);
const entries = await queryRunner.manager.getRepository(query).find({ select: { title: true, query: true } });
await queryRunner.clearTable("query");
await queryRunner.dropColumn("newsletter", "recipientsByQueryId");
await queryRunner.dropColumn("query", "id");
await queryRunner.addColumn(
"query",
new TableColumn({
name: "id",
...getTypeByORM("int"),
...isIncrementPrimary,
})
);
await queryRunner.addColumn(
"newsletter",
new TableColumn({
name: "recipientsByQueryId",
...getTypeByORM("int", true),
})
);
await queryRunner.manager
.createQueryBuilder()
.insert()
.into("query")
.values(entries.map((e, i) => ({ ...e, id: i + 1 })))
.execute();
await queryRunner.createForeignKey(
"newsletter",
new TableForeignKey({
columnNames: ["recipientsByQueryId"],
referencedColumnNames: ["id"],
referencedTableName: "query",
onDelete: "CASCADE",
onUpdate: "RESTRICT",
})
);
}
}

View file

@ -1,43 +0,0 @@
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
import { getDefaultByORM, getTypeByORM } from "./ormHelper";
import { newsletter } from "../entity/club/newsletter/newsletter";
export class NewsletterColumnType1744351418751 implements MigrationInterface {
name = "NewsletterColumnType1744351418751";
public async up(queryRunner: QueryRunner): Promise<void> {
let newsletters = await queryRunner.manager.getRepository("newsletter").find();
await queryRunner.dropColumn("newsletter", "newsletterTitle");
await queryRunner.dropColumn("newsletter", "newsletterSignatur");
await queryRunner.addColumn(
"newsletter",
new TableColumn({ name: "newsletterTitle", ...getTypeByORM("text"), default: getDefaultByORM("string") })
);
await queryRunner.addColumn(
"newsletter",
new TableColumn({ name: "newsletterSignatur", ...getTypeByORM("text"), default: getDefaultByORM("string") })
);
await queryRunner.manager.getRepository("newsletter").save(newsletters);
}
public async down(queryRunner: QueryRunner): Promise<void> {
let newsletters = await queryRunner.manager.getRepository("newsletter").find();
await queryRunner.dropColumn("newsletter", "newsletterTitle");
await queryRunner.dropColumn("newsletter", "newsletterSignatur");
await queryRunner.addColumn(
"newsletter",
new TableColumn({ name: "newsletterTitle", ...getTypeByORM("varchar"), default: getDefaultByORM("string") })
);
await queryRunner.addColumn(
"newsletter",
new TableColumn({ name: "newsletterSignatur", ...getTypeByORM("varchar"), default: getDefaultByORM("string") })
);
await queryRunner.manager.getRepository("newsletter").save(newsletters);
}
}

View file

@ -1,22 +0,0 @@
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
import { getTypeByORM, getDefaultByORM } from "./ormHelper";
export class QueryUpdatedAt1744795756230 implements MigrationInterface {
name = "QueryUpdatedAt1744795756230";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
"query",
new TableColumn({
name: "updatedAt",
...getTypeByORM("datetime", false, 6),
default: getDefaultByORM("currentTimestamp", 6),
onUpdate: getDefaultByORM<string>("currentTimestamp", 6),
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn("query", "updatedAt");
}
}

View file

@ -1,21 +1,19 @@
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 BackupHelper from "../helpers/backupHelper";
import { getTypeByORM, isIncrementPrimary, getDefaultByORM } from "./ormHelper";
export class BackupAndResetDatabase1738166124200 implements MigrationInterface {
name = "BackupAndResetDatabase1738166124200";
export class BackupAndResetDatabase1749296262915 implements MigrationInterface {
name = "BackupAndResetDatabase1749296262915";
public async up(queryRunner: QueryRunner): Promise<void> {
let query = DB_TYPE == "postgres" ? "SELECT name FROM migrations" : "SELECT `name` FROM `migrations`";
let migrations = await queryRunner.query(query);
let migrations = await queryRunner.query("SELECT name FROM migrations");
if (
(await queryRunner.hasTable("user")) &&
migrations.findIndex((m: any) => m.name == "MoveSendNewsletterFlag1737816852011") == -1
migrations.findIndex((m: any) => m.name == "MemberExtendData1748953828644") == -1
) {
throw new InternalException(
"Cannot update due to skiped version. Update to v1.2.2 Version first to prevent data loss and get access to the newer Versions."
"Cannot update due to skiped version. Update to v1.6.0 Version first to prevent data loss and get access to the newer Versions."
);
}

View file

@ -0,0 +1,176 @@
import { MigrationInterface, QueryRunner } from "typeorm";
import {
reset_table,
invite_table,
role_table,
role_permission_table,
user_table,
user_roles_table,
user_permission_table,
refresh_table,
webapi_table,
webapi_permission_table,
setting_table,
} from "./baseSchemaTables/admin";
import { calendar_type_table, calendar_table } from "./baseSchemaTables/calendar";
import {
salutation_table,
award_table,
communication_type_table,
membership_status_table,
executive_position_table,
qualification_table,
member_table,
member_awards_table,
member_communication_table,
membership_table,
member_executive_positions_table,
member_qualifications_table,
member_view,
membership_view,
member_qualifications_view,
member_executive_positions_view,
education_table,
member_educations_table,
membership_total_view,
} from "./baseSchemaTables/member";
import {
newsletter_config_table,
newsletter_table,
newsletter_dates_table,
newsletter_recipients_table,
} from "./baseSchemaTables/newsletter";
import {
protocol_table,
protocol_agenda_table,
protocol_decision_table,
protocol_presence_table,
protocol_voting_table,
protocol_printout_table,
} from "./baseSchemaTables/protocol";
import { query_table, template_table, template_usage_table } from "./baseSchemaTables/query_template";
import { availableTemplates } from "../type/templateTypes";
export class CreateSchema1749296280721 implements MigrationInterface {
name = "CreateSchema1749296280721";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(reset_table, true, true, true);
await queryRunner.createTable(invite_table, true, true, true);
await queryRunner.createTable(role_table, true, true, true);
await queryRunner.createTable(role_permission_table, true, true, true);
await queryRunner.createTable(user_table, true, true, true);
await queryRunner.createTable(user_roles_table, true, true, true);
await queryRunner.createTable(user_permission_table, true, true, true);
await queryRunner.createTable(refresh_table, true, true, true);
await queryRunner.createTable(webapi_table, true, true, true);
await queryRunner.createTable(webapi_permission_table, true, true, true);
await queryRunner.createTable(setting_table, true, true, true);
await queryRunner.createTable(salutation_table, true, true, true);
await queryRunner.createTable(award_table, true, true, true);
await queryRunner.createTable(communication_type_table, true, true, true);
await queryRunner.createTable(membership_status_table, true, true, true);
await queryRunner.createTable(executive_position_table, true, true, true);
await queryRunner.createTable(qualification_table, true, true, true);
await queryRunner.createTable(education_table, true, true, true);
await queryRunner.createTable(member_table, true, true, true);
await queryRunner.createTable(member_awards_table, true, true, true);
await queryRunner.createTable(member_communication_table, true, true, true);
await queryRunner.createTable(membership_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_educations_table, true, true, true);
await queryRunner.createView(member_view, true);
await queryRunner.createView(membership_view, true);
await queryRunner.createView(membership_total_view, true);
await queryRunner.createView(member_qualifications_view, true);
await queryRunner.createView(member_executive_positions_view, true);
await queryRunner.createTable(query_table, true, true, true);
await queryRunner.createTable(template_table, true, true, true);
await queryRunner.createTable(template_usage_table, true, true, true);
await queryRunner.manager
.createQueryBuilder()
.insert()
.into(template_usage_table.name)
.values(
availableTemplates.map((at) => ({
scope: at,
}))
)
.orIgnore()
.execute();
await queryRunner.createTable(protocol_table, true, true, true);
await queryRunner.createTable(protocol_agenda_table, true, true, true);
await queryRunner.createTable(protocol_decision_table, true, true, true);
await queryRunner.createTable(protocol_presence_table, true, true, true);
await queryRunner.createTable(protocol_voting_table, true, true, true);
await queryRunner.createTable(protocol_printout_table, true, true, true);
await queryRunner.createTable(calendar_type_table, true, true, true);
await queryRunner.createTable(calendar_table, true, true, true);
await queryRunner.createTable(newsletter_config_table, true, true, true);
await queryRunner.createTable(newsletter_table, true, true, true);
await queryRunner.createTable(newsletter_dates_table, true, true, true);
await queryRunner.createTable(newsletter_recipients_table, true, true, true);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable(newsletter_dates_table, true, true, true);
await queryRunner.dropTable(newsletter_recipients_table, true, true, true);
await queryRunner.dropTable(newsletter_table, true, true, true);
await queryRunner.dropTable(newsletter_config_table, true, true, true);
await queryRunner.dropTable(calendar_table, true, true, true);
await queryRunner.dropTable(calendar_type_table, true, true, true);
await queryRunner.dropTable(protocol_agenda_table, true, true, true);
await queryRunner.dropTable(protocol_decision_table, true, true, true);
await queryRunner.dropTable(protocol_presence_table, true, true, true);
await queryRunner.dropTable(protocol_voting_table, true, true, true);
await queryRunner.dropTable(protocol_printout_table, true, true, true);
await queryRunner.dropTable(protocol_table, true, true, true);
await queryRunner.dropTable(template_usage_table, true, true, true);
await queryRunner.dropTable(template_table, true, true, true);
await queryRunner.dropTable(query_table, true, true, true);
await queryRunner.dropView(member_view);
await queryRunner.dropView(membership_view);
await queryRunner.dropView(membership_total_view);
await queryRunner.dropView(member_qualifications_view);
await queryRunner.dropView(member_executive_positions_view);
await queryRunner.dropTable(member_awards_table, true, true, true);
await queryRunner.dropTable(member_communication_table, true, true, true);
await queryRunner.dropTable(membership_table, true, true, true);
await queryRunner.dropTable(member_executive_positions_table, true, true, true);
await queryRunner.dropTable(member_qualifications_table, true, true, true);
await queryRunner.dropTable(member_educations_table, true, true, true);
await queryRunner.dropTable(member_table, true, true, true);
await queryRunner.dropTable(salutation_table, true, true, true);
await queryRunner.dropTable(award_table, true, true, true);
await queryRunner.dropTable(communication_type_table, true, true, true);
await queryRunner.dropTable(membership_status_table, true, true, true);
await queryRunner.dropTable(executive_position_table, true, true, true);
await queryRunner.dropTable(qualification_table, true, true, true);
await queryRunner.dropTable(education_table, true, true, true);
await queryRunner.dropTable(setting_table, true, true, true);
await queryRunner.dropTable(webapi_permission_table, true, true, true);
await queryRunner.dropTable(webapi_table, true, true, true);
await queryRunner.dropTable(refresh_table, true, true, true);
await queryRunner.dropTable(user_permission_table, true, true, true);
await queryRunner.dropTable(user_roles_table, true, true, true);
await queryRunner.dropTable(user_table, true, true, true);
await queryRunner.dropTable(role_permission_table, true, true, true);
await queryRunner.dropTable(role_table, true, true, true);
await queryRunner.dropTable(invite_table, true, true, true);
await queryRunner.dropTable(reset_table, true, true, true);
}
}

View file

@ -1,5 +1,6 @@
import { Table, TableForeignKey } from "typeorm";
import { Table, TableForeignKey, TableIndex, TableUnique } from "typeorm";
import { getDefaultByORM, getTypeByORM, isIncrementPrimary, isUUIDPrimary } from "../ormHelper";
import { LoginRoutineEnum } from "../../enums/loginRoutineEnum";
export const invite_table = new Table({
name: "invite",
@ -16,7 +17,12 @@ export const role_table = new Table({
name: "role",
columns: [
{ name: "id", ...getTypeByORM("int"), ...isIncrementPrimary },
{ name: "role", ...getTypeByORM("varchar"), isUnique: true },
{ name: "role", ...getTypeByORM("varchar") },
],
uniques: [
new TableUnique({
columnNames: ["role"],
}),
],
});
@ -45,8 +51,8 @@ export const user_table = new Table({
{ name: "username", ...getTypeByORM("varchar"), isUnique: true },
{ name: "firstname", ...getTypeByORM("varchar") },
{ name: "lastname", ...getTypeByORM("varchar") },
{ name: "secret", ...getTypeByORM("varchar") },
{ name: "static", ...getTypeByORM("boolean"), default: getDefaultByORM("boolean", false) },
{ name: "secret", ...getTypeByORM("text") },
{ name: "routine", ...getTypeByORM("varchar"), default: getDefaultByORM("string", LoginRoutineEnum.totp) },
{ name: "isOwner", ...getTypeByORM("boolean"), default: getDefaultByORM("boolean", false) },
],
});
@ -73,6 +79,14 @@ export const user_roles_table = new Table({
onUpdate: "RESTRICT",
}),
],
indices: [
new TableIndex({
columnNames: ["userId"],
}),
new TableIndex({
columnNames: ["roleId"],
}),
],
});
export const user_permission_table = new Table({
@ -114,11 +128,19 @@ export const webapi_table = new Table({
name: "webapi",
columns: [
{ name: "id", ...getTypeByORM("int"), ...isIncrementPrimary },
{ name: "token", ...getTypeByORM("varchar"), isUnique: true },
{ name: "title", ...getTypeByORM("varchar"), isUnique: true },
{ name: "token", ...getTypeByORM("text") },
{ name: "title", ...getTypeByORM("varchar") },
{ name: "createdAt", ...getTypeByORM("datetime", false, 6), default: getDefaultByORM("currentTimestamp", 6) },
{ name: "lastUsage", ...getTypeByORM("datetime", true, 6), default: getDefaultByORM("null") },
{ name: "expiry", ...getTypeByORM("date", true), default: getDefaultByORM("null") },
{ name: "lastUsage", ...getTypeByORM("datetime", true, 6) },
{ name: "expiry", ...getTypeByORM("date", true) },
],
uniques: [
new TableUnique({
columnNames: ["token"],
}),
new TableUnique({
columnNames: ["title"],
}),
],
});
@ -148,3 +170,12 @@ export const reset_table = new Table({
{ name: "secret", ...getTypeByORM("varchar") },
],
});
export const setting_table = new Table({
name: "setting",
columns: [
{ name: "topic", ...getTypeByORM("varchar"), isPrimary: true },
{ name: "key", ...getTypeByORM("varchar"), isPrimary: true },
{ name: "value", ...getTypeByORM("text") },
],
});

View file

@ -1,4 +1,4 @@
import { Table, TableForeignKey } from "typeorm";
import { Table, TableForeignKey, TableUnique } from "typeorm";
import { getDefaultByORM, getTypeByORM, isIncrementPrimary, isUUIDPrimary } from "../ormHelper";
export const calendar_type_table = new Table({
@ -32,7 +32,7 @@ export const calendar_table = new Table({
default: getDefaultByORM("currentTimestamp", 6),
onUpdate: getDefaultByORM<string>("currentTimestamp", 6),
},
{ name: "webpageId", ...getTypeByORM("varchar", true), default: getDefaultByORM("null"), isUnique: true },
{ name: "webpageId", ...getTypeByORM("varchar", true) },
{ name: "typeId", ...getTypeByORM("int") },
],
foreignKeys: [
@ -44,4 +44,9 @@ export const calendar_table = new Table({
onUpdate: "RESTRICT",
}),
],
uniques: [
new TableUnique({
columnNames: ["webpageId"],
}),
],
});

View file

@ -1,4 +1,4 @@
import { Table, TableForeignKey, View } from "typeorm";
import { Table, TableForeignKey, TableUnique, View } from "typeorm";
import { getDefaultByORM, getTypeByORM, isIncrementPrimary, isUUIDPrimary } from "../ormHelper";
export const salutation_table = new Table({
@ -51,17 +51,28 @@ export const qualification_table = new Table({
],
});
export const education_table = new Table({
name: "education",
columns: [
{ name: "id", ...getTypeByORM("int"), ...isIncrementPrimary },
{ name: "education", ...getTypeByORM("varchar"), isUnique: true },
{ name: "description", ...getTypeByORM("varchar"), isNullable: true },
],
});
/** member and relations */
export const member_table = new Table({
name: "member",
columns: [
{ name: "id", ...getTypeByORM("uuid"), ...isUUIDPrimary },
{ name: "salutationId", ...getTypeByORM("int") },
{ name: "internalId", ...getTypeByORM("varchar", true), default: getDefaultByORM("null"), isUnique: true },
{ name: "internalId", ...getTypeByORM("varchar", true) },
{ name: "firstname", ...getTypeByORM("varchar") },
{ name: "lastname", ...getTypeByORM("varchar") },
{ name: "nameaffix", ...getTypeByORM("varchar") },
{ name: "birthdate", ...getTypeByORM("date") },
{ name: "createdAt", ...getTypeByORM("datetime", false, 6), default: getDefaultByORM("currentTimestamp", 6) },
{ name: "note", ...getTypeByORM("varchar", true) },
],
foreignKeys: [
new TableForeignKey({
@ -72,6 +83,11 @@ export const member_table = new Table({
onUpdate: "RESTRICT",
}),
],
uniques: [
new TableUnique({
columnNames: ["internalId"],
}),
],
});
export const membership_table = new Table({
@ -163,7 +179,7 @@ export const member_awards_table = new Table({
name: "member_awards",
columns: [
{ name: "id", ...getTypeByORM("int"), ...isIncrementPrimary },
{ name: "given", ...getTypeByORM("boolean"), default: getDefaultByORM("boolean", false) },
{ name: "given", ...getTypeByORM("boolean"), default: getDefaultByORM("boolean", true) },
{ name: "note", ...getTypeByORM("varchar"), isNullable: true },
{ name: "date", ...getTypeByORM("date") },
{ name: "memberId", ...getTypeByORM("uuid") },
@ -194,13 +210,13 @@ export const member_communication_table = new Table({
{ name: "preferred", ...getTypeByORM("boolean"), default: getDefaultByORM("boolean", false) },
{ name: "isSendNewsletter", ...getTypeByORM("boolean"), default: getDefaultByORM("boolean", false) },
{ name: "isSMSAlarming", ...getTypeByORM("boolean"), default: getDefaultByORM("boolean", false) },
{ name: "mobile", ...getTypeByORM("varchar"), isNullable: true },
{ name: "email", ...getTypeByORM("varchar"), isNullable: true },
{ name: "postalCode", ...getTypeByORM("varchar"), default: getDefaultByORM("null"), isNullable: true },
{ name: "city", ...getTypeByORM("varchar"), isNullable: true },
{ name: "street", ...getTypeByORM("varchar"), isNullable: true },
{ name: "mobile", ...getTypeByORM("varchar", true) },
{ name: "email", ...getTypeByORM("varchar", true) },
{ name: "postalCode", ...getTypeByORM("varchar", true) },
{ name: "city", ...getTypeByORM("varchar", true) },
{ name: "street", ...getTypeByORM("varchar", true) },
{ name: "streetNumber", ...getTypeByORM("int", true) },
{ name: "streetNumberAddition", ...getTypeByORM("varchar"), isNullable: true },
{ name: "streetNumberAddition", ...getTypeByORM("varchar", true) },
{ name: "memberId", ...getTypeByORM("uuid") },
{ name: "typeId", ...getTypeByORM("int") },
],
@ -222,108 +238,60 @@ export const member_communication_table = new Table({
],
});
/** views */
export const member_view_mysql = new View({
name: "member_view",
expression: `
SELECT
\`member\`.\`id\` AS \`id\`,
\`member\`.\`internalId\` AS \`internalId\`,
\`member\`.\`firstname\` AS \`firstname\`,
\`member\`.\`lastname\` AS \`lastname\`,
\`member\`.\`nameaffix\` AS \`nameaffix\`,
\`member\`.\`birthdate\` AS \`birthdate\`,
\`salutation\`.\`salutation\` AS \`salutation\`,
TIMESTAMPDIFF(YEAR, \`member\`.\`birthdate\`, CURDATE()) AS \`todayAge\`,
YEAR(CURDATE()) - YEAR(\`member\`.\`birthdate\`) AS \`ageThisYear\`,
CONCAT(
TIMESTAMPDIFF(YEAR, \`member\`.\`birthdate\`, CURDATE()), ' years ',
TIMESTAMPDIFF(MONTH, \`member\`.\`birthdate\`, CURDATE()) % 12, ' months ',
TIMESTAMPDIFF(DAY,
DATE_ADD(
\`member\`.\`birthdate\`,
INTERVAL TIMESTAMPDIFF(MONTH, \`member\`.\`birthdate\`, CURDATE()) MONTH
),
CURDATE()
), ' days'
) AS \`exactAge\`
FROM \`member\` \`member\`
LEFT JOIN \`salutation\` \`salutation\` ON \`salutation\`.\`id\`=\`member\`.\`salutationId\`
`,
export const member_educations_table = new Table({
name: "member_educations",
columns: [
{ name: "id", ...getTypeByORM("int"), ...isIncrementPrimary },
{ name: "start", ...getTypeByORM("date") },
{ name: "end", ...getTypeByORM("date", true), default: getDefaultByORM("null") },
{ name: "note", ...getTypeByORM("varchar"), isNullable: true },
{ name: "place", ...getTypeByORM("varchar"), isNullable: true },
{ name: "memberId", ...getTypeByORM("uuid") },
{ name: "educationId", ...getTypeByORM("int") },
],
foreignKeys: [
new TableForeignKey({
columnNames: ["memberId"],
referencedTableName: "member",
referencedColumnNames: ["id"],
onDelete: "CASCADE",
onUpdate: "RESTRICT",
}),
new TableForeignKey({
columnNames: ["educationId"],
referencedTableName: "education",
referencedColumnNames: ["id"],
onDelete: "RESTRICT",
onUpdate: "RESTRICT",
}),
],
});
export const member_view_postgres = new View({
/** views */
export const member_view = new View({
name: "member_view",
expression: `
SELECT
"member"."id" AS "id",
"member"."internalId" AS "internalId",
"member"."firstname" AS "firstname",
"member"."lastname" AS "lastname",
"member"."nameaffix" AS "nameaffix",
"member"."birthdate" AS "birthdate",
"member"."internalId" AS "internalId",
"member"."note" AS "note",
"salutation"."salutation" AS "salutation",
DATE_PART('year', AGE(CURRENT_DATE, member.birthdate)) AS "todayAge",
EXTRACT(YEAR FROM CURRENT_DATE) - EXTRACT(YEAR FROM member.birthdate) AS "ageThisYear",
AGE(CURRENT_DATE, member.birthdate) AS "exactAge"
DATE_PART('year', AGE(CURRENT_DATE, "member"."birthdate")) AS "todayAge",
EXTRACT(YEAR FROM CURRENT_DATE) - EXTRACT(YEAR FROM "member"."birthdate") AS "ageThisYear",
AGE(CURRENT_DATE, "member"."birthdate") AS "exactAge"
FROM "member" "member"
LEFT JOIN "salutation" "salutation" ON "salutation"."id"="member"."salutationId"
`,
`
.replace(/\s+/g, " ")
.trim(),
});
export const member_view_sqlite = new View({
name: "member_view",
expression: `
SELECT
member.id AS id,
member.internalId AS internalId,
member.firstname AS firstname,
member.lastname AS lastname,
member.nameaffix AS nameaffix,
member.birthdate AS birthdate,
salutation.salutation AS salutation,
(strftime('%Y', 'now') - strftime('%Y', member.birthdate) - (strftime('%m-%d', 'now') < strftime('%m-%d', member.birthdate))) AS todayAge,
FLOOR(strftime('%Y', 'now') - strftime('%Y', member.birthdate)) AS ageThisYear,
(strftime('%Y', 'now') - strftime('%Y', member.birthdate)) || ' years ' ||
(strftime('%m', 'now') - strftime('%m', member.birthdate)) || ' months ' ||
(strftime('%d', 'now') - strftime('%d', member.birthdate)) || ' days'
AS exactAge
FROM member member
LEFT JOIN salutation salutation ON salutation.id=member.salutationId
`,
});
export const member_executive_positions_view_mysql = new View({
name: "member_executive_positions_view",
expression: `
SELECT
\`executivePosition\`.\`id\` AS \`positionId\`,
\`executivePosition\`.\`position\` AS \`position\`,
\`member\`.\`id\` AS \`memberId\`,
\`member\`.\`firstname\` AS \`memberFirstname\`,
\`member\`.\`lastname\` AS \`memberLastname\`,
\`member\`.\`nameaffix\` AS \`memberNameaffix\`,
\`member\`.\`birthdate\` AS \`memberBirthdate\`,
\`salutation\`.\`salutation\` AS \`memberSalutation\`,
SUM(DATEDIFF(COALESCE(\`memberExecutivePositions\`.\`end\`, CURDATE()), \`memberExecutivePositions\`.\`start\`)) AS \`durationInDays\`,
SUM(TIMESTAMPDIFF(YEAR, \`memberExecutivePositions\`.\`start\`, COALESCE(\`memberExecutivePositions\`.\`end\`, CURDATE()))) AS \`durationInYears\`,
CONCAT(
SUM(FLOOR(TIMESTAMPDIFF(DAY, \`memberExecutivePositions\`.\`start\`, COALESCE(\`memberExecutivePositions\`.\`end\`, CURDATE())) / 365.25)),
' years ',
SUM(FLOOR(MOD(TIMESTAMPDIFF(MONTH, \`memberExecutivePositions\`.\`start\`, COALESCE(\`memberExecutivePositions\`.\`end\`, CURDATE())), 12))),
' months ',
SUM(FLOOR(MOD(TIMESTAMPDIFF(DAY, \`memberExecutivePositions\`.\`start\`, COALESCE(\`memberExecutivePositions\`.\`end\`, CURDATE())), 30))),
' days'
) AS \`exactDuration\`
FROM \`member_executive_positions\` \`memberExecutivePositions\`
LEFT JOIN \`executive_position\` \`executivePosition\` ON \`executivePosition\`.\`id\`=\`memberExecutivePositions\`.\`executivePositionId\`
LEFT JOIN \`member\` \`member\` ON \`member\`.\`id\`=\`memberExecutivePositions\`.\`memberId\`
LEFT JOIN \`salutation\` \`salutation\` ON \`salutation\`.\`id\`=\`member\`.\`salutationId\`
GROUP BY \`executivePosition\`.\`id\`, \`member\`.\`id\`, \`salutation\`.\`id\`
`,
});
export const member_executive_positions_view_postgres = new View({
export const member_executive_positions_view = new View({
name: "member_executive_positions_view",
expression: `
SELECT
@ -343,66 +311,12 @@ export const member_executive_positions_view_postgres = new View({
LEFT JOIN "member" "member" ON "member"."id"="memberExecutivePositions"."memberId"
LEFT JOIN "salutation" "salutation" ON "salutation"."id"="member"."salutationId"
GROUP BY "executivePosition"."id", "member"."id", "salutation"."id"
`,
`
.replace(/\s+/g, " ")
.trim(),
});
export const member_executive_positions_view_sqlite = new View({
name: "member_executive_positions_view",
expression: `
SELECT
executivePosition.id AS positionId,
executivePosition.position AS position,
member.id AS memberId,
member.firstname AS memberFirstname,
member.lastname AS memberLastname,
member.nameaffix AS memberNameaffix,
member.birthdate AS memberBirthdate,
salutation.salutation AS memberSalutation,
SUM(JULIANDAY(COALESCE(memberExecutivePositions.end, DATE('now'))) - JULIANDAY(memberExecutivePositions.start)) AS durationInDays,
SUM(FLOOR((JULIANDAY(COALESCE(memberExecutivePositions.end, DATE('now'))) - JULIANDAY(memberExecutivePositions.start)) / 365.25)) AS durationInYears,
SUM((strftime('%Y', COALESCE(memberExecutivePositions.end, DATE('now'))) - strftime('%Y', memberExecutivePositions.start))) || ' years ' ||
SUM((strftime('%m', COALESCE(memberExecutivePositions.end, DATE('now'))) - strftime('%m', memberExecutivePositions.start))) || ' months ' ||
SUM((strftime('%d', COALESCE(memberExecutivePositions.end, DATE('now'))) - strftime('%d', memberExecutivePositions.start))) || ' days'
AS exactDuration
FROM member_executive_positions memberExecutivePositions
LEFT JOIN executive_position executivePosition ON executivePosition.id=memberExecutivePositions.executivePositionId
LEFT JOIN member member ON member.id=memberExecutivePositions.memberId
LEFT JOIN salutation salutation ON salutation.id=member.salutationId
GROUP BY executivePosition.id, member.id, salutation.id
`,
});
export const member_qualifications_view_mysql = new View({
name: "member_qualifications_view",
expression: `
SELECT
\`qualification\`.\`id\` AS \`qualificationId\`,
\`qualification\`.\`qualification\` AS \`qualification\`,
\`member\`.\`id\` AS \`memberId\`,
\`member\`.\`firstname\` AS \`memberFirstname\`,
\`member\`.\`lastname\` AS \`memberLastname\`,
\`member\`.\`nameaffix\` AS \`memberNameaffix\`,
\`member\`.\`birthdate\` AS \`memberBirthdate\`,
\`salutation\`.\`salutation\` AS \`memberSalutation\`,
SUM(DATEDIFF(COALESCE(\`memberQualifications\`.\`end\`, CURDATE()), \`memberQualifications\`.\`start\`)) AS \`durationInDays\`,
SUM(TIMESTAMPDIFF(YEAR, \`memberQualifications\`.\`start\`, COALESCE(\`memberQualifications\`.\`end\`, CURDATE()))) AS \`durationInYears\`,
CONCAT(
SUM(FLOOR(TIMESTAMPDIFF(DAY, \`memberQualifications\`.\`start\`, COALESCE(\`memberQualifications\`.\`end\`, CURDATE())) / 365.25)),
' years ',
SUM(FLOOR(MOD(TIMESTAMPDIFF(MONTH, \`memberQualifications\`.\`start\`, COALESCE(\`memberQualifications\`.\`end\`, CURDATE())), 12))),
' months ',
SUM(FLOOR(MOD(TIMESTAMPDIFF(DAY, \`memberQualifications\`.\`start\`, COALESCE(\`memberQualifications\`.\`end\`, CURDATE())), 30))),
' days'
) AS \`exactDuration\`
FROM \`member_qualifications\` \`memberQualifications\`
LEFT JOIN \`qualification\` \`qualification\` ON \`qualification\`.\`id\`=\`memberQualifications\`.\`qualificationId\`
LEFT JOIN \`member\` \`member\` ON \`member\`.\`id\`=\`memberQualifications\`.\`memberId\`
LEFT JOIN \`salutation\` \`salutation\` ON \`salutation\`.\`id\`=\`member\`.\`salutationId\`
GROUP BY \`qualification\`.\`id\`, \`member\`.\`id\`, \`salutation\`.\`id\`
`,
});
export const member_qualifications_view_postgres = new View({
export const member_qualifications_view = new View({
name: "member_qualifications_view",
expression: `
SELECT
@ -422,66 +336,12 @@ export const member_qualifications_view_postgres = new View({
LEFT JOIN "member" "member" ON "member"."id"="memberQualifications"."memberId"
LEFT JOIN "salutation" "salutation" ON "salutation"."id"="member"."salutationId"
GROUP BY "qualification"."id", "member"."id", "salutation"."id"
`,
`
.replace(/\s+/g, " ")
.trim(),
});
export const member_qualifications_view_sqlite = new View({
name: "member_qualifications_view",
expression: `
SELECT
qualification.id AS qualificationId,
qualification.qualification AS qualification,
member.id AS memberId,
member.firstname AS memberFirstname,
member.lastname AS memberLastname,
member.nameaffix AS memberNameaffix,
member.birthdate AS memberBirthdate,
salutation.salutation AS memberSalutation,
SUM(JULIANDAY(COALESCE(memberQualifications.end, DATE('now'))) - JULIANDAY(memberQualifications.start)) AS durationInDays,
SUM(FLOOR((JULIANDAY(COALESCE(memberQualifications.end, DATE('now'))) - JULIANDAY(memberQualifications.start)) / 365.25)) AS durationInYears,
SUM((strftime('%Y', COALESCE(memberQualifications.end, DATE('now'))) - strftime('%Y', memberQualifications.start))) || ' years ' ||
SUM((strftime('%m', COALESCE(memberQualifications.end, DATE('now'))) - strftime('%m', memberQualifications.start))) || ' months ' ||
SUM((strftime('%d', COALESCE(memberQualifications.end, DATE('now'))) - strftime('%d', memberQualifications.start))) || ' days'
AS exactDuration
FROM member_qualifications memberQualifications
LEFT JOIN qualification qualification ON qualification.id=memberQualifications.qualificationId
LEFT JOIN member member ON member.id=memberQualifications.memberId
LEFT JOIN salutation salutation ON salutation.id=member.salutationId
GROUP BY qualification.id, member.id, salutation.id
`,
});
export const membership_view_mysql = new View({
name: "membership_view",
expression: `
SELECT
\`status\`.\`id\` AS \`statusId\`,
\`status\`.\`status\` AS \`status\`,
\`member\`.\`id\` AS \`memberId\`,
\`member\`.\`firstname\` AS \`memberFirstname\`,
\`member\`.\`lastname\` AS \`memberLastname\`,
\`member\`.\`nameaffix\` AS \`memberNameaffix\`,
\`member\`.\`birthdate\` AS \`memberBirthdate\`,
\`salutation\`.\`salutation\` AS \`memberSalutation\`,
SUM(DATEDIFF(COALESCE(\`membership\`.\`end\`, CURDATE()), \`membership\`.\`start\`)) AS \`durationInDays\`,
SUM(TIMESTAMPDIFF(YEAR, \`membership\`.\`start\`, COALESCE(\`membership\`.\`end\`, CURDATE()))) AS \`durationInYears\`,
CONCAT(
SUM(FLOOR(TIMESTAMPDIFF(DAY, \`membership\`.\`start\`, COALESCE(\`membership\`.\`end\`, CURDATE())) / 365.25)),
' years ',
SUM(FLOOR(MOD(TIMESTAMPDIFF(MONTH, \`membership\`.\`start\`, COALESCE(\`membership\`.\`end\`, CURDATE())), 12))),
' months ',
SUM(FLOOR(MOD(TIMESTAMPDIFF(DAY, \`membership\`.\`start\`, COALESCE(\`membership\`.\`end\`, CURDATE())), 30))),
' days'
) AS \`exactDuration\`
FROM \`membership\` \`membership\`
LEFT JOIN \`membership_status\` \`status\` ON \`status\`.\`id\`=\`membership\`.\`statusId\`
LEFT JOIN \`member\` \`member\` ON \`member\`.\`id\`=\`membership\`.\`memberId\`
LEFT JOIN \`salutation\` \`salutation\` ON \`salutation\`.\`id\`=\`member\`.\`salutationId\`
GROUP BY \`status\`.\`id\`, \`member\`.\`id\`, \`salutation\`.\`id\`
`,
});
export const membership_view_postgres = new View({
export const membership_view = new View({
name: "membership_view",
expression: `
SELECT
@ -500,32 +360,31 @@ export const membership_view_postgres = new View({
LEFT JOIN "membership_status" "status" ON "status"."id"="membership"."statusId"
LEFT JOIN "member" "member" ON "member"."id"="membership"."memberId"
LEFT JOIN "salutation" "salutation" ON "salutation"."id"="member"."salutationId"
GROUP BY "status"."id","member"."id", "salutation"."id"
`,
GROUP BY "status"."id", "member"."id", "salutation"."id"
`
.replace(/\s+/g, " ")
.trim(),
});
export const membership_view_sqlite = new View({
name: "membership_view",
export const membership_total_view = new View({
name: "membership_total_view",
expression: `
SELECT
status.id AS statusId,
status.status AS status,
member.id AS memberId,
member.firstname AS memberFirstname,
member.lastname AS memberLastname,
member.nameaffix AS memberNameaffix,
member.birthdate AS memberBirthdate,
salutation.salutation AS memberSalutation,
SUM(JULIANDAY(COALESCE(membership.end, DATE('now'))) - JULIANDAY(membership.start)) AS durationInDays,
SUM(FLOOR((JULIANDAY(COALESCE(membership.end, DATE('now'))) - JULIANDAY(membership.start)) / 365.25)) AS durationInYears,
SUM((strftime('%Y', COALESCE(membership.end, DATE('now'))) - strftime('%Y', membership.start))) || ' years ' ||
SUM((strftime('%m', COALESCE(membership.end, DATE('now'))) - strftime('%m', membership.start))) || ' months ' ||
SUM((strftime('%d', COALESCE(membership.end, DATE('now'))) - strftime('%d', membership.start))) || ' days'
AS exactDuration
FROM membership membership
LEFT JOIN membership_status status ON status.id=membership.statusId
LEFT JOIN member member ON member.id=membership.memberId
LEFT JOIN salutation salutation ON salutation.id=member.salutationId
GROUP BY status.id, member.id, salutation.id
`,
SELECT
"member"."id" AS "memberId",
"member"."firstname" AS "memberFirstname",
"member"."lastname" AS "memberLastname",
"member"."nameaffix" AS "memberNameaffix",
"member"."birthdate" AS "memberBirthdate",
"salutation"."salutation" AS "memberSalutation",
SUM(COALESCE("membership"."end", CURRENT_DATE) - "membership"."start") AS "durationInDays",
SUM(EXTRACT(YEAR FROM AGE(COALESCE("membership"."end", CURRENT_DATE), "membership"."start"))) AS "durationInYears",
SUM(AGE(COALESCE("membership"."end", CURRENT_DATE), "membership"."start")) AS "exactDuration"
FROM "membership" "membership"
LEFT JOIN "membership_status" "status" ON "status"."id"="membership"."statusId"
LEFT JOIN "member" "member" ON "member"."id"="membership"."memberId"
LEFT JOIN "salutation" "salutation" ON "salutation"."id"="member"."salutationId"
GROUP BY "member"."id", "salutation"."id"
`
.replace(/\s+/g, " ")
.trim(),
});

View file

@ -1,4 +1,4 @@
import { Table, TableForeignKey } from "typeorm";
import { Table, TableForeignKey, TableUnique } from "typeorm";
import { getDefaultByORM, getTypeByORM, isIncrementPrimary } from "../ormHelper";
export const newsletter_table = new Table({
@ -7,11 +7,12 @@ export const newsletter_table = new Table({
{ name: "id", ...getTypeByORM("int"), ...isIncrementPrimary },
{ name: "title", ...getTypeByORM("varchar") },
{ name: "description", ...getTypeByORM("varchar"), default: getDefaultByORM("string") },
{ name: "newsletterTitle", ...getTypeByORM("varchar"), default: getDefaultByORM("string") },
{ name: "newsletterTitle", ...getTypeByORM("text"), default: getDefaultByORM("string") },
{ name: "newsletterText", ...getTypeByORM("text"), default: getDefaultByORM("string") },
{ name: "newsletterSignatur", ...getTypeByORM("varchar"), default: getDefaultByORM("string") },
{ name: "newsletterSignatur", ...getTypeByORM("text"), default: getDefaultByORM("string") },
{ name: "isSent", ...getTypeByORM("boolean"), default: getDefaultByORM("boolean", false) },
{ name: "recipientsByQueryId", ...getTypeByORM("int", true) },
{ name: "recipientsByQueryId", ...getTypeByORM("uuid", true) },
{ name: "createdAt", ...getTypeByORM("datetime", false, 6), default: getDefaultByORM("currentTimestamp", 6) },
],
foreignKeys: [
new TableForeignKey({
@ -22,6 +23,11 @@ export const newsletter_table = new Table({
onUpdate: "RESTRICT",
}),
],
uniques: [
new TableUnique({
columnNames: ["title"],
}),
],
});
export const newsletter_dates_table = new Table({

View file

@ -1,11 +1,11 @@
import { Table, TableForeignKey } from "typeorm";
import { Table, TableForeignKey, TableUnique } from "typeorm";
import { getDefaultByORM, getTypeByORM, isIncrementPrimary } from "../ormHelper";
export const protocol_table = new Table({
name: "protocol",
columns: [
{ name: "id", ...getTypeByORM("int"), ...isIncrementPrimary },
{ name: "title", ...getTypeByORM("varchar") },
{ name: "title", ...getTypeByORM("varchar"), isUnique: true },
{ name: "date", ...getTypeByORM("date") },
{ name: "starttime", ...getTypeByORM("time", true) },
{ name: "endtime", ...getTypeByORM("time", true) },
@ -19,6 +19,7 @@ export const protocol_agenda_table = new Table({
{ name: "id", ...getTypeByORM("int"), ...isIncrementPrimary },
{ name: "topic", ...getTypeByORM("varchar") },
{ name: "context", ...getTypeByORM("text"), default: getDefaultByORM("string") },
{ name: "sort", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) },
{ name: "protocolId", ...getTypeByORM("int") },
],
foreignKeys: [
@ -38,6 +39,7 @@ export const protocol_decision_table = new Table({
{ name: "id", ...getTypeByORM("int"), ...isIncrementPrimary },
{ name: "topic", ...getTypeByORM("varchar") },
{ name: "context", ...getTypeByORM("text"), default: getDefaultByORM("string") },
{ name: "sort", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) },
{ name: "protocolId", ...getTypeByORM("int") },
],
foreignKeys: [
@ -86,6 +88,7 @@ export const protocol_voting_table = new Table({
{ name: "favour", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) },
{ name: "abstain", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) },
{ name: "against", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) },
{ name: "sort", ...getTypeByORM("int"), default: getDefaultByORM("number", 0) },
{ name: "protocolId", ...getTypeByORM("int") },
],
foreignKeys: [

View file

@ -1,12 +1,18 @@
import { Table, TableForeignKey } from "typeorm";
import { getDefaultByORM, getTypeByORM, isIncrementPrimary } from "../ormHelper";
import { getDefaultByORM, getTypeByORM, isIncrementPrimary, isUUIDPrimary } from "../ormHelper";
export const query_table = new Table({
name: "query",
columns: [
{ name: "id", ...getTypeByORM("int"), ...isIncrementPrimary },
{ name: "id", ...getTypeByORM("uuid"), ...isUUIDPrimary },
{ name: "title", ...getTypeByORM("varchar"), isUnique: true },
{ name: "query", ...getTypeByORM("text"), default: getDefaultByORM("string") },
{
name: "updatedAt",
...getTypeByORM("datetime", false, 6),
default: getDefaultByORM("currentTimestamp", 6),
onUpdate: getDefaultByORM<string>("currentTimestamp", 6),
},
],
});

View file

@ -13,83 +13,38 @@ export type Primary = {
};
export function getTypeByORM(type: ORMType, nullable: boolean = false, length: number = 255): ColumnConfig {
const dbType = process.env.DB_TYPE;
const typeMap: Record<string, Record<ORMType, string>> = {
mysql: {
int: "int",
bigint: "bigint",
boolean: "tinyint",
date: "date",
datetime: "datetime",
time: "time",
text: "text",
varchar: "varchar",
uuid: "varchar",
},
postgres: {
int: "integer",
bigint: "bigint",
boolean: "boolean",
date: "date",
datetime: "timestamp",
time: "time",
text: "text",
varchar: "character varying",
uuid: "uuid",
},
sqlite: {
int: "integer",
bigint: "integer",
boolean: "integer",
date: "date",
datetime: "datetime",
time: "text",
text: "text",
varchar: "varchar",
uuid: "varchar",
},
const typeMap: Record<ORMType, string> = {
int: "integer",
bigint: "bigint",
boolean: "boolean",
date: "date",
datetime: "timestamp",
time: "time",
text: "text",
varchar: "character varying",
uuid: "uuid",
};
let obj: ColumnConfig = {
type: typeMap[dbType]?.[type] || type,
type: typeMap[type] || type,
isNullable: nullable,
};
if (type == "datetime") obj.precision = length;
else if (dbType != "sqlite" && (obj.type == "varchar" || type == "varchar")) obj.length = `${length}`;
else if (dbType != "postgres" && type == "uuid") obj.length = "36";
else if (obj.type == "varchar" || type == "varchar") obj.length = `${length}`;
return obj;
}
export function getDefaultByORM<T = string | null>(type: ORMDefault, data?: string | number | boolean): T {
const dbType = process.env.DB_TYPE;
const typeMap: Record<string, Record<ORMDefault, string | null>> = {
mysql: {
currentTimestamp: `CURRENT_TIMESTAMP(${data ?? 6})`,
string: `'${data ?? ""}'`,
boolean: Boolean(data).toString(),
number: Number(data).toString(),
null: null,
},
postgres: {
currentTimestamp: `now()`,
string: `'${data ?? ""}'`,
boolean: Boolean(data) == true ? "true" : "false",
number: Number(data).toString(),
null: null,
},
sqlite: {
currentTimestamp: `datetime('now')`,
string: `'${data ?? ""}'`,
boolean: Boolean(data) == true ? "1" : "0",
number: Number(data).toString(),
null: null,
},
const typeMap: Record<ORMDefault, string | null> = {
currentTimestamp: `now()`,
string: `'${data ?? ""}'`,
boolean: Boolean(data) == true ? "true" : "false",
number: Number(data).toString(),
null: null,
};
return (typeMap[dbType]?.[type] || type) as T;
return (typeMap[type] || type) as T;
}
export const isIncrementPrimary: Primary = {

View file

@ -2,12 +2,14 @@ import express, { Request, Response } from "express";
import {
addAwardToMember,
addCommunicationToMember,
addEducationToMember,
addExecutivePositionToMember,
addMembershipToMember,
addQualificationToMember,
createMember,
deleteAwardOfMember,
deleteCommunicationOfMember,
deleteEducationsOfMember,
deleteExecutivePositionOfMember,
deleteMemberById,
deleteMembershipOfMember,
@ -17,19 +19,24 @@ import {
getAwardsByMember,
getCommunicationByMemberAndRecord,
getCommunicationsByMember,
getEducationByMemberAndRecord,
getEducationsByMember,
getExecutivePositionByMemberAndRecord,
getExecutivePositionsByMember,
getMemberById,
getMemberLastInternalId,
getMemberPrintoutById,
getMembersByIds,
getMembershipByMemberAndRecord,
getMembershipsByMember,
getMembershipStatisticsById,
getMembershipTotalStatisticsById,
getMemberStatisticsById,
getQualificationByMemberAndRecord,
getQualificationsByMember,
updateAwardOfMember,
updateCommunicationOfMember,
updateEducationOfMember,
updateExecutivePositionOfMember,
updateMemberById,
updateMembershipOfMember,
@ -43,6 +50,10 @@ router.get("/", async (req: Request, res: Response) => {
await getAllMembers(req, res);
});
router.get("/last/internalId", async (req: Request, res: Response) => {
await getMemberLastInternalId(req, res);
});
router.post("/ids", async (req: Request, res: Response) => {
await getMembersByIds(req, res);
});
@ -67,6 +78,10 @@ router.get("/:memberId/memberships/statistics", async (req: Request, res: Respon
await getMembershipStatisticsById(req, res);
});
router.get("/:memberId/memberships/totalstatistics", async (req: Request, res: Response) => {
await getMembershipTotalStatisticsById(req, res);
});
router.get("/:memberId/membership/:id", async (req: Request, res: Response) => {
await getMembershipByMemberAndRecord(req, res);
});
@ -87,6 +102,14 @@ router.get("/:memberId/qualification/:id", async (req: Request, res: Response) =
await getQualificationByMemberAndRecord(req, res);
});
router.get("/:memberId/educations", async (req: Request, res: Response) => {
await getEducationsByMember(req, res);
});
router.get("/:memberId/education/:id", async (req: Request, res: Response) => {
await getEducationByMemberAndRecord(req, res);
});
router.get("/:memberId/positions", async (req: Request, res: Response) => {
await getExecutivePositionsByMember(req, res);
});
@ -135,6 +158,14 @@ router.post(
}
);
router.post(
"/:memberId/education",
PermissionHelper.passCheckMiddleware("create", "club", "member"),
async (req: Request, res: Response) => {
await addEducationToMember(req, res);
}
);
router.post(
"/:memberId/position",
PermissionHelper.passCheckMiddleware("create", "club", "member"),
@ -183,6 +214,14 @@ router.patch(
}
);
router.patch(
"/:memberId/education/:recordId",
PermissionHelper.passCheckMiddleware("update", "club", "member"),
async (req: Request, res: Response) => {
await updateEducationOfMember(req, res);
}
);
router.patch(
"/:memberId/position/:recordId",
PermissionHelper.passCheckMiddleware("update", "club", "member"),
@ -231,6 +270,14 @@ router.delete(
}
);
router.delete(
"/:memberId/education/:recordId",
PermissionHelper.passCheckMiddleware("delete", "club", "member"),
async (req: Request, res: Response) => {
await deleteEducationsOfMember(req, res);
}
);
router.delete(
"/:memberId/position/:recordId",
PermissionHelper.passCheckMiddleware("delete", "club", "member"),

View file

@ -0,0 +1,45 @@
import express, { Request, Response } from "express";
import {
createEducation,
deleteEducation,
getAllEducations,
getEducationById,
updateEducation,
} from "../../../controller/admin/configuration/educationController";
import PermissionHelper from "../../../helpers/permissionHelper";
var router = express.Router({ mergeParams: true });
router.get("/", async (req: Request, res: Response) => {
await getAllEducations(req, res);
});
router.get("/:id", async (req: Request, res: Response) => {
await getEducationById(req, res);
});
router.post(
"/",
PermissionHelper.passCheckMiddleware("create", "configuration", "education"),
async (req: Request, res: Response) => {
await createEducation(req, res);
}
);
router.patch(
"/:id",
PermissionHelper.passCheckMiddleware("update", "configuration", "education"),
async (req: Request, res: Response) => {
await updateEducation(req, res);
}
);
router.delete(
"/:id",
PermissionHelper.passCheckMiddleware("delete", "configuration", "education"),
async (req: Request, res: Response) => {
await deleteEducation(req, res);
}
);
export default router;

View file

@ -7,6 +7,7 @@ import communicationType from "./configuration/communicationType";
import executivePosition from "./configuration/executivePosition";
import membershipStatus from "./configuration/membershipStatus";
import qualification from "./configuration/qualification";
import education from "./configuration/education";
import salutation from "./configuration/salutation";
import calendarType from "./configuration/calendarType";
import queryStore from "./configuration/queryStore";
@ -26,70 +27,79 @@ import user from "./management/user";
import invite from "./management/invite";
import api from "./management/webapi";
import backup from "./management/backup";
import setting from "./management/setting";
var router = express.Router({ mergeParams: true });
router.use(
"/award",
PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "configuration", module: "award" },
{ requiredPermissions: "read", section: "club", module: "member" },
{ requiredPermission: "read", section: "configuration", module: "award" },
{ requiredPermission: "read", section: "club", module: "member" },
]),
award
);
router.use(
"/communicationtype",
PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "configuration", module: "communication_type" },
{ requiredPermissions: "read", section: "club", module: "member" },
{ requiredPermission: "read", section: "configuration", module: "communication_type" },
{ requiredPermission: "read", section: "club", module: "member" },
]),
communicationType
);
router.use(
"/executiveposition",
PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "configuration", module: "executive_position" },
{ requiredPermissions: "read", section: "club", module: "member" },
{ requiredPermission: "read", section: "configuration", module: "executive_position" },
{ requiredPermission: "read", section: "club", module: "member" },
]),
executivePosition
);
router.use(
"/membershipstatus",
PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "configuration", module: "membership_status" },
{ requiredPermissions: "read", section: "club", module: "member" },
{ requiredPermission: "read", section: "configuration", module: "membership_status" },
{ requiredPermission: "read", section: "club", module: "member" },
]),
membershipStatus
);
router.use(
"/qualification",
PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "configuration", module: "qualification" },
{ requiredPermissions: "read", section: "club", module: "member" },
{ requiredPermission: "read", section: "configuration", module: "qualification" },
{ requiredPermission: "read", section: "club", module: "member" },
]),
qualification
);
router.use(
"/education",
PermissionHelper.passCheckSomeMiddleware([
{ requiredPermission: "read", section: "configuration", module: "education" },
{ requiredPermission: "read", section: "club", module: "member" },
]),
education
);
router.use(
"/salutation",
PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "configuration", module: "salutation" },
{ requiredPermissions: "read", section: "club", module: "member" },
{ requiredPermission: "read", section: "configuration", module: "salutation" },
{ requiredPermission: "read", section: "club", module: "member" },
]),
salutation
);
router.use(
"/calendartype",
PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "configuration", module: "calendar_type" },
{ requiredPermissions: "read", section: "club", module: "calendar" },
{ requiredPermission: "read", section: "configuration", module: "calendar_type" },
{ requiredPermission: "read", section: "club", module: "calendar" },
]),
calendarType
);
router.use(
"/querystore",
PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "configuration", module: "query_store" },
{ requiredPermissions: "read", section: "club", module: "listprint" },
{ requiredPermission: "read", section: "configuration", module: "query_store" },
{ requiredPermission: "read", section: "club", module: "listprint" },
]),
queryStore
);
@ -97,16 +107,16 @@ router.use("/template", PermissionHelper.passCheckMiddleware("read", "configurat
router.use(
"/templateusage",
PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "configuration", module: "template_usage" },
{ requiredPermissions: "read", section: "configuration", module: "template" },
{ requiredPermission: "read", section: "configuration", module: "template_usage" },
{ requiredPermission: "read", section: "configuration", module: "template" },
]),
templateUsage
);
router.use(
"/newsletterconfig",
PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "configuration", module: "newsletter_config" },
{ requiredPermissions: "read", section: "configuration", module: "communication_type" },
{ requiredPermission: "read", section: "configuration", module: "newsletter_config" },
{ requiredPermission: "read", section: "configuration", module: "communication_type" },
]),
newsletterConfig
);
@ -115,8 +125,8 @@ router.use("/member", PermissionHelper.passCheckMiddleware("read", "club", "memb
router.use(
"/protocol",
PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "club", module: "protocol" },
{ requiredPermissions: "read", section: "club", module: "member" },
{ requiredPermission: "read", section: "club", module: "protocol" },
{ requiredPermission: "read", section: "club", module: "member" },
]),
protocol
);
@ -124,19 +134,19 @@ router.use("/calendar", PermissionHelper.passCheckMiddleware("read", "club", "ca
router.use(
"/querybuilder",
PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "club", module: "query" },
{ requiredPermissions: "read", section: "configuration", module: "query_store" },
{ requiredPermission: "read", section: "club", module: "query" },
{ requiredPermission: "read", section: "configuration", module: "query_store" },
]),
queryBuilder
);
router.use(
"/newsletter",
PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "club", module: "newsletter" },
{ requiredPermissions: "read", section: "club", module: "member" },
{ requiredPermissions: "read", section: "club", module: "calendar" },
{ requiredPermissions: "read", section: "club", module: "query" },
{ requiredPermissions: "read", section: "configuration", module: "query_store" },
{ requiredPermission: "read", section: "club", module: "newsletter" },
{ requiredPermission: "read", section: "club", module: "member" },
{ requiredPermission: "read", section: "club", module: "calendar" },
{ requiredPermission: "read", section: "club", module: "query" },
{ requiredPermission: "read", section: "configuration", module: "query_store" },
]),
newsletter
);
@ -146,8 +156,8 @@ router.use("/role", PermissionHelper.passCheckMiddleware("read", "management", "
router.use(
"/user",
PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "management", module: "user" },
{ requiredPermissions: "read", section: "management", module: "role" },
{ requiredPermission: "read", section: "management", module: "user" },
{ requiredPermission: "read", section: "management", module: "role" },
]),
user
);
@ -159,5 +169,6 @@ router.use(
PermissionHelper.passCheckMiddleware("read", "management", "backup"),
backup
);
router.use("/setting", PermissionHelper.passCheckMiddleware("read", "management", "setting"), setting);
export default router;

View file

@ -0,0 +1,56 @@
import express, { Request, Response } from "express";
import PermissionHelper from "../../../helpers/permissionHelper";
import {
getSetting,
getSettings,
resetSetting,
setImages,
setSetting,
setSettings,
} from "../../../controller/admin/management/settingController";
import { clubImageUpload } from "../../../middleware/multer";
var router = express.Router({ mergeParams: true });
router.get("/", async (req: Request, res: Response) => {
await getSettings(req, res);
});
router.get("/:setting", async (req: Request, res: Response) => {
await getSetting(req, res);
});
router.put(
"/",
PermissionHelper.passCheckMiddleware("create", "management", "setting"),
async (req: Request, res: Response) => {
await setSetting(req, res);
}
);
router.put(
"/multi",
PermissionHelper.passCheckMiddleware("create", "management", "setting"),
async (req: Request, res: Response) => {
await setSettings(req, res);
}
);
router.put(
"/images",
PermissionHelper.passCheckMiddleware("create", "management", "setting"),
clubImageUpload,
async (req: Request, res: Response) => {
await setImages(req, res);
}
);
router.delete(
"/:setting",
PermissionHelper.passCheckMiddleware("delete", "management", "setting"),
async (req: Request, res: Response) => {
await resetSetting(req, res);
}
);
export default router;

View file

@ -10,7 +10,6 @@ import {
updateUserPermissions,
updateUserRoles,
} from "../../../controller/admin/management/userController";
import { inviteUser } from "../../../controller/inviteController";
var router = express.Router({ mergeParams: true });

View file

@ -1,8 +1,12 @@
import express from "express";
import { login, logout, refresh } from "../controller/authController";
import { kickof, login, logout, refresh } from "../controller/authController";
var router = express.Router({ mergeParams: true });
router.post("/kickof", async (req, res) => {
await kickof(req, res);
});
router.post("/login", async (req, res) => {
await login(req, res);
});

View file

@ -1,6 +1,5 @@
import express from "express";
import { isSetup } from "../controller/setupController";
import { finishInvite, inviteUser, verifyInvite } from "../controller/inviteController";
import { finishInvite, verifyInvite } from "../controller/inviteController";
import ParamaterPassCheckHelper from "../helpers/parameterPassCheckHelper";
var router = express.Router({ mergeParams: true });
@ -9,8 +8,12 @@ router.post("/verify", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mai
await verifyInvite(req, res);
});
router.put("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]), async (req, res) => {
await finishInvite(req, res);
});
router.put(
"/",
ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "secret", "routine "]),
async (req, res) => {
await finishInvite(req, res);
}
);
export default router;

View file

@ -1,5 +1,12 @@
import express from "express";
import { getCalendarItemsByTypes } from "../controller/publicController";
import {
getApplicationConfig,
getApplicationFavicon,
getApplicationIcon,
getApplicationLogo,
getApplicationManifest,
getCalendarItemsByTypes,
} from "../controller/publicController";
var router = express.Router({ mergeParams: true });
@ -7,4 +14,24 @@ router.get("/calendar", async (req, res) => {
await getCalendarItemsByTypes(req, res);
});
router.get("/configuration", async (req, res) => {
await getApplicationConfig(req, res);
});
router.get("/manifest.webmanifest", async (req, res) => {
await getApplicationManifest(req, res);
});
router.get("/applogo.png", async (req, res) => {
await getApplicationLogo(req, res);
});
router.get("/favicon.ico", async (req, res) => {
await getApplicationFavicon(req, res);
});
router.get("/icon.png", async (req, res) => {
await getApplicationIcon(req, res);
});
export default router;

View file

@ -12,8 +12,12 @@ router.post("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["username"
await startReset(req, res);
});
router.put("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]), async (req, res) => {
await finishReset(req, res);
});
router.put(
"/",
ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "secret", "routine"]),
async (req, res) => {
await finishReset(req, res);
}
);
export default router;

View file

@ -5,7 +5,7 @@ import Parser from "rss-parser";
var router = express.Router({ mergeParams: true });
router.get("/version", async (req: Request, res: Response) => {
let serverPackage = FileSystemHelper.readTemplateFile("/package.json");
let serverPackage = FileSystemHelper.readRootFile("/package.json");
let serverJson = JSON.parse(serverPackage);
res.send({
name: serverJson.name,

View file

@ -1,7 +1,14 @@
import express from "express";
import { isSetup } from "../controller/setupController";
import {
isSetup,
setAppIdentity,
setClubIdentity,
setMailConfig,
uploadClubImages,
} from "../controller/setupController";
import { finishInvite, inviteUser, verifyInvite } from "../controller/inviteController";
import ParamaterPassCheckHelper from "../helpers/parameterPassCheckHelper";
import { clubImageUpload } from "../middleware/multer";
var router = express.Router({ mergeParams: true });
@ -9,20 +16,44 @@ router.get("/", async (req, res) => {
await isSetup(req, res);
});
router.post("/club", async (req, res) => {
await setClubIdentity(req, res);
});
router.post("/club/images", clubImageUpload, async (req, res) => {
await uploadClubImages(req, res);
});
router.post("/app", async (req, res) => {
await setAppIdentity(req, res);
});
router.post(
"/mail",
ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "username", "password", "host", "port", "secure"]),
async (req, res) => {
await setMailConfig(req, res);
}
);
router.post("/verify", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token"]), async (req, res) => {
await verifyInvite(req, res);
});
router.post(
"/",
"/me",
ParamaterPassCheckHelper.requiredIncludedMiddleware(["username", "mail", "firstname", "lastname"]),
async (req, res) => {
await inviteUser(req, res, false);
await inviteUser(req, res, true);
}
);
router.put("/", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]), async (req, res) => {
await finishInvite(req, res, true);
});
router.post(
"/finish",
ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token", "totp"]),
async (req, res) => {
await finishInvite(req, res, true);
}
);
export default router;

View file

@ -1,5 +1,16 @@
import express from "express";
import { getMeById, getMyTotp, transferOwnership, updateMe, verifyMyTotp } from "../controller/userController";
import {
changeMyPassword,
changeToPW,
changeToTOTP,
getChangeToTOTP,
getMeById,
getMyRoutine,
getMyTotp,
transferOwnership,
updateMe,
verifyMyTotp,
} from "../controller/userController";
var router = express.Router({ mergeParams: true });
@ -7,14 +18,34 @@ router.get("/me", async (req, res) => {
await getMeById(req, res);
});
router.get("/routine", async (req, res) => {
await getMyRoutine(req, res);
});
router.get("/totp", async (req, res) => {
await getMyTotp(req, res);
});
router.get("/changeToTOTP", async (req, res) => {
await getChangeToTOTP(req, res);
});
router.post("/verify", async (req, res) => {
await verifyMyTotp(req, res);
});
router.patch("/changepw", async (req, res) => {
await changeMyPassword(req, res);
});
router.patch("/changeToTOTP", async (req, res) => {
await changeToTOTP(req, res);
});
router.patch("/changeToPW", async (req, res) => {
await changeToPW(req, res);
});
router.put("/transferOwner", async (req, res) => {
await transferOwnership(req, res);
});

Some files were not shown because too many files have changed in this diff Show more