- Next.js 環境構築
- PrismaとPostgreSQLの設定
- NextAuth ログイン機能の実装
- NextAuth ログイン制御の実施・機能拡張 👈ココ
- Tiptapでマークダウンエディタ作成
- 日記の登録
- 画像のアップロード MinIOの設定
概要
ログインが実装できたので、ログイン状態によってダッシュボード画面を開くか、ログイン画面を開くかの制御を行います。
ログイン状態を取得
NextAuth.jsではクライアント側・サーバ側どちらからでも取得できるようになっています。
サーバ処理のほうが高速なので、必要がなければそちらを使います。
ログイン状態の取得には getServerSession
を使います。
- INFO useSessionとは対照的に、useSessionはユーザーがログインしているかどうかに関係なく(クッキーが存在するかどうかにかかわらず)、常にセッションオブジェクトを返します。一方でgetServerSessionは、ユーザーがログインしている場合にのみ(認証されたクッキーが存在する場合のみ)、セッションオブジェクトを返し、それ以外の場合はnullを返します。
ダッシュボード画面にgetServerSessionを追加して、未ログインの場合はログイン画面にリダイレクトします。
import { authOptions } from "@/lib/auth";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
export default async function Home() {
const session = await getServerSession(authOptions);
if (!session) {
redirect("/login");
}
return <div>dashboard</div>;
}
同様にログイン済の場合はログイン画面を表示せず、ダッシュボードにリダイレクトさせます。ただし、ログイン画面はパスワードログインを行うので、こちらはクライアントコンポーネントとします。クライアントサイドで getServerSession が使えないので、useSessionでログイン状態を確認します。
"use client";
import GoogleSignInForm from "@/components/auth/google-signin-form";
import { SessionProvider, useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
export default function LoginPage() {
return (
<SessionProvider>
<Login />
</SessionProvider>
);
}
function Login() {
const { data: session } = useSession();
const router = useRouter();
if (session) {
router.push("/");
}
・
・
・
useSessionを利用する場合はSessionProviderが必要となるため、合わせて追加しています。
※サーバーコンポーネントでSessionProviderは利用できません。
ログイン済みの場合は useRouter を使ってダッシュボード画面に遷移させます。
ダッシュボードにログインユーザーの情報を表示する
ログイン制御ができたので、セッションから情報を取得していきます。
sessionに含まれる情報を確認しておきます。
import { authOptions } from "@/lib/auth";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
export default async function Home() {
const session = await getServerSession(authOptions);
if (!session) {
redirect("/login");
}
return (
<div>
dashboard
<p>{JSON.stringify(session)}</p>
</div>
);
}
画面から確認すると
{"user":
{
"name":"Googleのユーザ名",
"email":"Googleのメールアドレス",
"image":"アイコン画像URL"
}
}
がセッションに含まれていることが分かります。
ログインユーザに紐づく日記を作成したいですが、このままではどのユーザでログインしているかの情報が取得できないため、ユーザーIDをセッションからも参照できるように修正します。
セッションからユーザーIDが取得できるようにする
セッションに自分が欲しい値を追加するには、authOptionsにcallbacksに処理を追加します。
説明にある通りですが、以下のような振る舞いをします。
- JWTのコールバック関数はセッションが作成されるとき(≒セッションにアクセスされるとき)に必ず呼び出されます。
- 引数のuser、account、profileおよびisNewUserは、ユーザーがサインインした後、新しいセッションでこのコールバックが初めて呼び出されるときにのみ渡されます。それ以降の呼び出しでは、tokenのみが使用可能になります。
そのため、ログイン時にuserからidを取得し、jwtトークンに暗号化して含める……という実装を行います。
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { NextAuthOptions } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import db from "./db";
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(db),
secret: process.env.NEXTAUTH_SECRET, //JWTの暗号化に使う
session: {
strategy: "jwt", //デフォルトでjwt。今回は明示的に指定
},
pages: {
signIn: "/login", //ログインページのURL
},
debug: false, //デバッグtrueにすると大量のログが出力されるので注意
providers: [
//https://developers.google.com/identity/protocols/oauth2?hl=ja
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
callbacks: {
//https://next-auth.js.org/configuration/callbacks
async jwt({ token, user }) {
//authorize(認証時)のみuserに値が入ってくるので、tokenに追加しておく(sessionで使うため)
if (user) {
return {
...token,
// name: user.name,
id: user.id,
};
}
console.log("jwt", token);
return token;
},
},
};
上記修正を行った状態で改めてログインし直し、ログを確認してみます。
※公式のサンプルではprofileからidを取得する手順となっていますが、存在しないと怒られます。
※ログイン状態が残っている場合は、Cookieを削除するなどしてログアウトして実施してください。
jwt {
name: '',
email: '',
picture: '',
sub: '',
id: '',
iat: 1705057977,
exp: 1707649977,
jti: ''
}
tokenにidが追加されていることが分かります。sub(jwtのユーザー識別子)がidが同値であることが分かりますが、システムで利用したいidはidとして持たせます(※ここのベストプラクティスは分かっていません)。
sessionにユーザーIDを追加する
JWTには追加されていますが、画面に出力しているsessionにはまだIDが追加されていません。
sessionのコールバックも追加して、ユーザーIDを追加します。
callbacks: {
//https://next-auth.js.org/configuration/callbacks
async jwt({ token, user }) {
},
async session({ session, token }) {
session.user.id = token.id;
return session;
},
},
DefaultSession型にidがないため、以下でエラーが表示されます。
session.user.id = token.id;
idを保持するSessionの型定義も合わせて追加します。
//https://next-auth.js.org/getting-started/typescript
//記載されている定義を参考に、src/types/next-auth.d.tsを作成
import "next-auth";
declare module "next-auth" {
/**
* Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context
*/
// Userの定義を追加する
interface User {
name: string;
id: string;
}
interface Session {
user: User & DefaultSession["user"];
}
}
再度画面を確認すると、セッションにユーザーIDが格納されていることが確認できます。
ヘッダにアバターを追加
shadcn/ui のアバターコンポーネントを利用して実装していきます。

$ pnpm dlx shadcn-ui@latest add avatar
AvatarImageにuser.imageを設定し、AvatarFallbackには名字2文字を設定します。AvatarFallbackはAvatarImageの取得に失敗した場合に表示されるアイコン(イニシャル等)です。
import { authOptions } from "@/lib/auth";
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
async function UserAvatar() {
const session = await getServerSession(authOptions);
if (!session) {
redirect("/login");
}
return (
<Avatar>
<AvatarImage src={session.user?.image!} />
<AvatarFallback>
{session.user?.name?.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
);
}
export default UserAvatar;
Tips
DiceBearを使うとアバターアイコンが簡単に作成・利用できます。
DiceBear | Open Source Avatar Library
APIに対してSeed値を渡すとランダムの画像を返してくれるので、モック用アカウントなどでも有用です。
headerに適用します。モバイル端末では非表示としておきます。
import { ChevronDown } from "lucide-react";
import Link from "next/link";
import UserAvatar from "./user-avatar";
async function Header() {
return (
<header className="z-50 flex w-full flex-wrap border-b border-gray-200 bg-white py-3 text-sm sm:flex-nowrap sm:justify-start sm:py-0 dark:border-gray-700 dark:bg-gray-800">
<nav
className="relative mx-auto w-full max-w-7xl px-4 sm:flex sm:items-center sm:justify-between sm:px-6 lg:px-8"
aria-label="Global"
>
<div className="flex items-center justify-between">{/** 省略 **/}</div>
<div
id="navbar-collapse-with-animation"
className="hs-collapse hidden grow basis-full overflow-hidden transition-all duration-300 sm:block"
>
{/** 省略 **/}
</div>
<div className="hidden sm:ml-6 sm:block">
<UserAvatar />
</div>
</nav>
</header>
);
}
export default Header;