mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 13:01:56 +00:00
Merge pull request #42 from prowler-cloud/PRWLR-4393-Setup-NextAuth-client-session
Setup next auth -
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# openssl rand -base64 32
|
||||
AUTH_SECRET=your-secret-key
|
||||
@@ -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();
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./auth";
|
||||
export * from "./providers";
|
||||
|
||||
@@ -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 (
|
||||
<html suppressHydrationWarning lang="en">
|
||||
<head />
|
||||
<body
|
||||
suppressHydrationWarning
|
||||
className={cn(
|
||||
"min-h-screen bg-background font-sans antialiased",
|
||||
fontSans.variable,
|
||||
)}
|
||||
>
|
||||
<Providers themeProps={{ attribute: "class", defaultTheme: "dark" }}>
|
||||
{children}
|
||||
<Toaster />
|
||||
</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
|
||||
import { AuthForm } from "@/components/auth";
|
||||
|
||||
const SignIn = () => {
|
||||
return <AuthForm type="sign-in" />;
|
||||
};
|
||||
|
||||
export default SignIn;
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
|
||||
import { AuthForm } from "@/components/auth";
|
||||
|
||||
const SignUp = () => {
|
||||
return <AuthForm type="sign-up" />;
|
||||
};
|
||||
|
||||
export default SignUp;
|
||||
@@ -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 (
|
||||
<>
|
||||
<Header title="User Profile" icon="ci:users" />
|
||||
<Spacer y={4} />
|
||||
<Spacer y={6} />
|
||||
<pre>{JSON.stringify(session.user, null, 2)}</pre>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { handlers } from "@/auth.config";
|
||||
|
||||
export const { GET, POST } = handlers;
|
||||
+6
-3
@@ -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 (
|
||||
<NextUIProvider navigate={router.push}>
|
||||
<NextThemesProvider {...themeProps}>{children}</NextThemesProvider>
|
||||
</NextUIProvider>
|
||||
<SessionProvider>
|
||||
<NextUIProvider navigate={router.push}>
|
||||
<NextThemesProvider {...themeProps}>{children}</NextThemesProvider>
|
||||
</NextUIProvider>
|
||||
</SessionProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<any | null> {
|
||||
// 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);
|
||||
@@ -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 (
|
||||
<Button color="primary" type="submit" aria-disabled={pending}>
|
||||
{pending ? (
|
||||
<CircularProgress aria-label="Loading..." size="sm" />
|
||||
) : type === "sign-in" ? (
|
||||
"Log In"
|
||||
) : (
|
||||
"Sign Up"
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -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<z.infer<typeof formSchema>>({
|
||||
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<typeof formSchema>) => {
|
||||
// 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 (
|
||||
<div
|
||||
className="flex h-screen w-screen items-center justify-start overflow-hidden rounded-small bg-content1 p-2 sm:p-4 lg:p-8"
|
||||
style={{
|
||||
backgroundImage:
|
||||
"url(https://nextuipro.nyc3.cdn.digitaloceanspaces.com/components-images/black-background-texture-2.jpg)",
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
}}
|
||||
>
|
||||
{/* Brand Logo and ThemeSwitch */}
|
||||
<div className="absolute right-10 top-10">
|
||||
<div className="flex items-center self-center gap-4">
|
||||
<ThemeSwitch aria-label="Toggle theme" />
|
||||
<ProwlerExtended />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Testimonial */}
|
||||
<div className="absolute bottom-10 right-10 hidden md:block">
|
||||
<p className="max-w-xl text-right text-white/60 text-md">
|
||||
<span className="font-medium">“</span>
|
||||
Open Cloud Security
|
||||
<span className="font-medium">”</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex w-full max-w-sm flex-col gap-4 rounded-large bg-content1 px-8 pb-10 pt-6 shadow-small">
|
||||
<p className="pb-2 text-xl font-medium">
|
||||
{type === "sign-in" ? "Sign In" : "Sign Up"}
|
||||
</p>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex flex-col gap-3"
|
||||
// Fix TS types.
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
>
|
||||
{type === "sign-up" && (
|
||||
<>
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="firstName"
|
||||
type="text"
|
||||
label="Name"
|
||||
placeholder="Enter your name"
|
||||
/>
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="companyName"
|
||||
type="text"
|
||||
label="Company Name"
|
||||
placeholder="Enter your company name"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<CustomInput
|
||||
control={form.control}
|
||||
name="email"
|
||||
type="email"
|
||||
label="Email"
|
||||
placeholder="Enter your email"
|
||||
/>
|
||||
|
||||
<CustomInput control={form.control} password />
|
||||
|
||||
{type === "sign-in" && (
|
||||
<div className="flex items-center justify-between px-1 py-2">
|
||||
<Checkbox name="remember" size="sm">
|
||||
Remember me
|
||||
</Checkbox>
|
||||
<Link className="text-default-500" href="#">
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{/* <Input
|
||||
isRequired
|
||||
endContent={
|
||||
<button type="button" onClick={toggleConfirmVisibility}>
|
||||
{isConfirmVisible ? (
|
||||
<Icon
|
||||
className="pointer-events-none text-2xl text-default-400"
|
||||
icon="solar:eye-closed-linear"
|
||||
/>
|
||||
) : (
|
||||
<Icon
|
||||
className="pointer-events-none text-2xl text-default-400"
|
||||
icon="solar:eye-bold"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
label="Confirm Password"
|
||||
name="confirmPassword"
|
||||
placeholder="Confirm your password"
|
||||
type={isConfirmVisible ? "text" : "password"}
|
||||
variant="bordered"
|
||||
/> */}
|
||||
{type === "sign-up" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="termsAndConditions"
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
isRequired
|
||||
className="py-4"
|
||||
size="sm"
|
||||
checked={field.value === "true"}
|
||||
onChange={(e) =>
|
||||
field.onChange(e.target.checked ? "true" : "false")
|
||||
}
|
||||
>
|
||||
I agree with the
|
||||
<Link href="#" size="sm">
|
||||
Terms
|
||||
</Link>
|
||||
and
|
||||
<Link href="#" size="sm">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</Checkbox>
|
||||
</FormControl>
|
||||
<FormMessage className="text-system-error dark:text-system-error" />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{state?.message === "Credentials error" && (
|
||||
<div className="flex flex-row items-center gap-2 text-system-error">
|
||||
<NotificationIcon size={16} />
|
||||
<p className="text-s">Incorrect email or password</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<AuthButton type={type} />
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{type === "sign-in" && (
|
||||
<>
|
||||
<div className="flex items-center gap-4 py-2">
|
||||
<Divider className="flex-1" />
|
||||
<p className="shrink-0 text-tiny text-default-500">OR</p>
|
||||
<Divider className="flex-1" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
startContent={
|
||||
<Icon icon="flat-color-icons:google" width={24} />
|
||||
}
|
||||
variant="bordered"
|
||||
>
|
||||
Continue with Google
|
||||
</Button>
|
||||
<Button
|
||||
startContent={
|
||||
<Icon
|
||||
className="text-default-500"
|
||||
icon="fe:github"
|
||||
width={24}
|
||||
/>
|
||||
}
|
||||
variant="bordered"
|
||||
>
|
||||
Continue with Github
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{type === "sign-in" ? (
|
||||
<p className="text-center text-small">
|
||||
Need to create an account?
|
||||
<Link href="/sign-up">Sign Up</Link>
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-center text-small">
|
||||
Already have an account?
|
||||
<Link href="/sign-in">Log In</Link>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./AuthButton";
|
||||
export * from "./AuthForm";
|
||||
@@ -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 (
|
||||
<Button type="submit" area-disabled={pending}>
|
||||
{pending ? "Adding..." : "Add provider"}
|
||||
{pending ? <CircularProgress aria-label="Loading..." /> : "Add provider"}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<z.infer<typeof formSchema>>;
|
||||
isRequired?: boolean;
|
||||
} & (
|
||||
| {
|
||||
password?: false;
|
||||
name: FieldPath<z.infer<typeof formSchema>>;
|
||||
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 && (
|
||||
<button type="button" onClick={toggleVisibility}>
|
||||
<Icon
|
||||
className="pointer-events-none text-2xl text-default-400"
|
||||
icon={isVisible ? "solar:eye-closed-linear" : "solar:eye-bold"}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={control}
|
||||
name={inputName as FieldPath<z.infer<typeof formSchema>>}
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<FormControl>
|
||||
<Input
|
||||
isRequired={inputIsRequired}
|
||||
label={inputLabel}
|
||||
placeholder={inputPlaceholder}
|
||||
type={inputType}
|
||||
variant="bordered"
|
||||
endContent={endContent}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage className="text-system-error dark:text-system-error" />
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./CustomBox";
|
||||
export * from "./CustomInput";
|
||||
|
||||
@@ -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<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> = {
|
||||
name: TName;
|
||||
};
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue,
|
||||
);
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
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 <FormField>");
|
||||
}
|
||||
|
||||
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<FormItemContextValue>(
|
||||
{} as FormItemContextValue,
|
||||
);
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId();
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
});
|
||||
FormItem.displayName = "FormItem";
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField();
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-red-500 dark:text-red-900", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormLabel.displayName = "FormLabel";
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } =
|
||||
useFormField();
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormControl.displayName = "FormControl";
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField();
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn(
|
||||
"text-[0.8rem] text-slate-500 dark:text-slate-400",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
FormDescription.displayName = "FormDescription";
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField();
|
||||
const body = error ? String(error?.message) : children;
|
||||
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn(
|
||||
"text-[0.8rem] font-medium text-red-500 dark:text-red-900",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
);
|
||||
});
|
||||
FormMessage.displayName = "FormMessage";
|
||||
|
||||
export {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
useFormField,
|
||||
};
|
||||
@@ -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<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label };
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./Form";
|
||||
export * from "./Label";
|
||||
@@ -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 = () => {
|
||||
</div>
|
||||
<Spacer y={8} />
|
||||
|
||||
<UserAvatar
|
||||
userName={"User name"}
|
||||
position={"Software Engineer"}
|
||||
isCompact={isCompact}
|
||||
/>
|
||||
<Link href={"/profile"}>
|
||||
<UserAvatar
|
||||
userName={session?.user?.name ?? "Guest"}
|
||||
position={session?.user?.companyName ?? "Company Name"}
|
||||
isCompact={isCompact}
|
||||
/>{" "}
|
||||
</Link>
|
||||
|
||||
<ScrollShadow className="-mr-6 h-full max-h-full py-6 pr-6">
|
||||
<Sidebar
|
||||
@@ -123,9 +130,11 @@ export const SidebarWrap = () => {
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content="Log Out" isDisabled={!isCompact} placement="right">
|
||||
<Button
|
||||
aria-label="Log Out"
|
||||
onClick={() => logOut()}
|
||||
className={clsx(
|
||||
"justify-start text-default-500 data-[hover=true]:text-foreground",
|
||||
{
|
||||
|
||||
@@ -21,7 +21,7 @@ export const useLocalStorage = (
|
||||
setState(JSON.parse(value));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.error(error);
|
||||
}
|
||||
}, [key]);
|
||||
|
||||
@@ -38,7 +38,7 @@ export const useLocalStorage = (
|
||||
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
||||
setState(valueToStore);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./seed";
|
||||
export * from "./utils";
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import bcryptjs from "bcryptjs";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export const userMockData = [
|
||||
{
|
||||
id: uuidv4(), // Generate a unique UUID.
|
||||
email: "admin@prowler.com",
|
||||
name: "Admin Prowler",
|
||||
companyName: "Prowler",
|
||||
password: bcryptjs.hashSync("123123", 10),
|
||||
role: "admin",
|
||||
image: null,
|
||||
},
|
||||
{
|
||||
id: uuidv4(), // Generate a unique UUID.
|
||||
email: "user@prowler.com",
|
||||
name: "User Prowler",
|
||||
companyName: "Prowler",
|
||||
password: bcryptjs.hashSync("123123", 10),
|
||||
role: "user",
|
||||
image: null,
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,10 @@
|
||||
import NextAuth from "next-auth";
|
||||
|
||||
import { authConfig } from "./auth.config";
|
||||
|
||||
export default NextAuth(authConfig).auth;
|
||||
|
||||
export const config = {
|
||||
// https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher
|
||||
matcher: ["/((?!api|_next/static|_next/image|.*\\.png$).*)"],
|
||||
};
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
import { DefaultSession } from "next-auth";
|
||||
|
||||
declare module "next-auth" {
|
||||
interface Session {
|
||||
user: {
|
||||
id: string;
|
||||
firstName: string;
|
||||
companyName: string;
|
||||
email: string;
|
||||
role: string;
|
||||
image?: string;
|
||||
} & DefaultSession["user"];
|
||||
}
|
||||
}
|
||||
Generated
+1660
-543
File diff suppressed because it is too large
Load Diff
+12
-2
@@ -1,17 +1,21 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@nextui-org/react": "^2.4.2",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@nextui-org/react": "^2.4.6",
|
||||
"@nextui-org/system": "2.2.1",
|
||||
"@nextui-org/theme": "2.2.5",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-select": "^2.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.2.1",
|
||||
"@react-aria/ssr": "3.9.4",
|
||||
"@react-aria/visually-hidden": "3.8.12",
|
||||
"@tanstack/react-table": "^8.19.3",
|
||||
"add": "^2.0.6",
|
||||
"alert": "^6.0.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^3.6.0",
|
||||
@@ -19,20 +23,26 @@
|
||||
"intl-messageformat": "^10.5.0",
|
||||
"lucide-react": "^0.417.0",
|
||||
"next": "14.2.3",
|
||||
"next-auth": "^5.0.0-beta.20",
|
||||
"next-themes": "^0.2.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-hook-form": "^7.52.2",
|
||||
"recharts": "^2.13.0-alpha.4",
|
||||
"server-only": "^0.0.1",
|
||||
"shadcn-ui": "^0.2.3",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"uuid": "^10.0.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/react": "^5.0.1",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/node": "20.5.7",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.10.0",
|
||||
"@typescript-eslint/parser": "^7.10.0",
|
||||
"autoprefixer": "10.4.19",
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const authFormSchema = (type: string) =>
|
||||
z.object({
|
||||
// Sign Up
|
||||
companyName: type === "sign-in" ? z.string().optional() : z.string().min(3),
|
||||
firstName:
|
||||
type === "sign-in"
|
||||
? z.string().optional()
|
||||
: z
|
||||
.string()
|
||||
.min(3, {
|
||||
message: "The name must be at least 3 characters.",
|
||||
})
|
||||
.max(20),
|
||||
termsAndConditions:
|
||||
type === "sign-in"
|
||||
? z.enum(["true"]).optional()
|
||||
: z.enum(["true"], {
|
||||
errorMap: () => ({
|
||||
message: "You must accept the terms and conditions.",
|
||||
}),
|
||||
}),
|
||||
// both
|
||||
email: z.string().email(),
|
||||
password: z.string().min(6),
|
||||
});
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./auth";
|
||||
export * from "./components";
|
||||
|
||||
Reference in New Issue
Block a user