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

Feature/send notification #384

Merged
merged 17 commits into from
Apr 3, 2024
Merged
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
2 changes: 2 additions & 0 deletions .github/workflows/web-develop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ jobs:
kubectl set env deployment/cc-web-deploy CHAT_PROVIDER=openai
kubectl set env deployment/cc-web-deploy OPENAI_API_KEY=${{secrets.OPENAI_API_KEY}}
kubectl set env deployment/cc-web-deploy HUGGINGFACE_API_KEY=${{secrets.HUGGINGFACE_API_KEY}}
kubectl set env deployment/cc-web-deploy ADMIN_EMAILS="[email protected],[email protected],[email protected]"
kubectl set env deployment/cc-web-deploy ADMIN_NAMES=${{secrets.ADMIN_NAMES}}
kubectl set env deployment/cc-web-deploy "DEFAULT_ADMIN_EMAIL=${{secrets.DEFAULT_ADMIN_EMAIL}}"
kubectl set env deployment/cc-web-deploy "DEFAULT_ADMIN_PASSWORD=${{secrets.DEFAULT_ADMIN_PASSWORD}}"
kubectl create -f k8s/cc-create-admin.yml -n default
Expand Down
2 changes: 2 additions & 0 deletions app/env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ VERIFICATION_TOKEN_SECRET="80c70dfdeedf2c01757b880d39c79214e915c786dd48d5473c9c0
HUGGINGFACE_API_KEY=hf_MY_SECRET_KEY
OPENAI_API_KEY=sk-MY_SECRET_KEY
CHAT_PROVIDER=huggingface
ADMIN_EMAILS="[email protected]"
ADMIN_NAMES="John doe"
NEXT_PUBLIC_OPENCLIMATE_API_URL="https://openclimate.openearth.dev"
4 changes: 2 additions & 2 deletions app/src/app/[lng]/[inventory]/data/review/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import { appendFileToFormData } from "@/util/helpers";
import { useState } from "react";

export default function ReviewPage({
params: { lng, inventoryId },
params: { lng, inventory: inventoryId },
}: {
params: { lng: string; inventoryId: string };
params: { lng: string; inventory: string };
}) {
const { t } = useTranslation(lng, "data");
const router = useRouter();
Expand Down
99 changes: 81 additions & 18 deletions app/src/app/api/v0/city/[city]/file/route.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import NotificationService from "@/backend/NotificationService";
import AdminNotificationTemplate from "@/lib/emails/AdminNotificationTemplate";
import { db } from "@/models";
import { apiHandler } from "@/util/api";
import { fileEndingToMIMEType } from "@/util/helpers";
import { bytesToMB, fileEndingToMIMEType } from "@/util/helpers";
import { createUserFileRequset } from "@/util/validation";
import { render } from "@react-email/components";
import { randomUUID } from "crypto";
import createHttpError from "http-errors";
import { Session } from "next-auth";
import { NextRequest, NextResponse } from "next/server";
import session from "redux-persist/lib/storage/session";

// TODO: use these variables to configure file size and format
const MAX_FILE_SIZE = 5000000;
Expand Down Expand Up @@ -58,6 +62,19 @@ export const GET = apiHandler(async (_req: Request, context) => {

export const POST = apiHandler(async (req: NextRequest, context) => {
const userId = context.session?.user.id;
const service = new NotificationService();
Copy link
Contributor

Choose a reason for hiding this comment

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

Use static methods and don't create an instance of NotificationService here.

const user = context.session?.user;
const cityId = context.params.city;

const city = await db.models.City.findOne({
where: {
cityId,
},
});

if (!city) {
throw new createHttpError.Unauthorized("Unauthorized");
}

if (!context.session) {
throw new createHttpError.Unauthorized("Unauthorized");
Expand Down Expand Up @@ -104,23 +121,69 @@ export const POST = apiHandler(async (req: NextRequest, context) => {
throw new createHttpError.NotFound("User files not found");
}

return NextResponse.json({
data: {
id: userFile.id,
userId: userFile.userId,
cityId: userFile.cityId,
fileReference: userFile.fileReference,
url: userFile.url,
sector: userFile.sector,
fileName: userFile.fileName,
lastUpdated: userFile.lastUpdated,
status: userFile.status,
gpcRefNo: userFile.gpcRefNo,
file: {
fileName: file.name,
size: file.size,
fileType: userFile.fileType,
},
const newFileData = {
id: userFile.id,
userId: userFile.userId!,
cityId: userFile.cityId!,
fileReference: userFile.fileReference!,
url: userFile.url!,
sector: userFile.sector!,
subsectors: userFile.subsectors!,
scopes: userFile.scopes!,
fileName: userFile.fileName!,
lastUpdated: userFile.lastUpdated!,
status: userFile.status!,
gpcRefNo: userFile.gpcRefNo!,
file: {
fileName: file.name,
size: file.size,
fileType: userFile.fileType!,
},
};
const host = process.env.HOST ?? "http://localhost:3000";

const emailTemplate = `
Copy link
Contributor

Choose a reason for hiding this comment

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

Create mock for the send email functionality

Copy link
Contributor

Choose a reason for hiding this comment

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

Render template using React-Email again and move this to NotificationService.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Email Notification</title>
</head>
<body style="background-color: #ffffff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif; margin: 0; padding: 20px;">
<div style="margin: 0 auto; max-width: 580px; padding: 20px 0 48px;">
<!-- SVG Placeholder for ExcelFileIcon -->
<!-- Make sure to replace with actual SVG or an <img> tag pointing to the icon -->
<svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg"><!-- SVG content --></svg>
<h1 style="color: #2351DC; font-size: 20px; line-height: 1.5; font-weight: 700;">CityCatalyst</h1>
<h2 style="color: #484848; font-size: 24px; line-height: 1.3; font-weight: 700; margin-top: 50px;">${user?.name} From ${city.name} Uploaded New Files For Review</h2>
<p style="font-size: 14px; line-height: 1.4; color: #484848;">Hi ${process.env.ADMIN_NAMES},</p>
<p style="font-size: 14px; line-height: 1.4; color: #484848;">${user?.name} (${user?.email}) has uploaded files in CityCatalyst for revision and to upload to their inventories.</p>
<!-- Example for file link; adjust href as needed -->
<a href="${host}/api/v0/user/file/${newFileData.id}/download-file" style="text-decoration: none; color: #2351DC;"><div>
<div style="flex-direction: column; padding-left: 16px; align-items: center; gap: 16px; height: 100px; border-radius: 8px; border: 1px solid #E6E7FF; margin-top: 0px"><div>${file.name}</div><br /><div style="color:a6a6a6">${bytesToMB(file.size)}</div><div style="margin-top: 20px">${newFileData.subsectors.map((item: string) => `<span key=${item} style="background-color: #e8eafb; color: #2351dc; padding: 6px 8px; border-radius: 30px; margin-right: 8px; font-size: 14px; margin-top: 20px">${item}</span>`)}</div></div>
</div></a>
<!-- Placeholder for tags; repeat this structure for each tag as needed -->
<a href="${host}"><button style="font-size: 14px; padding: 16px; background-color: #2351DC; border-radius: 100px; line-height:1.5; color: #FFFFFF; margin-top: 20px; border:none">GOTO REVIEW</button></a>
<!-- Footer -->
<hr style="height: 2px; background: #EBEBEC; margin-top: 36px;" />
<p style="font-size: 12px; line-height: 16px; color: #79797A; font-weight: 400;">Open Earth Foundation is a nonprofit public benefit corporation from California, USA. EIN: 85-3261449</p>
</div>
</body>
</html>

`;

if (process.env.NODE_ENV !== "test") {
Copy link
Contributor

Choose a reason for hiding this comment

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

Mock this so you don't need to check here

await service.sendEmail({
to: process.env.ADMIN_EMAILS!,
subject: "CityCatalyst File Upload",
text: "City Catalyst",
html: emailTemplate,
});
}

return NextResponse.json({
data: newFileData,
});
});
48 changes: 48 additions & 0 deletions app/src/backend/NotificationService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { smtpOptions } from "@/lib/email";
import nodemailer, { Transporter } from "nodemailer";

interface EmailOptions {
to: string;
subject: string;
text: string;
html: string;
}

interface SendEmailResponse {
success: boolean;
messageId?: string;
error?: any;
}

class NotificationService {
private transporter: Transporter;
constructor() {
this.transporter = nodemailer.createTransport({ ...smtpOptions });
}
Comment on lines +18 to +21
Copy link
Contributor

Choose a reason for hiding this comment

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

No state in service. Create this in sendEmail or create static helper method in NotificationService.


async sendEmail({
Copy link
Contributor

Choose a reason for hiding this comment

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

Should be static so it can be called like this: await NotificationService.sendEmail(...);

Also this should handle template rendering with react-email.

to,
subject,
text,
html,
}: EmailOptions): Promise<SendEmailResponse> {
const mailOptions = {
from: "",
to,
subject,
text,
html,
};

try {
const info = await this.transporter.sendMail(mailOptions);
console.log("Message sent: %s", info.messageId);
return { success: true, messageId: info.messageId };
} catch (error) {
console.error("Error sending email:", error);
return { success: false, error };
}
}
}

export default NotificationService;
2 changes: 1 addition & 1 deletion app/src/lib/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ type EmailPayload = {
html: string;
};

const smtpOptions = {
export const smtpOptions = {
host: process.env.SMTP_HOST || "localhost",
port: parseInt(process.env.SMTP_PORT || "2525"),
secure: false,
Expand Down
Loading