supabaseでパスワードのリセットを実装しようとしましたがドキュメントのコピペだけではうまく動作しませんでした。
以下のようなエラーに遭遇しました。
[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
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>
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」については以下のドキュメントに記載のとおりです。
次に、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
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>
</>
);
}
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("/");
}
参考