第188章:ミニ課題:認証付きTODOの土台完成🔐✅
この章は「ログインしてる人だけが、自分のTODOを見れて、追加できる」ところまで仕上げるミニ課題だよ〜!🎀✨ (“土台完成”なので、見た目はシンプルでOK👍)
🎯 この章のゴール(できたら合格💮)
/todosは 未ログインだと /login に飛ばされる 🚪💨- ログイン中は 自分のTODOだけ 見える👀📋
- TODOを 追加できる ➕✨(Server ActionsでOK!)
- DBには
Todo.userIdが入って、誰のTODOか が分かる🗃️🔗
🧠 全体の流れ(まずは地図🗺️)
auth() をどこでも使えるのが v5 系の強みだよ〜!✨ (Auth.js)
✅ 手順1:Todoを「ユーザーにひもづけ」しよう🔗🗃️
prisma/schema.prisma の Todo に userId を追加して、User と関連を作るよ💡
(すでにあるなら確認だけでOK👌)
model User {
id String @id @default(cuid())
email String @unique
name String?
todos Todo[]
}
model Todo {
id String @id @default(cuid())
title String
done Boolean @default(false)
createdAt DateTime @default(now())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
}
マイグレーションも実行〜!🛠️✨
npx prisma migrate dev -n add_todo_user
✅ 手順2:session に user.id を入れて、TODOで使えるようにする🪪✨
Auth.js はデフォだと session に name/email/image だけ出すことが多いから、TODOの紐付け用に userId を足すよ🎯
公式も「user id を session に追加」は定番って言ってるやつ! (Auth.js)
auth.ts の callbacks に追加👇(すでにあるなら追記でOK)
// auth.ts(例:すでにある設定に callbacks を足す)
import NextAuth from "next-auth"
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
// 既存のプロバイダ設定(そのままでOK)
],
callbacks: {
jwt({ token, user }) {
if (user) token.id = user.id
return token
},
session({ session, token }) {
if (session.user && token.id) {
session.user.id = token.id as string
}
return session
},
},
})
TypeScript が怒る場合は型も足してあげよ〜🧷✨
// types/next-auth.d.ts
import { DefaultSession } from "next-auth"
declare module "next-auth" {
interface Session {
user: {
id: string
} & DefaultSession["user"]
}
}
declare module "next-auth/jwt" {
interface JWT {
id?: string
}
}
✅ 手順3:/todos を「未ログインなら弾く」🔐🚪
3-1) Middlewareで守る(おすすめ💪)
Auth.js の auth は Middleware でも使えるよ〜! (Auth.js)
// middleware.ts
import { auth } from "@/auth"
import { NextResponse } from "next/server"
export default auth((req) => {
const isTodos = req.nextUrl.pathname.startsWith("/todos")
const isLoggedIn = !!req.auth
if (isTodos && !isLoggedIn) {
const url = new URL("/login", req.nextUrl)
url.searchParams.set("from", req.nextUrl.pathname)
return NextResponse.redirect(url)
}
return NextResponse.next()
})
export const config = {
matcher: ["/todos/:path*"],
}
ちなみに Auth.js は
auth.tsを作ってhandlers/auth/signIn/signOutを export →/app/api/auth/[...nextauth]/route.tsで GET/POST を export するのが基本形だよ〜🧩(もうできてる前提でOK) (Auth.js)
✅ 手順4:認証付きTODOページを作る📋✨
4-1) Prisma クライアント(なければ)
// src/lib/prisma.ts(パスは好みでOK)
import { PrismaClient } from "@prisma/client"
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma
4-2) Server Actions(追加だけでもOK!➕)
// app/todos/actions.ts
"use server"
import { auth } from "@/auth"
import { prisma } from "@/lib/prisma"
import { revalidatePath } from "next/cache"
export async function addTodo(formData: FormData) {
const session = await auth()
const userId = session?.user?.id
if (!userId) throw new Error("Unauthenticated")
const title = String(formData.get("title") ?? "").trim()
if (!title) return
await prisma.todo.create({
data: { title, userId },
})
revalidatePath("/todos")
}
export async function toggleTodo(formData: FormData) {
const session = await auth()
const userId = session?.user?.id
if (!userId) throw new Error("Unauthenticated")
const id = String(formData.get("id") ?? "")
const done = String(formData.get("done") ?? "") === "true"
// ★ ここが超大事:自分のTODOだけ更新できるようにする🔐
const todo = await prisma.todo.findFirst({ where: { id, userId } })
if (!todo) throw new Error("Not found")
await prisma.todo.update({
where: { id },
data: { done },
})
revalidatePath("/todos")
}
4-3) /todos ページ(自分のTODOだけ表示👀)
// app/todos/page.tsx
import { auth } from "@/auth"
import { prisma } from "@/lib/prisma"
import { redirect } from "next/navigation"
import { addTodo, toggleTodo } from "./actions"
export default async function TodosPage() {
const session = await auth()
const userId = session?.user?.id
if (!userId) redirect("/login")
const todos = await prisma.todo.findMany({
where: { userId },
orderBy: { createdAt: "desc" },
})
return (
<main style={{ maxWidth: 560, margin: "40px auto", padding: 16 }}>
<h1>マイTODO 📝✨</h1>
<form action={addTodo} style={{ display: "flex", gap: 8, marginTop: 12 }}>
<input
name="title"
placeholder="TODOを入力…"
style={{ flex: 1, padding: 10 }}
/>
<button type="submit" style={{ padding: "10px 14px" }}>
追加➕
</button>
</form>
<ul style={{ marginTop: 16, padding: 0, listStyle: "none" }}>
{todos.map((t) => (
<li
key={t.id}
style={{
display: "flex",
alignItems: "center",
gap: 10,
padding: "10px 0",
borderBottom: "1px solid #ddd",
}}
>
<form action={toggleTodo}>
<input type="hidden" name="id" value={t.id} />
<input type="hidden" name="done" value={String(!t.done)} />
<button type="submit" style={{ width: 40 }}>
{t.done ? "✅" : "⬜"}
</button>
</form>
<span style={{ textDecoration: t.done ? "line-through" : "none" }}>
{t.title}
</span>
</li>
))}
</ul>
<p style={{ marginTop: 16, opacity: 0.7 }}>
ログイン中:{session.user?.email ?? "(不明)"} 💌
</p>
</main>
)
}
🧪 動作チェック(これ通れば完成🎉)
- 未ログインで
/todosに行く →/loginに飛ぶ🚪💨 - ログインする →
/todosで一覧が見える👀 - TODOを追加 → すぐ反映される➕🔄
- トグル(✅/⬜) → 自分のTODOだけ更新できる🔐
🌟 仕上げのひとこと(この章の“勝ち筋”🏆)
認証付きで一番大事なのはこれ👇
- 「表示」も「更新」も、必ず userId で絞る(UIじゃなくサーバー側で!)🔒✨
auth()を入口にして、毎回ログイン中か確認する😼🛡️ (Auth.js)
ここまでできたら、もう「認証付きアプリ」感ぜんぜん出てるよ〜!🎀🎉