mirror of
https://github.com/sourcebot-dev/sourcebot.git
synced 2025-12-14 21:35:25 +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',
|
mode: 'subscription',
|
||||||
subscription_data: {
|
subscription_data: {
|
||||||
trial_period_days: 14
|
trial_period_days: 7,
|
||||||
},
|
},
|
||||||
customer_email: user.email!,
|
customer_email: user.email!,
|
||||||
payment_method_collection: 'if_required',
|
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 { Toaster } from "@/components/ui/toaster";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { SessionProvider } from "next-auth/react";
|
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 = {
|
export const metadata: Metadata = {
|
||||||
title: "Sourcebot",
|
title: "Sourcebot",
|
||||||
description: "Sourcebot",
|
description: "Sourcebot",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
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 (
|
return (
|
||||||
<html
|
<html
|
||||||
lang="en"
|
lang="en"
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { SymbolIcon } from "@radix-ui/react-icons";
|
||||||
import { UpgradeToast } from "./components/upgradeToast";
|
import { UpgradeToast } from "./components/upgradeToast";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getCurrentUserOrg } from "../auth"
|
import { getCurrentUserOrg } from "../auth"
|
||||||
|
import { Footer } from "./components/footer";
|
||||||
|
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
|
|
@ -108,13 +109,7 @@ export default async function Home() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<footer className="w-full mt-auto py-4 flex flex-row justify-center items-center gap-4">
|
<Footer />
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,16 @@ import { NavigationMenu } from "../components/navigationMenu";
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { getUser } from "@/data/user";
|
import { getUser } from "@/data/user";
|
||||||
import { AcceptInviteButton } from "./components/acceptInviteButton"
|
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 {
|
interface RedeemPageProps {
|
||||||
searchParams?: {
|
searchParams?: {
|
||||||
invite_id?: string;
|
invite_id?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function RedeemPage({ searchParams }: RedeemPageProps) {
|
export default async function RedeemPage({ searchParams }: RedeemPageProps) {
|
||||||
const invite_id = searchParams?.invite_id;
|
const invite_id = searchParams?.invite_id;
|
||||||
|
|
||||||
|
|
@ -24,11 +27,24 @@ export default async function RedeemPage({ searchParams }: RedeemPageProps) {
|
||||||
|
|
||||||
if (!invite) {
|
if (!invite) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col justify-center items-center mt-8 mb-8 md:mt-18 w-full px-5">
|
||||||
<NavigationMenu />
|
<div className="max-h-44 w-auto mb-4">
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
<Image
|
||||||
<h1>This invite either expired or was revoked. Contact your organization owner.</h1>
|
src={logoDark}
|
||||||
</div>
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -44,12 +60,25 @@ export default async function RedeemPage({ searchParams }: RedeemPageProps) {
|
||||||
if (user) {
|
if (user) {
|
||||||
if (user.email !== invite.recipientEmail) {
|
if (user.email !== invite.recipientEmail) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col justify-center items-center mt-8 mb-8 md:mt-18 w-full px-5">
|
||||||
<NavigationMenu />
|
<div className="max-h-44 w-auto mb-4">
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
<Image
|
||||||
<h1>Sorry this invite does not belong to you.</h1>
|
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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
const orgName = await prisma.org.findUnique({
|
const orgName = await prisma.org.findUnique({
|
||||||
|
|
@ -59,18 +88,30 @@ export default async function RedeemPage({ searchParams }: RedeemPageProps) {
|
||||||
|
|
||||||
if (!orgName) {
|
if (!orgName) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col justify-center items-center mt-8 mb-8 md:mt-18 w-full px-5">
|
||||||
<NavigationMenu />
|
<div className="max-h-44 w-auto mb-4">
|
||||||
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
|
<Image
|
||||||
<h1>Organization not found. Please contact the invite sender.</h1>
|
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>
|
||||||
|
<div className="flex justify-center items-center">
|
||||||
|
<h1>This organization wasn't found. Please contact your organization owner.</h1>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<NavigationMenu />
|
|
||||||
<div className="flex justify-between items-center h-screen px-6">
|
<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>
|
<h1 className="text-2xl font-bold">You have been invited to org {orgName.name}</h1>
|
||||||
<AcceptInviteButton invite={invite} userId={user.id} />
|
<AcceptInviteButton invite={invite} userId={user.id} />
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import { Header } from "../components/header";
|
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { getUser } from "@/data/user";
|
import { getUser } from "@/data/user";
|
||||||
import { prisma } from "@/prisma";
|
import { prisma } from "@/prisma";
|
||||||
|
|
|
||||||
|
|
@ -111,3 +111,13 @@ export const getCurrentUserOrg = async () => {
|
||||||
|
|
||||||
return orgId;
|
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) => {
|
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
|
// 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
|
// that we can pipe the invite_id to the login page
|
||||||
if (!req.auth && req.nextUrl.pathname === "/redeem") {
|
if (!req.auth && req.nextUrl.pathname === "/redeem") {
|
||||||
return NextResponse.next();
|
return NextResponse.next({
|
||||||
|
request: {
|
||||||
|
headers: requestheaders,
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!req.auth && req.nextUrl.pathname !== "/login") {
|
if (!req.auth && req.nextUrl.pathname !== "/login") {
|
||||||
|
|
@ -37,7 +49,11 @@ const defaultMiddleware = (req: NextAuthRequest) => {
|
||||||
return NextResponse.redirect(newUrl);
|
return NextResponse.redirect(newUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.next();
|
return NextResponse.next({
|
||||||
|
request: {
|
||||||
|
headers: requestheaders,
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default auth(async (req) => {
|
export default auth(async (req) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue