Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions apps/framework-editor/app/(pages)/auth/Unauthorized.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use client";

import { authClient } from "@/app/lib/auth-client";
import { Button } from "@comp/ui/button";
import { useRouter } from "next/navigation";
import { useState } from "react";

export const Unauthorized = () => {
const router = useRouter();
const [loading, setLoading] = useState(false);

const handleSignOut = async () => {
setLoading(true);
await authClient.signOut({
fetchOptions: {
onSuccess: () => {
router.push("/auth");
},
},
});
};

return (
<div className="flex justify-center items-center min-h-screen">
<div className="flex w-full max-w-md flex-col gap-4">
<h1 className="font-bold text-3xl text-center">
Oops, you don't belong here
</h1>
<p className="text-center">
You are not authorized to access this page. Please sign in
with a different account.
</p>
<Button onClick={handleSignOut} disabled={loading}>
{loading ? "Signing out..." : "Sign Out"}
</Button>
</div>
</div>
);
};
38 changes: 38 additions & 0 deletions apps/framework-editor/app/(pages)/auth/button-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { cn } from "@comp/ui/cn";
import { motion } from "framer-motion";

export const ButtonIcon = ({
className,
children,
loading,
isLoading,
}: {
className?: string;
children: React.ReactNode;
loading?: React.ReactNode;
isLoading: boolean;
}) => {
return (
<div className={cn("relative", className)}>
<motion.div
initial={{ opacity: 1, scale: 1 }}
animate={{
opacity: isLoading ? 0 : 1,
scale: isLoading ? 0.8 : 1,
}}
>
{children}
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 0.8 }}
animate={{
opacity: isLoading ? 1 : 0,
scale: isLoading ? 1 : 0.8,
}}
className="absolute left-0 top-0"
>
{loading}
</motion.div>
</div>
);
};
47 changes: 47 additions & 0 deletions apps/framework-editor/app/(pages)/auth/google-sign-in.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";

import { ButtonIcon } from "./button-icon";
import { authClient } from "@/app/lib/auth-client";
import { Button } from "@comp/ui/button";
import { Icons } from "@comp/ui/icons";
import { Loader2 } from "lucide-react";
import { useState } from "react";

export function GoogleSignIn({
inviteCode,
}: {
inviteCode?: string;
}) {
const [isLoading, setLoading] = useState(false);

const handleSignIn = async () => {
setLoading(true);
let redirectTo = "/";

if (inviteCode) {
redirectTo = `/api/auth/invitation?code=${inviteCode}`;
}

await authClient.signIn.social({
provider: "google",
});
};

return (
<Button
onClick={handleSignIn}
className="flex h-[40px] w-full space-x-2 px-6 py-4 font-medium active:scale-[0.98]"
>
{isLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<>
<ButtonIcon isLoading={isLoading}>
<Icons.Google />
</ButtonIcon>
<span>Sign in with Google</span>
</>
)}
</Button>
);
}
78 changes: 78 additions & 0 deletions apps/framework-editor/app/(pages)/auth/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { auth } from "@/app/lib/auth";
import type { Metadata } from "next";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import Balancer from "react-wrap-balancer";
import { GoogleSignIn } from "./google-sign-in";
import { Unauthorized } from "./Unauthorized";

export const metadata: Metadata = {
title: "Login | Comp AI",
};

export default async function Page() {
const session = await auth.api.getSession({
headers: await headers(),
});

const hasSession = session?.user;
const isAllowed = session?.user.email.split("@")[1] === "trycomp.ai";

if (hasSession && !isAllowed) {
return <Unauthorized />;
}

if (hasSession && isAllowed) {
redirect("/frameworks");
}

let preferredSignInOption: React.ReactNode;

if (process.env.AUTH_GOOGLE_ID && process.env.AUTH_GOOGLE_SECRET) {
preferredSignInOption = (
<div className="flex flex-col space-y-2">
<GoogleSignIn />
</div>
);
}

return (
<div className="flex min-h-screen justify-center items-center overflow-hidden p-6 md:p-0">
<div className="relative z-20 m-auto flex w-full max-w-[380px] flex-col py-8">
<div className="flex w-full flex-col relative">
<Balancer>
<h1 className="font-medium text-3xl pb-1">
Get Started with Comp AI
</h1>
<h2 className="font-medium text-xl pb-1">
Sign in to continue
</h2>
</Balancer>

<div className="pointer-events-auto mt-6 flex flex-col mb-6">
{preferredSignInOption}
</div>

<p className="text-xs text-muted-foreground">
By clicking continue, you acknowledge that you have read
and agree to the{" "}
<a
href="https://trycomp.ai/terms-and-conditions"
className="underline"
>
Terms and Conditions
</a>{" "}
and{" "}
<a
href="https://trycomp.ai/privacy-policy"
className="underline"
>
Privacy Policy
</a>
.
</p>
</div>
</div>
</div>
);
}
125 changes: 67 additions & 58 deletions apps/framework-editor/app/(pages)/controls/[controlId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,71 +1,80 @@
import { notFound } from 'next/navigation';
import { ControlDetailsClientPage } from './ControlDetailsClientPage';
import type { FrameworkEditorControlTemplate } from '@prisma/client';
import { db } from '@comp/db';
import { notFound, redirect } from "next/navigation";
import { ControlDetailsClientPage } from "./ControlDetailsClientPage";
import type { FrameworkEditorControlTemplate } from "@prisma/client";
import { db } from "@comp/db";
import { isAuthorized } from "@/app/lib/utils";

// Define a more specific type for the control details, including relations
// Prisma will generate a similar type, but defining it explicitly can be helpful for props
export type ControlDetailsWithRelations = FrameworkEditorControlTemplate & {
policyTemplates: { id: string; name: string }[];
requirements: (
{
id: string;
name: string;
framework: { id: string; name: string | null } | null; // Added id to framework
}
)[];
taskTemplates: { id: string; name: string }[];
policyTemplates: { id: string; name: string }[];
requirements: {
id: string;
name: string;
framework: { id: string; name: string | null } | null; // Added id to framework
}[];
taskTemplates: { id: string; name: string }[];
};

async function getControlDetails(id: string): Promise<ControlDetailsWithRelations | null> {
const control = await db.frameworkEditorControlTemplate.findUnique({
where: { id },
include: {
policyTemplates: {
select: {
id: true,
name: true,
},
orderBy: { name: 'asc' } // Optional: sort related items
},
requirements: {
select: {
id: true,
name: true,
framework: {
select: {
id: true, // Ensure framework id is selected
name: true,
}
}
},
orderBy: { name: 'asc' } // Optional: sort related items
},
taskTemplates: {
select: {
id: true,
name: true,
},
orderBy: { name: 'asc' } // Optional: sort related items
},
}
});
return control;
async function getControlDetails(
id: string,
): Promise<ControlDetailsWithRelations | null> {
const control = await db.frameworkEditorControlTemplate.findUnique({
where: { id },
include: {
policyTemplates: {
select: {
id: true,
name: true,
},
orderBy: { name: "asc" }, // Optional: sort related items
},
requirements: {
select: {
id: true,
name: true,
framework: {
select: {
id: true, // Ensure framework id is selected
name: true,
},
},
},
orderBy: { name: "asc" }, // Optional: sort related items
},
taskTemplates: {
select: {
id: true,
name: true,
},
orderBy: { name: "asc" }, // Optional: sort related items
},
},
});
return control;
}

interface ControlDetailsPageProps {
params: {
controlId: string;
};
params: {
controlId: string;
};
}

export default async function ControlDetailsPage({ params }: ControlDetailsPageProps) {
const controlDetails = await getControlDetails(params.controlId);
export default async function ControlDetailsPage({
params,
}: ControlDetailsPageProps) {
const isAllowed = await isAuthorized();

if (!controlDetails) {
notFound();
}
if (!isAllowed) {
redirect("/auth");
}

// Ensure the props passed to client component match its expectation or update client component props
return <ControlDetailsClientPage controlDetails={controlDetails} />;
}
const controlDetails = await getControlDetails(params.controlId);

if (!controlDetails) {
notFound();
}

// Ensure the props passed to client component match its expectation or update client component props
return <ControlDetailsClientPage controlDetails={controlDetails} />;
}
Loading
Loading