家計簿アプリを作る #35:パスワード変更機能

家計簿アプリ作成シリーズの第35回です。ログイン中のユーザーが自分のパスワードを変更できる機能を実装します。

実装方針

Better Auth の changePassword API を使ってパスワードを変更します。src/routes/(auth)/settings/ に専用ページを作成し、(auth)/+layout.server.ts の認証チェックを利用します。

+page.server.ts の実装

import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
import { createAuth } from '$lib/server/auth';

export const actions: Actions = {
  changePassword: async ({ request, platform, cookies }) => {
    const formData = await request.formData();
    const currentPassword = String(formData.get('currentPassword') ?? '');
    const newPassword = String(formData.get('newPassword') ?? '');

    const errors: Record<string, string> = {};
    if (!currentPassword) {
      errors.currentPassword = '現在のパスワードを入力してください';
    }
    if (!newPassword) {
      errors.newPassword = '新しいパスワードを入力してください';
    }
    if (Object.keys(errors).length > 0) {
      return fail(400, { errors, values: { currentPassword, newPassword } });
    }

    const auth = createAuth(platform!.env.DB);
    try {
      await auth.api.changePassword({
        body: { currentPassword, newPassword },
        headers: request.headers,
      });
    } catch (e) {
      return fail(400, { error: 'パスワードの変更に失敗しました' });
    }

    // セキュリティのためログアウトしてログインページへ
    cookies.delete('better-auth.session_token', { path: '/' });
    redirect(303, '/login');
  }
};

ハマりポイント①:auth という固定エクスポートはない

$lib/server/auth.tscreateAuth(d1) 関数をエクスポートしており、auth という変数はそのままでは存在しません。D1 を渡してインスタンスを生成する必要があります。

// ❌ authという固定エクスポートはない
import { auth } from '$lib/server/auth';

// ✅ createAuthを呼び出してインスタンスを作る
import { createAuth } from '$lib/server/auth';
const auth = createAuth(platform!.env.DB);

ハマりポイント②:formData.get()null を含む

// ❌ FormDataEntryValue | null のまま渡すと型エラーになる
const currentPassword = formData.get('currentPassword');

// ✅ Stringでキャストする
const currentPassword = String(formData.get('currentPassword') ?? '');

ハマりポイント③:changePassword のレスポンスに ok はない

changePassword は成功時にユーザー情報を直接返し、fetch のような Response オブジェクトではありません。エラー時は例外がスローされるため try/catch で処理します。

// ❌ okプロパティは存在しない
const res = await auth.api.changePassword({ ... });
if (!res.ok) { ... }

// ✅ try/catchで例外を捕捉する
try {
  await auth.api.changePassword({ ... });
} catch (e) {
  return fail(400, { error: 'パスワードの変更に失敗しました' });
}

パスワード変更後はログアウトさせる

セキュリティの観点から、パスワード変更後はセッションを削除してログインページに戻すようにしました。

cookies.delete('better-auth.session_token', { path: '/' });
redirect(303, '/login');

+page.svelte の実装

登録・編集フォームと同じパターンでバリデーションエラーを表示します。

{#if form?.errors?.currentPassword}
  <Helper class="mt-2" color="red">
    <span class="font-medium">{form.errors.currentPassword}</span>
  </Helper>
{/if}

form?.errors.xxx のように ?. を途中で切らさず、form?.errors?.xxx まで連鎖させる必要があります。formnull のときに errors へアクセスするとエラーになるためです。

一覧ページへのリンク追加

<A href="/settings" class="hover:underline">パスワードの変更</A>

まとめ

ポイント 内容
Better Authのインスタンス化 createAuth(platform!.env.DB) で生成する
FormDataの型 String(formData.get(...) ?? '') でキャストする
エラーハンドリング changePasswordtry/catch で例外を捕捉する
変更後の挙動 セッションを削除してログインページにリダイレクト
オプショナルチェーン form?.errors?.xxx まで連鎖させる
← トップページに戻る