diff --git a/Dockerfile b/Dockerfile index 8d60e07..250b814 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,6 +17,7 @@ WORKDIR /app COPY --from=build /app/dist /app/dist COPY --from=build /app/node_modules /app/node_modules COPY --from=build /app/package.json /app/package.json +COPY --from=build /app/.env /app/.env EXPOSE 5000 diff --git a/README.md b/README.md index dc68a7b..08b682c 100644 --- a/README.md +++ b/README.md @@ -1,87 +1,30 @@ # member-administration-server -Mitgliederverwaltung für Feuerwehren und Vereine (Backend). +Memberadministration -## Einleitung - -Dieses Projekt, `member-administration-server`, ist das Backend zur Verwaltung von Mitgliederdaten. Die zugehörige Webapp ist im Repository [member-administration-ui](https://forgejo.jk-effects.cloud/Ehrenamt/member-administration-ui) zu finden. - -Eine Demo zusammen mit der `member-administration-ui` finden Sie unter [ff-admin-demo.jk-effects.cloud](ff-admin-demo.jk-effects.cloud). +Authentications is realized via JWT-Tokens. The server is able to send Mails to the members. +Login is possible via Username and TOTP. ## Installation -### Docker Compose Setup +### Requirements -Um den Container hochzufahren, erstellen Sie eine `docker-compose.yml` Datei mit folgendem Inhalt: +1. MySql Database +2. Access to the internet for sending Mails -```yaml -version: "3" +### Configuration -services: - ff-member-administration-server: - image: docker.registry.jk-effects.cloud/ehrenamt/member-administration/server:latest - 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= - - JWT_SECRET= - - JWT_EXPIRATION= - - REFRESH_EXPIRATION= - - MAIL_USERNAME= - - MAIL_PASSWORD= - - MAIL_HOST= - - MAIL_PORT= - - MAIL_SECURE= - - CLUB_NAME= - volumes: - - :/app/export - networks: - - ff_internal - depends_on: - - ff-db +1. Copy the .env.example file to .env and fill in the required information +2. Create a new Database in MySql named as in the .env file +3. Install all packages via `npm install` +4. Start the application to create the database schema - ff-db: - image: mariadb:11.2 - container_name: ff_db - restart: unless-stopped - environment: - - MYSQL_DATABASE=ffadmin - - MYSQL_USER=administration_backend - - MYSQL_PASSWORD= - - MYSQL_ROOT_PASSWORD= - volumes: - - :/var/lib/mysql - networks: - - ff_internal +## Testing -networks: - ff_internal: -``` - -Führen Sie dann den folgenden Befehl im Verzeichnis der compose-Datei aus, um den Container zu starten: - -```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. +1. Install the database-system-package you like (e.g. mysql, mariadb, postgresql, sqlite3) +2. Configure type inside src/data-source.ts to run the database-system you like. +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 + - 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) diff --git a/package-lock.json b/package-lock.json index c2cad47..75955bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "member-administration-server", - "version": "0.0.9", + "version": "0.0.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "member-administration-server", - "version": "0.0.9", + "version": "0.0.7", "license": "GPL-3.0-only", "dependencies": { "cors": "^2.8.5", diff --git a/package.json b/package.json index 8644e07..b73bff5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "member-administration-server", - "version": "0.0.9", + "version": "0.0.7", "description": "Feuerwehr/Verein Mitgliederverwaltung Server", "main": "dist/index.js", "scripts": { diff --git a/src/command/calendarTypeCommand.ts b/src/command/calendarTypeCommand.ts index d796058..fdab618 100644 --- a/src/command/calendarTypeCommand.ts +++ b/src/command/calendarTypeCommand.ts @@ -2,7 +2,6 @@ export interface CreateCalendarTypeCommand { type: string; nscdr: boolean; color: string; - passphrase?: string; } export interface UpdateCalendarTypeCommand { @@ -10,7 +9,6 @@ export interface UpdateCalendarTypeCommand { type: string; nscdr: boolean; color: string; - passphrase?: string; } export interface DeleteCalendarTypeCommand { diff --git a/src/command/calendarTypeCommandHandler.ts b/src/command/calendarTypeCommandHandler.ts index 223288c..0425425 100644 --- a/src/command/calendarTypeCommandHandler.ts +++ b/src/command/calendarTypeCommandHandler.ts @@ -18,7 +18,6 @@ export default abstract class CalendarTypeCommandHandler { type: createCalendarType.type, nscdr: createCalendarType.nscdr, color: createCalendarType.color, - passphrase: createCalendarType.nscdr ? null : createCalendarType.passphrase, }) .execute() .then((result) => { @@ -42,7 +41,6 @@ export default abstract class CalendarTypeCommandHandler { type: updateCalendarType.type, nscdr: updateCalendarType.nscdr, color: updateCalendarType.color, - passphrase: updateCalendarType.nscdr ? null : updateCalendarType.passphrase, }) .where("id = :id", { id: updateCalendarType.id }) .execute() diff --git a/src/command/communicationCommand.ts b/src/command/communicationCommand.ts index 6700110..1dfffd6 100644 --- a/src/command/communicationCommand.ts +++ b/src/command/communicationCommand.ts @@ -1,6 +1,5 @@ export interface CreateCommunicationCommand { preferred: boolean; - isSMSAlarming: boolean; mobile: string; email: string; city: string; @@ -14,7 +13,6 @@ export interface CreateCommunicationCommand { export interface UpdateCommunicationCommand { id: number; preferred: boolean; - isSMSAlarming: boolean; mobile: string; email: string; city: string; diff --git a/src/command/communicationCommandHandler.ts b/src/command/communicationCommandHandler.ts index c13bec6..4f2f801 100644 --- a/src/command/communicationCommandHandler.ts +++ b/src/command/communicationCommandHandler.ts @@ -22,7 +22,6 @@ export default abstract class CommunicationCommandHandler { .into(communication) .values({ preferred: createCommunication.preferred, - isSMSAlarming: createCommunication.isSMSAlarming, mobile: createCommunication.mobile, email: createCommunication.email, city: createCommunication.city, @@ -60,7 +59,6 @@ export default abstract class CommunicationCommandHandler { .update(communication) .set({ preferred: updateCommunication.preferred, - isSMSAlarming: updateCommunication.isSMSAlarming, mobile: updateCommunication.mobile, email: updateCommunication.email, city: updateCommunication.city, diff --git a/src/command/memberCommandHandler.ts b/src/command/memberCommandHandler.ts index 4c396cf..b37d89f 100644 --- a/src/command/memberCommandHandler.ts +++ b/src/command/memberCommandHandler.ts @@ -68,6 +68,7 @@ export default abstract class MemberCommandHandler { * @returns {Promise} */ static async updateNewsletter(updateMember: UpdateMemberNewsletterCommand): Promise { + console.log(updateMember); return await dataSource .createQueryBuilder() .update(member) @@ -87,26 +88,6 @@ export default abstract class MemberCommandHandler { }); } - /** - * @description update member newsletter to unset - * @param memberId string - * @returns {Promise} - */ - static async unsetNewsletter(memberId: number): Promise { - 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 * @param DeleteMemberCommand diff --git a/src/controller/admin/calendarController.ts b/src/controller/admin/calendarController.ts index f6a3bcd..0b319b1 100644 --- a/src/controller/admin/calendarController.ts +++ b/src/controller/admin/calendarController.ts @@ -101,13 +101,11 @@ export async function createCalendarType(req: Request, res: Response): Promise { const memberId = parseInt(req.params.memberId); const preferred = req.body.preferred; - const isSMSAlarming = req.body.isSMSAlarming; const mobile = req.body.mobile; const email = req.body.email; const city = req.body.city; @@ -358,7 +357,6 @@ export async function addCommunicationToMember(req: Request, res: Response): Pro let createCommunication: CreateCommunicationCommand = { preferred, - isSMSAlarming, mobile, email, city, @@ -530,7 +528,6 @@ export async function updateCommunicationOfMember(req: Request, res: Response): const memberId = parseInt(req.params.memberId); const recordId = parseInt(req.params.recordId); const preferred = req.body.preferred; - const isSMSAlarming = req.body.isSMSAlarming; const mobile = req.body.mobile; const email = req.body.email; const city = req.body.city; @@ -543,7 +540,6 @@ export async function updateCommunicationOfMember(req: Request, res: Response): let updateCommunication: UpdateCommunicationCommand = { id: recordId, preferred, - isSMSAlarming, mobile, email, city, @@ -554,16 +550,12 @@ export async function updateCommunicationOfMember(req: Request, res: Response): }; 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); diff --git a/src/controller/publicController.ts b/src/controller/publicController.ts index 8c627fc..ef675fa 100644 --- a/src/controller/publicController.ts +++ b/src/controller/publicController.ts @@ -4,96 +4,63 @@ import CalendarTypeService from "../service/calendarTypeService"; import { calendar } from "../entity/calendar"; import { createEvents } from "ics"; import moment from "moment"; -import InternalException from "../exceptions/internalException"; -import CalendarFactory from "../factory/admin/calendar"; /** * @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 res {Response} Express res object * @returns {Promise<*>} */ export async function getCalendarItemsByTypes(req: Request, res: Response): Promise { 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 = []; - if (types.length != 0) { - let typeIds = await CalendarTypeService.getByTypes((types as Array).map((t) => t.split(":")[0])); - typeIds = typeIds.filter( - (ti) => - ti.passphrase == null || - ti.passphrase == "" || - ti.passphrase == (types as Array).find((t) => t.includes(ti.type)).split(":")[1] - ); + if (types.length == 0) { + let typeIds = await CalendarTypeService.getByTypes(types as Array); items = await CalendarService.getByTypes(typeIds.map((t) => t.id)); } else { items = await CalendarService.getByTypeNSCDR(); } - if (output == "json") { - res.json(CalendarFactory.mapToBase(items)); - } else { - let events = createEvents( - items.map((i) => ({ - calName: process.env.CLUB_NAME, - uid: i.id, - sequence: 1, - ...(i.allDay - ? { - start: moment(i.starttime) - .format("YYYY-M-D") - .split("-") - .map((a) => parseInt(a)) as [number, number, number], - end: moment(i.endtime) - .format("YYYY-M-D") - .split("-") - .map((a) => parseInt(a)) as [number, number, number], - } - : { - start: moment(i.starttime) - .format("YYYY-M-D-H-m") - .split("-") - .map((a) => parseInt(a)) as [number, number, number, number, number], - end: moment(i.endtime) - .format("YYYY-M-D-H-m") - .split("-") - .map((a) => parseInt(a)) as [number, number, number, number, number], - }), - title: i.title, - description: i.content, - location: i.location, - categories: [i.type.type], - 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, - }, + let events = createEvents( + items.map((i) => ({ + calName: process.env.CLUB_NAME, + uid: i.id, + sequence: 1, + start: moment(i.starttime) + .format("YYYY-M-D-H-m") + .split("-") + .map((a) => parseInt(a)) as [number, number, number, number, number], + end: moment(i.endtime) + .format("YYYY-M-D-H-m") + .split("-") + .map((a) => parseInt(a)) as [number, number, number, number, number], + title: i.title, + description: i.content, + location: i.location, + categories: [i.type.type], + 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); } diff --git a/src/data-source.ts b/src/data-source.ts index 5271c7c..39a56c7 100644 --- a/src/data-source.ts +++ b/src/data-source.ts @@ -42,8 +42,6 @@ import { calendarType } from "./entity/calendarType"; import { Calendar1729947763295 } from "./migrations/1729947763295-calendar"; import { reset } from "./entity/reset"; import { ResetToken1732358596823 } from "./migrations/1732358596823-resetToken"; -import { SMSAlarming1732696919191 } from "./migrations/1732696919191-SMSAlarming"; -import { SecuringCalendarType1733249553766 } from "./migrations/1733249553766-securingCalendarType"; const dataSource = new DataSource({ type: DB_TYPE as any, @@ -96,8 +94,6 @@ const dataSource = new DataSource({ Protocol1729347911107, Calendar1729947763295, ResetToken1732358596823, - SMSAlarming1732696919191, - SecuringCalendarType1733249553766, ], migrationsRun: true, migrationsTransactionMode: "each", diff --git a/src/entity/calendarType.ts b/src/entity/calendarType.ts index 0214117..5f696cc 100644 --- a/src/entity/calendarType.ts +++ b/src/entity/calendarType.ts @@ -15,9 +15,6 @@ export class calendarType { @Column({ type: "varchar", length: 255 }) color: string; - @Column({ type: "varchar", length: 255, nullable: true, default: null }) - passphrase: string | null; - @OneToMany(() => calendar, (c) => c.type, { nullable: false, onDelete: "RESTRICT", diff --git a/src/entity/communication.ts b/src/entity/communication.ts index be006c1..dbf3983 100644 --- a/src/entity/communication.ts +++ b/src/entity/communication.ts @@ -10,9 +10,6 @@ export class communication { @Column({ type: "boolean", default: false }) preferred: boolean; - @Column({ type: "boolean", default: false }) - isSMSAlarming: boolean; - @Column({ type: "varchar", length: 255, nullable: true }) mobile: string; diff --git a/src/entity/member.ts b/src/entity/member.ts index 7bb0d78..d2be9b0 100644 --- a/src/entity/member.ts +++ b/src/entity/member.ts @@ -65,5 +65,4 @@ export class member { firstMembershipEntry?: membership; lastMembershipEntry?: membership; preferredCommunication?: Array; - smsAlarming?: Array; } diff --git a/src/factory/admin/calendarType.ts b/src/factory/admin/calendarType.ts index 25aed4e..d1f6850 100644 --- a/src/factory/admin/calendarType.ts +++ b/src/factory/admin/calendarType.ts @@ -13,7 +13,6 @@ export default abstract class CalendarTypeFactory { type: record.type, nscdr: record.nscdr, color: record.color, - passphrase: record.passphrase, }; } diff --git a/src/factory/admin/communication.ts b/src/factory/admin/communication.ts index 6fe454e..1333515 100644 --- a/src/factory/admin/communication.ts +++ b/src/factory/admin/communication.ts @@ -20,7 +20,6 @@ export default abstract class CommunicationFactory { streetNumberAddition: record.streetNumberAddition, type: CommunicationTypeFactory.mapToSingle(record.type), isNewsletterMain: isMain ? isMain : record?.member?.sendNewsletter?.id == record.id, - isSMSAlarming: record.isSMSAlarming, }; } diff --git a/src/factory/admin/member.ts b/src/factory/admin/member.ts index 44ddc70..0745d10 100644 --- a/src/factory/admin/member.ts +++ b/src/factory/admin/member.ts @@ -27,7 +27,6 @@ export default abstract class MemberFactory { preferredCommunication: record?.preferredCommunication ? CommunicationFactory.mapToBase(record.preferredCommunication) : null, - smsAlarming: record?.smsAlarming ? CommunicationFactory.mapToBase(record.smsAlarming) : null, }; } diff --git a/src/migrations/1732696919191-SMSAlarming.ts b/src/migrations/1732696919191-SMSAlarming.ts deleted file mode 100644 index 2ef756e..0000000 --- a/src/migrations/1732696919191-SMSAlarming.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { MigrationInterface, QueryRunner, TableColumn } from "typeorm"; - -export class SMSAlarming1732696919191 implements MigrationInterface { - name = "SMSAlarming1732696919191"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.addColumn( - "communication", - new TableColumn({ - name: "isSMSAlarming", - type: "tinyint", - default: 0, - isNullable: false, - }) - ); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropColumn("communication", "isSMSAlarming"); - } -} diff --git a/src/migrations/1733249553766-securingCalendarType.ts b/src/migrations/1733249553766-securingCalendarType.ts deleted file mode 100644 index 2b486de..0000000 --- a/src/migrations/1733249553766-securingCalendarType.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { MigrationInterface, QueryRunner, TableColumn } from "typeorm"; - -export class SecuringCalendarType1733249553766 implements MigrationInterface { - name = "SecuringCalendarType1733249553766"; - - public async up(queryRunner: QueryRunner): Promise { - await queryRunner.addColumns("calendar_type", [ - new TableColumn({ - name: "passphrase", - type: "varchar", - length: "255", - isNullable: true, - }), - ]); - } - - public async down(queryRunner: QueryRunner): Promise { - await queryRunner.dropColumn("calendar_type", "passphrase"); - } -} diff --git a/src/routes/index.ts b/src/routes/index.ts index 72cb526..eee3e7d 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -25,13 +25,13 @@ export default (app: Express) => { app.use(cors()); app.options("*", cors()); - app.use("/api/public", publicAvailable); - app.use("/api/setup", allowSetup, setup); - app.use("/api/reset", reset); - app.use("/api/invite", invite); - app.use("/api/auth", auth); + app.use("/public", publicAvailable); + app.use("/setup", allowSetup, setup); + app.use("/reset", reset); + app.use("/invite", invite); + app.use("/auth", auth); app.use(authenticate); - app.use("/api/admin", admin); - app.use("/api/user", user); + app.use("/admin", admin); + app.use("/user", user); app.use(errorHandler); }; diff --git a/src/service/communicationService.ts b/src/service/communicationService.ts index 03073fa..de86e49 100644 --- a/src/service/communicationService.ts +++ b/src/service/communicationService.ts @@ -56,6 +56,6 @@ export default abstract class CommunicationService { static getAvailableColumnsForCommunication(): Array { let metadata = dataSource.getMetadata(communication); let columns = metadata.columns.map((c) => c.propertyName); - return columns.filter((c) => !["id", "preferred", "isSMSAlarming", "type", "member"].includes(c)); + return columns.filter((c) => !["id", "preferred", "type", "member"].includes(c)); } } diff --git a/src/service/memberService.ts b/src/service/memberService.ts index 633497d..ae9e2d4 100644 --- a/src/service/memberService.ts +++ b/src/service/memberService.ts @@ -35,8 +35,6 @@ export default abstract class MemberService { "preferredCommunication.preferred = 1" ) .leftJoinAndSelect("preferredCommunication.type", "communicationtype_preferred") - .leftJoinAndMapMany("member.smsAlarming", "member.communications", "smsAlarming", "smsAlarming.isSMSAlarming = 1") - .leftJoinAndSelect("smsAlarming.type", "communicationtype_smsAlarming") .offset(offset) .limit(count) .orderBy("member.lastname") @@ -54,7 +52,7 @@ export default abstract class MemberService { /** * @description get member by id * @param {number} id - * @returns {Promise} + * @returns {Promise>} */ static async getById(id: number): Promise { return await dataSource @@ -82,9 +80,6 @@ export default abstract class MemberService { "preferredCommunication", "preferredCommunication.preferred = 1" ) - - .leftJoinAndMapMany("member.smsAlarming", "member.communications", "smsAlarming", "smsAlarming.isSMSAlarming = 1") - .leftJoinAndSelect("smsAlarming.type", "communicationtype_smsAlarming") .leftJoinAndSelect("preferredCommunication.type", "communicationtype_preferred") .where("member.id = :id", { id: id }) .getOneOrFail() @@ -95,24 +90,4 @@ export default abstract class MemberService { throw new InternalException("member not found by id", err); }); } - - /** - * @description get newsletter by member by id - * @param {number} id - * @returns {Promise} - */ - static async getNewsletterById(id: number): Promise { - 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); - }); - } } diff --git a/src/viewmodel/admin/calendarType.models.ts b/src/viewmodel/admin/calendarType.models.ts index 54dc465..e57dcb1 100644 --- a/src/viewmodel/admin/calendarType.models.ts +++ b/src/viewmodel/admin/calendarType.models.ts @@ -3,5 +3,4 @@ export interface CalendarTypeViewModel { type: string; nscdr: boolean; color: string; - passphrase: string | null; } diff --git a/src/viewmodel/admin/communication.models.ts b/src/viewmodel/admin/communication.models.ts index 64295f8..50e9ac5 100644 --- a/src/viewmodel/admin/communication.models.ts +++ b/src/viewmodel/admin/communication.models.ts @@ -11,5 +11,4 @@ export interface CommunicationViewModel { streetNumberAddition: string; type: CommunicationTypeViewModel; isNewsletterMain: boolean; - isSMSAlarming: boolean; } diff --git a/src/viewmodel/admin/member.models.ts b/src/viewmodel/admin/member.models.ts index c005d9d..5772ff5 100644 --- a/src/viewmodel/admin/member.models.ts +++ b/src/viewmodel/admin/member.models.ts @@ -12,6 +12,5 @@ export interface MemberViewModel { firstMembershipEntry?: MembershipViewModel; lastMembershipEntry?: MembershipViewModel; sendNewsletter?: CommunicationViewModel; - smsAlarming?: Array; preferredCommunication?: Array; }