【ハンズオン】Next.jsでログイン付きのTwitterクローンを作ろう【TypeScript/Supabase/TailwindCSS】

# はじめに

今回はNext.jsとSupabaseのAuthを利用してユーザーログイン機能を持ったTwitter風アプリを作成していきます。

このハンズオンで行うログイン機能を利用することで自分のアプリにも簡単にログイン認証を導入することが可能になります。

このハンズオンはReactをやったことがある人向けに作成しておりますので、Next.jsをやったことなかったり、Supabaseを触ったことない人でも学ぶことが可能です。

# 動画での解説

細かいところなどは動画でより詳しく解説していますので、合わせてご利用ください！

https://youtu.be/Z_9EpoGXH8k?si=ASvfSADZ2MTZxg5Z

# 1. 環境構築

まずはNext.jsとTailwindCSSの環境を用意します

ここではnpmが使えることを全体に説明しますので、npmが使えない方はネットで調べていただいて導入を行ってください

```
❯ npx create-next-app@latest
✔ What is your project named? … next-twitter
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes

$ cd next-twitter
$ npm i
$ npm run dev
```

[localhost:3000](http://localhost:3000)を開いて以下の画面がでれば環境構築が完了です

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/fa1679a6-d998-5a64-feed-62397bec0de1.png)

`next-twitter`をVSCodeで開きます

# 2. Authの設定

以下のページを参考にNext.jsのログインの初期設定を行います。
この内容に関しては公式ドキュメント通りに行っていきます。

https://supabase.com/docs/guides/auth/auth-helpers/nextjs

ここではsupabaseのアカウントを作成している前提で進めていくので、初めての方はアカウント作成とプロジェクト作成を行ってください

左メニューから`Project Settings`->`API`を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/7649af1b-eb82-51c0-2aac-f082285be064.png)

`Projiect URL`と`Project API Keys`の`anon`のキーを使って`.env`を作成します

```
$ touch .env
```

```.env
NEXT_PUBLIC_SUPABASE_URL=your-project-url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
```

次に必要なライブラリを追加します

```
$ npm install @supabase/supabase-js @supabase/ssr
```

ここからAuthを使うための設定をしていきます

```
$ mkdir -p src/utils/supabase
$ mkdir src/app/auth/confirm
$ touch src/utils/supabase/client.ts
$ touch src/utils/supabase/server.ts
$ touch src/middleware.ts
$ touch src/utils/supabase/midlleware.ts
$ touch src/app/auth/confirm/route.ts
```

```ts:src/utils/supabase/client.ts
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { cookies } from "next/headers";

export function createClient() {
  const cookieStore = 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.
          }
        },
        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.
          }
        },
      },
    }
  );
}
```

```ts:src/utils/supabase.server.ts
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { cookies } from "next/headers";

export function createClient() {
  const cookieStore = 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.
          }
        },
        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.
          }
        },
      },
    }
  );
}
```

```ts:src/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)$).*)",
  ],
};
```

```ts:src/utils/supabase/middleware.ts
import { createServerClient, type CookieOptions } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";

export async function updateSession(request: NextRequest) {
  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) {
          request.cookies.set({
            name,
            value,
            ...options,
          });
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          });
          response.cookies.set({
            name,
            value,
            ...options,
          });
        },
        remove(name: string, options: CookieOptions) {
          request.cookies.set({
            name,
            value: "",
            ...options,
          });
          response = NextResponse.next({
            request: {
              headers: request.headers,
            },
          });
          response.cookies.set({
            name,
            value: "",
            ...options,
          });
        },
      },
    }
  );

  await supabase.auth.getUser();

  return response;
}
```

```ts:src/app/auth/confirm/route.ts
import { type EmailOtpType } from '@supabase/supabase-js'
import { type NextRequest, NextResponse } from 'next/server'

import { createClient } from '@/utils/supabase/server'

export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url)
  const token_hash = searchParams.get('token_hash')
  const type = searchParams.get('type') as EmailOtpType | null
  const next = searchParams.get('next') ?? '/'

  const redirectTo = request.nextUrl.clone()
  redirectTo.pathname = next
  redirectTo.searchParams.delete('token_hash')
  redirectTo.searchParams.delete('type')

  if (token_hash && type) {
    const supabase = createClient()

    const { error } = await supabase.auth.verifyOtp({
      type,
      token_hash,
    })
    if (!error) {
      redirectTo.searchParams.delete('next')
      return NextResponse.redirect(redirectTo)
    }
  }

  // return the user to an error page with some instructions
  redirectTo.pathname = '/error'
  return NextResponse.redirect(redirectTo)
}
```

ここまでの内容はかなり難しいですが、公式ドキュメント通りに設定しております。
詳しく内容を理解する必要はいまのところないので先に進みましょう

# 3. ログイン画面

ログイン画面を作成します。

まずはログイン画面を作成します。

```
$ mkdir -p src/app/login
$ touch src/app/login/page.tsx
$ touch src/app/login/actions.ts
```

```tsx:src/app/login/page.tsx
"use client";
import Link from "next/link";
import { login } from "./actions";
import { useRouter } from "next/navigation";

export default function LoginPage() {
  const router = useRouter();
  return (
    <div className="flex justify-center items-center h-screen bg-gray-100">
      <form className="p-8 bg-white rounded-lg shadow-md">
        <div className="mb-6">
          <label
            htmlFor="email"
            className="block text-sm font-medium text-gray-700"
          >
            Email:
          </label>
          <input
            id="email"
            name="email"
            type="email"
            required
            className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
          />
        </div>
        <div className="mb-6">
          <label
            htmlFor="password"
            className="block text-sm font-medium text-gray-700"
          >
            Password:
          </label>
          <input
            id="password"
            name="password"
            type="password"
            required
            className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
          />
        </div>
        <div className="flex items-center justify-between mb-6">
          <button
            type="submit"
            formAction={login}
            className="w-full text-white bg-blue-500 hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center"
          >
            Log in
          </button>
        </div>
        <div className="flex items-center justify-between mb-6">
          <button
            type="button"
            className="w-full text-white bg-green-500 hover:bg-green-700 focus:ring-4 focus:ring-green-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center"
            onClick={() => router.push("/register")}
          >
            Sign up
          </button>
        </div>
        <div className="text-center">
          <Link href="/login/forget-password">
            <button className="text-sm text-blue-600 hover:underline">
              Forget password?
            </button>
          </Link>
        </div>
      </form>
    </div>
  );
}
```
```ts:src/app/login/actions.ts
"use server";

import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";

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

export async function login(formData: FormData) {
  const supabase = createClient();

  // type-casting here for convenience
  // in practice, you should validate your inputs
  const data = {
    email: formData.get("email") as string,
    password: formData.get("password") as string,
  };

  const { error } = await supabase.auth.signInWithPassword(data);

  if (error) {
    redirect("/error");
  }

  revalidatePath("/", "layout");
  redirect("/");
}
```

[localhost:3000/login](http://localhost:3000/login)にアクセウするとログイン画面を表示できます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/d784a9a4-2b44-3dc1-28f7-4623c7eb96f8.png)

ここではログインボタンを押したときの挙動について解説します

```ts
          <button
            type="submit"
            formAction={login}
            className="w-full text-white bg-blue-500 hover:bg-blue-700 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center"
          >
            Log in
          </button>
```

ここではNext.jsのServer Actionsを利用しています。
form要素のactionに'use server'をつけた関数を渡すことで、JavaScriptでなくHTMLの機能だけを用いてサーバーにデータを送信することを可能にします

ボタンを押したときのactionでは`signInWithPassword`にログイン情報を送ることで簡単にログイン認証することが可能です

```ts
  const { error } = await supabase.auth.signInWithPassword(data);
```

しかし、いまはまだユーザーがないためログインに失敗します。
ログインに失敗すると

```ts
  if (error) {
    redirect("/error");
  }
```

ここが実行され`localhost:3000/error`に遷移します。
なのでページを用意します

```
$ mkdir src/app/error
$ touch src/app/error/page.tsx
```

```src/app/error/page.tsx
export default function ErrorPage() {
  return <p>Sorry, something went wrong</p>;
}
```

適当なメールアドレスとパスワードをいれて`login`をクリックするとエラーページが表示されました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/05d8d466-1490-3bca-9774-49fb9388e95d.png)

# 3. サインアップ画面

ではユーザーを作成できるようにしましょう
ここでは、アカウント作成とユーザー情報作成をします。

- メールアドレス
- パスワード
- 名前

をいれることで、ログインしたあとにユーザーを作成することで、Tweetのユーザー名をツイートに紐づけできるようにします

```
$ mkdir src/app/register
$ touch src/app/register/page.tsx
$ touch src/app/register/actions.ts
```

```ts:src/app/register/page.tsx
import { signup } from "./actions";

export default function RegisterUser() {
  return (
    <div className="flex justify-center items-center h-screen bg-gray-100">
      <form className="p-8 bg-white rounded-lg shadow-md max-w-sm w-full">
        <div className="mb-4">
          <input
            type="text"
            name="name"
            placeholder="名前"
            className="w-full p-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
          />
        </div>
        <div className="mb-4">
          <input
            type="email"
            name="email"
            placeholder="メールアドレス"
            className="w-full p-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
          />
        </div>
        <div className="mb-6">
          <input
            type="password"
            name="password"
            placeholder="パスワード"
            className="w-full p-2 border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500"
          />
        </div>
        <button
          type="submit"
          formAction={signup}
          className="w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
        >
          登録
        </button>
      </form>
    </div>
  );
}
```

```ts:src/app/register/actions.ts
"use server";
import { createClient } from "@/utils/supabase/server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";

export async function signup(formdata: FormData) {
  const supabase = createClient();
  const email = formdata.get("email") as string;
  const password = formdata.get("password") as string;

  const { data, error } = await supabase.auth.signUp({
    email,
    password,
  });
  console.log(error);

  if (error || !data?.user) {
    redirect("/error");
  }

  const userData = await supabase.from("user").insert({
    name: formdata.get("name") as string,
    user_id: data.user.id,
  });

  if (userData.error) {
    redirect("/error");
  }

  revalidatePath("/", "layout");
  redirect("/");
}
```

[localhost:3000/register](http://localhost:3000/register)にアクセスすると以下が表示されます(signUpボタンからでも遷移できます)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/cad8b042-238d-484d-5934-b4d4672cf8bb.png)


ここでも先ほどと同じようにserver actionを利用しています

```ts
  const { data, error } = await supabase.auth.signUp({
    email,
    password,
  });
  console.log(error);

  if (error || !data?.user) {
    redirect("/error");
  }

  const userData = await supabase.from("user").insert({
    name: formdata.get("name") as string,
    user: data.user.id,
  });
```

今回は`auth.singUp`を使っています。この関数が呼び出されるとメール宛に確認メールが届くので、リンクをクリックすることでアカウント作成が行われます

このAuthの情報にはユーザー名などの情報を含められないため、ユーザーテーブルにAuthのIDと名前をインサートしています

このままではユーザーテーブルがないのでSupabase画面で作成していきます
左メニューから`Table Editor`→`create a new table`を選択して以下のテーブルを作成します

まずはRLSをオフにします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/d6ab9137-1cc0-c2a2-0237-af6020491ee3.png)

カラムは以下を作成します
nameとuser_idは歯車マークをクリックして`isNullable`のチェックを外してください

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/bc4ff1ce-7b23-d82e-07b3-3a614afd9d77.png)

次にuser_idと作成したアカウントのIDを紐づけます
`user_id`のリンクマークをクリックして以下のように設定します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/f7c3df92-1293-1549-e296-95fdd3a7b143.png)

設定したら保存をします

ここまでできたら実際にログインができるかをチェックします

![Peek 2024-06-02 15-41.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/a736e6d2-4f0c-6a25-a0c1-0bca225e8ecb.gif)

※ TwitterのUIはまだ作成していないのでlocalhost:3000にリダイレクトすれば大丈夫です

# 4. パスワード再設定

パスワード再設定画面を作ります

```
$ mkdir src/app/login/forget-password
$ mkdir src/app/login/reset-password
$ touch src/app/login/forget-password/page.tsx
$ touch src/app/login/reset-password/page.tsx
```

```ts:src/app/login/forget-password.tsx
"use client";
import { supabase } from "@/utils/supabase/client";
import { AuthError } from "@supabase/supabase-js";
import { useState } from "react";

export default function ForgetPassword() {
  const [email, setEmail] = useState<string>("");
  const [isSend, setIsSend] = useState<boolean>(false);
  const [error, setError] = useState<AuthError | null>(null);

  const handleSubmitEmail = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    try {
      const { error } = await supabase.auth.resetPasswordForEmail(email, {
        redirectTo: "http://localhost:3000/login/reset-password",
      });
      if (error) {
        setError(error);
        throw error;
      }
      setIsSend(true);
    } catch (error) {
      console.error(error);
    }
  };

  const handleSetEmail = (event: React.ChangeEvent<HTMLInputElement>) => {
    setEmail(event.target.value);
  };

  if (error) {
    return <p className="text-red-500">エラーが発生しました。</p>;
  }

  if (isSend) {
    return (
      <p className="text-green-500">
        メールを送信しました。メールボックスをご確認ください。
      </p>
    );
  }

  return (
    <div className="flex justify-center items-center h-screen bg-gray-100">
      <div className="w-full max-w-xs">
        <p className="mb-4 text-lg text-gray-700">
          登録されているメールアドレスを入力してください
        </p>
        <form
          onSubmit={handleSubmitEmail}
          className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"
        >
          <div className="mb-4">
            <input
              className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
              id="email"
              type="email"
              value={email}
              onChange={handleSetEmail}
              placeholder="メールアドレス"
            />
          </div>
          <div className="flex items-center justify-between">
            <button
              className="bg-blue-500 w-full hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
              type="submit"
            >
              送信
            </button>
          </div>
        </form>
      </div>
    </div>
  );
}
```

```ts
      const { error } = await supabase.auth.resetPasswordForEmail(email, {
        redirectTo: "http://localhost:3000/login/reset-password",
      });
```

この箇所でリセット用のメールを送信しています。メールのリンクを踏むと`reset-password`にリダイレクトするように設定しました

[localhost:3000/login/forget-password](http://localhost:3000/login/forget-password)にアクセスします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/af52c6fb-3f63-1e7f-c138-9100e6f6f4b6.png)

次にメールのリンクをふんだらリダイレクトする画面を作ります

```tsx:src/app/login/reset-password.tsx
"use client";
import { supabase } from "@/utils/supabase/client";
import { AuthError } from "@supabase/supabase-js";
import { useState } from "react";

export default function ResetPassword() {
  const [password, setPassword] = useState<string>("");
  const [isSend, setIsSend] = useState<boolean>(false);
  const [error, setError] = useState<AuthError | null>(null);

  const handleSubmitPassword = async (
    event: React.FormEvent<HTMLFormElement>
  ) => {
    event.preventDefault();
    try {
      const { error } = await supabase.auth.updateUser({ password: password });
      if (error) {
        setError(error);
        throw error;
      }
      setIsSend(true);
    } catch (error) {
      console.error(error);
    }
  };

  const handleSetPassword = (event: React.ChangeEvent<HTMLInputElement>) => {
    setPassword(event.target.value);
  };

  if (error) {
    return (
      <p className="text-red-500 text-center mt-4">
        エラーが発生しました。もう一度お試しください。
      </p>
    );
  }

  if (isSend) {
    return (
      <p className="text-green-500 text-center mt-4">
        パスワードが更新されました。
      </p>
    );
  }

  return (
    <div className="flex justify-center items-center h-screen bg-gray-100">
      <div className="w-full max-w-xs p-8 bg-white rounded-lg shadow-md">
        <p className="text-lg text-gray-700 mb-4">
          新しいパスワードを入力してください
        </p>
        <form onSubmit={handleSubmitPassword} className="space-y-4">
          <input
            className="w-full p-2 text-gray-700 border rounded focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
            type="password"
            value={password}
            onChange={handleSetPassword}
            placeholder="パスワード"
          />
          <button
            type="submit"
            className="w-full bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
          >
            送信
          </button>
        </form>
      </div>
    </div>
  );
}
```

[localhost:3000/login/reset-password](http://localhost:3000/login/reset-password)にアクセスします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/ae529fe6-7268-2445-4933-00108d9608d5.png)

では実際にパスワードの再設定をやってみます

![Peek 2024-06-02 16-03.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/9679e076-a4a2-d315-5771-0449c46e4461.gif)

# 4. Twitterクローンの作成

ここまででアカウント作成とアカウントに紐づくユーザー情報を作成できたのでこの情報を利用してTwitterクローンを作成します

まずはアイコンを表示させるためのライブラリを追加します

```
$ npm i boring-avatars
```

次に初期のCSSをなくします

```css:src/app/global.css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

ツイートをSupabaseで管理するためテーブルを作成します（RLSはオフにしてください)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/243eecdf-0f17-307c-6bb3-4d71c61b5edb.png)

user_idはリンクマークから外部キーを設定します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/4475b66b-c4ea-9fe4-aa8d-c825538c4a3d.png)



```tsx:src/app/page.tsx
"use client";
import { useEffect, useState } from "react";
import Avatar from "boring-avatars";
import { supabase } from "@/utils/supabase/client";
import { useRouter } from "next/navigation";

type Tweet = {
  id: number;
  username: string;
  content: string;
};

export default function Home() {
  const [userId, setUserId] = useState<string | null>(null);
  const [userName, setUserName] = useState<string | null>(null);
  const [tweet, setTweet] = useState("");
  const [tweetList, setTweetList] = useState<Tweet[]>([]);
  const router = useRouter();

  useEffect(() => {
    const getUser = async () => {
      const userData = await supabase.auth.getUser();
      if (!userData.data.user) {
        router.push("/login");
        return;
      }

      const usernameResponse = await supabase
        .from("user")
        .select("name, id")
        .eq("user_id", userData.data.user.id)
        .single();
      if (usernameResponse.data) {
        setUserId(usernameResponse.data.id);
        setUserName(usernameResponse.data.name);
      }
    };

    getUser();
    getTweet();
  }, []);

  const getTweet = async () => {
    const tweetsResponse = (await supabase
      .from("tweet")
      .select("id, content, user:user_id (name)")) as {
      data: { id: number; content: string; user: { name: string } }[];
    };
    if (tweetsResponse.data) {
      const tweetsData = tweetsResponse.data.map((tweet) => {
        return {
          id: tweet.id,
          username: tweet.user.name,
          content: tweet.content,
        };
      });
      setTweetList(tweetsData);
    }
  };

  const handleAddTweet = async () => {
    console.log(userId);
    const { error } = await supabase
      .from("tweet")
      .insert([{ content: tweet, user_id: userId }]);
    if (error) {
      console.error(error);
    } else {
      setTweet("");
      await getTweet();
    }
  };

  const handleLogout = async () => {
    const { error } = await supabase.auth.signOut();
    if (error) {
      console.error(error);
    } else {
      router.push("/login");
    }
  };

  if (!userId) {
    return <div>Loading...</div>;
  }

  return (
    <div className="w-full max-w-[600px] mx-auto p-4 bg-white shadow">
      <h1 className="text-2xl font-bold mb-4">Twitter Timeline</h1>
      <div className="mb-4 flex space-x-4">
        <Avatar size={50} name={userName || "User"} variant="beam" />
        <input
          type="text"
          className="flex-1 p-2 border border-gray-300 rounded-full"
          placeholder="What is happening?!"
          value={tweet}
          onChange={(e) => setTweet(e.target.value)}
        />
      </div>
      <div className="text-right mb-4">
        <button
          className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-full"
          onClick={handleAddTweet}
        >
          Post
        </button>
      </div>
      <div>
        {tweetList.map(({ id, username, content }) => (
          <div key={id} className="mb-4 p-4 border-b border-gray-200">
            <div className="flex gap-4">
              <Avatar size={50} name={username} variant="beam" />
              <div className="flex-1">
                <p className="font-bold">{username}</p>
                <p>{content}</p>
              </div>
            </div>
          </div>
        ))}
      </div>
      <div className="text-center mt-4">
        <button
          className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-full"
          onClick={handleLogout}
        >
          ログアウト
        </button>
      </div>
    </div>
  );
}
```

詳しくコードを説明します


ログインしたユーザーだけが利用できるように最初に認可を行っています

```ts
    const getUser = async () => {
      const userData = await supabase.auth.getUser();
      if (!userData.data.user) {
        router.push("/login");
        return;
      }
```

もしログインしていない場合はログイン画面にリダイレクトされます
画面ロード時に認可を行い、ユーザーの情報とすべてのツイートをロードしています

```ts
  useEffect(() => {
    const getUser = async () => {
      const userData = await supabase.auth.getUser();
      if (!userData.data.user) {
        router.push("/login");
        return;
      }

      const usernameResponse = await supabase
        .from("user")
        .select("name, id")
        .eq("user_id", userData.data.user.id)
        .single();
      if (usernameResponse.data) {
        setUserId(usernameResponse.data.id);
        setUserName(usernameResponse.data.name);
      }
    };

    getUser();
    getTweet();
  }, []);
```

ツイートをしたらsupabaseにインサートをしてデータの再取得をしています

```ts
  const handleAddTweet = async () => {
    console.log(userId);
    const { error } = await supabase
      .from("tweet")
      .insert([{ content: tweet, user_id: userId }]);
    if (error) {
      console.error(error);
    } else {
      setTweet("");
      await getTweet();
    }
  };
  ```

  最後にログアウトはAuthの関数を呼び出しています
  
```ts
  const handleLogout = async () => {
    const { error } = await supabase.auth.signOut();
    if (error) {
      console.error(error);
    } else {
      router.push("/login");
    }
  };
  ```

  [localhost:3000](http://localhost:3000)を開いて確認します

![Peek 2024-06-02 15-35.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/220e1723-89a7-1db0-4d4d-6645d116fe85.gif)

ここまでできればハンズオンは終了です！

# おわりに

今回はSupabaseのAuthを活用してログイン機能を作り、Twitterクローンを作成しました。ここまでの内容を自分のアプリに適応すれば簡単にログイン機能を追加することが可能です。

ここまではなかなか複雑なこともありますので動画もぜひ活用ください！

https://youtu.be/Z_9EpoGXH8k?si=ASvfSADZ2MTZxg5Z

ここまで読んでいただけた方はいいねとストックよろしくお願いします。
@Sicut_study をフォローいただけるととてもうれしく思います。

また明日の記事でお会いしましょう！

# JISOUのメンバー募集中！
プログラミングコーチングJISOUでは、新たなメンバーを募集しています。
実践的なカリキュラムで、あなたのエンジニアとしてのキャリアを最短で飛躍させましょう！
実践重視：即戦力を育てるアウトプット中心のプログラム。
モダンなスキル : Reactを中心としたモダンな技術を学べる。
キャリアアップ：スキルアップだけでなく、キャリアパスのサポートも充実。
興味のある方は、ぜひホームページからお気軽にカウンセリングをお申し込みください！
▼▼▼

https://projisou.jp