Open7
SvelteKitでログイン機能を作成する
環境構築はこちらで
最終的なディクレクトリ構成
src/
├── lib/
│ ├── components/
│ │ └── ui/
│ │ ├── button/
│ │ ├── card/
│ │ ├── form/
│ │ ├── input/
│ │ ├── label/
│ │ └── sonner/
│ ├── schemas/
│ │ ├── ja-zod.ts
│ │ └── login.ts
│ └── types/
│ └── user.ts
├── routes/
│ ├── (loggedIn)/
│ │ ├── logout/
│ │ │ └── +page.server.ts
│ │ ├── +layout.server.ts
│ │ ├── +layout.svelte
│ │ ├── +page.server.ts
│ │ └── +page.svelte
│ ├── (notLogged)/
│ │ └── login/
│ │ ├── +layout.svelte
│ │ ├── +page.server.ts
│ │ ├── +page.svelte
│ │ └── LoginForm.svelte
│ ├── +layout.server.ts
│ └── +layout.svelte
├── app.d.ts
└── hooks.server.ts
ログインフォームの作成
エラーメッセージの日本語化
# インストール
$ bun add i18next zod-i18n-map
zodを日本語化してexport
schemas/ja-zod.ts
import i18next from 'i18next'
import { z } from 'zod'
import { makeZodI18nMap } from 'zod-i18n-map'
import translation from 'zod-i18n-map/locales/ja/zod.json' assert { type: 'json' }
i18next.init({
lng: 'ja',
resources: {
ja: { translation }
}
})
z.setErrorMap(makeZodI18nMap({ t: i18next.t, ns: 'translation' }))
export { z }
ログインフォームのスキーマを作成
schemas/login.ts
import { z } from './ja-zod'
export const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
})
export type LoginSchema = typeof loginSchema
サーバーサイドでフォームを定義
routes/(notLogged)/login/+page.server.ts
import { loginSchema } from '$lib/schemas/login'
import { superValidate } from 'sveltekit-superforms'
import { zod } from 'sveltekit-superforms/adapters'
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async () => {
const form = await superValidate(zod(loginSchema))
return { form }
}
フォームで使用するコンポーネントを追加しておく
$ bunx shadcn-svelte@latest add button card form input sonner
ログインフォームのコンポーネントを作成
コード全文
routes/(notLogged)/login/LoginForm.svelte
<script lang="ts">
import * as Card from '$lib/components/ui/card'
import * as Form from '$lib/components/ui/form'
import { Input } from '$lib/components/ui/input'
import type { LoginSchema } from '$lib/schemas/login'
import type { Infer, SuperValidated } from 'sveltekit-superforms'
import { superForm } from 'sveltekit-superforms'
let {
data,
}: {
data: SuperValidated<Infer<LoginSchema>>
} = $props()
const form = superForm(data)
const { form: formData, enhance } = form
</script>
<form method="POST" use:enhance>
<Card.Root class="min-w-96">
<Card.Header>
<Card.Title>ログイン</Card.Title>
<Card.Description>ログイン機能のデモ用</Card.Description>
</Card.Header>
<Card.Content>
<div class="grid items-center gap-4">
<Form.Field {form} name="email">
<Form.Control let:attrs>
<Form.Label>メールアドレス</Form.Label>
<Input {...attrs} bind:value={$formData.email} />
</Form.Control>
<Form.Description>メールアドレスの形式</Form.Description>
<Form.FieldErrors />
</Form.Field>
<Form.Field {form} name="password">
<Form.Control let:attrs>
<Form.Label>パスワード</Form.Label>
<Input {...attrs} bind:value={$formData.password} type="password" />
</Form.Control>
<Form.Description>8文字以上</Form.Description>
<Form.FieldErrors />
</Form.Field>
</div>
</Card.Content>
<Card.Footer class="flex justify-end">
<Form.Button>ログイン</Form.Button>
</Card.Footer>
</Card.Root>
</form>
ページコンポーネントでログインフォームを表示
routes/(notLogged)/login/+page.svelte
<script lang="ts">
import LoginForm from './LoginForm.svelte'
let { data } = $props()
</script>
<LoginForm data={data.form} />
レイアウトコンポーネントでレイアウトの調整
routes/(notLogged)/login/+layout.svelte
<script lang="ts">
let { children } = $props()
</script>
<div class="grid place-content-center h-[100vh]">
{@render children()}
</div>
ログイン処理を仮実装
routes/(notLogged)/login/+page.server.ts
import { loginSchema } from '$lib/schemas/login'
import { fail } from '@sveltejs/kit'
import { redirect } from 'sveltekit-flash-message/server'
import { superValidate } from 'sveltekit-superforms'
import { zod } from 'sveltekit-superforms/adapters'
import type { Actions } from './$types'
export const actions: Actions = {
default: async (event) => {
const form = await superValidate(event, zod(loginSchema))
if (!form.valid) {
return fail(400, { form })
}
// ログイン処理を後で実装
// 取り急ぎ常に成功させる
return redirect(
'/',
{ message: 'ログインしました', type: 'success' },
event,
)
},
}
バリデーションエラーの表示
ログイン後のホーム画面の作成
ページコンポーネントを追加
routes/(loggedIn)/+page.svelte
<h1 class="text-3xl font-bold underline">
Home
</h1>
レイアウトファイルで余白の調整
routes/(loggedIn)/+layout.svelte
<script lang="ts">
let { children } = $props()
</script>
<div class="p-4">
{@render children()}
</div>
ログイン機能の作成
ログイン処理を追加
routes/(notLogged)/login/+page.server.ts
export const actions: Actions = {
default: async (event) => {
const form = await superValidate(event, zod(loginSchema))
if (!form.valid) {
return fail(400, { form })
}
// ログイン処理
// 本来はAPIを実行してログインする
// 特定の値のみ受け付けてエラーを再現する
if (
form.data.email !== 'test@test.com' ||
form.data.password !== 'password'
) {
setFlash({ message: 'ログインに失敗しました', type: 'error' }, event)
return fail(400, { form })
}
// 今回はcookieにダミー値を直接セットする
event.cookies.set('logged_in', 'true', {
httpOnly: true,
maxAge: 60 * 60 * 24 * 365,
secure: true,
path: '/',
})
return redirect(
'/',
{ message: 'ログインしました', type: 'success' },
event,
)
},
}
受け付ける値を表示しておく
routes/(notLogged)/login/LoginForm.svelte
<Form.Description>test@test.com と入力してください</Form.Description>
<Form.Description>password と入力してください</Form.Description>
エラーの検証
成功したらホームにリダイレクト
cookieに値がセットされる
ログアウト機能の作成
ホーム画面にログアウト用のリンクを追加
routes/(loggedIn)/+page.svelte
<!-- ホバーしたときにログアウトしないようにプリロードは切っておく -->
<a
href='/logout'
data-sveltekit-preload-data='off'
>
ログアウト
</a>
ログアウト用のルートを追加
routes/(loggedIn)/login/+page.server.ts
import { redirect } from '@sveltejs/kit'
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async ({ cookies }) => {
// 本来はAPIを実行してログアウトする
// 今回はcookieの値を削除して、ログアウトとみなす
cookies.delete('logged_in', { path: '/' })
redirect(302, '/login')
}
ログアウトを実行
cookieの値が削除され、ログイン画面にリダイレクト
ログイン状態のチェック
- ログイン状態によってリダイレクト処理を実行したい
- ログイン不要な画面
- ログイン済みの場合はホームへリダイレクト
- ログインが必要な画面
- 未ログインの場合はログイン画面へリダイレクト
- ルートのグループ化を行い、グループによって各種チェックを実行する
ログイン画面などログインが不要な画面はroutes/(notLogged)
配下にルートを配置する
ホーム画面などログインが必要な画面はroutes/(loggedin)
配下にルートを配置する
hooks.server.tsで各リクエスト時にログイン状態のチェックを行う
src/
├── routes/
│ ├── (loggedIn)/
│ ├── (notLogged)/
└── hooks.server.ts
hooks.server.ts
import type { Handle } from '@sveltejs/kit'
export const handle: Handle = async ({ resolve, event }) => {
const cookieLoggedIn = event.cookies.get('logged_in')
const loggedIn = cookieLoggedIn === 'true'
// ログインが必要な画面で未ログイン
if (!loggedIn && event.route.id?.startsWith('/(loggedIn)')) {
return Response.redirect(`${event.url.origin}/login`, 302)
}
// ログイン不要な画面でログイン済み
if (loggedIn && event.route.id?.startsWith('/(notLogged)')) {
return Response.redirect(`${event.url.origin}/`, 302)
}
// 実際は必要に応じてログインユーザー情報の取得やアクセストークンの検証を実施する
return await resolve(event)
}
ホーム画面でcookieの値を削除してリロード
未ログインなのでログイン画面へリダイレクトされる
ログイン画面でcookieに値を直接セットしてリロード
ログイン済みなのでホーム画面へリダイレクトされる