DELOGs
Prisma × PostgreSQLで始めるユーザー・ロール管理

Prisma × PostgreSQLで始めるユーザー・ロール管理

スキーマ設計とDB連携の基礎構築を通じて、認可の土台となるユーザー・ロール情報の管理を実践

初回公開日

最終更新日

0. はじめに

Webアプリケーションにおいて、「ユーザーが誰なのか」「どの権限を持っているのか」を管理する仕組みは、どんな規模のサービスであっても欠かせません。これを支えるのが、 ユーザー管理と認可(Authorization)の基盤設計 です。
本記事では、Next.jsでのバックエンド実装においてよく使われるORM「Prisma」と、信頼性の高いRDB「PostgreSQL」を用いて、ユーザー情報・ロール情報を持つデータベース構成の導入手順を解説していきます。
DELOGsプロジェクトでは、すでに [サーバ構築編#2] さくらクラウドでPostgreSQLのアプライアンスDBを作る で構築した PostgreSQLのアプライアンスDB を使用しており、本記事以降はこのDBとPrismaを接続して進めていきます。
💡 ご自身の環境にPostgreSQLがない場合は、ローカル環境や他のDBでの実装に置き換えて読み進めてください。スキーマの設計やAPIでの制御など、考え方そのものはどのDBでも共通です。
具体的には以下のような構成で進めます:
  • Prismaとは何か? なぜ選ばれるのか?
  • PostgreSQLに新しいDBを作成(template0を推奨)
  • スキーマ設計:Account/Role/Userの3テーブル
  • Prisma Studioでのデータ投入
  • Prisma Clientによる接続設定(database.ts)
  • ログインAPIのDB連携化(仮として平文パスワードを使用)
💡本記事は、次回記事「RBAC導入編 ─ ロールでAPIアクセスを制御する」の準備編として位置づけられます。今回は「DBと接続して動作する最小構成のログインAPI」を構築することがゴールです。
💡 前提:今回は過去記事JWTで保護されたユーザ一覧を実装する ─ 認証・ロール・一覧表示までで構築したソースを改変しながら進めます。先に、この過去記事をご覧になることをオススメいたします。
Prismaが初めての方でも理解できるよう、「 なぜこの設計にするのか 」という意図も丁寧に解説していきます。PostgreSQLとNext.jsによるバックエンド構築の第一歩として、ぜひご活用ください。

1. Prismaとは何か?(ORMの概要)

本格的にWebアプリを作っていくと、ユーザー情報や投稿データを保存する「データベース(DB)」が欠かせません。Prismaはその データベースとNext.jsをつなぐ橋渡し役 となる、非常に強力な ORM(Object Relational Mapper) です。

ORMとは?

ORMとは「Object Relational Mapping」の略で、データベースのテーブルを、プログラム上のオブジェクトとして扱えるようにする仕組みのこと。
  • 通常のSQLだと:
sql
1SELECT * FROM users WHERE email = 'test@example.com';
  • ORMを使うと:
ts
1await prisma.user.findUnique({ where: { email: 'test@example.com' } })
このように TypeScriptの文法で型安全にDB操作ができる ため、学習コストを抑えつつ、保守性・生産性の高いコードが書けます。

Prismaの開発スタイル

Prismaの最大の特徴は、「スキーマファイルでモデルを定義し、それをもとにDB構造とクライアントを生成する」という開発スタイルです。
  • Prismaで定義するのは「User」「Post」などの モデル
  • Prisma CLIがそれをもとに:
    • データベースにテーブルを作成・更新
    • DB操作用の型付きクライアント(Prisma Client)を自動生成
これによって、DB構造とアプリ側のコードが常に一致した状態で開発が進められます。

Prismaでよく出てくる用語:「マイグレーション」とは?

Prismaでは、スキーマファイル(schema.prisma)にモデルを定義して、その内容をデータベースに反映するために「マイグレーション(migration)」という仕組みを使います。
💬 マイグレーションとは?
「テーブルを新しく作る」「カラムを追加する」など、データベースの構造を変更する作業のこと。Prismaではコマンド1つで実行できます。
この仕組みにより、DBの状態をコードと同期させることが簡単になり、チーム開発や後々の保守でも混乱しにくくなります。

Prismaの特徴

Prismaは次のような特徴を持っています:
特徴説明
型安全なDB操作Prisma Clientは自動的に型定義を生成。開発中に型エラーを検知でき、安心してコーディング可能。
スキーマファイルでDB設計を管理schema.prismaファイルにモデルを記述し、それを元にマイグレーション実行。
クライアントの自動生成モデルに応じたCRUDクエリ関数が自動で生成される。SQLを直接書く必要はなし。
CLIとGUIツールが充実CLIでのマイグレーション操作、GUI(Prisma Studio)でのデータ編集が可能。
複数DBサポートPostgreSQL、MySQL、SQLite、MongoDB などに対応。特にPostgreSQLとの相性が良い。
💡 Prismaは“ORMを使うと逆に難しくなる…”という従来の印象を覆す「明快で直感的な操作性」を実現しています。

Prismaの構成ファイル

Prismaを導入すると、主に以下のファイルが作成されます:
ファイル名内容
schema.prismaDBの構造(モデル)を定義するファイル
.envデータベース接続情報などを環境変数として定義
@prisma/clientPrismaが自動生成する型付きクライアントライブラリ
prisma/migrations/マイグレーション履歴が保存されるディレクトリ
このあとの章では、実際にPostgreSQLに接続し、Account/User/Roleといったモデルを定義しながら、マイグレーションの手順も体験していきます。
まずは、Prismaが接続するための PostgreSQL側の準備から進めていきましょう。

2. PostgreSQLに新DB作成(template0推奨)

Prismaでデータベースを操作するためには、あらかじめ PostgreSQLに接続先となるデータベースを作成しておく必要 があります。 この章では、PostgreSQLでの新しいデータベース作成方法と、 template0 を使う理由について説明します。

新しいデータベースを作る理由

Prismaで開発を始める際は、次のような理由で 専用の新データベース を作るのがおすすめです:
  • Prismaのmigrateコマンドで、テーブル構成がどんどん変わる
  • テスト用のデータやスキーマ変更を 安全に試せる
  • 本番用のDBに影響を与えない

文字コード・ロケールも指定してデータベース作成(実用例)

PostgreSQLにログインし、以下のコマンドで新しいデータベースを作成します。

まずはDBサーバへ接続:

bash
1psql -h 192.168.XX.XX -p 5432 -U your_user
  • ホストIPやポート番号は環境に合わせてください

エンコーディング(UTF8) と ロケール(ja_JP.UTF-8) を指定してデータベースを作成するのが推奨される方法です。

sql
1CREATE DATABASE delogs_demo 2 WITH ENCODING 'UTF8' 3 LC_COLLATE='ja_JP.UTF-8' 4 LC_CTYPE='ja_JP.UTF-8' 5 TEMPLATE=template0;
  • この delogs_demo は任意の名前でOKです(プロジェクトごとに分けてもよいでしょう)。
  • ja_JP.UTF-8 が使えない環境では en_US.UTF-8C に置き換えてください。

template0 を使う理由

PostgreSQLでは CREATE DATABASE を実行すると、デフォルトで template1 というテンプレートDBをコピーして新しいDBが作られます。 しかし template1 には、以下のような 追加の設定や拡張が含まれている可能性 があるため、環境によっては不具合やロケールの警告が出ることもあります。
  • 拡張機能(pg_stat_statements など)
  • 語や文字コードの設定(LC_CTYPE、ENCODING など)
これを避けて クリーンな状態でデータベースを作る には、明示的に template0 を指定するのが安全です。

補足:DBeaverやGUIツールで作成する場合

DBeaverなどのGUIクライアントを利用している場合も、DB作成時に「template0」や「ロケール」「文字コード」の設定項目があれば、手動で明示的に選択するようにしてください。

DB作成後の接続確認

sqlコマンドはexit等で抜けてDBサーバからも一旦抜けます。
作成したデータベースに接続できるか確認しておきましょう:
sql
1psql -h 192.168.XX.XX -p 5432 -U your_user -d delogs_demo
無事接続できれば、準備完了です!
次章では、この新しく作ったデータベースに対して Prismaのスキーマ設計 を行っていきます。 Prismaを使って Account、User、Role といったモデルを定義し、マイグレーションでDBに反映していきましょう。

3. Prismaスキーマ設計(Account/Role/User)

この章では、Prismaを使ってユーザー管理とロール制御の基盤となる3つのモデル(Account/Role/User)を設計し、実際にPostgreSQLにテーブルを作成するところまで進めます。
💡 Prismaを使えば、モデル(テーブル定義)をTypeScriptのような構文で記述し、コマンドひとつでDBに反映できます。

Prismaのインストール

まずは必要なパッケージをインストールします。プロジェクトディレクトリに移動して下記のコマンド実行してください。
zsh
1cd /xxx/xxx/project/xxx 2npm install prisma --save-dev 3npm install @prisma/client
パッケージ名役割
prismaCLIツール(スキーマ定義、マイグレーションなど)
@prisma/clientアプリ内からDBにアクセスするクライアント(自動生成される)
💡 prismaは開発時のみ必要なため--save-devでOKですが、@prisma/clientは本番環境にも必要です。

Prismaの初期化

次に、プロジェクトをPrisma対応に初期化します。
zsh
1npx prisma init
これにより以下の2つが生成されます:
  • prisma/schema.prisma:スキーマ定義ファイル(ここにモデルを書く)
  • .env:DB接続用の環境変数ファイル(DATABASE_URL)
💡 すでに.envファイルが存在する場合、npx prisma initを実行しても中身は上書きされません。 下記のような行が挿入されます。
env
1# This was inserted by `prisma init`: 2[object Promise]
上記箇所[object Promise]を消して、下記「.envに接続情報を記述」のようにPrismaのスキーマが依存するDATABASE_URLを記載します。

.envに接続情報を記述

.envファイルを開いて、PostgreSQLへの接続URLを記述します。
env
1DATABASE_URL="postgresql://<ユーザー名>:<パスワード>@localhost:5432/delogs_demo?schema=public"
💡 DB名は前章で作成したもの(例:delogs_demo)にしてください。
💡 ローカル実行するためにSSHトンネルを張る場合、Mac限定になってしまいますが、過去記事SSH接続時に秘密鍵のパスフレーズ入力が不要に?!をご参考ください。

スキーマファイルの構造

まず、Prismaのスキーマファイルは以下の3セクションで構成されます。
prisma
1generator client { 2 provider = "prisma-client-js" 3// output = "../src/generated/prisma" ← 重要!一旦コメントアウト! 4} 5 6datasource db { 7 provider = "postgresql" 8 url = env("DATABASE_URL") 9}
  • generator:クライアント生成設定。 output のところは、 コメントアウト してください
  • datasource:接続するDB設定、デフォルトは PostgreSQLです。
  • model:以下で定義するテーブル群
⚠️ generator部分のoutputはコメントアウトする
ちょっと前は、これはデフォルトでは記載されなかったと思うのですが、今回インストールしたら、デフォルトで記載されていました。これがあると後ほど npx prisma generateしても、 DB接続できない という現象になります。通常の使用ではoutputは使用しないほうがシンプルです。
開発構成output の扱いimport 文の変更
通常の構成❌ 不要@prisma/client のまま
特殊な分離構成(DDDやモノレポ)✅ 使用する場合ありimport を手動変更

モデルを定義する(Account/Role/User)

法人向けシステムを想定しており、ユーザーは「アカウント(法人)」に所属し、かつ「ロール(役割)」を持つという構成にします。
txt
1Account(法人)1 ──── * User(所属ユーザー)1 ──── 1 Role(役割)

Accountテーブルの設計

まずは、法人アカウント情報を保持する Account モデルから見ていきます。
prisma
1model Account { 2 id String @id @default(uuid()) 3 createdAt DateTime @default(now()) 4 updatedAt DateTime @updatedAt 5 deletedAt DateTime? 6 isActive Boolean @default(true) 7 8 code String @unique 9 name String 10 11 users User[] 12}
◯各項目の解説:
フィールド説明
idUUIDで生成される一意なアカウントID(主キー)
createdAt作成日時。デフォルトで現在時刻を記録します
updatedAt更新日時。データの変更があるたびに自動更新されます
deletedAt論理削除用のタイムスタンプ。削除済みであればここに日時が入ります
isActiveアカウントが有効かどうか。退会や一時停止などで false に設定できます
codeログインなどに使用される一意のアカウント識別子(例:acme-corp
name表示用の企業名など
usersこのアカウントに所属するユーザーのリスト(リレーション)
💡code はログイン時などに入力する「会社ID」として使われます。
このようにコードで指定することで、マルチテナント構成における識別子として活用できます。

Roleテーブルの設計

次に、ロール情報を管理する Role モデルを定義します。
prisma
1model Role { 2 id String @id @default(uuid()) 3 createdAt DateTime @default(now()) 4 updatedAt DateTime @updatedAt 5 deletedAt DateTime? 6 isActive Boolean @default(true) 7 8 code String @unique 9 displayName String 10 priority Int 11 12 users User[] 13}
**◯各項目の解説: **
フィールド説明
idロールのUUID(主キー)
createdAt作成日時。現在時刻が自動で入ります
updatedAt更新日時。レコード更新時に自動で更新されます
deletedAt削除日時。論理削除で管理したい場合に使用
isActiveロールが有効かどうか(非推奨にする場合は false)
code論理的な識別子。例:ADMIN, EDITOR
displayName表示用名称。例:管理者, 編集者
priority権限レベルを示す数値(数が大きいほど強い)
usersこのロールを持つユーザー(1対多のリレーション)

Userテーブルの設計

最後に、ユーザー情報を保持する User モデルです。
prisma
1model User { 2 id String @id @default(uuid()) 3 createdAt DateTime @default(now()) 4 updatedAt DateTime @updatedAt 5 deletedAt DateTime? 6 isActive Boolean @default(true) 7 8 accountId String 9 account Account @relation(fields: [accountId], references: [id]) 10 11 roleId String 12 role Role @relation(fields: [roleId], references: [id]) 13 14 email String @unique 15 name String 16 password String 17 18 @@index([accountId]) 19 @@index([roleId]) 20 @@index([isActive, deletedAt]) 21}
◯各項目の解説:
フィールド説明
idUUIDで生成される一意なユーザーID(主キー)
createdAt作成日時。デフォルトで現在時刻を記録します
updatedAt更新日時。自動で現在時刻に更新されます
deletedAt論理削除用のタイムスタンプ。削除されたらここに日時が入ります
isActiveアカウントが有効かどうか。退会などで false にできます
accountId所属するアカウント(法人)を識別する外部キー
accountPrisma Client用のリレーション。user.account.code のように参照
roleIdユーザーに割り当てられたロールのID(外部キー)
rolePrisma Client用の構造体。user.role.displayName などで参照可能
emailログインなどに使われる一意のメールアドレス。@unique で重複を防ぎます
name表示名などに使われるユーザー名
passwordハッシュ化されたパスワードを格納します(生パスワードは絶対NG)
@@index([isActive, deletedAt]) の意図
このインデックスは「有効なユーザー一覧を取得する」ための検索を高速化するために貼っています。
例:
ts
1await prisma.user.findMany({ 2 where: { 3 isActive: true, 4 deletedAt: null 5 }, 6})
こうした検索が多発する場合、パフォーマンスの最適化に非常に有効です。
◯ Prismaでのリレーションの考え方(補足)
prisma
1roleId String 2role Role @relation(fields: [roleId], references: [id])
この2行は、以下の役割を持ちます:
フィールドPrisma上の意味実際のDBに存在する?
roleId外部キー✅ あり
rolePrisma Client用の構造体参照❌ なし(仮想フィールド)
この設計によって、user.role.displayName のように簡単にロール情報にアクセスできます。

PrismaでDBにテーブルを反映(マイグレーション)

💡 マイグレーションとは?
DBスキーマの変更履歴を記録・適用する仕組みのことです。Prismaでは、modelの変更内容をファイルとして保存し、それをもとにDBに反映させます。
実行コマンド:
zsh
1npx prisma migrate dev --name init-rbac
  • --name はマイグレーション名。例:init-rbac。わかりやすい名前をつけておくと、あとで振り返りやすくなります。
  • 実行後、DBに3つのテーブルが作成されます
  • prisma/migrations/ フォルダに履歴が残ります
以上で、スキーマ定義からPostgreSQLへのテーブル作成まで完了です。
次章では、このテーブルに Prisma Studio を使ってサンプルデータを投下していきます。

4. Prisma Studioでサンプルデータ投入

ここでは、先ほど作成した3つのテーブル(Account/Role/User)に、テスト用のサンプルデータを投入します。 UIがない状態でも、 Prisma Studio を使えばGUIで直感的に操作できます。これは、Prismaは同梱されている軽量のGUIツールです。

Prisma Studioを起動する

下記コマンドを実行すると、ブラウザが自動で立ち上がります。
zsh
1npx prisma studio
💡 localhost:5555 が開かない場合は、ターミナルの表示されたURLをコピーしてブラウザに貼り付けてください。
この画面から、各モデルに対してレコードの追加・編集・削除ができます。
Prisma Studioの画面

Step1:Accountの登録

最初に、ユーザーが所属するアカウント情報を登録しましょう。
項目入力例
codetestAccount0123
nameテスト株式会社
isActivetrue (デフォルトでOK)
✅ この「code」はログイン時に識別するための重要なキーになります。

Step2:Roleの登録

続いて、ユーザーの役割となるロールを3種類登録してみましょう。
項目入力例
codeADMIN
displayName管理者
priority100
isActivetrue(デフォルトでOK)
💡 priority を持たせることで、API側で「○○以上の権限」などの判定がしやすくなります。
続けて、以下のようなロールも登録しておくと便利です:
codedisplayNamepriority
EDITOR編集者50
VIEWER閲覧者10

Step3:Userの登録

AccountとRoleを事前に作っておけば、User登録時に accountrole をプルダウンで選択できます。
項目入力例
emailadmin@example.com
name山田太郎
passwordadminPass0123456(※仮の平文)
accounttestAccount0123 を選択
roleADMIN を選択
isActivetrue(デフォルトでOK)
⚠️ 現時点では「平文パスワード」を仮入力としています。
本番では必ずハッシュ化(bcrypt / argon2id)する必要がありますが、UI未実装の現段階ではスキップします。
続けて、以下のようなユーザも登録しておくとテストがしやくなります:
emailnamepasswordrole備考
editor@example.com佐藤花子editorPass1234567EDITOR編集専用ユーザー
viewer@example.com鈴木一郎viewerPass1234567VIEWER閲覧専用ユーザー

Prisma Studioの活用ポイント

Prisma Studioは、ちょっとしたデータの追加・検証・編集には非常に便利です。
  • 登録順やIDでの並び替え
  • JSON形式でコピー可能(他環境への複製にも使える)
  • 削除してやり直すのも簡単
💡 本番運用では当然GUIではなく、APIやバックオフィスUI経由で登録します。
ここでは初期構築フェーズの効率化のために活用します。
これで、APIからの認証・認可テストに使えるサンプルデータがそろいました。
次章では、Prisma Client を使って、アプリケーションからこのDBに接続する設定をしていきます。さらにログインAPIと連携して、ユーザー情報の照合に進みます。

5. Prisma Client接続(database.ts作成)

ここでは、Next.jsアプリケーションからPostgreSQLデータベースにアクセスするための Prisma Clientの接続設定 を行います。
💡 前提:今回は過去記事JWTで保護されたユーザ一覧を実装する ─ 認証・ロール・一覧表示までで構築したソースを改変しながら進めます。先に、この過去記事をご覧になることをオススメいたします。
Prismaは、CLIコマンドで schema.prisma をもとに 型付きのクライアントコード を自動生成してくれます。そのクライアントを、アプリケーション全体で再利用するために lib/database.ts ファイルを用意します。

Prisma Clientの生成

まずはクライアントコードを生成する必要があります。下記コマンドを実行してください。
zsh
1npx prisma generate
💡 すでに migrate dev を実行している場合は、この generate コマンドも同時に実行されているためスキップしてもOKです。

src/lib/database.ts を作成

開発環境(Next.js + TypeScript)で便利な、再利用可能な Prisma Client モジュールを作成します。開発中に起こりやすい “Hot Reloadによる多重インスタンス生成” を防ぐ処理も含めた、よく使われるパターンです。

💡 Hot Reloadによる問題とは?

Next.jsの開発モード(next dev)では、ファイル保存のたびにアプリケーションが自動で再読み込み(Hot Reload)されます。このとき、new PrismaClient() を毎回呼び出すと、Prismaのインスタンスがどんどん増えてしまい、接続数の上限に達してエラーになることがあります。
それを防ぐために、グローバル変数に Prisma Client を保持して再利用することで、開発中の無駄な再生成を防ぐという手法です。
src/lib/database.tsを下記の内容にて作成:
ts
1import { PrismaClient, Prisma } from "@prisma/client"; 2 3// グローバル変数としてPrismaクライアントを定義(開発環境用) 4const globalForPrisma = global as typeof globalThis & { 5 prisma?: PrismaClient; 6}; 7 8// Prismaクライアントを再利用(開発中のHot Reload対応) 9export const prisma = 10 globalForPrisma.prisma || 11 new PrismaClient({ 12 log: 13 process.env.NODE_ENV === "development" 14 ? ["query", "info", "warn", "error"] 15 : ["error"], 16 }); 17 18// 開発環境ではグローバル変数に設定して使い回す 19if (process.env.NODE_ENV !== "production") { 20 globalForPrisma.prisma = prisma; 21} 22 23// Prismaの型も必要に応じてexport 24export { Prisma };
このモジュールを使えば、アプリ内のどこからでも import { prisma } from "@/lib/database"として、安全かつ効率よくDBアクセスできます。

利用方法(例)

prisma をインポートすれば、型付きでDBにアクセスできます:
ts
1import { prisma } from "@/lib/database"; 2 3const users = await prisma.user.findMany({ 4 where: { isActive: true }, 5});
💡 Prismaが生成したClientはすでに型情報が含まれており、エディタ補完も効きます。
Prismaの「型安全性」は非常に強力で、実在しないカラムや関係にアクセスしようとすると即座にエラーとなるため、安心してクエリが書けます。

prisma/clientが動かない場合

クライアント生成がうまくいかない場合、次のように確認してください。
  • @prisma/clientnpm install @prisma/client 済みか?
  • npx prisma generate は済んでいるか?
  • .envDATABASE_URL が正しいか?
  • DBが起動しているか?
この prisma オブジェクトは、今後アプリケーション内でのすべてのDB操作に使用していきます。 次章では、このDB接続を使って、ログインAPIを仮データからDB連携に切り替える実装へと進みます。

6. ログインAPIのDB連携化(平文パスワード仮運用)

これまでの記事では、ログインフォームから送信された情報に対して、仮データをベタ書きで照合していました。しかし本番運用を考えると、当然これはセキュアでも拡張性があるものでもありません。
ここからは、Prisma経由でPostgreSQLのUserテーブルと連携し、「ログイン処理の本質的な流れ」を構築していきます。
💡 前提:今回は過去記事JWTで保護されたユーザ一覧を実装する ─ 認証・ロール・一覧表示までで構築したソースを改変しながら進めます。先に、この過去記事をご覧になることをオススメいたします。
⚠️ 注意:本記事では簡略化のため、パスワードを平文で保存/照合しています。本番では必ず argon2id や bcrypt 等によるハッシュ化を行ってください。導入は後続記事で扱います。

実装の概要

ログイン処理の中でやりたいことはシンプルです:
  • accountid と email と password を受け取る
  • Userテーブルを検索して該当ユーザーを取得(account.codeと紐付け)
  • パスワード(※現状は平文)を照合
  • 正常であればJWTを発行
  • ペイロードに userId, accountId, role などを含める

src/app/api/login/route.ts を編集する

次に前回作成したAPIルートファイルを開きます。前回までの内容は下記の通りです。
ts
1import { NextResponse } from "next/server"; 2import { signToken } from "@/lib/jwt"; 3 4const dummyUsers = [ 5 { 6 accountId: "adminAccount0123", 7 email: "admin@example.com", 8 password: "adminPass0123456", 9 role: "admin", 10 }, 11 { 12 accountId: "userAccount1234", 13 email: "user1@example.com", 14 password: "userPass1234567", 15 role: "user", 16 }, 17 { 18 accountId: "testuserAccount", 19 email: "user2@example.com", 20 password: "testPassword000", 21 role: "user", 22 }, 23]; 24 25export async function POST(req: Request) { 26 const body = await req.json(); 27 const { accountId, email, password } = body; 28 29 // 仮ユーザーの中から一致するものを検索 30 const matchedUser = dummyUsers.find( 31 (user) => 32 user.accountId === accountId && 33 user.email === email && 34 user.password === password, 35 ); 36 37 if (!matchedUser) { 38 return NextResponse.json({ error: "認証に失敗しました" }, { status: 401 }); 39 } 40 41 const payload = { 42 email: matchedUser.email, 43 role: matchedUser.role, 44 }; 45 46 // トークン発行 47 const token = await signToken(payload); 48 49 // クライアントに返す(今回はJSONで返却。Cookie保存ではない) 50 return NextResponse.json({ token }); 51}
これをDB連携して、さきほど作成した各テーブルに問い合わせて照合するように書き換えます。下記のようになります。
ts : route.ts
1import { NextRequest, NextResponse } from "next/server"; 2import { prisma } from "@/lib/database"; 3import { signToken } from "@/lib/jwt"; 4 5export async function POST(req: NextRequest) { 6 const { email, password, accountId } = await req.json(); 7 8 // Accountを指定コードで取得 9 const account = await prisma.account.findUnique({ 10 where: { code: accountId }, 11 }); 12 13 if (!account || !account.isActive || account.deletedAt !== null) { 14 return NextResponse.json( 15 { error: "アカウントが無効です" }, 16 { status: 401 }, 17 ); 18 } 19 20 // Userを取得(email & accountId & isActive) 21 const user = await prisma.user.findFirst({ 22 where: { 23 email, 24 accountId: account.id, 25 isActive: true, 26 deletedAt: null, 27 }, 28 include: { 29 role: true, // ロール情報も取得 30 }, 31 }); 32 33 if (!user) { 34 return NextResponse.json( 35 { error: "ユーザーが見つかりません" }, 36 { status: 401 }, 37 ); 38 } 39 40 // パスワードの照合(※平文!) 41 if (user.password !== password) { 42 return NextResponse.json( 43 { error: "パスワードが間違っています" }, 44 { status: 401 }, 45 ); 46 } 47 48 // JWTペイロード 49 const payload = { 50 email: user.email, 51 role: { 52 code: user.role.code, 53 priority: user.role.priority, 54 }, 55 }; 56 57 const token = await signToken(payload); 58 59 return NextResponse.json({ token }); 60}

◯修正前後の比較と詳しい解説:src/app/api/login/route.ts

♻️ 比較 1:ユーザー情報の取得方法
修正前(仮配列):
ts
1const matchedUser = dummyUsers.find( 2 (user) => 3 user.accountId === accountId && 4 user.email === email && 5 user.password === password, 6);
  • dummyUsers というベタ書きの配列からユーザーを検索していました。
修正後(Prisma + DB連携):
ts
1const account = await prisma.account.findUnique({ 2 where: { code: accountId }, 3});
ts
1const user = await prisma.user.findFirst({ 2 where: { 3 email, 4 accountId: account.id, 5 isActive: true, 6 deletedAt: null, 7 }, 8 include: { 9 role: true, 10 }, 11});
  • accountId を使って、法人アカウント(Account)をDBから検索。
  • 見つかった account.id を使って、ユーザー(User)を紐づけ検索。
  • さらに isActive: true & deletedAt: null の条件付きで、有効かつ論理削除されていないユーザーだけを許可。
  • include: { role: true } によって、Roleテーブルも同時に取得し、あとでトークンに役割を含めることが可能に。
🔍 このように、DB上の「本物のデータ」と照合する構成に変更することで、実運用に耐えられる構成になります。
♻️ 比較 2:JWTペイロードの内容と構造
修正前(シンプルな構造):
ts
1const payload = { 2 email: matchedUser.email, 3 role: matchedUser.role, 4};
  • accountId をそのまま sub に設定(一般的には sub はユーザーID)
  • role もただの文字列(“admin”など)
修正後(構造化された詳細なペイロード):
ts
1const payload = { 2 email: user.email, // メールアドレス 3 role: { 4 code: user.role.code, 5 priority: user.role.priority, 6 }, 7};
  • sub に UUIDのユーザーIDを設定(トークン仕様として正しい構成)
  • role をオブジェクト形式で含め、RBAC制御のために必要な情報(codeとpriority)を内包。
  • これにより、トークン内だけで「このユーザーの権限」を判断できる構成に進化。
💡 role.priority の値が 100 以上なら admin、50 以上なら editor、などの比較が可能になる。
♻️ 比較 3:バリデーションと安全性
修正前:
  • emailやpasswordに対する事前チェックなし
修正後:
ts
1if (!user || !user.role) { 2 return NextResponse.json({ error: "認証に失敗しました" }, { status: 401 }); 3}
  • DBに該当するユーザーがいない場合、またはRole情報が欠けている場合、即エラー応答。
  • isActive: truedeletedAt: null を組み込んでいるため、退会済みユーザーなどの不正ログインを防止。
♻️ 比較 4:テストデータと現実運用への準備
修正前:
  • テスト用ユーザーはコード上で定義
修正後:
  • Prisma Studio や SQLクライアントから自由にユーザー追加・編集が可能
  • 運用に近い状態でのテストが可能になり、ログインE2Eテストの構成も現実的になる
ログイン機能のDB連携は完了です。次はもう一つ、ログイン後のページ「ユーザ一覧」のDB連携を行います。

7. ユーザ一覧のDB連携(/usersページを本番仕様に)

この章では、仮データで構成していた /users ページを、Prisma経由でPostgreSQLに接続し、実際のユーザー情報を一覧表示できるようにします。
💡 ユーザー一覧は認証後のみに表示され、トークンによって保護されたルートになっています。認証・トークン処理については過去記事JWTで保護されたユーザ一覧を実装する ─ 認証・ロール・一覧表示までを参照してください。

APIルート /api/users/route.ts を作成

まずは、ユーザー情報を取得するAPIエンドポイントを作成します。
src/app/api/users/route.ts を新規作成し、以下のように記述します:
ts
1import { NextRequest, NextResponse } from "next/server"; 2import { prisma } from "@/lib/database"; 3import { verifyToken } from "@/lib/jwt"; 4 5export async function GET(req: NextRequest) { 6 const authHeader = req.headers.get("Authorization"); 7 const token = authHeader?.replace("Bearer ", ""); 8 9 if (!token) { 10 return NextResponse.json( 11 { error: "トークンがありません" }, 12 { status: 401 }, 13 ); 14 } 15 16 const payload = await verifyToken(token); 17 if (!payload || !payload.role) { 18 return NextResponse.json({ error: "認証に失敗しました" }, { status: 401 }); 19 } 20 21 // 22 const users = await prisma.user.findMany({ 23 where: { 24 isActive: true, 25 deletedAt: null, 26 }, 27 include: { 28 account: true, 29 role: true, 30 }, 31 }); 32 33 return NextResponse.json({ users }); 34}

💡 補足:

処理解説
トークン取得Authorization: Bearer xxx 形式からJWTを抽出
トークン検証verifyToken() で署名&期限チェック
ロールチェックpayload.role.priority >= 100 で管理者判断(RBAC)
ユーザー取得isActive: truedeletedAt: null でフィルタ
リレーション展開include: { account, role } で所属先・役割名も取得
  • トークンからペイロードを検証し、payload.role.priorityなどを用いた制限は今後のRBAC編で導入予定です。

src/app/users/UsersClient.tsx を修正

これまでは、src/lib/users.tsの配列を読み込んでいましたが、先ほど作成したエンドポイントを利用するように修正します。
tsx
1"use client"; 2 3import { useEffect, useState } from "react"; 4import AuthenticatedArea, { 5 User as AuthUser, 6} from "@/components/AuthenticatedArea"; 7import { columns } from "./columns"; 8import { DataTable } from "@/components/shared/data-table"; 9import { User, Account, Role } from "@prisma/client"; 10 11export type DBUser = User & { 12 account: Account; 13 role: Role; 14}; 15 16export default function UsersClient() { 17 const [users, setUsers] = useState<DBUser[]>([]); 18 const [loading, setLoading] = useState(true); 19 const [error, setError] = useState(""); 20 21 useEffect(() => { 22 const fetchUsers = async () => { 23 const token = localStorage.getItem("token"); 24 if (!token) { 25 setError("トークンが見つかりませんでした"); 26 setLoading(false); 27 return; 28 } 29 30 try { 31 const res = await fetch("/api/users", { 32 headers: { 33 Authorization: `Bearer ${token}`, 34 }, 35 }); 36 37 if (!res.ok) { 38 const errorData = await res.json(); 39 setError(errorData.error || "ユーザー取得に失敗しました"); 40 setLoading(false); 41 return; 42 } 43 44 const data = await res.json(); 45 setUsers(data.users); 46 } catch (e) { 47 setError("ユーザー取得中にエラーが発生しました"); 48 } finally { 49 setLoading(false); 50 } 51 }; 52 53 fetchUsers(); 54 }, []); 55 56 return ( 57 <AuthenticatedArea> 58 {(user: AuthUser) => ( 59 <div className="mx-auto max-w-4xl px-4 pt-10"> 60 <h1 className="mb-4 text-2xl font-bold">ユーザー一覧</h1> 61 <p className="text-muted-foreground mb-6 text-sm"> 62 ログイン中ユーザー:{user.email}(ロール: {user.role?.code}63 </p> 64 65 {loading ? ( 66 <p>読み込み中...</p> 67 ) : error ? ( 68 <div className="text-destructive font-medium">{error}</div> 69 ) : user.role?.code === "ADMIN" ? ( 70 <DataTable columns={columns} data={users} /> 71 ) : ( 72 <div className="text-destructive font-medium"> 73 このページは admin ロールのユーザーのみ閲覧可能です。 74 </div> 75 )} 76 </div> 77 )} 78 </AuthenticatedArea> 79 ); 80}

💡 補足:

修正箇所解説
DBUser 型の定義APIで返される構造に合わせて明示。型安全を保つために導入。
useEffect で fetch認証済トークンを使って /api/users にリクエスト送信。
エラーハンドリングトークン欠如・APIエラー時に明確なエラー表示。
DataTable 表示ADMIN ロールのみ表示。それ以外はアクセス拒否表示。
型定義のところは、下記のように型定義をテーブル定義と同期することができて便利です。
tsx
1import { User, Account, Role } from "@prisma/client"; 2 3export type DBUser = User & { 4 account: Account; 5 role: Role; 6};

src/app/users/columns.ts を修正

カラムの定義ファイルも修正が必要になります。型定義の部分です。
下記のように、src/app/users/UsersClient.tsxで定義した型を適用します。ColumnDef<DBUser>とします。
ts
1"use client"; 2 3import { ColumnDef } from "@tanstack/react-table"; 4import type { DBUser } from "./UsersClient"; 5 6export const columns: ColumnDef<DBUser>[] = [ 7 { 8 accessorKey: "id", 9 header: "ID", 10 }, 11 { 12 accessorKey: "name", 13 header: "名前", 14 }, 15 { 16 accessorKey: "email", 17 header: "メールアドレス", 18 }, 19 { 20 accessorKey: "role.code", 21 header: "ロール", 22 }, 23];
このように、今回の変更は 「開発用仮構成 → 実運用に耐えうるDB連携構成」への進化 を意味します。 実際の管理画面やUI操作に連携させるための土台として、不可欠な一歩となります。

8. テストログイン用データの復習(Prisma Studioで登録済)

これらの情報を使って、ログインフォームからログインすれば、これまでのDB連携されていることが確認できます。ぜひ、試してみてください。
アカウントID(code)emailpasswordロール
testAccount0123admin@example.comadminPass0123456ADMIN
testAccount0123editor@example.comeditorPass1234567EDITOR
testAccount0123viewer@example.comviewerPass1234567VIEWER
ログイン後のユーザ一覧画面
ユーザ一覧の画面
次回は RBAC を導入し、この JWT の role.priority を使ってAPIアクセス制御を行います。

参考文献

この記事の執筆・編集担当
DE

松本 孝太郎

DELOGs編集部/中年新米プログラマー

ここ数年はReact&MUIのフロントエンドエンジニアって感じでしたが、Next.jsを学んで少しずつできることが広がりつつあります。その実践記録をできるだけ共有していければと思っています。