Openstatus www.openstatus.dev
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Send subscriber update (#517)

* ๐Ÿ’Œ subscriber

* ๐Ÿ’Œ subscriber

* ๐Ÿ’Œ subscriber

* ๐Ÿ’Œ subscriber

* ๐Ÿ’Œ subscriber

* ๐Ÿ’Œ subscriber

* ๐Ÿ’Œ subscriber

authored by

Thibault Le Ouay and committed by
GitHub
775eb8f9 8f5efd01

+198 -23
+2 -1
apps/server/src/v1/page.ts
··· 3 3 import { and, eq } from "@openstatus/db"; 4 4 import { db } from "@openstatus/db/src/db"; 5 5 import { page, pageSubscriber } from "@openstatus/db/src/schema"; 6 - import { sendEmail, SubscribeEmail } from "@openstatus/emails"; 6 + import { SubscribeEmail } from "@openstatus/emails"; 7 + import { sendEmail } from "@openstatus/emails/emails/send"; 7 8 8 9 import type { Variables } from "."; 9 10 import { ErrorSchema } from "./shared";
+44 -2
apps/server/src/v1/statusReport.ts
··· 1 1 import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi"; 2 2 3 - import { and, db, eq } from "@openstatus/db"; 3 + import { and, db, eq, isNotNull } from "@openstatus/db"; 4 4 import { 5 + page, 6 + pagesToStatusReports, 7 + pageSubscriber, 5 8 statusReport, 6 9 statusReportStatus, 7 10 statusReportUpdate, 11 + workspacePlans, 8 12 } from "@openstatus/db/src/schema"; 13 + import { sendEmail, sendEmailHtml } from "@openstatus/emails/emails/send"; 14 + import { allPlans } from "@openstatus/plans"; 9 15 10 16 import type { Variables } from "./index"; 11 17 import { ErrorSchema } from "./shared"; ··· 336 342 }) 337 343 .returning() 338 344 .get(); 339 - 345 + // send email 346 + const workspacePlan = c.get("workspacePlan"); 347 + if (workspacePlan !== allPlans.free) { 348 + const allPages = await db 349 + .select() 350 + .from(pagesToStatusReports) 351 + .where(eq(pagesToStatusReports.statusReportId, statusReportId)) 352 + .all(); 353 + for (const currentPage of allPages) { 354 + const subscribers = await db 355 + .select() 356 + .from(pageSubscriber) 357 + .where( 358 + and( 359 + eq(pageSubscriber.pageId, currentPage.pageId), 360 + isNotNull(pageSubscriber.acceptedAt), 361 + ), 362 + ) 363 + .all(); 364 + const pageInfo = await db 365 + .select() 366 + .from(page) 367 + .where(eq(page.id, currentPage.pageId)) 368 + .get(); 369 + if (!pageInfo) continue; 370 + const subscribersEmails = subscribers.map( 371 + (subscriber) => subscriber.email, 372 + ); 373 + await sendEmailHtml({ 374 + to: subscribersEmails, 375 + subject: `New status update for ${pageInfo.title}`, 376 + html: `<p>Hi,</p><p>${pageInfo.title} just posted an update on their status page:</p><p>New Status : ${statusReportUpdate.status}</p><p>${statusReportUpdate.message}</p></p><p></p><p>Powered by OpenStatus</p><p></p><p></p><p></p><p></p><p></p> 377 + `, 378 + from: "Notification OpenStatus <notification@openstatus.dev>", 379 + }); 380 + } 381 + } 340 382 const data = statusUpdateSchema.parse(_statusReportUpdate); 341 383 342 384 return c.jsonT({
+43 -1
apps/server/src/v1/statusReportUpdate.ts
··· 1 1 import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi"; 2 2 3 - import { and, db, eq } from "@openstatus/db"; 3 + import { and, db, eq, isNotNull } from "@openstatus/db"; 4 4 import { 5 + page, 6 + pagesToStatusReports, 7 + pageSubscriber, 5 8 statusReport, 6 9 statusReportStatus, 7 10 statusReportUpdate, 8 11 } from "@openstatus/db/src/schema"; 12 + import { sendEmailHtml } from "@openstatus/emails"; 13 + import { allPlans } from "@openstatus/plans"; 9 14 10 15 import type { Variables } from "."; 11 16 import { ErrorSchema } from "./shared"; ··· 172 177 }) 173 178 .returning() 174 179 .get(); 180 + // send email 181 + const workspacePlan = c.get("workspacePlan"); 175 182 183 + if (workspacePlan !== allPlans.free) { 184 + const allPages = await db 185 + .select() 186 + .from(pagesToStatusReports) 187 + .where(eq(pagesToStatusReports.statusReportId, input.status_report_id)) 188 + .all(); 189 + for (const currentPage of allPages) { 190 + const subscribers = await db 191 + .select() 192 + .from(pageSubscriber) 193 + .where( 194 + and( 195 + eq(pageSubscriber.pageId, currentPage.pageId), 196 + isNotNull(pageSubscriber.acceptedAt), 197 + ), 198 + ) 199 + .all(); 200 + const pageInfo = await db 201 + .select() 202 + .from(page) 203 + .where(eq(page.id, currentPage.pageId)) 204 + .get(); 205 + if (!pageInfo) continue; 206 + const subscribersEmails = subscribers.map( 207 + (subscriber) => subscriber.email, 208 + ); 209 + await sendEmailHtml({ 210 + to: subscribersEmails, 211 + subject: `New status update for ${pageInfo.title}`, 212 + html: `<p>Hi,</p><p>${pageInfo.title} just posted an update on their status page:</p><p>New Status : ${statusReportUpdate.status}</p><p>${statusReportUpdate.message}</p></p><p></p><p>Powered by OpenStatus</p><p></p><p></p><p></p><p></p><p></p> 213 + `, 214 + from: "Notification OpenStatus <notification@openstatus.dev>", 215 + }); 216 + } 217 + } 176 218 const data = statusUpdateSchema.parse(res); 177 219 return c.jsonT(data); 178 220 });
+13 -2
apps/web/next.config.js
··· 4 4 const nextConfig = { 5 5 reactStrictMode: true, 6 6 swcMinify: true, 7 - transpilePackages: ["@openstatus/ui", "@openstatus/api"], 7 + transpilePackages: [ 8 + "@openstatus/ui", 9 + "@openstatus/api", 10 + "@react-email/components", 11 + "@react-email/render", 12 + "@react-email/html", 13 + ], 14 + 8 15 experimental: { 9 16 serverActions: true, 10 - serverComponentsExternalPackages: ["libsql"], 17 + serverComponentsExternalPackages: [ 18 + "libsql", 19 + "@react-email/components", 20 + "@react-email/render", 21 + ], 11 22 logging: { 12 23 level: "verbose", 13 24 fullUrl: true,
+1
packages/api/.env.test
··· 1 + RESEND_API_KEY='test'
+56 -2
packages/api/src/router/statusReport.ts
··· 1 1 import { z } from "zod"; 2 2 3 - import { and, eq, inArray } from "@openstatus/db"; 3 + import { and, eq, inArray, isNotNull } from "@openstatus/db"; 4 4 import { 5 5 insertStatusReportSchema, 6 6 insertStatusReportUpdateSchema, 7 7 monitorsToStatusReport, 8 + page, 8 9 pagesToStatusReports, 10 + pageSubscriber, 9 11 selectMonitorSchema, 10 12 selectStatusReportSchema, 11 13 selectStatusReportUpdateSchema, 12 14 statusReport, 13 15 statusReportStatusSchema, 14 16 statusReportUpdate, 17 + workspace, 15 18 } from "@openstatus/db/src/schema"; 19 + import { sendEmailHtml } from "@openstatus/emails/emails/send"; 16 20 17 21 import { createTRPCRouter, protectedProcedure } from "../trpc"; 18 22 ··· 78 82 .get(); 79 83 80 84 const { id, ...statusReportUpdateInput } = opts.input; 81 - return await opts.ctx.db 85 + 86 + // Send email 87 + 88 + const updatedValue = await opts.ctx.db 82 89 .insert(statusReportUpdate) 83 90 .values(statusReportUpdateInput) 84 91 .returning() 85 92 .get(); 93 + 94 + const currentWorkspace = await opts.ctx.db 95 + .select() 96 + .from(workspace) 97 + .where(eq(workspace.id, opts.ctx.workspace.id)) 98 + .get(); 99 + if (currentWorkspace?.plan !== "pro") { 100 + const allPages = await opts.ctx.db 101 + .select() 102 + .from(pagesToStatusReports) 103 + .where( 104 + eq( 105 + pagesToStatusReports.statusReportId, 106 + updatedValue.statusReportId, 107 + ), 108 + ) 109 + .all(); 110 + for (const currentPage of allPages) { 111 + const subscribers = await opts.ctx.db 112 + .select() 113 + .from(pageSubscriber) 114 + .where( 115 + and( 116 + eq(pageSubscriber.pageId, currentPage.pageId), 117 + isNotNull(pageSubscriber.acceptedAt), 118 + ), 119 + ) 120 + .all(); 121 + const pageInfo = await opts.ctx.db 122 + .select() 123 + .from(page) 124 + .where(eq(page.id, currentPage.pageId)) 125 + .get(); 126 + if (!pageInfo) continue; 127 + const subscribersEmails = subscribers.map( 128 + (subscriber) => subscriber.email, 129 + ); 130 + await sendEmailHtml({ 131 + to: subscribersEmails, 132 + subject: `New status update for ${pageInfo.title}`, 133 + html: `<p>Hi,</p><p>${pageInfo.title} just posted an update on their status page:</p><p>New Status : ${statusReportUpdate.status}</p><p>${statusReportUpdate.message}</p></p><p></p><p>Powered by OpenStatus</p><p></p><p></p><p></p><p></p><p></p> 134 + `, 135 + from: "Notification OpenStatus <notification@openstatus.dev>", 136 + }); 137 + } 138 + } 139 + return updatedValue; 86 140 }), 87 141 88 142 updateStatusReport: protectedProcedure
+38
packages/emails/emails/send.ts
··· 1 + import { Resend } from "resend"; 2 + 3 + import { env } from "../env"; 4 + 5 + export const resend = new Resend(env.RESEND_API_KEY); 6 + 7 + export interface Emails { 8 + react: JSX.Element; 9 + subject: string; 10 + to: string[]; 11 + from: string; 12 + } 13 + 14 + export type EmailHtml = { 15 + html: string; 16 + subject: string; 17 + to: string[]; 18 + from: string; 19 + }; 20 + export const sendEmail = async (email: Emails) => { 21 + await resend.emails.send(email); 22 + }; 23 + 24 + export const sendEmailHtml = async (email: EmailHtml) => { 25 + await fetch("https://api.resend.com/emails", { 26 + method: "POST", 27 + headers: { 28 + "Content-Type": "application/json", 29 + Authorization: `Bearer ${env.RESEND_API_KEY}`, 30 + }, 31 + body: JSON.stringify({ 32 + to: email.to, 33 + from: email.from, 34 + subject: email.subject, 35 + html: email.html, 36 + }), 37 + }); 38 + };
+1 -15
packages/emails/index.ts
··· 1 - import { Resend } from "resend"; 2 - 3 1 import { Alert, EmailDataSchema } from "./emails/alert"; 4 2 import SubscribeEmail from "./emails/subscribe"; 5 3 import { validateEmailNotDisposable } from "./emails/utils/utils"; 6 4 import WaitingList from "./emails/waiting-list"; 7 5 import WelcomeEmail from "./emails/welcome"; 8 - import { env } from "./env"; 9 6 10 7 export { 11 8 WelcomeEmail, ··· 16 13 SubscribeEmail, 17 14 }; 18 15 19 - export const resend = new Resend(env.RESEND_API_KEY); 20 - 21 - export interface Emails { 22 - react: JSX.Element; 23 - subject: string; 24 - to: string[]; 25 - from: string; 26 - } 27 - 28 - export const sendEmail = async (email: Emails) => { 29 - await resend.emails.send(email); 30 - }; 16 + export { sendEmail, sendEmailHtml } from "./emails/send";