Compare commits

..

33 commits
v1.1.0 ... main

Author SHA1 Message Date
1d9970fc56 1.2.2 2025-01-31 11:42:55 +01:00
29be563942 Merge pull request 'patches: v1.2.2' (#56) from develop into main
Reviewed-on: #56
2025-01-31 10:42:32 +00:00
fc694aa976 change: newsletter saving 2025-01-31 11:40:59 +01:00
7c08251ff6 fix: views drop view is exists 2025-01-27 16:26:00 +01:00
7a3a8a9d87 1.2.1 2025-01-27 15:20:22 +01:00
3165eb789f Merge pull request 'patches v1.2.1' (#54) from develop into main
Reviewed-on: #54
2025-01-27 14:19:55 +00:00
ebe587756a fix: member get by ids 2025-01-27 15:16:12 +01:00
b678067874 Merge pull request 'feature/#37-salutation' (#52) from feature/#37-salutation into develop
Reviewed-on: #52
2025-01-25 11:21:58 +00:00
eb78934959 service and query builder 2025-01-25 12:16:20 +01:00
1ab4d93d2b salutation CRUD and controller 2025-01-25 11:58:10 +01:00
b55d0554e4 migration update 2025-01-25 11:39:34 +01:00
21cf811a56 change salutation to separate table 2025-01-25 10:20:57 +01:00
ff651b7d8f fix: prevent deletion of owner 2025-01-24 09:20:08 +01:00
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
70 changed files with 2264 additions and 444 deletions

View file

@ -18,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

@ -20,13 +20,13 @@ Um den Container hochzufahren, erstellen Sie eine `docker-compose.yml` Datei mit
version: "3" version: "3"
services: services:
ff-ff-admin-server: ff-admin-server:
image: docker.registry.jk-effects.cloud/ehrenamt/ff-admin/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|sqlite> # default ist auf mysql gesetzt - DB_TYPE=<mysql|sqlite> # default ist auf mysql gesetzt
- DB_HOST=ffm-db - DB_HOST=ff-db
- DB_PORT=<number> # default ist auf 3306 gesetzt - DB_PORT=<number> # default ist auf 3306 gesetzt
- DB_NAME=ffadmin - DB_NAME=ffadmin
- DB_USERNAME=administration_backend - DB_USERNAME=administration_backend
@ -40,7 +40,7 @@ services:
- MAIL_HOST=<url> - MAIL_HOST=<url>
- MAIL_PORT=<port> # default ist auf 578 gesetzt - MAIL_PORT=<port> # default ist auf 578 gesetzt
- MAIL_SECURE=<boolean> # default ist auf false gesetzt - 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

52
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "ff-admin-server", "name": "ff-admin-server",
"version": "1.1.0", "version": "1.2.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ff-admin-server", "name": "ff-admin-server",
"version": "1.1.0", "version": "1.2.2",
"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": "ff-admin-server", "name": "ff-admin-server",
"version": "1.1.0", "version": "1.2.2",
"description": "Feuerwehr/Verein Mitgliederverwaltung Server", "description": "Feuerwehr/Verein Mitgliederverwaltung Server",
"main": "dist/index.js", "main": "dist/index.js",
"scripts": { "scripts": {
@ -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

@ -1,6 +1,7 @@
export interface CreateCommunicationCommand { export interface CreateCommunicationCommand {
preferred: boolean; preferred: boolean;
isSMSAlarming: boolean; isSMSAlarming: boolean;
isSendNewsletter: boolean;
mobile: string; mobile: string;
email: string; email: string;
postalCode: string; postalCode: string;
@ -16,6 +17,7 @@ export interface UpdateCommunicationCommand {
id: number; id: number;
preferred: boolean; preferred: boolean;
isSMSAlarming: boolean; isSMSAlarming: boolean;
isSendNewsletter: boolean;
mobile: string; mobile: string;
email: string; email: string;
postalCode: string; postalCode: string;

View file

@ -14,26 +14,44 @@ export default abstract class CommunicationCommandHandler {
* @returns {Promise<number>} * @returns {Promise<number>}
*/ */
static async create(createCommunication: CreateCommunicationCommand): Promise<number> { static async create(createCommunication: CreateCommunicationCommand): Promise<number> {
let insertId = -1;
return await dataSource return await dataSource
.createQueryBuilder() .transaction(async (manager) => {
.insert() await manager
.into(communication) .createQueryBuilder()
.values({ .insert()
preferred: createCommunication.preferred, .into(communication)
isSMSAlarming: createCommunication.isSMSAlarming, .values({
mobile: createCommunication.mobile, preferred: createCommunication.preferred,
email: createCommunication.email, isSMSAlarming: createCommunication.isSMSAlarming,
postalCode: createCommunication.postalCode, isSendNewsletter: createCommunication.isSendNewsletter,
city: createCommunication.city, mobile: createCommunication.mobile,
street: createCommunication.street, email: createCommunication.email,
streetNumber: createCommunication.streetNumber, postalCode: createCommunication.postalCode,
streetNumberAddition: createCommunication.streetNumberAddition, city: createCommunication.city,
memberId: createCommunication.memberId, street: createCommunication.street,
typeId: createCommunication.typeId, streetNumber: createCommunication.streetNumber,
streetNumberAddition: createCommunication.streetNumberAddition,
memberId: createCommunication.memberId,
typeId: createCommunication.typeId,
})
.execute()
.then((result) => {
insertId = result.identifiers[0].id;
});
await manager
.createQueryBuilder()
.update(communication)
.set({
isSendNewsletter: false,
})
.where("memberId = :memberId", { memberId: createCommunication.memberId })
.andWhere("id <> :id", { id: insertId })
.execute();
}) })
.execute() .then(() => {
.then((result) => { return insertId;
return result.identifiers[0].id;
}) })
.catch((err) => { .catch((err) => {
throw new InternalException("Failed creating communication", err); throw new InternalException("Failed creating communication", err);
@ -47,22 +65,36 @@ export default abstract class CommunicationCommandHandler {
*/ */
static async update(updateCommunication: UpdateCommunicationCommand): Promise<void> { static async update(updateCommunication: UpdateCommunicationCommand): Promise<void> {
return await dataSource return await dataSource
.createQueryBuilder() .transaction(async (manager) => {
.update(communication) await manager
.set({ .createQueryBuilder()
preferred: updateCommunication.preferred, .update(communication)
isSMSAlarming: updateCommunication.isSMSAlarming, .set({
mobile: updateCommunication.mobile, preferred: updateCommunication.preferred,
email: updateCommunication.email, isSMSAlarming: updateCommunication.isSMSAlarming,
postalCode: updateCommunication.postalCode, isSendNewsletter: updateCommunication.isSendNewsletter,
city: updateCommunication.city, mobile: updateCommunication.mobile,
street: updateCommunication.street, email: updateCommunication.email,
streetNumber: updateCommunication.streetNumber, postalCode: updateCommunication.postalCode,
streetNumberAddition: updateCommunication.streetNumberAddition, city: updateCommunication.city,
street: updateCommunication.street,
streetNumber: updateCommunication.streetNumber,
streetNumberAddition: updateCommunication.streetNumberAddition,
})
.where("id = :id", { id: updateCommunication.id })
.andWhere("memberId = :memberId", { memberId: updateCommunication.memberId })
.execute();
await manager
.createQueryBuilder()
.update(communication)
.set({
isSendNewsletter: false,
})
.where("memberId = :memberId", { memberId: updateCommunication.memberId })
.andWhere("id <> :id", { id: updateCommunication.id })
.execute();
}) })
.where("id = :id", { id: updateCommunication.id })
.andWhere("memberId = :memberId", { memberId: updateCommunication.memberId })
.execute()
.then(() => {}) .then(() => {})
.catch((err) => { .catch((err) => {
throw new InternalException("Failed updating communication", err); throw new InternalException("Failed updating communication", err);

View file

@ -1,7 +1,5 @@
import { Salutation } from "../../../enums/salutation";
export interface CreateMemberCommand { export interface CreateMemberCommand {
salutation: Salutation; salutationId: number;
firstname: string; firstname: string;
lastname: string; lastname: string;
nameaffix: string; nameaffix: string;
@ -11,7 +9,7 @@ export interface CreateMemberCommand {
export interface UpdateMemberCommand { export interface UpdateMemberCommand {
id: number; id: number;
salutation: Salutation; salutationId: number;
firstname: string; firstname: string;
lastname: string; lastname: string;
nameaffix: string; nameaffix: string;
@ -19,11 +17,6 @@ export interface UpdateMemberCommand {
internalId?: string; internalId?: string;
} }
export interface UpdateMemberNewsletterCommand {
id: number;
communicationId: number;
}
export interface DeleteMemberCommand { export interface DeleteMemberCommand {
id: number; id: number;
} }

View file

@ -2,12 +2,7 @@ import { dataSource } from "../../../data-source";
import { communication } from "../../../entity/club/member/communication"; import { communication } from "../../../entity/club/member/communication";
import { member } from "../../../entity/club/member/member"; import { member } from "../../../entity/club/member/member";
import InternalException from "../../../exceptions/internalException"; import InternalException from "../../../exceptions/internalException";
import { import { CreateMemberCommand, DeleteMemberCommand, UpdateMemberCommand } from "./memberCommand";
CreateMemberCommand,
DeleteMemberCommand,
UpdateMemberCommand,
UpdateMemberNewsletterCommand,
} from "./memberCommand";
export default abstract class MemberCommandHandler { export default abstract class MemberCommandHandler {
/** /**
@ -21,7 +16,7 @@ export default abstract class MemberCommandHandler {
.insert() .insert()
.into(member) .into(member)
.values({ .values({
salutation: createMember.salutation, salutationId: createMember.salutationId,
firstname: createMember.firstname, firstname: createMember.firstname,
lastname: createMember.lastname, lastname: createMember.lastname,
nameaffix: createMember.nameaffix, nameaffix: createMember.nameaffix,
@ -33,7 +28,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
);
}); });
} }
@ -47,7 +45,7 @@ export default abstract class MemberCommandHandler {
.createQueryBuilder() .createQueryBuilder()
.update(member) .update(member)
.set({ .set({
salutation: updateMember.salutation, salutationId: updateMember.salutationId,
firstname: updateMember.firstname, firstname: updateMember.firstname,
lastname: updateMember.lastname, lastname: updateMember.lastname,
nameaffix: updateMember.nameaffix, nameaffix: updateMember.nameaffix,
@ -58,52 +56,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
);
/**
* @description update member newsletter
* @param {UpdateMemberCommand} updateMember
* @returns {Promise<void>}
*/
static async updateNewsletter(updateMember: UpdateMemberNewsletterCommand): Promise<void> {
return await dataSource
.createQueryBuilder()
.update(member)
.set({
sendNewsletter: await dataSource
.getRepository(communication)
.createQueryBuilder("communication")
.where("id = :id", { id: updateMember.communicationId })
.andWhere("memberId = :memberId", { memberId: updateMember.id })
.getOneOrFail(),
})
.where("id = :id", { id: updateMember.id })
.execute()
.then(() => {})
.catch((err) => {
throw new InternalException("Failed updating member", err);
});
}
/**
* @description update member newsletter to unset
* @param {number} memberId
* @returns {Promise<void>}
*/
static async unsetNewsletter(memberId: number): Promise<void> {
return await dataSource
.createQueryBuilder()
.update(member)
.set({
sendNewsletter: null,
})
.where("id = :id", { id: memberId })
.execute()
.then(() => {})
.catch((err) => {
throw new InternalException("Failed updating member", err);
}); });
} }

View file

@ -0,0 +1,12 @@
export interface CreateSalutationCommand {
salutation: string;
}
export interface UpdateSalutationCommand {
id: number;
salutation: string;
}
export interface DeleteSalutationCommand {
id: number;
}

View file

@ -0,0 +1,69 @@
import { dataSource } from "../../../data-source";
import { salutation } from "../../../entity/settings/salutation";
import InternalException from "../../../exceptions/internalException";
import { CreateSalutationCommand, DeleteSalutationCommand, UpdateSalutationCommand } from "./salutationCommand";
export default abstract class SalutationCommandHandler {
/**
* @description create salutation
* @param {CreateSalutationCommand} createSalutation
* @returns {Promise<number>}
*/
static async create(createSalutation: CreateSalutationCommand): Promise<number> {
return await dataSource
.createQueryBuilder()
.insert()
.into(salutation)
.values({
salutation: createSalutation.salutation,
})
.execute()
.then((result) => {
return result.identifiers[0].id;
})
.catch((err) => {
throw new InternalException("Failed creating salutation", err);
});
}
/**
* @description update salutation
* @param {UpdateSalutationCommand} updateSalutation
* @returns {Promise<void>}
*/
static async update(updateSalutation: UpdateSalutationCommand): Promise<void> {
return await dataSource
.createQueryBuilder()
.update(salutation)
.set({
salutation: updateSalutation.salutation,
})
.where("id = :id", { id: updateSalutation.id })
.execute()
.then(() => {})
.catch((err) => {
throw new InternalException("Failed updating salutation", err);
});
}
/**
* @description delete salutation
* @param {DeleteSalutationCommand} deleteSalutation
* @returns {Promise<void>}
*/
static async delete(deleteSalutation: DeleteSalutationCommand): Promise<void> {
return await dataSource
.createQueryBuilder()
.delete()
.from(salutation)
.where("id = :id", { id: deleteSalutation.id })
.execute()
.then(() => {})
.catch((err) => {
throw new InternalException(
`Failed deleting salutation ${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

@ -15,7 +15,6 @@ import {
CreateMemberCommand, CreateMemberCommand,
DeleteMemberCommand, DeleteMemberCommand,
UpdateMemberCommand, UpdateMemberCommand,
UpdateMemberNewsletterCommand,
} from "../../../command/club/member/memberCommand"; } from "../../../command/club/member/memberCommand";
import MemberCommandHandler from "../../../command/club/member/memberCommandHandler"; import MemberCommandHandler from "../../../command/club/member/memberCommandHandler";
import { import {
@ -61,8 +60,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 +89,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 +115,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
@ -254,7 +284,7 @@ export async function createMemberPrintoutList(req: Request, res: Response): Pro
* @returns {Promise<*>} * @returns {Promise<*>}
*/ */
export async function createMember(req: Request, res: Response): Promise<any> { export async function createMember(req: Request, res: Response): Promise<any> {
const salutation = req.body.salutation; const salutationId = parseInt(req.body.salutationId);
const firstname = req.body.firstname; const firstname = req.body.firstname;
const lastname = req.body.lastname; const lastname = req.body.lastname;
const nameaffix = req.body.nameaffix; const nameaffix = req.body.nameaffix;
@ -262,7 +292,7 @@ export async function createMember(req: Request, res: Response): Promise<any> {
const internalId = req.body.internalId; const internalId = req.body.internalId;
let createMember: CreateMemberCommand = { let createMember: CreateMemberCommand = {
salutation, salutationId,
firstname, firstname,
lastname, lastname,
nameaffix, nameaffix,
@ -378,6 +408,7 @@ export async function addCommunicationToMember(req: Request, res: Response): Pro
const memberId = parseInt(req.params.memberId); const memberId = parseInt(req.params.memberId);
const preferred = req.body.preferred; const preferred = req.body.preferred;
const isSMSAlarming = req.body.isSMSAlarming; const isSMSAlarming = req.body.isSMSAlarming;
const isSendNewsletter = req.body.isNewsletterMain;
const mobile = req.body.mobile; const mobile = req.body.mobile;
const email = req.body.email; const email = req.body.email;
const postalCode = req.body.postalCode; const postalCode = req.body.postalCode;
@ -386,11 +417,11 @@ export async function addCommunicationToMember(req: Request, res: Response): Pro
const streetNumber = req.body.streetNumber; const streetNumber = req.body.streetNumber;
const streetNumberAddition = req.body.streetNumberAddition; const streetNumberAddition = req.body.streetNumberAddition;
const typeId = req.body.typeId; const typeId = req.body.typeId;
const isNewsletterMain = req.body.isNewsletterMain;
let createCommunication: CreateCommunicationCommand = { let createCommunication: CreateCommunicationCommand = {
preferred, preferred,
isSMSAlarming, isSMSAlarming,
isSendNewsletter,
mobile, mobile,
email, email,
postalCode, postalCode,
@ -403,14 +434,6 @@ export async function addCommunicationToMember(req: Request, res: Response): Pro
}; };
let id = await CommunicationCommandHandler.create(createCommunication); let id = await CommunicationCommandHandler.create(createCommunication);
if (isNewsletterMain) {
let updateNewsletter: UpdateMemberNewsletterCommand = {
id: memberId,
communicationId: id,
};
await MemberCommandHandler.updateNewsletter(updateNewsletter);
}
res.sendStatus(204); res.sendStatus(204);
} }
@ -422,7 +445,7 @@ export async function addCommunicationToMember(req: Request, res: Response): Pro
*/ */
export async function updateMemberById(req: Request, res: Response): Promise<any> { export async function updateMemberById(req: Request, res: Response): Promise<any> {
const memberId = parseInt(req.params.id); const memberId = parseInt(req.params.id);
const salutation = req.body.salutation; const salutationId = parseInt(req.body.salutationId);
const firstname = req.body.firstname; const firstname = req.body.firstname;
const lastname = req.body.lastname; const lastname = req.body.lastname;
const nameaffix = req.body.nameaffix; const nameaffix = req.body.nameaffix;
@ -431,7 +454,7 @@ export async function updateMemberById(req: Request, res: Response): Promise<any
let updateMember: UpdateMemberCommand = { let updateMember: UpdateMemberCommand = {
id: memberId, id: memberId,
salutation, salutationId,
firstname, firstname,
lastname, lastname,
nameaffix, nameaffix,
@ -564,6 +587,7 @@ export async function updateCommunicationOfMember(req: Request, res: Response):
const recordId = parseInt(req.params.recordId); const recordId = parseInt(req.params.recordId);
const preferred = req.body.preferred; const preferred = req.body.preferred;
const isSMSAlarming = req.body.isSMSAlarming; const isSMSAlarming = req.body.isSMSAlarming;
const isSendNewsletter = req.body.isNewsletterMain;
const mobile = req.body.mobile; const mobile = req.body.mobile;
const email = req.body.email; const email = req.body.email;
const postalCode = req.body.postalCode; const postalCode = req.body.postalCode;
@ -571,12 +595,12 @@ export async function updateCommunicationOfMember(req: Request, res: Response):
const street = req.body.street; const street = req.body.street;
const streetNumber = req.body.streetNumber; const streetNumber = req.body.streetNumber;
const streetNumberAddition = req.body.streetNumberAddition; const streetNumberAddition = req.body.streetNumberAddition;
const isNewsletterMain = req.body.isNewsletterMain;
let updateCommunication: UpdateCommunicationCommand = { let updateCommunication: UpdateCommunicationCommand = {
id: recordId, id: recordId,
preferred, preferred,
isSMSAlarming, isSMSAlarming,
isSendNewsletter,
mobile, mobile,
email, email,
postalCode, postalCode,
@ -588,18 +612,6 @@ export async function updateCommunicationOfMember(req: Request, res: Response):
}; };
await CommunicationCommandHandler.update(updateCommunication); await CommunicationCommandHandler.update(updateCommunication);
let currentUserNewsletterMain = await MemberService.getNewsletterById(memberId);
if (isNewsletterMain) {
let updateNewsletter: UpdateMemberNewsletterCommand = {
id: memberId,
communicationId: recordId,
};
await MemberCommandHandler.updateNewsletter(updateNewsletter);
} else if (currentUserNewsletterMain.sendNewsletter?.id == recordId) {
await MemberCommandHandler.unsetNewsletter(memberId);
}
res.sendStatus(204); res.sendStatus(204);
} }

View file

@ -21,7 +21,6 @@ import UserService from "../../../service/user/userService";
import { TemplateHelper } from "../../../helpers/templateHelper"; import { TemplateHelper } from "../../../helpers/templateHelper";
import MailHelper from "../../../helpers/mailHelper"; import MailHelper from "../../../helpers/mailHelper";
import { NewsletterEventType, NewsletterHelper } from "../../../helpers/newsletterHelper"; import { NewsletterEventType, NewsletterHelper } from "../../../helpers/newsletterHelper";
import { Salutation } from "../../../enums/salutation";
/** /**
* @description get all newsletters * @description get all newsletters
@ -142,7 +141,7 @@ export async function createNewsletterPrintoutPreviewById(req: Request, res: Res
data.recipient = { data.recipient = {
firstname: recipient.firstname, firstname: recipient.firstname,
lastname: recipient.lastname, lastname: recipient.lastname,
salutation: Salutation.none, salutation: "Anrede",
nameaffix: "", nameaffix: "",
street: "Straße", street: "Straße",
streetNumber: "Hausnummer", streetNumber: "Hausnummer",
@ -248,7 +247,7 @@ export async function createNewsletterMailPreviewById(req: Request, res: Respons
data.recipient = { data.recipient = {
firstname: recipient.firstname, firstname: recipient.firstname,
lastname: recipient.lastname, lastname: recipient.lastname,
salutation: Salutation.none, salutation: "Anrede",
nameaffix: "", nameaffix: "",
street: "Straße", street: "Straße",
streetNumber: "Hausnummer", streetNumber: "Hausnummer",

View file

@ -0,0 +1,87 @@
import { Request, Response } from "express";
import SalutationService from "../../../service/settings/salutationService";
import SalutationFactory from "../../../factory/admin/settings/salutation";
import {
CreateSalutationCommand,
DeleteSalutationCommand,
UpdateSalutationCommand,
} from "../../../command/settings/salutation/salutationCommand";
import SalutationCommandHandler from "../../../command/settings/salutation/salutationCommandHandler";
/**
* @description get all salutations
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getAllSalutations(req: Request, res: Response): Promise<any> {
let salutations = await SalutationService.getAll();
res.json(SalutationFactory.mapToBase(salutations));
}
/**
* @description get salutation by id
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function getSalutationById(req: Request, res: Response): Promise<any> {
const id = parseInt(req.params.id);
let salutation = await SalutationService.getById(id);
res.json(SalutationFactory.mapToSingle(salutation));
}
/**
* @description create new salutation
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function createSalutation(req: Request, res: Response): Promise<any> {
const salutation = req.body.salutation;
let createSalutation: CreateSalutationCommand = {
salutation: salutation,
};
await SalutationCommandHandler.create(createSalutation);
res.sendStatus(204);
}
/**
* @description update salutation
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function updateSalutation(req: Request, res: Response): Promise<any> {
const id = parseInt(req.params.id);
const salutation = req.body.salutation;
let updateSalutation: UpdateSalutationCommand = {
id: id,
salutation: salutation,
};
await SalutationCommandHandler.update(updateSalutation);
res.sendStatus(204);
}
/**
* @description delete salutation
* @param req {Request} Express req object
* @param res {Response} Express res object
* @returns {Promise<*>}
*/
export async function deleteSalutation(req: Request, res: Response): Promise<any> {
const id = parseInt(req.params.id);
let deleteSalutation: DeleteSalutationCommand = {
id: id,
};
await SalutationCommandHandler.delete(deleteSalutation);
res.sendStatus(204);
}

View file

@ -10,6 +10,7 @@ import MailHelper from "../../../helpers/mailHelper";
import { CLUB_NAME } from "../../../env.defaults"; import { CLUB_NAME } from "../../../env.defaults";
import { UpdateUserPermissionsCommand } from "../../../command/user/user/userPermissionCommand"; import { UpdateUserPermissionsCommand } from "../../../command/user/user/userPermissionCommand";
import UserPermissionCommandHandler from "../../../command/user/user/userPermissionCommandHandler"; import UserPermissionCommandHandler from "../../../command/user/user/userPermissionCommandHandler";
import BadRequestException from "../../../exceptions/badRequestException";
/** /**
* @description get All users * @description get All users
@ -137,7 +138,11 @@ export async function updateUserRoles(req: Request, res: Response): Promise<any>
export async function deleteUser(req: Request, res: Response): Promise<any> { export async function deleteUser(req: Request, res: Response): Promise<any> {
const id = parseInt(req.params.id); const id = parseInt(req.params.id);
let user = await UserService.getById(id); let { mail, isOwner } = await UserService.getById(id);
if (isOwner) {
throw new BadRequestException("Owner cannot be deleted");
}
let deleteUser: DeleteUserCommand = { let deleteUser: DeleteUserCommand = {
id: id, id: id,
@ -147,7 +152,7 @@ export async function deleteUser(req: Request, res: Response): Promise<any> {
try { try {
// sendmail // sendmail
await MailHelper.sendMail( await MailHelper.sendMail(
user.mail, mail,
`Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`, `Email Bestätigung für Mitglieder Admin-Portal von ${CLUB_NAME}`,
`Ihr Nutzerkonto des Adminportals wurde erfolgreich gelöscht.` `Ihr Nutzerkonto des Adminportals wurde erfolgreich gelöscht.`
); );

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

@ -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

@ -68,6 +68,13 @@ 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 { FinishInternalIdTransfer1736505324488 } from "./migrations/1736505324488-finishInternalIdTransfer";
import { ProtocolPresenceExcuse1737287798828 } from "./migrations/1737287798828-protocolPresenceExcuse"; 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";
import { salutation } from "./entity/settings/salutation";
import { SalutationAsTable1737796878058 } from "./migrations/1737796878058-salutationAsTable";
import { UpdateViews1737800468938 } from "./migrations/1737800468938-updateViews";
import { MoveSendNewsletterFlag1737816852011 } from "./migrations/1737816852011-moveSendNewsletterFlag";
const dataSource = new DataSource({ const dataSource = new DataSource({
type: DB_TYPE as any, type: DB_TYPE as any,
@ -93,6 +100,7 @@ const dataSource = new DataSource({
executivePosition, executivePosition,
membershipStatus, membershipStatus,
qualification, qualification,
salutation,
member, member,
memberAwards, memberAwards,
memberExecutivePositions, memberExecutivePositions,
@ -117,6 +125,8 @@ const dataSource = new DataSource({
memberExecutivePositionsView, memberExecutivePositionsView,
memberQualificationsView, memberQualificationsView,
membershipView, membershipView,
webapi,
webapiPermission,
], ],
migrations: [ migrations: [
Initial1724317398939, Initial1724317398939,
@ -146,6 +156,10 @@ const dataSource = new DataSource({
ExtendViewValues1736084198860, ExtendViewValues1736084198860,
FinishInternalIdTransfer1736505324488, FinishInternalIdTransfer1736505324488,
ProtocolPresenceExcuse1737287798828, ProtocolPresenceExcuse1737287798828,
AddWebapiTokens1737453096674,
SalutationAsTable1737796878058,
UpdateViews1737800468938,
MoveSendNewsletterFlag1737816852011,
], ],
migrationsRun: true, migrationsRun: true,
migrationsTransactionMode: "each", migrationsTransactionMode: "each",

View file

@ -1,6 +1,5 @@
import { calendar } from "../entity/club/calendar"; import { calendar } from "../entity/club/calendar";
import { member } from "../entity/club/member/member"; import { member } from "../entity/club/member/member";
import { Salutation } from "../enums/salutation";
export const newsletterDemoData: { export const newsletterDemoData: {
title: string; title: string;
@ -18,7 +17,9 @@ export const newsletterDemoData: {
} }
> >
>; >;
recipient: Partial<member & { street: string; streetNumber: string; streetNumberAdd: string }>; recipient: Partial<
Omit<member, "salutation"> & { salutation: string; street: string; streetNumber: string; streetNumberAdd: string }
>;
} = { } = {
title: "Beispiel Newsletter Daten", title: "Beispiel Newsletter Daten",
description: "Zusammenfassung der Demodaten.", description: "Zusammenfassung der Demodaten.",
@ -63,7 +64,7 @@ export const newsletterDemoData: {
recipient: { recipient: {
firstname: "Julian", firstname: "Julian",
lastname: "Krauser", lastname: "Krauser",
salutation: Salutation.sir, salutation: "Herr",
nameaffix: "", nameaffix: "",
street: "Straße", street: "Straße",
streetNumber: "Hausnummer", streetNumber: "Hausnummer",

View file

@ -13,6 +13,9 @@ export class communication {
@Column({ type: "boolean", default: false }) @Column({ type: "boolean", default: false })
isSMSAlarming: boolean; isSMSAlarming: boolean;
@Column({ type: "boolean", default: false })
isSendNewsletter: boolean;
@Column({ type: "varchar", length: 255, nullable: true }) @Column({ type: "varchar", length: 255, nullable: true })
mobile: string; mobile: string;

View file

@ -1,32 +1,16 @@
import { Column, Entity, JoinColumn, ManyToOne, OneToMany, OneToOne, PrimaryColumn } from "typeorm"; import { Column, Entity, JoinColumn, ManyToOne, OneToMany, OneToOne, PrimaryColumn } from "typeorm";
import { Salutation } from "../../../enums/salutation";
import { membership } from "./membership"; import { membership } from "./membership";
import { memberAwards } from "./memberAwards"; import { memberAwards } from "./memberAwards";
import { memberQualifications } from "./memberQualifications"; import { memberQualifications } from "./memberQualifications";
import { memberExecutivePositions } from "./memberExecutivePositions"; import { memberExecutivePositions } from "./memberExecutivePositions";
import { communication } from "./communication"; import { communication } from "./communication";
import { CommunicationViewModel } from "../../../viewmodel/admin/club/member/communication.models"; import { salutation } from "../../settings/salutation";
@Entity() @Entity()
export class member { export class member {
@PrimaryColumn({ generated: "increment", type: "int" }) @PrimaryColumn({ generated: "increment", type: "int" })
id: number; id: number;
@Column({
type: "varchar",
length: "255",
default: Salutation.none.toString(),
transformer: {
to(value: Salutation) {
return value.toString();
},
from(value: string) {
return Salutation[value as keyof typeof Salutation];
},
},
})
salutation: Salutation;
@Column({ type: "varchar", length: 255 }) @Column({ type: "varchar", length: 255 })
firstname: string; firstname: string;
@ -42,16 +26,14 @@ export class member {
@Column({ type: "varchar", length: 255, unique: true, nullable: true }) @Column({ type: "varchar", length: 255, unique: true, nullable: true })
internalId?: string; internalId?: string;
@Column()
salutationId: number;
@OneToMany(() => communication, (communications) => communications.member) @OneToMany(() => communication, (communications) => communications.member)
communications: communication[]; communications: communication[];
@OneToOne(() => communication, { @ManyToOne(() => salutation, (salutation) => salutation.members)
nullable: true, salutation: salutation;
onDelete: "SET NULL",
onUpdate: "RESTRICT",
})
@JoinColumn()
sendNewsletter?: communication;
@OneToMany(() => membership, (membership) => membership.member) @OneToMany(() => membership, (membership) => membership.member)
memberships: membership[]; memberships: membership[];
@ -69,4 +51,5 @@ export class member {
lastMembershipEntry?: membership; lastMembershipEntry?: membership;
preferredCommunication?: Array<communication>; preferredCommunication?: Array<communication>;
smsAlarming?: Array<communication>; smsAlarming?: Array<communication>;
sendNewsletter?: communication;
} }

View file

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

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

@ -1,6 +0,0 @@
export enum Salutation {
sir = "sir",
madam = "madam",
other = "other",
none = "none",
}

View file

@ -21,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() {

View file

@ -20,7 +20,7 @@ export default abstract class CommunicationFactory {
streetNumber: record.streetNumber, streetNumber: record.streetNumber,
streetNumberAddition: record.streetNumberAddition, streetNumberAddition: record.streetNumberAddition,
type: CommunicationTypeFactory.mapToSingle(record.type), type: CommunicationTypeFactory.mapToSingle(record.type),
isNewsletterMain: isMain ? isMain : record?.member?.sendNewsletter?.id == record.id, isNewsletterMain: record?.isSendNewsletter,
isSMSAlarming: record.isSMSAlarming, isSMSAlarming: record.isSMSAlarming,
}; };
} }

View file

@ -1,5 +1,7 @@
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 SalutationFactory from "../../settings/salutation";
import CommunicationFactory from "./communication"; import CommunicationFactory from "./communication";
import MembershipFactory from "./membership"; import MembershipFactory from "./membership";
@ -12,7 +14,7 @@ export default abstract class MemberFactory {
public static mapToSingle(record: member): MemberViewModel { public static mapToSingle(record: member): MemberViewModel {
return { return {
id: record?.id, id: record?.id,
salutation: record?.salutation, salutation: SalutationFactory.mapToSingle(record?.salutation),
firstname: record?.firstname, firstname: record?.firstname,
lastname: record?.lastname, lastname: record?.lastname,
nameaffix: record?.nameaffix, nameaffix: record?.nameaffix,
@ -40,4 +42,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

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

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

@ -11,6 +11,7 @@ export default abstract class DynamicQueryBuilder {
"executivePosition", "executivePosition",
"membershipStatus", "membershipStatus",
"qualification", "qualification",
"salutation",
"member", "member",
"memberAwards", "memberAwards",
"memberExecutivePositions", "memberExecutivePositions",

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

@ -100,7 +100,7 @@ export abstract class NewsletterHelper {
recipient: { recipient: {
firstname: recipient.firstname, firstname: recipient.firstname,
lastname: recipient.lastname, lastname: recipient.lastname,
salutation: recipient.salutation, salutation: recipient.salutation.salutation,
nameaffix: recipient.nameaffix, nameaffix: recipient.nameaffix,
...(showAdress ...(showAdress
? { ? {
@ -141,7 +141,7 @@ export abstract class NewsletterHelper {
} }
} }
let members = await MemberService.getAll(0, 1000); let members = await MemberService.getAll({ noLimit: true });
return members[0].filter((m) => queryMemberIds.includes(m.id)); return members[0].filter((m) => queryMemberIds.includes(m.id));
} }

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

@ -13,6 +13,7 @@ declare global {
isOwner: boolean; isOwner: boolean;
permissions: PermissionObject; permissions: PermissionObject;
isPWA: boolean; isPWA: boolean;
isWebApiRequest: boolean;
} }
} }
} }

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,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

@ -8,105 +8,105 @@ export class MemberDataViews1734520998539 implements MigrationInterface {
name = "MemberDataViews1734520998539"; name = "MemberDataViews1734520998539";
public async up(queryRunner: QueryRunner): Promise<void> { public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createView( // await queryRunner.createView(
new View({ // new View({
name: "member_view", // name: "member_view",
expression: (datasource: DataSource) => // expression: (datasource: DataSource) =>
datasource // datasource
.getRepository(member) // .getRepository(member)
.createQueryBuilder("member") // .createQueryBuilder("member")
.select("member.id", "id") // .select("member.id", "id")
.addSelect("member.salutation", "salutation") // .addSelect("member.salutation", "salutation")
.addSelect("member.firstname", "firstname") // .addSelect("member.firstname", "firstname")
.addSelect("member.lastname", "lastname") // .addSelect("member.lastname", "lastname")
.addSelect("member.nameaffix", "nameaffix") // .addSelect("member.nameaffix", "nameaffix")
.addSelect("member.birthdate", "birthdate") // .addSelect("member.birthdate", "birthdate")
.addSelect("TIMESTAMPDIFF(YEAR, member.birthdate, CURDATE())", "todayAge") // .addSelect("TIMESTAMPDIFF(YEAR, member.birthdate, CURDATE())", "todayAge")
.addSelect("YEAR(CURDATE()) - YEAR(member.birthdate)", "ageThisYear"), // .addSelect("YEAR(CURDATE()) - YEAR(member.birthdate)", "ageThisYear"),
}), // }),
true // true
); // );
await queryRunner.createView( // await queryRunner.createView(
new View({ // new View({
name: "member_executive_positions_view", // name: "member_executive_positions_view",
expression: (datasource: DataSource) => // expression: (datasource: DataSource) =>
datasource // datasource
.getRepository(memberExecutivePositions) // .getRepository(memberExecutivePositions)
.createQueryBuilder("memberExecutivePositions") // .createQueryBuilder("memberExecutivePositions")
.select("executivePosition.id", "positionId") // .select("executivePosition.id", "positionId")
.addSelect("executivePosition.position", "position") // .addSelect("executivePosition.position", "position")
.addSelect("member.id", "memberId") // .addSelect("member.id", "memberId")
.addSelect("member.salutation", "memberSalutation") // .addSelect("member.salutation", "memberSalutation")
.addSelect("member.firstname", "memberFirstname") // .addSelect("member.firstname", "memberFirstname")
.addSelect("member.lastname", "memberLastname") // .addSelect("member.lastname", "memberLastname")
.addSelect("member.nameaffix", "memberNameaffix") // .addSelect("member.nameaffix", "memberNameaffix")
.addSelect("member.birthdate", "memberBirthdate") // .addSelect("member.birthdate", "memberBirthdate")
.addSelect( // .addSelect(
"SUM(TIMESTAMPDIFF(DAY, memberExecutivePositions.start, COALESCE(memberExecutivePositions.end, CURRENT_DATE)))", // "SUM(TIMESTAMPDIFF(DAY, memberExecutivePositions.start, COALESCE(memberExecutivePositions.end, CURRENT_DATE)))",
"durationInDays" // "durationInDays"
) // )
.leftJoin("memberExecutivePositions.executivePosition", "executivePosition") // .leftJoin("memberExecutivePositions.executivePosition", "executivePosition")
.leftJoin("memberExecutivePositions.member", "member") // .leftJoin("memberExecutivePositions.member", "member")
.groupBy("executivePosition.id"), // .groupBy("executivePosition.id"),
}), // }),
true // true
); // );
await queryRunner.createView( // await queryRunner.createView(
new View({ // new View({
name: "member_qualifications_view", // name: "member_qualifications_view",
expression: (datasource: DataSource) => // expression: (datasource: DataSource) =>
datasource // datasource
.getRepository(memberQualifications) // .getRepository(memberQualifications)
.createQueryBuilder("memberQualifications") // .createQueryBuilder("memberQualifications")
.select("qualification.id", "qualificationId") // .select("qualification.id", "qualificationId")
.addSelect("qualification.qualification", "qualification") // .addSelect("qualification.qualification", "qualification")
.addSelect("member.id", "memberId") // .addSelect("member.id", "memberId")
.addSelect("member.salutation", "memberSalutation") // .addSelect("member.salutation", "memberSalutation")
.addSelect("member.firstname", "memberFirstname") // .addSelect("member.firstname", "memberFirstname")
.addSelect("member.lastname", "memberLastname") // .addSelect("member.lastname", "memberLastname")
.addSelect("member.nameaffix", "memberNameaffix") // .addSelect("member.nameaffix", "memberNameaffix")
.addSelect("member.birthdate", "memberBirthdate") // .addSelect("member.birthdate", "memberBirthdate")
.addSelect( // .addSelect(
"SUM(TIMESTAMPDIFF(DAY, memberQualifications.start, COALESCE(memberQualifications.end, CURRENT_DATE)))", // "SUM(TIMESTAMPDIFF(DAY, memberQualifications.start, COALESCE(memberQualifications.end, CURRENT_DATE)))",
"durationInDays" // "durationInDays"
) // )
.leftJoin("memberQualifications.qualification", "qualification") // .leftJoin("memberQualifications.qualification", "qualification")
.leftJoin("memberQualifications.member", "member") // .leftJoin("memberQualifications.member", "member")
.groupBy("qualification.id"), // .groupBy("qualification.id"),
}), // }),
true // true
); // );
await queryRunner.createView( // await queryRunner.createView(
new View({ // new View({
name: "membership_view", // name: "membership_view",
expression: (datasource: DataSource) => // expression: (datasource: DataSource) =>
datasource // datasource
.getRepository(membership) // .getRepository(membership)
.createQueryBuilder("membership") // .createQueryBuilder("membership")
.select("status.id", "statusId") // .select("status.id", "statusId")
.addSelect("status.status", "status") // .addSelect("status.status", "status")
.addSelect("member.id", "memberId") // .addSelect("member.id", "memberId")
.addSelect("member.salutation", "memberSalutation") // .addSelect("member.salutation", "memberSalutation")
.addSelect("member.firstname", "memberFirstname") // .addSelect("member.firstname", "memberFirstname")
.addSelect("member.lastname", "memberLastname") // .addSelect("member.lastname", "memberLastname")
.addSelect("member.nameaffix", "memberNameaffix") // .addSelect("member.nameaffix", "memberNameaffix")
.addSelect("member.birthdate", "memberBirthdate") // .addSelect("member.birthdate", "memberBirthdate")
.addSelect( // .addSelect(
"SUM(TIMESTAMPDIFF(DAY, membership.start, COALESCE(membership.end, CURRENT_DATE)))", // "SUM(TIMESTAMPDIFF(DAY, membership.start, COALESCE(membership.end, CURRENT_DATE)))",
"durationInDays" // "durationInDays"
) // )
.leftJoin("membership.status", "status") // .leftJoin("membership.status", "status")
.leftJoin("membership.member", "member") // .leftJoin("membership.member", "member")
.groupBy("status.id"), // .groupBy("status.id"),
}), // }),
true // true
); // );
} }
public async down(queryRunner: QueryRunner): Promise<void> { public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropView("membership_view"); // await queryRunner.dropView("membership_view");
await queryRunner.dropView("member_qualifications_view"); // await queryRunner.dropView("member_qualifications_view");
await queryRunner.dropView("member_executive_positions_view"); // await queryRunner.dropView("member_executive_positions_view");
await queryRunner.dropView("member_view"); // await queryRunner.dropView("member_view");
} }
} }

View file

@ -8,106 +8,104 @@ export class ExtendViewValues1736084198860 implements MigrationInterface {
name = "ExtendViewValues1736084198860"; name = "ExtendViewValues1736084198860";
public async up(queryRunner: QueryRunner): Promise<void> { public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropView("membership_view"); // await queryRunner.dropView("membership_view");
await queryRunner.dropView("member_view"); // await queryRunner.dropView("member_view");
// await queryRunner.createView(
await queryRunner.createView( // new View({
new View({ // name: "member_view",
name: "member_view", // expression: (datasource: DataSource) =>
expression: (datasource: DataSource) => // datasource
datasource // .getRepository(member)
.getRepository(member) // .createQueryBuilder("member")
.createQueryBuilder("member") // .select("member.id", "id")
.select("member.id", "id") // .addSelect("member.salutation", "salutation")
.addSelect("member.salutation", "salutation") // .addSelect("member.firstname", "firstname")
.addSelect("member.firstname", "firstname") // .addSelect("member.lastname", "lastname")
.addSelect("member.lastname", "lastname") // .addSelect("member.nameaffix", "nameaffix")
.addSelect("member.nameaffix", "nameaffix") // .addSelect("member.birthdate", "birthdate")
.addSelect("member.birthdate", "birthdate") // .addSelect("TIMESTAMPDIFF(YEAR, member.birthdate, CURDATE())", "todayAge")
.addSelect("TIMESTAMPDIFF(YEAR, member.birthdate, CURDATE())", "todayAge") // .addSelect("YEAR(CURDATE()) - YEAR(member.birthdate)", "ageThisYear")
.addSelect("YEAR(CURDATE()) - YEAR(member.birthdate)", "ageThisYear") // .addSelect("CONCAT('_', FROM_DAYS(TIMESTAMPDIFF(DAY, member.birthdate, CURDATE())))", "exactAge"),
.addSelect("CONCAT('_', FROM_DAYS(TIMESTAMPDIFF(DAY, member.birthdate, CURDATE())))", "exactAge"), // }),
}), // true
true // );
); // await queryRunner.createView(
await queryRunner.createView( // new View({
new View({ // name: "membership_view",
name: "membership_view", // expression: (datasource: DataSource) =>
expression: (datasource: DataSource) => // datasource
datasource // .getRepository(membership)
.getRepository(membership) // .createQueryBuilder("membership")
.createQueryBuilder("membership") // .select("status.id", "statusId")
.select("status.id", "statusId") // .addSelect("status.status", "status")
.addSelect("status.status", "status") // .addSelect("member.id", "memberId")
.addSelect("member.id", "memberId") // .addSelect("member.salutation", "memberSalutation")
.addSelect("member.salutation", "memberSalutation") // .addSelect("member.firstname", "memberFirstname")
.addSelect("member.firstname", "memberFirstname") // .addSelect("member.lastname", "memberLastname")
.addSelect("member.lastname", "memberLastname") // .addSelect("member.nameaffix", "memberNameaffix")
.addSelect("member.nameaffix", "memberNameaffix") // .addSelect("member.birthdate", "memberBirthdate")
.addSelect("member.birthdate", "memberBirthdate") // .addSelect(
.addSelect( // "SUM(TIMESTAMPDIFF(DAY, membership.start, COALESCE(membership.end, CURRENT_DATE)))",
"SUM(TIMESTAMPDIFF(DAY, membership.start, COALESCE(membership.end, CURRENT_DATE)))", // "durationInDays"
"durationInDays" // )
) // .addSelect(
.addSelect( // "CONCAT('_', FROM_DAYS(SUM(TIMESTAMPDIFF(DAY, membership.start, COALESCE(membership.end, CURRENT_DATE)))))",
"CONCAT('_', FROM_DAYS(SUM(TIMESTAMPDIFF(DAY, membership.start, COALESCE(membership.end, CURRENT_DATE)))))", // "durationInYears"
"durationInYears" // )
) // .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"),
.addGroupBy("member.id"), // }),
}), // true
true // );
);
} }
public async down(queryRunner: QueryRunner): Promise<void> { public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropView("membership_view"); // await queryRunner.dropView("membership_view");
await queryRunner.dropView("member_view"); // await queryRunner.dropView("member_view");
// await queryRunner.createView(
await queryRunner.createView( // new View({
new View({ // name: "member_view",
name: "member_view", // expression: (datasource: DataSource) =>
expression: (datasource: DataSource) => // datasource
datasource // .getRepository(member)
.getRepository(member) // .createQueryBuilder("member")
.createQueryBuilder("member") // .select("member.id", "id")
.select("member.id", "id") // .addSelect("member.salutation", "salutation")
.addSelect("member.salutation", "salutation") // .addSelect("member.firstname", "firstname")
.addSelect("member.firstname", "firstname") // .addSelect("member.lastname", "lastname")
.addSelect("member.lastname", "lastname") // .addSelect("member.nameaffix", "nameaffix")
.addSelect("member.nameaffix", "nameaffix") // .addSelect("member.birthdate", "birthdate")
.addSelect("member.birthdate", "birthdate") // .addSelect("TIMESTAMPDIFF(YEAR, member.birthdate, CURDATE())", "todayAge")
.addSelect("TIMESTAMPDIFF(YEAR, member.birthdate, CURDATE())", "todayAge") // .addSelect("YEAR(CURDATE()) - YEAR(member.birthdate)", "ageThisYear"),
.addSelect("YEAR(CURDATE()) - YEAR(member.birthdate)", "ageThisYear"), // }),
}), // true
true // );
); // await queryRunner.createView(
await queryRunner.createView( // new View({
new View({ // name: "membership_view",
name: "membership_view", // expression: (datasource: DataSource) =>
expression: (datasource: DataSource) => // datasource
datasource // .getRepository(membership)
.getRepository(membership) // .createQueryBuilder("membership")
.createQueryBuilder("membership") // .select("status.id", "statusId")
.select("status.id", "statusId") // .addSelect("status.status", "status")
.addSelect("status.status", "status") // .addSelect("member.id", "memberId")
.addSelect("member.id", "memberId") // .addSelect("member.salutation", "memberSalutation")
.addSelect("member.salutation", "memberSalutation") // .addSelect("member.firstname", "memberFirstname")
.addSelect("member.firstname", "memberFirstname") // .addSelect("member.lastname", "memberLastname")
.addSelect("member.lastname", "memberLastname") // .addSelect("member.nameaffix", "memberNameaffix")
.addSelect("member.nameaffix", "memberNameaffix") // .addSelect("member.birthdate", "memberBirthdate")
.addSelect("member.birthdate", "memberBirthdate") // .addSelect(
.addSelect( // "SUM(TIMESTAMPDIFF(DAY, membership.start, COALESCE(membership.end, CURRENT_DATE)))",
"SUM(TIMESTAMPDIFF(DAY, membership.start, COALESCE(membership.end, CURRENT_DATE)))", // "durationInDays"
"durationInDays" // )
) // .leftJoin("membership.status", "status")
.leftJoin("membership.status", "status") // .leftJoin("membership.member", "member")
.leftJoin("membership.member", "member") // .groupBy("status.id"),
.groupBy("status.id"), // }),
}), // true
true // );
);
} }
} }

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

@ -0,0 +1,91 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from "typeorm";
import { DB_TYPE } from "../env.defaults";
import { member } from "../entity/club/member/member";
import { salutation } from "../entity/settings/salutation";
export class SalutationAsTable1737796878058 implements MigrationInterface {
name = "SalutationAsTable1737796878058";
public async up(queryRunner: QueryRunner): Promise<void> {
const variableType_int = DB_TYPE == "mysql" ? "int" : "integer";
await queryRunner.createTable(
new Table({
name: "salutation",
columns: [
{ name: "id", type: variableType_int, isPrimary: true, isGenerated: true, generationStrategy: "increment" },
{ name: "salutation", type: "varchar", length: "255", isUnique: true, isNullable: false },
],
}),
true
);
// ! has to be sql. Else the column would be dropped and created - resulting in data loss.
await queryRunner.query(
`ALTER TABLE \`member\` CHANGE \`salutation\` \`salutationId\` varchar(255) NOT NULL DEFAULT ''`
);
// ! has to be sql. Else no data is returned.
const existing_salutations = await queryRunner.query(
"SELECT DISTINCT salutationId FROM `member` `member` GROUP BY salutationId"
);
for (let s of existing_salutations.map((s: any) => s.salutationId) as Array<string>) {
await queryRunner.manager.getRepository(salutation).save({ salutation: s });
}
const salutations = await queryRunner.manager.getRepository(salutation).find();
for (let salutation of salutations) {
await queryRunner.manager
.getRepository(member)
.createQueryBuilder("member")
.update({ salutationId: salutation.id })
.where({ salutationId: salutation.salutation })
.execute();
}
await queryRunner.query(
`ALTER TABLE \`member\` CHANGE \`salutationId\` \`salutationId\` ${variableType_int} NOT NULL`
);
await queryRunner.createForeignKey(
"member",
new TableForeignKey({
columnNames: ["salutationId"],
referencedColumnNames: ["id"],
referencedTableName: "salutation",
onDelete: "RESTRICT",
onUpdate: "RESTRICT",
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const table = await queryRunner.getTable("member");
const foreignKey = table.foreignKeys.find((fk) => fk.columnNames.indexOf("salutationId") !== -1);
await queryRunner.dropForeignKey("member", foreignKey);
// ! has to be sql. Else the column would be dropped and created - resulting in data loss.
await queryRunner.query(
`ALTER TABLE \`member\` CHANGE \`salutationId\` \`salutationId\` varchar(255) NOT NULL DEFAULT ''`
);
const salutations = await queryRunner.manager.getRepository(salutation).find();
for (let salutation of salutations) {
await queryRunner.manager
.getRepository(member)
.createQueryBuilder("member")
.update({ salutationId: salutation.salutation as unknown as number })
.where({ salutationId: salutation.id })
.execute();
}
await queryRunner.query(
`ALTER TABLE \`member\` CHANGE \`salutationId\` \`salutation\` varchar(255) NOT NULL DEFAULT 'none'`
);
await queryRunner.dropTable("webapi");
}
}

View file

@ -0,0 +1,233 @@
import { DataSource, MigrationInterface, QueryRunner, View } from "typeorm";
import { member } from "../entity/club/member/member";
import { memberExecutivePositions } from "../entity/club/member/memberExecutivePositions";
import { memberQualifications } from "../entity/club/member/memberQualifications";
import { membership } from "../entity/club/member/membership";
export class UpdateViews1737800468938 implements MigrationInterface {
name = "UpdateViews1737800468938";
public async up(queryRunner: QueryRunner): Promise<void> {
// await queryRunner.dropView("membership_view");
// await queryRunner.dropView("member_executive_positions_view");
// await queryRunner.dropView("member_qualifications_view");
// await queryRunner.dropView("member_view");
await queryRunner.query(`DROP VIEW IF EXISTS membership_view`);
await queryRunner.query(`DROP VIEW IF EXISTS member_executive_positions_view`);
await queryRunner.query(`DROP VIEW IF EXISTS member_qualifications_view`);
await queryRunner.query(`DROP VIEW IF EXISTS member_view`);
await queryRunner.createView(
new View({
name: "member_view",
expression: (datasource: DataSource) =>
datasource
.getRepository(member)
.createQueryBuilder("member")
.select("member.id", "id")
.addSelect("member.firstname", "firstname")
.addSelect("member.lastname", "lastname")
.addSelect("member.nameaffix", "nameaffix")
.addSelect("member.birthdate", "birthdate")
.addSelect("salutation.salutation", "salutation")
.addSelect("TIMESTAMPDIFF(YEAR, member.birthdate, CURDATE())", "todayAge")
.addSelect("YEAR(CURDATE()) - YEAR(member.birthdate)", "ageThisYear")
.addSelect("CONCAT('_', FROM_DAYS(TIMESTAMPDIFF(DAY, member.birthdate, CURDATE())))", "exactAge")
.leftJoin("member.salutation", "salutation"),
}),
true
);
await queryRunner.createView(
new View({
name: "member_executive_positions_view",
expression: (datasource: DataSource) =>
datasource
.getRepository(memberExecutivePositions)
.createQueryBuilder("memberExecutivePositions")
.select("executivePosition.id", "positionId")
.addSelect("executivePosition.position", "position")
.addSelect("member.id", "memberId")
.addSelect("member.firstname", "memberFirstname")
.addSelect("member.lastname", "memberLastname")
.addSelect("member.nameaffix", "memberNameaffix")
.addSelect("member.birthdate", "memberBirthdate")
.addSelect("salutation.salutation", "memberSalutation")
.addSelect(
"SUM(TIMESTAMPDIFF(DAY, memberExecutivePositions.start, COALESCE(memberExecutivePositions.end, CURRENT_DATE)))",
"durationInDays"
)
.leftJoin("memberExecutivePositions.executivePosition", "executivePosition")
.leftJoin("memberExecutivePositions.member", "member")
.leftJoin("member.salutation", "salutation")
.groupBy("executivePosition.id")
.addGroupBy("member.id"),
}),
true
);
await queryRunner.createView(
new View({
name: "member_qualifications_view",
expression: (datasource: DataSource) =>
datasource
.getRepository(memberQualifications)
.createQueryBuilder("memberQualifications")
.select("qualification.id", "qualificationId")
.addSelect("qualification.qualification", "qualification")
.addSelect("member.id", "memberId")
.addSelect("member.firstname", "memberFirstname")
.addSelect("member.lastname", "memberLastname")
.addSelect("member.nameaffix", "memberNameaffix")
.addSelect("member.birthdate", "memberBirthdate")
.addSelect("salutation.salutation", "memberSalutation")
.addSelect(
"SUM(TIMESTAMPDIFF(DAY, memberQualifications.start, COALESCE(memberQualifications.end, CURRENT_DATE)))",
"durationInDays"
)
.leftJoin("memberQualifications.qualification", "qualification")
.leftJoin("memberQualifications.member", "member")
.leftJoin("member.salutation", "salutation")
.groupBy("qualification.id")
.addGroupBy("member.id"),
}),
true
);
await queryRunner.createView(
new View({
name: "membership_view",
expression: (datasource: DataSource) =>
datasource
.getRepository(membership)
.createQueryBuilder("membership")
.select("status.id", "statusId")
.addSelect("status.status", "status")
.addSelect("member.id", "memberId")
.addSelect("member.firstname", "memberFirstname")
.addSelect("member.lastname", "memberLastname")
.addSelect("member.nameaffix", "memberNameaffix")
.addSelect("member.birthdate", "memberBirthdate")
.addSelect("salutation.salutation", "memberSalutation")
.addSelect(
"SUM(TIMESTAMPDIFF(DAY, membership.start, COALESCE(membership.end, CURRENT_DATE)))",
"durationInDays"
)
.addSelect(
"CONCAT('_', FROM_DAYS(SUM(TIMESTAMPDIFF(DAY, membership.start, COALESCE(membership.end, CURRENT_DATE)))))",
"durationInYears"
)
.leftJoin("membership.status", "status")
.leftJoin("membership.member", "member")
.leftJoin("member.salutation", "salutation")
.groupBy("status.id")
.addGroupBy("member.id"),
}),
true
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropView("membership_view");
await queryRunner.dropView("member_executive_positions_view");
await queryRunner.dropView("member_qualifications_view");
await queryRunner.dropView("member_view");
await queryRunner.createView(
new View({
name: "member_view",
expression: (datasource: DataSource) =>
datasource
.getRepository(member)
.createQueryBuilder("member")
.select("member.id", "id")
.addSelect("member.salutation", "salutation")
.addSelect("member.firstname", "firstname")
.addSelect("member.lastname", "lastname")
.addSelect("member.nameaffix", "nameaffix")
.addSelect("member.birthdate", "birthdate")
.addSelect("TIMESTAMPDIFF(YEAR, member.birthdate, CURDATE())", "todayAge")
.addSelect("YEAR(CURDATE()) - YEAR(member.birthdate)", "ageThisYear")
.addSelect("CONCAT('_', FROM_DAYS(TIMESTAMPDIFF(DAY, member.birthdate, CURDATE())))", "exactAge"),
}),
true
);
await queryRunner.createView(
new View({
name: "member_executive_positions_view",
expression: (datasource: DataSource) =>
datasource
.getRepository(memberExecutivePositions)
.createQueryBuilder("memberExecutivePositions")
.select("executivePosition.id", "positionId")
.addSelect("executivePosition.position", "position")
.addSelect("member.id", "memberId")
.addSelect("member.salutation", "memberSalutation")
.addSelect("member.firstname", "memberFirstname")
.addSelect("member.lastname", "memberLastname")
.addSelect("member.nameaffix", "memberNameaffix")
.addSelect("member.birthdate", "memberBirthdate")
.addSelect(
"SUM(TIMESTAMPDIFF(DAY, memberExecutivePositions.start, COALESCE(memberExecutivePositions.end, CURRENT_DATE)))",
"durationInDays"
)
.leftJoin("memberExecutivePositions.executivePosition", "executivePosition")
.leftJoin("memberExecutivePositions.member", "member")
.groupBy("executivePosition.id"),
}),
true
);
await queryRunner.createView(
new View({
name: "member_qualifications_view",
expression: (datasource: DataSource) =>
datasource
.getRepository(memberQualifications)
.createQueryBuilder("memberQualifications")
.select("qualification.id", "qualificationId")
.addSelect("qualification.qualification", "qualification")
.addSelect("member.id", "memberId")
.addSelect("member.salutation", "memberSalutation")
.addSelect("member.firstname", "memberFirstname")
.addSelect("member.lastname", "memberLastname")
.addSelect("member.nameaffix", "memberNameaffix")
.addSelect("member.birthdate", "memberBirthdate")
.addSelect(
"SUM(TIMESTAMPDIFF(DAY, memberQualifications.start, COALESCE(memberQualifications.end, CURRENT_DATE)))",
"durationInDays"
)
.leftJoin("memberQualifications.qualification", "qualification")
.leftJoin("memberQualifications.member", "member")
.groupBy("qualification.id"),
}),
true
);
await queryRunner.createView(
new View({
name: "membership_view",
expression: (datasource: DataSource) =>
datasource
.getRepository(membership)
.createQueryBuilder("membership")
.select("status.id", "statusId")
.addSelect("status.status", "status")
.addSelect("member.id", "memberId")
.addSelect("member.salutation", "memberSalutation")
.addSelect("member.firstname", "memberFirstname")
.addSelect("member.lastname", "memberLastname")
.addSelect("member.nameaffix", "memberNameaffix")
.addSelect("member.birthdate", "memberBirthdate")
.addSelect(
"SUM(TIMESTAMPDIFF(DAY, membership.start, COALESCE(membership.end, CURRENT_DATE)))",
"durationInDays"
)
.addSelect(
"CONCAT('_', FROM_DAYS(SUM(TIMESTAMPDIFF(DAY, membership.start, COALESCE(membership.end, CURRENT_DATE)))))",
"durationInYears"
)
.leftJoin("membership.status", "status")
.leftJoin("membership.member", "member")
.groupBy("status.id")
.addGroupBy("member.id"),
}),
true
);
}
}

View file

@ -0,0 +1,71 @@
import { MigrationInterface, QueryRunner, TableColumn, TableForeignKey, TableIndex } from "typeorm";
import { communication } from "../entity/club/member/communication";
import { member } from "../entity/club/member/member";
export class MoveSendNewsletterFlag1737816852011 implements MigrationInterface {
name = "MoveSendNewsletterFlag1737816852011";
public async up(queryRunner: QueryRunner): Promise<void> {
const table = await queryRunner.getTable("member");
const foreignKey = table.foreignKeys.find((fk) => fk.columnNames.indexOf("sendNewsletterId") !== -1);
await queryRunner.dropForeignKey("member", foreignKey);
await queryRunner.addColumn(
"communication",
new TableColumn({ name: "isSendNewsletter", type: "tinyint", isNullable: false, default: 0 })
);
// ! has to be sql. Else no data is returned.
const member_newsletter_send = await queryRunner.query("SELECT sendNewsletterId, id FROM `member` `member`");
for (let assigned of member_newsletter_send.map((mns: any) => ({
id: mns.id,
sendNewsletterId: mns.sendNewsletterId,
})) as Array<{ id: number; sendNewsletterId: number }>) {
await queryRunner.manager
.getRepository(communication)
.createQueryBuilder("communication")
.update({ isSendNewsletter: true })
.where({ memberId: assigned.id, id: assigned.sendNewsletterId })
.execute();
}
await queryRunner.dropColumn("member", "sendNewsletterId");
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
"member",
new TableColumn({ name: "sendNewsletterId", type: "int", isNullable: true, isUnique: true, default: null })
);
const member_newsletter_send = await queryRunner.manager
.getRepository(communication)
.createQueryBuilder("communication")
.where("communication.isSendNewsletter = :isSendNewsletter", { isSendNewsletter: true })
.getMany();
for (let assigned of member_newsletter_send.map((mns: any) => ({
id: mns.id,
memberId: mns.memberId,
})) as Array<{ id: number; memberId: number }>) {
await queryRunner.query("UPDATE `member` SET sendNewsletterId = ? WHERE id = ?", [
assigned.id,
assigned.memberId,
]);
}
await queryRunner.createForeignKey(
"member",
new TableForeignKey({
columnNames: ["sendNewsletterId"],
referencedColumnNames: ["id"],
referencedTableName: "communication",
onDelete: "SET NULL",
onUpdate: "RESTRICT",
})
);
await queryRunner.dropColumn("communication", "isSendNewsletter");
}
}

View file

@ -1,36 +1,38 @@
import express, { Request, Response } from "express"; import express, { Request, Response } from "express";
import { import {
addAwardToMember, addAwardToMember,
addCommunicationToMember, addCommunicationToMember,
addExecutivePositionToMember, addExecutivePositionToMember,
addMembershipToMember, addMembershipToMember,
addQualificationToMember, addQualificationToMember,
createMember, createMember,
createMemberPrintoutList, createMemberPrintoutList,
deleteAwardOfMember, deleteAwardOfMember,
deleteCommunicationOfMember, deleteCommunicationOfMember,
deleteExecutivePositionOfMember, deleteExecutivePositionOfMember,
deleteMemberById, deleteMemberById,
deleteMembershipOfMember, deleteMembershipOfMember,
deleteQualificationOfMember, deleteQualificationOfMember,
getAllMembers, getAllMembers,
getAwardByMemberAndRecord, getAwardByMemberAndRecord,
getAwardsByMember, getAwardsByMember,
getCommunicationByMemberAndRecord, getCommunicationByMemberAndRecord,
getCommunicationsByMember, getCommunicationsByMember,
getExecutivePositionByMemberAndRecord, getExecutivePositionByMemberAndRecord,
getExecutivePositionsByMember, getExecutivePositionsByMember,
getMemberById, getMemberById,
getMembershipByMemberAndRecord, getMembershipByMemberAndRecord,
getMembershipsByMember, getMembershipsByMember,
getQualificationByMemberAndRecord, getMembershipStatisticsById,
getQualificationsByMember, getMemberStatisticsById,
updateAwardOfMember, getQualificationByMemberAndRecord,
updateCommunicationOfMember, getQualificationsByMember,
updateExecutivePositionOfMember, updateAwardOfMember,
updateMemberById, updateCommunicationOfMember,
updateMembershipOfMember, updateExecutivePositionOfMember,
updateQualificationOfMember, updateMemberById,
updateMembershipOfMember,
updateQualificationOfMember,
} from "../../../controller/admin/club/memberController"; } from "../../../controller/admin/club/memberController";
import PermissionHelper from "../../../helpers/permissionHelper"; import PermissionHelper from "../../../helpers/permissionHelper";
@ -44,14 +46,22 @@ 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);
}); });
router.get("/:memberId/memberships", async (req: Request, res: Response) => { 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

@ -6,6 +6,7 @@ import communicationType from "./settings/communicationType";
import executivePosition from "./settings/executivePosition"; import executivePosition from "./settings/executivePosition";
import membershipStatus from "./settings/membershipStatus"; import membershipStatus from "./settings/membershipStatus";
import qualification from "./settings/qualification"; import qualification from "./settings/qualification";
import salutation from "./settings/salutation";
import calendarType from "./settings/calendarType"; import calendarType from "./settings/calendarType";
import queryStore from "./settings/queryStore"; import queryStore from "./settings/queryStore";
import template from "./settings/template"; import template from "./settings/template";
@ -21,6 +22,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 });
@ -41,6 +44,7 @@ router.use(
membershipStatus membershipStatus
); );
router.use("/qualification", PermissionHelper.passCheckMiddleware("read", "settings", "qualification"), qualification); router.use("/qualification", PermissionHelper.passCheckMiddleware("read", "settings", "qualification"), qualification);
router.use("/salutation", PermissionHelper.passCheckMiddleware("read", "settings", "salutation"), salutation);
router.use("/calendartype", PermissionHelper.passCheckMiddleware("read", "settings", "calendar_type"), calendarType); router.use("/calendartype", PermissionHelper.passCheckMiddleware("read", "settings", "calendar_type"), calendarType);
router.use("/querystore", PermissionHelper.passCheckMiddleware("read", "settings", "query_store"), queryStore); router.use("/querystore", PermissionHelper.passCheckMiddleware("read", "settings", "query_store"), queryStore);
router.use("/template", PermissionHelper.passCheckMiddleware("read", "settings", "template"), template); router.use("/template", PermissionHelper.passCheckMiddleware("read", "settings", "template"), template);
@ -60,5 +64,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

@ -0,0 +1,45 @@
import express, { Request, Response } from "express";
import {
createSalutation,
deleteSalutation,
getAllSalutations,
getSalutationById,
updateSalutation,
} from "../../../controller/admin/settings/salutationController";
import PermissionHelper from "../../../helpers/permissionHelper";
var router = express.Router({ mergeParams: true });
router.get("/", async (req: Request, res: Response) => {
await getAllSalutations(req, res);
});
router.get("/:id", async (req: Request, res: Response) => {
await getSalutationById(req, res);
});
router.post(
"/",
PermissionHelper.passCheckMiddleware("create", "settings", "salutation"),
async (req: Request, res: Response) => {
await createSalutation(req, res);
}
);
router.patch(
"/:id",
PermissionHelper.passCheckMiddleware("update", "settings", "salutation"),
async (req: Request, res: Response) => {
await updateSalutation(req, res);
}
);
router.delete(
"/:id",
PermissionHelper.passCheckMiddleware("delete", "settings", "salutation"),
async (req: Request, res: Response) => {
await deleteSalutation(req, res);
}
);
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

@ -14,6 +14,11 @@ 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 detectPWA from "../middleware/detectPWA";
import webapi from "./webapi";
import authenticateAPI from "../middleware/authenticateAPI";
import server from "./server";
import PermissionHelper from "../helpers/permissionHelper";
import preventWebapiAccess from "../middleware/preventWebApiAccess";
export default (app: Express) => { export default (app: Express) => {
app.set("query parser", "extended"); app.set("query parser", "extended");
@ -28,12 +33,14 @@ export default (app: Express) => {
app.use(detectPWA); app.use(detectPWA);
app.use("/api/public", publicAvailable); app.use("/api/public", publicAvailable);
app.use("/api/setup", allowSetup, setup); app.use("/api/setup", preventWebapiAccess, allowSetup, setup);
app.use("/api/reset", reset); app.use("/api/reset", preventWebapiAccess, reset);
app.use("/api/invite", invite); app.use("/api/invite", preventWebapiAccess, invite);
app.use("/api/auth", auth); app.use("/api/auth", preventWebapiAccess, auth);
app.use("/api/webapi", authenticateAPI, webapi);
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", preventWebapiAccess, user);
app.use("/api/server", preventWebapiAccess, 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

@ -14,7 +14,6 @@ export default abstract class CommunicationService {
.createQueryBuilder("communication") .createQueryBuilder("communication")
.leftJoinAndSelect("communication.type", "communicationType") .leftJoinAndSelect("communication.type", "communicationType")
.leftJoinAndSelect("communication.member", "member") .leftJoinAndSelect("communication.member", "member")
.leftJoinAndSelect("member.sendNewsletter", "sendNewsletter")
.where("communication.memberId = :memberId", { memberId: memberId }) .where("communication.memberId = :memberId", { memberId: memberId })
.orderBy("communicationType.type", "ASC") .orderBy("communicationType.type", "ASC")
.getMany() .getMany()
@ -38,7 +37,6 @@ export default abstract class CommunicationService {
.createQueryBuilder("communication") .createQueryBuilder("communication")
.leftJoinAndSelect("communication.type", "communicationType") .leftJoinAndSelect("communication.type", "communicationType")
.leftJoinAndSelect("communication.member", "member") .leftJoinAndSelect("communication.member", "member")
.leftJoinAndSelect("member.sendNewsletter", "sendNewsletter")
.where("communication.memberId = :memberId", { memberId: memberId }) .where("communication.memberId = :memberId", { memberId: memberId })
.andWhere("communication.id = :recordId", { recordId: recordId }) .andWhere("communication.id = :recordId", { recordId: recordId })
.getOneOrFail() .getOneOrFail()

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")
@ -26,8 +39,6 @@ export default abstract class MemberService {
) )
.leftJoinAndSelect("membership_first.status", "status_first") .leftJoinAndSelect("membership_first.status", "status_first")
.leftJoinAndSelect("membership_last.status", "status_last") .leftJoinAndSelect("membership_last.status", "status_last")
.leftJoinAndSelect("member.sendNewsletter", "sendNewsletter")
.leftJoinAndSelect("sendNewsletter.type", "communicationtype")
.leftJoinAndMapMany( .leftJoinAndMapMany(
"member.preferredCommunication", "member.preferredCommunication",
"member.communications", "member.communications",
@ -35,8 +46,16 @@ export default abstract class MemberService {
"preferredCommunication.preferred = 1" "preferredCommunication.preferred = 1"
) )
.leftJoinAndSelect("preferredCommunication.type", "communicationtype_preferred") .leftJoinAndSelect("preferredCommunication.type", "communicationtype_preferred")
.leftJoinAndMapOne(
"member.sendNewsletter",
"member.communications",
"sendNewsletter",
"sendNewsletter.isSendNewsletter = 1"
)
.leftJoinAndSelect("sendNewsletter.type", "communicationtype")
.leftJoinAndMapMany("member.smsAlarming", "member.communications", "smsAlarming", "smsAlarming.isSMSAlarming = 1") .leftJoinAndMapMany("member.smsAlarming", "member.communications", "smsAlarming", "smsAlarming.isSMSAlarming = 1")
.leftJoinAndSelect("smsAlarming.type", "communicationtype_smsAlarming"); .leftJoinAndSelect("smsAlarming.type", "communicationtype_smsAlarming")
.leftJoinAndSelect("member.salutation", "salutation");
if (search != "") { if (search != "") {
search.split(" ").forEach((term, index) => { search.split(" ").forEach((term, index) => {
@ -54,9 +73,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")
@ -92,18 +117,23 @@ export default abstract class MemberService {
) )
.leftJoinAndSelect("membership_first.status", "status_first") .leftJoinAndSelect("membership_first.status", "status_first")
.leftJoinAndSelect("membership_last.status", "status_last") .leftJoinAndSelect("membership_last.status", "status_last")
.leftJoinAndSelect("member.sendNewsletter", "sendNewsletter")
.leftJoinAndSelect("sendNewsletter.type", "communicationtype")
.leftJoinAndMapMany( .leftJoinAndMapMany(
"member.preferredCommunication", "member.preferredCommunication",
"member.communications", "member.communications",
"preferredCommunication", "preferredCommunication",
"preferredCommunication.preferred = 1" "preferredCommunication.preferred = 1"
) )
.leftJoinAndSelect("preferredCommunication.type", "communicationtype_preferred")
.leftJoinAndMapOne(
"member.sendNewsletter",
"member.communications",
"sendNewsletter",
"sendNewsletter.isSendNewsletter = 1"
)
.leftJoinAndSelect("sendNewsletter.type", "communicationtype")
.leftJoinAndMapMany("member.smsAlarming", "member.communications", "smsAlarming", "smsAlarming.isSMSAlarming = 1") .leftJoinAndMapMany("member.smsAlarming", "member.communications", "smsAlarming", "smsAlarming.isSMSAlarming = 1")
.leftJoinAndSelect("smsAlarming.type", "communicationtype_smsAlarming") .leftJoinAndSelect("smsAlarming.type", "communicationtype_smsAlarming")
.leftJoinAndSelect("preferredCommunication.type", "communicationtype_preferred") .leftJoinAndSelect("member.salutation", "salutation")
.where("member.id = :id", { id: id }) .where("member.id = :id", { id: id })
.getOneOrFail() .getOneOrFail()
.then((res) => { .then((res) => {
@ -114,6 +144,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>}
@ -145,7 +194,12 @@ export default abstract class MemberService {
return await dataSource return await dataSource
.getRepository(member) .getRepository(member)
.createQueryBuilder("member") .createQueryBuilder("member")
.leftJoinAndSelect("member.sendNewsletter", "sendNewsletter") .leftJoinAndMapOne(
"member.sendNewsletter",
"member.communications",
"sendNewsletter",
"sendNewsletter.isSendNewsletter = 1"
)
.where("member.id = :id", { id: id }) .where("member.id = :id", { id: id })
.getOneOrFail() .getOneOrFail()
.then((res) => { .then((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

@ -0,0 +1,41 @@
import { dataSource } from "../../data-source";
import { salutation } from "../../entity/settings/salutation";
import InternalException from "../../exceptions/internalException";
export default abstract class SalutationService {
/**
* @description get all salutations
* @returns {Promise<Array<salutation>>}
*/
static async getAll(): Promise<Array<salutation>> {
return await dataSource
.getRepository(salutation)
.createQueryBuilder("salutation")
.orderBy("salutation", "ASC")
.getMany()
.then((res) => {
return res;
})
.catch((err) => {
throw new InternalException("salutations not found", err);
});
}
/**
* @description get salutation by id
* @returns {Promise<salutation>}
*/
static async getById(id: number): Promise<salutation> {
return await dataSource
.getRepository(salutation)
.createQueryBuilder("salutation")
.where("salutation.id = :id", { id: id })
.getOneOrFail()
.then((res) => {
return res;
})
.catch((err) => {
throw new InternalException("salutation not found by id", err);
});
}
}

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,9 +11,11 @@ export type PermissionModule =
| "executive_position" | "executive_position"
| "communication_type" | "communication_type"
| "membership_status" | "membership_status"
| "salutation"
| "calendar_type" | "calendar_type"
| "user" | "user"
| "role" | "role"
| "webapi"
| "query" | "query"
| "query_store" | "query_store"
| "template" | "template"
@ -52,9 +54,11 @@ export const permissionModules: Array<PermissionModule> = [
"executive_position", "executive_position",
"communication_type", "communication_type",
"membership_status", "membership_status",
"salutation",
"calendar_type", "calendar_type",
"user", "user",
"role", "role",
"webapi",
"query", "query",
"query_store", "query_store",
"template", "template",
@ -69,11 +73,12 @@ export const sectionsAndModules: SectionsAndModulesObject = {
"executive_position", "executive_position",
"communication_type", "communication_type",
"membership_status", "membership_status",
"salutation",
"calendar_type", "calendar_type",
"query_store", "query_store",
"template", "template",
"template_usage", "template_usage",
"newsletter_config", "newsletter_config",
], ],
user: ["user", "role"], user: ["user", "role", "webapi"],
}; };

View file

@ -1,10 +1,10 @@
import { Salutation } from "../../../../enums/salutation"; import { SalutationViewModel } from "../../settings/salutation.models";
import { CommunicationViewModel } from "./communication.models"; import { CommunicationViewModel } from "./communication.models";
import { MembershipViewModel } from "./membership.models"; import { MembershipViewModel } from "./membership.models";
export interface MemberViewModel { export interface MemberViewModel {
id: number; id: number;
salutation: Salutation; salutation: SalutationViewModel;
firstname: string; firstname: string;
lastname: string; lastname: string;
nameaffix: string; nameaffix: string;
@ -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: string;
firstname: string;
lastname: string;
nameaffix: string;
birthdate: Date;
todayAge: number;
ageThisYear: number;
exactAge: string;
}

View file

@ -6,3 +6,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: string;
memberFirstname: string;
memberLastname: string;
memberNameaffix: string;
memberBirthdate: Date;
}

View file

@ -0,0 +1,4 @@
export interface SalutationViewModel {
id: number;
salutation: string;
}

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

@ -1,6 +1,5 @@
import { DataSource, ViewColumn, ViewEntity } from "typeorm"; import { DataSource, ViewColumn, ViewEntity } from "typeorm";
import { memberExecutivePositions } from "../entity/club/member/memberExecutivePositions"; import { memberExecutivePositions } from "../entity/club/member/memberExecutivePositions";
import { Salutation } from "../enums/salutation";
@ViewEntity({ @ViewEntity({
expression: (datasource: DataSource) => expression: (datasource: DataSource) =>
@ -10,18 +9,20 @@ import { Salutation } from "../enums/salutation";
.select("executivePosition.id", "positionId") .select("executivePosition.id", "positionId")
.addSelect("executivePosition.position", "position") .addSelect("executivePosition.position", "position")
.addSelect("member.id", "memberId") .addSelect("member.id", "memberId")
.addSelect("member.salutation", "memberSalutation")
.addSelect("member.firstname", "memberFirstname") .addSelect("member.firstname", "memberFirstname")
.addSelect("member.lastname", "memberLastname") .addSelect("member.lastname", "memberLastname")
.addSelect("member.nameaffix", "memberNameaffix") .addSelect("member.nameaffix", "memberNameaffix")
.addSelect("member.birthdate", "memberBirthdate") .addSelect("member.birthdate", "memberBirthdate")
.addSelect("salutation.salutation", "memberSalutation")
.addSelect( .addSelect(
"SUM(TIMESTAMPDIFF(DAY, memberExecutivePositions.start, COALESCE(memberExecutivePositions.end, CURRENT_DATE)))", "SUM(TIMESTAMPDIFF(DAY, memberExecutivePositions.start, COALESCE(memberExecutivePositions.end, CURRENT_DATE)))",
"durationInDays" "durationInDays"
) )
.leftJoin("memberExecutivePositions.executivePosition", "executivePosition") .leftJoin("memberExecutivePositions.executivePosition", "executivePosition")
.leftJoin("memberExecutivePositions.member", "member") .leftJoin("memberExecutivePositions.member", "member")
.groupBy("executivePosition.id"), .leftJoin("member.salutation", "salutation")
.groupBy("executivePosition.id")
.addGroupBy("member.id"),
}) })
export class memberExecutivePositionsView { export class memberExecutivePositionsView {
@ViewColumn() @ViewColumn()
@ -37,7 +38,7 @@ export class memberExecutivePositionsView {
memberId: number; memberId: number;
@ViewColumn() @ViewColumn()
memberSalutation: Salutation; memberSalutation: string;
@ViewColumn() @ViewColumn()
memberFirstname: string; memberFirstname: string;

View file

@ -1,6 +1,5 @@
import { DataSource, ViewColumn, ViewEntity } from "typeorm"; import { DataSource, ViewColumn, ViewEntity } from "typeorm";
import { memberQualifications } from "../entity/club/member/memberQualifications"; import { memberQualifications } from "../entity/club/member/memberQualifications";
import { Salutation } from "../enums/salutation";
@ViewEntity({ @ViewEntity({
expression: (datasource: DataSource) => expression: (datasource: DataSource) =>
@ -10,18 +9,20 @@ import { Salutation } from "../enums/salutation";
.select("qualification.id", "qualificationId") .select("qualification.id", "qualificationId")
.addSelect("qualification.qualification", "qualification") .addSelect("qualification.qualification", "qualification")
.addSelect("member.id", "memberId") .addSelect("member.id", "memberId")
.addSelect("member.salutation", "memberSalutation")
.addSelect("member.firstname", "memberFirstname") .addSelect("member.firstname", "memberFirstname")
.addSelect("member.lastname", "memberLastname") .addSelect("member.lastname", "memberLastname")
.addSelect("member.nameaffix", "memberNameaffix") .addSelect("member.nameaffix", "memberNameaffix")
.addSelect("member.birthdate", "memberBirthdate") .addSelect("member.birthdate", "memberBirthdate")
.addSelect("salutation.salutation", "memberSalutation")
.addSelect( .addSelect(
"SUM(TIMESTAMPDIFF(DAY, memberQualifications.start, COALESCE(memberQualifications.end, CURRENT_DATE)))", "SUM(TIMESTAMPDIFF(DAY, memberQualifications.start, COALESCE(memberQualifications.end, CURRENT_DATE)))",
"durationInDays" "durationInDays"
) )
.leftJoin("memberQualifications.qualification", "qualification") .leftJoin("memberQualifications.qualification", "qualification")
.leftJoin("memberQualifications.member", "member") .leftJoin("memberQualifications.member", "member")
.groupBy("qualification.id"), .leftJoin("member.salutation", "salutation")
.groupBy("qualification.id")
.addGroupBy("member.id"),
}) })
export class memberQualificationsView { export class memberQualificationsView {
@ViewColumn() @ViewColumn()
@ -37,7 +38,7 @@ export class memberQualificationsView {
memberId: number; memberId: number;
@ViewColumn() @ViewColumn()
memberSalutation: Salutation; memberSalutation: string;
@ViewColumn() @ViewColumn()
memberFirstname: string; memberFirstname: string;

View file

@ -1,6 +1,5 @@
import { DataSource, ViewColumn, ViewEntity } from "typeorm"; import { DataSource, ViewColumn, ViewEntity } from "typeorm";
import { member } from "../entity/club/member/member"; import { member } from "../entity/club/member/member";
import { Salutation } from "../enums/salutation";
@ViewEntity({ @ViewEntity({
expression: (datasource: DataSource) => expression: (datasource: DataSource) =>
@ -8,21 +7,22 @@ import { Salutation } from "../enums/salutation";
.getRepository(member) .getRepository(member)
.createQueryBuilder("member") .createQueryBuilder("member")
.select("member.id", "id") .select("member.id", "id")
.addSelect("member.salutation", "salutation")
.addSelect("member.firstname", "firstname") .addSelect("member.firstname", "firstname")
.addSelect("member.lastname", "lastname") .addSelect("member.lastname", "lastname")
.addSelect("member.nameaffix", "nameaffix") .addSelect("member.nameaffix", "nameaffix")
.addSelect("member.birthdate", "birthdate") .addSelect("member.birthdate", "birthdate")
.addSelect("salutation.salutation", "salutation")
.addSelect("TIMESTAMPDIFF(YEAR, member.birthdate, CURDATE())", "todayAge") .addSelect("TIMESTAMPDIFF(YEAR, member.birthdate, CURDATE())", "todayAge")
.addSelect("YEAR(CURDATE()) - YEAR(member.birthdate)", "ageThisYear") .addSelect("YEAR(CURDATE()) - YEAR(member.birthdate)", "ageThisYear")
.addSelect("CONCAT('_', FROM_DAYS(TIMESTAMPDIFF(DAY, member.birthdate, CURDATE())))", "exactAge"), .addSelect("CONCAT('_', FROM_DAYS(TIMESTAMPDIFF(DAY, member.birthdate, CURDATE())))", "exactAge")
.leftJoin("member.salutation", "salutation"),
}) })
export class memberView { export class memberView {
@ViewColumn() @ViewColumn()
id: number; id: number;
@ViewColumn() @ViewColumn()
salutation: Salutation; salutation: string;
@ViewColumn() @ViewColumn()
firstname: string; firstname: string;
@ -43,5 +43,5 @@ export class memberView {
ageThisYear: number; ageThisYear: number;
@ViewColumn() @ViewColumn()
exactAge: Date; exactAge: string;
} }

View file

@ -1,6 +1,5 @@
import { DataSource, ViewColumn, ViewEntity } from "typeorm"; import { DataSource, ViewColumn, ViewEntity } from "typeorm";
import { membership } from "../entity/club/member/membership"; import { membership } from "../entity/club/member/membership";
import { Salutation } from "../enums/salutation";
@ViewEntity({ @ViewEntity({
expression: (datasource: DataSource) => expression: (datasource: DataSource) =>
@ -10,11 +9,11 @@ import { Salutation } from "../enums/salutation";
.select("status.id", "statusId") .select("status.id", "statusId")
.addSelect("status.status", "status") .addSelect("status.status", "status")
.addSelect("member.id", "memberId") .addSelect("member.id", "memberId")
.addSelect("member.salutation", "memberSalutation")
.addSelect("member.firstname", "memberFirstname") .addSelect("member.firstname", "memberFirstname")
.addSelect("member.lastname", "memberLastname") .addSelect("member.lastname", "memberLastname")
.addSelect("member.nameaffix", "memberNameaffix") .addSelect("member.nameaffix", "memberNameaffix")
.addSelect("member.birthdate", "memberBirthdate") .addSelect("member.birthdate", "memberBirthdate")
.addSelect("salutation.salutation", "memberSalutation")
.addSelect("SUM(TIMESTAMPDIFF(DAY, membership.start, COALESCE(membership.end, CURRENT_DATE)))", "durationInDays") .addSelect("SUM(TIMESTAMPDIFF(DAY, membership.start, COALESCE(membership.end, CURRENT_DATE)))", "durationInDays")
.addSelect( .addSelect(
"CONCAT('_', FROM_DAYS(SUM(TIMESTAMPDIFF(DAY, membership.start, COALESCE(membership.end, CURRENT_DATE)))))", "CONCAT('_', FROM_DAYS(SUM(TIMESTAMPDIFF(DAY, membership.start, COALESCE(membership.end, CURRENT_DATE)))))",
@ -22,6 +21,7 @@ import { Salutation } from "../enums/salutation";
) )
.leftJoin("membership.status", "status") .leftJoin("membership.status", "status")
.leftJoin("membership.member", "member") .leftJoin("membership.member", "member")
.leftJoin("member.salutation", "salutation")
.groupBy("status.id") .groupBy("status.id")
.addGroupBy("member.id"), .addGroupBy("member.id"),
}) })
@ -30,7 +30,7 @@ export class membershipView {
durationInDays: number; durationInDays: number;
@ViewColumn() @ViewColumn()
durationInYears: Date; durationInYears: string;
@ViewColumn() @ViewColumn()
status: string; status: string;
@ -42,7 +42,7 @@ export class membershipView {
memberId: number; memberId: number;
@ViewColumn() @ViewColumn()
memberSalutation: Salutation; memberSalutation: string;
@ViewColumn() @ViewColumn()
memberFirstname: string; memberFirstname: string;