家計簿アプリを作る #8:ログイン・ログアウトの実装
家計簿アプリ作成シリーズの第8回(後編)です。前回に引き続き、ログイン・ログアウトを実装します。
Step 1:hooks.server.ts でセッションを取得する
すべてのリクエストでセッションを検証するため src/hooks.server.ts を作成します。
import type { Handle } from '@sveltejs/kit';
import { createAuth } from '$lib/server/auth';
export const handle: Handle = async ({ event, resolve }) => {
const auth = createAuth(event.platform!.env.DB);
const session = await auth.api.getSession({
headers: event.request.headers
});
event.locals.user = session?.user || null;
event.locals.session = session?.session || null;
return resolve(event);
};
ハマりポイント①:ファイル名に + を付けてはいけない
SvelteKit のルーティングファイル(+page.svelte 等)は + が必要ですが、hooks.server.ts は + なしです。+hooks.server.ts にしてしまうと以下のエラーが出てファイルが認識されません。
> Files prefixed with + are reserved (saw src/routes/+hooks.server.ts)
また hooks.server.ts は src/routes/ ではなく src/ 直下に置く必要があります。
Step 2:ログインページを作成する
src/routes/login/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
import { createAuth } from '$lib/server/auth';
export const actions: Actions = {
login: async ({ request, platform, cookies }) => {
const formData = await request.formData();
const email = formData.get('email');
const password = formData.get('password');
if (!email || !password) {
return fail(400, { error: 'メールアドレスとパスワードを入力してください' });
}
const auth = createAuth(platform!.env.DB);
const response = await auth.api.signInEmail({
body: {
email: String(email),
password: String(password)
},
asResponse: true
});
if (!response.ok) {
return fail(400, { error: 'ログインに失敗しました' });
}
const setCookieHeader = response.headers.get('set-cookie') ?? '';
if (setCookieHeader) {
const [nameValue] = setCookieHeader.split(';');
const eqIndex = nameValue.indexOf('=');
const cookieName = nameValue.substring(0, eqIndex).trim();
const cookieValue = decodeURIComponent(nameValue.substring(eqIndex + 1).trim());
cookies.set(cookieName, cookieValue, {
path: '/',
httpOnly: true,
sameSite: 'lax',
maxAge: 604800
});
}
redirect(303, '/');
}
};
src/routes/login/+page.svelte
<script lang="ts">
import { enhance } from "$app/forms";
let { form } = $props();
</script>
<form method="POST" action="?/login" use:enhance>
<div>
<label for="email">メールアドレス:</label>
<input type="email" id="email" name="email" required />
</div>
<div>
<label for="password">パスワード:</label>
<input type="password" id="password" name="password" required />
</div>
<button type="submit">ログイン</button>
</form>
<a href="/signup">アカウントを作成</a>
{#if form?.error}
<p style="color: red">{form.error}</p>
{/if}
ハマりポイント②:Cookie の値を decodeURIComponent でデコードする
asResponse: true で取得したレスポンスの Set-Cookie ヘッダーには Cookie の値が URL エンコードされた状態で入っています(%2F、%3D 等)。
そのまま cookies.set に渡すと Better Auth の getSession で照合に失敗します。decodeURIComponent でデコードしてから設定する必要があります。
const cookieValue = decodeURIComponent(nameValue.substring(eqIndex + 1).trim());
ハマりポイント③:Set-Cookie ヘッダーをそのまま Cookie の値にしない
// ❌ Set-Cookie ヘッダー文字列全体を値にしてしまう
cookies.set('better-auth.session_token', setCookieHeader, { ... });
// ✅ name=value; Max-Age=...; ... から name と value だけ取り出す
const [nameValue] = setCookieHeader.split(';');
const eqIndex = nameValue.indexOf('=');
const cookieName = nameValue.substring(0, eqIndex).trim();
const cookieValue = decodeURIComponent(nameValue.substring(eqIndex + 1).trim());
Step 3:認証チェックを追加する
ルートページ(/)で未ログインの場合は /login にリダイレクトします。
// src/routes/+page.server.ts
export const load: PageServerLoad = async ({ locals, platform }) => {
if (!locals.user) {
redirect(303, '/login');
}
// ...
};
Step 4:ログアウトを実装する
トップページにログアウトボタンを追加し、Cookie を削除してログイン画面に戻ります。
src/routes/+page.server.ts の actions に追加
logout: async ({ cookies }) => {
cookies.delete('better-auth.session_token', { path: '/' });
redirect(303, '/login');
}
src/routes/+page.svelte に追加
<form method="POST" action="?/logout" use:enhance>
<button type="submit">ログアウト</button>
</form>
まとめ
| ポイント | 内容 |
|---|---|
hooks.server.ts の場所 |
src/ 直下。src/routes/ ではない |
| ファイル名 | + なし。hooks.server.ts(+hooks.server.ts ではない) |
redirect の位置 |
try/catch の外に出す |
| Cookie の値 | decodeURIComponent でデコードしてから cookies.set に渡す |
| Cookie の解析 | Set-Cookie ヘッダーから name と value だけ取り出す |
| ログアウト | cookies.delete でセッション Cookie を削除する |