Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: collect email for subscription #2124

Merged
merged 28 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b352136
feat: collect email for subscription
tejaskh3 Sep 3, 2024
7e975f1
feat: add API to send email & test locally
tejaskh3 Sep 6, 2024
c9b12d4
feat: write test cases for subscription APIs
tejaskh3 Sep 8, 2024
80a1c5a
refactor: add comment
tejaskh3 Sep 8, 2024
fa46bf8
feat: add test for send-email API
tejaskh3 Sep 8, 2024
eac79a9
feat: validating email and add contants
tejaskh3 Sep 10, 2024
b172b39
refactor: change file extension
tejaskh3 Sep 10, 2024
d4abe57
feat: add test for invalid email
tejaskh3 Sep 12, 2024
f2e1226
feat: add feature flag
tejaskh3 Sep 12, 2024
e58f5ab
refactor: change config details
tejaskh3 Sep 12, 2024
5f8be59
refactor: change test email
tejaskh3 Sep 13, 2024
c0fe636
fix: change phone number to optional
tejaskh3 Oct 8, 2024
47897cb
fix: make phoneNumber optional
tejaskh3 Oct 9, 2024
82f9585
fix: change API name
tejaskh3 Oct 9, 2024
5ef628f
fix: change API name
tejaskh3 Oct 9, 2024
6f0a148
fix: try-catch Indain phone numbers controllers
tejaskh3 Oct 12, 2024
28e7a14
fix: put to patch
tejaskh3 Oct 12, 2024
534b6b8
Merge branch 'develop' into subscription-feature
vinit717 Oct 12, 2024
620b15c
feat: add devFlagMiddleware
tejaskh3 Oct 12, 2024
783f96d
fix: comments
tejaskh3 Oct 12, 2024
496bf58
Merge branch 'subscription-feature' of https://github.com/Real-Dev-Sq…
tejaskh3 Oct 12, 2024
e472233
fix: return message
tejaskh3 Oct 12, 2024
7a6c4ff
remove: comments
tejaskh3 Oct 12, 2024
1ffe7f1
fix: failing test case
tejaskh3 Oct 12, 2024
75e80f4
fix: add test cases
tejaskh3 Oct 13, 2024
a36a05c
fix: remove comment
tejaskh3 Oct 13, 2024
f79bb5a
fix: custom environment variables
tejaskh3 Oct 14, 2024
9f870db
rename: emailSubscriptonCrenderials
tejaskh3 Oct 14, 2024
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
7 changes: 7 additions & 0 deletions config/custom-environment-variables.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ module.exports = {
},
},

emailSubscriptionCredentials: {
email: "<RDS_EMAIL>",
password: "<EMAIL PASSWORD GENERATED AFTER 2FA>",
tejaskh3 marked this conversation as resolved.
Show resolved Hide resolved
host: "<smtp host>",
port: "<number>",
},

userToken: {
cookieName: "COOKIE_NAME",
ttl: {
Expand Down
7 changes: 7 additions & 0 deletions config/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@
clientSecret: "<clientSecret>",
},

emailSubscriptionCredentials: {
email: "<RDS_EMAIL>",
password: "<EMAIL PASSWORD GENERATED AFTER 2FA>",
host: "<smtp host>",
port: "<number>",
},

firestore: `{
"type": "service_account",
"project_id": "<project-name>",
Expand Down Expand Up @@ -59,7 +66,7 @@
},

cors: {
allowedOrigins: /(https:\/\/([a-zA-Z0-9-_]+\.)?realdevsquad\.com$)/, // Allow realdevsquad.com, *.realdevsquad.com

Check warning on line 69 in config/default.js

View workflow job for this annotation

GitHub Actions / build (20.11.x)

Unsafe Regular Expression
},

userToken: {
Expand Down
7 changes: 7 additions & 0 deletions config/development.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ module.exports = {
},
},

emailSubscriptionCredentials: {
email: "<RDS_EMAIL>",
password: "<EMAIL PASSWORD GENERATED AFTER 2FA>",
host: "<smtp host>",
port: "<number>",
},

userToken: {
publicKey:
"-----BEGIN PUBLIC KEY-----\n" +
Expand Down
2 changes: 2 additions & 0 deletions constants/subscription-validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
tejaskh3 marked this conversation as resolved.
Show resolved Hide resolved
export const phoneNumberRegex = /^[+]{1}(?:[0-9\-\\(\\)\\/.]\s?){6,15}[0-9]{1}$/;
74 changes: 74 additions & 0 deletions controllers/subscription.ts
tejaskh3 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { CustomRequest, CustomResponse } from "../types/global";
const { addOrUpdate } = require("../models/users");
const { INTERNAL_SERVER_ERROR } = require("../constants/errorMessages");
const nodemailer = require("nodemailer");
const config = require("config");
const emailSubscriptionCredentials = config.get("emailSubscriptionCredentials");

export const subscribe = async (req: CustomRequest, res: CustomResponse) => {
const { email } = req.body;
tejaskh3 marked this conversation as resolved.
Show resolved Hide resolved
const phoneNumber = req.body.phoneNumber || null;
const userId = req.userData.id;
const data = { email, isSubscribed: true, phoneNumber };
const userAlreadySubscribed = req.userData.isSubscribed;
try {
if (userAlreadySubscribed) {
return res.boom.badRequest("User already subscribed");
}
await addOrUpdate(data, userId);
tejaskh3 marked this conversation as resolved.
Show resolved Hide resolved
return res.status(201).json("User subscribed successfully");
} catch (error) {
logger.error(`Error occurred while subscribing: ${error.message}`);
return res.boom.badImplementation(INTERNAL_SERVER_ERROR);
}
};

export const unsubscribe = async (req: CustomRequest, res: CustomResponse) => {
iamitprakash marked this conversation as resolved.
Show resolved Hide resolved
const userId = req.userData.id;
const userAlreadySubscribed = req.userData.isSubscribed;
try {
if (!userAlreadySubscribed) {
return res.boom.badRequest("User is already unsubscribed");
}
await addOrUpdate(
{
isSubscribed: false,
},
userId
);
return res.status(200).json("User unsubscribed successfully");
} catch (error) {
logger.error(`Error occurred while unsubscribing: ${error.message}`);
return res.boom.badImplementation(INTERNAL_SERVER_ERROR);
}
};

// TODO: currently we are sending test email to a user only (i.e., Tejas sir as decided)
// later we need to make service which send email to all subscribed user
export const sendEmail = async (req: CustomRequest, res: CustomResponse) => {
tejaskh3 marked this conversation as resolved.
Show resolved Hide resolved
try {
tejaskh3 marked this conversation as resolved.
Show resolved Hide resolved
const transporter = nodemailer.createTransport({
host: emailSubscriptionCredentials.host,
port: emailSubscriptionCredentials.port,
secure: false,

auth: {
user: emailSubscriptionCredentials.email,
pass: emailSubscriptionCredentials.password,
},
});

const info = await transporter.sendMail({
from: `"Real Dev Squad" <${emailSubscriptionCredentials.email}>`,
to: "[email protected]",
subject: "Hello local, Testing in progress.",
text: "working for notification feature",
html: "<b>Hello world!</b>",
});

return res.send({ message: "Email sent successfully", info });
} catch (error) {
logger.error("Error occurred while sending email:", error.message);
return res.boom.badImplementation(INTERNAL_SERVER_ERROR);
}
};
15 changes: 15 additions & 0 deletions middlewares/devFlag.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { NextFunction } from "express";
import { CustomRequest, CustomResponse } from "../types/global";

export const devFlagMiddleware = (req: CustomRequest, res: CustomResponse, next: NextFunction) => {
iamitprakash marked this conversation as resolved.
Show resolved Hide resolved
try {
const dev = req.query.dev === "true";
if (!dev) {
return res.boom.notFound("Route not found");
}
next();
} catch (err) {
logger.error("Error occurred in devFlagMiddleware:", err.message);
next(err);
}
};
23 changes: 23 additions & 0 deletions middlewares/validators/subscription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { NextFunction } from "express";
import { CustomRequest, CustomResponse } from "../../types/global";
import { emailRegex, phoneNumberRegex } from "../../constants/subscription-validator"; // Removed phoneNumberRegex import
tejaskh3 marked this conversation as resolved.
Show resolved Hide resolved
import Joi from 'joi';

export const validateSubscribe = (req: CustomRequest, res: CustomResponse, next: NextFunction) => {
iamitprakash marked this conversation as resolved.
Show resolved Hide resolved

if(req.body.email){
req.body.email = req.body.email.trim();
}
if (req.body.phoneNumber) {
req.body.phoneNumber = req.body.phoneNumber.trim();
}
const subscribeSchema = Joi.object({
phoneNumber: Joi.string().allow('').optional().regex(phoneNumberRegex),
email: Joi.string().required().regex(emailRegex)
});
const { error } = subscribeSchema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
next();
};
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"tdd:watch": "sh scripts/tests/tdd.sh"
},
"dependencies": {
"@types/nodemailer": "^6.4.15",
"axios": "1.7.2",
"cloudinary": "2.0.3",
"config": "3.3.7",
Expand All @@ -34,6 +35,8 @@
"morgan": "1.10.0",
"multer": "1.4.5-lts.1",
"newrelic": "11.19.0",
"nodemailer": "^6.9.15",
"nodemailer-mock": "^2.0.6",
"passport": "0.7.0",
"passport-github2": "0.1.12",
"rate-limiter-flexible": "5.0.3",
Expand Down
2 changes: 2 additions & 0 deletions routes/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import express from "express";
const app = express.Router();
import { devFlagMiddleware } from "../middlewares/devFlag";

app.use("/answers", require("./answers"));
app.use("/auctions", require("./auctions"));
Expand Down Expand Up @@ -39,4 +40,5 @@ app.use("/v1/notifications", require("./notify"));
app.use("/goals", require("./goals"));
app.use("/invites", require("./invites"));
app.use("/requests", require("./requests"));
app.use("/subscription", devFlagMiddleware, require("./subscription"));
module.exports = app;
12 changes: 12 additions & 0 deletions routes/subscription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import express from "express";
import authenticate from "../middlewares/authenticate";
import { subscribe, unsubscribe, sendEmail } from "../controllers/subscription";
import { validateSubscribe } from "../middlewares/validators/subscription";
const authorizeRoles = require("../middlewares/authorizeRoles");
const router = express.Router();
const { SUPERUSER } = require("../constants/roles");

router.post("/", authenticate, validateSubscribe, subscribe);
router.patch("/", authenticate, unsubscribe);
Dismissed Show dismissed Hide dismissed
router.get("/notify", authenticate, authorizeRoles([SUPERUSER]), sendEmail);
Fixed Show fixed Hide fixed
module.exports = router;
7 changes: 7 additions & 0 deletions test/config/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ module.exports = {
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-hqc2v%40dev-rds.iam.gserviceaccount.com"
}`,

emailSubscriptionCredentials: {
email: "<RDS_EMAIL>",
password: "<EMAIL PASSWORD GENERATED AFTER 2FA>",
host: "<smtp host>",
port: "<number>",
},
services: {
rdsApi: {
baseUrl: `http://localhost:${port}`,
Expand Down
7 changes: 7 additions & 0 deletions test/fixtures/subscription/subscription.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const subscribedMessage = "User subscribed successfully";
export const unSubscribedMessage = "User unsubscribed successfully";
export const subscriptionData = {
phoneNumber: "+911234567890",
email: "[email protected]",
};

115 changes: 115 additions & 0 deletions test/integration/subscription.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
const chai = require("chai");
const sinon = require("sinon");
iamitprakash marked this conversation as resolved.
Show resolved Hide resolved
const app = require("../../server");
const cookieName = config.get("userToken.cookieName");
const { subscribedMessage, unSubscribedMessage, subscriptionData } = require("../fixtures/subscription/subscription");
const addUser = require("../utils/addUser");
const authService = require("../../services/authService");
const chaiHttp = require("chai-http");
chai.use(chaiHttp);
const nodemailer = require("nodemailer");
const nodemailerMock = require("nodemailer-mock");
const userData = require("../fixtures/user/user")();
const { expect } = chai;
let userId = "";
const superUser = userData[4];
let superUserAuthToken = "";
describe("/subscription email notifications", function () {
let jwt;

beforeEach(async function () {
userId = await addUser();
jwt = authService.generateAuthToken({ userId });
});

it("Should return 401 if the user is not logged in", function (done) {
chai
.request(app)
.post("/subscription?dev=true")
.end((err, res) => {
if (err) {
return done();
}
expect(res).to.have.status(401);
expect(res.body).to.be.a("object");
expect(res.body.message).to.equal("Unauthenticated User");
return done();
});
});

it("should add user's data and make them subscribe to us.", function (done) {
chai
.request(app)
.post(`/subscription?dev=true`)
.set("cookie", `${cookieName}=${jwt}`)
.send(subscriptionData)
.end((err, res) => {
if (err) {
return done(err);
}
expect(res).to.have.status(201);
expect(res.body).to.equal(subscribedMessage);
return done();
});
});

it("should unsubscribe the user", function (done) {
chai
.request(app)
.patch(`/subscription?dev=true`)
.set("cookie", `${cookieName}=${jwt}`)
.end((err, res) => {
if (err) {
return done(err);
}
expect(res).to.have.status(200);
expect(res.body).to.equal(unSubscribedMessage);
return done();
});
});

describe("/notify endpoint", function () {
beforeEach(async function () {
const superUserId = await addUser(superUser);
superUserAuthToken = authService.generateAuthToken({ userId: superUserId });
sinon.stub(nodemailerMock, "createTransport").callsFake(nodemailerMock.createTransport);
});

afterEach(function () {
sinon.restore();
nodemailerMock.mock.reset();
});

it("Should return 401 if the super user is not logged in", function (done) {
chai
.request(app)
.get("/subscription/notify?dev=true")
.end((err, res) => {
if (err) {
return done();
}
expect(res).to.have.status(401);
expect(res.body).to.be.a("object");
expect(res.body.message).to.equal("Unauthenticated User");
return done();
});
});

it("should handle errors if sending email fails", function (done) {
sinon.stub(nodemailer, "createTransport").callsFake(() => {
throw new Error("Transport error");
});

chai
.request(app)
.get("/subscription/notify?dev=true")
.set("Cookie", `${cookieName}=${superUserAuthToken}`)
.end((err, res) => {
if (err) return done(err);
expect(res).to.have.status(500);
expect(res.body).to.have.property("message", "An internal server error occurred");
return done();
});
});
});
});
Loading
Loading