Compare commits

..

49 commits
v1.0.0 ... main

Author SHA1 Message Date
d9132e472b 1.2.0 2025-01-23 13:15:14 +01:00
c661a588c5 Merge pull request 'minor v1.2.0' (#51) from develop into main
Reviewed-on: #51
2025-01-23 12:14:24 +00:00
3beeec38f9 Merge pull request 'feature/#43-server-version' (#50) from feature/#43-server-version into develop
Reviewed-on: #50
2025-01-23 10:59:21 +00:00
3d8f1da059 provide server version and rss feeds 2025-01-23 11:21:54 +01:00
bbfdc69049 Merge pull request 'feature/#22-API-Tokens' (#49) from feature/#22-API-Tokens into develop
Reviewed-on: #49
2025-01-22 10:59:46 +00:00
a165231c47 jwt gen & rename fixes 2025-01-22 11:57:19 +01:00
313785b4ac renaming api module to webapi 2025-01-22 09:39:31 +01:00
0b40b9d92c permissions & routes with middleware 2025-01-22 09:27:15 +01:00
d3870b4c8d Merge pull request 'feature/#32-Membership-statistics' (#48) from feature/#32-Membership-statistics into develop
Reviewed-on: #48
2025-01-22 08:06:26 +00:00
ba2155d7c3 fix types and params 2025-01-22 08:59:38 +01:00
07d31bfe7d member and membership statistics 2025-01-21 14:49:14 +01:00
4568bef10e api Controller & token 2025-01-21 13:54:52 +01:00
36ecccd0dc controller & factory 2025-01-21 11:47:28 +01:00
3f0549bd44 api & permission services and commandHandler 2025-01-21 11:37:28 +01:00
ca6dbafaf1 extend api table by id 2025-01-21 11:25:26 +01:00
26d2f288e9 add api tables with permission 2025-01-21 11:03:48 +01:00
3e256dc2f7 1.1.1 2025-01-20 12:49:04 +01:00
bf5aec351c Merge pull request 'patches v1.1.1' (#46) from develop into main
Reviewed-on: #46
2025-01-20 11:48:30 +00:00
0bbe22e9ae fix: undefined value on ids search 2025-01-20 12:25:57 +01:00
9734149848 extend controller for search 2025-01-20 09:44:03 +01:00
28e05c70a9 1.1.0 2025-01-19 13:57:59 +01:00
1116b50773 Merge pull request 'minor: v1.1.0' (#45) from develop into main
Reviewed-on: #45
2025-01-19 12:56:41 +00:00
8b59f3b7a8 Merge branch 'main' into develop 2025-01-19 12:56:31 +00:00
2d8e8a2e42 Merge pull request 'feature/#40-protocol-presence-status' (#44) from feature/#40-protocol-presence-status into develop
Reviewed-on: #44
2025-01-19 12:46:46 +00:00
eb8f3fef3e update template 2025-01-19 13:42:18 +01:00
b5509ba162 update controller and syncing 2025-01-19 13:19:50 +01:00
4dd6fa6d8a add excused state to protocol presence 2025-01-19 13:14:12 +01:00
a4b26013a7 weburl change 2025-01-18 14:58:32 +01:00
c803c2a583 1.0.2 2025-01-13 11:23:16 +01:00
ba03b6f097 Merge pull request 'patches v1.0.2' (#39) from develop into main
Reviewed-on: #39
2025-01-13 10:22:29 +00:00
e738be4e13 change: update entity files 2025-01-13 11:16:35 +01:00
c9b8c3090d fix: end date update date error 2025-01-13 10:38:44 +01:00
a2cc408e85 change: remove unused command 2025-01-13 10:38:25 +01:00
d206d7a77c enhance: provide more details to command delete error 2025-01-13 10:37:53 +01:00
03036c8b77 add: sort data 2025-01-12 18:17:54 +01:00
1c6c123142 fix: stream newsletter progress to client 2025-01-12 13:21:34 +01:00
e1f6b36336 change: removed dev logging 2025-01-12 11:41:22 +01:00
916a6da4a0 enhance: allow extended refresh duration to PWAs 2025-01-12 11:41:02 +01:00
b1e949dce2 enhance: detect requests of pwa 2025-01-11 14:45:37 +01:00
69134b210a 1.0.1 2025-01-10 12:10:33 +01:00
0f7516bd3d Merge pull request 'patches v1.0.1' (#38) from develop into main
Reviewed-on: #38
2025-01-10 11:10:00 +00:00
632a8290ac wrapping up: membership id transfer 2025-01-10 12:06:24 +01:00
c173f917e3 fix: possibly undefined newsletter com 2025-01-10 11:26:48 +01:00
55d4a952c3 change: minimize logging on custom errors 2025-01-09 12:38:07 +01:00
03cc0f02b7 fix: member sorting at active members 2025-01-08 18:09:22 +01:00
a968c6af25 fix: member list template 2025-01-06 15:08:47 +01:00
8ba12b752c force exposed prod port to 5000 2025-01-06 11:51:33 +01:00
ad5f624d5d fix: grouping of memberships 2025-01-05 18:32:41 +01:00
b547c2bbca Rename to FF Admin 2025-01-05 16:37:04 +01:00
94 changed files with 1499 additions and 267 deletions

View file

@ -10,6 +10,7 @@ SERVER_PORT = portnumber
JWT_SECRET = ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 JWT_SECRET = ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890
JWT_EXPIRATION = [0-9]*(y|d|h|m|s) JWT_EXPIRATION = [0-9]*(y|d|h|m|s)
REFRESH_EXPIRATION = [0-9]*(y|d|h|m|s) REFRESH_EXPIRATION = [0-9]*(y|d|h|m|s)
PWA_REFRESH_EXPIRATION = [0-9]*(y|d|h|m|s)
MAIL_USERNAME = mail_username MAIL_USERNAME = mail_username
MAIL_PASSWORD = mail_password MAIL_PASSWORD = mail_password
@ -17,5 +18,5 @@ MAIL_HOST = mail_hoststring
MAIL_PORT = mail_portnumber MAIL_PORT = mail_portnumber
MAIL_SECURE (true|false) // true for port 465, fals for other ports MAIL_SECURE (true|false) // true for port 465, fals for other ports
CLUB_NAME = clubname CLUB_NAME = clubname #default FF Admin
CLUB_WEBSITE = https://my-club-website-url CLUB_WEBSITE = https://my-club-website-url

View file

@ -1,15 +1,17 @@
# member-administration-server # ff-admin-server
Mitgliederverwaltung für Feuerwehren und Vereine (Backend). Administration für Feuerwehren und Vereine (Backend).
## Einleitung ## Einleitung
Dieses Projekt, `member-administration-server`, ist das Backend zur Verwaltung von Mitgliederdaten. Die zugehörige Webapp ist im Repository [member-administration-ui](https://forgejo.jk-effects.cloud/Ehrenamt/member-administration-ui) zu finden. Dieses Projekt, `ff-admin-server`, ist das Backend zur Verwaltung von Mitgliederdaten. Die zugehörige Webapp ist im Repository [ff-admin-ui](https://forgejo.jk-effects.cloud/Ehrenamt/ff-admin) zu finden.
Eine Demo zusammen mit der `member-administration-ui` finden Sie unter [ff-admin-demo.jk-effects.cloud](ff-admin-demo.jk-effects.cloud). Eine Demo zusammen mit der `ff-admin` finden Sie unter [https://admin-demo.ff-admin.de](https://admin-demo.ff-admin.de).
## Installation ## Installation
Das Image exposed nur den Port 5000. Die Env-Variable SERVER_PORT kann nur im lokal ausführenden dev-Kontext verwendet werden.
### Docker Compose Setup ### Docker Compose Setup
Um den Container hochzufahren, erstellen Sie eine `docker-compose.yml` Datei mit folgendem Inhalt: Um den Container hochzufahren, erstellen Sie eine `docker-compose.yml` Datei mit folgendem Inhalt:
@ -18,26 +20,27 @@ Um den Container hochzufahren, erstellen Sie eine `docker-compose.yml` Datei mit
version: "3" version: "3"
services: services:
ff-member-administration-server: ff-ff-admin-server:
image: docker.registry.jk-effects.cloud/ehrenamt/member-administration/server:latest image: docker.registry.jk-effects.cloud/ehrenamt/ff-admin/server:latest
container_name: ff_member_administration_server container_name: ff_member_administration_server
restart: unless-stopped restart: unless-stopped
environment: environment:
- DB_TYPE=mysql - DB_TYPE=<mysql|sqlite> # default ist auf mysql gesetzt
- DB_HOST=ffm-db - DB_HOST=ffm-db
- DB_PORT=number # fallback ist auf 3306 gesetzt - DB_PORT=<number> # default ist auf 3306 gesetzt
- DB_NAME=administration - DB_NAME=ffadmin
- DB_USERNAME=administration_backend - DB_USERNAME=administration_backend
- DB_PASSWORD=<dbuserpasswd> - DB_PASSWORD=<dbuserpasswd>
- JWT_SECRET=<tobemodified> - JWT_SECRET=<tobemodified>
- JWT_EXPIRATION=<number[m|d] - bsp.:15m> - JWT_EXPIRATION=<number[m|d] - bsp.:15m> # default ist auf 15m gesetzt
- REFRESH_EXPIRATION=<number[m|d] - bsp.:1d> - 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_USERNAME=<mailadress|username>
- MAIL_PASSWORD=<password> - MAIL_PASSWORD=<password>
- MAIL_HOST=<url> - MAIL_HOST=<url>
- MAIL_PORT=<port> - MAIL_PORT=<port> # default ist auf 578 gesetzt
- MAIL_SECURE=<boolean> - MAIL_SECURE=<boolean> # default ist auf false gesetzt
- CLUB_NAME=<tobemodified> - CLUB_NAME=<tobemodified> # default ist auf FF Admin gesetzt
- CLUB_WEBSITE=<tobemodified> - CLUB_WEBSITE=<tobemodified>
volumes: volumes:
- <volume|local path>:/app/files - <volume|local path>:/app/files
@ -75,8 +78,8 @@ docker-compose up -d
Klonen Sie dieses Repository und installieren Sie die Abhängigkeiten: Klonen Sie dieses Repository und installieren Sie die Abhängigkeiten:
```sh ```sh
git clone https://forgejo.jk-effects.cloud/Ehrenamt/member-administration-server.git git clone https://forgejo.jk-effects.cloud/Ehrenamt/ff-admin-server.git
cd member-administration-server cd ff-admin-server
npm install npm install
npm run build npm run build
npm run start npm run start

56
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "member-administration-server", "name": "ff-admin-server",
"version": "1.0.0", "version": "1.2.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "member-administration-server", "name": "ff-admin-server",
"version": "1.0.0", "version": "1.2.0",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"dependencies": { "dependencies": {
"cors": "^2.8.5", "cors": "^2.8.5",
@ -24,6 +24,7 @@
"puppeteer": "^23.11.1", "puppeteer": "^23.11.1",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rss-parser": "^3.13.0",
"socket.io": "^4.7.5", "socket.io": "^4.7.5",
"speakeasy": "^2.0.0", "speakeasy": "^2.0.0",
"typeorm": "^0.3.20", "typeorm": "^0.3.20",
@ -1496,6 +1497,15 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}, },
"node_modules/entities": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
"license": "BSD-2-Clause",
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/env-paths": { "node_modules/env-paths": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
@ -3152,6 +3162,16 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/rss-parser": {
"version": "3.13.0",
"resolved": "https://registry.npmjs.org/rss-parser/-/rss-parser-3.13.0.tgz",
"integrity": "sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==",
"license": "MIT",
"dependencies": {
"entities": "^2.0.3",
"xml2js": "^0.5.0"
}
},
"node_modules/runes2": { "node_modules/runes2": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/runes2/-/runes2-1.1.4.tgz", "resolved": "https://registry.npmjs.org/runes2/-/runes2-1.1.4.tgz",
@ -3182,6 +3202,12 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
}, },
"node_modules/sax": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
"license": "ISC"
},
"node_modules/semver": { "node_modules/semver": {
"version": "7.6.3", "version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
@ -4253,6 +4279,28 @@
} }
} }
}, },
"node_modules/xml2js": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
"integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
"license": "MIT",
"dependencies": {
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
},
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/xmlbuilder": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
"license": "MIT",
"engines": {
"node": ">=4.0"
}
},
"node_modules/y18n": { "node_modules/y18n": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",

View file

@ -1,6 +1,6 @@
{ {
"name": "member-administration-server", "name": "ff-admin-server",
"version": "1.0.0", "version": "1.2.0",
"description": "Feuerwehr/Verein Mitgliederverwaltung Server", "description": "Feuerwehr/Verein Mitgliederverwaltung Server",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
@ -16,7 +16,7 @@
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://forgejo.jk-effects.cloud/Ehrenamt/member-administration-server.git" "url": "https://forgejo.jk-effects.cloud/Ehrenamt/ff-admin-server.git"
}, },
"keywords": [ "keywords": [
"Feuerwehr" "Feuerwehr"
@ -39,6 +39,7 @@
"puppeteer": "^23.11.1", "puppeteer": "^23.11.1",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"reflect-metadata": "^0.2.2", "reflect-metadata": "^0.2.2",
"rss-parser": "^3.13.0",
"socket.io": "^4.7.5", "socket.io": "^4.7.5",
"speakeasy": "^2.0.0", "speakeasy": "^2.0.0",
"typeorm": "^0.3.20", "typeorm": "^0.3.20",

View file

@ -33,7 +33,10 @@ export default abstract class MemberCommandHandler {
return result.identifiers[0].id; return result.identifiers[0].id;
}) })
.catch((err) => { .catch((err) => {
throw new InternalException("Failed creating member", err); throw new InternalException(
`Failed creating member${err.code.includes("ER_DUP_ENTRY") ? " due to duplicate entry for column" : ""}`,
err
);
}); });
} }
@ -43,7 +46,6 @@ export default abstract class MemberCommandHandler {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
static async update(updateMember: UpdateMemberCommand): Promise<void> { static async update(updateMember: UpdateMemberCommand): Promise<void> {
console.log(updateMember);
return await dataSource return await dataSource
.createQueryBuilder() .createQueryBuilder()
.update(member) .update(member)
@ -59,7 +61,10 @@ export default abstract class MemberCommandHandler {
.execute() .execute()
.then(() => {}) .then(() => {})
.catch((err) => { .catch((err) => {
throw new InternalException("Failed updating member", err); throw new InternalException(
`Failed updating member${err.code.includes("ER_DUP_ENTRY") ? " due to duplicate entry for column" : ""}`,
err
);
}); });
} }
@ -84,7 +89,7 @@ export default abstract class MemberCommandHandler {
.execute() .execute()
.then(() => {}) .then(() => {})
.catch((err) => { .catch((err) => {
throw new InternalException("Failed updating member", err); throw new InternalException(`Failed updating member`, err);
}); });
} }

View file

@ -31,7 +31,7 @@ export default abstract class MembershipCommandHandler {
.createQueryBuilder() .createQueryBuilder()
.update(membership) .update(membership)
.set({ .set({
end: new Date(createMembership.start).setDate(new Date(createMembership.start).getDate() - 1), end: new Date(new Date(createMembership.start).setDate(new Date(createMembership.start).getDate() - 1)),
terminationReason: "beendet durch neuen Eintrag.", terminationReason: "beendet durch neuen Eintrag.",
}) })
.where("end IS NULL") .where("end IS NULL")

View file

@ -6,4 +6,5 @@ export interface SynchronizeProtocolPresenceCommand {
export interface ProtocolPresenceCommand { export interface ProtocolPresenceCommand {
memberId: number; memberId: number;
absent: boolean; absent: boolean;
excused: boolean;
} }

View file

@ -74,6 +74,7 @@ export default abstract class ProtocolPresenceCommandHandler {
.update(protocolPresence) .update(protocolPresence)
.set({ .set({
absent: member.absent, absent: member.absent,
excused: member.excused,
}) })
.where("memberId = :memberId", { memberId: member.memberId }) .where("memberId = :memberId", { memberId: member.memberId })
.andWhere("protocolId = :protocolId", { protocolId }) .andWhere("protocolId = :protocolId", { protocolId })

View file

@ -1,5 +1,6 @@
export interface CreateRefreshCommand { export interface CreateRefreshCommand {
userId: number; userId: number;
isFromPwa?: boolean;
} }
export interface DeleteRefreshCommand { export interface DeleteRefreshCommand {

View file

@ -1,11 +1,9 @@
import { dataSource } from "../data-source"; import { dataSource } from "../data-source";
import { refresh } from "../entity/refresh"; import { refresh } from "../entity/refresh";
import { REFRESH_EXPIRATION } from "../env.defaults"; import { PWA_REFRESH_EXPIRATION, REFRESH_EXPIRATION } from "../env.defaults";
import InternalException from "../exceptions/internalException"; import InternalException from "../exceptions/internalException";
import { JWTHelper } from "../helpers/jwtHelper";
import { StringHelper } from "../helpers/stringHelper"; import { StringHelper } from "../helpers/stringHelper";
import UserService from "../service/user/userService"; import UserService from "../service/user/userService";
import { JWTRefresh } from "../type/jwtTypes";
import { CreateRefreshCommand, DeleteRefreshCommand } from "./refreshCommand"; import { CreateRefreshCommand, DeleteRefreshCommand } from "./refreshCommand";
import ms from "ms"; import ms from "ms";
@ -16,10 +14,6 @@ export default abstract class RefreshCommandHandler {
* @returns {Promise<string>} * @returns {Promise<string>}
*/ */
static async create(createRefresh: CreateRefreshCommand): Promise<string> { static async create(createRefresh: CreateRefreshCommand): Promise<string> {
// let createRefreshToken: JWTRefresh = {
// userId: createRefresh.userId,
// };
// const refreshToken = await JWTHelper.create(createRefreshToken);
const refreshToken = StringHelper.random(32); const refreshToken = StringHelper.random(32);
return await dataSource return await dataSource
@ -29,7 +23,9 @@ export default abstract class RefreshCommandHandler {
.values({ .values({
token: refreshToken, token: refreshToken,
user: await UserService.getById(createRefresh.userId), user: await UserService.getById(createRefresh.userId),
expiry: new Date(Date.now() + ms(REFRESH_EXPIRATION)), expiry: createRefresh.isFromPwa
? new Date(Date.now() + ms(PWA_REFRESH_EXPIRATION))
: new Date(Date.now() + ms(REFRESH_EXPIRATION)),
}) })
.execute() .execute()
.then((result) => { .then((result) => {

View file

@ -60,7 +60,10 @@ export default abstract class AwardCommandHandler {
.execute() .execute()
.then(() => {}) .then(() => {})
.catch((err) => { .catch((err) => {
throw new InternalException("Failed deleting award", err); throw new InternalException(
`Failed deleting award ${err.code.includes("ER_ROW_IS_REFERENCED") ? " due to referenced data" : ""}`,
err
);
}); });
} }
} }

View file

@ -66,7 +66,10 @@ export default abstract class CalendarTypeCommandHandler {
.execute() .execute()
.then(() => {}) .then(() => {})
.catch((err) => { .catch((err) => {
throw new InternalException("Failed deleting calendarType", err); throw new InternalException(
`Failed deleting calendarType${err.code.includes("ER_ROW_IS_REFERENCED") ? " due to referenced data" : ""}`,
err
);
}); });
} }
} }

View file

@ -66,7 +66,12 @@ export default abstract class CommunicationTypeCommandHandler {
.execute() .execute()
.then(() => {}) .then(() => {})
.catch((err) => { .catch((err) => {
throw new InternalException("Failed deleting communicationType", err); throw new InternalException(
`Failed deleting communicationType${
err.code.includes("ER_ROW_IS_REFERENCED") ? " due to referenced data" : ""
}`,
err
);
}); });
} }
} }

View file

@ -64,7 +64,12 @@ export default abstract class ExecutivePositionCommandHandler {
.execute() .execute()
.then(() => {}) .then(() => {})
.catch((err) => { .catch((err) => {
throw new InternalException("Failed deleting executivePosition", err); throw new InternalException(
`Failed deleting executivePosition${
err.code.includes("ER_ROW_IS_REFERENCED") ? " due to referenced data" : ""
}`,
err
);
}); });
} }
} }

View file

@ -64,7 +64,12 @@ export default abstract class MembershipStatusCommandHandler {
.execute() .execute()
.then(() => {}) .then(() => {})
.catch((err) => { .catch((err) => {
throw new InternalException("Failed deleting membershipStatus", err); throw new InternalException(
`Failed deleting membershipStatus${
err.code.includes("ER_ROW_IS_REFERENCED") ? " due to referenced data" : ""
}`,
err
);
}); });
} }
} }

View file

@ -4,7 +4,3 @@ export interface SetNewsletterConfigCommand {
comTypeId: number; comTypeId: number;
config: NewsletterConfigType; config: NewsletterConfigType;
} }
export interface DeleteNewsletterConfigCommand {
comTypeId: number;
}

View file

@ -1,7 +1,7 @@
import { dataSource } from "../../../data-source"; import { dataSource } from "../../../data-source";
import { newsletterConfig } from "../../../entity/settings/newsletterConfig"; import { newsletterConfig } from "../../../entity/settings/newsletterConfig";
import InternalException from "../../../exceptions/internalException"; import InternalException from "../../../exceptions/internalException";
import { DeleteNewsletterConfigCommand, SetNewsletterConfigCommand } from "./newsletterConfigCommand"; import { SetNewsletterConfigCommand } from "./newsletterConfigCommand";
export default abstract class NewsletterConfigCommandHandler { export default abstract class NewsletterConfigCommandHandler {
/** /**
@ -27,21 +27,4 @@ export default abstract class NewsletterConfigCommandHandler {
throw new InternalException("Failed setting newsletterConfig", err); throw new InternalException("Failed setting newsletterConfig", err);
}); });
} }
/**
* @description delete newsletterConfig
* @param {DeleteNewsletterConfigCommand} deleteNewsletterConfig
* @returns {Promise<void>}
*/
static async delete(deleteNewsletterConfig: DeleteNewsletterConfigCommand): Promise<void> {
return await dataSource
.createQueryBuilder()
.delete()
.from(newsletterConfig)
.where("comTypeId = :comTypeId", { comTypeId: deleteNewsletterConfig.comTypeId })
.execute()
.then(() => {})
.catch((err) => {
throw new InternalException("Failed setting newsletterConfig", err);
});
}
} }

View file

@ -66,7 +66,10 @@ export default abstract class QualificationCommandHandler {
.execute() .execute()
.then(() => {}) .then(() => {})
.catch((err) => { .catch((err) => {
throw new InternalException("Failed deleting qualification", err); throw new InternalException(
`Failed deleting qualification${err.code.includes("ER_ROW_IS_REFERENCED") ? " due to referenced data" : ""}`,
err
);
}); });
} }
} }

View file

@ -63,7 +63,10 @@ export default abstract class QueryStoreCommandHandler {
.execute() .execute()
.then(() => {}) .then(() => {})
.catch((err) => { .catch((err) => {
throw new InternalException("Failed deleting queryStore", err); throw new InternalException(
`Failed deleting queryStore${err.code.includes("ER_ROW_IS_REFERENCED") ? " due to referenced data" : ""}`,
err
);
}); });
} }
} }

View file

@ -64,7 +64,10 @@ export default abstract class TemplateCommandHandler {
.execute() .execute()
.then(() => {}) .then(() => {})
.catch((err) => { .catch((err) => {
throw new InternalException("Failed deleting template", err); throw new InternalException(
`Failed deleting template${err.code.includes("ER_ROW_IS_REFERENCED") ? " due to referenced data" : ""}`,
err
);
}); });
} }
} }

View file

@ -0,0 +1,19 @@
export interface CreateWebapiCommand {
title: string;
token: string;
expiry?: Date;
}
export interface UpdateWebapiCommand {
id: number;
title: string;
expiry?: Date;
}
export interface UpdateLastUsageWebapiCommand {
id: number;
}
export interface DeleteWebapiCommand {
id: number;
}

View file

@ -0,0 +1,100 @@
import { dataSource } from "../../../data-source";
import { webapi } from "../../../entity/user/webapi";
import InternalException from "../../../exceptions/internalException";
import {
CreateWebapiCommand,
DeleteWebapiCommand,
UpdateLastUsageWebapiCommand,
UpdateWebapiCommand,
} from "./webapiCommand";
export default abstract class WebapiCommandHandler {
/**
* @description create api
* @param {CreateWebapiCommand} createWebapi
* @returns {Promise<number>}
*/
static async create(createWebapi: CreateWebapiCommand): Promise<number> {
return await dataSource
.createQueryBuilder()
.insert()
.into(webapi)
.values({
token: createWebapi.token,
title: createWebapi.title,
expiry: createWebapi.expiry,
})
.execute()
.then((result) => {
return result.identifiers[0].token;
})
.catch((err) => {
throw new InternalException(
`Failed creating api${err.code.includes("ER_DUP_ENTRY") ? " due to duplicate entry for column" : ""}`,
err
);
});
}
/**
* @description update api
* @param {UpdateWebapiCommand} updateWebapi
* @returns {Promise<void>}
*/
static async update(updateWebapi: UpdateWebapiCommand): Promise<void> {
return await dataSource
.createQueryBuilder()
.update(webapi)
.set({
title: updateWebapi.title,
expiry: updateWebapi.expiry,
})
.where("id = :id", { id: updateWebapi.id })
.execute()
.then(() => {})
.catch((err) => {
throw new InternalException(
`Failed updating api${err.code.includes("ER_DUP_ENTRY") ? " due to duplicate entry for column" : ""}`,
err
);
});
}
/**
* @description update api usage
* @param {UpdateLastUsageWebapiCommand} updateWebapi
* @returns {Promise<void>}
*/
static async updateUsage(updateWebapi: UpdateLastUsageWebapiCommand): Promise<void> {
return await dataSource
.createQueryBuilder()
.update(webapi)
.set({
lastUsage: new Date(),
})
.where("id = :id", { id: updateWebapi.id })
.execute()
.then(() => {})
.catch((err) => {
throw new InternalException(`Failed updating api last usage`, err);
});
}
/**
* @description delete api
* @param {DeleteWebapiCommand} deleteWebapi
* @returns {Promise<void>}
*/
static async delete(deleteWebapi: DeleteWebapiCommand): Promise<void> {
return await dataSource
.createQueryBuilder()
.delete()
.from(webapi)
.where("id = :id", { id: deleteWebapi.id })
.execute()
.then(() => {})
.catch((err) => {
throw new InternalException("Failed deleting api", err);
});
}
}

View file

@ -0,0 +1,16 @@
import { PermissionString } from "../../../type/permissionTypes";
export interface CreateWebapiPermissionCommand {
permission: PermissionString;
webapiId: number;
}
export interface DeleteWebapiPermissionCommand {
permission: PermissionString;
webapiId: number;
}
export interface UpdateWebapiPermissionsCommand {
webapiId: number;
permissions: Array<PermissionString>;
}

View file

@ -0,0 +1,119 @@
import { DeleteResult, EntityManager, InsertResult } from "typeorm";
import { dataSource } from "../../../data-source";
import { webapiPermission } from "../../../entity/user/webapi_permission";
import InternalException from "../../../exceptions/internalException";
import WebapiService from "../../../service/user/webapiService";
import {
CreateWebapiPermissionCommand,
DeleteWebapiPermissionCommand,
UpdateWebapiPermissionsCommand,
} from "./webapiPermissionCommand";
import PermissionHelper from "../../../helpers/permissionHelper";
import WebapiPermissionService from "../../../service/user/webapiPermissionService";
import { PermissionString } from "../../../type/permissionTypes";
export default abstract class WebapiPermissionCommandHandler {
/**
* @description update api permissions
* @param {UpdateWebapiPermissionsCommand} updateWebapiPermissions
* @returns {Promise<void>}
*/
static async updatePermissions(updateWebapiPermissions: UpdateWebapiPermissionsCommand): Promise<void> {
let currentPermissions = (await WebapiPermissionService.getByApi(updateWebapiPermissions.webapiId)).map(
(r) => r.permission
);
return await dataSource.manager
.transaction(async (manager) => {
let newPermissions = PermissionHelper.getWhatToAdd(currentPermissions, updateWebapiPermissions.permissions);
let removePermissions = PermissionHelper.getWhatToRemove(
currentPermissions,
updateWebapiPermissions.permissions
);
if (newPermissions.length != 0) {
await this.updatePermissionsAdd(manager, updateWebapiPermissions.webapiId, newPermissions);
}
if (removePermissions.length != 0) {
await this.updatePermissionsRemove(manager, updateWebapiPermissions.webapiId, removePermissions);
}
})
.then(() => {})
.catch((err) => {
throw new InternalException("Failed saving api permissions", err);
});
}
private static async updatePermissionsAdd(
manager: EntityManager,
webapiId: number,
permissions: Array<PermissionString>
): Promise<InsertResult> {
return await manager
.createQueryBuilder()
.insert()
.into(webapiPermission)
.values(
permissions.map((p) => ({
permission: p,
webapiId: webapiId,
}))
)
.orIgnore()
.execute();
}
private static async updatePermissionsRemove(
manager: EntityManager,
webapiId: number,
permissions: Array<PermissionString>
): Promise<DeleteResult> {
return await manager
.createQueryBuilder()
.delete()
.from(webapiPermission)
.where("webapiId = :id", { id: webapiId })
.andWhere("permission IN (:...permission)", { permission: permissions })
.execute();
}
/**
* @description grant permission to user
* @param {CreateWebapiPermissionCommand} createPermission
* @returns {Promise<number>}
*/
static async create(createPermission: CreateWebapiPermissionCommand): Promise<number> {
return await dataSource
.createQueryBuilder()
.insert()
.into(webapiPermission)
.values({
permission: createPermission.permission,
webapiId: createPermission.webapiId,
})
.execute()
.then((result) => {
return result.identifiers[0].id;
})
.catch((err) => {
throw new InternalException("Failed saving api permission", err);
});
}
/**
* @description remove permission from api
* @param {DeleteWebapiPermissionCommand} deletePermission
* @returns {Promise<any>}
*/
static async delete(deletePermission: DeleteWebapiPermissionCommand): Promise<any> {
return await dataSource
.createQueryBuilder()
.delete()
.from(webapiPermission)
.where("webapiId = :id", { id: deletePermission.webapiId })
.andWhere("permission = :permission", { permission: deletePermission.permission })
.execute()
.then(() => {})
.catch((err) => {
throw new InternalException("failed api permission removal", err);
});
}
}

View file

@ -61,8 +61,13 @@ export async function getAllMembers(req: Request, res: Response): Promise<any> {
let offset = parseInt((req.query.offset as string) ?? "0"); let offset = parseInt((req.query.offset as string) ?? "0");
let count = parseInt((req.query.count as string) ?? "25"); let count = parseInt((req.query.count as string) ?? "25");
let search = (req.query.search as string) ?? ""; let search = (req.query.search as string) ?? "";
let noLimit = req.query.noLimit === "true";
let ids = ((req.query.ids ?? "") as string)
.split(",")
.filter((i) => i)
.map((i) => parseInt(i));
let [members, total] = await MemberService.getAll(offset, count, search); let [members, total] = await MemberService.getAll({ offset, count, search, noLimit, ids });
res.json({ res.json({
members: MemberFactory.mapToBase(members), members: MemberFactory.mapToBase(members),
@ -85,6 +90,19 @@ export async function getMemberById(req: Request, res: Response): Promise<any> {
res.json(MemberFactory.mapToSingle(member)); res.json(MemberFactory.mapToSingle(member));
} }
/**
* @description get member statistics by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getMemberStatisticsById(req: Request, res: Response): Promise<any> {
const memberId = parseInt(req.params.id);
let member = await MemberService.getStatisticsById(memberId);
res.json(MemberFactory.mapToMemberStatistic(member));
}
/** /**
* @description get memberships by member * @description get memberships by member
* @param req {Request} Express req object * @param req {Request} Express req object
@ -98,6 +116,19 @@ export async function getMembershipsByMember(req: Request, res: Response): Promi
res.json(MembershipFactory.mapToBase(memberships)); res.json(MembershipFactory.mapToBase(memberships));
} }
/**
* @description get member statistics by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getMembershipStatisticsById(req: Request, res: Response): Promise<any> {
const memberId = parseInt(req.params.memberId);
let member = await MembershipService.getStatisticsById(memberId);
res.json(MembershipFactory.mapToBaseStatistics(member));
}
/** /**
* @description get membership by member and record * @description get membership by member and record
* @param req {Request} Express req object * @param req {Request} Express req object
@ -227,14 +258,14 @@ export async function getCommunicationByMemberAndRecord(req: Request, res: Respo
* @returns {Promise<*>} * @returns {Promise<*>}
*/ */
export async function createMemberPrintoutList(req: Request, res: Response): Promise<any> { export async function createMemberPrintoutList(req: Request, res: Response): Promise<any> {
let members = await MemberService.getByRunningMembership() let members = await MemberService.getByRunningMembership();
let pdf = await PdfExport.renderFile({ let pdf = await PdfExport.renderFile({
title: "Mitgliederliste", title: "Mitgliederliste",
template: "member.list", template: "member.list",
saveToDisk: false, saveToDisk: false,
data: { data: {
member: members member: members,
}, },
}); });
@ -596,7 +627,7 @@ export async function updateCommunicationOfMember(req: Request, res: Response):
communicationId: recordId, communicationId: recordId,
}; };
await MemberCommandHandler.updateNewsletter(updateNewsletter); await MemberCommandHandler.updateNewsletter(updateNewsletter);
} else if (currentUserNewsletterMain.sendNewsletter.id == recordId) { } else if (currentUserNewsletterMain.sendNewsletter?.id == recordId) {
await MemberCommandHandler.unsetNewsletter(memberId); await MemberCommandHandler.unsetNewsletter(memberId);
} }

View file

@ -6,7 +6,10 @@ import NewsletterDatesFactory from "../../../factory/admin/club/newsletter/newsl
import NewsletterRecipientsService from "../../../service/club/newsletter/newsletterRecipientsService"; import NewsletterRecipientsService from "../../../service/club/newsletter/newsletterRecipientsService";
import NewsletterRecipientsFactory from "../../../factory/admin/club/newsletter/newsletterRecipients"; import NewsletterRecipientsFactory from "../../../factory/admin/club/newsletter/newsletterRecipients";
import { FileSystemHelper } from "../../../helpers/fileSystemHelper"; import { FileSystemHelper } from "../../../helpers/fileSystemHelper";
import { CreateNewsletterCommand, SynchronizeNewsletterCommand } from "../../../command/club/newsletter/newsletterCommand"; import {
CreateNewsletterCommand,
SynchronizeNewsletterCommand,
} from "../../../command/club/newsletter/newsletterCommand";
import NewsletterCommandHandler from "../../../command/club/newsletter/newsletterCommandHandler"; import NewsletterCommandHandler from "../../../command/club/newsletter/newsletterCommandHandler";
import { SynchronizeNewsletterDatesCommand } from "../../../command/club/newsletter/newsletterDatesCommand"; import { SynchronizeNewsletterDatesCommand } from "../../../command/club/newsletter/newsletterDatesCommand";
import NewsletterDatesCommandHandler from "../../../command/club/newsletter/newsletterDatesCommandHandler"; import NewsletterDatesCommandHandler from "../../../command/club/newsletter/newsletterDatesCommandHandler";
@ -188,15 +191,14 @@ export async function createNewsletter(req: Request, res: Response): Promise<any
export async function getNewsletterPrintoutProgressById(req: Request, res: Response): Promise<any> { export async function getNewsletterPrintoutProgressById(req: Request, res: Response): Promise<any> {
let newsletterId = parseInt(req.params.newsletterId); let newsletterId = parseInt(req.params.newsletterId);
res.setHeader("Content-Type", "text/event-stream"); res.writeHead(200, {
res.setHeader("Cache-Control", "no-cache"); "Content-Type": "text/plain",
res.setHeader("Connection", "keep-alive"); "Transfer-Encoding": "chunked",
});
res.flushHeaders();
const progressHandler = (data: NewsletterEventType) => { const progressHandler = (data: NewsletterEventType) => {
if (data.newsletterId == newsletterId && data.kind == "pdf") { if (data.newsletterId == newsletterId && data.kind == "pdf") {
res.write(JSON.stringify(data)); res.write(JSON.stringify(data) + "//");
} }
}; };
@ -287,15 +289,14 @@ export async function sendNewsletterById(req: Request, res: Response): Promise<a
export async function getNewsletterSendingProgressById(req: Request, res: Response): Promise<any> { export async function getNewsletterSendingProgressById(req: Request, res: Response): Promise<any> {
let newsletterId = parseInt(req.params.newsletterId); let newsletterId = parseInt(req.params.newsletterId);
res.setHeader("Content-Type", "text/event-stream"); res.writeHead(200, {
res.setHeader("Cache-Control", "no-cache"); "Content-Type": "text/plain",
res.setHeader("Connection", "keep-alive"); "Transfer-Encoding": "chunked",
});
res.flushHeaders();
const progressHandler = (data: NewsletterEventType) => { const progressHandler = (data: NewsletterEventType) => {
if (data.newsletterId == newsletterId && data.kind == "mail") { if (data.newsletterId == newsletterId && data.kind == "mail") {
res.write(JSON.stringify(data)); res.write(JSON.stringify(data) + "//");
} }
}; };

View file

@ -260,7 +260,9 @@ export async function createProtocolPrintoutById(req: Request, res: Response): P
agenda, agenda,
decisions, decisions,
presence: presence.filter((p) => !p.absent).map((p) => p.member), presence: presence.filter((p) => !p.absent).map((p) => p.member),
absent: presence.filter((p) => p.absent).map((p) => p.member), absent: presence.filter((p) => p.absent).map((p) => ({ ...p.member, excused: p.excused })),
excused_absent: presence.filter((p) => p.absent && p.excused).map((p) => p.member),
unexcused_absent: presence.filter((p) => p.absent && !p.excused).map((p) => p.member),
votings, votings,
}, },
}); });
@ -389,6 +391,7 @@ export async function synchronizeProtocolPrecenseById(req: Request, res: Respons
members: presence.map((p) => ({ members: presence.map((p) => ({
memberId: p.memberId, memberId: p.memberId,
absent: p.absent, absent: p.absent,
excused: p.excused,
})), })),
protocolId, protocolId,
}; };

View file

@ -2,7 +2,7 @@ import { Request, Response } from "express";
import NewsletterConfigService from "../../../service/settings/newsletterConfigService"; import NewsletterConfigService from "../../../service/settings/newsletterConfigService";
import NewsletterConfigFactory from "../../../factory/admin/settings/newsletterConfig"; import NewsletterConfigFactory from "../../../factory/admin/settings/newsletterConfig";
import NewsletterConfigCommandHandler from "../../../command/settings/newsletterConfig/newsletterConfigCommandHandler"; import NewsletterConfigCommandHandler from "../../../command/settings/newsletterConfig/newsletterConfigCommandHandler";
import { DeleteNewsletterConfigCommand, SetNewsletterConfigCommand } from "../../../command/settings/newsletterConfig/newsletterConfigCommand"; import { SetNewsletterConfigCommand } from "../../../command/settings/newsletterConfig/newsletterConfigCommand";
/** /**
* @description get all newsletterConfigs * @description get all newsletterConfigs
@ -47,20 +47,3 @@ export async function setNewsletterConfig(req: Request, res: Response): Promise<
res.send(id); res.send(id);
} }
/**
* @description delete award
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function deleteNewsletterConfig(req: Request, res: Response): Promise<any> {
const comTypeId = parseInt(req.params.comTypeId);
let deleteNewsletterConfig: DeleteNewsletterConfigCommand = {
comTypeId: comTypeId,
};
await NewsletterConfigCommandHandler.delete(deleteNewsletterConfig);
res.sendStatus(204);
}

View file

@ -0,0 +1,155 @@
import { Request, Response } from "express";
import WebapiService from "../../../service/user/webapiService";
import ApiFactory from "../../../factory/admin/user/webapi";
import WebapiPermissionService from "../../../service/user/webapiPermissionService";
import PermissionHelper from "../../../helpers/permissionHelper";
import {
CreateWebapiCommand,
DeleteWebapiCommand,
UpdateWebapiCommand,
} from "../../../command/user/webapi/webapiCommand";
import WebapiCommandHandler from "../../../command/user/webapi/webapiCommandHandler";
import { UpdateWebapiPermissionsCommand } from "../../../command/user/webapi/webapiPermissionCommand";
import WebapiPermissionCommandHandler from "../../../command/user/webapi/webapiPermissionCommandHandler";
import { JWTHelper } from "../../../helpers/jwtHelper";
import { CLUB_NAME } from "../../../env.defaults";
import { StringHelper } from "../../../helpers/stringHelper";
/**
* @description get All apis
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getAllWebapis(req: Request, res: Response): Promise<any> {
let apis = await WebapiService.getAll();
res.json(ApiFactory.mapToBase(apis));
}
/**
* @description get api by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getWebapiById(req: Request, res: Response): Promise<any> {
const id = parseInt(req.params.id);
let api = await WebapiService.getById(id);
res.json(ApiFactory.mapToSingle(api));
}
/**
* @description get api token by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getWebapiTokenById(req: Request, res: Response): Promise<any> {
const id = parseInt(req.params.id);
let { token } = await WebapiService.getTokenById(id);
res.send(token);
}
/**
* @description get permissions by api
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getWebapiPermissions(req: Request, res: Response): Promise<any> {
const id = parseInt(req.params.id);
let permissions = await WebapiPermissionService.getByApi(id);
res.json(PermissionHelper.convertToObject(permissions.map((p) => p.permission)));
}
/**
* @description create new api
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function createWebapi(req: Request, res: Response): Promise<any> {
let title = req.body.title;
let expiry = req.body.expiry || null;
let token = await JWTHelper.create(
{
iss: CLUB_NAME,
sub: "api_token_retrieve",
aud: StringHelper.random(32),
},
{ useExpiration: false }
);
let createApi: CreateWebapiCommand = {
token: token,
title: title,
expiry: expiry,
};
await WebapiCommandHandler.create(createApi);
res.sendStatus(204);
}
/**
* @description update api data
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function updateWebapi(req: Request, res: Response): Promise<any> {
const id = parseInt(req.params.id);
let title = req.body.title;
let expiry = req.body.expiry || null;
let updateApi: UpdateWebapiCommand = {
id: id,
title: title,
expiry: expiry,
};
await WebapiCommandHandler.update(updateApi);
res.sendStatus(204);
}
/**
* @description update api assigned permission strings
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function updateWebapiPermissions(req: Request, res: Response): Promise<any> {
const id = parseInt(req.params.id);
let permissions = req.body.permissions;
let permissionStrings = PermissionHelper.convertToStringArray(permissions);
let updateApiPermissions: UpdateWebapiPermissionsCommand = {
webapiId: id,
permissions: permissionStrings,
};
await WebapiPermissionCommandHandler.updatePermissions(updateApiPermissions);
res.sendStatus(204);
}
/**
* @description delete api by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function deleteWebapi(req: Request, res: Response): Promise<any> {
const id = parseInt(req.params.id);
let deleteApi: DeleteWebapiCommand = {
id: id,
};
await WebapiCommandHandler.delete(deleteApi);
res.sendStatus(204);
}

View file

@ -8,9 +8,6 @@ import UserService from "../service/user/userService";
import speakeasy from "speakeasy"; import speakeasy from "speakeasy";
import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException"; import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
import RefreshService from "../service/refreshService"; import RefreshService from "../service/refreshService";
import UserPermissionService from "../service/user/userPermissionService";
import PermissionHelper from "../helpers/permissionHelper";
import RolePermissionService from "../service/user/rolePermissionService";
/** /**
* @description Check authentication status by token * @description Check authentication status by token
@ -39,6 +36,7 @@ export async function login(req: Request, res: Response): Promise<any> {
let refreshCommand: CreateRefreshCommand = { let refreshCommand: CreateRefreshCommand = {
userId: id, userId: id,
isFromPwa: req.isPWA,
}; };
let refreshToken = await RefreshCommandHandler.create(refreshCommand); let refreshToken = await RefreshCommandHandler.create(refreshCommand);
@ -83,6 +81,7 @@ export async function refresh(req: Request, res: Response): Promise<any> {
let refreshCommand: CreateRefreshCommand = { let refreshCommand: CreateRefreshCommand = {
userId: tokenUserId, userId: tokenUserId,
isFromPwa: req.isPWA,
}; };
let refreshToken = await RefreshCommandHandler.create(refreshCommand); let refreshToken = await RefreshCommandHandler.create(refreshCommand);

View file

@ -0,0 +1,37 @@
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/user/userService";
import speakeasy from "speakeasy";
import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
import RefreshService from "../service/refreshService";
import WebapiService from "../service/user/webapiService";
import ForbiddenRequestException from "../exceptions/forbiddenRequestException";
import WebapiCommandHandler from "../command/user/webapi/webapiCommandHandler";
/**
* @description Check authentication status by token
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getWebApiAccess(req: Request, res: Response): Promise<any> {
const bearer = req.headers.authorization?.split(" ")?.[1] ?? undefined;
let { id, expiry } = await WebapiService.getByToken(bearer);
if (expiry != null && new Date() > new Date(expiry)) {
throw new ForbiddenRequestException("api token expired");
}
await WebapiCommandHandler.updateUsage({ id });
let accessToken = await JWTHelper.buildWebapiToken(bearer, expiry);
res.json({
accessToken,
});
}

View file

@ -66,6 +66,11 @@ import { PostalCode1735927918979 } from "./migrations/1735927918979-postalCode";
import { ProtocolAbsent1736072179716 } from "./migrations/1736072179716-protocolAbsent"; import { ProtocolAbsent1736072179716 } from "./migrations/1736072179716-protocolAbsent";
import { Memberlist1736079005086 } from "./migrations/1736079005086-memberlist"; import { Memberlist1736079005086 } from "./migrations/1736079005086-memberlist";
import { ExtendViewValues1736084198860 } from "./migrations/1736084198860-extendViewValues"; import { ExtendViewValues1736084198860 } from "./migrations/1736084198860-extendViewValues";
import { FinishInternalIdTransfer1736505324488 } from "./migrations/1736505324488-finishInternalIdTransfer";
import { ProtocolPresenceExcuse1737287798828 } from "./migrations/1737287798828-protocolPresenceExcuse";
import { webapi } from "./entity/user/webapi";
import { webapiPermission } from "./entity/user/webapi_permission";
import { AddWebapiTokens1737453096674 } from "./migrations/1737453096674-addwebapiTokens";
const dataSource = new DataSource({ const dataSource = new DataSource({
type: DB_TYPE as any, type: DB_TYPE as any,
@ -115,6 +120,8 @@ const dataSource = new DataSource({
memberExecutivePositionsView, memberExecutivePositionsView,
memberQualificationsView, memberQualificationsView,
membershipView, membershipView,
webapi,
webapiPermission,
], ],
migrations: [ migrations: [
Initial1724317398939, Initial1724317398939,
@ -142,6 +149,9 @@ const dataSource = new DataSource({
ProtocolAbsent1736072179716, ProtocolAbsent1736072179716,
Memberlist1736079005086, Memberlist1736079005086,
ExtendViewValues1736084198860, ExtendViewValues1736084198860,
FinishInternalIdTransfer1736505324488,
ProtocolPresenceExcuse1737287798828,
AddWebapiTokens1737453096674,
], ],
migrationsRun: true, migrationsRun: true,
migrationsTransactionMode: "each", migrationsTransactionMode: "each",

View file

@ -40,17 +40,17 @@ export class communication {
@Column() @Column()
typeId: number; typeId: number;
@ManyToOne(() => communicationType, (communicationType) => communicationType.communications, {
nullable: false,
onDelete: "RESTRICT",
onUpdate: "RESTRICT",
})
type: communicationType;
@ManyToOne(() => member, (member) => member.awards, { @ManyToOne(() => member, (member) => member.awards, {
nullable: false, nullable: false,
onDelete: "CASCADE", onDelete: "CASCADE",
onUpdate: "RESTRICT", onUpdate: "RESTRICT",
}) })
member: member; member: member;
@ManyToOne(() => communicationType, (communicationType) => communicationType.communications, {
nullable: false,
onDelete: "RESTRICT",
onUpdate: "RESTRICT",
})
type: communicationType;
} }

View file

@ -7,12 +7,6 @@ export class membership {
@PrimaryColumn({ generated: "increment", type: "int" }) @PrimaryColumn({ generated: "increment", type: "int" })
id: number; id: number;
/**
* @deprecated
*/
@Column({ type: "varchar", length: 255, unique: true, nullable: true })
internalId?: string;
@Column({ type: "date" }) @Column({ type: "date" })
start: Date; start: Date;

View file

@ -13,6 +13,9 @@ export class protocolPresence {
@Column({ type: "boolean", default: false }) @Column({ type: "boolean", default: false })
absent: boolean; absent: boolean;
@Column({ type: "boolean", default: true })
excused: boolean;
@ManyToOne(() => member, { @ManyToOne(() => member, {
nullable: false, nullable: false,
onDelete: "CASCADE", onDelete: "CASCADE",

View file

@ -9,7 +9,7 @@ export class protocolPrintout {
@Column({ type: "varchar", length: 255 }) @Column({ type: "varchar", length: 255 })
title: string; title: string;
@Column({ type: "int" }) @Column({ type: "int", default: "1" })
iteration: number; iteration: number;
@Column({ type: "varchar", length: 255 }) @Column({ type: "varchar", length: 255 })

26
src/entity/user/webapi.ts Normal file
View file

@ -0,0 +1,26 @@
import { Column, CreateDateColumn, Entity, OneToMany, PrimaryColumn } from "typeorm";
import { webapiPermission } from "./webapi_permission";
@Entity()
export class webapi {
@PrimaryColumn({ generated: "increment", type: "int" })
id: number;
@Column({ type: "text", unique: true, select: false })
token: string;
@Column({ type: "varchar", length: 255, unique: true })
title: string;
@CreateDateColumn()
createdAt: Date;
@Column({ type: "datetime", nullable: true })
lastUsage?: Date;
@Column({ type: "date", nullable: true })
expiry?: Date;
@OneToMany(() => webapiPermission, (apiPermission) => apiPermission.webapi)
permissions: webapiPermission[];
}

View file

@ -0,0 +1,19 @@
import { Column, Entity, ManyToOne, OneToMany, PrimaryColumn } from "typeorm";
import { PermissionObject, PermissionString } from "../../type/permissionTypes";
import { webapi } from "./webapi";
@Entity()
export class webapiPermission {
@PrimaryColumn({ type: "int" })
webapiId: number;
@PrimaryColumn({ type: "varchar", length: 255 })
permission: PermissionString;
@ManyToOne(() => webapi, {
nullable: false,
onDelete: "CASCADE",
onUpdate: "RESTRICT",
})
webapi: webapi;
}

View file

@ -13,6 +13,7 @@ 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_SECRET = process.env.JWT_SECRET ?? "my_jwt_secret_string_ilughfnadiuhgq§$IUZGFVRweiouarbt1oub3h5q4a";
export const JWT_EXPIRATION = process.env.JWT_EXPIRATION ?? "15m"; export const JWT_EXPIRATION = process.env.JWT_EXPIRATION ?? "15m";
export const REFRESH_EXPIRATION = process.env.REFRESH_EXPIRATION ?? "1d"; export const REFRESH_EXPIRATION = process.env.REFRESH_EXPIRATION ?? "1d";
export const PWA_REFRESH_EXPIRATION = process.env.PWA_REFRESH_EXPIRATION ?? "5d";
export const MAIL_USERNAME = process.env.MAIL_USERNAME ?? ""; export const MAIL_USERNAME = process.env.MAIL_USERNAME ?? "";
export const MAIL_PASSWORD = process.env.MAIL_PASSWORD ?? ""; export const MAIL_PASSWORD = process.env.MAIL_PASSWORD ?? "";
@ -20,7 +21,7 @@ export const MAIL_HOST = process.env.MAIL_HOST ?? "";
export const MAIL_PORT = Number(process.env.MAIL_PORT ?? "587"); export const MAIL_PORT = Number(process.env.MAIL_PORT ?? "587");
export const MAIL_SECURE = process.env.MAIL_SECURE ?? "false"; export const MAIL_SECURE = process.env.MAIL_SECURE ?? "false";
export const CLUB_NAME = process.env.CLUB_NAME ?? ""; export const CLUB_NAME = process.env.CLUB_NAME ?? "FF Admin";
export const CLUB_WEBSITE = process.env.CLUB_WEBSITE ?? ""; export const CLUB_WEBSITE = process.env.CLUB_WEBSITE ?? "";
export function configCheck() { export function configCheck() {
@ -35,6 +36,7 @@ export function configCheck() {
if (JWT_SECRET == "" || typeof JWT_SECRET != "string") throw new Error("set valid value to JWT_SECRET"); if (JWT_SECRET == "" || typeof JWT_SECRET != "string") throw new Error("set valid value to JWT_SECRET");
checkMS(JWT_EXPIRATION, "JWT_EXPIRATION"); checkMS(JWT_EXPIRATION, "JWT_EXPIRATION");
checkMS(REFRESH_EXPIRATION, "REFRESH_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_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_PASSWORD == "" || typeof MAIL_PASSWORD != "string") throw new Error("set valid value to MAIL_PASSWORD");
@ -42,7 +44,6 @@ export function configCheck() {
if (typeof MAIL_PORT != "number") throw new Error("set valid numeric value to MAIL_PORT"); if (typeof MAIL_PORT != "number") 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 (MAIL_SECURE != "true" && MAIL_SECURE != "false") throw new Error("set 'true' or 'false' to MAIL_SECURE");
console.log(CLUB_WEBSITE);
if ( if (
CLUB_WEBSITE != "" && CLUB_WEBSITE != "" &&
!/^(http(s):\/\/.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/.test(CLUB_WEBSITE) !/^(http(s):\/\/.)[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)$/.test(CLUB_WEBSITE)

View file

@ -1,5 +1,6 @@
import { member } from "../../../../entity/club/member/member"; import { member } from "../../../../entity/club/member/member";
import { MemberViewModel } from "../../../../viewmodel/admin/club/member/member.models"; import { MemberStatisticsViewModel, MemberViewModel } from "../../../../viewmodel/admin/club/member/member.models";
import { memberView } from "../../../../views/memberView";
import CommunicationFactory from "./communication"; import CommunicationFactory from "./communication";
import MembershipFactory from "./membership"; import MembershipFactory from "./membership";
@ -40,4 +41,23 @@ export default abstract class MemberFactory {
public static mapToBase(records: Array<member>): Array<MemberViewModel> { public static mapToBase(records: Array<member>): Array<MemberViewModel> {
return records.map((r) => this.mapToSingle(r)); return records.map((r) => this.mapToSingle(r));
} }
/**
* @description map view record to MemberMembershipStatisticsViewModel
* @param {memberView} record
* @returns {MemberStatisticsViewModel}
*/
public static mapToMemberStatistic(record: memberView): MemberStatisticsViewModel {
return {
id: record.id,
salutation: record.salutation,
firstname: record.firstname,
lastname: record.lastname,
nameaffix: record.nameaffix,
birthdate: record.birthdate,
todayAge: record.todayAge,
ageThisYear: record.ageThisYear,
exactAge: record.exactAge,
};
}
} }

View file

@ -1,5 +1,9 @@
import { membership } from "../../../../entity/club/member/membership"; import { membership } from "../../../../entity/club/member/membership";
import { MembershipViewModel } from "../../../../viewmodel/admin/club/member/membership.models"; import {
MembershipStatisticsViewModel,
MembershipViewModel,
} from "../../../../viewmodel/admin/club/member/membership.models";
import { membershipView } from "../../../../views/membershipsView";
export default abstract class MembershipFactory { export default abstract class MembershipFactory {
/** /**
@ -26,4 +30,33 @@ export default abstract class MembershipFactory {
public static mapToBase(records: Array<membership>): Array<MembershipViewModel> { public static mapToBase(records: Array<membership>): Array<MembershipViewModel> {
return records.map((r) => this.mapToSingle(r)); return records.map((r) => this.mapToSingle(r));
} }
/**
* @description map view record to MembershipStatisticsViewModel
* @param {membershipView} record
* @returns {MembershipStatisticsViewModel}
*/
public static mapToSingleStatistic(record: membershipView): MembershipStatisticsViewModel {
return {
durationInDays: record.durationInDays,
durationInYears: record.durationInYears,
status: record.status,
statusId: record.statusId,
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
* @returns {Array<MembershipStatisticsViewModel>}
*/
public static mapToBaseStatistics(records: Array<membershipView>): Array<MembershipStatisticsViewModel> {
return records.map((r) => this.mapToSingleStatistic(r));
}
} }

View file

@ -12,6 +12,7 @@ export default abstract class ProtocolPresenceFactory {
return { return {
memberId: record.member.id, memberId: record.member.id,
absent: record.absent, absent: record.absent,
excused: record.excused,
protocolId: record.protocolId, protocolId: record.protocolId,
}; };
} }

View file

@ -0,0 +1,30 @@
import { webapi } from "../../../entity/user/webapi";
import PermissionHelper from "../../../helpers/permissionHelper";
import { ApiViewModel } from "../../../viewmodel/admin/user/webapi.models";
export default abstract class ApiFactory {
/**
* @description map record to api
* @param {webapi} record
* @returns {apiViewModel}
*/
public static mapToSingle(record: webapi): ApiViewModel {
return {
id: record.id,
permissions: PermissionHelper.convertToObject(record.permissions.map((e) => e.permission)),
title: record.title,
expiry: record.expiry,
lastUsage: record.lastUsage,
createdAt: record.createdAt,
};
}
/**
* @description map records to api
* @param {Array<webapi>} records
* @returns {Array<apiViewModel>}
*/
public static mapToBase(records: Array<webapi>): Array<ApiViewModel> {
return records.map((r) => this.mapToSingle(r));
}
}

View file

@ -6,6 +6,9 @@ import RolePermissionService from "../service/user/rolePermissionService";
import UserPermissionService from "../service/user/userPermissionService"; import UserPermissionService from "../service/user/userPermissionService";
import UserService from "../service/user/userService"; import UserService from "../service/user/userService";
import PermissionHelper from "./permissionHelper"; import PermissionHelper from "./permissionHelper";
import WebapiService from "../service/user/webapiService";
import WebapiPermissionService from "../service/user/webapiPermissionService";
import ms from "ms";
export abstract class JWTHelper { export abstract class JWTHelper {
static validate(token: string): Promise<string | jwt.JwtPayload> { static validate(token: string): Promise<string | jwt.JwtPayload> {
@ -17,13 +20,16 @@ export abstract class JWTHelper {
}); });
} }
static create(data: JWTData): Promise<string> { static create(
data: JWTData,
{ expOverwrite, useExpiration }: { expOverwrite?: number; useExpiration?: boolean } = { useExpiration: true }
): Promise<string> {
return new Promise<string>((resolve, reject) => { return new Promise<string>((resolve, reject) => {
jwt.sign( jwt.sign(
data, data,
JWT_SECRET, JWT_SECRET,
{ {
expiresIn: JWT_EXPIRATION, ...(useExpiration ?? true ? { expiresIn: expOverwrite ?? JWT_EXPIRATION } : {}),
}, },
(err, token) => { (err, token) => {
if (err) reject(err.message); if (err) reject(err.message);
@ -72,4 +78,35 @@ export abstract class JWTHelper {
throw new InternalException("Failed accessToken creation", err); throw new InternalException("Failed accessToken creation", err);
}); });
} }
static async buildWebapiToken(token: string, expiration?: Date): Promise<string> {
let { id, title } = await WebapiService.getByToken(token);
let webapiPermissions = await WebapiPermissionService.getByApi(id);
let webapiPermissionStrings = webapiPermissions.map((e) => e.permission);
let permissionObject = PermissionHelper.convertToObject(webapiPermissionStrings);
let jwtData: JWTToken = {
userId: id,
mail: "",
username: title,
firstname: "",
lastname: "",
isOwner: false,
permissions: permissionObject,
sub: "webapi_access_token",
};
let overwriteExpiration =
ms(JWT_EXPIRATION) < new Date().getTime() - new Date(expiration).getTime()
? null
: Date.now() - new Date(expiration).getTime();
return await JWTHelper.create(jwtData, { expOverwrite: overwriteExpiration, useExpiration: true })
.then((result) => {
return result;
})
.catch((err) => {
throw new InternalException("Failed webapi accessToken creation", err);
});
}
} }

View file

@ -33,12 +33,21 @@ export abstract class NewsletterHelper {
private static formatJobEmit( private static formatJobEmit(
event: "progress" | "complete", event: "progress" | "complete",
kind: "pdf" | "mail", kind: "pdf" | "mail",
factor: "success" | "failed" | "info",
newsletterId: number, newsletterId: number,
total: number, total: number,
iteration: number, iteration: number,
msg: string msg: string
) { ) {
this.jobStatus.emit<NewsletterEventType>(event, { kind, newsletterId, total, iteration, msg, date: new Date() }); this.jobStatus.emit<NewsletterEventType>(event, {
kind,
newsletterId,
factor,
total,
iteration,
msg,
date: new Date(),
});
} }
public static buildData( public static buildData(
@ -132,9 +141,7 @@ export abstract class NewsletterHelper {
} }
} }
console.log(queryMemberIds); let members = await MemberService.getAll({ noLimit: true });
let members = await MemberService.getAll(0, 1000);
return members[0].filter((m) => queryMemberIds.includes(m.id)); return members[0].filter((m) => queryMemberIds.includes(m.id));
} }
@ -171,18 +178,9 @@ export abstract class NewsletterHelper {
allowedForMail.includes(m.sendNewsletter?.type?.id) allowedForMail.includes(m.sendNewsletter?.type?.id)
); );
this.formatJobEmit("progress", "mail", newsletterId, mailRecipients.length, 0, "starting sending"); this.formatJobEmit("progress", "mail", "info", newsletterId, mailRecipients.length, 0, "starting sending");
for (const [index, rec] of mailRecipients.entries()) { for (const [index, rec] of mailRecipients.entries()) {
this.formatJobEmit(
"progress",
"mail",
newsletterId,
mailRecipients.length,
index,
`start sending to ${rec.sendNewsletter.email}`
);
let data = this.buildData(newsletter, dates, rec); let data = this.buildData(newsletter, dates, rec);
const { body } = await TemplateHelper.renderFileForModule({ const { body } = await TemplateHelper.renderFileForModule({
@ -197,9 +195,10 @@ export abstract class NewsletterHelper {
this.formatJobEmit( this.formatJobEmit(
"progress", "progress",
"mail", "mail",
"success",
newsletterId, newsletterId,
mailRecipients.length, mailRecipients.length,
index, index + 1,
`successfully sent to ${rec.sendNewsletter.email}` `successfully sent to ${rec.sendNewsletter.email}`
); );
}) })
@ -207,18 +206,19 @@ export abstract class NewsletterHelper {
this.formatJobEmit( this.formatJobEmit(
"progress", "progress",
"mail", "mail",
"failed",
newsletterId, newsletterId,
mailRecipients.length, mailRecipients.length,
index, index + 1,
`failed to send to ${rec.sendNewsletter.email}` `failed to send to ${rec.sendNewsletter.email}`
); );
console.log("mail send", err);
}); });
} }
this.formatJobEmit( this.formatJobEmit(
"complete", "complete",
"mail", "mail",
"info",
newsletterId, newsletterId,
mailRecipients.length, mailRecipients.length,
mailRecipients.length, mailRecipients.length,
@ -249,21 +249,12 @@ export abstract class NewsletterHelper {
(m) => !notAllowedForPdf.includes(m.sendNewsletter?.type?.id) || m.sendNewsletter == null (m) => !notAllowedForPdf.includes(m.sendNewsletter?.type?.id) || m.sendNewsletter == null
); );
this.formatJobEmit("progress", "pdf", newsletterId, pdfRecipients.length, 0, "starting sending"); this.formatJobEmit("progress", "pdf", "info", newsletterId, pdfRecipients.length + 1, 0, "starting sending");
for (const [index, rec] of [ for (const [index, rec] of [
...pdfRecipients, ...pdfRecipients,
{ id: 0, firstname: "Alle Mitglieder", lastname: CLUB_NAME } as member, { id: 0, firstname: "Alle Mitglieder", lastname: CLUB_NAME } as member,
].entries()) { ].entries()) {
this.formatJobEmit(
"progress",
"pdf",
newsletterId,
pdfRecipients.length,
index,
`start print for ${rec.lastname}, ${rec.firstname}`
);
let data = this.buildData(newsletter, dates, rec, printWithAdress.includes(rec.sendNewsletter?.type?.id)); let data = this.buildData(newsletter, dates, rec, printWithAdress.includes(rec.sendNewsletter?.type?.id));
await PdfExport.renderFile({ await PdfExport.renderFile({
@ -277,9 +268,10 @@ export abstract class NewsletterHelper {
this.formatJobEmit( this.formatJobEmit(
"progress", "progress",
"pdf", "pdf",
"success",
newsletterId, newsletterId,
pdfRecipients.length, pdfRecipients.length + 1,
index, index + 1,
`successfully printed for ${rec.lastname}, ${rec.firstname}` `successfully printed for ${rec.lastname}, ${rec.firstname}`
); );
}) })
@ -287,24 +279,15 @@ export abstract class NewsletterHelper {
this.formatJobEmit( this.formatJobEmit(
"progress", "progress",
"pdf", "pdf",
"failed",
newsletterId, newsletterId,
pdfRecipients.length, pdfRecipients.length + 1,
index, index + 1,
`failed print for ${rec.lastname}, ${rec.firstname}` `failed print for ${rec.lastname}, ${rec.firstname}`
); );
console.log("pdf print", err);
}); });
} }
this.formatJobEmit(
"progress",
"pdf",
newsletterId,
pdfRecipients.length,
pdfRecipients.length,
"starting pdf combine"
);
await PdfExport.sqashToSingleFile( await PdfExport.sqashToSingleFile(
`newsletter/${newsletter.id}_${newsletter.title.replace(" ", "")}`, `newsletter/${newsletter.id}_${newsletter.title.replace(" ", "")}`,
"allPdfsTogether", "allPdfsTogether",
@ -314,9 +297,10 @@ export abstract class NewsletterHelper {
this.formatJobEmit( this.formatJobEmit(
"progress", "progress",
"pdf", "pdf",
"success",
newsletterId, newsletterId,
pdfRecipients.length, pdfRecipients.length + 1,
pdfRecipients.length, pdfRecipients.length + 1,
"sucessfully combined pdf" "sucessfully combined pdf"
); );
}) })
@ -324,20 +308,21 @@ export abstract class NewsletterHelper {
this.formatJobEmit( this.formatJobEmit(
"progress", "progress",
"pdf", "pdf",
"failed",
newsletterId, newsletterId,
pdfRecipients.length, pdfRecipients.length + 1,
pdfRecipients.length, pdfRecipients.length + 1,
"failed combining pdf" "failed combining pdf"
); );
console.log("pdf squash", err);
}); });
this.formatJobEmit( this.formatJobEmit(
"complete", "complete",
"pdf", "pdf",
"info",
newsletterId, newsletterId,
pdfRecipients.length, pdfRecipients.length + 1,
pdfRecipients.length, pdfRecipients.length + 1,
`completed printing process` `completed printing process`
); );
} }

View file

@ -89,6 +89,19 @@ export default class PermissionHelper {
}; };
} }
static isAdminMiddleware(): (req: Request, res: Response, next: Function) => void {
return (req: Request, res: Response, next: Function) => {
const permissions = req.permissions;
const isOwner = req.isOwner;
if (isOwner || permissions.admin) {
next();
} else {
throw new ForbiddenRequestException(`missing admin permission`);
}
};
}
static convertToObject(permissions: Array<PermissionString>): PermissionObject { static convertToObject(permissions: Array<PermissionString>): PermissionObject {
if (permissions.includes("*")) { if (permissions.includes("*")) {
return { return {

View file

@ -12,6 +12,8 @@ declare global {
username: string; username: string;
isOwner: boolean; isOwner: boolean;
permissions: PermissionObject; permissions: PermissionObject;
isPWA: boolean;
isWebApiRequest: boolean;
} }
} }
} }
@ -23,7 +25,7 @@ const app = express();
import router from "./routes/index"; import router from "./routes/index";
router(app); router(app);
app.listen(process.env.NODE_ENV ? SERVER_PORT : 5000, () => { app.listen(process.env.NODE_ENV ? SERVER_PORT : 5000, () => {
console.log(`listening on *:${SERVER_PORT}`); console.log(`listening on *:${process.env.NODE_ENV ? SERVER_PORT : 5000}`);
}); });
import schedule from "node-schedule"; import schedule from "node-schedule";

View file

@ -29,10 +29,15 @@ export default async function authenticate(req: Request, res: Response, next: Fu
throw new InternalException("process failed"); throw new InternalException("process failed");
} }
if (decoded?.sub == "api_token_retrieve") {
throw new BadRequestException("This token is only authorized to get temporary access tokens via GET /api/webapi");
}
req.userId = decoded.userId; req.userId = decoded.userId;
req.username = decoded.username; req.username = decoded.username;
req.isOwner = decoded.isOwner; req.isOwner = decoded.isOwner;
req.permissions = decoded.permissions; req.permissions = decoded.permissions;
req.isWebApiRequest = decoded?.sub == "webapi_access_token";
next(); next();
} }

View file

@ -0,0 +1,37 @@
import { Request, Response } from "express";
import jwt from "jsonwebtoken";
import BadRequestException from "../exceptions/badRequestException";
import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
import InternalException from "../exceptions/internalException";
import { JWTHelper } from "../helpers/jwtHelper";
export default async function authenticateAPI(req: Request, res: Response, next: Function) {
const bearer = req.headers.authorization?.split(" ")?.[1] ?? undefined;
if (!bearer) {
throw new BadRequestException("Provide valid Authorization Header");
}
let decoded: string | jwt.JwtPayload;
await JWTHelper.validate(bearer)
.then((result) => {
decoded = result;
})
.catch((err) => {
if (err == "jwt expired") {
throw new UnauthorizedRequestException("Token expired", err);
} else {
throw new BadRequestException("Failed Authorization Header decoding", err);
}
});
if (typeof decoded == "string" || !decoded) {
throw new InternalException("process failed");
}
if (decoded?.sub != "api_token_retrieve") {
throw new BadRequestException("This route can only be accessed via a api token");
}
next();
}

View file

@ -0,0 +1,11 @@
import { Request, Response } from "express";
export default async function detectPWA(req: Request, res: Response, next: Function) {
const userAgent = req.headers["user-agent"] || "";
if ((userAgent.includes("Mobile") && userAgent.includes("Standalone")) || req.headers["x-pwa-client"] === "true") {
req.isPWA = true;
} else {
req.isPWA = false;
}
next();
}

View file

@ -1,6 +1,7 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { ExceptionBase } from "../exceptions/exceptionsBaseType"; import { ExceptionBase } from "../exceptions/exceptionsBaseType";
import CustomRequestException from "../exceptions/customRequestException"; import CustomRequestException from "../exceptions/customRequestException";
import UnauthorizedRequestException from "../exceptions/unauthorizedRequestException";
export default function errorHandler(err: ExceptionBase | Error, req: Request, res: Response, next: Function) { export default function errorHandler(err: ExceptionBase | Error, req: Request, res: Response, next: Function) {
let status = 500; let status = 500;
@ -11,7 +12,11 @@ export default function errorHandler(err: ExceptionBase | Error, req: Request, r
msg = err.message; msg = err.message;
} }
console.log("Handler", err); if (err instanceof CustomRequestException) {
console.log("Custom Handler", status, msg);
} else {
console.log("Error Handler", err);
}
res.status(status).send(msg); res.status(status).send(msg);
} }

View file

@ -0,0 +1,10 @@
import { Request, Response } from "express";
import ForbiddenRequestException from "../exceptions/forbiddenRequestException";
export default async function preventWebapiAccess(req: Request, res: Response, next: Function) {
if (req.isWebApiRequest) {
throw new ForbiddenRequestException("This route cannot be accessed via webapi");
} else {
next();
}
}

View file

@ -18,27 +18,25 @@ export class InternalId1735822722235 implements MigrationInterface {
}) })
); );
let memberships = await queryRunner.manager.getRepository(membership).find(); // let memberships = await queryRunner.manager.getRepository(membership).find();
console.log(memberships); // let internalIds = memberships.reduce<{ [key: number]: Array<string> }>((acc, cur) => {
let internalIds = memberships.reduce<{ [key: number]: Array<string> }>((acc, cur) => { // let memberId = cur.memberId;
let memberId = cur.memberId; // let setIds = acc[memberId] ?? [];
let setIds = acc[memberId] ?? []; // if (cur?.internalId) {
if (cur?.internalId) { // setIds.push(cur.internalId);
setIds.push(cur.internalId); // }
} // acc[memberId] = setIds;
acc[memberId] = setIds; // return acc;
return acc; // }, {});
}, {});
console.log(internalIds); // for (const [id, value] of Object.entries(internalIds)) {
for (const [id, value] of Object.entries(internalIds)) { // const ids = value.filter((v) => v != null).join(", ");
const ids = value.filter((v) => v != null).join(", "); // if (ids) {
if (ids) { // let m = await queryRunner.manager.getRepository(member).findOneByOrFail({ id: parseInt(id) });
let m = await queryRunner.manager.getRepository(member).findOneByOrFail({ id: parseInt(id) }); // m.internalId = ids;
m.internalId = ids; // await queryRunner.manager.getRepository(member).save(m);
await queryRunner.manager.getRepository(member).save(m); // }
} // }
}
} }
public async down(queryRunner: QueryRunner): Promise<void> { public async down(queryRunner: QueryRunner): Promise<void> {

View file

@ -55,7 +55,8 @@ export class ExtendViewValues1736084198860 implements MigrationInterface {
) )
.leftJoin("membership.status", "status") .leftJoin("membership.status", "status")
.leftJoin("membership.member", "member") .leftJoin("membership.member", "member")
.groupBy("status.id"), .groupBy("status.id")
.addGroupBy("member.id"),
}), }),
true true
); );

View file

@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
export class FinishInternalIdTransfer1736505324488 implements MigrationInterface {
name = "FinishInternalIdTransfer1736505324488";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn("membership", "internalId");
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
"membership",
new TableColumn({
name: "internalId",
type: "varchar",
length: "255",
default: null,
isNullable: true,
isUnique: true,
})
);
}
}

View file

@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
export class ProtocolPresenceExcuse1737287798828 implements MigrationInterface {
name = "ProtocolPresenceExcuse1737287798828";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
"protocol_presence",
new TableColumn({
name: "excused",
type: "tinyint",
default: "1",
isNullable: false,
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn("protocol_presence", "excused");
}
}

View file

@ -0,0 +1,55 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from "typeorm";
import { DB_TYPE } from "../env.defaults";
export class AddWebapiTokens1737453096674 implements MigrationInterface {
name = "AddWebApiTokens1737453096674";
public async up(queryRunner: QueryRunner): Promise<void> {
const variableType_int = DB_TYPE == "mysql" ? "int" : "integer";
await queryRunner.createTable(
new Table({
name: "webapi",
columns: [
{ name: "id", type: variableType_int, isPrimary: true, isGenerated: true, generationStrategy: "increment" },
{ name: "token", type: "text", isUnique: true, isNullable: false },
{ name: "title", type: "varchar", isUnique: true, length: "255", isNullable: false },
{ name: "createdAt", type: "datetime", default: "CURRENT_TIMESTAMP(6)", isNullable: false },
{ name: "lastUsage", type: "datetime", isNullable: true, default: null },
{ name: "expiry", type: "date", isNullable: true, default: null },
],
}),
true
);
await queryRunner.createTable(
new Table({
name: "webapi_permission",
columns: [
{ name: "webapiId", type: variableType_int, isPrimary: true, isNullable: false },
{ name: "permission", type: "varchar", length: "255", isPrimary: true, isNullable: false },
],
}),
true
);
await queryRunner.createForeignKey(
"webapi_permission",
new TableForeignKey({
columnNames: ["webapiId"],
referencedColumnNames: ["id"],
referencedTableName: "webapi",
onDelete: "CASCADE",
onUpdate: "RESTRICT",
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const table = await queryRunner.getTable("webapi_permission");
const foreignKey = table.foreignKeys.find((fk) => fk.columnNames.indexOf("webapiId") !== -1);
await queryRunner.dropForeignKey("webapi_permission", foreignKey);
await queryRunner.dropTable("webapi_permission");
await queryRunner.dropTable("webapi");
}
}

View file

@ -23,6 +23,8 @@ import {
getMemberById, getMemberById,
getMembershipByMemberAndRecord, getMembershipByMemberAndRecord,
getMembershipsByMember, getMembershipsByMember,
getMembershipStatisticsById,
getMemberStatisticsById,
getQualificationByMemberAndRecord, getQualificationByMemberAndRecord,
getQualificationsByMember, getQualificationsByMember,
updateAwardOfMember, updateAwardOfMember,
@ -44,6 +46,10 @@ router.get("/:id", async (req: Request, res: Response) => {
await getMemberById(req, res); await getMemberById(req, res);
}); });
router.get("/:id/statistics", async (req: Request, res: Response) => {
await getMemberStatisticsById(req, res);
});
router.get("/print/namelist", async (req: Request, res: Response) => { router.get("/print/namelist", async (req: Request, res: Response) => {
await createMemberPrintoutList(req, res); await createMemberPrintoutList(req, res);
}); });
@ -52,6 +58,10 @@ router.get("/:memberId/memberships", async (req: Request, res: Response) => {
await getMembershipsByMember(req, res); await getMembershipsByMember(req, res);
}); });
router.get("/:memberId/memberships/statistics", async (req: Request, res: Response) => {
await getMembershipStatisticsById(req, res);
});
router.get("/:memberId/membership/:id", async (req: Request, res: Response) => { router.get("/:memberId/membership/:id", async (req: Request, res: Response) => {
await getMembershipByMemberAndRecord(req, res); await getMembershipByMemberAndRecord(req, res);
}); });

View file

@ -21,6 +21,8 @@ import newsletter from "./club/newsletter";
import role from "./user/role"; import role from "./user/role";
import user from "./user/user"; import user from "./user/user";
import invite from "./user/invite"; import invite from "./user/invite";
import api from "./user/webapi";
import preventWebapiAccess from "../../middleware/preventWebApiAccess";
var router = express.Router({ mergeParams: true }); var router = express.Router({ mergeParams: true });
@ -60,5 +62,6 @@ router.use("/newsletter", PermissionHelper.passCheckMiddleware("read", "club", "
router.use("/role", PermissionHelper.passCheckMiddleware("read", "user", "role"), role); router.use("/role", PermissionHelper.passCheckMiddleware("read", "user", "role"), role);
router.use("/user", PermissionHelper.passCheckMiddleware("read", "user", "user"), user); router.use("/user", PermissionHelper.passCheckMiddleware("read", "user", "user"), user);
router.use("/invite", PermissionHelper.passCheckMiddleware("read", "user", "user"), invite); router.use("/invite", PermissionHelper.passCheckMiddleware("read", "user", "user"), invite);
router.use("/webapi", preventWebapiAccess, PermissionHelper.passCheckMiddleware("read", "user", "webapi"), api);
export default router; export default router;

View file

@ -1,6 +1,5 @@
import express, { Request, Response } from "express"; import express, { Request, Response } from "express";
import { import {
deleteNewsletterConfig,
getAllNewsletterConfigs, getAllNewsletterConfigs,
getNewsletterConfigById, getNewsletterConfigById,
setNewsletterConfig, setNewsletterConfig,
@ -25,12 +24,4 @@ router.put(
} }
); );
router.delete(
"/:comTypeId",
PermissionHelper.passCheckMiddleware("create", "settings", "newsletter_config"),
async (req: Request, res: Response) => {
await deleteNewsletterConfig(req, res);
}
);
export default router; export default router;

View file

@ -0,0 +1,64 @@
import express, { Request, Response } from "express";
import PermissionHelper from "../../../helpers/permissionHelper";
import {
createWebapi,
deleteWebapi,
getAllWebapis,
getWebapiById,
getWebapiPermissions,
getWebapiTokenById,
updateWebapi,
updateWebapiPermissions,
} from "../../../controller/admin/user/webapiController";
var router = express.Router({ mergeParams: true });
router.get("/", async (req: Request, res: Response) => {
await getAllWebapis(req, res);
});
router.get("/:id", async (req: Request, res: Response) => {
await getWebapiById(req, res);
});
router.get("/:id/token", async (req: Request, res: Response) => {
await getWebapiTokenById(req, res);
});
router.get("/:id/permissions", async (req: Request, res: Response) => {
await getWebapiPermissions(req, res);
});
router.post(
"/",
PermissionHelper.passCheckMiddleware("create", "user", "webapi"),
async (req: Request, res: Response) => {
await createWebapi(req, res);
}
);
router.patch(
"/:id",
PermissionHelper.passCheckMiddleware("update", "user", "webapi"),
async (req: Request, res: Response) => {
await updateWebapi(req, res);
}
);
router.patch(
"/:id/permissions",
PermissionHelper.passCheckMiddleware("admin", "user", "webapi"),
async (req: Request, res: Response) => {
await updateWebapiPermissions(req, res);
}
);
router.delete(
"/:id",
PermissionHelper.passCheckMiddleware("delete", "user", "webapi"),
async (req: Request, res: Response) => {
await deleteWebapi(req, res);
}
);
export default router;

View file

@ -13,6 +13,11 @@ import reset from "./reset";
import auth from "./auth"; import auth from "./auth";
import admin from "./admin/index"; import admin from "./admin/index";
import user from "./user"; import user from "./user";
import detectPWA from "../middleware/detectPWA";
import api from "./webapi";
import authenticateAPI from "../middleware/authenticateAPI";
import server from "./server";
import PermissionHelper from "../helpers/permissionHelper";
export default (app: Express) => { export default (app: Express) => {
app.set("query parser", "extended"); app.set("query parser", "extended");
@ -25,13 +30,16 @@ export default (app: Express) => {
app.use(cors()); app.use(cors());
app.options("*", cors()); app.options("*", cors());
app.use(detectPWA);
app.use("/api/public", publicAvailable); app.use("/api/public", publicAvailable);
app.use("/api/setup", allowSetup, setup); app.use("/api/setup", allowSetup, setup);
app.use("/api/reset", reset); app.use("/api/reset", reset);
app.use("/api/invite", invite); app.use("/api/invite", invite);
app.use("/api/auth", auth); app.use("/api/auth", auth);
app.use("/api/webapi", authenticateAPI, api);
app.use(authenticate); app.use(authenticate);
app.use("/api/admin", admin); app.use("/api/admin", admin);
app.use("/api/user", user); app.use("/api/user", user);
app.use("/api/server", PermissionHelper.isAdminMiddleware(), server);
app.use(errorHandler); app.use(errorHandler);
}; };

35
src/routes/server.ts Normal file
View file

@ -0,0 +1,35 @@
import express, { Request, Response } from "express";
import { FileSystemHelper } from "../helpers/fileSystemHelper";
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 serverJson = JSON.parse(serverPackage);
res.send({
name: serverJson.name,
description: serverJson.description,
version: serverJson.version,
author: serverJson.author,
license: serverJson.license,
});
});
router.get("/settings", async (req: Request, res: Response) => {
res.json({});
});
router.get("/serverrss", async (req: Request, res: Response) => {
const parser = new Parser();
let feed = await parser.parseURL("https://forgejo.jk-effects.cloud/Ehrenamt/ff-admin-server/releases.rss");
res.json(feed);
});
router.get("/clientrss", async (req: Request, res: Response) => {
const parser = new Parser();
let feed = await parser.parseURL("https://forgejo.jk-effects.cloud/Ehrenamt/ff-admin/releases.rss");
res.json(feed);
});
export default router;

10
src/routes/webapi.ts Normal file
View file

@ -0,0 +1,10 @@
import express, { Request, Response } from "express";
import { getWebApiAccess } from "../controller/webapiController";
var router = express.Router({ mergeParams: true });
router.get("/retrieve", async (req: Request, res: Response) => {
await getWebApiAccess(req, res);
});
export default router;

View file

@ -12,6 +12,7 @@ export default abstract class CalendarService {
.getRepository(calendar) .getRepository(calendar)
.createQueryBuilder("calendar") .createQueryBuilder("calendar")
.leftJoinAndSelect("calendar.type", "type") .leftJoinAndSelect("calendar.type", "type")
.orderBy("starttime", "ASC")
.getMany() .getMany()
.then((res) => { .then((res) => {
return res; return res;

View file

@ -16,6 +16,7 @@ export default abstract class CommunicationService {
.leftJoinAndSelect("communication.member", "member") .leftJoinAndSelect("communication.member", "member")
.leftJoinAndSelect("member.sendNewsletter", "sendNewsletter") .leftJoinAndSelect("member.sendNewsletter", "sendNewsletter")
.where("communication.memberId = :memberId", { memberId: memberId }) .where("communication.memberId = :memberId", { memberId: memberId })
.orderBy("communicationType.type", "ASC")
.getMany() .getMany()
.then((res) => { .then((res) => {
return res; return res;

View file

@ -14,6 +14,7 @@ export default abstract class MemberAwardService {
.createQueryBuilder("memberAwards") .createQueryBuilder("memberAwards")
.leftJoinAndSelect("memberAwards.award", "award") .leftJoinAndSelect("memberAwards.award", "award")
.where("memberAwards.memberId = :memberId", { memberId: memberId }) .where("memberAwards.memberId = :memberId", { memberId: memberId })
.orderBy("award.award", "ASC")
.getMany() .getMany()
.then((res) => { .then((res) => {
return res; return res;

View file

@ -14,6 +14,8 @@ export default abstract class MemberExecutivePositionService {
.createQueryBuilder("memberExecutivePositions") .createQueryBuilder("memberExecutivePositions")
.leftJoinAndSelect("memberExecutivePositions.executivePosition", "executivePosition") .leftJoinAndSelect("memberExecutivePositions.executivePosition", "executivePosition")
.where("memberExecutivePositions.memberId = :memberId", { memberId: memberId }) .where("memberExecutivePositions.memberId = :memberId", { memberId: memberId })
.orderBy("executivePosition.position", "ASC")
.addOrderBy("memberExecutivePositions.start", "DESC")
.getMany() .getMany()
.then((res) => { .then((res) => {
return res; return res;

View file

@ -14,6 +14,7 @@ export default abstract class MemberQualificationService {
.createQueryBuilder("memberQualifications") .createQueryBuilder("memberQualifications")
.leftJoinAndSelect("memberQualifications.qualification", "qualification") .leftJoinAndSelect("memberQualifications.qualification", "qualification")
.where("memberQualifications.memberId = :memberId", { memberId: memberId }) .where("memberQualifications.memberId = :memberId", { memberId: memberId })
.orderBy("qualification.qualification", "ASC")
.getMany() .getMany()
.then((res) => { .then((res) => {
return res; return res;

View file

@ -2,13 +2,26 @@ import { dataSource } from "../../../data-source";
import { member } from "../../../entity/club/member/member"; import { member } from "../../../entity/club/member/member";
import { membership } from "../../../entity/club/member/membership"; import { membership } from "../../../entity/club/member/membership";
import InternalException from "../../../exceptions/internalException"; import InternalException from "../../../exceptions/internalException";
import { memberView } from "../../../views/memberView";
export default abstract class MemberService { export default abstract class MemberService {
/** /**
* @description get all members * @description get all members
* @returns {Promise<[Array<member>, number]>} * @returns {Promise<[Array<member>, number]>}
*/ */
static async getAll(offset: number = 0, count: number = 25, search: string = ""): Promise<[Array<member>, number]> { static async getAll({
offset = 0,
count = 25,
search = "",
noLimit = false,
ids = [],
}: {
offset?: number;
count?: number;
search?: string;
noLimit?: boolean;
ids?: Array<number>;
}): Promise<[Array<member>, number]> {
let query = dataSource let query = dataSource
.getRepository(member) .getRepository(member)
.createQueryBuilder("member") .createQueryBuilder("member")
@ -54,9 +67,15 @@ export default abstract class MemberService {
}); });
} }
if (ids.length != 0) {
query = query.where({ id: ids });
}
if (!noLimit) {
query = query.offset(offset).limit(count);
}
return await query return await query
.offset(offset)
.limit(count)
.orderBy("member.lastname") .orderBy("member.lastname")
.addOrderBy("member.firstname") .addOrderBy("member.firstname")
.addOrderBy("member.nameaffix") .addOrderBy("member.nameaffix")
@ -114,6 +133,25 @@ export default abstract class MemberService {
}); });
} }
/**
* @description get member statistics by id
* @param {number} id
* @returns {Promise<memberView>}
*/
static async getStatisticsById(id: number): Promise<memberView> {
return await dataSource
.getRepository(memberView)
.createQueryBuilder("memberView")
.where("memberView.id = :id", { id: id })
.getOneOrFail()
.then((res) => {
return res;
})
.catch((err) => {
throw new InternalException("memberView not found by id", err);
});
}
/** /**
* @description get members where membership is setz * @description get members where membership is setz
* @returns {Promise<member>} * @returns {Promise<member>}
@ -124,6 +162,9 @@ export default abstract class MemberService {
.createQueryBuilder("member") .createQueryBuilder("member")
.leftJoinAndSelect("member.memberships", "membership") .leftJoinAndSelect("member.memberships", "membership")
.where("membership.end IS NULL") .where("membership.end IS NULL")
.orderBy("member.lastname")
.addOrderBy("member.firstname")
.addOrderBy("member.nameaffix")
.getMany() .getMany()
.then((res) => { .then((res) => {
return res; return res;

View file

@ -1,6 +1,7 @@
import { dataSource } from "../../../data-source"; import { dataSource } from "../../../data-source";
import { membership } from "../../../entity/club/member/membership"; import { membership } from "../../../entity/club/member/membership";
import InternalException from "../../../exceptions/internalException"; import InternalException from "../../../exceptions/internalException";
import { membershipView } from "../../../views/membershipsView";
export default abstract class MembershipService { export default abstract class MembershipService {
/** /**
@ -45,4 +46,23 @@ export default abstract class MembershipService {
throw new InternalException("member membership not found by id", err); throw new InternalException("member membership not found by id", err);
}); });
} }
/**
* @description get membership statistics by memberId
* @param {number} memberId
* @returns {Promise<Array<membershipView>>}
*/
static async getStatisticsById(memberId: number): Promise<Array<membershipView>> {
return await dataSource
.getRepository(membershipView)
.createQueryBuilder("membershipView")
.where("membershipView.memberId = :memberId", { memberId: memberId })
.getMany()
.then((res) => {
return res;
})
.catch((err) => {
throw new InternalException("membershipView not found by id", err);
});
}
} }

View file

@ -12,6 +12,7 @@ export default abstract class AwardService {
return await dataSource return await dataSource
.getRepository(award) .getRepository(award)
.createQueryBuilder("award") .createQueryBuilder("award")
.orderBy("award", "ASC")
.getMany() .getMany()
.then((res) => { .then((res) => {
return res; return res;

View file

@ -11,6 +11,7 @@ export default abstract class CalendarTypeService {
return await dataSource return await dataSource
.getRepository(calendarType) .getRepository(calendarType)
.createQueryBuilder("calendarType") .createQueryBuilder("calendarType")
.orderBy("type", "ASC")
.getMany() .getMany()
.then((res) => { .then((res) => {
return res; return res;

View file

@ -11,6 +11,7 @@ export default abstract class CommunicationTypeService {
return await dataSource return await dataSource
.getRepository(communicationType) .getRepository(communicationType)
.createQueryBuilder("communicationType") .createQueryBuilder("communicationType")
.orderBy("type", "ASC")
.getMany() .getMany()
.then((res) => { .then((res) => {
return res; return res;

View file

@ -12,6 +12,7 @@ export default abstract class ExecutivePositionService {
return await dataSource return await dataSource
.getRepository(executivePosition) .getRepository(executivePosition)
.createQueryBuilder("executivePosition") .createQueryBuilder("executivePosition")
.orderBy("position", "ASC")
.getMany() .getMany()
.then((res) => { .then((res) => {
return res; return res;

View file

@ -12,6 +12,7 @@ export default abstract class MembershipStatusService {
return await dataSource return await dataSource
.getRepository(membershipStatus) .getRepository(membershipStatus)
.createQueryBuilder("membershipStatus") .createQueryBuilder("membershipStatus")
.orderBy("status", "ASC")
.getMany() .getMany()
.then((res) => { .then((res) => {
return res; return res;

View file

@ -13,6 +13,7 @@ export default abstract class NewsletterConfigService {
.getRepository(newsletterConfig) .getRepository(newsletterConfig)
.createQueryBuilder("newsletterConfig") .createQueryBuilder("newsletterConfig")
.leftJoinAndSelect("newsletterConfig.comType", "comType") .leftJoinAndSelect("newsletterConfig.comType", "comType")
.orderBy("comType.type", "ASC")
.getMany() .getMany()
.then((res) => { .then((res) => {
return res; return res;

View file

@ -13,6 +13,7 @@ export default abstract class QualificationService {
return await dataSource return await dataSource
.getRepository(qualification) .getRepository(qualification)
.createQueryBuilder("qualification") .createQueryBuilder("qualification")
.orderBy("qualification", "ASC")
.getMany() .getMany()
.then((res) => { .then((res) => {
return res; return res;

View file

@ -11,6 +11,7 @@ export default abstract class QueryStoreService {
return await dataSource return await dataSource
.getRepository(query) .getRepository(query)
.createQueryBuilder("queryStore") .createQueryBuilder("queryStore")
.orderBy("title", "ASC")
.getMany() .getMany()
.then((res) => { .then((res) => {
return res; return res;

View file

@ -12,6 +12,7 @@ export default abstract class TemplateService {
return await dataSource return await dataSource
.getRepository(template) .getRepository(template)
.createQueryBuilder("template") .createQueryBuilder("template")
.orderBy("template", "ASC")
.getMany() .getMany()
.then((res) => { .then((res) => {
return res; return res;

View file

@ -14,6 +14,7 @@ export default abstract class TemplateUsageService {
.leftJoinAndSelect("templateUsage.header", "headerTemplate") .leftJoinAndSelect("templateUsage.header", "headerTemplate")
.leftJoinAndSelect("templateUsage.body", "bodyTemplate") .leftJoinAndSelect("templateUsage.body", "bodyTemplate")
.leftJoinAndSelect("templateUsage.footer", "footerTemplate") .leftJoinAndSelect("templateUsage.footer", "footerTemplate")
.orderBy("scope", "ASC")
.getMany() .getMany()
.then((res) => { .then((res) => {
return res; return res;

View file

@ -12,6 +12,7 @@ export default abstract class RoleService {
.getRepository(role) .getRepository(role)
.createQueryBuilder("role") .createQueryBuilder("role")
.leftJoinAndSelect("role.permissions", "role_permissions") .leftJoinAndSelect("role.permissions", "role_permissions")
.orderBy("role", "ASC")
.getMany() .getMany()
.then((res) => { .then((res) => {
return res; return res;

View file

@ -15,6 +15,8 @@ export default abstract class UserService {
.leftJoinAndSelect("user.roles", "roles") .leftJoinAndSelect("user.roles", "roles")
.leftJoinAndSelect("user.permissions", "permissions") .leftJoinAndSelect("user.permissions", "permissions")
.leftJoinAndSelect("roles.permissions", "role_permissions") .leftJoinAndSelect("roles.permissions", "role_permissions")
.orderBy("firstname", "ASC")
.addOrderBy("lastname", "ASC")
.getMany() .getMany()
.then((res) => { .then((res) => {
return res; return res;

View file

@ -0,0 +1,24 @@
import { dataSource } from "../../data-source";
import { webapiPermission } from "../../entity/user/webapi_permission";
import InternalException from "../../exceptions/internalException";
export default abstract class WebapiPermissionService {
/**
* @description get permission by api
* @param webapiId number
* @returns {Promise<Array<webapiPermission>>}
*/
static async getByApi(webapiId: number): Promise<Array<webapiPermission>> {
return await dataSource
.getRepository(webapiPermission)
.createQueryBuilder("webapi_permission")
.where("webapi_permission.webapiId = :webapiId", { webapiId: webapiId })
.getMany()
.then((res) => {
return res;
})
.catch((err) => {
throw new InternalException("webapi permissions not found by api", err);
});
}
}

View file

@ -0,0 +1,83 @@
import { dataSource } from "../../data-source";
import { webapi } from "../../entity/user/webapi";
import InternalException from "../../exceptions/internalException";
export default abstract class WebapiService {
/**
* @description get apis
* @returns {Promise<Array<webapi>>}
*/
static async getAll(): Promise<Array<webapi>> {
return await dataSource
.getRepository(webapi)
.createQueryBuilder("webapi")
.leftJoinAndSelect("webapi.permissions", "permissions")
.getMany()
.then((res) => {
return res;
})
.catch((err) => {
throw new InternalException("webapis not found", err);
});
}
/**
* @description get api by id
* @param id number
* @returns {Promise<webapi>}
*/
static async getById(id: number): Promise<webapi> {
return await dataSource
.getRepository(webapi)
.createQueryBuilder("webapi")
.leftJoinAndSelect("webapi.permissions", "permissions")
.where("webapi.id = :id", { id: id })
.getOneOrFail()
.then((res) => {
return res;
})
.catch((err) => {
throw new InternalException("webapi not found by id", err);
});
}
/**
* @description get api by token
* @param token string
* @returns {Promise<webapi>}
*/
static async getByToken(token: string): Promise<webapi> {
return await dataSource
.getRepository(webapi)
.createQueryBuilder("webapi")
.leftJoinAndSelect("webapi.permissions", "permissions")
.where("webapi.token = :token", { token: token })
.getOneOrFail()
.then((res) => {
return res;
})
.catch((err) => {
throw new InternalException("webapi not found by token", err);
});
}
/**
* @description get api by id
* @param id number
* @returns {Promise<webapi>}
*/
static async getTokenById(id: number): Promise<webapi> {
return await dataSource
.getRepository(webapi)
.createQueryBuilder("webapi")
.select("webapi.token")
.where("webapi.id = :id", { id: id })
.getOneOrFail()
.then((res) => {
return res;
})
.catch((err) => {
throw new InternalException("webapi token not found by id", err);
});
}
}

View file

@ -11,7 +11,9 @@
<table style="width: 100%"> <table style="width: 100%">
{{#each member}} {{#each member}}
<tr> <tr>
<th style="width: 35%;padding: 10px 5px">{{this.firstname}} {{this.lastname}} {{this.nameaffix}}</th> <th style="width: 35%; padding: 10px 5px">
{{this.lastname}} {{this.firstname}}{{#if this.nameaffix}} - {{this.nameaffix}}{{/if}}
</th>
<th style="width: 65%; padding: 10px 0"></th> <th style="width: 65%; padding: 10px 0"></th>
</tr> </tr>
{{/each}} {{/each}}
@ -37,7 +39,9 @@
margin-bottom: 5px; margin-bottom: 5px;
} }
table, th, td { table,
th,
td {
border: 1px solid black; border: 1px solid black;
border-collapse: collapse; border-collapse: collapse;
text-align: start; text-align: start;

View file

@ -14,9 +14,19 @@
<br /> <br />
<br /> <br />
<h2>Anwesenheit ({{presence.length}})</h2> <h2>Anwesenheit ({{presence.length}})</h2>
<p>{{#each presence}} {{this.firstname}} {{this.lastname}}{{#unless @last}}, {{/unless}} {{/each}}</p> <p>
{{#each presence}}{{this.firstname}} {{this.lastname}}{{#unless @last}}, {{/unless}}{{/each}}{{#unless
presence.length}}---{{/unless}}
</p>
<h2>Abwesenheit ({{absent.length}})</h2> <h2>Abwesenheit ({{absent.length}})</h2>
<p>{{#each absent}} {{this.firstname}} {{this.lastname}}{{#unless @last}}, {{/unless}} {{/each}}</p> <p>
entschuldigt: {{#each excused_absent}}{{this.firstname}} {{this.lastname}}{{#unless @last}},
{{/unless}}{{/each}}{{#unless excused_absent.length}}---{{/unless}}
</p>
<p>
unentschuldigt: {{#each unexcused_absent}}{{this.firstname}} {{this.lastname}}{{#unless @last}},
{{/unless}}{{/each}}{{#unless unexcused_absent.length}}---{{/unless}}
</p>
<br /> <br />
<h2>Agenda</h2> <h2>Agenda</h2>
{{#each agenda}} {{#each agenda}}

View file

@ -14,6 +14,7 @@ export type PermissionModule =
| "calendar_type" | "calendar_type"
| "user" | "user"
| "role" | "role"
| "webapi"
| "query" | "query"
| "query_store" | "query_store"
| "template" | "template"
@ -55,6 +56,7 @@ export const permissionModules: Array<PermissionModule> = [
"calendar_type", "calendar_type",
"user", "user",
"role", "role",
"webapi",
"query", "query",
"query_store", "query_store",
"template", "template",
@ -75,5 +77,5 @@ export const sectionsAndModules: SectionsAndModulesObject = {
"template_usage", "template_usage",
"newsletter_config", "newsletter_config",
], ],
user: ["user", "role"], user: ["user", "role", "webapi"],
}; };

View file

@ -16,3 +16,15 @@ export interface MemberViewModel {
smsAlarming?: Array<CommunicationViewModel>; smsAlarming?: Array<CommunicationViewModel>;
preferredCommunication?: Array<CommunicationViewModel>; preferredCommunication?: Array<CommunicationViewModel>;
} }
export interface MemberStatisticsViewModel {
id: number;
salutation: Salutation;
firstname: string;
lastname: string;
nameaffix: string;
birthdate: Date;
todayAge: number;
ageThisYear: number;
exactAge: string;
}

View file

@ -1,3 +1,5 @@
import { Salutation } from "../../../../enums/salutation";
export interface MembershipViewModel { export interface MembershipViewModel {
id: number; id: number;
start: Date; start: Date;
@ -6,3 +8,16 @@ export interface MembershipViewModel {
status: string; status: string;
statusId: number; statusId: number;
} }
export interface MembershipStatisticsViewModel {
durationInDays: number;
durationInYears: string;
status: string;
statusId: number;
memberId: number;
memberSalutation: Salutation;
memberFirstname: string;
memberLastname: string;
memberNameaffix: string;
memberBirthdate: Date;
}

View file

@ -3,5 +3,6 @@ import { MemberViewModel } from "../member/member.models";
export interface ProtocolPresenceViewModel { export interface ProtocolPresenceViewModel {
memberId: number; memberId: number;
absent: boolean; absent: boolean;
excused: boolean;
protocolId: number; protocolId: number;
} }

View file

@ -0,0 +1,10 @@
import { PermissionObject } from "../../../type/permissionTypes";
export interface ApiViewModel {
id: number;
permissions: PermissionObject;
title: string;
createdAt: Date;
lastUsage?: Date;
expiry?: Date;
}

View file

@ -43,5 +43,5 @@ export class memberView {
ageThisYear: number; ageThisYear: number;
@ViewColumn() @ViewColumn()
exactAge: Date; exactAge: string;
} }

View file

@ -22,14 +22,15 @@ import { Salutation } from "../enums/salutation";
) )
.leftJoin("membership.status", "status") .leftJoin("membership.status", "status")
.leftJoin("membership.member", "member") .leftJoin("membership.member", "member")
.groupBy("status.id"), .groupBy("status.id")
.addGroupBy("member.id"),
}) })
export class membershipView { export class membershipView {
@ViewColumn() @ViewColumn()
durationInDays: number; durationInDays: number;
@ViewColumn() @ViewColumn()
durationInYears: Date; durationInYears: string;
@ViewColumn() @ViewColumn()
status: string; status: string;