Open18

supabaseのlocalとoauth(google)ができるまで。

torihazitorihazi
  • config.tomlはlocalでしか使わない
  • supabase initで作成される
  • google oauthのclient idとかを記載
  • ファイル内では envに記載したものをenv(GITHUB_CLIENT_ID)で参照可能
torihazitorihazi

新しくClientを作成する
local用と本番用を作るがここではlocal用だけ

torihazitorihazi

scopeはDataAccesから設定する
.../auth/userinfo.email
...auth/userinfo.profile
openid

torihazitorihazi

config.tomlに以下を設定
127.0.0.1とかいうループバックアドレス?とかではなくてlocalhostにした方がいいそうな。
それにまつわるzennの記事もあった。

authに
site_url = "http://localhost:3000"
additional_redirect_urls = ["http://localhost:3000"]

[auth.external.google]
enabled = true
client_id = "【ここにキー】"
secret = "【ここにシークレットキー】"
redirect_uri = "http://localhost:54321/auth/v1/callback"

torihazitorihazi

https://supabase.com/docs/guides/getting-started/quickstarts/nextjs
localhostのstudioから参照できるconnectの中にあるものはデマ。上のやつが正しい。

utis/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}
utisl/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = await cookies()

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // The `setAll` method was called from a Server Component.
            // This can be ignored if you have middleware refreshing
            // user sessions.
          }
        },
      },
    }
  )
}
middleware.ts
import { type NextRequest } from 'next/server'
import { updateSession } from '@/utils/supabase/middleware'

export async function middleware(request: NextRequest) {
  return await updateSession(request)
}

export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     * Feel free to modify this pattern to include more paths.
     */
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}
utis/supabase/middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function updateSession(request: NextRequest) {
  let supabaseResponse = NextResponse.next({
    request,
  })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll()
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) => request.cookies.set(name, value))
          supabaseResponse = NextResponse.next({
            request,
          })
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  // IMPORTANT: Avoid writing any logic between createServerClient and
  // supabase.auth.getUser(). A simple mistake could make it very hard to debug
  // issues with users being randomly logged out.

  const {
    data: { user },
  } = await supabase.auth.getUser()

  if (
    !user &&
    !request.nextUrl.pathname.startsWith('/login') &&
    !request.nextUrl.pathname.startsWith('/auth')
  ) {
    // no user, potentially respond by redirecting the user to the login page
    const url = request.nextUrl.clone()
    url.pathname = '/login'
    return NextResponse.redirect(url)
  }

  // IMPORTANT: You *must* return the supabaseResponse object as it is. If you're
  // creating a new response object with NextResponse.next() make sure to:
  // 1. Pass the request in it, like so:
  //    const myNewResponse = NextResponse.next({ request })
  // 2. Copy over the cookies, like so:
  //    myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())
  // 3. Change the myNewResponse object to fit your needs, but avoid changing
  //    the cookies!
  // 4. Finally:
  //    return myNewResponse
  // If this is not done, you may be causing the browser and server to go out
  // of sync and terminate the user's session prematurely!

  return supabaseResponse
}
torihazitorihazi

AIにlayout.tsxとlogin用のpage.tsxを作ってもらった。

app/(auth)/login.tsx
"use client";

import React from "react";
import { handleGoogleLogin } from "@/lib/actions/auth";

export default function LoginPage() {
  return (
    <div className="flex flex-col items-center justify-center space-y-6">
      <h1 className="text-2xl font-bold">ログイン</h1>

      <button
        className="flex items-center justify-center gap-2 px-4 py-2 bg-white text-gray-700 border border-gray-300 rounded-md shadow-sm hover:bg-gray-50 transition-colors"
        onClick={handleGoogleLogin}
      >
        <GoogleIcon />
        <span>Googleでログイン</span>
      </button>
    </div>
  );
}

// シンプルなGoogleアイコンコンポーネント
function GoogleIcon() {
  return (
    <svg
      xmlns="http://www.w3.org/2000/svg"
      viewBox="0 0 24 24"
      width="24"
      height="24"
      className="w-5 h-5"
    >
      <path
        fill="#4285F4"
        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"
      />
      <path
        fill="#34A853"
        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"
      />
      <path
        fill="#FBBC05"
        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"
      />
      <path
        fill="#EA4335"
        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"
      />
    </svg>
  );
}
torihazitorihazi

server action

lib/auth.ts
"use server";

import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";

export async function handleGoogleLogin() {
  const supabase = await createClient();
  const { data, error } = await supabase.auth.signInWithOAuth({
    provider: "google",
    options: {
      redirectTo: `http://localhost:3000/api/auth/callback`,
    },
  });

  console.log(data);

  if (data?.url) {
    redirect(data.url);
  } else {
    console.error(error);
  }
}

torihazitorihazi

記載してないが、現状 .env.localには以下がある想定
記述は全部大文字だがめんどいので小文字にしている。
google_client_id,
google_client_secret
NEXT_PUBLIC_SUPABASE_URL!,
NEXT_PUBLIC_SUPABASE_ANON_KEY!,

torihazitorihazi

これだけやって、localサーバ立ち上げて、googleログインボタン押したら
localhost:3000に遷移しているはず。何もしていないので今はnext.jsのlogoが見えるお馴染みのやつが見えるはず。またlocalのsupabase studioのAuthenticationを覗いてみると先ほどのgmailアカウントのレコードがinsertされているはず。

torihazitorihazi

本番についてはsupabaseのdashboardのauthentication/providersのところからgoogleを見つけて、今度はgoogle consoleで本番用のclient_secretとかを作ってそこに記入すれば良い。

torihazitorihazi

またauthが作られると同時にUserとしても作りたいとなったらPostgreSQLのtriggerとかを設定すればいける。それは今回は別であるのでやらないが、調べたら無限に出てくるので割愛。