Openstatus www.openstatus.dev

fix(status-page): verify subscription custom domain (#1434)

* fix: verify subscription custom domain

* chore: improve verify page

* fix: legacy status page

* fix: nerd mode

authored by

Maximilian Kaske and committed by
GitHub
ccceb00d 92e48be0

+86 -58
+5 -2
apps/server/src/routes/v1/pageSubscribers/post.ts
··· 100 100 .returning() 101 101 .get(); 102 102 103 + const link = _page.customDomain 104 + ? `https://${_page.customDomain}/verify/${token}` 105 + : `https://${_page.slug}.openstatus.dev/verify/${token}`; 106 + 103 107 await sendEmail({ 104 108 react: SubscribeEmail({ 105 - domain: _page.slug, 106 - token, 109 + link, 107 110 page: _page.title, 108 111 }), 109 112 from: "OpenStatus <notification@notifications.openstatus.dev>",
+36
apps/status-page/src/app/(status-page)/[domain]/(public)/verify/[token]/layout.tsx
··· 1 + "use client"; 2 + 3 + import { 4 + Status, 5 + StatusContent, 6 + StatusDescription, 7 + StatusHeader, 8 + StatusTitle, 9 + } from "@/components/status-page/status"; 10 + import { useTRPC } from "@/lib/trpc/client"; 11 + import { useQuery } from "@tanstack/react-query"; 12 + import { useParams } from "next/navigation"; 13 + 14 + export default function EventLayout({ 15 + children, 16 + }: { 17 + children: React.ReactNode; 18 + }) { 19 + const { domain } = useParams<{ domain: string }>(); 20 + const trpc = useTRPC(); 21 + const { data: page } = useQuery( 22 + trpc.statusPage.get.queryOptions({ slug: domain }), 23 + ); 24 + 25 + if (!page) return null; 26 + 27 + return ( 28 + <Status> 29 + <StatusHeader> 30 + <StatusTitle>{page.title}</StatusTitle> 31 + <StatusDescription>{page.description}</StatusDescription> 32 + </StatusHeader> 33 + <StatusContent>{children}</StatusContent> 34 + </Status> 35 + ); 36 + }
+28 -41
apps/status-page/src/app/(status-page)/[domain]/(public)/verify/[token]/page.tsx
··· 1 1 "use client"; 2 2 3 - import { ButtonBack } from "@/components/button/button-back"; 4 3 import { 5 - Status, 6 - StatusHeader, 7 - StatusTitle, 8 - } from "@/components/status-page/status"; 4 + StatusBlankContainer, 5 + StatusBlankContent, 6 + StatusBlankLink, 7 + StatusBlankTitle, 8 + } from "@/components/status-page/status-blank"; 9 9 import { useTRPC } from "@/lib/trpc/client"; 10 10 import { cn } from "@/lib/utils"; 11 - import { useMutation, useQuery } from "@tanstack/react-query"; 11 + import { useMutation } from "@tanstack/react-query"; 12 12 import { useParams } from "next/navigation"; 13 13 import { useEffect } from "react"; 14 14 15 15 export default function VerifyPage() { 16 16 const trpc = useTRPC(); 17 17 const { token, domain } = useParams<{ token: string; domain: string }>(); 18 - const { data: page } = useQuery( 19 - trpc.statusPage.get.queryOptions({ slug: domain }), 20 - ); 21 - 22 18 const verifyEmailMutation = useMutation( 23 19 trpc.statusPage.verifyEmail.mutationOptions({}), 24 20 ); ··· 28 24 verifyEmailMutation.mutate({ slug: domain, token }); 29 25 }, [domain, token]); 30 26 31 - if (!page) return null; 27 + const title = verifyEmailMutation.isSuccess 28 + ? `All set to receive updates to ${verifyEmailMutation.data?.email}!` 29 + : verifyEmailMutation.isError 30 + ? verifyEmailMutation.error?.message || "Something went wrong" 31 + : "Hang tight - we're confirming your subscription"; 32 32 33 33 return ( 34 - <Status className="my-auto text-center"> 35 - <StatusHeader className="space-y-2 font-mono"> 36 - {verifyEmailMutation.isSuccess ? ( 37 - <StatusTitle> 38 - All set to receive updates from to {verifyEmailMutation.data?.email} 39 - </StatusTitle> 40 - ) : verifyEmailMutation.isError ? ( 41 - <StatusTitle 42 - className={cn( 43 - verifyEmailMutation.error?.data?.code === "NOT_FOUND" 44 - ? "text-destructive" 45 - : "", 46 - )} 47 - > 48 - {verifyEmailMutation.error?.message} 49 - </StatusTitle> 50 - ) : ( 51 - <StatusTitle> 52 - Hang tight - we're confirming your subscription 53 - </StatusTitle> 54 - )} 55 - <ButtonBack 34 + <StatusBlankContainer> 35 + <StatusBlankContent> 36 + <StatusBlankTitle 37 + className={cn({ 38 + "text-destructive": verifyEmailMutation.isError, 39 + "text-success": verifyEmailMutation.isSuccess, 40 + })} 41 + > 42 + {title} 43 + </StatusBlankTitle> 44 + <StatusBlankLink 56 45 href="../" 57 - className={cn( 58 - verifyEmailMutation.isSuccess || verifyEmailMutation.isError 59 - ? "visible" 60 - : "invisible", 61 - )} 62 - /> 63 - </StatusHeader> 64 - </Status> 46 + disabled={verifyEmailMutation.isPending || !verifyEmailMutation.data} 47 + > 48 + Go back 49 + </StatusBlankLink> 50 + </StatusBlankContent> 51 + </StatusBlankContainer> 65 52 ); 66 53 }
+1 -1
apps/status-page/src/components/chart/chart-legend-badge.tsx
··· 109 109 <itemConfig.icon /> 110 110 ) : ( 111 111 <div 112 - className="h-2 w-2 shrink-0 rounded-[2px]" 112 + className="h-2 w-2 shrink-0 rounded-(--radius-xs)" 113 113 style={{ 114 114 backgroundColor: item.color, 115 115 }}
+5 -3
apps/web/src/app/status-page/[domain]/subscribe/route.ts
··· 12 12 props: { params: Promise<{ domain: string }> }, 13 13 ) { 14 14 const params = await props.params; 15 - // 16 15 const data = await req.json(); 17 16 const result = z.object({ email: z.string().email() }).parse(data); 18 17 ··· 52 51 }) 53 52 .execute(); 54 53 54 + const link = pageData.customDomain 55 + ? `https://${pageData.customDomain}/verify/${token}` 56 + : `https://${pageData.slug}.openstatus.dev/verify/${token}`; 57 + 55 58 await sendEmail({ 56 59 react: SubscribeEmail({ 57 - domain: params.domain, 58 - token, 60 + link, 59 61 page: pageData.title, 60 62 }), 61 63 from: "OpenStatus <notification@notifications.openstatus.dev>",
+7 -3
apps/web/src/app/status-page/[domain]/verify/[token]/route.ts
··· 6 6 props: { params: Promise<{ domain: string; token: string }> }, 7 7 ) { 8 8 const params = await props.params; 9 - const pageId = await db 9 + const _page = await db 10 10 .select() 11 11 .from(page) 12 12 .where(eq(page.slug, params.domain)) 13 13 .get(); 14 - if (!pageId) { 14 + if (!_page) { 15 15 return new Response("Not found", { status: 401 }); 16 16 } 17 17 ··· 21 21 .where( 22 22 and( 23 23 eq(pageSubscriber.token, params.token), 24 - eq(pageSubscriber.pageId, pageId?.id), 24 + eq(pageSubscriber.pageId, _page?.id), 25 25 ), 26 26 ) 27 27 .get(); ··· 35 35 .set({ acceptedAt: new Date() }) 36 36 .where(eq(pageSubscriber.id, subscriber.id)) 37 37 .execute(); 38 + 39 + if (_page.customDomain) { 40 + return Response.redirect(`https://${_page.customDomain}`); 41 + } 38 42 39 43 return Response.redirect(`https://${params.domain}.openstatus.dev`); 40 44 }
+4 -8
packages/emails/emails/subscribe.tsx
··· 3 3 import { Body, Head, Html, Link, Preview } from "@react-email/components"; 4 4 5 5 interface SubscribeProps { 6 - token: string; 7 6 page: string; 8 - domain: string; 7 + link: string; 9 8 } 10 9 11 - const SubscribeEmail = ({ token, page, domain }: SubscribeProps) => { 10 + const SubscribeEmail = ({ page, link }: SubscribeProps) => { 12 11 return ( 13 12 <Html> 14 13 <Head> ··· 26 25 believe this is a mistake, please ignore this email. 27 26 </p> 28 27 <p> 29 - <a href={`https://${domain}.openstatus.dev/verify/${token}`}> 30 - Confirm subscription 31 - </a> 28 + <a href={link}>Confirm subscription</a> 32 29 </p> 33 30 <br />🚀 Powered by{" "} 34 31 <Link href="https://www.openstatus.dev">OpenStatus.dev</Link> ··· 38 35 }; 39 36 40 37 SubscribeEmail.PreviewProps = { 41 - token: "token", 42 38 page: "OpenStatus", 43 - domain: "slug", 39 + link: "https://slug.openstatus.dev/verify/token-xyz", 44 40 } satisfies SubscribeProps; 45 41 46 42 export default SubscribeEmail;