Openstatus www.openstatus.dev

fix: verify subscription link (#1437)

* fix: duplicate verification email

* fix: api page subscriber

* fix: subscription link

* fix: link

* chore: email subscription tracking

* fix: already subscribed

authored by

Maximilian Kaske and committed by
GitHub
311f362d ccceb00d

+27 -28
+3 -4
apps/server/src/routes/v1/pageSubscribers/post.ts
··· 3 3 import { OpenStatusApiError, openApiErrorResponses } from "@/libs/errors"; 4 4 import { trackMiddleware } from "@/libs/middlewares"; 5 5 import { Events } from "@openstatus/analytics"; 6 - import { and, eq } from "@openstatus/db"; 6 + import { and, eq, isNotNull } from "@openstatus/db"; 7 7 import { db } from "@openstatus/db/src/db"; 8 8 import { page, pageSubscriber } from "@openstatus/db/src/schema"; 9 9 import { SubscribeEmail, sendEmail } from "@openstatus/emails"; ··· 75 75 and( 76 76 eq(pageSubscriber.email, input.email), 77 77 eq(pageSubscriber.pageId, Number(id)), 78 + isNotNull(pageSubscriber.acceptedAt), 78 79 ), 79 80 ) 80 81 .get(); ··· 100 101 .returning() 101 102 .get(); 102 103 103 - const link = _page.customDomain 104 - ? `https://${_page.customDomain}/verify/${token}` 105 - : `https://${_page.slug}.openstatus.dev/verify/${token}`; 104 + const link = `https://${_page.slug}.openstatus.dev/verify/${token}`; 106 105 107 106 await sendEmail({ 108 107 react: SubscribeEmail({
+5 -3
apps/web/src/app/status-page/[domain]/_components/actions.ts
··· 4 4 5 5 import { env } from "@/env"; 6 6 import { Events, setupAnalytics } from "@openstatus/analytics"; 7 - import { and, eq, sql } from "@openstatus/db"; 7 + import { and, eq, isNotNull, sql } from "@openstatus/db"; 8 8 import { db } from "@openstatus/db/src/db"; 9 9 import { page, pageSubscriber } from "@openstatus/db/src/schema"; 10 10 import { EmailClient } from "@openstatus/emails"; ··· 57 57 and( 58 58 eq(pageSubscriber.email, validatedFields.data.email), 59 59 eq(pageSubscriber.pageId, pageData.id), 60 + isNotNull(pageSubscriber.acceptedAt), 60 61 ), 61 62 ) 62 63 .get(); ··· 79 80 }) 80 81 .execute(); 81 82 83 + const link = `https://${pageData.slug}.openstatus.dev/verify/${token}`; 84 + 82 85 await emailClient.sendPageSubscription({ 83 - domain: pageData.slug, 84 - token, 86 + link, 85 87 page: pageData.title, 86 88 to: validatedFields.data.email, 87 89 });
+3 -4
apps/web/src/app/status-page/[domain]/subscribe/route.ts
··· 1 1 import { z } from "zod"; 2 2 3 - import { and, eq } from "@openstatus/db"; 3 + import { and, eq, isNotNull } from "@openstatus/db"; 4 4 import { db } from "@openstatus/db/src/db"; 5 5 import { page, pageSubscriber } from "@openstatus/db/src/schema"; 6 6 import { SubscribeEmail, sendEmail } from "@openstatus/emails"; ··· 31 31 and( 32 32 eq(pageSubscriber.email, data.email), 33 33 eq(pageSubscriber.pageId, pageData?.id), 34 + isNotNull(pageSubscriber.acceptedAt), 34 35 ), 35 36 ) 36 37 .get(); ··· 51 52 }) 52 53 .execute(); 53 54 54 - const link = pageData.customDomain 55 - ? `https://${pageData.customDomain}/verify/${token}` 56 - : `https://${pageData.slug}.openstatus.dev/verify/${token}`; 55 + const link = `https://${pageData.slug}.openstatus.dev/verify/${token}`; 57 56 58 57 await sendEmail({ 59 58 react: SubscribeEmail({
+4
packages/analytics/src/events.ts
··· 64 64 name: "user_subscribed", 65 65 channel: "page", 66 66 }, 67 + VerifySubscribePage: { 68 + name: "user_subscribe_verified", 69 + channel: "page", 70 + }, 67 71 CreateReport: { 68 72 name: "report_created", 69 73 channel: "report",
+5 -3
packages/api/src/router/email/index.ts
··· 137 137 }); 138 138 } 139 139 140 + const link = _pageSubscriber.page.customDomain 141 + ? `https://${_pageSubscriber.page.customDomain}/verify/${_pageSubscriber.token}` 142 + : `https://${_pageSubscriber.page.slug}.openstatus.dev/verify/${_pageSubscriber.token}`; 143 + 140 144 await emailClient.sendPageSubscription({ 141 145 to: _pageSubscriber.email, 142 - token: _pageSubscriber.token, 143 146 page: _pageSubscriber.page.title, 144 - // TODO: or use custom domain 145 - domain: _pageSubscriber.page.slug, 147 + link, 146 148 }); 147 149 }), 148 150 });
+3
packages/api/src/router/statusPage.ts
··· 12 12 statusReport, 13 13 } from "@openstatus/db/src/schema"; 14 14 15 + import { Events } from "@openstatus/analytics"; 15 16 import { TRPCError } from "@trpc/server"; 16 17 import { createTRPCRouter, publicProcedure } from "../trpc"; 17 18 import { ··· 521 522 }), 522 523 523 524 subscribe: publicProcedure 525 + .meta({ track: Events.SubscribePage, trackProps: ["slug", "email"] }) 524 526 .input( 525 527 z.object({ slug: z.string().toLowerCase(), email: z.string().email() }), 526 528 ) ··· 588 590 }), 589 591 590 592 verifyEmail: publicProcedure 593 + .meta({ track: Events.VerifySubscribePage, trackProps: ["slug"] }) 591 594 .input(z.object({ slug: z.string().toLowerCase(), token: z.string() })) 592 595 .mutation(async (opts) => { 593 596 if (!opts.input.slug) return null;
+4 -14
packages/emails/emails/page-subscription.tsx
··· 14 14 import { styles } from "./_components/styles"; 15 15 16 16 export const PageSubscriptionSchema = z.object({ 17 - token: z.string(), 18 17 page: z.string(), 19 - domain: z.string(), 18 + link: z.string(), 20 19 img: z 21 20 .object({ 22 21 src: z.string(), ··· 28 27 29 28 export type PageSubscriptionProps = z.infer<typeof PageSubscriptionSchema>; 30 29 31 - const PageSubscriptionEmail = ({ 32 - token, 33 - page, 34 - domain, 35 - img, 36 - }: PageSubscriptionProps) => { 30 + const PageSubscriptionEmail = ({ page, link, img }: PageSubscriptionProps) => { 37 31 return ( 38 32 <Html> 39 33 <Head /> ··· 53 47 this email. 54 48 </Text> 55 49 <Text> 56 - <Link 57 - style={styles.link} 58 - href={`https://${domain}.openstatus.dev/verify/${token}`} 59 - > 50 + <Link style={styles.link} href={link}> 60 51 Confirm subscription 61 52 </Link> 62 53 </Text> ··· 67 58 }; 68 59 69 60 PageSubscriptionEmail.PreviewProps = { 70 - token: "token", 61 + link: "https://slug.openstatus.dev/verify/token", 71 62 page: "OpenStatus", 72 - domain: "slug", 73 63 } satisfies PageSubscriptionProps; 74 64 75 65 export default PageSubscriptionEmail;