Skip to content

Commit a376b22

Browse files
authored
Merge pull request #652 from trycompai/mariano/comp-122-secure-the-framework-editor
[dev] [Marfuen] mariano/comp-122-secure-the-framework-editor
2 parents 7ed96b4 + 1e0667d commit a376b22

File tree

24 files changed

+841
-382
lines changed

24 files changed

+841
-382
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"use client";
2+
3+
import { authClient } from "@/app/lib/auth-client";
4+
import { Button } from "@comp/ui/button";
5+
import { useRouter } from "next/navigation";
6+
import { useState } from "react";
7+
8+
export const Unauthorized = () => {
9+
const router = useRouter();
10+
const [loading, setLoading] = useState(false);
11+
12+
const handleSignOut = async () => {
13+
setLoading(true);
14+
await authClient.signOut({
15+
fetchOptions: {
16+
onSuccess: () => {
17+
router.push("/auth");
18+
},
19+
},
20+
});
21+
};
22+
23+
return (
24+
<div className="flex justify-center items-center min-h-screen">
25+
<div className="flex w-full max-w-md flex-col gap-4">
26+
<h1 className="font-bold text-3xl text-center">
27+
Oops, you don't belong here
28+
</h1>
29+
<p className="text-center">
30+
You are not authorized to access this page. Please sign in
31+
with a different account.
32+
</p>
33+
<Button onClick={handleSignOut} disabled={loading}>
34+
{loading ? "Signing out..." : "Sign Out"}
35+
</Button>
36+
</div>
37+
</div>
38+
);
39+
};
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { cn } from "@comp/ui/cn";
2+
import { motion } from "framer-motion";
3+
4+
export const ButtonIcon = ({
5+
className,
6+
children,
7+
loading,
8+
isLoading,
9+
}: {
10+
className?: string;
11+
children: React.ReactNode;
12+
loading?: React.ReactNode;
13+
isLoading: boolean;
14+
}) => {
15+
return (
16+
<div className={cn("relative", className)}>
17+
<motion.div
18+
initial={{ opacity: 1, scale: 1 }}
19+
animate={{
20+
opacity: isLoading ? 0 : 1,
21+
scale: isLoading ? 0.8 : 1,
22+
}}
23+
>
24+
{children}
25+
</motion.div>
26+
<motion.div
27+
initial={{ opacity: 0, scale: 0.8 }}
28+
animate={{
29+
opacity: isLoading ? 1 : 0,
30+
scale: isLoading ? 1 : 0.8,
31+
}}
32+
className="absolute left-0 top-0"
33+
>
34+
{loading}
35+
</motion.div>
36+
</div>
37+
);
38+
};
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"use client";
2+
3+
import { ButtonIcon } from "./button-icon";
4+
import { authClient } from "@/app/lib/auth-client";
5+
import { Button } from "@comp/ui/button";
6+
import { Icons } from "@comp/ui/icons";
7+
import { Loader2 } from "lucide-react";
8+
import { useState } from "react";
9+
10+
export function GoogleSignIn({
11+
inviteCode,
12+
}: {
13+
inviteCode?: string;
14+
}) {
15+
const [isLoading, setLoading] = useState(false);
16+
17+
const handleSignIn = async () => {
18+
setLoading(true);
19+
let redirectTo = "/";
20+
21+
if (inviteCode) {
22+
redirectTo = `/api/auth/invitation?code=${inviteCode}`;
23+
}
24+
25+
await authClient.signIn.social({
26+
provider: "google",
27+
});
28+
};
29+
30+
return (
31+
<Button
32+
onClick={handleSignIn}
33+
className="flex h-[40px] w-full space-x-2 px-6 py-4 font-medium active:scale-[0.98]"
34+
>
35+
{isLoading ? (
36+
<Loader2 className="h-4 w-4 animate-spin" />
37+
) : (
38+
<>
39+
<ButtonIcon isLoading={isLoading}>
40+
<Icons.Google />
41+
</ButtonIcon>
42+
<span>Sign in with Google</span>
43+
</>
44+
)}
45+
</Button>
46+
);
47+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { auth } from "@/app/lib/auth";
2+
import type { Metadata } from "next";
3+
import { headers } from "next/headers";
4+
import { redirect } from "next/navigation";
5+
import Balancer from "react-wrap-balancer";
6+
import { GoogleSignIn } from "./google-sign-in";
7+
import { Unauthorized } from "./Unauthorized";
8+
9+
export const metadata: Metadata = {
10+
title: "Login | Comp AI",
11+
};
12+
13+
export default async function Page() {
14+
const session = await auth.api.getSession({
15+
headers: await headers(),
16+
});
17+
18+
const hasSession = session?.user;
19+
const isAllowed = session?.user.email.split("@")[1] === "trycomp.ai";
20+
21+
if (hasSession && !isAllowed) {
22+
return <Unauthorized />;
23+
}
24+
25+
if (hasSession && isAllowed) {
26+
redirect("/frameworks");
27+
}
28+
29+
let preferredSignInOption: React.ReactNode;
30+
31+
if (process.env.AUTH_GOOGLE_ID && process.env.AUTH_GOOGLE_SECRET) {
32+
preferredSignInOption = (
33+
<div className="flex flex-col space-y-2">
34+
<GoogleSignIn />
35+
</div>
36+
);
37+
}
38+
39+
return (
40+
<div className="flex min-h-screen justify-center items-center overflow-hidden p-6 md:p-0">
41+
<div className="relative z-20 m-auto flex w-full max-w-[380px] flex-col py-8">
42+
<div className="flex w-full flex-col relative">
43+
<Balancer>
44+
<h1 className="font-medium text-3xl pb-1">
45+
Get Started with Comp AI
46+
</h1>
47+
<h2 className="font-medium text-xl pb-1">
48+
Sign in to continue
49+
</h2>
50+
</Balancer>
51+
52+
<div className="pointer-events-auto mt-6 flex flex-col mb-6">
53+
{preferredSignInOption}
54+
</div>
55+
56+
<p className="text-xs text-muted-foreground">
57+
By clicking continue, you acknowledge that you have read
58+
and agree to the{" "}
59+
<a
60+
href="https://trycomp.ai/terms-and-conditions"
61+
className="underline"
62+
>
63+
Terms and Conditions
64+
</a>{" "}
65+
and{" "}
66+
<a
67+
href="https://trycomp.ai/privacy-policy"
68+
className="underline"
69+
>
70+
Privacy Policy
71+
</a>
72+
.
73+
</p>
74+
</div>
75+
</div>
76+
</div>
77+
);
78+
}
Lines changed: 67 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,80 @@
1-
import { notFound } from 'next/navigation';
2-
import { ControlDetailsClientPage } from './ControlDetailsClientPage';
3-
import type { FrameworkEditorControlTemplate } from '@prisma/client';
4-
import { db } from '@comp/db';
1+
import { notFound, redirect } from "next/navigation";
2+
import { ControlDetailsClientPage } from "./ControlDetailsClientPage";
3+
import type { FrameworkEditorControlTemplate } from "@prisma/client";
4+
import { db } from "@comp/db";
5+
import { isAuthorized } from "@/app/lib/utils";
56

67
// Define a more specific type for the control details, including relations
78
// Prisma will generate a similar type, but defining it explicitly can be helpful for props
89
export type ControlDetailsWithRelations = FrameworkEditorControlTemplate & {
9-
policyTemplates: { id: string; name: string }[];
10-
requirements: (
11-
{
12-
id: string;
13-
name: string;
14-
framework: { id: string; name: string | null } | null; // Added id to framework
15-
}
16-
)[];
17-
taskTemplates: { id: string; name: string }[];
10+
policyTemplates: { id: string; name: string }[];
11+
requirements: {
12+
id: string;
13+
name: string;
14+
framework: { id: string; name: string | null } | null; // Added id to framework
15+
}[];
16+
taskTemplates: { id: string; name: string }[];
1817
};
1918

20-
async function getControlDetails(id: string): Promise<ControlDetailsWithRelations | null> {
21-
const control = await db.frameworkEditorControlTemplate.findUnique({
22-
where: { id },
23-
include: {
24-
policyTemplates: {
25-
select: {
26-
id: true,
27-
name: true,
28-
},
29-
orderBy: { name: 'asc' } // Optional: sort related items
30-
},
31-
requirements: {
32-
select: {
33-
id: true,
34-
name: true,
35-
framework: {
36-
select: {
37-
id: true, // Ensure framework id is selected
38-
name: true,
39-
}
40-
}
41-
},
42-
orderBy: { name: 'asc' } // Optional: sort related items
43-
},
44-
taskTemplates: {
45-
select: {
46-
id: true,
47-
name: true,
48-
},
49-
orderBy: { name: 'asc' } // Optional: sort related items
50-
},
51-
}
52-
});
53-
return control;
19+
async function getControlDetails(
20+
id: string,
21+
): Promise<ControlDetailsWithRelations | null> {
22+
const control = await db.frameworkEditorControlTemplate.findUnique({
23+
where: { id },
24+
include: {
25+
policyTemplates: {
26+
select: {
27+
id: true,
28+
name: true,
29+
},
30+
orderBy: { name: "asc" }, // Optional: sort related items
31+
},
32+
requirements: {
33+
select: {
34+
id: true,
35+
name: true,
36+
framework: {
37+
select: {
38+
id: true, // Ensure framework id is selected
39+
name: true,
40+
},
41+
},
42+
},
43+
orderBy: { name: "asc" }, // Optional: sort related items
44+
},
45+
taskTemplates: {
46+
select: {
47+
id: true,
48+
name: true,
49+
},
50+
orderBy: { name: "asc" }, // Optional: sort related items
51+
},
52+
},
53+
});
54+
return control;
5455
}
5556

5657
interface ControlDetailsPageProps {
57-
params: {
58-
controlId: string;
59-
};
58+
params: {
59+
controlId: string;
60+
};
6061
}
6162

62-
export default async function ControlDetailsPage({ params }: ControlDetailsPageProps) {
63-
const controlDetails = await getControlDetails(params.controlId);
63+
export default async function ControlDetailsPage({
64+
params,
65+
}: ControlDetailsPageProps) {
66+
const isAllowed = await isAuthorized();
6467

65-
if (!controlDetails) {
66-
notFound();
67-
}
68+
if (!isAllowed) {
69+
redirect("/auth");
70+
}
6871

69-
// Ensure the props passed to client component match its expectation or update client component props
70-
return <ControlDetailsClientPage controlDetails={controlDetails} />;
71-
}
72+
const controlDetails = await getControlDetails(params.controlId);
73+
74+
if (!controlDetails) {
75+
notFound();
76+
}
77+
78+
// Ensure the props passed to client component match its expectation or update client component props
79+
return <ControlDetailsClientPage controlDetails={controlDetails} />;
80+
}

0 commit comments

Comments
 (0)