Skip to content

Commit a82d3cb

Browse files
authored
Merge pull request #10 from nomhiro/feature-nextauth
feat: Implement authentication with NextAuth, including email restric…
2 parents e0c47b0 + fdde639 commit a82d3cb

24 files changed

Lines changed: 483 additions & 154 deletions

File tree

frontend/auth.config.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { NextAuthConfig } from "next-auth";
2+
import Google from "next-auth/providers/google";
3+
4+
const allowedEmails = process.env.ALLOWED_EMAILS?.split(",") || []; // 環境変数から許可されるメールアドレスを取得
5+
6+
export const authConfig: NextAuthConfig = {
7+
providers: [Google],
8+
callbacks: {
9+
async signIn({ user }) {
10+
if (allowedEmails.includes(user.email || "")) {
11+
console.log("許可されたメールアドレス:", user.email);
12+
return true; // 許可されたメールアドレスの場合ログインを許可
13+
}
14+
console.log("許可されていないメールアドレス:", user.email);
15+
return false; // 許可されていない場合ログインを拒否
16+
},
17+
async jwt({ token, user, account }) {
18+
if (user && account?.id_token) {
19+
token.idToken = account?.id_token;
20+
}
21+
return token;
22+
},
23+
async session({ token, session }) {
24+
session.idToken = token.idToken;
25+
return session;
26+
},
27+
},
28+
};

frontend/auth.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import NextAuth from "next-auth";
2+
import { authConfig } from "./auth.config";
3+
4+
export const { handlers, auth } = NextAuth(authConfig);

frontend/middleware.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { auth as middleware } from "./auth"

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"axios": "^1.7.2",
2121
"framer-motion": "^11.1.9",
2222
"next": "^14.2.23",
23+
"next-auth": "^5.0.0-beta.27",
2324
"openai": "^4.77.0",
2425
"postcss": "8.4.38",
2526
"react": "18.3.1",
Lines changed: 143 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
"use client";
22

33
import { useState, useEffect, Suspense } from "react";
4-
import { FaTrash } from "react-icons/fa";
4+
import { FaTrash, FaEdit, FaPlus } from "react-icons/fa"; // FaEdit, FaPlusを追加
55
import { CategoryItem, CosmosItem } from "@/models/models";
66
import CategoryDropdown from "../../../components/documents/CategoryDropdown";
77
import DocumentForm from "../../../components/documents/DocumentForm";
88
import DocumentList from "../../../components/documents/DocumentList";
99
import CategoryModal from "@/components/documents/CategoryModal";
10+
import Head from "next/head"; // Headを追加
1011

1112
export default function DocumentsPage() {
1213
const [selectedCategory, setSelectedCategory] = useState("");
@@ -113,109 +114,149 @@ export default function DocumentsPage() {
113114
};
114115

115116
return (
116-
<main className="flex flex-col text-gray-800 w-full h-full overflow-y-auto">
117-
<h1 className="text-xl font-bold text-center">ドキュメント管理</h1>
118-
<div className="flex justify-center items-center p-4 space-x-2">
119-
<Suspense fallback={<p>カテゴリを取得中...</p>}>
120-
<CategoryDropdown
121-
categories={categories}
122-
onCategoryChange={handleCategoryChange}
123-
onAddCategory={() => {
124-
setEditingCategory(null);
125-
setIsCategoryModalOpen(true);
126-
}}
127-
value={selectedCategory} // ドロップダウンの選択状態をバインド
128-
/>
129-
</Suspense>
117+
<>
118+
<Head>
119+
<title>ドキュメント管理</title> {/* タイトルを設定 */}
120+
</Head>
121+
<main className="flex flex-col text-gray-800 w-full h-full overflow-y-auto">
122+
<h1 className="text-xl font-bold text-center">ドキュメント管理</h1>
123+
<div className="flex justify-center items-center p-4 space-x-2">
124+
<Suspense fallback={<p>カテゴリを取得中...</p>}>
125+
<CategoryDropdown
126+
categories={categories}
127+
onCategoryChange={handleCategoryChange}
128+
onAddCategory={() => {
129+
setEditingCategory(null);
130+
setIsCategoryModalOpen(true);
131+
}}
132+
value={selectedCategory} // ドロップダウンの選択状態をバインド
133+
/>
134+
</Suspense>
135+
{selectedCategory && (
136+
<div className="flex items-center space-x-2">
137+
<button
138+
className="bg-yellow-500 text-white p-2 rounded-full"
139+
onClick={() => {
140+
const categoryToEdit = categories.find((cat) => cat.id === selectedCategory);
141+
setEditingCategory(categoryToEdit || null);
142+
setIsCategoryModalOpen(true);
143+
}}
144+
>
145+
<FaEdit />
146+
</button>
147+
<button
148+
className="bg-blue-500 text-white p-2 rounded-full"
149+
onClick={() => {
150+
setEditingCategory(null);
151+
setIsCategoryModalOpen(true);
152+
}}
153+
>
154+
<FaPlus />
155+
</button>
156+
<button
157+
className="bg-red-500 text-white p-2 rounded-full"
158+
onClick={() => handleDeleteCategory(selectedCategory)}
159+
>
160+
<FaTrash />
161+
</button>
162+
</div>
163+
)}
164+
</div>
130165
{selectedCategory && (
131-
<button
132-
className="bg-red-500 text-white p-2 rounded-full"
133-
onClick={() => handleDeleteCategory(selectedCategory)}
134-
>
135-
<FaTrash />
136-
</button>
137-
)}
138-
</div>
139-
{selectedCategory && (
140-
<div className="flex flex-col md:flex-row flex-1">
141-
<DocumentForm
142-
text={text}
143-
setText={setText}
144-
isSubmitting={isSubmitting}
145-
onSubmit={async () => {
146-
if (!text.trim()) return alert("テキストを入力してください。");
147-
if (!selectedCategory) return alert("カテゴリを選択してください。");
148-
149-
setIsSubmitting(true);
150-
try {
151-
const method = editingDocument ? "PATCH" : "POST";
152-
const url = `/api/document`;
153-
154-
const payload = {
155-
category_id: selectedCategory,
156-
content: text,
157-
};
158-
159-
const response = await fetch(url, {
160-
method,
161-
headers: { "Content-Type": "application/json" },
162-
body: JSON.stringify(payload),
163-
});
164-
165-
if (response.ok) {
166-
alert(editingDocument ? "更新が成功しました!" : "登録が成功しました!");
167-
setText("");
168-
setEditingDocument(null);
169-
fetchDocuments(selectedCategory);
170-
} else {
171-
alert(editingDocument ? "更新に失敗しました。" : "登録に失敗しました。");
166+
<div className="flex flex-col md:flex-row flex-1">
167+
<DocumentForm
168+
text={text}
169+
setText={setText}
170+
isSubmitting={isSubmitting}
171+
onSubmit={async () => {
172+
if (!text.trim()) {
173+
alert("テキストを入力してください。");
174+
return;
172175
}
173-
} catch (error) {
174-
console.error("エラー:", error);
175-
alert("エラーが発生しました。");
176-
} finally {
177-
setIsSubmitting(false);
178-
}
179-
}}
180-
editingDocument={editingDocument}
181-
/>
182-
<DocumentList
183-
documents={documents}
184-
onEdit={(doc) => {
185-
setEditingDocument(doc);
186-
setText(doc.content);
187-
}}
188-
onDelete={async (docId) => {
189-
if (!confirm("本当にこのドキュメントを削除しますか?")) return;
190-
191-
try {
192-
const response = await fetch(`/api/document`, {
193-
method: "DELETE",
194-
headers: { "Content-Type": "application/json" },
195-
body: JSON.stringify({ id: docId }),
196-
});
197-
198-
if (response.ok) {
199-
alert("削除が成功しました!");
200-
fetchDocuments(selectedCategory);
201-
} else {
202-
const errorData = await response.json();
203-
alert(`削除に失敗しました: ${errorData.message}`);
176+
if (!selectedCategory) {
177+
alert("カテゴリを選択してください。");
178+
return;
204179
}
205-
} catch (error) {
206-
console.error("エラー:", error);
207-
alert("エラーが発生しました。");
208-
}
209-
}}
210-
/>
211-
</div>
212-
)}
213-
<CategoryModal
214-
isOpen={isCategoryModalOpen}
215-
onClose={() => setIsCategoryModalOpen(false)}
216-
onSubmit={handleCategorySubmit}
217-
initialCategory={editingCategory?.category}
218-
/>
219-
</main>
180+
181+
setIsSubmitting(true);
182+
try {
183+
const method = editingDocument ? "PATCH" : "POST";
184+
const url = `/api/document`;
185+
186+
const payload = {
187+
id: editingDocument?.id, // 更新時はIDを含める
188+
category_id: selectedCategory,
189+
content: text,
190+
};
191+
192+
const response = await fetch(url, {
193+
method,
194+
headers: { "Content-Type": "application/json" },
195+
body: JSON.stringify(payload),
196+
});
197+
198+
if (response.ok) {
199+
const message = editingDocument ? "更新が成功しました!" : "登録が成功しました!";
200+
alert(message);
201+
setText("");
202+
setEditingDocument(null);
203+
fetchDocuments(selectedCategory);
204+
} else {
205+
const errorData = await response.json();
206+
const message = editingDocument ? "更新に失敗しました。" : "登録に失敗しました。";
207+
alert(`${message} エラー: ${errorData.message}`);
208+
}
209+
} catch (error) {
210+
console.error("エラー:", error);
211+
alert("エラーが発生しました。");
212+
} finally {
213+
setIsSubmitting(false);
214+
}
215+
}}
216+
editingDocument={editingDocument}
217+
/>
218+
<DocumentList
219+
documents={documents}
220+
onEdit={(doc) => {
221+
setEditingDocument(doc);
222+
setText(doc.content);
223+
}}
224+
onDelete={async (docId) => {
225+
if (!confirm("本当にこのドキュメントを削除しますか?")) return;
226+
227+
try {
228+
const response = await fetch(`/api/document`, {
229+
method: "DELETE",
230+
headers: { "Content-Type": "application/json" },
231+
body: JSON.stringify({ id: docId }),
232+
});
233+
234+
if (response.ok) {
235+
alert("削除が成功しました!");
236+
fetchDocuments(selectedCategory);
237+
} else {
238+
const errorData = await response.json();
239+
alert(`削除に失敗しました: ${errorData.message}`);
240+
}
241+
} catch (error) {
242+
console.error("エラー:", error);
243+
alert("エラーが発生しました。");
244+
}
245+
}}
246+
onAdd={() => {
247+
setEditingDocument(null); // 新規登録モードに切り替え
248+
setText(""); // フォームをクリア
249+
}}
250+
/>
251+
</div>
252+
)}
253+
<CategoryModal
254+
isOpen={isCategoryModalOpen}
255+
onClose={() => setIsCategoryModalOpen(false)}
256+
onSubmit={handleCategorySubmit}
257+
initialCategory={editingCategory?.category}
258+
/>
259+
</main>
260+
</>
220261
);
221262
}

frontend/src/app/(main)/layout.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ const MainLayout = ({ children }: { children: React.ReactNode }) => {
99
return (
1010
<div className='flex flex-col h-screen'>
1111
<Provider store={store}>
12-
{/* Adjust Header to avoid duplication with page-specific titles */}
1312
<Header />
1413
<main className='bg-slate-50 flex-1 overflow-auto p-2 sm:p-1 md:p-2'>
1514
{children}

frontend/src/app/(main)/page.tsx

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
1+
"use client";
2+
3+
import { useSession } from "next-auth/react";
14
import FormInput from "@/components/FormInput/FormInput";
25
import MessageArea from "@/components/MessageArea/MessageArea";
6+
import Head from "next/head"; // Headを追加
37

48
export default function Home() {
9+
const { data: session } = useSession();
10+
11+
console.log(session?.idToken); // ID トークンを sessionに格納できている
12+
console.log(session?.user?.email); // auth() と同様に取得できる
13+
514
return (
6-
<main className="flex flex-col text-gray-800 w-full h-full overflow-y-auto">
7-
<div className="flex bg-slate-300 h-5/6 justify-center">
8-
<MessageArea />
9-
</div>
10-
<div className="flex h-1/6 justify-center items-center">
11-
<FormInput />
12-
</div>
13-
</main>
15+
<>
16+
<Head>
17+
<title>AIチャット</title> {/* タイトルを設定 */}
18+
</Head>
19+
<main className="flex flex-col text-gray-800 w-full h-full overflow-y-auto">
20+
<div className="flex bg-slate-300 h-5/6 justify-center">
21+
<MessageArea />
22+
</div>
23+
<div className="flex h-1/6 justify-center items-center">
24+
<FormInput />
25+
</div>
26+
</main>
27+
</>
1428
);
1529
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { handlers } from "../../../../../auth";
2+
3+
export const { GET, POST } = handlers;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { NextResponse } from "next/server";
2+
3+
export async function GET() {
4+
const allowedEmails = process.env.ALLOWED_EMAILS?.split(",") || [];
5+
return NextResponse.json(allowedEmails);
6+
}

0 commit comments

Comments
 (0)