diff --git a/package-lock.json b/package-lock.json index e96d86a4..f5c498a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -53,6 +53,7 @@ "@types/mailgun-js": "^0.22.18", "@types/mocha": "^10.0.7", "@types/morgan": "^1.9.9", + "@types/node": "^20.14.9", "@types/node-cron": "^3.0.11", "@types/node-fetch": "^2.6.11", "@types/nodemailer": "^6.4.15", @@ -1883,9 +1884,9 @@ } }, "node_modules/@types/node": { - "version": "20.12.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.8.tgz", - "integrity": "sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w==", + "version": "20.14.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz", + "integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==", "dependencies": { "undici-types": "~5.26.4" } diff --git a/package.json b/package.json index 44670bb8..31c6b895 100644 --- a/package.json +++ b/package.json @@ -91,6 +91,7 @@ "@types/mailgun-js": "^0.22.18", "@types/mocha": "^10.0.7", "@types/morgan": "^1.9.9", + "@types/node": "^20.14.9", "@types/node-cron": "^3.0.11", "@types/node-fetch": "^2.6.11", "@types/nodemailer": "^6.4.15", diff --git a/src/__test__/subscribe.Test.ts b/src/__test__/subscribe.Test.ts index cd33133d..298f98eb 100644 --- a/src/__test__/subscribe.Test.ts +++ b/src/__test__/subscribe.Test.ts @@ -70,7 +70,7 @@ describe('DELETE /api/v1/subscribe/delete/:id', () => { await subscribeRepository.save(subscription); const response = await request(app) - .delete(`/api/v1/subscribe/delete/${subscription.id}`) + .get(`/api/v1/subscribe/delete/${subscription.id}`) .send(); expect(response.status).toBe(200); @@ -79,7 +79,7 @@ describe('DELETE /api/v1/subscribe/delete/:id', () => { it('should return 404 if the subscription does not exist', async () => { const response = await request(app) - .delete('/api/v1/subscribe/delete/450') + .get('/api/v1/subscribe/delete/450') .send(); expect(response.status).toBe(404); @@ -88,10 +88,37 @@ describe('DELETE /api/v1/subscribe/delete/:id', () => { it('should return 400 for invalid ID', async () => { const response = await request(app) - .delete('/api/v1/subscribe/delete/noid') + .get('/api/v1/subscribe/delete/noid') .send(); expect(response.status).toBe(400); expect(response.body.message).toBeUndefined(); }); }); + +describe('GET /api/v1/subscribe/getAll', () => { + beforeEach(async () => { + await subscribeRepository.clear(); + }); + + it('should return all subscriptions', async () => { + const subscription1 = new Subscription(); + subscription1.email = 'test1@example.com'; + await subscribeRepository.save(subscription1); + + const subscription2 = new Subscription(); + subscription2.email = 'test2@example.com'; + await subscribeRepository.save(subscription2); + + const response = await request(app).get('/api/v1/subscribe/getAll').send(); + + expect(response.status).toBe(200); + expect(response.body.subscription).toHaveLength(2); + expect(response.body.subscription).toEqual( + expect.arrayContaining([ + expect.objectContaining({ email: 'test1@example.com' }), + expect.objectContaining({ email: 'test2@example.com' }), + ]) + ); + }); +}); diff --git a/src/controller/subscribeController.ts b/src/controller/subscribeController.ts index 1ff37d30..8136a03d 100644 --- a/src/controller/subscribeController.ts +++ b/src/controller/subscribeController.ts @@ -3,6 +3,7 @@ import errorHandler from '../middlewares/errorHandler'; import dbConnection from '../database'; import Subscription from '../database/models/subscribe'; import { check, validationResult } from 'express-validator'; +import sendEmail from '../emails'; const subscribeRepository = dbConnection.getRepository(Subscription); const userEmailRules = [ @@ -28,6 +29,11 @@ export const subscribe = [ subscription.email = email; await subscribeRepository.save(subscription); + const unsubscribeLink = process.env.unsubscribe; + await sendEmail('subscribe', email, { + name: email, + link: `${unsubscribeLink}/${subscription.id}`, + }); res.status(201).json({ message: 'Subscribed successfully', subscription }); }), ]; @@ -62,3 +68,28 @@ export const removeSubscriber = [ } }), ]; + +export const getAllSubscriber = [ + errorHandler(async (req: Request, res: Response) => { + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(400).json({ errors: errors.array() }); + } + + try { + const subscription = await subscribeRepository.find({ + select: { + email: true, + }, + }); + + if (!subscription) { + return res.status(404).json({ message: 'Subscription not found' }); + } + + res.status(200).json({ subscription }); + } catch (error) { + res.status(500).json({ message: 'Internal error', error }); + } + }), +]; diff --git a/src/docs/subscribe.ts b/src/docs/subscribe.ts index 511af5ac..f801a3e8 100644 --- a/src/docs/subscribe.ts +++ b/src/docs/subscribe.ts @@ -43,7 +43,7 @@ /** * @openapi * /api/v1/subscribe/delete/{id}: - * delete: + * get: * tags: [Subscribe] * summary: Removes a user from subscription * parameters: @@ -85,3 +85,36 @@ * type: string * example: Invalid ID */ + +/** + * @openapi + * /api/v1/subscribe/getAll: + * get: + * tags: [Subscribe] + * summary: Retrieve all subscribers + * description: Get a list of all subscribers in the system. + * responses: + * 200: + * description: A list of subscribers + * content: + * application/json: + * schema: + * type: array + * items: + * type: object + * properties: + * id: + * type: integer + * example: 1 + * email: + * type: string + * example: user@example.com + * createdAt: + * type: string + * format: date-time + * example: 2024-06-29T12:34:56Z + * updatedAt: + * type: string + * format: date-time + * example: 2024-06-29T12:34:56Z + */ diff --git a/src/emails/index.ts b/src/emails/index.ts index 8f88ef6f..afeeca88 100644 --- a/src/emails/index.ts +++ b/src/emails/index.ts @@ -1,7 +1,7 @@ import axios from 'axios'; import handlebars from 'handlebars'; import fs from 'fs'; -type EmailType = 'confirm' | 'reset'; +type EmailType = 'confirm' | 'reset' | 'subscribe'; type Data = { name: string; link: string; diff --git a/src/emails/subscribe.ts b/src/emails/subscribe.ts new file mode 100644 index 00000000..b3de3ebb --- /dev/null +++ b/src/emails/subscribe.ts @@ -0,0 +1,62 @@ +import axios from 'axios'; +import handlebars from 'handlebars'; +import fs from 'fs'; +import dotenv from 'dotenv'; +dotenv.config(); +type EmailType = 'subscribe' | 'unsubscribe'; +type Data = { + name: string; + link: string; +}; +/** + * Sends an email of the specified type to the recipient using the provided data. + * + * @param emailType - The type of email to send. Must be either "confirm" or "reset". + * @param recipient - The email address of the recipient. + * @param data - The data to be used for generating the email content. A name and link are required. + * @returns A Promise that resolves to the response from the email service. + * @throws An error if there is an issue sending the email. + */ +async function sendEmail(emailType: EmailType, recipient: string, data: Data) { + const templatePath = `./src/emails/templates/${emailType}.html`; + try { + // Read the Handlebars template file + const templateFile = fs.readFileSync(templatePath, 'utf-8'); + + // Compile the template + const template = handlebars.compile(templateFile); + + // Generate the HTML content using the template and data + const html = template(data); + + // Send the Email + + const domain = process.env.MAILGUN_DOMAIN; + const key = process.env.MAILGUN_TOKEN as string; + const body = { + from: `Dynamites Account Team `, + to: [recipient], + subject: 'Verification Email', + html: html, + }; + const mailgunResponse = await axios.post( + `https://api.mailgun.net/v3/${domain}/messages`, + body, + { + auth: { + username: 'api', + password: key, + }, + headers: { + 'Content-Type': 'multipart/form-data', + }, + } + ); + + return mailgunResponse; + } catch (error) { + throw new Error(`Error sending email: ${error}`); + } +} + +export default sendEmail; diff --git a/src/emails/templates/subscribe.html b/src/emails/templates/subscribe.html new file mode 100644 index 00000000..72f84e10 --- /dev/null +++ b/src/emails/templates/subscribe.html @@ -0,0 +1,71 @@ + + + + + + Welcome to Our Newsletter + + + +
+
+

Welcome to Our Newsletter!

+
+
+

Hi there,

+

+ Thank you for subscribing to our newsletter. We're excited to have you + with us! +

+

+ You'll receive regular updates on our latest content, news, and + special offers. +

+
+ +
+ + diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index 97b6565b..b9af02f0 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -18,7 +18,11 @@ import { } from '../controller/changestatusController'; import { checkRole } from '../middlewares/authorize'; import { IsLoggedIn } from '../middlewares/isLoggedIn'; -import { subscribe, removeSubscriber } from '../controller/subscribeController'; +import { + subscribe, + removeSubscriber, + getAllSubscriber, +} from '../controller/subscribeController'; const userRouter = Router(); userRouter.post('/register', registerUser); @@ -46,5 +50,6 @@ userRouter.put('/recover/confirm', updateNewPassword); userRouter.put('/updateProfile/:id', updateProfile); userRouter.post('/subscribe', subscribe); -userRouter.delete('/subscribe/delete/:id', removeSubscriber); +userRouter.get('/subscribe/delete/:id', removeSubscriber); +userRouter.get('/subscribe/getAll', getAllSubscriber); export default userRouter;