Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions server/config/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ async function createTestNodemailerTransport() {
});
return transporter;
} catch (error: any) {
console.error("Failed to create a testing account. " + error.message);
logger.error("E: Failed to create a testing account. " + error.message);
}
}
interface MailContent {
Expand All @@ -50,27 +50,34 @@ interface MailFrom {
}

/**
* Sends an e-mail to a single recipent or multiple recipents.
* Sends an e-mail to a single recipient or multiple recipients.
*
* @param {string|Array} mailAddresses A single email address of type string oder multiple addresses as an array of strings.
* @param {object} content A object containing a "title" and a "text" property of type string.
* @param {string} [replyTo] The replyTo address which will be added to the send e-mail.
* @returns Returns true if a mail was sucessfully delegated to the E-Mail Service Provider.
* @returns Returns true if a mail was successfully delegated to the E-Mail Service Provider.
*/
const sendMail = async (
mailAddresses: string | string[],
content: MailContent,
content: MailContent | undefined,
replyTo?: string
) => {
if (!content) {
logger.error("E: email content is undefined");
return;
}

if (
!content.title ||
!content.text ||
content.title.length == 0 ||
content.text.length == 0
)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Der Error wurde nie gecatched und hat deshalb den Server heruntergerissen.
Habe deshalb aus dem Error ein Log gemacht.

throw new Error(
"content.title and content.text are not allowed to be empty"
) {
logger.error(
"E: content.title and content.text are not allowed to be empty"
);
return;
}

const message = createMessage(
{
Expand Down
4 changes: 2 additions & 2 deletions server/constants/email-message-constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ Du kannst direkt auf diese E-Mail antworten.

module.exports.REGISTRATION_TITLE = "Deine Anmeldung bei XCCup.net";

module.exports.REGISTRATION_TEXT = (firstName, activateLink) =>
module.exports.REGISTRATION_TEXT = (firstName, activationLink) =>
`Hallo ${firstName}! Willkommen beim XCCup.

Um dein Konto zu aktivieren klicke bitte auf den folgenden Link:
${activateLink}
${activationLink}

Wir wünschen Dir allzeit gute Flüge und viel Spaß.

Expand Down
2 changes: 1 addition & 1 deletion server/controller/CacheManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const deleteCache = (keysArrayToDelete) => {
keysArrayToDelete.some((includeKey) => ck.includes(includeKey))
);

logger.debug("Will delete these keys from cache: " + keysToDelete);
logger.debug("CM: Will delete these keys from cache: " + keysToDelete);

return cache.del(keysToDelete);
};
Expand Down
6 changes: 6 additions & 0 deletions server/controller/UserController.js
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,12 @@ router.post(
}
);

router.get("/testmail", async (req, res) => {
const user = await service.getById("cd1583d1-fb7f-4a93-b732-effd59e5c3ae");
mailService.sendActivationMail(user);
res.sendStatus(OK);
});

// @desc Edits a user
// @route PUT /users/
// @access Only owner
Expand Down
2 changes: 2 additions & 0 deletions server/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { initSponsor } from "./models/Sponsor";
import { initTeam } from "./models/Team";
import { initToken } from "./models/Token";
import { initUser } from "./models/User";
import { initMessage } from "./models/Message";
import { Models } from "../types/Models";

// Config
Expand Down Expand Up @@ -63,6 +64,7 @@ const models: Models = {
Team: initTeam(sequelize),
Token: initToken(sequelize),
User: initUser(sequelize),
Message: initMessage(sequelize),
};

const db = {
Expand Down
53 changes: 53 additions & 0 deletions server/db/models/Message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Sequelize, Model, DataTypes } from "sequelize";
import { Models } from "../../types/Models";

export const MessageTypes = {
EMAIL: "email",
OTHER: "other",
} as const;

export type MessageTypesType = typeof MessageTypes[keyof typeof MessageTypes];

export const EmailMessagePosition = {
TITLE: "title",
TEXT: "text",
} as const;

export type EmailMessagePositionType =
typeof EmailMessagePosition[keyof typeof EmailMessagePosition];

interface MessageAttributes {
name: string;
content: string;
typeOfMessage: MessageTypesType;
position: EmailMessagePositionType;
}

export interface MessageInstance
extends Model<MessageAttributes>,
MessageAttributes {
createdAt?: Date;
updatedAt?: Date;
}

export function initMessage(sequelize: Sequelize): Models["Message"] {
const MessageTemplate = sequelize.define<MessageInstance>("Message", {
name: {
type: DataTypes.STRING,
allowNull: false,
},
content: {
type: DataTypes.STRING(512),
allowNull: false,
},
typeOfMessage: {
type: DataTypes.STRING(64),
defaultValue: "other",
},
position: {
type: DataTypes.STRING(64),
},
});

return MessageTemplate;
}
9 changes: 5 additions & 4 deletions server/service/MailService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import type {
} from "../db/models/Flight";
import type { Comment } from "../types/Comment";
import { FlightCommentInstance } from "../db/models/FlightComment";
import { getMessageByNameForEmail } from "./MessageService";

const clientUrl = config.get("clientUrl");
const userActivateLink = config.get("clientActivateProfil");
Expand Down Expand Up @@ -101,10 +102,10 @@ const service = {

const activationLink = `${clientUrl}${userActivateLink}?userId=${user.id}&token=${user.token}`;

const content = {
title: REGISTRATION_TITLE,
text: REGISTRATION_TEXT(user.firstName, activationLink),
};
const content = await getMessageByNameForEmail("REGISTRATION", {
firstName: user.firstName,
activationLink,
});

return sendMail(user.email, content);
},
Expand Down
117 changes: 117 additions & 0 deletions server/service/MessageService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import logger from "../config/logger";
import { cache } from "../controller/CacheManager";
import db from "../db";
import {
EmailMessagePosition,
EmailMessagePositionType,
MessageTypes,
MessageTypesType,
} from "../db/models/Message";

const PLACEHOLDER_PREFIX = "$";
const CACHE_PREFIX = "msg_template_";

interface PlaceholderReplacements {
[name: string]: string | undefined;
}

interface EmailMessage {
title: string;
text: string;
}

export async function getMessageByNameForEmail(
name: string,
placeholderReplacementsText?: PlaceholderReplacements,
placeholderReplacementsTitle?: PlaceholderReplacements
) {
const CACHE_KEY = CACHE_PREFIX + name;

let message = cache.get(CACHE_KEY) as EmailMessage | null;

if (!message) {
try {
message = await retrieveEmailParts(name);
} catch (error) {
logger.error("MS: " + error);
return;
}

cache.set(CACHE_KEY, message, 0);
}

console.log("JSON: ", JSON.stringify(message, null, 2));

try {
message.text = replacePlaceholder(
message.text,
placeholderReplacementsText
);
message.title = replacePlaceholder(
message.title,
placeholderReplacementsTitle
);
} catch (error) {
logger.error("MS: " + error);
return;
}

return message;
}

async function retrieveEmailParts(name: string) {
const [title, text] = await Promise.all([
retrieveMessageFromDb(name, MessageTypes.EMAIL, EmailMessagePosition.TITLE),
retrieveMessageFromDb(name, MessageTypes.EMAIL, EmailMessagePosition.TEXT),
]);

if (!title) {
throw `No title for message with name ${name} found`;
}

if (!text) {
throw `No text for message with name ${name} found`;
}

return { title, text };
}

async function retrieveMessageFromDb(
name: string,
typeOfMessage?: MessageTypesType,
position?: EmailMessagePositionType
) {
const dbEntry = await db.Message.findOne({
where: { name, typeOfMessage, position },
raw: true,
attributes: ["content"],
});

return dbEntry?.content;
}

/**
* Replaces the placeholder in the message template with a specified value.
* The name of the placeholder has to match the name of the property in the parameters object.
*/
function replacePlaceholder(
messageWithPlaceholders: string,
placeholderReplacements?: PlaceholderReplacements
) {
if (!placeholderReplacements) {
logger.debug("MS: No replacements specified");
return messageWithPlaceholders;
}

for (const [key, value] of Object.entries(placeholderReplacements)) {
if (!value) throw `Value of ${key} is undefined`;
console.log("K: " + key);
console.log("V: " + value);
messageWithPlaceholders = messageWithPlaceholders.replaceAll(
PLACEHOLDER_PREFIX + key,
value
);
console.log("MWP: " + messageWithPlaceholders);
}
return messageWithPlaceholders;
}
2 changes: 2 additions & 0 deletions server/test/DbTestDataLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const FlyingSite = require("../db")["FlyingSite"];
const FlightFixes = require("../db")["FlightFixes"];
const FlightPhoto = require("../db")["FlightPhoto"];
const SeasonDetail = require("../db")["SeasonDetail"];
const Message = require("../db")["Message"];
const Airspace = require("../db")["Airspace"];
const News = require("../db")["News"];
const Sponsor = require("../db")["Sponsor"];
Expand Down Expand Up @@ -48,6 +49,7 @@ const dbTestData = {
[Sponsor, require("./testdatasets/sponsors.json")],
[Brand, require("./testdatasets/brands.json")],
[Logo, require("./testdatasets/logos.json")],
[Message, require("./testdatasets/messages.json")],
];
// Test data with personal data
if (config.get("serverImportTestData")) {
Expand Down
14 changes: 14 additions & 0 deletions server/test/testdatasets/messages.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[
{
"name": "REGISTRATION",
"content": "Deine Anmeldung bei XCCup.net",
"typeOfMessage": "email",
"position": "title"
},
{
"name": "REGISTRATION",
"content": "Hallo $firstName!\nWillkommen beim XCCup.\n\nUm dein Konto zu aktivieren klicke bitte auf den folgenden Link:\n$activationLink\n\nWir wünschen Dir allzeit gute Flüge und viel Spaß.\n\nDein XCCup Team",
"typeOfMessage": "email",
"position": "text"
}
]
1 change: 1 addition & 0 deletions server/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"module": "commonjs",
"sourceMap": true,
"outDir": "./dist",
"lib": ["ES2021"],
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
Expand Down
2 changes: 2 additions & 0 deletions server/types/Models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { SponsorInstance } from "../db/models/Sponsor";
import { TeamInstance } from "../db/models/Team";
import { TokenInstance } from "../db/models/Token";
import { UserInstance } from "../db/models/User";
import { MessageInstance } from "../db/models/Message";

interface extendedModel<M extends Model<any, any>> extends ModelStatic<M> {
associate?: (props: Models) => void;
Expand All @@ -39,4 +40,5 @@ export interface Models {
Team: extendedModel<TeamInstance>;
Token: extendedModel<TokenInstance>;
User: extendedModel<UserInstance>;
Message: extendedModel<MessageInstance>;
}