Openstatus www.openstatus.dev

fiix: stpg subscribers (#1432)

authored by

Maximilian Kaske and committed by
GitHub
75d14084 7478eb16

+83 -21
+15 -1
apps/status-page/src/components/nav/header.tsx
··· 20 20 import { useTRPC } from "@/lib/trpc/client"; 21 21 import { cn } from "@/lib/utils"; 22 22 import { useMutation, useQuery } from "@tanstack/react-query"; 23 + import { isTRPCClientError } from "@trpc/client"; 23 24 import { Menu, MessageCircleMore } from "lucide-react"; 24 25 import NextLink from "next/link"; 25 26 import { useParams, usePathname } from "next/navigation"; 26 27 import { useState } from "react"; 28 + import { toast } from "sonner"; 27 29 28 30 function useNav() { 29 31 const pathname = usePathname(); ··· 62 64 const subscribeMutation = useMutation( 63 65 trpc.statusPage.subscribe.mutationOptions({ 64 66 onSuccess: (id) => { 67 + console.log("subscribeMutation onSuccess", id); 65 68 if (!id) return; 66 - sendPageSubscriptionMutation.mutate({ id }); 69 + sendPageSubscriptionMutation.mutate( 70 + { id }, 71 + { 72 + onError: (error) => { 73 + if (isTRPCClientError(error)) { 74 + toast.error(error.message); 75 + } else { 76 + toast.error("Failed to subscribe"); 77 + } 78 + }, 79 + }, 80 + ); 67 81 }, 68 82 }), 69 83 );
+44 -17
packages/api/src/router/email/index.ts
··· 4 4 import { 5 5 invitation, 6 6 pageSubscriber, 7 + selectWorkspaceSchema, 7 8 statusReportUpdate, 8 9 } from "@openstatus/db/src/schema"; 9 10 import { EmailClient } from "@openstatus/emails"; 11 + import { TRPCError } from "@trpc/server"; 10 12 import { env } from "../../env"; 11 - import { createTRPCRouter, protectedProcedure } from "../../trpc"; 13 + import { 14 + createTRPCRouter, 15 + protectedProcedure, 16 + publicProcedure, 17 + } from "../../trpc"; 12 18 13 19 const emailClient = new EmailClient({ apiKey: env.RESEND_API_KEY }); 14 20 ··· 93 99 } 94 100 }), 95 101 96 - sendPageSubscription: protectedProcedure 102 + sendPageSubscription: publicProcedure 97 103 .input(z.object({ id: z.number() })) 98 104 .mutation(async (opts) => { 99 - const limits = opts.ctx.workspace.limits; 100 - 101 - if (limits["status-subscribers"]) { 102 - const _pageSubscriber = 103 - await opts.ctx.db.query.pageSubscriber.findFirst({ 104 - where: eq(pageSubscriber.id, opts.input.id), 105 + const _pageSubscriber = await opts.ctx.db.query.pageSubscriber.findFirst({ 106 + where: eq(pageSubscriber.id, opts.input.id), 107 + with: { 108 + page: { 105 109 with: { 106 - page: true, 110 + workspace: true, 107 111 }, 108 - }); 112 + }, 113 + }, 114 + }); 115 + 116 + if (!_pageSubscriber || !_pageSubscriber.token) { 117 + throw new TRPCError({ 118 + code: "NOT_FOUND", 119 + message: "Page subscriber not found", 120 + }); 121 + } 109 122 110 - if (!_pageSubscriber || !_pageSubscriber.token) return; 123 + const workspace = selectWorkspaceSchema.safeParse( 124 + _pageSubscriber.page.workspace, 125 + ); 111 126 112 - await emailClient.sendPageSubscription({ 113 - to: _pageSubscriber.email, 114 - token: _pageSubscriber.token, 115 - page: _pageSubscriber.page.title, 116 - // TODO: or use custom domain 117 - domain: _pageSubscriber.page.slug, 127 + if (!workspace.success) { 128 + throw new TRPCError({ 129 + code: "NOT_FOUND", 130 + message: "Workspace not found", 118 131 }); 119 132 } 133 + if (!workspace.data.limits["status-subscribers"]) { 134 + throw new TRPCError({ 135 + code: "FORBIDDEN", 136 + message: "Upgrade to use status subscribers", 137 + }); 138 + } 139 + 140 + await emailClient.sendPageSubscription({ 141 + to: _pageSubscriber.email, 142 + token: _pageSubscriber.token, 143 + page: _pageSubscriber.page.title, 144 + // TODO: or use custom domain 145 + domain: _pageSubscriber.page.slug, 146 + }); 120 147 }), 121 148 });
+24 -3
packages/api/src/router/statusPage.ts
··· 1 1 import { z } from "zod"; 2 2 3 - import { and, eq, inArray, sql } from "@openstatus/db"; 3 + import { and, eq, inArray, isNotNull, sql } from "@openstatus/db"; 4 4 import { 5 5 maintenance, 6 6 monitorsToPages, ··· 8 8 pageSubscriber, 9 9 selectPublicMonitorSchema, 10 10 selectPublicPageSchemaWithRelation, 11 + selectWorkspaceSchema, 11 12 statusReport, 12 13 } from "@openstatus/db/src/schema"; 13 14 ··· 533 534 }, 534 535 }); 535 536 536 - if (!_page) return null; 537 + if (!_page) { 538 + throw new TRPCError({ 539 + code: "NOT_FOUND", 540 + message: "Page not found", 541 + }); 542 + } 537 543 538 - if (_page.workspace.plan === "free") return null; 544 + const workspace = selectWorkspaceSchema.safeParse(_page.workspace); 545 + 546 + if (!workspace.success) { 547 + throw new TRPCError({ 548 + code: "BAD_REQUEST", 549 + message: "Workspace data is invalid", 550 + }); 551 + } 552 + 553 + if (!workspace.data.limits["status-subscribers"]) { 554 + throw new TRPCError({ 555 + code: "FORBIDDEN", 556 + message: "Upgrade to use status subscribers", 557 + }); 558 + } 539 559 540 560 const _alreadySubscribed = 541 561 await opts.ctx.db.query.pageSubscriber.findFirst({ 542 562 where: and( 543 563 eq(pageSubscriber.pageId, _page.id), 544 564 eq(pageSubscriber.email, opts.input.email), 565 + isNotNull(pageSubscriber.acceptedAt), 545 566 ), 546 567 }); 547 568