今天要來實作登入功能,目前登入的規劃有兩種登入方式:
那為了讓小夥伴快速實作第三方登入部分,會採用 Next-auth
這框架幫我們完成,Next-auth
提供的登入方式非常多元包含 email
驗證、SSO
、SMS
等等都支援,算是整合目前常見的登入方式 ,甚至可以自定義 Credentials
實作帳密註冊,next-auth
還可以根據你喜愛的 db
透過 adapters
管理用戶資料,所以使用起來非常方便,同時Next-Auth
本身也會是 trpc
生態中會用到的框架,所以也順便介紹~
MySQL
、PostgreSQL
、 mongodb
、firebase
等等next-auth
使採用 cookies
做身份驗證,安全性部分也有做額外處理,例如 csrf
部分採用 csrf-token
預防。從下面流程中我們可以知道要拿到 user
第三方資料是需要經過非常多層的資料轉換,如果今天只有做 github
的登入那還好,但如果之後要做 google
、apple
對於開發成本是不少的,而且每家的實作方式可能還會不一樣,能不能全部整合好又是另外一件事,但 Next-auth
就是幫我們處理好這些事情,只要添加不同的的 provider
就 ok~
這邊就快速帶過主要介紹著要 call 的 function 有哪些~
首先先定義 Login
跟 Register
的 schema。
//~src/validate/auth.ts
import { z } from "zod";
export const registerFormSchema = z.object({
name: z.string({
required_error: 'name is require',
invalid_type_error: 'invalidate name type'
}).min(1, 'name must be provider'),
email: z.string({
required_error: 'email is require',
}).email('invalidate email type'),
password: z.string({
required_error: 'password is require',
invalid_type_error: 'invalidate password type'
}).min(4, { message: 'password must be at least 4 characters' }),
confirmPassword: z.string({
required_error: 'confirmPassword is require',
invalid_type_error: 'invalidate confirmPassword type'
}).min(4, { message: 'confirmPassword must be at least 4 characters' }),
}).superRefine(({ password, confirmPassword }, ctx) => {
if (password !== confirmPassword) {
ctx.addIssue({
code: 'custom',
message: 'password not match',
path: ['confirmPassword']
})
}
})
export type RegisterFormSchema = z.infer<typeof registerFormSchema>
export const loginFormSchema = z.object({
email: z.string({
required_error: 'email is require',
}).email('invalidate email type'),
password: z.string({
required_error: 'password is require',
invalid_type_error: 'invalidate password type'
}).min(4, { message: 'password must be at least 4 characters' }),
})
export type LoginFormSchema = z.infer<typeof loginFormSchema>
登入部分與註冊方面全部寫在 AuthForm
中,透過 variants
決定要 render loginForm 還是 auth-form
// !/src/pages/indx.tsx
import { AuthForm } from '@/components/AuthForm'
import { getServerSession } from 'next-auth'
import Image from 'next/image'
import React, { useState } from 'react'
import { authOptions } from './api/auth/[...nextauth]'
function AuthPage() {
const [variants, setVariants] = useState<VARIANTS>('Login')
const toggleVariants = () => {
if (variants === 'Login') setVariants('Register')
if (variants === 'Register') setVariants('Login')
}
return (
<div className='min-h-full flex justify-center flex-col bg-gray-100 py-12 sm:py-6 lg:px-8 h-screen'>
<div className='sm:mx-auto sm:w-full sm: max-w-md '>
<Image
alt='Logo'
src={'/image/logo.png'}
width={'48'}
height={'48'}
className='mx-auto'
style={{ aspectRatio: 1 }}
/>
<h2
className='mt-6 text-center text-3xl font-bold tracking-tight text-gray-900'
>
{variants == 'Login'
? 'sign in to your account'
: 'register account'
}
</h2>
</div>
<AuthForm variants={variants} toggleVariants={toggleVariants} />
</div>
)
}
schema 切換
: 根據 variants
更改 useForm
中的resolver
是使用 registerFormSchema
還是 loginFormSchema
。onSubmit 的 value type
: 在 onSubmit
這邊透過 formSchemaTypeGuard
type guard 劃分 value
的 type
。劃分 login 跟 register 執行地方
: 在onSubmit
中 根據
variants 執行
login或是
register` 。AuthSocialButton
: 第三方登入的按鈕。 , ,同時定義
import { LoginFormSchema, RegisterFormSchema, loginFormSchema, registerFormSchema } from '@/validate/auth'
import { zodResolver } from '@hookform/resolvers/zod'
import React, { useMemo, useState } from 'react'
import { FieldErrors, useForm } from 'react-hook-form'
import axios, { AxiosError, AxiosResponse } from 'axios'
import { Button } from './Button'
import { Input } from './Input'
import { AuthSocialButton } from './AuthSocialButton'
import { BsGithub, BsGoogle, BsDiscord } from 'react-icons/bs';
import { RegisterSchema } from '@/pages/api/register'
import { toast } from 'react-toastify'
import { useRouter } from 'next/router'
interface AuthFormProps {
variants: VARIANTS
toggleVariants: () => void
}
export const AuthForm = ({ variants, toggleVariants }: AuthFormProps) => {
const router = useRouter()
const [isLoading, setIsLoading] = useState<boolean>(false)
const formSchemaTypeGuard = (value: LoginFormSchema | RegisterFormSchema): value is RegisterFormSchema => {
return value.hasOwnProperty('confirmPassword')
}
const isRegister = useMemo(() => variants === 'Register', [variants])
const { register, formState: { errors }, handleSubmit } = useForm<LoginFormSchema | RegisterFormSchema>({
resolver: zodResolver(
isRegister
? registerFormSchema
: loginFormSchema
),
mode: 'onChange'
})
const onSubmit = async (value: LoginFormSchema | RegisterFormSchema) => {
try {
setIsLoading(true)
if (variants === 'Login') {
// login form
}
if (formSchemaTypeGuard(value) && variants === 'Register') {
// register form
}
} catch (e) {
if (e instanceof AxiosError) {
const message = e.response?.data.message
toast.error(message)
return
}
console.log(e)
} finally {
setIsLoading(false)
}
}
const socialAction = (type: 'discord' | 'google' | 'github') => {
console.log(type)
}
return (
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div
className="
bg-white
px-4
py-8
shadow
sm:rounded-lg
sm:px-10
"
>
<form
className="space-y-6"
onSubmit={handleSubmit(onSubmit)}
>
{isRegister && (
<Input
disabled={isLoading}
label='name'
id='name'
register={register}
error={(errors as FieldErrors<RegisterFormSchema>).name}
/>
)}
<Input
disabled={isLoading}
label='email'
id='email'
register={register}
error={errors.email}
/>
<Input
disabled={isLoading}
label='password'
id='password'
register={register}
error={errors.password}
/>
{isRegister && (
<Input
disabled={isLoading}
label='confirmPassword'
id='confirmPassword'
register={register}
error={(errors as FieldErrors<RegisterFormSchema>).confirmPassword}
/>
)}
<Button
disabled={isLoading}
type='submit'
fullWidth
>submit</Button >
</form>
<div className="mt-6">
<div className="relative">
<div
className="
absolute
inset-0
flex
items-center
"
>
<div className="w-full border-t border-gray-300" />
</div>
<div className="relative flex justify-center text-sm">
<span className="bg-white px-2 text-gray-500">
Or continue with
</span>
</div>
</div>
<div className="mt-6 flex gap-2">
<AuthSocialButton
icon={BsGithub}
onClick={() => socialAction('github')}
/>
<AuthSocialButton
icon={BsGoogle}
onClick={() => socialAction('google')}
/>
<AuthSocialButton
icon={BsDiscord}
onClick={() => socialAction('discord')}
/>
</div>
</div>
<div
className="
flex
gap-2
justify-center
text-sm
mt-6
px-2
text-gray-500
"
>
<div>
{variants === 'Login' ? 'New to Messenger?' : 'Already have an account?'}
</div>
<div
onClick={toggleVariants}
className="underline cursor-pointer"
>
{variants === 'Login' ? 'Create an account' : 'Login'}
</div>
</div>
</div>
</div>
)
}
如果讀者需要這是的 code 可以點擊下方連結~
code:
https://github.com/Danny101201/next_demo/tree/main
今天先介紹到這邊明天我們開始製作 API
相關連結:
https://docs.github.com/zh/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
https://github.com/Danny101201/next_demo/tree/main
https://docs.github.com/zh/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
https://github.com/Danny101201/next_demo/tree/main
✅ 前端社群 :
https://lihi3.cc/kBe0Y