User Authentication with Auth.js in Next.js App Router
Yu Hamada

Yu Hamada @jamescroissant

About: Hi,There! I’m a Full-Stack Web Developer in Toronto. React.js, TypeScript, Next.js, Node.js, Python, WordPress

Location:
Toronto, Canada
Joined:
Apr 3, 2024

User Authentication with Auth.js in Next.js App Router

Publish Date: Nov 10 '24
20 4

Table of Contents

Initial Setup

Implementing Authentication: Credentials and Google OAuth

Initial Setup

Install

npm install next-auth@beta
Enter fullscreen mode Exit fullscreen mode
// env.local
AUTH_SECRET=GENERATETD_RANDOM_VALUE
Enter fullscreen mode Exit fullscreen mode

Configure

NextAuthConfig settings

// src/auth.ts
import NextAuth from "next-auth"

export const config = {
  providers: [],
}

export const { handlers, signIn, signOut, auth } = NextAuth(config)
Enter fullscreen mode Exit fullscreen mode

It should be put inside of src folder

Providers means in Auth.js are services that can be used to sign in a user. There are four ways a user can be signed in.

  • Using a built-in OAuth Provider(e.g Github, Google, etc...)
  • Using a custom OAuth Provider
  • Using Email
  • Using Credentials

https://authjs.dev/reference/nextjs#providers

Route Handler Setup

// src/app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth" // Referring to the auth.ts we just created
export const { GET, POST } = handlers
Enter fullscreen mode Exit fullscreen mode

This file is used for setting route handler with Next.js App Router.

Middleware

// src/middleware.ts
import { auth } from "@/auth"

export default auth((req) => {
  // Add your logic here
}

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"], //  It's default setting
}
Enter fullscreen mode Exit fullscreen mode

Write inside the src folder.
If written outside the src folder, middleware will not work.

Middleware is a function that allows you to run code before a request is completed. It is particularly useful for protecting routes and handling authentication across your application.

Matcher is a configuration option for specifying which routes middleware should apply to. It helps optimize performance by running middleware only on necessary routes.
Example matcher: ['/dashboard/:path*'] applies middleware only to dashboard routes.

https://authjs.dev/getting-started/session-management/protecting?framework=express#nextjs-middleware

Get Session in Server Component

// src/app/page.tsx
import { auth } from "@/auth"
import { redirect } from "next/navigation"

export default async function page() {
  const session = await auth()

  if (!session) {
    redirect('/login')
  }

  return (
    <div>
      <h1>Hello World!</h1>
      <img src={session.user.image} alt="User Avatar" />
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Get Session in Client Component

// src/app/page.tsx
"use client"
import { useSession } from "next-auth/react"
import { useRouter } from "next/navigation"

export default async function page() {
  const { data: session } = useSession()
  const router = useRouter()

  if (!session.user) {
    router.push('/login')
  }

  return (
    <div>
      <h1>Hello World!</h1>
      <img src={session.user.image} alt="User Avatar" />
    </div>
  )
}

// src/app/layout.tsx
import type { Metadata } from "next";
import "./globals.css";
import { SessionProvider } from "next-auth/react"

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body>
        <SessionProvider>
          {children}
        </SessionProvider>
      </body>
    </html>
  );
}
Enter fullscreen mode Exit fullscreen mode

Folder structure

/src
  /app
    /api
      /auth
        [...nextauth]
          /route.ts  // Route Handler
    layout.tsx
    page.tsx

  auth.ts  // Provider, Callback, Logic etc
  middleware.ts  // A function before request
Enter fullscreen mode Exit fullscreen mode

Implementing Authentication: Credentials and Google OAuth

Setting up prisma

// prisma/schema.prisma

model User {
  id            String    @id @default(cuid())
  name          String?
  email         String?   @unique
  emailVerified DateTime?
  image         String?
  password      String?
  accounts      Account[]
  sessions      Session[]
}

model Account {
  // ... (standard Auth.js Account model)
}

model Session {
  // ... (standard Auth.js Session model)
}

// ... (other necessary models)

Enter fullscreen mode Exit fullscreen mode
// src/lib/prisma.ts

import { PrismaClient } from "@prisma/client"

const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }

export const prisma = globalForPrisma.prisma || new PrismaClient()

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma
Enter fullscreen mode Exit fullscreen mode

Credentials

Credentials, in the context of authentication, refer to a method of verifying a user's identity using information that the user provides, typically a username (or email) and password.

We can add credentials in src/auth.ts.

// src/auth.ts

import NextAuth from "next-auth";
import type { NextAuthConfig } from "next-auth";
import Credentials from "next-auth/providers/credentials"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/lib/prisma"
import bcrypt from 'bcryptjs';

export const config = {
  adapter: PrismaAdapter(prisma),
  providers: [
    Credentials({
      credentials: {
        email: { label: "Email", type: "text" },
        password: { label: "Password", type: "password" }
      },
      authorize: async (credentials): Promise<any> => {
        if (!credentials?.email || !credentials?.password) {
          return null;
        }

        try {
          const user = await prisma.user.findUnique({
            where: {
              email: credentials.email as string
            }
          })

          if (!user || !user.hashedPassword) {
            return null
          }

          const isPasswordValid = await bcrypt.compare(
            credentials.password as string,
            user.hashedPassword
          )

          if (!isPasswordValid) {
            return null
          }

          return {
            id: user.id as string,
            email: user.email as string,
            name: user.name as string,
          }
        } catch (error) {
          console.error('Error during authentication:', error)
          return null 
        }
      }
    })
  ],
  secret: process.env.AUTH_SECRET,
  pages: {
    signIn: '/login',
  },
  session: {
    strategy: "jwt",
  },
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.id = user.id
        token.email = user.email
        token.name = user.name
      }
      return token
    },
    async session({ session, token }) {
      if (session.user) {
        session.user.id = token.id as string
        session.user.email = token.email as string
        session.user.name = token.name as string
      }
      return session
    },
  },
} satisfies NextAuthConfig;

export const { handlers, auth, signIn, signOut } = NextAuth(config);

Enter fullscreen mode Exit fullscreen mode

adapters:

  • modules that connect your authentication system to your database or data storage solution.

secret:

  • This is a random string used to hash tokens, sign/encrypt cookies, and generate cryptographic keys.
  • It's crucial for security and should be kept secret.
  • In this case, it's set using an environment variable AUTH_SECRET.

pages:

  • This object allows you to customize the URLs for authentication pages.
  • In your example, signIn: '/login' means the sign-in page will be at the '/login' route instead of the default '/api/auth/signin'.

session:

  • This configures how sessions are handled.
  • strategy: "jwt" means JSON Web Tokens will be used for session management instead of database sessions.

callbacks:

  • These are functions that are called at various points in the authentication flow, allowing you to customize the process.

jwt callback:

  • This runs when a JWT is created or updated.
  • In your code, it's adding user information (id, email, name) to the token.

session callback:

  • This runs whenever a session is checked.
  • Your code is adding the user information from the token to the session object.

Add Google OAuth Provider

Setting Google OAuth application

Create new OAuth Client ID from GCP Console > APIs & Services > Credentials

Image description

Once created, save your Client ID and Client Secret for later use.

Setting Redirect URI

When we work in local, set http://localhost:3000/api/auth/callback/google

In production environment, just replace http://localhost:3000 with https://------.

Image description

Setup Environment Variables

// .env.local
GOOGLE_CLIENT_ID={CLIENT_ID}
GOOGLE_CLIENT_SECRET={CLIENT_SECRET}
Enter fullscreen mode Exit fullscreen mode

Setup Provider

// src/auth.ts

import GoogleProvider from "next-auth/providers/google"  // add this import.

export const { handlers, auth } = NextAuth({
  adapter: PrismaAdapter(prisma),
  providers: [
    CredentialsProvider({
      // ... (previous Credentials configuration)
    }),
    GoogleProvider({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
  ],
  // ... other configurations
})
Enter fullscreen mode Exit fullscreen mode

https://authjs.dev/getting-started/authentication/oauth

Creating Login and Signup page

//// UI pages
// src/app/login/LoginPage.tsx
import Link from 'next/link'
import { LoginForm } from '@/components/auth/LoginForm'
import { Separator } from '@/components/auth/Separator'
import { AuthLayout } from '@/components/auth/AuthLayout'
import { GoogleAuthButton } from '@/components/auth/GoogleAuthButton'

export default function LoginPage() {
  return (
    <AuthLayout title="Welcome Back!">
      <LoginForm />
      <Separator />
      <GoogleAuthButton text="Sign In with Google" />
      <div className="mt-6 text-center">
        <p className="text-sm text-gray-400">
          Do not have an account?{' '}
          <Link href="/signup" className="pl-1.5 font-medium text-[#3ba55c] hover:text-[#2d7d46]">
            Sign up
          </Link>
        </p>
      </div>
    </AuthLayout>
  )
}


// src/app/signup/SignupPage.tsx
import Link from 'next/link'
import { SignUpForm } from '@/components/auth/SignUpForm'
import { Separator } from '@/components/auth/Separator'
import { AuthLayout } from '@/components/auth/AuthLayout'
import { GoogleAuthButton } from '@/components/auth/GoogleAuthButton'

export default function SignUpPage() {
  return (
    <AuthLayout title="Welcome!">
      <SignUpForm />
      <Separator />
      <GoogleAuthButton text="Sign Up with Google" />
      <div className="mt-6 text-center">
        <p className="text-sm text-gray-400">
          Already have an account?{' '}
          <Link href="/login" className="pl-1.5 font-medium text-[#3ba55c] hover:text-[#2d7d46]">
            Sign in
          </Link>
        </p>
      </div>
    </AuthLayout>
  )
}

Enter fullscreen mode Exit fullscreen mode
//// Components
// src/components/auth/AuthLayout.tsx
import React from 'react'

interface AuthLayoutProps {
  children: React.ReactNode
  title: string
}

export const AuthLayout: React.FC<AuthLayoutProps> = ({ children, title }) => {
  return (
    <div className="min-h-screen bg-[#36393f] flex flex-col justify-center py-12 sm:px-6 lg:px-8">
      <div className="sm:mx-auto sm:w-full sm:max-w-md">
        <h2 className="mt-6 text-center text-3xl font-extrabold text-white">
          {title}
        </h2>
      </div>

      <div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
        <div className="bg-[#2f3136] py-8 px-4 shadow sm:rounded-lg sm:px-10">
          {children}
        </div>
      </div>
    </div>
  )
}

// src/components/auth/GoogleAuthButton.tsx
import { signIn } from "@/auth"
import { Button } from "@/components/ui/button"

interface GoogleAuthButtonProps {
  text: string
}

export const GoogleAuthButton: React.FC<GoogleAuthButtonProps> = ({ text }) => {
  return (
    <form
      action={async () => {
        "use server"
        await signIn("google", { redirectTo: '/' })
      }}
    >
      <Button
        className="my-1 w-full bg-white text-gray-700 hover:bg-slate-100"
      >
        <svg className="h-5 w-5 mr-2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
          <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
          <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
          <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
          <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
          <path d="M1 1h22v22H1z" fill="none"/>
        </svg>
        {text}
      </Button>
    </form>
  )
}

// src/components/auth/LoginForm.tsx
'use client'

import { useTransition } from "react"
import { useForm } from "react-hook-form"
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { LoginResolver, LoginSchema } from "@/schema/login"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { FormError } from "@/components/auth/FormError"
import { FormSuccess } from "@/components/auth/FormSuccess"
import { login } from "@/app/actions/auth/login"
import { Loader2 } from "lucide-react"

export const LoginForm = () => {
  const [error, setError] = useState<string | undefined>('')
  const [success, setSuccess] = useState<string | undefined>('')
  const [isPending, startTransition] = useTransition()
  const router = useRouter();

  const form = useForm<LoginSchema>({
    defaultValues: { email: '', password: ''},
    resolver: LoginResolver,
  })

  const onSubmit = (formData: LoginSchema) => { 
    startTransition(() => {
      setError('')
      setSuccess('')
      login(formData)
        .then((data) => {
          if (data.success) {
            setSuccess(data.success)
            router.push('/setup')
          } else if (data.error) {
            setError(data.error)
          }
        })
        .catch((data) => {
          setError(data.error)
        })
    })
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <div className="space-y-3">
          <FormField
            control={form.control}
            name="email"
            render={({ field }) => (
              <FormItem>
                <FormLabel className="text-white">Email address</FormLabel>
                <FormControl>
                  <Input
                    placeholder="Enter your email address" 
                    {...field} 
                    disabled={isPending}
                    className="bg-[#40444b] text-white border-gray-600 focus:border-2 focus:border-[#2d7d46]"
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          <FormField
            control={form.control}
            name="password"
            render={({ field }) => (
              <FormItem>
                <FormLabel className="text-white">Password</FormLabel>
                <FormControl>
                  <Input 
                    type="password"
                    placeholder="Enter your password" 
                    {...field} 
                    disabled={isPending}
                    className="bg-[#40444b] text-white border-gray-600 focus:border-2 focus:border-[#2d7d46]"
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          <FormError message={error} />
          <FormSuccess message={success} />
        </div>
        <Button
          type="submit"
          disabled={isPending}
          className="mt-8 w-full bg-[#3ba55c] hover:bg-[#2d7d46] text-white"
        >
          {isPending ? (
            <>
              <Loader2 className="mr-2 h-4 w-4 animate-spin" />
              Loading...
            </>
          ) : (
            'Login'
          )}
        </Button>
      </form>
    </Form>
  )
}


// src/components/auth/SignUpForm.tsx
'use client'

import { useTransition } from "react"
import { useForm } from "react-hook-form"
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
import { SignUpResolver, SignUpSchema } from "@/schema/signup"
import { useState } from "react"
import { useRouter } from "next/navigation"
import { FormError } from "@/components/auth/FormError"
import { FormSuccess } from "@/components/auth/FormSuccess"
import { signUp } from "@/app/actions/auth/signup"
import { Loader2 } from "lucide-react"

export const SignUpForm = () => {
  const [error, setError] = useState<string | undefined>('')
  const [success, setSuccess] = useState<string | undefined>('')
  const [isPending, startTransition] = useTransition()
  const router = useRouter();

  const form = useForm<SignUpSchema>({
    defaultValues: { name: '', email: '', password: ''},
    resolver: SignUpResolver,
  })

  const onSubmit = async (formData: SignUpSchema) => { 
    startTransition(() => {
      setError('')
      setSuccess('')
      signUp(formData)
        .then((data) => {
          if (data.success) {
            setSuccess(data.success)
            router.push('/login')
          } else if (data.error) {
            setError(data.error)
          }
        })
        .catch((data) => {
          setError(data.error)
        })
    })
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <div className="space-y-3">
          <FormField
            control={form.control}
            name="name"
            render={({ field }) => (
              <FormItem>
                <FormLabel className="text-white">Username</FormLabel>
                <FormControl>
                  <Input
                    placeholder="Enter your name" 
                    {...field} 
                    disabled={isPending}
                    className="bg-[#40444b] text-white border-gray-600 focus:border-2 focus:border-[#2d7d46]"
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          <FormField
            control={form.control}
            name="email"
            render={({ field }) => (
              <FormItem>
                <FormLabel className="text-white">Email address</FormLabel>
                <FormControl>
                  <Input
                    placeholder="Enter your email address" 
                    {...field} 
                    disabled={isPending}
                    className="bg-[#40444b] text-white border-gray-600 focus:border-2 focus:border-[#2d7d46]"
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          <FormField
            control={form.control}
            name="password"
            render={({ field }) => (
              <FormItem>
                <FormLabel className="text-white">Password</FormLabel>
                <FormControl>
                  <Input 
                    type="password"
                    placeholder="Enter your password" 
                    {...field} 
                    disabled={isPending}
                    className="bg-[#40444b] text-white border-gray-600 focus:border-2 focus:border-[#2d7d46]"
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          <FormError message={error} />
          <FormSuccess message={success} />
        </div>
        <Button
          type="submit"
          disabled={isPending}
          className="mt-8 w-full bg-[#3ba55c] hover:bg-[#2d7d46] text-white"
        >
          {isPending ? (
            <>
              <Loader2 className="mr-2 h-4 w-4 animate-spin" />
              Loading...
            </>
          ) : (
            'Sign Up'
          )}
        </Button>
      </form>
    </Form>
  )
}


// src/components/auth/FormSuccess.tsx
import { CheckCircledIcon } from "@radix-ui/react-icons";

interface FormSuccessProps {
  message?: string;
}

export const FormSuccess = ({ message }: FormSuccessProps) => {
  if (!message) return null;

  return (
    <div className="bg-emerald-500/15 p-3 rounded-md flex items-center gap-x-2 text-sm text-emerald-500">
      <CheckCircledIcon className="h-4 w-4" />
      <p>{message}</p>
    </div>
  );
};


// src/components/auth/FormError.tsx
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";

interface FormErrorProps {
  message?: string;
}

export const FormError = ({ message }: FormErrorProps) => {
  if (!message) return null;

  return (
    <div className="bg-destructive/15 p-3 rounded-md flex items-center gap-x-2 text-sm text-destructive">
      <ExclamationTriangleIcon className="h-4 w-4" />
      <p>{message}</p>
    </div>
  );
};


// src/components/auth/Separator.tsx
export const Separator = () => {
  return (
    <div className="my-4 relative">
      <div className="absolute inset-0 flex items-center">
        <div className="w-full border-t border-gray-600" />
      </div>
      <div className="relative flex justify-center text-sm">
        <span className="px-2 bg-[#2f3136] text-gray-400">Or continue with</span>
      </div>
    </div>
  )
}

Enter fullscreen mode Exit fullscreen mode
//// Actions
// src/app/actions/auth/login.ts
'use server'

import { LoginSchema, loginSchema } from '@/schema/login'
import { signIn } from '@/auth'


export const login = async (formData: LoginSchema) => {
  const email = formData['email'] as string
  const password = formData['password'] as string

  const validatedFields = loginSchema.safeParse({
    email: formData.email as string,
    password: formData.password as string,
  })

  if (!validatedFields.success) {
    return { 
      errors: validatedFields.error.flatten().fieldErrors,
      message: 'Login failed. Please check your input.'
    }
  }

  try {
    const result = await signIn('credentials', {
      redirect: false,
      callbackUrl: '/setup',
      email,
      password
    })

    if (result?.error) {
      return { error : 'Invalid email or password'}
    } else {
      return { success : 'Login successfully'}
    }
  } catch {
    return { error : 'Login failed'}
  }
}

// src/app/actions/auth/signup.ts
'use server'

import bcrypt from 'bcryptjs'
import { SignUpSchema, signUpSchema } from "@/schema/signup"
import { prisma } from '@/lib/prisma';

export const signUp = async (formData: SignUpSchema) => {
  const validatedFields = signUpSchema.safeParse({
    name: formData.name as string,
    email: formData.email as string,
    password: formData.password as string,
  })

  if (!validatedFields.success) {
    return { 
      errors: validatedFields.error.flatten().fieldErrors,
      message: 'Sign up failed. Please check your input.'
    }
  }

  try {
    const hashedPassword = await bcrypt.hash(validatedFields.data.password, 10);
    const existingUser = await prisma.user.findUnique({
      where: { email: validatedFields.data.email }
    })

    if (existingUser) {
      return { error: 'User already exists!' }
    }

    await prisma.user.create({
      data: {
        name:  validatedFields.data.name,
        email:  validatedFields.data.email,
        hashedPassword: hashedPassword,
      },

    });

    return { success: 'User created successfully!' }
  } catch (error) {
    return { error : `Sign up failed`}
  }
}
Enter fullscreen mode Exit fullscreen mode
//// Validations
// src/schema/login.ts
import * as z from 'zod';
import { zodResolver } from '@hookform/resolvers/zod'; 

export const loginSchema = z.object({
  email: z.string().email('This is not valid email address'),
  password: z
    .string()
    .min(8, { message: 'Password must contain at least 8 characters' }),
});

export type LoginSchema = z.infer<typeof loginSchema>;
export const LoginResolver = zodResolver(loginSchema);

// src/schema/signup.ts
import * as z from 'zod';
import { zodResolver } from '@hookform/resolvers/zod'; 

export const signUpSchema = z.object({
  name: z.string().min(1, {
    message: 'Name is required'
  }),
  email: z.string().email('This is not valid email address'),
  password: z
    .string()
    .min(8, { message: 'Password must contain at least 8 characters' }),
});

export type SignUpSchema = z.infer<typeof signUpSchema>;
export const SignUpResolver = zodResolver(signUpSchema);

Enter fullscreen mode Exit fullscreen mode
// src/middleware.ts
import { NextResponse } from 'next/server'
import { auth } from "@/auth"

export default auth((req) => {
  const { nextUrl, auth: session } = req
  const isLoggedIn = !!session
  const isLoginPage = nextUrl.pathname === "/login"
  const isSignUpPage = nextUrl.pathname === "/signup"
  const isSetupPage = nextUrl.pathname === "/setup"

  // If trying to access /setup while not logged in
  if (!isLoggedIn && isSetupPage) {
    const loginUrl = new URL("/login", nextUrl.origin)
    return NextResponse.redirect(loginUrl)
  }

  // If trying to access /login or /signup while already logged in
  if (isLoggedIn && (isLoginPage || isSignUpPage)) {
    const dashboardUrl = new URL("/", nextUrl.origin)
    return NextResponse.redirect(dashboardUrl)
  }

  // For all other cases, allow the request to pass through
  return NextResponse.next()
})

export const config = {
  matcher: ["/login","/signup", "/setup", "/"],
};

Enter fullscreen mode Exit fullscreen mode

Folder Structure

/src
  /app
    /actions
      /login.ts  // Login Action
      /signup.ts  // Signup Action
    /api
      /auth
        [...nextauth]
          /route.ts
    /login
      page.tsx  // Login Page
    /signup
      page.tsx  // Sign Up Page
    layout.tsx
    page.tsx

  /components
    /auth
      AuthLayout.tsx
      GoogleAuthButton.tsx
      LoginForm.tsx
      SignupForm.tsx
      FormSuccess.tsx
      FormError.tsx
      Separator.tsx

  /schema
    login.ts
    signup.ts

  auth.ts  // in src folder
  middleware.ts  // in src folder

Enter fullscreen mode Exit fullscreen mode

Comments 4 total

  • Marcel Weikum
    Marcel WeikumNov 11, 2024

    Thank you! I recently followed a tutorial on YouTube since Authjs is not really supporting credentials as login... But this is way more clear!

    • Yu Hamada
      Yu HamadaNov 12, 2024

      No worries! Thank you for checking it out!

      • Артем Долгополов
        Артем ДолгополовNov 14, 2024

        I've never used prisma before

        Could you explain how to set this and where should I place this file in the project?

        // prisma/schema.prisma

        • Yu Hamada
          Yu HamadaNov 15, 2024

          Thank you for asking question.
          In general, you should set prisma folder out of src folder like this.

          // code structure

          • src

            • app
            • page.tsx
            • layout.tsx
          • prisma

            • schema.prisma
Add comment