Openstatus www.openstatus.dev
6
fork

Configure Feed

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

๐Ÿš€ create user and redirect (#40)

* ๐Ÿš€ create user and redirect

* ๐Ÿงน

* ๐Ÿงน

authored by

Thibault Le Ouay and committed by
GitHub
3eac2a9a 08cff6f9

+189 -27
+1 -1
apps/web/package.json
··· 9 9 "lint": "next lint" 10 10 }, 11 11 "dependencies": { 12 - "@clerk/nextjs": "4.21.10", 12 + "@clerk/nextjs": "4.21.14", 13 13 "@openstatus/api": "workspace:^", 14 14 "@openstatus/db": "workspace:^", 15 15 "@openstatus/tinybird": "workspace:*",
-1
apps/web/src/app/api/webhook/clerk/route.ts
··· 31 31 32 32 const ctx = createTRPCContext({ req }); 33 33 const caller = lambdaRouter.createCaller(ctx); 34 - 35 34 const event = r.data.type; 36 35 switch (event) { 37 36 case "user.created":
+7 -3
apps/web/src/app/app/(auth)/sign-in/[[...sign-in]]/page.tsx
··· 1 - import { SignIn } from "@clerk/nextjs"; 1 + "use client"; 2 + 3 + import { SignIn, useSignIn, useSignUp } from "@clerk/nextjs"; 2 4 3 5 export default function Page() { 4 - return <SignIn />; 5 - } 6 + const { isLoaded, signIn } = useSignIn(); 7 + 8 + return <SignIn redirectUrl={"/app"} />; 9 + }
+6 -3
apps/web/src/app/app/(auth)/sign-up/[[...sign-up]]/page.tsx
··· 1 - import { SignUp } from "@clerk/nextjs"; 1 + import { SignUp, useSignUp } from "@clerk/nextjs"; 2 2 3 3 export default function Page() { 4 - return <SignUp />; 5 - } 4 + const { signUp } = useSignUp(); 5 + 6 + console.log("signUp", signUp); 7 + return <SignUp redirectUrl={"/app"} />; 8 + }
+14
apps/web/src/app/app/(dashboard)/[workspaceId]/page.tsx
··· 1 + import * as React from "react"; 2 + import { auth, currentUser } from "@clerk/nextjs"; 3 + 4 + import { Header } from "@/components/header"; 5 + 6 + export default async function AppPage() { 7 + const user = await currentUser(); 8 + console.log({ user }); 9 + return ( 10 + <div> 11 + <Header title="App" description="Overview of all your websites" /> 12 + </div> 13 + ); 14 + }
apps/web/src/app/app/(dashboard)/endpoint/page.tsx apps/web/src/app/app/(dashboard)/[workspaceId]/endpoint/page.tsx
apps/web/src/app/app/(dashboard)/incident/page.tsx apps/web/src/app/app/(dashboard)/[workspaceId]/incident/page.tsx
apps/web/src/app/app/(dashboard)/monitor/page.tsx apps/web/src/app/app/(dashboard)/[workspaceId]/monitor/page.tsx
+3 -2
apps/web/src/components/layout/app-sidebar.tsx
··· 1 1 "use client"; 2 2 3 3 import Link from "next/link"; 4 - import { usePathname } from "next/navigation"; 4 + import { useParams, usePathname } from "next/navigation"; 5 5 6 6 import { pagesConfig } from "@/config/pages"; 7 7 import { cn } from "@/lib/utils"; ··· 9 9 10 10 export function AppSidebar() { 11 11 const pathname = usePathname(); 12 + const params = useParams(); 12 13 return ( 13 14 <ul className="grid gap-1"> 14 15 {pagesConfig.map(({ title, href, icon, disabled }) => { ··· 16 17 return ( 17 18 <li key={title} className="w-full"> 18 19 <Link 19 - href={href} 20 + href={`/app/${params.workspaceId}/${href}`} 20 21 className={cn( 21 22 "hover:bg-muted/50 hover:text-foreground text-muted-foreground group -mx-2 flex w-full min-w-[200px] items-center rounded-md border border-transparent px-3 py-1", 22 23 pathname === href &&
+4 -4
apps/web/src/config/pages.ts
··· 12 12 { 13 13 title: "Dashboard", 14 14 description: "Get an overview of what's hot.", 15 - href: "/app", 15 + href: "/", 16 16 icon: "layout-dashboard", 17 17 }, 18 18 { 19 19 title: "Endpoint", 20 20 description: "Keep track of all your endpoints.", 21 - href: "/app/endpoint", 21 + href: "/endpoint", 22 22 icon: "link", 23 23 }, 24 24 { 25 25 title: "Monitor", 26 26 description: "Check all the responses in one place.", 27 - href: "/app/monitor", 27 + href: "/monitor", 28 28 icon: "activity", 29 29 }, 30 30 { 31 31 title: "Incident", 32 32 description: "War room where you handle the incidents.", 33 - href: "/app/incident", 33 + href: "/incident", 34 34 icon: "siren", 35 35 disabled: true, 36 36 },
+35 -2
apps/web/src/middleware.ts
··· 1 - import { authMiddleware } from "@clerk/nextjs"; 1 + import { NextResponse } from "next/server"; 2 + import { authMiddleware, redirectToSignIn } from "@clerk/nextjs"; 3 + 4 + import { createTRPCContext } from "@openstatus/api"; 5 + import { edgeRouter } from "@openstatus/api/src/edge"; 6 + import { db, eq } from "@openstatus/db"; 7 + import { user, usersToWorkspaces } from "@openstatus/db/src/schema"; 2 8 3 9 export default authMiddleware({ 4 10 publicRoutes: [ ··· 15 21 "/api/checker/regions/(.*)", 16 22 "/api/checker/cron/10m", 17 23 ], 24 + async afterAuth(auth, req, evt) { 25 + // handle users who aren't authenticated 26 + if (!auth.userId && !auth.isPublicRoute) { 27 + return redirectToSignIn({ returnBackUrl: req.url }); 28 + } 29 + 30 + // redirect them to organization selection page 31 + if (auth.userId && req.nextUrl.pathname === "/app") { 32 + const userQuery = db 33 + .select() 34 + .from(user) 35 + .where(eq(user.tenantId, auth.userId)) 36 + .as("userQuery"); 37 + const result = await db 38 + .select() 39 + .from(usersToWorkspaces) 40 + .leftJoin(userQuery, eq(userQuery.id, usersToWorkspaces.userId)) 41 + .execute(); 42 + if (result.length) { 43 + const orgSelection = new URL( 44 + `/app/${result[0].users_to_workspaces.workspaceId}`, 45 + req.url, 46 + ); 47 + return NextResponse.redirect(orgSelection); 48 + } 49 + } 50 + }, 18 51 }); 19 52 20 53 export const config = { 21 - matcher: ["/((?!api|trpc|_next/static|_next/image|favicon.ico).*)"], 54 + matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"], 22 55 };
+23 -4
packages/api/src/router/clerk/webhook.ts
··· 1 1 import * as z from "zod"; 2 2 3 - import { user } from "@openstatus/db/src/schema"; 3 + import { user, usersToWorkspaces, workspace } from "@openstatus/db/src/schema"; 4 4 5 5 import { createTRPCRouter, publicProcedure } from "../../trpc"; 6 6 import { clerkEvent } from "./type"; ··· 14 14 export const webhookRouter = createTRPCRouter({ 15 15 userCreated: webhookProcedure.mutation(async (opts) => { 16 16 if (opts.input.data.type === "user.created") { 17 - await opts.ctx.db.insert(user).values({ 18 - tenantId: opts.input.data.data.id, 19 - }); 17 + // There's no primary key with drizzle I checked the tennant is not already in the database 18 + const alreadyExists = await opts.ctx.db 19 + .select({ id: user.id }) 20 + .from(user); 21 + 22 + if (alreadyExists.length) return; 23 + 24 + const userResult = await opts.ctx.db 25 + .insert(user) 26 + .values({ 27 + tenantId: opts.input.data.data.id, 28 + }) 29 + .execute(); 30 + const workspaceResult = await opts.ctx.db.insert(workspace).values({}); 31 + 32 + await opts.ctx.db 33 + .insert(usersToWorkspaces) 34 + .values({ 35 + userId: Number(userResult.insertId), 36 + workspaceId: Number(workspaceResult.insertId), 37 + }) 38 + .execute(); 20 39 } 21 40 }), 22 41 userUpdated: webhookProcedure.mutation(async (opts) => {
+6 -1
packages/api/src/router/workspace.ts
··· 8 8 export const workspaceRouter = createTRPCRouter({ 9 9 getUserWorkspace: protectedProcedure.query(async (opts) => { 10 10 return await opts.ctx.db.query.workspace.findMany({ 11 - with: { user: { where: eq(user.tenantId, opts.ctx.auth.userId) } }, 11 + with: { 12 + usersToWorkspaces: { 13 + where: eq(user.tenantId, opts.ctx.auth.userId), 14 + with: { user: true }, 15 + }, 16 + }, 12 17 }); 13 18 }), 14 19 getWorkspace: protectedProcedure
+19 -2
packages/db/src/schema/user.ts
··· 1 1 import { relations } from "drizzle-orm"; 2 - import { int, mysqlTable, timestamp, varchar } from "drizzle-orm/mysql-core"; 2 + import { 3 + int, 4 + mysqlTable, 5 + primaryKey, 6 + timestamp, 7 + varchar, 8 + } from "drizzle-orm/mysql-core"; 3 9 4 10 import { workspace } from "./workspace"; 5 11 ··· 12 18 }); 13 19 14 20 export const userRelations = relations(user, ({ many }) => ({ 15 - workspace: many(workspace), 21 + usersToWorkspaces: many(usersToWorkspaces), 16 22 })); 23 + 24 + export const usersToWorkspaces = mysqlTable( 25 + "users_to_workspaces", 26 + { 27 + userId: int("user_id").notNull(), 28 + workspaceId: int("workspace_id").notNull(), 29 + }, 30 + (t) => ({ 31 + pk: primaryKey(t.userId, t.workspaceId), 32 + }), 33 + );
+2 -2
packages/db/src/schema/workspace.ts
··· 8 8 } from "drizzle-orm/mysql-core"; 9 9 10 10 import { page } from "./page"; 11 - import { user } from "./user"; 11 + import { user, usersToWorkspaces } from "./user"; 12 12 13 13 export const workspace = mysqlTable("workspace", { 14 14 id: int("id").autoincrement().primaryKey(), ··· 22 22 23 23 export const workspaceRelations = relations(workspace, ({ many }) => ({ 24 24 page: many(page), 25 - user: many(user), 25 + usersToWorkspaces: many(usersToWorkspaces), 26 26 }));
+69 -2
pnpm-lock.yaml
··· 36 36 apps/web: 37 37 dependencies: 38 38 '@clerk/nextjs': 39 - specifier: 4.21.10 40 - version: 4.21.10(next@13.4.8)(react-dom@18.2.0)(react@18.2.0) 39 + specifier: 4.21.14 40 + version: 4.21.14(next@13.4.8)(react-dom@18.2.0)(react@18.2.0) 41 41 '@openstatus/api': 42 42 specifier: workspace:^ 43 43 version: link:../../packages/api ··· 568 568 tslib: 2.4.1 569 569 dev: false 570 570 571 + /@clerk/backend@0.24.0: 572 + resolution: {integrity: sha512-An1VKCFlFTROUYuELDwY5B6FmbVmIDuW837t0JfgbOLBYo1yoUgXZHB/wttGISaHt+CiOs1+CAI2U3iHxkaxhA==} 573 + engines: {node: '>=14'} 574 + dependencies: 575 + '@clerk/types': 3.46.1 576 + '@peculiar/webcrypto': 1.4.1 577 + '@types/node': 16.18.6 578 + deepmerge: 4.2.2 579 + node-fetch-native: 1.0.1 580 + snakecase-keys: 5.4.4 581 + tslib: 2.4.1 582 + dev: false 583 + 571 584 /@clerk/clerk-react@4.20.5(react@18.2.0): 572 585 resolution: {integrity: sha512-y5edxot+7adnyUqOGNgS7bEA3eoM53FMC8jV8VmUFvKLkOOMQpgfdo3/I7PWoTrRaNwqNWmhQGy1J3uqw8gv4g==} 573 586 engines: {node: '>=14'} ··· 581 594 tslib: 2.4.1 582 595 dev: false 583 596 597 + /@clerk/clerk-react@4.22.0(react@18.2.0): 598 + resolution: {integrity: sha512-vtGOszKYpDdV3cFtiG24jf1UdFGrXIfIzTyjh3aHd2mXGE2dIm8m04X3+mHBDStJFpJNzim8f+7HMLVeMRJhkA==} 599 + engines: {node: '>=14'} 600 + peerDependencies: 601 + react: '>=16' 602 + dependencies: 603 + '@clerk/shared': 0.19.1(react@18.2.0) 604 + '@clerk/types': 3.46.1 605 + react: 18.2.0 606 + swr: 1.3.0(react@18.2.0) 607 + tslib: 2.4.1 608 + dev: false 609 + 584 610 /@clerk/clerk-sdk-node@4.10.12: 585 611 resolution: {integrity: sha512-6LzLFEMZMnUE4O19XOW/6LAoAnHP7mZ2scMCib4dOvTRQxcZz1ql34ZMzewNYKrS6V5loQlslc1bnPlJ2zoX3g==} 586 612 engines: {node: '>=14'} ··· 596 622 tslib: 2.4.1 597 623 dev: false 598 624 625 + /@clerk/clerk-sdk-node@4.10.15: 626 + resolution: {integrity: sha512-fif8iwedLDFgllPLx8xwxA66ICbp7KjsSULqb8mcKXHPnZ/xL17eZSx4puHg4CEGBmOxY6IL5nbRj1+s+TfgqQ==} 627 + engines: {node: '>=14'} 628 + dependencies: 629 + '@clerk/backend': 0.24.0 630 + '@clerk/types': 3.46.1 631 + '@types/cookies': 0.7.7 632 + '@types/express': 4.17.14 633 + '@types/node-fetch': 2.6.2 634 + camelcase-keys: 6.2.2 635 + cookie: 0.5.0 636 + snakecase-keys: 3.2.1 637 + tslib: 2.4.1 638 + dev: false 639 + 599 640 /@clerk/nextjs@4.21.10(next@13.4.8)(react-dom@18.2.0)(react@18.2.0): 600 641 resolution: {integrity: sha512-5G02qZOBwbYvBxbHVPKpINyhZ1a3TLZ3noBExx5JHo8+UbpEp7ENd1TyFF/oO0EWG/VTbloP1Gegjm2XG9KeKw==} 601 642 engines: {node: '>=14'} ··· 615 656 tslib: 2.4.1 616 657 dev: false 617 658 659 + /@clerk/nextjs@4.21.14(next@13.4.8)(react-dom@18.2.0)(react@18.2.0): 660 + resolution: {integrity: sha512-r+s9EKG0CPPj31p6UZmxPQ2A3+JVE70PmBYiC/pRrF3Oo57pYJGVmBB+eUWVYaYJNcJxhYoYb/QLsL8Xl0dUVQ==} 661 + engines: {node: '>=14'} 662 + peerDependencies: 663 + next: '>=10' 664 + react: ^17.0.2 || ^18.0.0-0 665 + react-dom: ^17.0.2 || ^18.0.0-0 666 + dependencies: 667 + '@clerk/backend': 0.24.0 668 + '@clerk/clerk-react': 4.22.0(react@18.2.0) 669 + '@clerk/clerk-sdk-node': 4.10.15 670 + '@clerk/types': 3.46.1 671 + next: 13.4.8(@babel/core@7.22.5)(react-dom@18.2.0)(react@18.2.0) 672 + path-to-regexp: 6.2.1 673 + react: 18.2.0 674 + react-dom: 18.2.0(react@18.2.0) 675 + tslib: 2.4.1 676 + dev: false 677 + 618 678 /@clerk/shared@0.19.1(react@18.2.0): 619 679 resolution: {integrity: sha512-5RwASoff6CPgHcrM3kuvgUwp9qQa02zkjUdV8lNzQYEhojz6dfahLv3xZW3MS1UqDOIGrbrPviqi9pBmjpNKPA==} 620 680 peerDependencies: ··· 628 688 629 689 /@clerk/types@3.46.0: 630 690 resolution: {integrity: sha512-IooGZ64uARKg+GS6b38usrWqhzU0PcrDbZBaZm4JmtYd5CZbWABklBNkiXQhnnaKYdvHr5K8vhJd5spsJarNpw==} 691 + engines: {node: '>=14'} 692 + dependencies: 693 + csstype: 3.1.1 694 + dev: false 695 + 696 + /@clerk/types@3.46.1: 697 + resolution: {integrity: sha512-IA/iSXJJZym4Z6f9OcT9OJayK2opVhxUVJky15aY4iVsJCPScgApBuuzC7N4vfXwA6RI/wGebFbvmsEQ3oI/UA==} 631 698 engines: {node: '>=14'} 632 699 dependencies: 633 700 csstype: 3.1.1