iT邦幫忙

2023 iThome 鐵人賽

DAY 1
0
Modern Web

一些讓你看來很強的全端- trcp 伴讀系列 第 17

Day-017. 一些讓你看來很強的全端 TRPC 伴讀 -Next-Auth( install )

  • 分享至 

  • xImage
  •  

今天要來實作登入功能,目前登入的規劃有兩種登入方式:

  1. 一般的帳密註冊
  2. 第三方登入

那為了讓小夥伴快速實作第三方登入部分,會採用 Next-auth 這框架幫我們完成,Next-auth 提供的登入方式非常多元包含 email 驗證、SSOSMS 等等都支援,算是整合目前常見的登入方式 ,甚至可以自定義 Credentials 實作帳密註冊,next-auth 還可以根據你喜愛的 db 透過 adapters 管理用戶資料,所以使用起來非常方便,同時Next-Auth 本身也會是 trpc 生態中會用到的框架,所以也順便介紹~

簡單列出 Next-auth 優點

  • 靈活且易於使用,支援 OAuth1.0 OAuth2.0 和 OpenId 鏈接
  • 方便管理 db 資料,同時提供多種 db 例如 MySQLPostgreSQLmongodbfirebase 等等
  • 同時 next-auth 使採用 cookies 做身份驗證,安全性部分也有做額外處理,例如 csrf 部分採用 csrf-token 預防。
  • 同時也可以使用 serverless 方式部署應用。
  • 簡化第三方服務應用。

GitHub 授權流程

從下面流程中我們可以知道要拿到 user 第三方資料是需要經過非常多層的資料轉換,如果今天只有做 github 的登入那還好,但如果之後要做 googleapple 對於開發成本是不少的,而且每家的實作方式可能還會不一樣,能不能全部整合好又是另外一件事,但 Next-auth 就是幫我們處理好這些事情,只要添加不同的的 provider 就 ok~

https://ithelp.ithome.com.tw/upload/images/20231004/20145677L0OKBXqinT.png

開始之前先切版吧~

這邊就快速帶過主要介紹著要 call 的 function 有哪些~

首先先定義 LoginRegister 的 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>
  )
}

AuthForm

  1. schema 切換: 根據 variants 更改 useForm中的resolver 是使用 registerFormSchema 還是 loginFormSchema
  2. onSubmit 的 value type : 在 onSubmit 這邊透過 formSchemaTypeGuard type guard 劃分 valuetype
  3. 劃分 login 跟 register 執行地方 : 在onSubmit根據 variants 執行login或是register` 。
  4. 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>
  )
}

Register page

https://ithelp.ithome.com.tw/upload/images/20231004/20145677KnMIvGAvIO.png

login page

https://ithelp.ithome.com.tw/upload/images/20231004/2014567739vX06vbyb.png
如果讀者需要這是的 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


上一篇
Day-016. 一些讓你看來很強的全端 TRPC 伴讀 -InfiniteQuery (下)
下一篇
Day-018. 一些讓你看來很強的全端 TRPC 伴讀 -Next-Auth(schema model)
系列文
一些讓你看來很強的全端- trcp 伴讀30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言