diff --git a/frontend/auth.config.ts b/frontend/auth.config.ts new file mode 100644 index 0000000..dc05b91 --- /dev/null +++ b/frontend/auth.config.ts @@ -0,0 +1,28 @@ +import type { NextAuthConfig } from "next-auth"; +import Google from "next-auth/providers/google"; + +const allowedEmails = process.env.ALLOWED_EMAILS?.split(",") || []; // 環境変数から許可されるメールアドレスを取得 + +export const authConfig: NextAuthConfig = { + providers: [Google], + callbacks: { + async signIn({ user }) { + if (allowedEmails.includes(user.email || "")) { + console.log("許可されたメールアドレス:", user.email); + return true; // 許可されたメールアドレスの場合ログインを許可 + } + console.log("許可されていないメールアドレス:", user.email); + return false; // 許可されていない場合ログインを拒否 + }, + async jwt({ token, user, account }) { + if (user && account?.id_token) { + token.idToken = account?.id_token; + } + return token; + }, + async session({ token, session }) { + session.idToken = token.idToken; + return session; + }, + }, +}; \ No newline at end of file diff --git a/frontend/auth.ts b/frontend/auth.ts new file mode 100644 index 0000000..8cdb3bd --- /dev/null +++ b/frontend/auth.ts @@ -0,0 +1,4 @@ +import NextAuth from "next-auth"; +import { authConfig } from "./auth.config"; + +export const { handlers, auth } = NextAuth(authConfig); \ No newline at end of file diff --git a/frontend/middleware.ts b/frontend/middleware.ts new file mode 100644 index 0000000..7080c5b --- /dev/null +++ b/frontend/middleware.ts @@ -0,0 +1 @@ +export { auth as middleware } from "./auth" \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 2616303..c0fb291 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "axios": "^1.7.2", "framer-motion": "^11.1.9", "next": "^14.2.23", + "next-auth": "^5.0.0-beta.27", "openai": "^4.77.0", "postcss": "8.4.38", "react": "18.3.1", diff --git a/frontend/src/app/(main)/documents/page.tsx b/frontend/src/app/(main)/documents/page.tsx index d35dac8..6f8c914 100644 --- a/frontend/src/app/(main)/documents/page.tsx +++ b/frontend/src/app/(main)/documents/page.tsx @@ -1,12 +1,13 @@ "use client"; import { useState, useEffect, Suspense } from "react"; -import { FaTrash } from "react-icons/fa"; +import { FaTrash, FaEdit, FaPlus } from "react-icons/fa"; // FaEdit, FaPlusを追加 import { CategoryItem, CosmosItem } from "@/models/models"; import CategoryDropdown from "../../../components/documents/CategoryDropdown"; import DocumentForm from "../../../components/documents/DocumentForm"; import DocumentList from "../../../components/documents/DocumentList"; import CategoryModal from "@/components/documents/CategoryModal"; +import Head from "next/head"; // Headを追加 export default function DocumentsPage() { const [selectedCategory, setSelectedCategory] = useState(""); @@ -113,109 +114,149 @@ export default function DocumentsPage() { }; return ( -
-

ドキュメント管理

-
- カテゴリを取得中...

}> - { - setEditingCategory(null); - setIsCategoryModalOpen(true); - }} - value={selectedCategory} // ドロップダウンの選択状態をバインド - /> -
+ <> + + ドキュメント管理 {/* タイトルを設定 */} + +
+

ドキュメント管理

+
+ カテゴリを取得中...

}> + { + setEditingCategory(null); + setIsCategoryModalOpen(true); + }} + value={selectedCategory} // ドロップダウンの選択状態をバインド + /> +
+ {selectedCategory && ( +
+ + + +
+ )} +
{selectedCategory && ( - - )} -
- {selectedCategory && ( -
- { - if (!text.trim()) return alert("テキストを入力してください。"); - if (!selectedCategory) return alert("カテゴリを選択してください。"); - - setIsSubmitting(true); - try { - const method = editingDocument ? "PATCH" : "POST"; - const url = `/api/document`; - - const payload = { - category_id: selectedCategory, - content: text, - }; - - const response = await fetch(url, { - method, - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); - - if (response.ok) { - alert(editingDocument ? "更新が成功しました!" : "登録が成功しました!"); - setText(""); - setEditingDocument(null); - fetchDocuments(selectedCategory); - } else { - alert(editingDocument ? "更新に失敗しました。" : "登録に失敗しました。"); +
+ { + if (!text.trim()) { + alert("テキストを入力してください。"); + return; } - } catch (error) { - console.error("エラー:", error); - alert("エラーが発生しました。"); - } finally { - setIsSubmitting(false); - } - }} - editingDocument={editingDocument} - /> - { - setEditingDocument(doc); - setText(doc.content); - }} - onDelete={async (docId) => { - if (!confirm("本当にこのドキュメントを削除しますか?")) return; - - try { - const response = await fetch(`/api/document`, { - method: "DELETE", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ id: docId }), - }); - - if (response.ok) { - alert("削除が成功しました!"); - fetchDocuments(selectedCategory); - } else { - const errorData = await response.json(); - alert(`削除に失敗しました: ${errorData.message}`); + if (!selectedCategory) { + alert("カテゴリを選択してください。"); + return; } - } catch (error) { - console.error("エラー:", error); - alert("エラーが発生しました。"); - } - }} - /> -
- )} - setIsCategoryModalOpen(false)} - onSubmit={handleCategorySubmit} - initialCategory={editingCategory?.category} - /> -
+ + setIsSubmitting(true); + try { + const method = editingDocument ? "PATCH" : "POST"; + const url = `/api/document`; + + const payload = { + id: editingDocument?.id, // 更新時はIDを含める + category_id: selectedCategory, + content: text, + }; + + const response = await fetch(url, { + method, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + if (response.ok) { + const message = editingDocument ? "更新が成功しました!" : "登録が成功しました!"; + alert(message); + setText(""); + setEditingDocument(null); + fetchDocuments(selectedCategory); + } else { + const errorData = await response.json(); + const message = editingDocument ? "更新に失敗しました。" : "登録に失敗しました。"; + alert(`${message} エラー: ${errorData.message}`); + } + } catch (error) { + console.error("エラー:", error); + alert("エラーが発生しました。"); + } finally { + setIsSubmitting(false); + } + }} + editingDocument={editingDocument} + /> + { + setEditingDocument(doc); + setText(doc.content); + }} + onDelete={async (docId) => { + if (!confirm("本当にこのドキュメントを削除しますか?")) return; + + try { + const response = await fetch(`/api/document`, { + method: "DELETE", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ id: docId }), + }); + + if (response.ok) { + alert("削除が成功しました!"); + fetchDocuments(selectedCategory); + } else { + const errorData = await response.json(); + alert(`削除に失敗しました: ${errorData.message}`); + } + } catch (error) { + console.error("エラー:", error); + alert("エラーが発生しました。"); + } + }} + onAdd={() => { + setEditingDocument(null); // 新規登録モードに切り替え + setText(""); // フォームをクリア + }} + /> + + )} + setIsCategoryModalOpen(false)} + onSubmit={handleCategorySubmit} + initialCategory={editingCategory?.category} + /> + + ); } diff --git a/frontend/src/app/(main)/layout.tsx b/frontend/src/app/(main)/layout.tsx index 5647495..73a9ed9 100644 --- a/frontend/src/app/(main)/layout.tsx +++ b/frontend/src/app/(main)/layout.tsx @@ -9,7 +9,6 @@ const MainLayout = ({ children }: { children: React.ReactNode }) => { return (
- {/* Adjust Header to avoid duplication with page-specific titles */}
{children} diff --git a/frontend/src/app/(main)/page.tsx b/frontend/src/app/(main)/page.tsx index 51934e3..20e23b8 100644 --- a/frontend/src/app/(main)/page.tsx +++ b/frontend/src/app/(main)/page.tsx @@ -1,15 +1,29 @@ +"use client"; + +import { useSession } from "next-auth/react"; import FormInput from "@/components/FormInput/FormInput"; import MessageArea from "@/components/MessageArea/MessageArea"; +import Head from "next/head"; // Headを追加 export default function Home() { + const { data: session } = useSession(); + + console.log(session?.idToken); // ID トークンを sessionに格納できている + console.log(session?.user?.email); // auth() と同様に取得できる + return ( -
-
- -
-
- -
-
+ <> + + AIチャット {/* タイトルを設定 */} + +
+
+ +
+
+ +
+
+ ); } diff --git a/frontend/src/app/api/auth/[...nextauth]/route.ts b/frontend/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..05759bf --- /dev/null +++ b/frontend/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,3 @@ +import { handlers } from "../../../../../auth"; + +export const { GET, POST } = handlers; \ No newline at end of file diff --git a/frontend/src/app/api/auth/allowed-emails/route.ts b/frontend/src/app/api/auth/allowed-emails/route.ts new file mode 100644 index 0000000..7fa612b --- /dev/null +++ b/frontend/src/app/api/auth/allowed-emails/route.ts @@ -0,0 +1,6 @@ +import { NextResponse } from "next/server"; + +export async function GET() { + const allowedEmails = process.env.ALLOWED_EMAILS?.split(",") || []; + return NextResponse.json(allowedEmails); +} diff --git a/frontend/src/app/api/document/route.ts b/frontend/src/app/api/document/route.ts index 419853a..c3e1a40 100644 --- a/frontend/src/app/api/document/route.ts +++ b/frontend/src/app/api/document/route.ts @@ -1,8 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; -import { registerItem } from "../../../util/cosmos/document"; +import { registerItem, deleteItem, updateItem } from "../../../util/cosmos/document"; // 修正: updateItemをインポート import { getEmbedding, getChatCompletions } from "../../../util/openai"; import { v4 as uuidv4 } from "uuid"; -import { deleteItem } from "../../../util/cosmos/document"; // 修正: deleteItemをインポート import { getCategoryById } from "../../../util/cosmos/category" /** @@ -86,3 +85,59 @@ export const DELETE = async (req: NextRequest) => { } }; +/** + * PATCHメソッドでドキュメントを更新するAPIエンドポイント + * @param req - 更新するドキュメントのID、新しい内容、カテゴリIDを含むリクエストオブジェクト + * @returns - 更新結果を含むレスポンスオブジェクト + */ +export const PATCH = async (req: NextRequest) => { + try { + const { id, content, category_id } = await req.json(); + + // 入力検証 + if (!id || typeof id !== "string") { + return NextResponse.json({ message: "Invalid or missing 'id'" }, { status: 400 }); + } + if (!content || typeof content !== "string") { + return NextResponse.json({ message: "Invalid or missing 'content'" }, { status: 400 }); + } + if (!category_id || typeof category_id !== "string") { + return NextResponse.json({ message: "Invalid or missing 'category_id'" }, { status: 400 }); + } + + console.log("🚀Updating document with ID:", id, "new content:", content, "and category ID:", category_id); + + // カテゴリIDからカテゴリ情報を取得 + const category = await getCategoryById(category_id); + if (!category) { + return NextResponse.json({ message: "Category not found" }, { status: 404 }); + } + + // OpenAIのAPIを使用して新しいコンテンツのベクトルを取得 + const contentVector = await getEmbedding(`${category.category} \n${content}`); + + // OpenAIのAPIを使用してドキュメントタイトルを取得 + const systemMessage = `あなたは優秀なライターです。与えられた内容からタイトルを生成してください。\n\n- タイトルのみを出力してください。「」など不要です。`; + const titleResponse = await getChatCompletions(systemMessage, content); + + // ドキュメントを更新 + const updatedItem = { + content, + content_vector: contentVector, + category_id, + file_name: titleResponse[0].message.content, // タイトルフィールドとする + }; + + await updateItem(id, updatedItem); + + return NextResponse.json({ message: "Document updated successfully" }, { status: 200 }); + } catch (error: any) { + console.error("エラー詳細:", { + message: error.message, + stack: error.stack, + additionalInfo: error.response || "追加情報なし", + }); + return NextResponse.json({ message: "Internal server error", details: error.message }, { status: 500 }); + } +}; + diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 183d715..40aab41 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,22 +1,62 @@ -import type { Metadata } from "next"; +"use client"; + import { Inter } from "next/font/google"; import "./globals.css"; +import { SessionProvider, useSession, signIn } from "next-auth/react"; +import { useEffect, useState } from "react"; const inter = Inter({ subsets: ["latin"] }); -export const metadata: Metadata = { - title: "AIアシスタント", - description: "AIアシスタント", -}; - export default function RootLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + useEffect(() => { + const checkSession = async () => { + const session = await fetch("/api/auth/session").then((res) => res.json()); + if (!session?.user) { + signIn(); + } + }; + checkSession(); + }, []); + return ( - {children} + + + {children} {/* 認可ガードを適用 */} + + ); } + +function AuthGuard({ children }: { children: React.ReactNode }) { + const { data: session, status } = useSession(); + const [allowedEmails, setAllowedEmails] = useState([]); + + useEffect(() => { + const fetchAllowedEmails = async () => { + const response = await fetch("/api/auth/allowed-emails"); + const emails = await response.json(); + setAllowedEmails(emails); + }; + fetchAllowedEmails(); + }, []); + + if (status === "loading" || allowedEmails.length === 0) { + return
読み込み中...
; // ローディング状態 + } + + if (!session?.user) { + return
ログインが必要です。
; // 未ログインの場合 + } + + if (!allowedEmails.includes(session.user.email || "")) { + return
認可されていないアカウントです。
; // 許可されていない場合 + } + + return <>{children}; // 許可された場合のみ子要素を表示 +} diff --git a/frontend/src/components/AuthButton.tsx b/frontend/src/components/AuthButton.tsx new file mode 100644 index 0000000..fe45ff4 --- /dev/null +++ b/frontend/src/components/AuthButton.tsx @@ -0,0 +1,37 @@ +import { signIn, signOut } from "next-auth/react"; + +interface AuthButtonProps { + onClick: () => void; + children: React.ReactNode; + variant: "default" | "outline"; +} + +const AuthButton = ({ onClick, children, variant }: AuthButtonProps) => { + const baseClasses = "px-2 py-1 rounded text-xs font-medium focus:outline-none focus:ring-2"; // サイズを小さく変更 + const variantClasses = + variant === "default" + ? "bg-white text-gray-800 border border-gray-300 hover:bg-gray-100 focus:ring-gray-200" + : "bg-white text-gray-800 border border-gray-300 hover:bg-gray-100 focus:ring-gray-200"; // サインアウトボタンも白に変更 + + return ( + + ); +}; + +export const LogInButton = () => { + return ( + signIn()} variant={"default"}> + Log In + + ); +}; + +export const LogOutButton = () => { + return ( + signOut()} variant={"outline"}> + Log Out + + ); +}; \ No newline at end of file diff --git a/frontend/src/components/FormInput/FormInput.tsx b/frontend/src/components/FormInput/FormInput.tsx index 20bf1ef..6e04219 100644 --- a/frontend/src/components/FormInput/FormInput.tsx +++ b/frontend/src/components/FormInput/FormInput.tsx @@ -13,6 +13,10 @@ const FormInput = () => { const [isLoading, setIsLoading] = useState(false) const pathname = usePathname() + const clearChat = () => { + dispatch(inputMessageToReduxStore({ pathname, clear: true })) + } + const sendMessage = async () => { setIsLoading(true) dispatch(inputMessageToReduxStore({ @@ -54,6 +58,20 @@ const FormInput = () => { Your Message
+