DELOGs
JWTログインAPIをNext.jsで実装する

JWTログインAPIをNext.jsで実装する

Shadcn/uiとつなぐ認証基盤の第一歩

初回公開日

最終更新日

0. はじめに

本記事は、前回のShadcn/uiで作るログイン画面の続編となります。 ここでは UI だけではなく、実際にフォームから情報を送信して、JWTトークンを発行するログインAPIを実装します。
また、JWTって何? Next.js での認証方式とトークンの仕組みを徹底解説(超入門)で解説したように、JWTはステートレスで軟弱性もある認証トークンです。 この記事ではそれを前提に、ミニマムな構成で実装する方法を紹介します。

1. 目標と構成

今回は、ログインAPIを実装し、JWTを使ったログイン後のユーザー認証フローを構築することが目的です。
  • /api/login にログイン用APIを実装し、JWTを発行
  • localStorage にJWTを保存し、後続ページで利用
  • /dashboard ページでは、JWTを使って認証チェックを行い、ユーザー情報を表示
  • 認証処理は AuthenticatedArea.tsx に共通化し、どのページでも再利用可能な設計に
下記の構成で、フロント〜バックエンドを含めた最小構成のJWT認証機能を実現します。
txt
1📁 src 2├── app 3│ ├── login/page.tsx ← UI(前回実装) 4│ ├── api/login/route.ts ← ログインAPI(今回実装) 5│ ├── api/protected/route.ts ← 認証付きAPI 6│ ├── dashboard/ 7│ │ ├── page.tsx ← ダッシュボード(SSRラッパー) 8│ │ └── DashboardClient.tsx ← トークン検証+ユーザー表示 9├── components 10│ └── AuthenticatedArea.tsx ← トークン検証の共通コンポーネント 11├── lib 12│ ├── jwt.ts ← JWTの署名・検証ユーティリティ 13│ └── users.ts ← 仮ユーザーデータ(今後利用)

2. JWT生成・検証の基盤実装

まずは JWT を発行・検証する関数を lib/jwt.ts に実装します。ここでは jose ライブラリを使用します。

joseのインストール

joseライブラリとは?

jose は、Node.jsとブラウザの両方で使える JWTの生成と検証に特化した暗号ライブラリです。
  • SignJWT はトークンの発行に使用します
  • jwtVerify は受け取ったトークンが正しい署名かを検証します
下記のコマンドでインストールを実行します。
zsh
1npm install jose

🔧 src/lib/jwt.ts のコード

認証制御をsrc/lib/jwt.tsを作成して下記のように記述します。 今回は jsonwebtoken パッケージではなく、上記でインストールした、より軽量かつブラウザ対応も意識された jose パッケージを利用して JWT の署名・検証を行います。
ts : jwt.ts
1import { SignJWT, jwtVerify } from "jose"; 2 3const secret = new TextEncoder().encode(process.env.JWT_SECRET || "secret-key"); 4const alg = "HS256"; 5 6// JWTの署名を行う関数(アクセストークン発行) 7export async function signToken(payload: JWTPayload): Promise<string> { 8 return await new SignJWT(payload) 9 .setProtectedHeader({ alg }) 10 .setIssuedAt() 11 .setExpirationTime("1h") 12 .sign(secret); 13} 14 15// JWTの検証を行う関数(Cookieから読み取ったtokenを確認) 16export async function verifyToken(token: string): Promise<JWTPayload | null> { 17 try { 18 const { payload } = await jwtVerify(token, secret); 19 return payload as JWTPayload; 20 } catch (e) { 21 if (process.env.NODE_ENV === "development") { 22 console.error(e); // 開発中だけログ出す 23 } 24 return null; 25 } 26} 27 28// JWTペイロードの型(型安全に記述) 29export type JWTPayload = { 30 sub: string; // アカウントID(ログインに使うID) 31 email: string; // 入力されたメールアドレス 32 role?: string; // 今後RBACに使う可能性があるので任意で保持 33 iat?: number; // 発行時刻(JWT標準) 34 exp?: number; // 有効期限(JWT標準) 35};

💡 コードの解説

secret の設定方法

ts
1const secret = new TextEncoder().encode(process.env.JWT_SECRET || 'secret-key')
  • process.env.JWT_SECRET: **.env**ファイルやVercel環境変数などに定義した「秘密鍵」です。
  • TextEncoderUint8Array に変換するのは jose ライブラリが求める形式のためです
  • .encode(...):joseライブラリではUint8Array型のバイト列が必要なので、文字列をバイト配列に変換しています。
安全な process.env.JWT_SECRET の条件
  • 英数字 + 記号(ASCII文字のみ)
  • 最低32文字以上(64〜128文字推奨)
  • Base64形式 or ランダム生成(openssl rand -base64 64など)
◯JWT_SECRETの作成と設定例:
これはmacOSなら普通に利用可能なコマンドだと思います:
zsh
1openssl rand -base64 64
dotenv : env
1JWT_SECRET=BIKG9f2KSsxj4lMT+iDl3BEgCg6PvURpqSUw8umE9g/ju/mUNi6DiEjOlM9bwz6GVo4+qhJUTUJWNxLzBkWRsQ==

🔒 SignJWTjwtVerify の使い分け

  • SignJWT(payload):トークンを生成(署名)するためのクラス
  • jwtVerify(token, secret):受け取ったトークンを検証し、署名と有効期限を確認

signToken() の各処理の意味

ts
1new SignJWT(payload) 2 .setProtectedHeader({ alg }) 3 .setIssuedAt() 4 .setExpirationTime('1h') 5 .sign(secret)
  • new SignJWT(payload):ペイロード(トークンの中身)をセットします。このときの payload は、JWTPayload 型で「sub(アカウントID)」「email」「role」などを含むオブジェクトです。
  • .setProtectedHeader({ alg }):使用する署名アルゴリズムを指定します(ここでは HS256)。
  • .setIssuedAt():現在時刻(UNIX時間)を iat として付加します。
  • .setExpirationTime('1h'):トークンの有効期限を「1時間」に設定します。
  • .sign(secret):最後に秘密鍵で署名し、安全なトークンとして返します。
この関数によって、改ざん不可な1時間有効のJWTが発行されます。

verifyToken() の役割

ts
1export async function verifyToken(token: string): Promise<JWTPayload | null> { 2 try { 3 const { payload } = await jwtVerify(token, secret); 4 return payload as JWTPayload; 5 } catch (e) { 6 return null; 7 } 8}
  • この関数はトークンの整合性(署名・有効期限)を検証します。
  • 失敗時は null を返すことで「未ログイン」扱いにできます。
⚠️ トークンが 改ざん されていたり、有効期限切れ の場合は catch に入り、ログイン済み扱いにはなりません。
次のステップでは、このトークンの発行と検証を行うAPIルート /api/login を実装し、ログイン処理を完成させていきます。

3. /api/login の実装:トークン発行API

ここからは、ログインフォームから送信された情報を受け取り、JWTを発行して返すAPIルート src/app/api/login/route.ts を作成します。
ts : route.ts
1import { NextResponse } from "next/server"; 2import { signToken } from "@/lib/jwt"; 3 4// 仮のユーザーデータ(デモ用) 5const dummyUser = { 6 accountId: "demoaccount0123", 7 email: "demo@example.com", 8 password: "password0123456", // 本番ではハッシュ化必須 9 role: "admin", 10}; 11 12export async function POST(req: Request) { 13 const body = await req.json(); 14 const { accountId, email, password } = body; 15 16 // 入力値の検証(実際はZodやDB連携が必要) 17 if ( 18 accountId !== dummyUser.accountId || 19 email !== dummyUser.email || 20 password !== dummyUser.password 21 ) { 22 return NextResponse.json({ error: "認証に失敗しました" }, { status: 401 }); 23 } 24 25 // JWTのペイロード 26 const payload = { 27 sub: dummyUser.accountId, 28 email: dummyUser.email, 29 role: dummyUser.role, 30 }; 31 32 // トークン発行 33 const token = await signToken(payload); 34 35 // クライアントに返す(今回はJSONで返却。Cookie保存ではない) 36 return NextResponse.json({ token }); 37}

🔰 コードの解説

📥 POST(req: Request) とは?

Next.js App Routerでは、app/api/... フォルダ以下の route.tsGET()POST() を定義することでAPIエンドポイントを実装します。
ここではログインフォームからPOSTされたデータを受け取りたいので、POST() を使います。

🧪 dummyUser で仮ログイン検証

ts
1const dummyUser = { 2 accountId: 'demo123', 3 email: 'demo@example.com', 4 password: 'password123', 5 role: 'admin' 6}
実際のアプリではデータベースからユーザー情報を取得して照合しますが、今回は簡易デモとして固定ユーザー情報を直接コードに記述しています。

✅ 入力値をチェックしてJWTを発行

ts
1if (accountId !== dummyUser.accountId || ...) { 2 return 401 3}
  • ログイン失敗時は HTTPステータス401 Unauthorized を返します
ts
1const payload = { sub, email, role }
  • signToken() 関数に渡す payload には、ユーザーを一意に特定するために sub(アカウントID)を含めます

🔐 今回は Cookie 保存ではない理由

今回はトークンを HttpOnly Cookie に保存するのではなく、フロントエンド側で localStorage に保存する設計にしています。
その理由は以下の通りです:
  • 初学者が JWT の仕組みを「目で見て理解」しやすくするため
  • localStorage に保存すれば、開発者ツールからトークンの中身が確認できる
  • Cookie + HttpOnly 方式は本番向けだが、CSRF対策やトークン無効化処理など複雑さが増す
  • 今回の記事では「最低限のログイン処理」に焦点をあて、後続記事でセキュアな保存方法に拡張予定
✅ 本番環境では、セキュリティの観点から HttpOnly Cookie + SameSite=Strict + Secure による保存が推奨されます。
次はこのAPIをフロントエンドのLoginForm.tsxから呼び出し、実際にフォームから認証 → トークン保存 → リダイレクトの流れを作っていきます。

4. LoginForm.tsx からトークンを受け取り保存する

フロントエンドのログインフォームから、先ほどの /api/login API を呼び出して、認証成功時には JWT を localStorage に保存し、失敗時にはエラーメッセージを表示します。
Shadcn/uiで作るログイン画面 で作成した src/components/LoginForm.tsx に追記していきます。
diff : LoginForm.tsx
1"use client"; 2 3import { useState } from "react"; 4+ import { useRouter } from "next/navigation"; 5import { useForm } from "react-hook-form"; 6import { zodResolver } from "@hookform/resolvers/zod"; 7import { loginSchema } from "@/lib/schema"; // ログインスキーマ 8import { z } from "zod"; 9import { 10 Form, 11 FormField, 12 FormItem, 13 FormLabel, 14 FormControl, 15 FormMessage, 16} from "@/components/ui/form"; 17import { Input } from "@/components/ui/input"; 18import { Button } from "@/components/ui/button"; 19import { Eye, EyeOff } from "lucide-react"; 20 21export default function LoginForm() { 22 const form = useForm<z.infer<typeof loginSchema>>({ 23 resolver: zodResolver(loginSchema), 24 defaultValues: { 25 accountId: "", 26 email: "", 27 password: "", 28 }, 29 }); 30 31+ const router = useRouter(); 32+ const [error, setError] = useState(""); 33 34 const [showPassword, setShowPassword] = useState(false); 35 36 function onSubmit(values: z.infer<typeof loginSchema>) { 37+ setError(""); 38+ fetch("/api/login", { 39+ method: "POST", 40+ headers: { 41+ "Content-Type": "application/json", 42+ }, 43+ body: JSON.stringify(values), 44+ }) 45+ .then(async (res) => { 46+ if (!res.ok) { 47+ const { error } = await res.json(); 48+ setError(error || "ログインに失敗しました"); 49+ return; 50+ } 51+ const { token } = await res.json(); 52+ localStorage.setItem("token", token); 53+ router.push("/dashboard"); 54+ }) 55+ .catch(() => { 56+ setError("通信エラーが発生しました"); 57+ }); 58 } 59 60 return ( 61 <Form {...form}> 62 <form 63 onSubmit={form.handleSubmit(onSubmit)} 64 className="mx-auto max-w-md space-y-4 pt-4 pb-10" 65 > 66 <FormField 67 control={form.control} 68 name="accountId" 69 render={({ field }) => ( 70 <FormItem> 71 <FormLabel>アカウントID</FormLabel> 72 <FormControl> 73 <Input 74 data-testid="accountId" 75 placeholder="CORP000123456" 76 autoFocus 77 {...field} 78 /> 79 </FormControl> 80 <FormMessage data-testid="accountId-error" /> 81 </FormItem> 82 )} 83 /> 84 <FormField 85 control={form.control} 86 name="email" 87 render={({ field }) => ( 88 <FormItem> 89 <FormLabel>メールアドレス</FormLabel> 90 <FormControl> 91 <Input 92 data-testid="email" 93 type="email" 94 autoComplete="email" 95 placeholder="your@email.com" 96 {...field} 97 /> 98 </FormControl> 99 <FormMessage data-testid="email-error" /> 100 </FormItem> 101 )} 102 /> 103 <FormField 104 control={form.control} 105 name="password" 106 render={({ field }) => ( 107 <FormItem> 108 <FormLabel>パスワード</FormLabel> 109 <div className="flex items-start gap-2"> 110 <FormControl> 111 <Input 112 {...field} 113 data-testid="password" 114 type={showPassword ? "text" : "password"} 115 autoComplete="current-password" 116 placeholder="半角英数字15文字以上" 117 /> 118 </FormControl> 119 <Button 120 data-testid="password-toggle" 121 type="button" 122 size="icon" 123 variant="outline" 124 onClick={() => setShowPassword((prev) => !prev)} 125 aria-label={ 126 showPassword 127 ? "パスワードを非表示にする" 128 : "パスワードを表示する" 129 } 130 className="shrink-0 cursor-pointer" 131 > 132 {showPassword ? <EyeOff size={18} /> : <Eye size={18} />} 133 </Button> 134 </div> 135 <FormMessage data-testid="password-error" /> 136 </FormItem> 137 )} 138 /> 139 <Button 140 data-testid="submit" 141 type="submit" 142 className="mt-4 w-full cursor-pointer" 143 > 144 ログイン 145 </Button> 146+ {error && <p className="mt-2 text-sm text-red-500">{error}</p>} 147 </form> 148 </Form> 149 ); 150}

🔰 コードの解説:この実装の各ポイント

🧪 fetch("/api/login") の使い方

  • 第2引数で method: POST を指定し、body にJSON形式のログイン情報を送信
  • headersContent-Type: application/json を指定しないと Express/Next 側が解釈できない

🧠 res.ok とは?

  • HTTPステータスが200系(成功)であれば true、それ以外(400/500系など)は false になります
  • 認証失敗時には 401 を返す設計なので、ここで弾ける

💾 localStorage.setItem("token", token) の意味

  • ブラウザのローカルストレージにJWTトークンを保存します
  • これにより、後続のAPIリクエストなどでトークンを付与する実装が可能になる(※今回はまだ未実装)

✅ なぜ localStorage に保存?

  • 今回は Cookie 保存を行っていないため、トークンの保持先をフロントエンド側で持つ必要がある
  • localStorage に保持すれば、次回のリクエストで fetch ヘッダーに Authorization をつけるなどの操作ができる

🔁 router.push("/dashboard")

  • 認証成功時にダッシュボードページ(仮)へ遷移します
  • 実際の画面がまだ存在しない場合は page.tsx を用意してください(仮でもOK)

🧾 <LoginForm /> 側の表示追加

送信ボタンの下に エラー表示 <p> を1行追加しています:
tsx
1{error && <p className="text-sm text-red-500 mt-2">{error}</p>}
次は、この保存されたトークンを使って「保護されたAPI」を呼び出す方法や、ログイン中ユーザーの状態を表示する Dashboard ページの構築に進みます。

5. /dashboard ページの構築と保護APIの呼び出し

ログイン後に遷移する /dashboard ページでは、次の処理を行います:
  • localStorage から JWT トークンを取得
  • API /api/protected にトークンを付けて送信
  • トークンの有効性を検証し、ユーザー情報を表示
このようにして「ログイン済みユーザーだけがアクセスできる保護ページ」を構築します。

6. APIルート /api/protected の実装

まずはバックエンド側から。新しいAPIエンドポイントを作成し、トークンを検証してユーザー情報を返すようにします。 src/app/api/protected/route.tsを下記の内容で作成します。
ts : route.ts
1import { NextRequest, NextResponse } from "next/server"; 2import { verifyToken } from "@/lib/jwt"; 3 4export async function GET(req: NextRequest) { 5 const authHeader = req.headers.get("Authorization"); 6 const token = authHeader?.replace("Bearer ", ""); 7 8 if (!token) { 9 return NextResponse.json( 10 { error: "トークンがありません" }, 11 { status: 401 }, 12 ); 13 } 14 15 const payload = await verifyToken(token); 16 17 if (!payload) { 18 return NextResponse.json({ error: "無効なトークンです" }, { status: 401 }); 19 } 20 21 return NextResponse.json({ user: payload }); 22}

🔍 詳細解説:API 側の役割

🔸 req.headers.get("Authorization")

  • クライアントから送られてくる Authorization ヘッダー("Bearer トークン文字列")を取得します。
  • 形式は Authorization: Bearer eyJhbGciOi... のようになっています。

🔸 replace("Bearer ", "")

  • ヘッダーの中から "Bearer " の部分を取り除き、実際のトークン文字列だけを抽出します。
  • スペースや大文字・小文字を間違えると正しく動作しないため注意が必要です。

🔸 verifyToken(token)

  • src/lib/jwt.ts で定義したトークン検証関数を使って、有効なトークンかどうかを確認します。
  • 有効であればユーザー情報(JWTのペイロード)を返し、無効であれば null を返します。

🔸 返却内容

  • トークンが正しければ user 情報(sub, email, roleなど)を JSON で返します。
  • トークンがない、または無効な場合は 401 Unauthorized エラーを返してガードします。

7. /dashboard ページの実装

ログイン成功後、ユーザーを /dashboard に遷移させる構成にします。このページでは「トークンが正しいことを検証」し、ログインユーザー情報(メールアドレスや権限など)を表示します。
ただし、今回の実装ではJWTをlocalStorageに保存しているため、サーバー側(``)ではトークンを取得できません。したがって、クライアントコンポーネント側でトークンの読み取り・検証処理を行う必要があります。
この目的のため、以下のようなファイル構成にします:
txt
1📁 src 2├── app 3│ └── dashboard 4│ ├── page.tsx ← サーバーサイドは構造だけ 5│ └── DashboardClient.tsx ← トークンを検証し、ユーザー情報を表示

📄 page.tsx(サーバーコンポーネント)

src/app/dashboard/page.tsxは下記のようになります。
tsx
1import DashboardClient from "./DashboardClient"; 2 3export default function DashboardPage() { 4 return <DashboardClient />; 5}
このファイルはSSRで処理されますが、認証処理は含みません。単にクライアントコンポーネントをラップして返すだけです。

🧠 なぜこの構成?

  • localStorageはサーバー側で使えないlocalStorage はブラウザ専用APIのため、Node.js側ではアクセスできません
  • JWTをlocalStorageに保存している限り、認証チェックはクライアントで行う必要がある
  • 将来Cookieに保存する場合でもこの構成を活かせる:APIを通じて認証確認する設計は、Cookie方式にも応用可能です

8. DashboardClient.tsx の役割と構成

この DashboardClient.tsx に以下の機能を実装します:
  • localStorageからJWTを取得
  • /api/protected にリクエストを送り、ログイン状態を検証
  • 成功時:メールアドレスや権限情報を表示
  • 失敗時:ログイン画面へのリダイレクト、またはエラー表示
また、この処理は今後 /users などのページでも再利用できるよう、汎用コンポーネント AuthenticatedArea.tsx に切り出す予定です。
このように、Next.js + JWT認証構成においては「サーバーで無理に認証処理をしない」ことで、構成の明快さとクライアント制御の柔軟性を最大化できます。
次は、DashboardClient.tsx の具体的なコード実装と、ログイン後ユーザーの状態管理UIを構築していきます。

9. DashboardClient.tsx の実装

ここでは、/dashboard ページでログイン済みユーザーの情報を表示するためのクライアントコンポーネント DashboardClient.tsx を実装します。
トークンの取得と認証チェックの処理は、他ページでも再利用可能にするため、共通コンポーネント src/components/AuthenticatedArea.tsxに切り出して利用します。
txt
1📁 src 2├── components 3│ └── AuthenticatedArea.tsx ← トークンの読み取り&API検証 4├── app 5│ └── dashboard 6│ ├── page.tsx 7│ └── DashboardClient.tsx ← ここで認証結果を使ってUIを構成

✅ AuthenticatedArea.tsx の内容

トークンの読み取り&API検証を行うsrc/components/AuthenticatedArea.tsxを作成します。
tsx
1import { useEffect, useState } from "react"; 2import { useRouter } from "next/navigation"; 3 4export type User = { 5 sub: string; 6 email: string; 7 role?: string; 8}; 9 10type Props = { 11 children: (user: User) => React.ReactNode; 12}; 13 14export default function AuthenticatedArea({ children }: Props) { 15 const [user, setUser] = useState<User | null>(null); 16 const [checking, setChecking] = useState(true); 17 const router = useRouter(); 18 19 useEffect(() => { 20 const token = localStorage.getItem("token"); 21 if (!token) { 22 router.replace("/"); 23 return; 24 } 25 26 fetch("/api/protected", { 27 method: "GET", 28 headers: { 29 Authorization: `Bearer ${token}`, 30 }, 31 }) 32 .then(async (res) => { 33 if (!res.ok) { 34 throw new Error("Unauthorized"); 35 } 36 const { user } = await res.json(); 37 setUser(user); 38 setChecking(false); 39 }) 40 .catch(() => { 41 localStorage.removeItem("token"); 42 router.push("/"); 43 }); 44 }, [router]); 45 46 if (checking) return <p>読み込み中...</p>; 47 if (!user) return null; // ログインページにリダイレクト中 48 49 return <>{children(user)}</>; 50}

🔍 解説:このコンポーネントの目的

処理説明
localStorage.getItem("token")ローカルストレージに保存されたトークンを読み込む
/api/protected に fetchJWT が有効かどうかをAPI経由で検証
children(user) で描画ログイン済みユーザーを親コンポーネントに引き渡す

✅ DashboardClient.tsx の内容

tsx
1"use client"; 2 3import AuthenticatedArea, { User } from "@/components/AuthenticatedArea"; 4 5export default function DashboardClient() { 6 return ( 7 <AuthenticatedArea> 8 {(user: User) => ( 9 <div className="max-w-xl mx-auto pt-10"> 10 <h1 className="text-2xl font-bold mb-4">ダッシュボード</h1> 11 <p>ようこそ、{user.email} さん</p> 12 <p className="text-sm text-muted-foreground mt-2"> 13 アカウントID: {user.sub} 14 </p> 15 {user.role && <p>ロール: {user.role}</p>} 16 </div> 17 )} 18 </AuthenticatedArea> 19 ); 20}

🧠 解説:このコンポーネントの責務

  • 認証に成功したユーザーの情報を受け取り、UIに表示します
  • AuthenticatedArea が認証状態を判断・管理してくれるため、ロジックは完全に分離可能
  • User 型に基づいて安全に emailrole を参照できます
このようにすることで:
  • トークン検証処理を一箇所に集約
  • 複数ページで使い回せる
  • 認証結果を明確に props として扱える
  • 今後の RBAC 対応やログインユーザーの切り替えにも対応しやすくなる

10. テスト実行

ここまで作成したら、基本的な動作の確認です。
zsh
1npm run dev
ログイン画面が起動したら、src/app/api/login/route.tsで記載したテスト用のログインデータを入力します。
txt
1 accountId: "demoAccount0123", 2 email: "demo@example.com", 3 password: "passWord0123456",
ログイン成功時:
ログイン成功時の画面
ログイン失敗時:
ログイン失敗時の画面
上記のような2パターンの状態を確認できます。 また、ログインしていない状態(別ブラウザなどチェックするほうが簡単かも)で直接http://localhost:3000/dashboardでダッシュボードページを表示しようとしてもログイン画面へ遷移することを確認可能です。
これで、ログイン機能のUIを利用した実践ができました。まだDB連携などこれからですが、まずは基本の流れが体験できました。
次回はこの AuthenticatedArea を使って、/users ページでログイン済みユーザー一覧を管理者限定で表示するUIの構築に進みます。

参考文献

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

松本 孝太郎

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

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

この記事の更新履歴
  • 2025/7/30

    `src/components/AuthenticatedArea.tsx`を修正:ログイン状態でないときに、ログイン後の画面(/dashboard)へ直接アクセスした際、エラーメッセージではなくログイン画面へ遷移させるように修正

  • 2025/7/29

    初回公開