Openstatus www.openstatus.dev
6
fork

Configure Feed

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

at main 215 lines 6.9 kB view raw
1import { z } from "zod"; 2 3import { and, eq, isNotNull } from "@openstatus/db"; 4import { 5 invitation, 6 maintenance, 7 pageSubscriber, 8 selectWorkspaceSchema, 9 statusReportUpdate, 10} from "@openstatus/db/src/schema"; 11import { EmailClient } from "@openstatus/emails"; 12import { TRPCError } from "@trpc/server"; 13import { env } from "../../env"; 14import { 15 createTRPCRouter, 16 protectedProcedure, 17 publicProcedure, 18} from "../../trpc"; 19 20const emailClient = new EmailClient({ apiKey: env.RESEND_API_KEY }); 21 22export const emailRouter = createTRPCRouter({ 23 sendStatusReport: protectedProcedure 24 .input(z.object({ id: z.number() })) 25 .mutation(async (opts) => { 26 const limits = opts.ctx.workspace.limits; 27 28 if (limits["status-subscribers"]) { 29 const _statusReportUpdate = 30 await opts.ctx.db.query.statusReportUpdate.findFirst({ 31 where: eq(statusReportUpdate.id, opts.input.id), 32 with: { 33 statusReport: { 34 with: { 35 statusReportsToPageComponents: { 36 with: { 37 pageComponent: true, 38 }, 39 }, 40 page: { 41 with: { 42 pageSubscribers: { 43 where: isNotNull(pageSubscriber.acceptedAt), 44 }, 45 }, 46 }, 47 }, 48 }, 49 }, 50 }); 51 52 if (!_statusReportUpdate) return; 53 if (!_statusReportUpdate.statusReport.page) return; 54 if ( 55 _statusReportUpdate.statusReport.page.workspaceId !== 56 opts.ctx.workspace.id 57 ) 58 return; 59 const validSubscribers = 60 _statusReportUpdate.statusReport.page.pageSubscribers.filter( 61 (s): s is typeof s & { token: string } => 62 s.token !== null && 63 s.acceptedAt !== null && 64 s.unsubscribedAt === null, 65 ); 66 if (!validSubscribers.length) return; 67 68 await emailClient.sendStatusReportUpdate({ 69 subscribers: validSubscribers.map((subscriber) => ({ 70 email: subscriber.email, 71 token: subscriber.token, 72 })), 73 pageTitle: _statusReportUpdate.statusReport.page.title, 74 pageSlug: _statusReportUpdate.statusReport.page.slug, 75 customDomain: _statusReportUpdate.statusReport.page.customDomain, 76 reportTitle: _statusReportUpdate.statusReport.title, 77 status: _statusReportUpdate.status, 78 message: _statusReportUpdate.message, 79 date: new Date(_statusReportUpdate.date).toISOString(), 80 pageComponents: 81 _statusReportUpdate.statusReport.statusReportsToPageComponents.map( 82 (i) => i.pageComponent.name, 83 ), 84 }); 85 } 86 }), 87 sendMaintenance: protectedProcedure 88 .input(z.object({ id: z.number() })) 89 .mutation(async (opts) => { 90 const limits = opts.ctx.workspace.limits; 91 92 if (limits["status-subscribers"]) { 93 const _maintenance = await opts.ctx.db.query.maintenance.findFirst({ 94 where: and( 95 eq(maintenance.id, opts.input.id), 96 eq(maintenance.workspaceId, opts.ctx.workspace.id), 97 ), 98 with: { 99 maintenancesToPageComponents: { 100 with: { 101 pageComponent: true, 102 }, 103 }, 104 page: { 105 with: { 106 pageSubscribers: { 107 where: isNotNull(pageSubscriber.acceptedAt), 108 }, 109 }, 110 }, 111 }, 112 }); 113 114 if (!_maintenance) return; 115 if (!_maintenance.page) return; 116 const validSubscribers = _maintenance.page.pageSubscribers.filter( 117 (s): s is typeof s & { token: string } => 118 s.token !== null && 119 s.acceptedAt !== null && 120 s.unsubscribedAt === null, 121 ); 122 if (!validSubscribers.length) return; 123 124 await emailClient.sendStatusReportUpdate({ 125 subscribers: validSubscribers.map((subscriber) => ({ 126 email: subscriber.email, 127 token: subscriber.token, 128 })), 129 pageTitle: _maintenance.page.title, 130 pageSlug: _maintenance.page.slug, 131 customDomain: _maintenance.page.customDomain, 132 reportTitle: _maintenance.title, 133 status: "maintenance", 134 message: _maintenance.message, 135 date: new Date(_maintenance.from).toISOString(), 136 pageComponents: _maintenance.maintenancesToPageComponents.map( 137 (i) => i.pageComponent.name, 138 ), 139 }); 140 } 141 }), 142 sendTeamInvitation: protectedProcedure 143 .input(z.object({ id: z.number(), baseUrl: z.string().optional() })) 144 .mutation(async (opts) => { 145 const limits = opts.ctx.workspace.limits; 146 147 if (limits.members === "Unlimited" || limits.members > 1) { 148 const _invitation = await opts.ctx.db.query.invitation.findFirst({ 149 where: and( 150 eq(invitation.id, opts.input.id), 151 eq(invitation.workspaceId, opts.ctx.workspace.id), 152 ), 153 }); 154 155 if (!_invitation) return; 156 157 await emailClient.sendTeamInvitation({ 158 to: _invitation.email, 159 token: _invitation.token, 160 invitedBy: `${opts.ctx.user.email}`, 161 workspaceName: opts.ctx.workspace.name || "OpenStatus", 162 baseUrl: opts.input.baseUrl, 163 }); 164 } 165 }), 166 167 sendPageSubscription: publicProcedure 168 .input(z.object({ id: z.number() })) 169 .mutation(async (opts) => { 170 const _pageSubscriber = await opts.ctx.db.query.pageSubscriber.findFirst({ 171 where: eq(pageSubscriber.id, opts.input.id), 172 with: { 173 page: { 174 with: { 175 workspace: true, 176 }, 177 }, 178 }, 179 }); 180 181 if (!_pageSubscriber || !_pageSubscriber.token) { 182 throw new TRPCError({ 183 code: "NOT_FOUND", 184 message: "Page subscriber not found", 185 }); 186 } 187 188 const workspace = selectWorkspaceSchema.safeParse( 189 _pageSubscriber.page.workspace, 190 ); 191 192 if (!workspace.success) { 193 throw new TRPCError({ 194 code: "NOT_FOUND", 195 message: "Workspace not found", 196 }); 197 } 198 if (!workspace.data.limits["status-subscribers"]) { 199 throw new TRPCError({ 200 code: "FORBIDDEN", 201 message: "Upgrade to use status subscribers", 202 }); 203 } 204 205 const link = _pageSubscriber.page.customDomain 206 ? `https://${_pageSubscriber.page.customDomain}/verify/${_pageSubscriber.token}` 207 : `https://${_pageSubscriber.page.slug}.openstatus.dev/verify/${_pageSubscriber.token}`; 208 209 await emailClient.sendPageSubscription({ 210 to: _pageSubscriber.email, 211 page: _pageSubscriber.page.title, 212 link, 213 }); 214 }), 215});