こんにちは!「SvelteKitで作るフルスタック個人ブログ」シリーズの第5回へようこそ。第4回では、SvelteKitのAPIルートとDrizzle ORMを使ってバックエンドを構築し、ブログ記事をデータベースで管理しました。今回は、Luciaを使って認証機能(ログイン・登録)を実装し、ユーザーがログイン後に新しい記事を作成できるようにします。保護されたページ(ダッシュボード)も作り、Next.jsの認証ライブラリ(NextAuthなど)との比較を通じて、Luciaのシンプルさを体感しましょう!
この記事の目標
- Luciaを使ってユーザー登録・ログイン機能を実装する。
- SvelteKitのフォームアクションを活用して、サーバーサイドで認証を処理する。
- 保護されたダッシュボードページを作成し、ログイン済みユーザーのみが記事を作成可能にする。
- Skeleton UIでモダンな認証UIを構築する。
- NextAuthとの違いを比較し、Luciaの開発体験(DX)の良さを確認する。
最終的には、ユーザーがログインして記事を投稿できるブログになり、フルスタックアプリケーションとしてさらに完成度を高めます。では、始めましょう!
Luciaとは?
Luciaは、SvelteKit向けの軽量な認証ライブラリです。NextAuthのようなフル機能のソリューションに比べ、以下のような特徴があります:
- シンプル:最小限のAPIで、必要な機能だけを提供。
- 柔軟:データベースやセッション管理を自由にカスタマイズ可能。
- 型安全:TypeScriptとの統合が強力。
- SvelteKitとの相性:サーバーサイドとクライアントサイドの認証がスムーズ。
Luciaは、認証の基本(ユーザー登録、ログイン、セッション管理)を簡単に実装でき、SvelteKitのフォームアクションと組み合わせることで、クライアントサイドの状態管理を最小限に抑えられます。
認証機能のセットアップ
1. Luciaと依存パッケージのインストール
Luciaと関連パッケージをインストールします。以下のコマンドを実行:
npm install lucia @lucia-auth/adapter-sqlite
-
@lucia-auth/adapter-sqlite
:SQLiteデータベース用のLuciaアダプター。第4回で設定したSQLiteを再利用します。
2. データベーススキーマの拡張
ユーザー情報を保存するため、users
テーブルとsessions
テーブルを追加します。src/lib/db/schema.ts
を更新:
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
export const users = sqliteTable('users', {
id: text('id').primaryKey(),
email: text('email').notNull().unique(),
password: text('password').notNull()
});
export const sessions = sqliteTable('sessions', {
id: text('id').primaryKey(),
userId: text('user_id')
.notNull()
.references(() => users.id),
expiresAt: integer('expires_at').notNull()
});
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
slug: text('slug').notNull().unique(),
excerpt: text('excerpt').notNull(),
content: text('content').notNull(),
createdAt: text('created_at').notNull().default(new Date().toISOString())
});
-
解説:
-
users
:ユーザーID、メールアドレス、パスワードを保存。 -
sessions
:セッションID、ユーザーID、有効期限を保存。 -
posts
:第4回で作成したテーブル(変更なし)。
-
マイグレーションを再実行:
npx drizzle-kit generate
npx drizzle-kit migrate
3. Luciaの初期化
Luciaをセットアップします。src/lib/auth.ts
を作成:
import { Lucia } from 'lucia';
import { DrizzleSQLiteAdapter } from '@lucia-auth/adapter-sqlite';
import { db } from './db';
import { sessions, users } from './schema';
const adapter = new DrizzleSQLiteAdapter(db, sessions, users);
export const lucia = new Lucia(adapter, {
sessionCookie: {
expires: false,
attributes: {
secure: process.env.NODE_ENV === 'production'
}
},
getUserAttributes: (attributes) => {
return {
email: attributes.email
};
}
});
declare module 'lucia' {
interface Register {
Lucia: typeof lucia;
DatabaseUserAttributes: {
email: string;
};
}
}
-
解説:
-
DrizzleSQLiteAdapter
でSQLiteをLuciaに接続。 -
sessionCookie
:セッションクッキーの設定。本番環境ではセキュアに。 -
getUserAttributes
:ユーザー情報の取得時にメールアドレスを返す。
-
認証APIとフォームの実装
1. ユーザー登録の実装
ユーザー登録用のフォームとAPIを作成します。src/routes/register/+page.svelte
を作成:
<script>
import { enhance } from '$app/forms';
import { Input, Button, Card } from '@skeletonlabs/skeleton';
</script>
<div class="container mx-auto p-8">
<Card class="max-w-md mx-auto">
<form method="POST" action="/register?/signup" use:enhance class="p-6 space-y-4">
<h2 class="h2 mb-4">ユーザー登録</h2>
<Input type="email" name="email" placeholder="メールアドレス" required />
<Input type="password" name="password" placeholder="パスワード" required />
<Button type="submit" variant="filled-primary">登録</Button>
<p class="text-sm">
すでにアカウントをお持ちですか? <a href="/login" class="text-primary-500">ログイン</a>
</p>
</form>
</Card>
</div>
登録処理をサーバーサイドで実装するため、src/routes/register/+page.server.ts
を作成:
import { fail, redirect } from '@sveltejs/kit';
import { lucia } from '$lib/auth';
import { db } from '$lib/db';
import { users } from '$lib/db/schema';
import { generateId } from 'lucia';
import bcrypt from 'bcrypt';
export const actions = {
signup: async ({ request }) => {
const form = await request.formData();
const email = form.get('email')?.toString();
const password = form.get('password')?.toString();
if (!email || !password) {
return fail(400, { message: 'メールアドレスとパスワードを入力してください' });
}
const hashedPassword = await bcrypt.hash(password, 10);
const userId = generateId(15);
try {
await db.insert(users).values({
id: userId,
email,
password: hashedPassword
});
const session = await lucia.createSession(userId, {});
const sessionCookie = lucia.createSessionCookie(session.id);
return {
headers: {
'set-cookie': sessionCookie.serialize()
},
redirect: '/dashboard'
};
} catch (e) {
return fail(400, { message: 'このメールアドレスはすでに登録されています' });
}
}
};
-
解説:
-
bcrypt
でパスワードをハッシュ化(npm install bcrypt @types/bcrypt
が必要)。 -
generateId
で一意なユーザーIDを生成。 - 登録成功後、セッションを作成し、ダッシュボードにリダイレクト。
-
2. ログインの実装
ログイン用のフォームを作成します。src/routes/login/+page.svelte
:
<script>
import { enhance } from '$app/forms';
import { Input, Button, Card } from '@skeletonlabs/skeleton';
</script>
<div class="container mx-auto p-8">
<Card class="max-w-md mx-auto">
<form method="POST" action="/login?/signin" use:enhance class="p-6 space-y-4">
<h2 class="h2 mb-4">ログイン</h2>
<Input type="email" name="email" placeholder="メールアドレス" required />
<Input type="password" name="password" placeholder="パスワード" required />
<Button type="submit" variant="filled-primary">ログイン</Button>
<p class="text-sm">
アカウントをお持ちでないですか? <a href="/register" class="text-primary-500">登録</a>
</p>
</form>
</Card>
</div>
ログイン処理をsrc/routes/login/+page.server.ts
に実装:
import { fail, redirect } from '@sveltejs/kit';
import { lucia } from '$lib/auth';
import { db } from '$lib/db';
import { users } from '$lib/db/schema';
import { eq } from 'drizzle-orm';
import bcrypt from 'bcrypt';
export const actions = {
signin: async ({ request }) => {
const form = await request.formData();
const email = form.get('email')?.toString();
const password = form.get('password')?.toString();
if (!email || !password) {
return fail(400, { message: 'メールアドレスとパスワードを入力してください' });
}
const user = await db.select().from(users).where(eq(users.email, email)).get();
if (!user || !(await bcrypt.compare(password, user.password))) {
return fail(400, { message: 'メールアドレスまたはパスワードが正しくありません' });
}
const session = await lucia.createSession(user.id, {});
const sessionCookie = lucia.createSessionCookie(session.id);
return {
headers: {
'set-cookie': sessionCookie.serialize()
},
redirect: '/dashboard'
};
}
};
-
解説:
- メールアドレスでユーザーを検索し、パスワードを
bcrypt.compare
で検証。 - ログイン成功後、セッションを作成し、ダッシュボードにリダイレクト。
- メールアドレスでユーザーを検索し、パスワードを
3. NextAuthとの比較
NextAuthで同様の認証を実装する場合、以下のような設定が必要です:
// Next.js (pages/api/auth/[...nextauth].ts)
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
export default NextAuth({
providers: [
CredentialsProvider({
async authorize(credentials) {
// 認証ロジック
}
})
]
});
NextAuthは、OAuthや多様なプロバイダに対応する強力なライブラリですが、設定が複雑で、シンプルな認証にはオーバーヘッドがあります。一方、Luciaは最小限のAPIで、SvelteKitのフォームアクションと組み合わせることで、サーバーサイドの処理が直感的です。
ダッシュボードと記事作成
1. ダッシュボードの実装
ログイン済みユーザー専用のダッシュボードを作成します。src/routes/dashboard/+page.svelte
:
<script>
import { enhance } from '$app/forms';
import { Input, Textarea, Button, Card } from '@skeletonlabs/skeleton';
</script>
<div class="container mx-auto p-8">
<Card class="max-w-2xl mx-auto">
<form method="POST" action="/dashboard?/createPost" use:enhance class="p-6 space-y-4">
<h2 class="h2 mb-4">新しい記事を作成</h2>
<Input type="text" name="title" placeholder="記事のタイトル" required />
<Input type="text" name="slug" placeholder="スラッグ(例: my-post)" required />
<Textarea name="excerpt" placeholder="記事の概要" required />
<Textarea name="content" placeholder="記事の本文" required />
<Button type="submit" variant="filled-primary">投稿</Button>
</form>
</Card>
</div>
2. ダッシュボードの保護
ダッシュボードをログイン済みユーザーのみに制限します。src/routes/dashboard/+page.server.ts
:
import { fail, redirect } from '@sveltejs/kit';
import { lucia } from '$lib/auth';
import { db } from '$lib/db';
import { posts } from '$lib/db/schema';
export const load = async ({ locals }) => {
if (!locals.session) {
throw redirect(302, '/login');
}
};
export const actions = {
createPost: async ({ request, locals }) => {
if (!locals.session) {
throw redirect(302, '/login');
}
const form = await request.formData();
const title = form.get('title')?.toString();
const slug = form.get('slug')?.toString();
const excerpt = form.get('excerpt')?.toString();
const content = form.get('content')?.toString();
if (!title || !slug || !excerpt || !content) {
return fail(400, { message: 'すべてのフィールドを入力してください' });
}
try {
await db.insert(posts).values({
title,
slug,
excerpt,
content,
createdAt: new Date().toISOString()
});
throw redirect(302, '/blog');
} catch (e) {
return fail(400, { message: 'このスラッグはすでに使用されています' });
}
}
};
-
解説:
-
load
関数でlocals.session
をチェック。セッションがない場合はログイン画面にリダイレクト。 -
createPost
アクションで新しい記事をデータベースに保存。
-
3. セッション管理
Luciaのセッションを全ページで利用可能にするため、src/hooks.server.ts
を作成:
import { lucia } from '$lib/auth';
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
const sessionId = event.cookies.get(lucia.sessionCookieName);
if (!sessionId) {
event.locals.user = null;
event.locals.session = null;
return resolve(event);
}
const { session, user } = await lucia.validateSession(sessionId);
if (session && session.fresh) {
const sessionCookie = lucia.createSessionCookie(session.id);
event.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
}
if (!session) {
const sessionCookie = lucia.createBlankSessionCookie();
event.cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
}
event.locals.user = user;
event.locals.session = session;
return resolve(event);
};
これで、locals.session
とlocals.user
がすべてのサーバー処理で利用可能になります。
動作確認
プロジェクトを起動:
npm run dev
1. ユーザー登録とログイン
-
http://localhost:5173/register
:メールアドレス(例:test@example.com
)とパスワードを入力して登録。成功後、ダッシュボードにリダイレクト。 -
http://localhost:5173/login
:同じメールアドレスとパスワードでログイン。
2. ダッシュボードで記事作成
-
http://localhost:5173/dashboard
:ログイン済みユーザーのみアクセス可能。記事のタイトル、スラッグ、概要、本文を入力して投稿。 - 投稿後、
/blog
で新しい記事が一覧に表示される。
Skeleton UIのフォームコンポーネントにより、入力画面はモダンで使いやすいです。Luciaのシンプルなセッション管理とSvelteKitのフォームアクションにより、クライアントサイドの状態管理がほぼ不要です。
やってみよう!(チャレンジ)
ログアウト機能を追加してみましょう!src/routes/logout/+server.ts
を作成:
import { lucia } from '$lib/auth';
import { redirect } from '@sveltejs/kit';
export async function POST({ locals, cookies }) {
if (!locals.session) {
throw redirect(302, '/login');
}
await lucia.invalidateSession(locals.session.id);
const sessionCookie = lucia.createBlankSessionCookie();
cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
throw redirect(302, '/login');
}
次に、ダッシュボードにログアウトボタンを追加:
<form method="POST" action="/logout" use:enhance class="mt-4">
<Button variant="filled-error">ログアウト</Button>
</form>
さらに、ナビゲーションバーに「ダッシュボード」と「ログアウト」のリンクを条件付きで表示するようにsrc/routes/+layout.svelte
を更新してみてください。Luciaのセッション管理の柔軟性を体感できます!
まとめ
この記事では、Luciaを使ってユーザー認証(登録・ログイン)を実装し、保護されたダッシュボードで記事作成機能を追加しました。SvelteKitのフォームアクションとSkeleton UIにより、直感的でモダンなUIを簡単に構築できました。NextAuthと比べ、Luciaは軽量でカスタマイズしやすく、SvelteKitのサーバーサイド処理と相性が良いと感じていただけたと思います。
次回は、SEO最適化やパフォーマンス向上を行い、Vercelでブログをデプロイして本番環境に公開します。お楽しみに!
この記事が役に立ったと思ったら、LGTMやストックしていただけると励みになります!質問や改善アイデアがあれば、コメントで教えてください。次の記事でまたお会いしましょう!