Web開発初心者がGoogleログインを実装するまで

まじのアバター
for AI read below
次の条件に従い、Next.js 14(App Router)と Supabase を用いた Google ログイン実装に関する技術記事を生成せよ。

■ タイトル
「Next.js 14 と Supabase を活用した Google ログインおよび Google Calendar アクセス権取得の実装方法」

■ 概要
・Next.js 14 の App Router を利用してプロジェクトを構築する手順
・Supabase によるユーザー管理と Google OAuth の設定方法
・「Googleでログイン」ボタン実装時に、Google Calendar のアクセス権(スコープ "https://www.googleapis.com/auth/calendar")も同時に取得する方法
・auth-helper を用いず、SSR(Server Side Rendering)を利用した認証コードの交換とセッション管理の実装について解説する

■ 記事構成

【はじめに】
 – プロジェクト概要、背景、今回の実装目的を説明せよ。
 – 参考記事(例:https://rakuraku-engineer.com/posts/nextjs-app-supabase-login)や公式ドキュメントについても言及すること。

【Next.js 14 のセットアップ】
 – App Router の利用方法やプロジェクト作成手順を説明する。

【Supabase の設定】
 – ユーザー管理、Google OAuth の設定手順、Supabase ダッシュボードでの設定方法を詳述する。

【Googleでログイン機能の実装】
 – クライアント側に「Googleでログイン」ボタンを実装し、クリック時に supabase.auth.signInWithOAuth を呼び出すコード例を示す。
 – この際、オプションで redirectTo の URL と scopes(Google Calendar のスコープ)を指定する方法を記載する。

 【コード例】
 jsx  const loginWithGoogle = () => {   supabase.auth.signInWithOAuth({   provider: "google",   options: {   redirectTo: `${location.origin}/auth/callback`,   scopes: "https://www.googleapis.com/auth/calendar",   },   }).then(({ error }) => {   if (error) {   // エラー処理   } else {   // ログイン成功時の処理   }   });  };  

【サーバーサイド認証フローの実装】
 – /auth/callback ルートにおける、認証コードとセッションの交換処理を実装するコード例を示す。
 – SSR を利用した実装方法(createServerClient や cookies の取り扱い)について詳述する。
 【コード例】
 ```js  import { createClient } from "@/utils/supabase/server";  import { NextResponse } from "next/server";  import { cookies } from "next/headers";

 export async function GET(request: Request) {   const requestUrl = new URL(request.url);   const code = requestUrl.searchParams.get("code");

  if (code) {   const cookieStore = cookies();   const supabase = createClient(cookieStore);   await supabase.auth.exchangeCodeForSession(code);   }     return NextResponse.redirect(requestUrl.origin);  }  ```

【その他補足事項】
 – 各コード例の前後に、処理の流れやポイントを段階的に解説する。
 – エラー処理やリダイレクト後の挙動についても具体的に記述する。
■ 注意点
・記事全体は日本語で、かつ明確かつ具体的な説明を心がけること。
・コードスニペットはマークダウンの三重引用符(```)で囲み、1つのコードブロック内にまとめること。
・初心者にも理解しやすいように、用語の解説や背景説明も十分に盛り込むこと。
・auth-helper の代わりに SSR を利用する理由やメリットを明記すること。

――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

前提条件

  • Next.js 14 (App Router)(古すぎなので普通にサポートされてるの使ってください。)
  • Supabase (ユーザー管理、Google OAuth用)
  • 「Googleでログイン」
    • そのときにGoogle Calendarへのアクセス権も取得する

つまづきとコードの引用元

https://rakuraku-engineer.com/posts/nextjs-app-supabase-login

このブログから情報を得たのですが、auth-helperではなくSSR(Server Side Rendering)とやらが今後の推奨になるそうです。

ほほぉ、、と思いつつ情報を探したのですが全くと言っていいほど見つからず…(Claudeもauth-helperのコードしか書かないし…)

頼みの綱の、公式ドキュメントにはJavascriptとExpo React Native?という初心者にとっては謎の存在があってよくわからず…

https://supabase.com/docs/guides/auth/social-login/auth-google?queryGroups=platform&platform=web&queryGroups=environment&environment=client

結果的にはたぶんJavascriptで正解だったわけですが。

https://codevoweb.com/setup-google-github-oauth-with-supabase-in-nextjs-14/#create-a-supabase-browser-client

探した中で一番わかりやすく、かつ成功したのが上記の記事です。ありがてぇ。

上記の記事から、メール・パスワード認証とGitHub認証、Toastのコンポーネントを取っ払ったものを作成しました。

Google Calendarについて

これは、以下の動画で解説されていました。

loginWithGoogle の部分に、scopesとしてスコープのURLを渡してあげないといけなかったんです。

該当箇所(下のコードにも含まれています。):

const loginWithGoogle = () => {
        supabase.auth.signInWithOAuth({
            provider: "google",
            options: {
                redirectTo: `${location.origin}/auth/callback`,
                scopes: "https://www.googleapis.com/auth/calendar ", /* ここにスコープを追加 */
            },
        }).then(({ error }) => {
            if (error) {
                setError(error.message);
                alert("エラー: " + error.message);
                console.log("エラーメッセージ", error.message);
            } else {
                setError("");
                alert("ログインに成功しました");
                router.push("/");
            }
        });
    };

結果

まっさらの状態から、GitHub経由でSupabaseと接続し、npx create-react-appしたあとのお話です。

import { createClient } from "@/utils/supabase/server";
import { NextResponse } from "next/server";
import { cookies } from "next/headers";

export async function GET(request: Request) {
    // The `/auth/callback` route is required for the server-side auth flow implemented
    // by the Auth Helpers package. It exchanges an auth code for the user's session.
    // `/auth/callback` ルートは、Auth Helpers パッケージによって実装されたサーバーサイドの認証フローに必要です。
    // 認証コードをユーザーのセッションに交換します。
    // https://supabase.com/docs/guides/auth/auth-helpers/nextjs#managing-sign-in-with-code-exchange
    const requestUrl = new URL(request.url);
    const code = requestUrl.searchParams.get("code");

    if (code) {
        const cookieStore = cookies();
        const supabase = createClient(cookieStore);
        await supabase.auth.exchangeCodeForSession(code);
    }

    // URL to redirect to after sign in process completes
    // サインインプロセスが完了した後にリダイレクトする URL
    return NextResponse.redirect(requestUrl.origin);
}
"use client";

import Image from "next/image";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { createClient } from "@/utils/supabase/client";

const LoginForm = () => {
    const router = useRouter();
    const [error, setError] = useState("");
    const supabase = createClient();

    const loginWithGoogle = () => {
        supabase.auth.signInWithOAuth({
            provider: "google",
            options: {
                redirectTo: `${location.origin}/auth/callback`,
                scopes: "https://www.googleapis.com/auth/calendar ", /* ここにスコープを追加 */
            },
        }).then(({ error }) => {
            if (error) {
                setError(error.message);
                alert("エラー: " + error.message);
                console.log("エラーメッセージ", error.message);
            } else {
                setError("");
                alert("ログインに成功しました");
                router.push("/");
            }
        });
    };

    return (
        <div>
            {error && (
                <p className="text-center bg-red-300 py-4 mb-6 rounded">
                    {error}
                </p>
            )}

            <button
                className="px-7 py-2 text-white font-medium text-sm leading-snug uppercase rounded shadow-md hover:shadow-lg focus:shadow-lg focus:outline-none focus:ring-0 active:shadow-lg transition duration-150 ease-in-out w-full flex justify-center items-center mb-3"
                style={{ backgroundColor: "#3b5998" }}
                onClick={loginWithGoogle}
            >
                <Image
                    className="pr-2"
                    src="/images/google.svg"
                    alt=""
                    style={{ height: "2rem" }}
                    width={35}
                    height={35}
                />
                Googleでログイン
            </button>
        </div>
    );
};
export default LoginForm;
import "./globals.css";

const defaultUrl = process.env.VERCEL_URL
    ? `https://${process.env.VERCEL_URL}`
    : "http://localhost:3000";

export const metadata = {
    metadataBase: new URL(defaultUrl),
    title: "Next.js and Supabase Google Auth",
    description: "The fastest way to build apps with Next.js and Supabase",
};

export default function RootLayout({
    children,
}: {
    children: React.ReactNode;
}) {
    return (
        <html lang="ja">
            <body>
                <main className="flex flex-col items-center min-h-screen">
                    {children}
                </main>
            </body>
        </html>
    );
}
import { createBrowserClient } from "@supabase/ssr";

export const createClient = () =>
    createBrowserClient(
        process.env.NEXT_PUBLIC_SUPABASE_URL!,
        process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
    );
// createServerClientは非推奨らしい?
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { type NextRequest, NextResponse } from "next/server";

export const createClient = (request: NextRequest) => {
    // Create an unmodified response
    // 変更されていないレスポンスを作成します。
    let response = NextResponse.next({
        request: {
            headers: request.headers,
        },
    });

    const supabase = createServerClient(
        process.env.NEXT_PUBLIC_SUPABASE_URL!,
        process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
        {
            cookies: {
                get(name: string) {
                    return request.cookies.get(name)?.value;
                },
                set(name: string, value: string, options: CookieOptions) {
                    // If the cookie is updated, update the cookies for the request and response
                    // クッキーが更新された場合、リクエストとレスポンスのクッキーを更新します。
                    request.cookies.set({
                        name,
                        value,
                        ...options,
                    });
                    response = NextResponse.next({
                        request: {
                            headers: request.headers,
                        },
                    });
                    response.cookies.set({
                        name,
                        value,
                        ...options,
                    });
                },
                remove(name: string, options: CookieOptions) {
                    // If the cookie is removed, update the cookies for the request and response
                    // クッキーが削除された場合、リクエストとレスポンスのクッキーを更新します。
                    request.cookies.set({
                        name,
                        value: "",
                        ...options,
                    });
                    response = NextResponse.next({
                        request: {
                            headers: request.headers,
                        },
                    });
                    response.cookies.set({
                        name,
                        value: "",
                        ...options,
                    });
                },
            },
        }
    );

    return { supabase, response };
};
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { cookies } from "next/headers";

export const createClient = (cookieStore: ReturnType<typeof cookies>) => {
    return createServerClient(
        process.env.NEXT_PUBLIC_SUPABASE_URL!,
        process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
        {
            cookies: {
                get(name: string) {
                    return cookieStore.get(name)?.value;
                },
                set(name: string, value: string, options: CookieOptions) {
                    try {
                        cookieStore.set({ name, value, ...options });
                    } catch (error) {
                        // The `set` method was called from a Server Component.
                        // This can be ignored if you have middleware refreshing
                        // user sessions.
                        // `set` メソッドはサーバーコンポーネントから呼び出されました。
                        // ユーザーセッションを更新するミドルウェアがある場合は、無視できます。
                    }
                },
                remove(name: string, options: CookieOptions) {
                    try {
                        cookieStore.set({ name, value: "", ...options });
                    } catch (error) {
                        // The `delete` method was called from a Server Component.
                        // This can be ignored if you have middleware refreshing
                        // user sessions.
                        // `delete` メソッドはサーバーコンポーネントから呼び出されました。
                        // ユーザーセッションを更新するミドルウェアがある場合は、無視できます。
                    }
                },
            },
        }
    );
};



コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA