diff --git a/.env.template b/.env.template
new file mode 100644
index 0000000000..35ac3b9b58
--- /dev/null
+++ b/.env.template
@@ -0,0 +1,2 @@
+# openssl rand -base64 32
+AUTH_SECRET=your-secret-key
diff --git a/actions/auth.ts b/actions/auth.ts
new file mode 100644
index 0000000000..3c93021a12
--- /dev/null
+++ b/actions/auth.ts
@@ -0,0 +1,52 @@
+"use server";
+
+import { AuthError } from "next-auth";
+
+import { signIn, signOut } from "@/auth.config";
+// import { authFormSchema } from "@/types";
+
+// const formSchema = authFormSchema("sign-in");
+
+const defaultValues = {
+ email: "",
+ password: "",
+};
+
+// Fix TS types.
+export async function authenticate(prevState: any, formData: any) {
+ try {
+ await new Promise((resolve) => setTimeout(resolve, 2000));
+ await signIn("credentials", {
+ ...formData,
+ redirect: false,
+ });
+ return {
+ message: "Success",
+ };
+ } catch (error) {
+ if (error instanceof AuthError) {
+ switch (error.type) {
+ case "CredentialsSignin":
+ return {
+ message: "Credentials error",
+ errors: {
+ ...defaultValues,
+ credentials: "Incorrect email or password",
+ },
+ };
+ default:
+ return {
+ message: "Unknown error",
+ errors: {
+ ...defaultValues,
+ unknown: "Unknown error",
+ },
+ };
+ }
+ }
+ }
+}
+
+export async function logOut() {
+ await signOut();
+}
diff --git a/actions/index.ts b/actions/index.ts
index 5532383f5f..0085735bb3 100644
--- a/actions/index.ts
+++ b/actions/index.ts
@@ -1 +1,2 @@
+export * from "./auth";
export * from "./providers";
diff --git a/app/(auth)/layout.tsx b/app/(auth)/layout.tsx
new file mode 100644
index 0000000000..a9fdc8c394
--- /dev/null
+++ b/app/(auth)/layout.tsx
@@ -0,0 +1,60 @@
+import "@/styles/globals.css";
+
+import { Metadata, Viewport } from "next";
+import { redirect } from "next/navigation";
+
+import { auth } from "@/auth.config";
+import { Toaster } from "@/components/ui";
+import { fontSans } from "@/config/fonts";
+import { siteConfig } from "@/config/site";
+import { cn } from "@/lib";
+
+import { Providers } from "../providers";
+
+export const metadata: Metadata = {
+ title: {
+ default: siteConfig.name,
+ template: `%s - ${siteConfig.name}`,
+ },
+ description: siteConfig.description,
+ icons: {
+ icon: "/favicon.ico",
+ },
+};
+
+export const viewport: Viewport = {
+ themeColor: [
+ { media: "(prefers-color-scheme: light)", color: "white" },
+ { media: "(prefers-color-scheme: dark)", color: "black" },
+ ],
+};
+
+export default async function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const session = await auth();
+
+ if (session?.user) {
+ redirect("/");
+ }
+
+ return (
+
+
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/app/(auth)/sign-in/page.tsx b/app/(auth)/sign-in/page.tsx
new file mode 100644
index 0000000000..f012c3f358
--- /dev/null
+++ b/app/(auth)/sign-in/page.tsx
@@ -0,0 +1,9 @@
+import React from "react";
+
+import { AuthForm } from "@/components/auth";
+
+const SignIn = () => {
+ return ;
+};
+
+export default SignIn;
diff --git a/app/(auth)/sign-up/page.tsx b/app/(auth)/sign-up/page.tsx
new file mode 100644
index 0000000000..dfe04cadeb
--- /dev/null
+++ b/app/(auth)/sign-up/page.tsx
@@ -0,0 +1,9 @@
+import React from "react";
+
+import { AuthForm } from "@/components/auth";
+
+const SignUp = () => {
+ return ;
+};
+
+export default SignUp;
diff --git a/app/(prowler)/profile/page.tsx b/app/(prowler)/profile/page.tsx
new file mode 100644
index 0000000000..76c72a8800
--- /dev/null
+++ b/app/(prowler)/profile/page.tsx
@@ -0,0 +1,24 @@
+import { Spacer } from "@nextui-org/react";
+import { redirect } from "next/navigation";
+import React from "react";
+
+import { auth } from "@/auth.config";
+import { Header } from "@/components/ui";
+
+export default async function Profile() {
+ const session = await auth();
+
+ if (!session?.user) {
+ // redirect("/sign-in?returnTo=/profile");
+ redirect("/sign-in");
+ }
+
+ return (
+ <>
+
+
+
+ {JSON.stringify(session.user, null, 2)}
+ >
+ );
+}
diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts
new file mode 100644
index 0000000000..316cf49c14
--- /dev/null
+++ b/app/api/auth/[...nextauth]/route.ts
@@ -0,0 +1,3 @@
+import { handlers } from "@/auth.config";
+
+export const { GET, POST } = handlers;
diff --git a/app/providers.tsx b/app/providers.tsx
index 294f0647a6..942bce43dc 100644
--- a/app/providers.tsx
+++ b/app/providers.tsx
@@ -2,6 +2,7 @@
import { NextUIProvider } from "@nextui-org/system";
import { useRouter } from "next/navigation";
+import { SessionProvider } from "next-auth/react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import { ThemeProviderProps } from "next-themes/dist/types";
import * as React from "react";
@@ -15,8 +16,10 @@ export function Providers({ children, themeProps }: ProvidersProps) {
const router = useRouter();
return (
-
- {children}
-
+
+
+ {children}
+
+
);
}
diff --git a/auth.config.ts b/auth.config.ts
new file mode 100644
index 0000000000..93d8387346
--- /dev/null
+++ b/auth.config.ts
@@ -0,0 +1,86 @@
+import bcryptjs from "bcryptjs";
+import NextAuth, { type NextAuthConfig } from "next-auth";
+import Credentials from "next-auth/providers/credentials";
+import { z } from "zod";
+
+import { userMockData } from "./lib";
+
+async function getUser(email: string, password: string): Promise {
+ // Check if the user exists in the userMockData array.
+ const user = userMockData.find((user) => user.email === email);
+ if (!user) return null;
+
+ if (!bcryptjs.compareSync(password, user.password)) return null;
+
+ return {
+ id: user.id,
+ name: user.name,
+ companyName: user.companyName,
+ email: user.email,
+ role: user.role,
+ image: user.image,
+ };
+}
+
+export const authConfig = {
+ session: {
+ strategy: "jwt",
+ },
+ pages: {
+ signIn: "/sign-in",
+ newUser: "/sign-up",
+ },
+ callbacks: {
+ authorized({ auth, request: { nextUrl } }) {
+ const isLoggedIn = !!auth?.user;
+ const isOnDashboard = nextUrl.pathname.startsWith("/");
+ if (isOnDashboard) {
+ if (isLoggedIn) return true;
+ return false; // Redirect unauthenticated users to login page
+ } else if (isLoggedIn) {
+ return Response.redirect(new URL("/", nextUrl));
+ }
+ return true;
+ },
+
+ jwt({ token, user }) {
+ if (user) {
+ token.data = user;
+ }
+ return token;
+ },
+
+ session({ session, token }) {
+ session.user = token.data as any;
+ return session;
+ },
+ },
+ providers: [
+ Credentials({
+ name: "credentials",
+ credentials: {
+ email: { label: "email", type: "text" },
+ password: { label: "password", type: "password" },
+ },
+ async authorize(credentials) {
+ const parsedCredentials = z
+ .object({
+ email: z.string().email(),
+ password: z.string().min(6),
+ })
+ .safeParse(credentials);
+
+ if (!parsedCredentials.success) {
+ return null;
+ }
+ const { email, password } = parsedCredentials.data;
+
+ const user = await getUser(email, password);
+ if (!user) return null;
+ return user;
+ },
+ }),
+ ],
+} satisfies NextAuthConfig;
+
+export const { signIn, signOut, auth, handlers } = NextAuth(authConfig);
diff --git a/components/auth/AuthButton.tsx b/components/auth/AuthButton.tsx
new file mode 100644
index 0000000000..0eda81782a
--- /dev/null
+++ b/components/auth/AuthButton.tsx
@@ -0,0 +1,21 @@
+"use client";
+
+import { Button, CircularProgress } from "@nextui-org/react";
+import React from "react";
+import { useFormStatus } from "react-dom";
+
+export const AuthButton = ({ type }: { type: string }) => {
+ const { pending } = useFormStatus();
+
+ return (
+
+ );
+};
diff --git a/components/auth/AuthForm.tsx b/components/auth/AuthForm.tsx
new file mode 100644
index 0000000000..bef65115d0
--- /dev/null
+++ b/components/auth/AuthForm.tsx
@@ -0,0 +1,251 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { Icon } from "@iconify/react";
+import { Button, Checkbox, Divider, Link } from "@nextui-org/react";
+import { useRouter } from "next/navigation";
+import { useEffect } from "react";
+import { useFormState } from "react-dom";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+import { authenticate } from "@/actions";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormMessage,
+} from "@/components/ui/form";
+import { authFormSchema } from "@/types";
+
+import { NotificationIcon, ProwlerExtended } from "../icons";
+import { ThemeSwitch } from "../ThemeSwitch";
+import { CustomInput } from "../ui/custom";
+import { AuthButton } from "./AuthButton";
+
+export const AuthForm = ({ type }: { type: string }) => {
+ const formSchema = authFormSchema(type);
+ const router = useRouter();
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ email: "",
+ password: "",
+ },
+ });
+
+ const [state, dispatch] = useFormState(authenticate, undefined);
+
+ useEffect(() => {
+ if (state?.message === "Success") {
+ router.push("/");
+ }
+ }, [state]);
+
+ const onSubmit = async (data: z.infer) => {
+ // Do something with the form values
+ // this will be type-safe and validated
+ try {
+ // Sign-up logic will be here.
+ if (type === "sign-in") {
+ dispatch({
+ email: data.email.toLowerCase(),
+ password: data.password,
+ });
+ }
+ // if (type === "sign-up") {
+ // const newUser = await signUp(data);
+ // }
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ return (
+
+ {/* Brand Logo and ThemeSwitch */}
+
+
+ {/* Testimonial */}
+
+
+ “
+ Open Cloud Security
+ ”
+
+
+
+
+
+ {type === "sign-in" ? "Sign In" : "Sign Up"}
+
+
+
+
+
+ {type === "sign-in" && (
+ <>
+
+
+
+ }
+ variant="bordered"
+ >
+ Continue with Google
+
+
+ }
+ variant="bordered"
+ >
+ Continue with Github
+
+
+ >
+ )}
+ {type === "sign-in" ? (
+
+ Need to create an account?
+ Sign Up
+
+ ) : (
+
+ Already have an account?
+ Log In
+
+ )}
+
+
+ );
+};
diff --git a/components/auth/index.ts b/components/auth/index.ts
new file mode 100644
index 0000000000..26161c473c
--- /dev/null
+++ b/components/auth/index.ts
@@ -0,0 +1,2 @@
+export * from "./AuthButton";
+export * from "./AuthForm";
diff --git a/components/providers/ButtonAddProvider.tsx b/components/providers/ButtonAddProvider.tsx
index d4b82e1064..30f537ceae 100644
--- a/components/providers/ButtonAddProvider.tsx
+++ b/components/providers/ButtonAddProvider.tsx
@@ -1,6 +1,6 @@
"use client";
-import { Button } from "@nextui-org/react";
+import { Button, CircularProgress } from "@nextui-org/react";
import React from "react";
import { useFormStatus } from "react-dom";
@@ -8,7 +8,7 @@ export const ButtonAddProvider = () => {
const { pending } = useFormStatus();
return (
);
};
diff --git a/components/ui/custom/CustomInput.tsx b/components/ui/custom/CustomInput.tsx
new file mode 100644
index 0000000000..8a876eec62
--- /dev/null
+++ b/components/ui/custom/CustomInput.tsx
@@ -0,0 +1,84 @@
+"use client";
+
+import { Icon } from "@iconify/react";
+import { Input } from "@nextui-org/react";
+import React, { useState } from "react";
+import { Control, FieldPath } from "react-hook-form";
+import { z } from "zod";
+
+import { FormControl, FormField, FormMessage } from "@/components/ui/form";
+import { authFormSchema } from "@/types";
+
+const formSchema = authFormSchema("sign-up");
+
+type CustomInputProps = {
+ control: Control>;
+ isRequired?: boolean;
+} & (
+ | {
+ password?: false;
+ name: FieldPath>;
+ label: string;
+ type: "text" | "email";
+ placeholder: string;
+ }
+ | {
+ password: true;
+ name?: never;
+ label?: never;
+ type?: never;
+ placeholder?: never;
+ }
+);
+
+export const CustomInput = ({
+ control,
+ name,
+ type,
+ label,
+ placeholder,
+ password = false,
+ isRequired = true,
+}: CustomInputProps) => {
+ const [isVisible, setIsVisible] = useState(false);
+
+ const toggleVisibility = () => setIsVisible(!isVisible);
+
+ const inputName = password ? "password" : name!;
+ const inputLabel = password ? "Password" : label;
+ const inputType = password ? (isVisible ? "text" : "password") : type;
+ const inputPlaceholder = password ? "Enter your password" : placeholder;
+ const inputIsRequired = password ? true : isRequired;
+
+ const endContent = password && (
+
+ );
+
+ return (
+ >}
+ render={({ field }) => (
+ <>
+
+
+
+
+ >
+ )}
+ />
+ );
+};
diff --git a/components/ui/custom/index.ts b/components/ui/custom/index.ts
index 00732c48c8..126110ade6 100644
--- a/components/ui/custom/index.ts
+++ b/components/ui/custom/index.ts
@@ -1 +1,2 @@
export * from "./CustomBox";
+export * from "./CustomInput";
diff --git a/components/ui/form/Form.tsx b/components/ui/form/Form.tsx
new file mode 100644
index 0000000000..0e6296cfd4
--- /dev/null
+++ b/components/ui/form/Form.tsx
@@ -0,0 +1,186 @@
+"use client";
+
+import * as LabelPrimitive from "@radix-ui/react-label";
+import { Slot } from "@radix-ui/react-slot";
+import * as React from "react";
+import {
+ Controller,
+ ControllerProps,
+ FieldPath,
+ FieldValues,
+ FormProvider,
+ useFormContext,
+} from "react-hook-form";
+
+import { cn } from "@/lib/utils";
+
+import { Label } from "./Label";
+
+const Form = FormProvider;
+
+type FormFieldContextValue<
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath,
+> = {
+ name: TName;
+};
+
+const FormFieldContext = React.createContext(
+ {} as FormFieldContextValue,
+);
+
+const FormField = <
+ TFieldValues extends FieldValues = FieldValues,
+ TName extends FieldPath = FieldPath,
+>({
+ ...props
+}: ControllerProps) => {
+ return (
+
+
+
+ );
+};
+
+const useFormField = () => {
+ const fieldContext = React.useContext(FormFieldContext);
+ const itemContext = React.useContext(FormItemContext);
+ const { getFieldState, formState } = useFormContext();
+
+ const fieldState = getFieldState(fieldContext.name, formState);
+
+ if (!fieldContext) {
+ throw new Error("useFormField should be used within ");
+ }
+
+ const { id } = itemContext;
+
+ return {
+ id,
+ name: fieldContext.name,
+ formItemId: `${id}-form-item`,
+ formDescriptionId: `${id}-form-item-description`,
+ formMessageId: `${id}-form-item-message`,
+ ...fieldState,
+ };
+};
+
+type FormItemContextValue = {
+ id: string;
+};
+
+const FormItemContext = React.createContext(
+ {} as FormItemContextValue,
+);
+
+const FormItem = React.forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const id = React.useId();
+
+ return (
+
+
+
+ );
+});
+FormItem.displayName = "FormItem";
+
+const FormLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => {
+ const { error, formItemId } = useFormField();
+
+ return (
+
+ );
+});
+FormLabel.displayName = "FormLabel";
+
+const FormControl = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ ...props }, ref) => {
+ const { error, formItemId, formDescriptionId, formMessageId } =
+ useFormField();
+
+ return (
+
+ );
+});
+FormControl.displayName = "FormControl";
+
+const FormDescription = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => {
+ const { formDescriptionId } = useFormField();
+
+ return (
+
+ );
+});
+FormDescription.displayName = "FormDescription";
+
+const FormMessage = React.forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, children, ...props }, ref) => {
+ const { error, formMessageId } = useFormField();
+ const body = error ? String(error?.message) : children;
+
+ if (!body) {
+ return null;
+ }
+
+ return (
+
+ {body}
+
+ );
+});
+FormMessage.displayName = "FormMessage";
+
+export {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+ useFormField,
+};
diff --git a/components/ui/form/Label.tsx b/components/ui/form/Label.tsx
new file mode 100644
index 0000000000..9bdf1a5044
--- /dev/null
+++ b/components/ui/form/Label.tsx
@@ -0,0 +1,26 @@
+"use client";
+
+import * as LabelPrimitive from "@radix-ui/react-label";
+import { cva, type VariantProps } from "class-variance-authority";
+import * as React from "react";
+
+import { cn } from "@/lib/utils";
+
+const labelVariants = cva(
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
+);
+
+const Label = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef &
+ VariantProps
+>(({ className, ...props }, ref) => (
+
+));
+Label.displayName = LabelPrimitive.Root.displayName;
+
+export { Label };
diff --git a/components/ui/form/index.ts b/components/ui/form/index.ts
new file mode 100644
index 0000000000..ddee43fd25
--- /dev/null
+++ b/components/ui/form/index.ts
@@ -0,0 +1,2 @@
+export * from "./Form";
+export * from "./Label";
diff --git a/components/ui/sidebar/SidebarWrap.tsx b/components/ui/sidebar/SidebarWrap.tsx
index 2c009b1820..201cecdecf 100644
--- a/components/ui/sidebar/SidebarWrap.tsx
+++ b/components/ui/sidebar/SidebarWrap.tsx
@@ -3,10 +3,13 @@
import { Icon } from "@iconify/react";
import { Button, ScrollShadow, Spacer, Tooltip } from "@nextui-org/react";
import clsx from "clsx";
+import Link from "next/link";
import { usePathname } from "next/navigation";
+import { useSession } from "next-auth/react";
import React, { useCallback } from "react";
import { useMediaQuery } from "usehooks-ts";
+import { logOut } from "@/actions";
import { useLocalStorage } from "@/hooks/useLocalStorage";
import {
@@ -20,6 +23,8 @@ import { UserAvatar } from "./UserAvatar";
export const SidebarWrap = () => {
const pathname = usePathname();
+ const { data: session } = useSession();
+
const [isCollapsed, setIsCollapsed] = useLocalStorage("isCollapsed", false);
const isMobile = useMediaQuery("(max-width: 768px)");
@@ -64,11 +69,13 @@ export const SidebarWrap = () => {
-
+
+ {" "}
+
{
)}
+