From 88b068bc2cfc1a87277e4ffe1cfe55379c254720 Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 1 Apr 2024 00:48:57 +0200 Subject: [PATCH] newsletter --- server.ts | 2 + src/controllers/newsletterController.ts | 158 ++++++++++++++++++++++++ src/logger/logger.ts | 7 +- src/models/index.ts | 6 +- src/models/newsletter.ts | 56 +++++++++ src/routes/newsletterRoutes.ts | 16 +++ src/utils/utils.ts | 4 + 7 files changed, 245 insertions(+), 4 deletions(-) create mode 100644 src/controllers/newsletterController.ts create mode 100644 src/models/newsletter.ts create mode 100644 src/routes/newsletterRoutes.ts diff --git a/server.ts b/server.ts index 34f9fcc..4849536 100644 --- a/server.ts +++ b/server.ts @@ -9,6 +9,7 @@ import session from "express-session"; import useragent from "express-useragent"; import calendarRoutes from "./src/routes/calendarRoutes"; +import newsletterRoutes from "./src/routes/newsletterRoutes"; import paymentRoutes from "./src/routes/paymentRoutes"; import storeRoutes from "./src/routes/storeRoutes"; import storeServicesRoutes from "./src/routes/storeServicesRoutes"; @@ -121,6 +122,7 @@ app.use( ); app.use(bodyParser.json()); app.use("/api/v1/calendar", calendarRoutes); +app.use("/api/v1/newsletter", newsletterRoutes); app.use("/api/v1/payment", paymentRoutes); app.use("/api/v1/store", storeRoutes); app.use("/api/v1/store/services", storeServicesRoutes); diff --git a/src/controllers/newsletterController.ts b/src/controllers/newsletterController.ts new file mode 100644 index 0000000..02d37b6 --- /dev/null +++ b/src/controllers/newsletterController.ts @@ -0,0 +1,158 @@ +import { Request, Response } from "express"; +import Newsletter from "../models/newsletter"; +import { newsletterLogger } from "../logger/logger"; +import { newNewsletterId } from "../utils/utils"; +import { isEmailValid } from "../validator/validator"; +import rabbitmq from "../rabbitmq/rabbitmq"; + +const NEWSLETTER_STATE = { + NOT_CONFIRMED: 0, + CONFIRMED: 1, +}; + +export async function AddNewsletterParticipant(req: Request, res: Response) { + try { + const { type, email } = req.body; + + if ( + !type || + !email || + !(await isEmailValid(email, false)) || + !process.env.NEWSLETTER_ALLOWED_TYPES?.split(",").includes(type) + ) { + newsletterLogger.warn( + "AddNewsletterParticipant: invalid request - missing email or type" + ); + return res.status(400).send({ err: "invalid request" }); + } + + // only add the participant if they are not already in the newsletter + + const existingNewsletterParticipant = await Newsletter.findOne({ + where: { + type: type, + email: email, + }, + }); + + if (!existingNewsletterParticipant) { + const newsletterId = newNewsletterId(); + + await Newsletter.create({ + newsletter_id: newsletterId, + email: email, + type: type, + state: NEWSLETTER_STATE.NOT_CONFIRMED, + }); + + newsletterLogger.info("AddNewsletterParticipant", email); + + rabbitmq.sendEmail(email, "confirmNewsletterParticipation", "de", { + confirmNewsletterParticipationLink: `${process.env.DASHBOARD_API_URL}/v1/newsletter/confirm/${newsletterId}`, + unsubscribeNewsletterParticipationLink: `${process.env.DASHBOARD_API_URL}/v1/newsletter/unsubscribe/${newsletterId}`, + privacyPolicyLink: + process.env.NEWSLETTER_PRIVACY_POLICY_URLS?.split(",")[0], + }); + } else { + newsletterLogger.warn( + `AddNewsletterParticipant: email: ${email} already in newsletter of type: ${type}` + ); + } + + return res.status(200).send({ status: "success" }); + } catch (error) { + newsletterLogger.error("AddNewsletterParticipant err:", error as string); + res.status(500).send({ err: "invalid request" }); + } +} + +export async function ConfirmNewsletterParticipation( + req: Request, + res: Response +) { + try { + const { newsletterId } = req.params; + + if (!newsletterId) { + newsletterLogger.warn( + "ConfirmNewsletterParticipation: invalid request - missing email or type" + ); + return res.status(400).send({ err: "invalid request" }); + } + + const newsletterParticipant = await Newsletter.findOne({ + where: { + newsletter_id: newsletterId, + }, + }); + + if (!newsletterParticipant) { + newsletterLogger.warn( + `ConfirmNewsletterParticipation: newsletter id: ${newsletterId} not found` + ); + return res.status(400).send({ err: "invalid request" }); + } + + newsletterParticipant.state = NEWSLETTER_STATE.CONFIRMED; + await newsletterParticipant.save(); + + rabbitmq.sendEmail( + newsletterParticipant.email, + "confirmedNewsletterParticipation", + "de", + { + unsubscribeNewsletterParticipationLink: `${process.env.DASHBOARD_API_URL}/v1/newsletter/unsubscribe/${newsletterId}`, + privacyPolicyLink: process.env.NEWSLETTER_PRIVACY_POLICY_URLS?.split( + "," + ).find((url) => url.includes(newsletterParticipant.type)), + } + ); + + newsletterLogger.info("ConfirmNewsletterParticipation", newsletterId); + + return res.status(200).send({ status: "success" }); + } catch (error) { + newsletterLogger.error( + "ConfirmNewsletterParticipation err:", + error as string + ); + res.status(500).send({ err: "invalid request" }); + } +} + +export async function RemoveNewsletterParticipant(req: Request, res: Response) { + try { + const { newsletterId } = req.params; + + if (!newsletterId) { + newsletterLogger.warn( + "RemoveNewsletterParticipant: invalid request - missing email or type" + ); + return res.status(400).send({ err: "invalid request" }); + } + + const newsletterParticipant = await Newsletter.findOne({ + where: { + newsletter_id: newsletterId, + }, + }); + + if (!newsletterParticipant) { + newsletterLogger.warn( + `RemoveNewsletterParticipant: newsletter id: ${newsletterId} not found` + ); + return res.status(400).send({ err: "invalid request" }); + } + + await newsletterParticipant.destroy(); + + newsletterLogger.info( + `RemoveNewsletterParticipant newsletterId: ${newsletterId} removed. Email: ${newsletterParticipant.email}, type: ${newsletterParticipant.type}, state: ${newsletterParticipant.state}` + ); + + return res.status(200).send({ status: "success" }); + } catch (error) { + newsletterLogger.error("RemoveNewsletterParticipant err:", error as string); + res.status(500).send({ err: "invalid request" }); + } +} diff --git a/src/logger/logger.ts b/src/logger/logger.ts index 9b3f3f7..682b4d0 100644 --- a/src/logger/logger.ts +++ b/src/logger/logger.ts @@ -151,6 +151,8 @@ class StoreLogger extends BaseLogger { } class SystemLogger { + constructor(private logTypePrefix: string) {} + info(...messages: string[]) { this.log("info", ...messages); } @@ -170,7 +172,7 @@ class SystemLogger { private log(level: string, ...messages: string[]) { winlogger.log({ level: level, - logType: process.env.NODE_APP_NAME, + logType: this.logTypePrefix, message: messages.join(" "), }); } @@ -178,6 +180,7 @@ class SystemLogger { export const userLogger = new UserLogger(); export const storeLogger = new StoreLogger(); -export const logger = new SystemLogger(); +export const newsletterLogger = new SystemLogger("newsletter"); +export const logger = new SystemLogger(process.env.NODE_APP_NAME || "system"); export default logger; diff --git a/src/models/index.ts b/src/models/index.ts index b8a5bf3..1899d04 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,17 +1,19 @@ -import Feedback from "./feedback"; import EmailVerification from "./emailVerification"; +import Feedback from "./feedback"; +import Newsletter from "./newsletter"; import Session from "./session"; import Store from "./store"; import StoreService from "./storeService"; import StoreServiceActivity from "./storeServiceActivity"; import StoreServiceActivityUsers from "./storeServiceActivityUsers"; import User from "./user"; -import UserPendingEmailChange from "./userPendingEmailChange"; import UserAccountExport from "./userAccountExport"; +import UserPendingEmailChange from "./userPendingEmailChange"; function syncModels() { EmailVerification.sync(); Feedback.sync(); + Newsletter.sync(); Session.sync(); Store.sync(); StoreService.sync(); diff --git a/src/models/newsletter.ts b/src/models/newsletter.ts new file mode 100644 index 0000000..f28f68d --- /dev/null +++ b/src/models/newsletter.ts @@ -0,0 +1,56 @@ +import { DataTypes, Model } from "sequelize"; +import sequelize from "../database/database"; + +interface NewsletterAttributes { + newsletter_id: string; + type: string; + email: string; + state: number; +} + +class Newsletter + extends Model + implements NewsletterAttributes +{ + declare newsletter_id: string; + declare type: string; // e. g. jannex or zeitadler + declare email: string; + declare state: number; // 0 = not confirmed, 1 = confirmed + declare created_at: Date; +} + +Newsletter.init( + { + newsletter_id: { + primaryKey: true, + type: DataTypes.STRING, + allowNull: false, + }, + type: { + type: DataTypes.STRING, + allowNull: false, + }, + email: { + type: DataTypes.STRING, + allowNull: false, + }, + state: { + type: DataTypes.INTEGER, + allowNull: false, + }, + }, + { + tableName: "newsletters", + sequelize, + createdAt: "created_at", + updatedAt: "updated_at", + indexes: [ + { + unique: true, + fields: ["email", "type"], // combination of email and type must be unique + }, + ], + } +); + +export default Newsletter; diff --git a/src/routes/newsletterRoutes.ts b/src/routes/newsletterRoutes.ts new file mode 100644 index 0000000..a00d5cd --- /dev/null +++ b/src/routes/newsletterRoutes.ts @@ -0,0 +1,16 @@ +import express from "express"; +const router = express.Router(); + +import * as newsletterController from "../controllers/newsletterController"; + +router.post("/", newsletterController.AddNewsletterParticipant); +router.get( + "/confirm/:newsletterId", + newsletterController.ConfirmNewsletterParticipation +); +router.get( + "/unsubscribe/:newsletterId", + newsletterController.RemoveNewsletterParticipant +); + +export default router; diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 156b952..f31ffb0 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -57,6 +57,10 @@ export function newFeedbackId() { return uuidv4(); } +export function newNewsletterId() { + return uuidv4(); +} + export function newAccountExportId() { return crypto.randomBytes(ACCOUNT_EXPORT_ID_LENGTH).toString("hex"); }