Skip to content

Commit

Permalink
Merge pull request #384 from Open-Earth-Foundation/feature/send-notif…
Browse files Browse the repository at this point in the history
…ication

Feature/send notification
  • Loading branch information
lemilonkh authored Apr 3, 2024
2 parents d12c29b + 688c2af commit 5801bf9
Show file tree
Hide file tree
Showing 9 changed files with 357 additions and 23 deletions.
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();
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 = `
<!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") {
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 });
}

async sendEmail({
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

0 comments on commit 5801bf9

Please sign in to comment.