🎃

【Better Auth】NextJs & tRPC 【#14 Better Auth Protect Page 補足】

に公開

【#14 Better Auth Protect Page 補足】

YouTube: https://youtu.be/7sqXPaOR0BY
https://youtu.be/7sqXPaOR0BY

今回は前回のページのプロテクトについての補足について解説します。

まずは、
サインインとサインアップページに
セッションの有無でトップページへのリダイレクトの設定をしましたので、
「router.push('/')」が無くてもサインイン後にTOPページへ遷移するように
なっています。

src/components/auth/sign-in-view.tsx
'use client'

import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { OctagonAlertIcon } from 'lucide-react'

import { authClient } from '@/lib/auth-client'

import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Alert, AlertTitle } from '@/components/ui/alert'
import Link from 'next/link'

const formSchema = z.object({
  email: z.email(),
  password: z.string().min(1, { message: 'Password is required' }),
})

export const SignInView = () => {
  const router = useRouter()
  const [isPending, setIsPending] = useState(false)
  const [error, setError] = useState<string | null>(null)

  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      email: '',
      password: '',
    },
  })

  const onSubmit = async (data: z.infer<typeof formSchema>) => {
    setError(null)
    setIsPending(true)

    await authClient.signIn.email(
      {
        email: data.email,
        password: data.password,
        callbackURL: '/',
        rememberMe: false,
      },
      {
        onSuccess: () => {
          setIsPending(false)
          // router.push('/')
        },
        onError: ({ error }) => {
          setIsPending(false)
          setError(error.message)
        },
      }
    )
  }

  // const onSocialSignIn = async (provider: 'github' | 'google') => {
  //   setError(null)
  //   setIsPending(true)

  //   await authClient.signIn.social(
  //     {
  //       provider: provider,
  //       callbackURL: '/',
  //     },
  //     {
  //       onSuccess: () => {
  //         setIsPending(false)
  //       },
  //       onError: ({ error }) => {
  //         setIsPending(false)
  //         setError(error.message)
  //       },
  //     }
  //   )
  // }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <div className="flex flex-col gap-y-6">
          <div className="flex flex-col items-center text-center gap-2">
            <h1 className="text-3xl font-bold">Sign In</h1>
            <p className="text-muted-foreground text-balance">
              Enter email & password
            </p>
          </div>

          <FormField
            control={form.control}
            name="email"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Email</FormLabel>
                <FormControl>
                  <Input
                    type="text"
                    placeholder="Enter your email"
                    {...field}
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />

          <FormField
            control={form.control}
            name="password"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Password</FormLabel>
                <FormControl>
                  <Input
                    type="password"
                    placeholder="Enter your password"
                    {...field}
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />

          {!!error && (
            <Alert className="bg-rose-100 border-none text-rose-500">
              <OctagonAlertIcon className="size-4" />
              <AlertTitle>{error}</AlertTitle>
            </Alert>
          )}

          <Button disabled={isPending} type="submit" className="w-full">
            Sign in
          </Button>
          {/* <Button
            disabled={isPending}
            type="button"
            onClick={() => onSocialSignIn('google')}
            className="w-full"
          >
            Google Sign up
          </Button> */}
          <div className="text-center text-sm text-blue-500">
            <Link href="/sign-up" className="underline underline-offset-4">
              Create your account
            </Link>
          </div>
        </div>
      </form>
    </Form>
  )
}
src/components/auth/sign-up-view.tsx
'use client'

import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { z } from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import { useForm } from 'react-hook-form'
import { OctagonAlertIcon } from 'lucide-react'

import { authClient } from '@/lib/auth-client'

import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Alert, AlertTitle } from '@/components/ui/alert'
import Link from 'next/link'

const formSchema = z
  .object({
    name: z.string().min(1, { message: 'Name is required' }),
    email: z.email(),
    password: z.string().min(1, { message: 'Password is required' }),
    confirmPassword: z
      .string()
      .min(1, { message: 'Confirm password is required' }),
  })
  .refine((data) => data.password === data.confirmPassword, {
    message: 'Passwords do not match',
    path: ['confirmPassword'],
  })

export const SignUpView = () => {
  const router = useRouter()
  const [isPending, setIsPending] = useState(false)
  const [error, setError] = useState<string | null>(null)

  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      name: '',
      email: '',
      password: '',
      confirmPassword: '',
    },
  })

  const onSubmit = async (data: z.infer<typeof formSchema>) => {
    setError(null)
    setIsPending(true)

    await authClient.signUp.email(
      {
        name: data.name,
        email: data.email,
        password: data.password,
        callbackURL: '/',
      },
      {
        onSuccess: () => {
          setIsPending(false)
          // router.push('/')
        },
        onError: ({ error }) => {
          setIsPending(false)
          setError(error.message)
        },
      }
    )
  }

  // const onSocialSignIn = async (provider: 'github' | 'google') => {
  //   setError(null)
  //   setIsPending(true)

  //   await authClient.signIn.social(
  //     {
  //       provider: provider,
  //       callbackURL: '/',
  //     },
  //     {
  //       onSuccess: () => {
  //         setIsPending(false)
  //       },
  //       onError: ({ error }) => {
  //         setIsPending(false)
  //         setError(error.message)
  //       },
  //     }
  //   )
  // }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <div className="flex flex-col gap-y-6">
          <div className="flex flex-col items-center text-center gap-2">
            <h1 className="text-3xl font-bold">Sign Up</h1>
            <p className="text-muted-foreground text-balance">
              Create your account
            </p>
          </div>

          <FormField
            control={form.control}
            name="name"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Name</FormLabel>
                <FormControl>
                  <Input type="text" placeholder="Enter your name" {...field} />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />

          <FormField
            control={form.control}
            name="email"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Email</FormLabel>
                <FormControl>
                  <Input
                    type="text"
                    placeholder="Enter your email"
                    {...field}
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />

          <FormField
            control={form.control}
            name="password"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Password</FormLabel>
                <FormControl>
                  <Input
                    type="password"
                    placeholder="Enter your password"
                    {...field}
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />

          <FormField
            control={form.control}
            name="confirmPassword"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Confirm Password</FormLabel>
                <FormControl>
                  <Input
                    type="password"
                    placeholder="Enter your confirm password"
                    {...field}
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          {!!error && (
            <Alert className="bg-rose-100 border-none text-rose-500">
              <OctagonAlertIcon className="size-4" />
              <AlertTitle>{error}</AlertTitle>
            </Alert>
          )}
          <Button disabled={isPending} type="submit" className="w-full">
            Sign up
          </Button>
          {/* <Button
            disabled={isPending}
            type="button"
            onClick={() => onSocialSignIn('google')}
            className="w-full"
          >
            Google Sign up
          </Button> */}
          <div className="text-center text-sm text-blue-500">
            <Link href="/sign-in" className="underline underline-offset-4">
              Sign in your account
            </Link>
          </div>
        </div>
      </form>
    </Form>
  )
}

次に、ログアウトボタンをクリックした後、
サインインページが表示された後にブラウザバックをすると
トップページが表示されてしまう問題につきましては、
onLogoutの関数のonSuccessに
「router.replace('/sign-in')」と「router.refresh()」を設定します。

src/app/client-greeting.tsx
'use client'
import { useRouter } from 'next/navigation'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useTRPC } from '@/trpc/client'

import { authClient } from '@/lib/auth-client'

import { Button } from '@/components/ui/button'

export function ClientGreeting() {
  const router = useRouter()
  const trpc = useTRPC()
  const { data } = useSuspenseQuery(trpc.hello.queryOptions({ text: 'hello' }))

  const {
    data: session,
    isPending, //loading state
    error, //error object
    refetch, //refetch the session
  } = authClient.useSession()

  const onLogout = async () => {
    await authClient.signOut({
      fetchOptions: {
        onSuccess: () => {
          router.replace('/sign-in')
          router.refresh()
        },
      },
    })
  }

  return (
    <>
      <div>{data.greeting}</div>
      {isPending ? (
        <p>Loading...</p>
      ) : session ? (
        <p>Username: {JSON.stringify(session.user.name, null, 2)}</p>
      ) : (
        <p>null</p>
      )}
      <Button onClick={onLogout}>Logout</Button>
    </>
  )
}

Discussion