Openstatus www.openstatus.dev

fix: unique workspace slug, header actions, server unkey middleware (#305)

* fix: unique workspace slug, header actions, server unkey middleware

* chore: add log

authored by

Maximilian Kaske and committed by
GitHub
a35bd7b4 04a7db81

+74 -37
+4 -6
apps/server/src/middleware.ts
··· 1 - import { Unkey } from "@unkey/api"; 1 + import { verifyKey } from "@unkey/api"; 2 2 import type { Context, Env, Next } from "hono"; 3 3 4 - const unkey = new Unkey({ token: "test-key" }); 5 - 6 4 export async function middleware(c: Context<Env, "/v1/*", {}>, next: Next) { 7 - const auth = c.req.header("x-openstatus-key"); 8 - if (!auth) return c.text("Unauthorized", 401); 5 + const key = c.req.header("x-openstatus-key"); 6 + if (!key) return c.text("Unauthorized", 401); 9 7 10 8 if (process.env.NODE_ENV === "production") { 11 - const { error, result } = await unkey.keys.verify({ key: auth }); 9 + const { error, result } = await verifyKey(key); 12 10 13 11 if (error) return c.text("Bad Request", 400); 14 12
+9 -5
apps/web/src/app/app/(dashboard)/[workspaceSlug]/incidents/page.tsx
··· 24 24 }); 25 25 return ( 26 26 <div className="grid gap-6 md:grid-cols-1 md:gap-8"> 27 - <Header title="Incidents" description="Overview of all your incidents."> 28 - <Button asChild> 29 - <Link href="./incidents/edit">Create</Link> 30 - </Button> 31 - </Header> 27 + <Header 28 + title="Incidents" 29 + description="Overview of all your incidents." 30 + actions={ 31 + <Button asChild> 32 + <Link href="./incidents/edit">Create</Link> 33 + </Button> 34 + } 35 + /> 32 36 {Boolean(incidents?.length) ? ( 33 37 <div className="col-span-full grid sm:grid-cols-6"> 34 38 <ul role="list" className="grid gap-4 sm:col-span-6">
+1 -2
apps/web/src/app/app/(dashboard)/[workspaceSlug]/integrations/page.tsx
··· 19 19 20 20 return ( 21 21 <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 22 - <Header title="Integrations" description="All our integrations"></Header> 23 - 22 + <Header title="Integrations" description="All our integrations" /> 24 23 <Container 25 24 title="Vercel" 26 25 key={"vercel"}
+14 -1
apps/web/src/app/app/(dashboard)/[workspaceSlug]/layout.tsx
··· 1 1 import * as React from "react"; 2 + import { notFound } from "next/navigation"; 2 3 3 4 import { Shell } from "@/components/dashboard/shell"; 4 5 import { AppHeader } from "@/components/layout/app-header"; 5 6 import { AppMenu } from "@/components/layout/app-menu"; 6 7 import { AppSidebar } from "@/components/layout/app-sidebar"; 8 + import { api } from "@/trpc/server"; 7 9 8 10 // TODO: make the container min-h-screen and the footer below! 9 - export default function AppLayout({ children }: { children: React.ReactNode }) { 11 + export default async function AppLayout({ 12 + children, 13 + params, 14 + }: { 15 + children: React.ReactNode; 16 + params: { workspaceSlug: string }; 17 + }) { 18 + const workspace = await api.workspace.getWorkspace.query({ 19 + slug: params.workspaceSlug, 20 + }); 21 + if (!workspace) return notFound(); // TODO: discuss if we should move to middleware 22 + 10 23 return ( 11 24 <div className="container relative mx-auto flex min-h-screen w-full flex-col items-center justify-center gap-6 p-4 lg:p-8"> 12 25 <AppHeader />
+1 -1
apps/web/src/app/app/(dashboard)/[workspaceSlug]/monitors/[id]/data/page.tsx
··· 48 48 49 49 return ( 50 50 <div className="grid gap-6 md:gap-8"> 51 - <Header title={monitor.name} description={monitor.url}></Header> 51 + <Header title={monitor.name} description={monitor.url} /> 52 52 {data && <DataTable columns={columns} data={data} />} 53 53 </div> 54 54 );
+13 -9
apps/web/src/app/app/(dashboard)/[workspaceSlug]/monitors/page.tsx
··· 31 31 32 32 return ( 33 33 <div className="grid gap-6 md:grid-cols-2 md:gap-8"> 34 - <Header title="Monitors" description="Overview of all your monitors."> 35 - <ButtonWithDisableTooltip 36 - tooltip="You reached the limits" 37 - asChild={!isLimit} 38 - disabled={isLimit} 39 - > 40 - <Link href="./monitors/edit">Create</Link> 41 - </ButtonWithDisableTooltip> 42 - </Header> 34 + <Header 35 + title="Monitors" 36 + description="Overview of all your monitors." 37 + actions={ 38 + <ButtonWithDisableTooltip 39 + tooltip="You reached the limits" 40 + asChild={!isLimit} 41 + disabled={isLimit} 42 + > 43 + <Link href="./monitors/edit">Create</Link> 44 + </ButtonWithDisableTooltip> 45 + } 46 + /> 43 47 {Boolean(monitors?.length) ? ( 44 48 monitors?.map((monitor, index) => ( 45 49 <Container
+10 -9
apps/web/src/app/app/(dashboard)/[workspaceSlug]/status-pages/page.tsx
··· 44 44 <Header 45 45 title="Status Page" 46 46 description="Overview of all your status pages." 47 - > 48 - <ButtonWithDisableTooltip 49 - tooltip="You reached the limits" 50 - asChild={!disableButton} 51 - disabled={disableButton} 52 - > 53 - <Link href="./status-pages/edit">Create</Link> 54 - </ButtonWithDisableTooltip> 55 - </Header> 47 + actions={ 48 + <ButtonWithDisableTooltip 49 + tooltip="You reached the limits" 50 + asChild={!disableButton} 51 + disabled={disableButton} 52 + > 53 + <Link href="./status-pages/edit">Create</Link> 54 + </ButtonWithDisableTooltip> 55 + } 56 + /> 56 57 {Boolean(pages?.length) ? ( 57 58 pages?.map((page, index) => ( 58 59 <Container
+6 -3
apps/web/src/components/dashboard/header.tsx
··· 4 4 interface HeaderProps extends React.HTMLAttributes<HTMLDivElement> { 5 5 title: string; 6 6 description?: string | null; 7 + actions?: React.ReactNode | React.ReactNode[]; 7 8 } 8 9 9 10 /** 10 11 * use `children` to include a Button e.g. 11 12 */ 12 - function Header({ title, description, className, children }: HeaderProps) { 13 + function Header({ title, description, className, actions }: HeaderProps) { 13 14 return ( 14 15 <div 15 16 className={cn( 16 - "col-span-full mr-12 flex justify-between lg:mr-0", 17 + "col-span-full mr-12 flex items-start justify-between lg:mr-0", 17 18 className, 18 19 )} 19 20 > ··· 23 24 <p className="text-muted-foreground">{description}</p> 24 25 ) : null} 25 26 </div> 26 - {children} 27 + {actions ? ( 28 + <div className="flex items-center gap-2">{actions}</div> 29 + ) : null} 27 30 </div> 28 31 ); 29 32 }
+16 -1
packages/api/src/router/clerk/webhook.ts
··· 36 36 .returning() 37 37 .get(); 38 38 39 - const slug = generateSlug(2); 39 + // guarantee the slug is unique accross our workspace entries 40 + let slug: string | undefined = undefined; 41 + 42 + while (!slug) { 43 + slug = generateSlug(2); 44 + const slugAlreadyExists = await opts.ctx.db 45 + .select() 46 + .from(workspace) 47 + .where(eq(workspace.slug, slug)) 48 + .get(); 49 + if (slugAlreadyExists) { 50 + console.log(`slug already exists: '${slug}'`); 51 + slug = undefined; 52 + } 53 + } 54 + 40 55 const workspaceResult = await opts.ctx.db 41 56 .insert(workspace) 42 57 .values({ slug, name: "" })