Kullanıcıların güvenliğini ve gizliliğini sağlamak her zamankinden daha önemli. Web kimlik doğrulaması bu açıdan çok önemli bir rol oynar ve kullanıcıların bilgilerini ve verilerini korumak için ilk savunma hattı olarak hizmet eder.
Bugün elimizde NextAuth.js gibi işimizi çok kolaylaştıran, Next.js uygulamalarımızda farklı kimlik doğrulama türlerini kolaylıkla uygulamamıza olanak tanıyan araçlara sahibiz.
Bu eğitim serisinde, Next.js 14'te temel bilgilerden başlayarak eksiksiz bir kimlik doğrulama sistemi oluşturacağız: e-posta ve parola ile kimlik doğrulama.
JavaScript ekosisteminde ve daha spesifik olarak Next.js ile geliştirilen uygulamalarda, kimlik doğrulamayı yönetmeye yönelik en öne çıkan kitaplıklardan biri NextAuth.js'dir.
Bu araç, uygulamalarımıza kimlik doğrulama eklemek için basit ve uygulaması kolay bir çözüm sunar. En iyi şey esnekliğidir; klasik e-posta ve şifre gibi kimlik bilgilerine dayalı kimlik doğrulamanın yanı sıra Google, Facebook ve Twitter gibi farklı kimlik doğrulama sağlayıcılarının entegrasyonuna olanak tanır.
Kimlik bilgisi kimlik doğrulaması, özellikle kimlik doğrulama süreci ve kullanıcı kimlik bilgilerinin saklanması üzerinde tam kontrole ihtiyaç duyduğunuz veya harici kimlik doğrulama sağlayıcılarına güvenmek istemediğiniz uygulamalarda kullanışlıdır.
src/
klasörünü kullanacağız.
npx create-next-app@latest
2. Projede ihtiyaç duyulan bağımlılıkları yükleyin. Bu sefer pnpm
kullanacağız; İstediğiniz paket yöneticisini kullanabilirsiniz.
pnpm install next-auth prisma react-hook-form zod, bcrypt
Kullanıcı arayüzü için Shadcn/ui kullanacağız.
pnpm dlx shadcn-ui@latest init
3. Proje için aşağıdaki yapıyı oluşturun:
... ├── prisma/ ... ├── src/ │ ├── actions/ │ │ └── auth-actions.tsx │ ├── app/ │ │ ├── api/auth/[...nextauth] │ │ │ └── route.ts │ │ ├── auth/ │ │ │ ├── signin │ │ │ │ └── page.tsx │ │ │ └── signup │ │ │ └── page.tsx │ │ │ ... │ ├── components/ │ │ ├── auth/ │ │ │ ├── auth-buttons.tsx │ │ │ ├── signin-form.tsx │ │ │ ├── signup-form.tsx │ │ │ └── user-nav.ts │ │ ├── ui/ │ │ │ ... │ │ ├── auth-provider.tsx │ │ ├── icons.tsx │ │ └── theme-provider.tsx │ ├── lib/ │ │ ├── prisma.ts │ │ ├── types.d.ts │ │ └── utils.ts │ ... ...
Veritabanındaki kullanıcıları depolamak ve almak için Prisma'yı kullanacağız. Prisma farklı veritabanı türlerinin entegrasyonuna izin verir, böylece ihtiyacınız olan herhangi bir veritabanını kullanabilirsiniz, biz SQLite kullanacağız.
npx prisma init --datasource-provider sqlite
Bu, şemalarıyla birlikte prizma klasörünü oluşturur.
Modelleri oluşturmak için @auth/prisma-adapter tarafından sağlanan modelleri kullanacağız ve bunları aşağıdaki gibi biraz özelleştireceğiz:
prisma/schema.prisma
:
generator client { provider = "prisma-client-js" output = "../../node_modules/.prisma/client" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } ... model User { id String @id @default(cuid()) username String password String email String @unique emailVerified DateTime? phone String? image String? } ...
3. İlkini oluşturmak
npx prisma migrate dev --name first-migration
Bu komutla Prisma klasöründe daha fazla dosya oluşturuldu ve veritabanı modellerle senkronize edildi.
Son olarak bir Prisma Client oluşturuyoruz.
lib/prisma.ts
:
import { PrismaClient } from "@prisma/client"; const globalForPrisma = global as unknown as { prisma: PrismaClient; }; export const prisma = globalForPrisma.prisma || new PrismaClient(); if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; export default prisma;
env
değişkenleri oluşturma.
# Secret key for NextAuth.js, used for encryption and session security. It should be a long, # random string unique to your application. NEXTAUTH_SECRET=XXX3B2CC28F123456C6934531CXXXXX # Base URL for your Next.js app, used by NextAuth.js for redirects and callbacks. NEXTAUTH_URL=http://localhost:3000/
Bu yol, tüm kimlik doğrulama isteklerinin (oturum açma, oturum kapatma ve satıcı geri aramaları gibi) tek bir uç noktada işlenmesine olanak tanır.
src/app/api/auth/[...nextauth]
... // Imports the Prisma User type for typing. import { User } from '@prisma/client' // Configuration of authentication options for NextAuth. export const authOptions: AuthOptions = { ... // Defines authentication providers, in this case, only credentials. providers: [ CredentialsProvider({ name: 'Credentials', // Defines the required fields for authentication. credentials: { email: { label: 'Email', type: 'text' }, password: { label: 'Password', type: 'password' }, }, // Function to authenticate the user with the provided credentials. async authorize(credentials) { // Searches for the user in the database by email. const user = await prisma.user.findUnique({ where: { email: credentials?.email, }, }) // Checks if the user exists and if the password is correct. if (!user) throw new Error('User name or password is not correct') if (!credentials?.password) throw new Error('Please Provide Your Password') const isPasswordCorrect = await bcrypt.compare(credentials.password, user.password) if (!isPasswordCorrect) throw new Error('User name or password is not correct') // Returns the user without including the password. const { password, ...userWithoutPass } = user return userWithoutPass }, }), ], } // Exports the configured NextAuth handler to handle GET and POST requests. const handler = NextAuth(authOptions) export { handler as GET, handler as POST }
4. Yetki Sağlayıcıyı Oluşturma.
src/components/auth-provider.tsx
:
'use client' import { SessionProvider } from 'next-auth/react' export default function AuthProvider({ children }: { children: React.ReactNode }) { return <SessionProvider>{children}</SessionProvider> }
Bu bileşen, kimlik doğrulama için NextAuth kullanan Next.js uygulamaları için oturum sağlayıcı görevi görür.
Bileşenlerin veya sayfaların bu sağlayıcıya sarılması, onlara oturum bağlamına erişim izni vererek alt bileşenlerin, kullanıcının geçerli oturumunun durumuna erişmek veya bu durumu değiştirmek için NextAuth kancalarını ve useSession
gibi işlevleri kullanmasına olanak tanır.
src/app/layout.tsx
:
/* All imports */ export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang='en' suppressHydrationWarning > <body className={`${inter.className} relative`}> <AuthProvider> <main>{children}</main> </AuthProvider> </body> </html> ) }
Would you like to use TypeScript (recommended)? yes Which style would you like to use? › Default Which color would you like to use as base color? › Slate Where is your global CSS file? › src/app/globals.css Do you want to use CSS variables for colors? › yes Are you using a custom tailwind prefix eg. tw-? Leave blank Where is your tailwind.config.js located? › tailwind.config.js Configure the import alias for components: › @/components Configure the import alias for utils: › @/lib/utils Are you using React Server Components? › yes
Dilediğiniz temayı kullanabilirsiniz.
2. Uygulama
src/app/layout.tsx
:
/* All imports */ export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang='en' suppressHydrationWarning > <body className={`${inter.className} relative`}> <AuthProvider> <ThemeProvider attribute='class' defaultTheme='dark' enableSystem disableTransitionOnChange > <main>{children}</main> <Toaster /> </ThemeProvider> </AuthProvider> </body> </html> ) }
2. Aşağıdaki shadcn/ui bileşenlerinin kurulması:
pnpm dlx shadcn-ui@latest add avatar button dropdown-menu form input label tabs toast
src/components/auth-buttons.tsx
:
'use client' import Link from 'next/link' import { signIn, useSession } from 'next-auth/react' import { Button } from '../ui/button' import { UserNav } from './user-nav' export default function AuthButtons() { // Use the useSession hook to access session data const { data: session } = useSession() return ( <div className='flex justify-end gap-4'> {session && session.user ? ( <UserNav user={session.user} /> ) : ( <> <Button size={'sm'} variant={'secondary'} onClick={() => signIn()} > Sign In </Button> <Button size={'sm'} asChild className='text-foreground' > <Link href='/auth/signup'>Sign Up</Link> </Button> </> )} </div> ) }
Bu bileşen, kullanıcının oturum durumuna göre kimlik doğrulama seçeneklerini dinamik olarak görüntüler. Kullanıcı oturum açmışsa kullanıcıya özel gezinme gösterilir. Aksi takdirde, sorunsuz bir kullanıcı deneyimi için Next.js'nin yönlendirmesinden ve NextAuth'un kimlik doğrulama özelliklerinden yararlanarak oturum açma veya kaydolma düğmeleri sunar.
Ana sayfayı değiştirin ve kimlik doğrulama düğmelerini ekleyin. Bu şekilde görünüyor:
src/components/auth/signup-form.tsx
:
'use client' /* all imports */ // Function to register a new user import { registerUser } from '@/actions/auth-actions' // Define the validation schema for the signup form using Zod const formSchema = z .object({ username: z .string({ required_error: 'Username is required', }) .min(2, 'User name must have at least 2 characters') .max(12, 'Username must be up to 12 characters') .regex(new RegExp('^[a-zA-Z0-9]+$'), 'No special characters allowed!'), email: z.string({ required_error: 'Email is required' }).email('Please enter a valid email address'), password: z .string({ required_error: 'Password is required' }) .min(6, 'Password must have at least 6 characters') .max(20, 'Password must be up to 20 characters'), confirmPassword: z .string({ required_error: 'Confirm your password is required' }) .min(6, 'Password must have at least 6 characters') .max(20, 'Password must be up to 20 characters'), }) .refine(values => values.password === values.confirmPassword, { message: "Password and Confirm Password doesn't match!", path: ['confirmPassword'], }) // Type inference for form inputs based on the Zod schema type InputType = z.infer<typeof formSchema> export function SignUpForm() { const [isLoading, setIsLoading] = useState(false) const { toast } = useToast() // Hook to show toast notifications // Initialize form handling with React Hook Form and Zod for validation const form = useForm<InputType>({ resolver: zodResolver(formSchema), }) // Handles form submission async function onSubmit(values: InputType) { try { setIsLoading(true) const { confirmPassword, ...user } = values // Exclude confirmPassword from data to be sent const response = await registerUser(user) // Register the user if ('error' in response) { toast({ title: 'Something went wrong!', description: response.error, variant: 'success', }) } else { toast({ title: 'Account Created!', description: 'Your account has been created successfully! You can now login.', }) } } catch (error) { console.error(error) toast({ title: 'Something went wrong!', description: "We couldn't create your account. Please try again later!", variant: 'destructive', }) } finally { setIsLoading(false) } } return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)}> <div className='grid gap-2'> // Each FormField validates and displays an input <FormField control={form.control} name='username' render={({ field }) => ( <FormItem> <FormControl> <div className='flex items-center gap-2'> <Icons.user className={`${form.formState.errors.username ? 'text-destructive' : 'text-muted-foreground'} `} /> <Input placeholder='Your Username' className={`${form.formState.errors.username && 'border-destructive bg-destructive/30'}`} {...field} /> </div> </FormControl> <FormMessage /> </FormItem> )} /> // Repeated structure for email, password, and confirmPassword with respective validations and icons <Button className='text-foreground mt-4' disabled={isLoading} // Disable button during form submission > {isLoading && <Icons.spinner className='mr-2 h-4 w-4 animate-spin' />} // Show loading icon if isLoading is true Sign Up </Button> </div> </form> </Form> ) }
Bu bileşen, form durumu yönetimi için react-hook-form'u ve şema doğrulaması için Zod'u kullanan bir kullanıcı kayıt formunu içerir.
Bu, bu teknolojiler hakkında bir eğitim olmasa da, yalnızca temellerini kullanıyoruz. Daha fazla sorunuz varsa belgelerine gidebilirsiniz.
Sayfaya birkaç stil daha ekledim ve şöyle görünüyor:
src/actions/auth-action/ts
:
'use server' /* all imports */ export async function registerUser(user: Omit<User, 'id' | 'phone' | 'emailVerified' | 'image'>) { try { // Attempt to create a new user record in the database const result = await prisma.user.create({ data: { ...user, // Hash the password before storing it password: await bcrypt.hash(user.password, 10), }, }) return result } catch (error) { console.log(error) // Handle known request errors from Prisma if (error instanceof Prisma.PrismaClientKnownRequestError) { // Check for unique constraint failure (eg, email already exists) if (error.code === 'P2002') { return { error: 'Email already exists.' } } } // Return a generic error message for any other errors return { error: 'An unexpected error occurred.' } } }
registerUser
işlevi, id
, phone
, emailVerified
ve image
gibi alanlar hariç olmak üzere, sağlanan kullanıcı bilgileriyle veritabanında bir kayıt oluşturarak yeni bir kullanıcıyı güvenli bir şekilde kaydetmek için tasarlanmıştır.
Güvenli depolama amacıyla kullanıcının şifresini karma hale getirmek için bcrypt'i kullanır.
Kaydolma işlemimizi test etmek ve kullanıcının doğru şekilde kaydolduğunu doğrulamak için bazı geri aramalar eklememiz gerekir; bunlar, kimlik doğrulama ve oturum yönetimi davranışını özelleştirmenize olanak tanıyan işlevlerdir.
src/app/api/auth/[...nextauth]
:
... export const authOptions: AuthOptions = { // Define custom pages for authentication flow pages: { signIn: '/auth/signin', // Custom sign-in page }, // Configure session management to use JSON Web Tokens (JWT) session: { strategy: 'jwt', }, // JWT configuration, including secret for token signing jwt: { secret: process.env.NEXTAUTH_SECRET, // Secret used to sign the JWT, stored in environment variables }, ... // Callbacks for customizing JWT and session behaviors callbacks: { // Callback to modify the JWT content. Adds user information if available. async jwt({ token, user }) { if (user) token.user = user as User // Cast user object to User type and assign to token return token }, // Callback to modify session content. Adds user information to the session. async session({ token, session }) { session.user = token.user // Assign user information from token to session return session }, }, } ...
Geri arama jwt
: Bu geri arama, kimlik doğrulama yaşam döngüsü sırasında bir JSON Web Token (JWT) oluşturulduğunda veya güncellendiğinde gerçekleştirilir. İmzalanmadan ve istemciye gönderilmeden veya sunucuda saklanmadan önce belirtecin içeriğini değiştirmenize olanak tanır.
Bu, uygulama mantığınızla alakalı olabilecek ek bilgileri tokena eklemek için kullanışlıdır.
Geri arama session
: Bu geri arama, sunucu tarafı oluşturma veya korumalı API istekleri gibi oturum verileri her okunduğunda çağrılır. Oturum verilerinin istemciye gönderilmeden önce değiştirilmesine olanak tanır.
Bu, özellikle JWT'de veya diğer ölçütlerde depolanan bilgilere dayalı olarak oturum verilerini eklemek veya değiştirmek için kullanışlıdır.
Son olarak NextAuth Session
ve JWT
tür tanımlarını ek kullanıcı bilgilerini içerecek şekilde genişletmemiz gerekiyor.
src/lib/types.d.ts
:
import { User } from '@prisma/client' declare module 'next-auth' { interface Session { user: User } } declare module 'next-auth/jwt' { interface JWT { user: User } }
Şimdi formu doldurup gönderirsek başarı kadehini görebileceğiz. Kullanıcının veritabanına kaydedildiğini doğrulamak için aşağıdaki komutla Prisma'nın oluşturduğu tabloları grafiksel olarak görebiliriz:
nxp prisma studio
Aşağıdaki rotayı kullanıma sunacağız http://localhost:5555
src/components/auth/user-nav.tsx
:
/* all imports */ interface Props { user: User // Expect a user object of type User from Prisma client } export function UserNav({ user }: Props) { return ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant='ghost' className='relative h-8 w-8 rounded-full' > <Avatar className='h-9 w-9'> <AvatarImage src='/img/avatars/01.png' alt='' /> <AvatarFallback>UU</AvatarFallback> </Avatar> </Button> </DropdownMenuTrigger> <DropdownMenuContent className='w-56' align='end' forceMount > <DropdownMenuLabel className='font-normal'> <div className='flex flex-col space-y-1'> <p className='text-sm font-medium leading-none'>{user.username}</p> <p className='text-xs leading-none text-muted-foreground'>{user.email}</p> </div> </DropdownMenuLabel> <DropdownMenuSeparator /> <DropdownMenuItem> <Link href={'/api/auth/signout'} // Link to the signout API route className='w-full' > Sign Out </Link> </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> ) }
src/components/auth/signin-form.tsx
:
/* all imports */ // Schema definition for form validation using Zod const formSchema = z.object({ email: z.string({ required_error: 'Please enter your email' }).email('Please enter a valid email address'), password: z.string({ required_error: 'Please enter your password', }), }) // Type inference for form inputs based on the Zod schema type InputType = z.infer<typeof formSchema> // Props definition, optionally including a callback URL interface Props { callbackUrl?: string } export function SignInForm({ callbackUrl }: Props) { const [isLoading, setIsLoading] = useState(false) const { toast } = useToast() const router = useRouter() // Hook to control routing const form = useForm<InputType>({ resolver: zodResolver(formSchema), // Set up Zod as the form validation resolver }) // Function to handle form submission async function onSubmit(values: InputType) { try { setIsLoading(true) // Attempt to sign in using the 'credentials' provider const response = await signIn('credentials', { redirect: false, // Prevent automatic redirection email: values.email, password: values.password, }) // Handle unsuccessful sign in attempts if (!response?.ok) { toast({ title: 'Something went wrong!', description: response?.error, variant: 'destructive', }) return } toast({ title: 'Welcome back! ', description: 'Redirecting you to your dashboard!', }) router.push(callbackUrl ? callbackUrl : '/') // Redirect to the callback URL or home page } catch (error) { toast({ title: 'Something went wrong!', description: "We couldn't create your account. Please try again later!", variant: 'destructive', }) } finally { setIsLoading(false) } } return ( <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)}> <div className='grid gap-2'> <div className='grid gap-1'> <FormField control={form.control} name='email' render={({ field }) => ( <FormItem> <FormControl> <div className='flex items-center gap-2'> <Icons.email className={`${form.formState.errors.email ? 'text-destructive' : 'text-muted-foreground'} `}/> <Input type='email' placeholder='Your Email' className={`${form.formState.errors.email && 'border-destructive bg-destructive/30'}`} {...field} /> </div> </FormControl> <FormMessage /> </FormItem> )} /> {/* Password field */} {/* Similar structure to email field, customized for password input */} </div> <Button className='text-foreground mt-4' disabled={isLoading} // Disable button while loading > {isLoading && <Icons.spinner className='mr-2 h-4 w-4 animate-spin' />} // Show loading spinner when processing Sign In </Button> </div> </form> </Form> ) }
🎉Tamamlandı!
NextAuth.js ile temel kimlik doğrulamayı uygulamayı tamamladık. Eksiksiz bir kimlik doğrulama sistemine sahip olmak için hala yapılması gereken birçok şey var ve bunları sonraki eğitimlerde ele alacağız.
Özetle, NextAuth kullanarak Next.js'de bir kimlik doğrulama sisteminin nasıl uygulanacağını ve özelleştirileceğini, kullanıcı yönetimini zenginleştirmek için oturumların ve JWT'lerin nasıl genişletileceğini ve react-hook-form ve Zod kullanarak formların etkili doğrulamayla nasıl yönetileceğini araştırdık.
Yazarla bağlantı kurmak ister misiniz?
Dünyanın her yerindeki arkadaşlarla bağlantı kurmayı seviyorum
Burada da yayınlandı