supabase+Next.jsでパスワードリセットで「Auth session missing」というエラーが出た

supabaseでパスワードのリセットを実装しようとしましたがドキュメントのコピペだけではうまく動作しませんでした。

https://supabase.com/docs/reference/javascript/auth-resetpasswordforemail?queryGroups=platform&platform=npm

以下のようなエラーに遭遇しました。

```
 [Error [AuthSessionMissingError]: Auth session missing!] {
 __isAuthError: true,
 status: 400,
code: undefined
```

パスワードリセット用のトークンを適切に処理できていないことに起因していると思われます。本記事ではその解決法を記載します。

# パスワードリセットメールを送る機能の実装

まず、Next.js側で以下のようにpage.tsxとactions.tsを作成します。MUIを使用して記載していますが、適宜form、input、buttonに変更します。


```
app/
|
└reset-password/
    └actions.ts
    └page.tsx
```
```typescript:app/reset-password/page.tsx
  <Paper
    component="form"
  >
    <InputLabel sx={{ mt: 2 }}>メールアドレス</InputLabel>
    <TextField
      id="email"
      variant="standard"
      fullWidth
      name="email"
      type="email"
    />

    <Box>
      <Button formAction={sendResetPasswordEmail}>送信</Button>
    </Box>
  </Paper>

```

```typescript:app/reset-password/actions.ts

"use server";

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

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

  const { error } = await supabase.auth.resetPasswordForEmail(
    formData.get("email") as string,
    {
      redirectTo: `${getURL()}set-password`,
    }
  );

  if (error) {
    console.error("Reset password error:", error);
    redirect("/error");
  }
}

const getURL = () => {
  let url =
    process?.env?.NEXT_PUBLIC_SITE_URL ?? // Set this to your site URL in production env.
    process?.env?.NEXT_PUBLIC_VERCEL_URL ?? // Automatically set by Vercel.
    "http://localhost:3000/";
  // Make sure to include `https://` when not localhost.
  url = url.startsWith("http") ? url : `https://${url}`;
  // Make sure to include a trailing `/`.
  url = url.endsWith("/") ? url : `${url}/`;
  return url;
};
```
上記コード中の「utils/supabase/server」については以下のドキュメントに記載のとおりです。

https://supabase.com/docs/guides/getting-started/quickstarts/nextjs

次に、SupabaseでAuthentication
→EmailsのReset Passwordのメールテンプレートを以下のように変更します。
```
<h2>Reset Password</h2>

<p>Follow this link to reset the password for your user:</p>
<!-- <p><a href="{{ .ConfirmationURL }}">Reset Password</a></p> -->
<p><a href="{{ .RedirectTo }}?code={{ .TokenHash }}">Reset Password</a></p>
```

# 新しいパスワードの設定の実装
これまでの方法でパスワードリセットメールを送るまでを実装しました。続いてはメールに記載のリンク先のページの作成、新しいパスワードの設定について記載していきます。

```
app/
|
└reset-password/
    └actions.ts
    └page.tsx
└set-password/
    └actions.ts
    └page.tsx
```

```typescript:app/set-password/page.tsx
export default async function SetPassword({
  searchParams,
}: {
  searchParams: Promise<{ code?: string }>;
}) {
  // ✅ searchParams を await してから使う
  const { code } = await searchParams;
  return (
    <>
      <Paper component="form">
        <Grid
          container
          direction="column"
          justifyContent="flex-start"
          alignItems="center"
        >
          <Typography variant={"h5"} sx={{ m: "30px" }}>
            新しいパスワード
          </Typography>
        </Grid>

        <input type="hidden" name="code" value={code} />
        <TextField
          type="password"
          variant="standard"
          fullWidth
          name="password"
        />
        <Box>
          <Button
            type="submit"
            variant="outlined"
            fullWidth
            formAction={setPassword}
          >
            パスワードをセット
          </Button>
        </Box>
      </Paper>
    </>
  );
}
```

```typescript:app/set-password/actions.ts
export async function setPassword(formData: FormData) {
  const supabase = await createClient();

  // type-casting here for convenience
  // in practice, you should validate your inputs
  const code = formData.get("code") as string;



  // 🔁 再度 verifyOtp でセッション復元
  const { error: verifyError } = await supabase.auth.verifyOtp({
    token_hash: code,
    type: "recovery",
  });

  if (verifyError) {
    console.error("Failed to verify OTP:", verifyError);
    redirect("/error");
  }

  const { error } = await supabase.auth.updateUser({
    password: formData.get("password") as string,
  });
  if (error) {
    console.error("Login error:", error);
    redirect("/error");
  }

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

```

# 参考
https://supabase.com/docs/reference/javascript/auth-onauthstatechange