feat: Nextauth is working

This commit is contained in:
Pablo Lara
2024-08-27 18:37:45 +02:00
parent 1985b16824
commit b5a40d07cf
16 changed files with 1728 additions and 630 deletions
+32 -10
View File
@@ -3,25 +3,47 @@
import { AuthError } from "next-auth";
import { signIn, signOut } from "@/auth.config";
// import { authFormSchema } from "@/types";
export async function authenticate(
prevState: string | undefined,
formData: FormData,
) {
// 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));
console.log(formData);
await signIn("credentials", formData);
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 "Invalid credentials.";
return {
message: "Credentials error",
errors: {
...defaultValues,
credentials: "Incorrect email or password",
},
};
default:
return "Something went wrong.";
return {
message: "Unknown error",
errors: {
...defaultValues,
unknown: "Unknown error",
},
};
}
}
throw error;
}
}
+9 -1
View File
@@ -1,7 +1,9 @@
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";
@@ -27,11 +29,17 @@ export const viewport: Viewport = {
],
};
export default function RootLayout({
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await auth();
if (session?.user) {
redirect("/");
}
return (
<html suppressHydrationWarning lang="en">
<head />
+24
View File
@@ -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>
</>
);
}
+3
View File
@@ -0,0 +1,3 @@
import { handlers } from "@/auth.config";
export const { GET, POST } = handlers;
+6 -3
View File
@@ -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>
);
}
+65 -18
View File
@@ -3,39 +3,86 @@ 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;
// console.log({ auth }, "llegas o no");
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) })
.object({
email: z.string().email(),
password: z.string().min(6),
})
.safeParse(credentials);
if (!parsedCredentials.success) return null;
if (!parsedCredentials.success) {
return null;
}
const { email, password } = parsedCredentials.data;
console.log({ email, password }, "from AuthConfig.ts");
// Check the user using the email
// const user = await getUser(email);
// if (!user) return null;
// Compare passwords
// if (!bcryptjs.compareSync(password, user.password)) return null;
// Return the user object without the password field
// const { password: _, ...rest } = user;
// return rest;
const user = await getUser(email, password);
if (!user) return null;
// console.log({ user });
return user;
},
}),
],
} satisfies NextAuthConfig;
export const { signIn, signOut, auth } = NextAuth(authConfig);
export const { signIn, signOut, auth, handlers } = NextAuth(authConfig);
+2 -1
View File
@@ -6,10 +6,11 @@ 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..." />
<CircularProgress aria-label="Loading..." size="sm" />
) : type === "sign-in" ? (
"Log In"
) : (
+43 -10
View File
@@ -3,7 +3,8 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Icon } from "@iconify/react";
import { Button, Checkbox, Divider, Link } from "@nextui-org/react";
import { useState } from "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";
@@ -17,17 +18,14 @@ import {
} from "@/components/ui/form";
import { authFormSchema } from "@/types";
import { ProwlerExtended } from "../icons";
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 [user, setUser] = useState(null);
const formSchema = authFormSchema(type);
// const [state, dispath] = useFormState(authenticate, undefined);
const router = useRouter();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
@@ -36,10 +34,33 @@ export const AuthForm = ({ type }: { type: string }) => {
},
});
const onSubmit = (values: z.infer<typeof formSchema>) => {
const [state, dispatch] = useFormState(authenticate, undefined);
useEffect(() => {
if (state?.message === "Success") {
router.push("/");
}
}, [state]);
console.log(state, "el state desde el AuthForm");
const onSubmit = async (data: z.infer<typeof formSchema>) => {
// Do something with the form values
// this will be type-safe and validated
console.log(values);
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 signUpNew(data);
// }
} catch (error) {
console.log(error);
}
};
return (
@@ -106,6 +127,7 @@ export const AuthForm = ({ type }: { type: string }) => {
/>
<CustomInput control={form.control} password />
{type === "sign-in" && (
<div className="flex items-center justify-between px-1 py-2">
<Checkbox name="remember" size="sm">
@@ -150,7 +172,10 @@ export const AuthForm = ({ type }: { type: string }) => {
isRequired
className="py-4"
size="sm"
{...field}
checked={field.value === "true"}
onChange={(e) =>
field.onChange(e.target.checked ? "true" : "false")
}
>
I agree with the&nbsp;
<Link href="#" size="sm">
@@ -162,15 +187,23 @@ export const AuthForm = ({ type }: { type: string }) => {
</Link>
</Checkbox>
</FormControl>
<FormMessage />
<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">
+18 -20
View File
@@ -11,25 +11,25 @@ import { authFormSchema } from "@/types";
const formSchema = authFormSchema("sign-up");
type CustomInputProps =
type CustomInputProps = {
control: Control<z.infer<typeof formSchema>>;
isRequired?: boolean;
} & (
| {
control: Control<z.infer<typeof formSchema>>;
password?: false;
name: FieldPath<z.infer<typeof formSchema>>;
label: string;
type: "text" | "email";
placeholder: string;
isRequired?: boolean;
password?: false;
}
| {
control: Control<z.infer<typeof formSchema>>;
password: true;
name?: never;
label?: never;
type?: never;
placeholder?: never;
isRequired?: never;
};
}
);
export const CustomInput = ({
control,
@@ -50,6 +50,15 @@ export const CustomInput = ({
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}
@@ -63,22 +72,11 @@ export const CustomInput = ({
placeholder={inputPlaceholder}
type={inputType}
variant="bordered"
endContent={endContent}
{...field}
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>
) : null
}
/>
</FormControl>
<FormMessage />
<FormMessage className="text-system-error dark:text-system-error" />
</>
)}
/>
+1 -1
View File
@@ -14,7 +14,7 @@ import {
import { cn } from "@/lib/utils";
import { Label } from "./label";
import { Label } from "./Label";
const Form = FormProvider;
+12 -5
View File
@@ -3,7 +3,9 @@
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";
@@ -21,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)");
@@ -65,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
@@ -124,6 +130,7 @@ export const SidebarWrap = () => {
)}
</Button>
</Tooltip>
<Tooltip content="Log Out" isDisabled={!isCompact} placement="right">
<Button
aria-label="Log Out"
+21 -16
View File
@@ -1,18 +1,23 @@
import bcryptjs from "bcryptjs";
import { v4 as uuidv4 } from "uuid";
export const initialData = {
users: [
{
email: "admin@admin.com",
name: "Admin User",
password: bcryptjs.hashSync("123456", 10),
role: "admin",
},
{
email: "user@user.com",
name: "Prowler User",
password: bcryptjs.hashSync("123456", 10),
role: "user",
},
],
};
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,
},
];
+10
View File
@@ -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$).*)"],
};
+1477 -542
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -1,7 +1,7 @@
{
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@nextui-org/react": "^2.4.2",
"@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",
@@ -33,6 +33,7 @@
"shadcn-ui": "^0.2.3",
"tailwind-merge": "^2.5.2",
"tailwindcss-animate": "^1.0.7",
"uuid": "^10.0.0",
"zod": "^3.23.8"
},
"devDependencies": {
@@ -41,6 +42,7 @@
"@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",
+2 -2
View File
@@ -15,8 +15,8 @@ export const authFormSchema = (type: string) =>
.max(20),
termsAndConditions:
type === "sign-in"
? z.literal(true).optional()
: z.literal(true, {
? z.enum(["true"]).optional()
: z.enum(["true"], {
errorMap: () => ({
message: "You must accept the terms and conditions.",
}),