#5-intelligent-groups #23
27 changed files with 285 additions and 74 deletions
|
@ -17,7 +17,6 @@ WORKDIR /app
|
||||||
COPY --from=build /app/dist /app/dist
|
COPY --from=build /app/dist /app/dist
|
||||||
COPY --from=build /app/node_modules /app/node_modules
|
COPY --from=build /app/node_modules /app/node_modules
|
||||||
COPY --from=build /app/package.json /app/package.json
|
COPY --from=build /app/package.json /app/package.json
|
||||||
COPY --from=build /app/.env /app/.env
|
|
||||||
|
|
||||||
EXPOSE 5000
|
EXPOSE 5000
|
||||||
|
|
||||||
|
|
95
README.md
95
README.md
|
@ -1,30 +1,87 @@
|
||||||
# member-administration-server
|
# member-administration-server
|
||||||
|
|
||||||
Memberadministration
|
Mitgliederverwaltung für Feuerwehren und Vereine (Backend).
|
||||||
|
|
||||||
Authentications is realized via JWT-Tokens. The server is able to send Mails to the members.
|
## Einleitung
|
||||||
Login is possible via Username and TOTP.
|
|
||||||
|
Dieses Projekt, `member-administration-server`, ist das Backend zur Verwaltung von Mitgliederdaten. Die zugehörige Webapp ist im Repository [member-administration-ui](https://forgejo.jk-effects.cloud/Ehrenamt/member-administration-ui) zu finden.
|
||||||
|
|
||||||
|
Eine Demo zusammen mit der `member-administration-ui` finden Sie unter [ff-admin-demo.jk-effects.cloud](ff-admin-demo.jk-effects.cloud).
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Requirements
|
### Docker Compose Setup
|
||||||
|
|
||||||
1. MySql Database
|
Um den Container hochzufahren, erstellen Sie eine `docker-compose.yml` Datei mit folgendem Inhalt:
|
||||||
2. Access to the internet for sending Mails
|
|
||||||
|
|
||||||
### Configuration
|
```yaml
|
||||||
|
version: "3"
|
||||||
|
|
||||||
1. Copy the .env.example file to .env and fill in the required information
|
services:
|
||||||
2. Create a new Database in MySql named as in the .env file
|
ff-member-administration-server:
|
||||||
3. Install all packages via `npm install`
|
image: docker.registry.jk-effects.cloud/ehrenamt/member-administration/server:latest
|
||||||
4. Start the application to create the database schema
|
container_name: ff_member_administration_server
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- DB_TYPE = mysql
|
||||||
|
- DB_HOST=ffm-db
|
||||||
|
- DB_NAME=administration
|
||||||
|
- DB_USERNAME=administration_backend
|
||||||
|
- DB_PASSWORD=<dbuserpasswd>
|
||||||
|
- JWT_SECRET=<tobemodified>
|
||||||
|
- JWT_EXPIRATION=<number[m|d] - bsp.:15m>
|
||||||
|
- REFRESH_EXPIRATION=<number[m|d] - bsp.:1d>
|
||||||
|
- MAIL_USERNAME=<mailadress|username>
|
||||||
|
- MAIL_PASSWORD=<password>
|
||||||
|
- MAIL_HOST=<url>
|
||||||
|
- MAIL_PORT=<port>
|
||||||
|
- MAIL_SECURE=<boolean>
|
||||||
|
- CLUB_NAME=<tobemodified>
|
||||||
|
volumes:
|
||||||
|
- <volume|local path>:/app/export
|
||||||
|
networks:
|
||||||
|
- ff_internal
|
||||||
|
depends_on:
|
||||||
|
- ff-db
|
||||||
|
|
||||||
## Testing
|
ff-db:
|
||||||
|
image: mariadb:11.2
|
||||||
|
container_name: ff_db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- MYSQL_DATABASE=ffadmin
|
||||||
|
- MYSQL_USER=administration_backend
|
||||||
|
- MYSQL_PASSWORD=<dbuserpasswd>
|
||||||
|
- MYSQL_ROOT_PASSWORD=<dbrootpasswd>
|
||||||
|
volumes:
|
||||||
|
- <volume|local path>:/var/lib/mysql
|
||||||
|
networks:
|
||||||
|
- ff_internal
|
||||||
|
|
||||||
1. Install the database-system-package you like (e.g. mysql, mariadb, postgresql, sqlite3)
|
networks:
|
||||||
2. Configure type inside src/data-source.ts to run the database-system you like.
|
ff_internal:
|
||||||
3. Set migrationsRun to false and synchronize to true for rapid prototyping
|
```
|
||||||
4. Building the schema via CLI:
|
|
||||||
- Run `npm run update-database` to build the schema using the migrations without starting the application
|
Führen Sie dann den folgenden Befehl im Verzeichnis der compose-Datei aus, um den Container zu starten:
|
||||||
- Run `npm run synchronize-database` to build the schema without using migrations without starting the application
|
|
||||||
5. Run `npm run dev` to run inside dev-environment (runs migrations if migrationsRun is set to true)
|
```sh
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manuelle Installation
|
||||||
|
|
||||||
|
Klonen Sie dieses Repository und installieren Sie die Abhängigkeiten:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://forgejo.jk-effects.cloud/Ehrenamt/member-administration-server.git
|
||||||
|
cd member-administration-server
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fragen und Wünsche
|
||||||
|
|
||||||
|
Bei Fragen, Anregungen oder Wünschen können Sie sich gerne melden.\
|
||||||
|
Wir freuen uns über Ihr Feedback und helfen Ihnen gerne weiter.\
|
||||||
|
Schreiben Sie dafür eine Mail an julian.krauser@jk-effects.com.
|
||||||
|
|
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "member-administration-server",
|
"name": "member-administration-server",
|
||||||
"version": "0.0.7",
|
"version": "0.0.9",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "member-administration-server",
|
"name": "member-administration-server",
|
||||||
"version": "0.0.7",
|
"version": "0.0.9",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "member-administration-server",
|
"name": "member-administration-server",
|
||||||
"version": "0.0.7",
|
"version": "0.0.9",
|
||||||
"description": "Feuerwehr/Verein Mitgliederverwaltung Server",
|
"description": "Feuerwehr/Verein Mitgliederverwaltung Server",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -2,6 +2,7 @@ export interface CreateCalendarTypeCommand {
|
||||||
type: string;
|
type: string;
|
||||||
nscdr: boolean;
|
nscdr: boolean;
|
||||||
color: string;
|
color: string;
|
||||||
|
passphrase?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateCalendarTypeCommand {
|
export interface UpdateCalendarTypeCommand {
|
||||||
|
@ -9,6 +10,7 @@ export interface UpdateCalendarTypeCommand {
|
||||||
type: string;
|
type: string;
|
||||||
nscdr: boolean;
|
nscdr: boolean;
|
||||||
color: string;
|
color: string;
|
||||||
|
passphrase?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeleteCalendarTypeCommand {
|
export interface DeleteCalendarTypeCommand {
|
||||||
|
|
|
@ -18,6 +18,7 @@ export default abstract class CalendarTypeCommandHandler {
|
||||||
type: createCalendarType.type,
|
type: createCalendarType.type,
|
||||||
nscdr: createCalendarType.nscdr,
|
nscdr: createCalendarType.nscdr,
|
||||||
color: createCalendarType.color,
|
color: createCalendarType.color,
|
||||||
|
passphrase: createCalendarType.nscdr ? null : createCalendarType.passphrase,
|
||||||
})
|
})
|
||||||
.execute()
|
.execute()
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
@ -41,6 +42,7 @@ export default abstract class CalendarTypeCommandHandler {
|
||||||
type: updateCalendarType.type,
|
type: updateCalendarType.type,
|
||||||
nscdr: updateCalendarType.nscdr,
|
nscdr: updateCalendarType.nscdr,
|
||||||
color: updateCalendarType.color,
|
color: updateCalendarType.color,
|
||||||
|
passphrase: updateCalendarType.nscdr ? null : updateCalendarType.passphrase,
|
||||||
})
|
})
|
||||||
.where("id = :id", { id: updateCalendarType.id })
|
.where("id = :id", { id: updateCalendarType.id })
|
||||||
.execute()
|
.execute()
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export interface CreateCommunicationCommand {
|
export interface CreateCommunicationCommand {
|
||||||
preferred: boolean;
|
preferred: boolean;
|
||||||
|
isSMSAlarming: boolean;
|
||||||
mobile: string;
|
mobile: string;
|
||||||
email: string;
|
email: string;
|
||||||
city: string;
|
city: string;
|
||||||
|
@ -13,6 +14,7 @@ export interface CreateCommunicationCommand {
|
||||||
export interface UpdateCommunicationCommand {
|
export interface UpdateCommunicationCommand {
|
||||||
id: number;
|
id: number;
|
||||||
preferred: boolean;
|
preferred: boolean;
|
||||||
|
isSMSAlarming: boolean;
|
||||||
mobile: string;
|
mobile: string;
|
||||||
email: string;
|
email: string;
|
||||||
city: string;
|
city: string;
|
||||||
|
|
|
@ -22,6 +22,7 @@ export default abstract class CommunicationCommandHandler {
|
||||||
.into(communication)
|
.into(communication)
|
||||||
.values({
|
.values({
|
||||||
preferred: createCommunication.preferred,
|
preferred: createCommunication.preferred,
|
||||||
|
isSMSAlarming: createCommunication.isSMSAlarming,
|
||||||
mobile: createCommunication.mobile,
|
mobile: createCommunication.mobile,
|
||||||
email: createCommunication.email,
|
email: createCommunication.email,
|
||||||
city: createCommunication.city,
|
city: createCommunication.city,
|
||||||
|
@ -59,6 +60,7 @@ export default abstract class CommunicationCommandHandler {
|
||||||
.update(communication)
|
.update(communication)
|
||||||
.set({
|
.set({
|
||||||
preferred: updateCommunication.preferred,
|
preferred: updateCommunication.preferred,
|
||||||
|
isSMSAlarming: updateCommunication.isSMSAlarming,
|
||||||
mobile: updateCommunication.mobile,
|
mobile: updateCommunication.mobile,
|
||||||
email: updateCommunication.email,
|
email: updateCommunication.email,
|
||||||
city: updateCommunication.city,
|
city: updateCommunication.city,
|
||||||
|
|
|
@ -68,7 +68,6 @@ export default abstract class MemberCommandHandler {
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
static async updateNewsletter(updateMember: UpdateMemberNewsletterCommand): Promise<void> {
|
static async updateNewsletter(updateMember: UpdateMemberNewsletterCommand): Promise<void> {
|
||||||
console.log(updateMember);
|
|
||||||
return await dataSource
|
return await dataSource
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.update(member)
|
.update(member)
|
||||||
|
@ -88,6 +87,26 @@ export default abstract class MemberCommandHandler {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description update member newsletter to unset
|
||||||
|
* @param memberId string
|
||||||
|
* @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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description delete member
|
* @description delete member
|
||||||
* @param DeleteMemberCommand
|
* @param DeleteMemberCommand
|
||||||
|
|
|
@ -101,11 +101,13 @@ export async function createCalendarType(req: Request, res: Response): Promise<a
|
||||||
const type = req.body.type;
|
const type = req.body.type;
|
||||||
const nscdr = req.body.nscdr;
|
const nscdr = req.body.nscdr;
|
||||||
const color = req.body.color;
|
const color = req.body.color;
|
||||||
|
const passphrase = req.body.passphrase;
|
||||||
|
|
||||||
let createType: CreateCalendarTypeCommand = {
|
let createType: CreateCalendarTypeCommand = {
|
||||||
type,
|
type,
|
||||||
nscdr,
|
nscdr,
|
||||||
color,
|
color,
|
||||||
|
passphrase,
|
||||||
};
|
};
|
||||||
let id = await CalendarTypeCommandHandler.create(createType);
|
let id = await CalendarTypeCommandHandler.create(createType);
|
||||||
|
|
||||||
|
@ -154,12 +156,14 @@ export async function updateCalendarType(req: Request, res: Response): Promise<a
|
||||||
const type = req.body.type;
|
const type = req.body.type;
|
||||||
const nscdr = req.body.nscdr;
|
const nscdr = req.body.nscdr;
|
||||||
const color = req.body.color;
|
const color = req.body.color;
|
||||||
|
const passphrase = req.body.passphrase;
|
||||||
|
|
||||||
let updateType: UpdateCalendarTypeCommand = {
|
let updateType: UpdateCalendarTypeCommand = {
|
||||||
id,
|
id,
|
||||||
type,
|
type,
|
||||||
nscdr,
|
nscdr,
|
||||||
color,
|
color,
|
||||||
|
passphrase,
|
||||||
};
|
};
|
||||||
await CalendarTypeCommandHandler.update(updateType);
|
await CalendarTypeCommandHandler.update(updateType);
|
||||||
|
|
||||||
|
|
|
@ -346,6 +346,7 @@ export async function addExecutivePositionToMember(req: Request, res: Response):
|
||||||
export async function addCommunicationToMember(req: Request, res: Response): Promise<any> {
|
export async function addCommunicationToMember(req: Request, res: Response): Promise<any> {
|
||||||
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 mobile = req.body.mobile;
|
const mobile = req.body.mobile;
|
||||||
const email = req.body.email;
|
const email = req.body.email;
|
||||||
const city = req.body.city;
|
const city = req.body.city;
|
||||||
|
@ -357,6 +358,7 @@ export async function addCommunicationToMember(req: Request, res: Response): Pro
|
||||||
|
|
||||||
let createCommunication: CreateCommunicationCommand = {
|
let createCommunication: CreateCommunicationCommand = {
|
||||||
preferred,
|
preferred,
|
||||||
|
isSMSAlarming,
|
||||||
mobile,
|
mobile,
|
||||||
email,
|
email,
|
||||||
city,
|
city,
|
||||||
|
@ -528,6 +530,7 @@ export async function updateCommunicationOfMember(req: Request, res: Response):
|
||||||
const memberId = parseInt(req.params.memberId);
|
const memberId = parseInt(req.params.memberId);
|
||||||
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 mobile = req.body.mobile;
|
const mobile = req.body.mobile;
|
||||||
const email = req.body.email;
|
const email = req.body.email;
|
||||||
const city = req.body.city;
|
const city = req.body.city;
|
||||||
|
@ -540,6 +543,7 @@ export async function updateCommunicationOfMember(req: Request, res: Response):
|
||||||
let updateCommunication: UpdateCommunicationCommand = {
|
let updateCommunication: UpdateCommunicationCommand = {
|
||||||
id: recordId,
|
id: recordId,
|
||||||
preferred,
|
preferred,
|
||||||
|
isSMSAlarming,
|
||||||
mobile,
|
mobile,
|
||||||
email,
|
email,
|
||||||
city,
|
city,
|
||||||
|
@ -550,12 +554,16 @@ export async function updateCommunicationOfMember(req: Request, res: Response):
|
||||||
};
|
};
|
||||||
await CommunicationCommandHandler.update(updateCommunication);
|
await CommunicationCommandHandler.update(updateCommunication);
|
||||||
|
|
||||||
|
let currentUserNewsletterMain = await MemberService.getNewsletterById(memberId);
|
||||||
|
|
||||||
if (isNewsletterMain) {
|
if (isNewsletterMain) {
|
||||||
let updateNewsletter: UpdateMemberNewsletterCommand = {
|
let updateNewsletter: UpdateMemberNewsletterCommand = {
|
||||||
id: memberId,
|
id: memberId,
|
||||||
communicationId: recordId,
|
communicationId: recordId,
|
||||||
};
|
};
|
||||||
await MemberCommandHandler.updateNewsletter(updateNewsletter);
|
await MemberCommandHandler.updateNewsletter(updateNewsletter);
|
||||||
|
} else if (currentUserNewsletterMain.sendNewsletter.id == recordId) {
|
||||||
|
await MemberCommandHandler.unsetNewsletter(memberId);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.sendStatus(204);
|
res.sendStatus(204);
|
||||||
|
|
|
@ -4,63 +4,96 @@ import CalendarTypeService from "../service/calendarTypeService";
|
||||||
import { calendar } from "../entity/calendar";
|
import { calendar } from "../entity/calendar";
|
||||||
import { createEvents } from "ics";
|
import { createEvents } from "ics";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
import InternalException from "../exceptions/internalException";
|
||||||
|
import CalendarFactory from "../factory/admin/calendar";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description get all calendar items by types or nscdr
|
* @description get all calendar items by types or nscdr
|
||||||
|
* @summary passphrase is passed as value pair like `type:passphrase`
|
||||||
* @param req {Request} Express req object
|
* @param req {Request} Express req object
|
||||||
* @param res {Response} Express res object
|
* @param res {Response} Express res object
|
||||||
* @returns {Promise<*>}
|
* @returns {Promise<*>}
|
||||||
*/
|
*/
|
||||||
export async function getCalendarItemsByTypes(req: Request, res: Response): Promise<any> {
|
export async function getCalendarItemsByTypes(req: Request, res: Response): Promise<any> {
|
||||||
let types = Array.isArray(req.query.types) ? req.query.types : [req.query.types];
|
let types = Array.isArray(req.query.types) ? req.query.types : [req.query.types];
|
||||||
|
let output = (req.query.output as "ics" | "json") ?? "ics";
|
||||||
|
|
||||||
|
if (output != "ics" && output != "json") {
|
||||||
|
throw new InternalException("set output query value to `ics` or `json` (defaults to `ics`)");
|
||||||
|
}
|
||||||
|
|
||||||
|
types = types.filter((t) => t);
|
||||||
|
|
||||||
let items: Array<calendar> = [];
|
let items: Array<calendar> = [];
|
||||||
if (types.length == 0) {
|
if (types.length != 0) {
|
||||||
let typeIds = await CalendarTypeService.getByTypes(types as Array<string>);
|
let typeIds = await CalendarTypeService.getByTypes((types as Array<string>).map((t) => t.split(":")[0]));
|
||||||
|
typeIds = typeIds.filter(
|
||||||
|
(ti) =>
|
||||||
|
ti.passphrase == null ||
|
||||||
|
ti.passphrase == "" ||
|
||||||
|
ti.passphrase == (types as Array<string>).find((t) => t.includes(ti.type)).split(":")[1]
|
||||||
|
);
|
||||||
items = await CalendarService.getByTypes(typeIds.map((t) => t.id));
|
items = await CalendarService.getByTypes(typeIds.map((t) => t.id));
|
||||||
} else {
|
} else {
|
||||||
items = await CalendarService.getByTypeNSCDR();
|
items = await CalendarService.getByTypeNSCDR();
|
||||||
}
|
}
|
||||||
|
|
||||||
let events = createEvents(
|
if (output == "json") {
|
||||||
items.map((i) => ({
|
res.json(CalendarFactory.mapToBase(items));
|
||||||
calName: process.env.CLUB_NAME,
|
} else {
|
||||||
uid: i.id,
|
let events = createEvents(
|
||||||
sequence: 1,
|
items.map((i) => ({
|
||||||
start: moment(i.starttime)
|
calName: process.env.CLUB_NAME,
|
||||||
.format("YYYY-M-D-H-m")
|
uid: i.id,
|
||||||
.split("-")
|
sequence: 1,
|
||||||
.map((a) => parseInt(a)) as [number, number, number, number, number],
|
...(i.allDay
|
||||||
end: moment(i.endtime)
|
? {
|
||||||
.format("YYYY-M-D-H-m")
|
start: moment(i.starttime)
|
||||||
.split("-")
|
.format("YYYY-M-D")
|
||||||
.map((a) => parseInt(a)) as [number, number, number, number, number],
|
.split("-")
|
||||||
title: i.title,
|
.map((a) => parseInt(a)) as [number, number, number],
|
||||||
description: i.content,
|
end: moment(i.endtime)
|
||||||
location: i.location,
|
.format("YYYY-M-D")
|
||||||
categories: [i.type.type],
|
.split("-")
|
||||||
created: moment(i.createdAt)
|
.map((a) => parseInt(a)) as [number, number, number],
|
||||||
.format("YYYY-M-D-H-m")
|
}
|
||||||
.split("-")
|
: {
|
||||||
.map((a) => parseInt(a)) as [number, number, number, number, number],
|
start: moment(i.starttime)
|
||||||
lastModified: moment(i.updatedAt)
|
.format("YYYY-M-D-H-m")
|
||||||
.format("YYYY-M-D-H-m")
|
.split("-")
|
||||||
.split("-")
|
.map((a) => parseInt(a)) as [number, number, number, number, number],
|
||||||
.map((a) => parseInt(a)) as [number, number, number, number, number],
|
end: moment(i.endtime)
|
||||||
transp: "OPAQUE" as "OPAQUE",
|
.format("YYYY-M-D-H-m")
|
||||||
url: "https://www.ff-merching.de",
|
.split("-")
|
||||||
alarms: [
|
.map((a) => parseInt(a)) as [number, number, number, number, number],
|
||||||
{
|
}),
|
||||||
action: "display",
|
title: i.title,
|
||||||
description: "Erinnerung",
|
description: i.content,
|
||||||
trigger: {
|
location: i.location,
|
||||||
minutes: 30,
|
categories: [i.type.type],
|
||||||
before: true,
|
created: moment(i.createdAt)
|
||||||
|
.format("YYYY-M-D-H-m")
|
||||||
|
.split("-")
|
||||||
|
.map((a) => parseInt(a)) as [number, number, number, number, number],
|
||||||
|
lastModified: moment(i.updatedAt)
|
||||||
|
.format("YYYY-M-D-H-m")
|
||||||
|
.split("-")
|
||||||
|
.map((a) => parseInt(a)) as [number, number, number, number, number],
|
||||||
|
transp: "OPAQUE" as "OPAQUE",
|
||||||
|
url: "https://www.ff-merching.de",
|
||||||
|
alarms: [
|
||||||
|
{
|
||||||
|
action: "display",
|
||||||
|
description: "Erinnerung",
|
||||||
|
trigger: {
|
||||||
|
minutes: 30,
|
||||||
|
before: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
],
|
}))
|
||||||
}))
|
);
|
||||||
);
|
|
||||||
|
|
||||||
res.type("ics").send(events.value);
|
res.type("ics").send(events.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,8 @@ import { calendarType } from "./entity/calendarType";
|
||||||
import { Calendar1729947763295 } from "./migrations/1729947763295-calendar";
|
import { Calendar1729947763295 } from "./migrations/1729947763295-calendar";
|
||||||
import { reset } from "./entity/reset";
|
import { reset } from "./entity/reset";
|
||||||
import { ResetToken1732358596823 } from "./migrations/1732358596823-resetToken";
|
import { ResetToken1732358596823 } from "./migrations/1732358596823-resetToken";
|
||||||
|
import { SMSAlarming1732696919191 } from "./migrations/1732696919191-SMSAlarming";
|
||||||
|
import { SecuringCalendarType1733249553766 } from "./migrations/1733249553766-securingCalendarType";
|
||||||
|
|
||||||
const dataSource = new DataSource({
|
const dataSource = new DataSource({
|
||||||
type: DB_TYPE as any,
|
type: DB_TYPE as any,
|
||||||
|
@ -94,6 +96,8 @@ const dataSource = new DataSource({
|
||||||
Protocol1729347911107,
|
Protocol1729347911107,
|
||||||
Calendar1729947763295,
|
Calendar1729947763295,
|
||||||
ResetToken1732358596823,
|
ResetToken1732358596823,
|
||||||
|
SMSAlarming1732696919191,
|
||||||
|
SecuringCalendarType1733249553766,
|
||||||
],
|
],
|
||||||
migrationsRun: true,
|
migrationsRun: true,
|
||||||
migrationsTransactionMode: "each",
|
migrationsTransactionMode: "each",
|
||||||
|
|
|
@ -15,6 +15,9 @@ export class calendarType {
|
||||||
@Column({ type: "varchar", length: 255 })
|
@Column({ type: "varchar", length: 255 })
|
||||||
color: string;
|
color: string;
|
||||||
|
|
||||||
|
@Column({ type: "varchar", length: 255, nullable: true, default: null })
|
||||||
|
passphrase: string | null;
|
||||||
|
|
||||||
@OneToMany(() => calendar, (c) => c.type, {
|
@OneToMany(() => calendar, (c) => c.type, {
|
||||||
nullable: false,
|
nullable: false,
|
||||||
onDelete: "RESTRICT",
|
onDelete: "RESTRICT",
|
||||||
|
|
|
@ -10,6 +10,9 @@ export class communication {
|
||||||
@Column({ type: "boolean", default: false })
|
@Column({ type: "boolean", default: false })
|
||||||
preferred: boolean;
|
preferred: boolean;
|
||||||
|
|
||||||
|
@Column({ type: "boolean", default: false })
|
||||||
|
isSMSAlarming: boolean;
|
||||||
|
|
||||||
@Column({ type: "varchar", length: 255, nullable: true })
|
@Column({ type: "varchar", length: 255, nullable: true })
|
||||||
mobile: string;
|
mobile: string;
|
||||||
|
|
||||||
|
|
|
@ -65,4 +65,5 @@ export class member {
|
||||||
firstMembershipEntry?: membership;
|
firstMembershipEntry?: membership;
|
||||||
lastMembershipEntry?: membership;
|
lastMembershipEntry?: membership;
|
||||||
preferredCommunication?: Array<communication>;
|
preferredCommunication?: Array<communication>;
|
||||||
|
smsAlarming?: Array<communication>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ export default abstract class CalendarTypeFactory {
|
||||||
type: record.type,
|
type: record.type,
|
||||||
nscdr: record.nscdr,
|
nscdr: record.nscdr,
|
||||||
color: record.color,
|
color: record.color,
|
||||||
|
passphrase: record.passphrase,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ export default abstract class CommunicationFactory {
|
||||||
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: isMain ? isMain : record?.member?.sendNewsletter?.id == record.id,
|
||||||
|
isSMSAlarming: record.isSMSAlarming,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ export default abstract class MemberFactory {
|
||||||
preferredCommunication: record?.preferredCommunication
|
preferredCommunication: record?.preferredCommunication
|
||||||
? CommunicationFactory.mapToBase(record.preferredCommunication)
|
? CommunicationFactory.mapToBase(record.preferredCommunication)
|
||||||
: null,
|
: null,
|
||||||
|
smsAlarming: record?.smsAlarming ? CommunicationFactory.mapToBase(record.smsAlarming) : null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
21
src/migrations/1732696919191-SMSAlarming.ts
Normal file
21
src/migrations/1732696919191-SMSAlarming.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
|
||||||
|
|
||||||
|
export class SMSAlarming1732696919191 implements MigrationInterface {
|
||||||
|
name = "SMSAlarming1732696919191";
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.addColumn(
|
||||||
|
"communication",
|
||||||
|
new TableColumn({
|
||||||
|
name: "isSMSAlarming",
|
||||||
|
type: "tinyint",
|
||||||
|
default: 0,
|
||||||
|
isNullable: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropColumn("communication", "isSMSAlarming");
|
||||||
|
}
|
||||||
|
}
|
20
src/migrations/1733249553766-securingCalendarType.ts
Normal file
20
src/migrations/1733249553766-securingCalendarType.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
|
||||||
|
|
||||||
|
export class SecuringCalendarType1733249553766 implements MigrationInterface {
|
||||||
|
name = "SecuringCalendarType1733249553766";
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.addColumns("calendar_type", [
|
||||||
|
new TableColumn({
|
||||||
|
name: "passphrase",
|
||||||
|
type: "varchar",
|
||||||
|
length: "255",
|
||||||
|
isNullable: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.dropColumn("calendar_type", "passphrase");
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,13 +25,13 @@ export default (app: Express) => {
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.options("*", cors());
|
app.options("*", cors());
|
||||||
|
|
||||||
app.use("/public", publicAvailable);
|
app.use("/api/public", publicAvailable);
|
||||||
app.use("/setup", allowSetup, setup);
|
app.use("/api/setup", allowSetup, setup);
|
||||||
app.use("/reset", reset);
|
app.use("/api/reset", reset);
|
||||||
app.use("/invite", invite);
|
app.use("/api/invite", invite);
|
||||||
app.use("/auth", auth);
|
app.use("/api/auth", auth);
|
||||||
app.use(authenticate);
|
app.use(authenticate);
|
||||||
app.use("/admin", admin);
|
app.use("/api/admin", admin);
|
||||||
app.use("/user", user);
|
app.use("/api/user", user);
|
||||||
app.use(errorHandler);
|
app.use(errorHandler);
|
||||||
};
|
};
|
||||||
|
|
|
@ -56,6 +56,6 @@ export default abstract class CommunicationService {
|
||||||
static getAvailableColumnsForCommunication(): Array<string> {
|
static getAvailableColumnsForCommunication(): Array<string> {
|
||||||
let metadata = dataSource.getMetadata(communication);
|
let metadata = dataSource.getMetadata(communication);
|
||||||
let columns = metadata.columns.map((c) => c.propertyName);
|
let columns = metadata.columns.map((c) => c.propertyName);
|
||||||
return columns.filter((c) => !["id", "preferred", "type", "member"].includes(c));
|
return columns.filter((c) => !["id", "preferred", "isSMSAlarming", "type", "member"].includes(c));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,8 @@ export default abstract class MemberService {
|
||||||
"preferredCommunication.preferred = 1"
|
"preferredCommunication.preferred = 1"
|
||||||
)
|
)
|
||||||
.leftJoinAndSelect("preferredCommunication.type", "communicationtype_preferred")
|
.leftJoinAndSelect("preferredCommunication.type", "communicationtype_preferred")
|
||||||
|
.leftJoinAndMapMany("member.smsAlarming", "member.communications", "smsAlarming", "smsAlarming.isSMSAlarming = 1")
|
||||||
|
.leftJoinAndSelect("smsAlarming.type", "communicationtype_smsAlarming")
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
.limit(count)
|
.limit(count)
|
||||||
.orderBy("member.lastname")
|
.orderBy("member.lastname")
|
||||||
|
@ -52,7 +54,7 @@ export default abstract class MemberService {
|
||||||
/**
|
/**
|
||||||
* @description get member by id
|
* @description get member by id
|
||||||
* @param {number} id
|
* @param {number} id
|
||||||
* @returns {Promise<Array<member>>}
|
* @returns {Promise<member>}
|
||||||
*/
|
*/
|
||||||
static async getById(id: number): Promise<member> {
|
static async getById(id: number): Promise<member> {
|
||||||
return await dataSource
|
return await dataSource
|
||||||
|
@ -80,6 +82,9 @@ export default abstract class MemberService {
|
||||||
"preferredCommunication",
|
"preferredCommunication",
|
||||||
"preferredCommunication.preferred = 1"
|
"preferredCommunication.preferred = 1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
.leftJoinAndMapMany("member.smsAlarming", "member.communications", "smsAlarming", "smsAlarming.isSMSAlarming = 1")
|
||||||
|
.leftJoinAndSelect("smsAlarming.type", "communicationtype_smsAlarming")
|
||||||
.leftJoinAndSelect("preferredCommunication.type", "communicationtype_preferred")
|
.leftJoinAndSelect("preferredCommunication.type", "communicationtype_preferred")
|
||||||
.where("member.id = :id", { id: id })
|
.where("member.id = :id", { id: id })
|
||||||
.getOneOrFail()
|
.getOneOrFail()
|
||||||
|
@ -90,4 +95,24 @@ export default abstract class MemberService {
|
||||||
throw new InternalException("member not found by id", err);
|
throw new InternalException("member not found by id", err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description get newsletter by member by id
|
||||||
|
* @param {number} id
|
||||||
|
* @returns {Promise<member>}
|
||||||
|
*/
|
||||||
|
static async getNewsletterById(id: number): Promise<member> {
|
||||||
|
return await dataSource
|
||||||
|
.getRepository(member)
|
||||||
|
.createQueryBuilder("member")
|
||||||
|
.leftJoinAndSelect("member.sendNewsletter", "sendNewsletter")
|
||||||
|
.where("member.id = :id", { id: id })
|
||||||
|
.getOneOrFail()
|
||||||
|
.then((res) => {
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
throw new InternalException("member not found by id", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,4 +3,5 @@ export interface CalendarTypeViewModel {
|
||||||
type: string;
|
type: string;
|
||||||
nscdr: boolean;
|
nscdr: boolean;
|
||||||
color: string;
|
color: string;
|
||||||
|
passphrase: string | null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,4 +11,5 @@ export interface CommunicationViewModel {
|
||||||
streetNumberAddition: string;
|
streetNumberAddition: string;
|
||||||
type: CommunicationTypeViewModel;
|
type: CommunicationTypeViewModel;
|
||||||
isNewsletterMain: boolean;
|
isNewsletterMain: boolean;
|
||||||
|
isSMSAlarming: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,5 +12,6 @@ export interface MemberViewModel {
|
||||||
firstMembershipEntry?: MembershipViewModel;
|
firstMembershipEntry?: MembershipViewModel;
|
||||||
lastMembershipEntry?: MembershipViewModel;
|
lastMembershipEntry?: MembershipViewModel;
|
||||||
sendNewsletter?: CommunicationViewModel;
|
sendNewsletter?: CommunicationViewModel;
|
||||||
|
smsAlarming?: Array<CommunicationViewModel>;
|
||||||
preferredCommunication?: Array<CommunicationViewModel>;
|
preferredCommunication?: Array<CommunicationViewModel>;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue