mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-12 20:35:24 +00:00
properly block access to pages if user isn't in an org
This commit is contained in:
parent
e7f8f51c05
commit
3ad6c2de48
9 changed files with 174 additions and 27 deletions
|
|
@ -409,7 +409,7 @@ export async function fetchStripeClientSecret(name: string, domain: string) {
|
|||
],
|
||||
mode: 'subscription',
|
||||
subscription_data: {
|
||||
trial_period_days: 14
|
||||
trial_period_days: 7,
|
||||
},
|
||||
customer_email: user.email!,
|
||||
payment_method_collection: 'if_required',
|
||||
|
|
|
|||
14
packages/web/src/app/components/footer.tsx
Normal file
14
packages/web/src/app/components/footer.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import Link from "next/link";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="w-full mt-auto py-4 flex flex-row justify-center items-center gap-4">
|
||||
<Link href="https://sourcebot.dev" className="text-gray-400 text-sm hover:underline">About</Link>
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
<Link href="https://github.com/sourcebot-dev/sourcebot/issues/new" className="text-gray-400 text-sm hover:underline">Support</Link>
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
<Link href="mailto:team@sourcebot.dev" className="text-gray-400 text-sm hover:underline">Contact Us</Link>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
43
packages/web/src/app/components/noOrganizationCard.tsx
Normal file
43
packages/web/src/app/components/noOrganizationCard.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
"use client"
|
||||
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { Building2 } from "lucide-react"
|
||||
|
||||
export function NoOrganizationCard() {
|
||||
const router = useRouter()
|
||||
|
||||
const handleOnboard = () => {
|
||||
router.push("/onboard")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center p-4">
|
||||
<Card className="w-[400px] animate-fade-in-up">
|
||||
<CardHeader className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<Building2 className="w-5 h-5 text-muted-foreground" />
|
||||
<CardTitle>No Organization</CardTitle>
|
||||
</div>
|
||||
<CardDescription className="text-base">You're not part of any organization yet.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Please complete the onboarding process to create an organization. Alternatively, ask your teammate to invite
|
||||
you to their organization.
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Something seem wrong? Contact us at team@sourcebot.dev
|
||||
</p>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button onClick={handleOnboard} className="w-full bg-[#6366F1] hover:bg-[#5558DD]">
|
||||
Complete Onboarding
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -6,17 +6,46 @@ import { PHProvider } from "./posthogProvider";
|
|||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { getCurrentUserOrg } from "@/auth";
|
||||
import { isServiceError } from "@/lib/utils";
|
||||
import { NavigationMenu } from "./components/navigationMenu";
|
||||
import { NoOrganizationCard } from "./components/noOrganizationCard";
|
||||
import { Footer } from "./components/footer";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Sourcebot",
|
||||
description: "Sourcebot",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const orgId = await getCurrentUserOrg();
|
||||
const byPassOrgCheck = (await headers()).get("x-bypass-org-check")! == "true";
|
||||
if (isServiceError(orgId) && !byPassOrgCheck) {
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
// @see : https://github.com/pacocoursey/next-themes?tab=readme-ov-file#with-app
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<body>
|
||||
<div className="flex flex-col items-center overflow-hidden min-h-screen">
|
||||
<SessionProvider>
|
||||
<NavigationMenu />
|
||||
<NoOrganizationCard />
|
||||
<Footer />
|
||||
</SessionProvider>
|
||||
</div>
|
||||
)
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<html
|
||||
lang="en"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { SymbolIcon } from "@radix-ui/react-icons";
|
|||
import { UpgradeToast } from "./components/upgradeToast";
|
||||
import Link from "next/link";
|
||||
import { getCurrentUserOrg } from "../auth"
|
||||
import { Footer } from "./components/footer";
|
||||
|
||||
|
||||
export default async function Home() {
|
||||
|
|
@ -108,13 +109,7 @@ export default async function Home() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
<footer className="w-full mt-auto py-4 flex flex-row justify-center items-center gap-4">
|
||||
<Link href="https://sourcebot.dev" className="text-gray-400 text-sm hover:underline">About</Link>
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
<Link href="https://github.com/sourcebot-dev/sourcebot/issues/new" className="text-gray-400 text-sm hover:underline">Support</Link>
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
<Link href="mailto:team@sourcebot.dev" className="text-gray-400 text-sm hover:underline">Contact Us</Link>
|
||||
</footer>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,16 @@ import { NavigationMenu } from "../components/navigationMenu";
|
|||
import { auth } from "@/auth";
|
||||
import { getUser } from "@/data/user";
|
||||
import { AcceptInviteButton } from "./components/acceptInviteButton"
|
||||
import Image from "next/image";
|
||||
import logoDark from "@/public/sb_logo_dark_large.png";
|
||||
import logoLight from "@/public/sb_logo_light_large.png";
|
||||
|
||||
interface RedeemPageProps {
|
||||
searchParams?: {
|
||||
invite_id?: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export default async function RedeemPage({ searchParams }: RedeemPageProps) {
|
||||
const invite_id = searchParams?.invite_id;
|
||||
|
||||
|
|
@ -24,11 +27,24 @@ export default async function RedeemPage({ searchParams }: RedeemPageProps) {
|
|||
|
||||
if (!invite) {
|
||||
return (
|
||||
<div>
|
||||
<NavigationMenu />
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
||||
<h1>This invite either expired or was revoked. Contact your organization owner.</h1>
|
||||
</div>
|
||||
<div className="flex flex-col justify-center items-center mt-8 mb-8 md:mt-18 w-full px-5">
|
||||
<div className="max-h-44 w-auto mb-4">
|
||||
<Image
|
||||
src={logoDark}
|
||||
className="h-18 md:h-40 w-auto hidden dark:block"
|
||||
alt={"Sourcebot logo"}
|
||||
priority={true}
|
||||
/>
|
||||
<Image
|
||||
src={logoLight}
|
||||
className="h-18 md:h-40 w-auto block dark:hidden"
|
||||
alt={"Sourcebot logo"}
|
||||
priority={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-center items-center">
|
||||
<h1>This invite has either expired or was revoked. Contact your organization owner.</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -44,12 +60,25 @@ export default async function RedeemPage({ searchParams }: RedeemPageProps) {
|
|||
if (user) {
|
||||
if (user.email !== invite.recipientEmail) {
|
||||
return (
|
||||
<div>
|
||||
<NavigationMenu />
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
||||
<h1>Sorry this invite does not belong to you.</h1>
|
||||
<div className="flex flex-col justify-center items-center mt-8 mb-8 md:mt-18 w-full px-5">
|
||||
<div className="max-h-44 w-auto mb-4">
|
||||
<Image
|
||||
src={logoDark}
|
||||
className="h-18 md:h-40 w-auto hidden dark:block"
|
||||
alt={"Sourcebot logo"}
|
||||
priority={true}
|
||||
/>
|
||||
<Image
|
||||
src={logoLight}
|
||||
className="h-18 md:h-40 w-auto block dark:hidden"
|
||||
alt={"Sourcebot logo"}
|
||||
priority={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-center items-center">
|
||||
<h1>This invite doesn't belong to you. You're currenly signed in with ${user.email}</h1>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
const orgName = await prisma.org.findUnique({
|
||||
|
|
@ -59,18 +88,30 @@ export default async function RedeemPage({ searchParams }: RedeemPageProps) {
|
|||
|
||||
if (!orgName) {
|
||||
return (
|
||||
<div>
|
||||
<NavigationMenu />
|
||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
||||
<h1>Organization not found. Please contact the invite sender.</h1>
|
||||
<div className="flex flex-col justify-center items-center mt-8 mb-8 md:mt-18 w-full px-5">
|
||||
<div className="max-h-44 w-auto mb-4">
|
||||
<Image
|
||||
src={logoDark}
|
||||
className="h-18 md:h-40 w-auto hidden dark:block"
|
||||
alt={"Sourcebot logo"}
|
||||
priority={true}
|
||||
/>
|
||||
<Image
|
||||
src={logoLight}
|
||||
className="h-18 md:h-40 w-auto block dark:hidden"
|
||||
alt={"Sourcebot logo"}
|
||||
priority={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-center items-center">
|
||||
<h1>This organization wasn't found. Please contact your organization owner.</h1>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<NavigationMenu />
|
||||
<div className="flex justify-between items-center h-screen px-6">
|
||||
<h1 className="text-2xl font-bold">You have been invited to org {orgName.name}</h1>
|
||||
<AcceptInviteButton invite={invite} userId={user.id} />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { Header } from "../components/header";
|
||||
import { auth } from "@/auth";
|
||||
import { getUser } from "@/data/user";
|
||||
import { prisma } from "@/prisma";
|
||||
|
|
|
|||
|
|
@ -111,3 +111,13 @@ export const getCurrentUserOrg = async () => {
|
|||
|
||||
return orgId;
|
||||
}
|
||||
|
||||
export const doesUserHaveOrg = async (userId: string) => {
|
||||
const orgs = await prisma.userToOrg.findMany({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
return orgs.length > 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,10 +23,22 @@ const apiMiddleware = (req: NextAuthRequest) => {
|
|||
}
|
||||
|
||||
const defaultMiddleware = (req: NextAuthRequest) => {
|
||||
// We're not able to check if the user doesn't belong to any orgs in the middleware, since we cannot call prisma. As a result, we do this check
|
||||
// in the root layout. However, there are certain endpoints (ex. login, redeem, onboard) that we want the user to be able to hit even if they don't
|
||||
// belong to an org. It seems like the easiest way to do this is to check for these paths here and pass in a flag to the root layout using the headers
|
||||
// https://github.com/vercel/next.js/discussions/43657#discussioncomment-5981981
|
||||
const bypassOrgCheck = req.nextUrl.pathname === "/login" || req.nextUrl.pathname === "/redeem" || req.nextUrl.pathname.includes("onboard");
|
||||
const requestheaders = new Headers(req.headers);
|
||||
requestheaders.set("x-bypass-org-check", bypassOrgCheck.toString());
|
||||
|
||||
// if we're trying to redeem an invite while not authed we continue to the redeem page so
|
||||
// that we can pipe the invite_id to the login page
|
||||
if (!req.auth && req.nextUrl.pathname === "/redeem") {
|
||||
return NextResponse.next();
|
||||
return NextResponse.next({
|
||||
request: {
|
||||
headers: requestheaders,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!req.auth && req.nextUrl.pathname !== "/login") {
|
||||
|
|
@ -37,7 +49,11 @@ const defaultMiddleware = (req: NextAuthRequest) => {
|
|||
return NextResponse.redirect(newUrl);
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
return NextResponse.next({
|
||||
request: {
|
||||
headers: requestheaders,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default auth(async (req) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue