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
)
}
-
Send Message
diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx
index c7e96e1..fc01edb 100644
--- a/frontend/src/components/Header/Header.tsx
+++ b/frontend/src/components/Header/Header.tsx
@@ -2,22 +2,30 @@ import React from 'react';
import { IoHome } from 'react-icons/io5';
import { MdFolder } from 'react-icons/md';
import Link from 'next/link';
+import { useSession } from "next-auth/react"; // 追加
+import { LogInButton, LogOutButton } from "@/components/AuthButton";
const Header = () => {
+ const { data: session } = useSession(); // セッション情報を取得
+
return (
);
};
diff --git a/frontend/src/components/documents/CategoryDropdown.tsx b/frontend/src/components/documents/CategoryDropdown.tsx
index c8df138..9dfaa2f 100644
--- a/frontend/src/components/documents/CategoryDropdown.tsx
+++ b/frontend/src/components/documents/CategoryDropdown.tsx
@@ -30,13 +30,6 @@ export default function CategoryDropdown({
))}
-
);
}
diff --git a/frontend/src/components/documents/CategoryModal.tsx b/frontend/src/components/documents/CategoryModal.tsx
index d5ef74a..6f65676 100644
--- a/frontend/src/components/documents/CategoryModal.tsx
+++ b/frontend/src/components/documents/CategoryModal.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from "react";
+import React, { useState, useEffect } from "react";
interface CategoryModalProps {
isOpen: boolean;
@@ -15,6 +15,12 @@ const CategoryModal: React.FC = ({
}) => {
const [categoryName, setCategoryName] = useState(initialCategory);
+ useEffect(() => {
+ if (isOpen) {
+ setCategoryName(initialCategory || "");
+ }
+ }, [isOpen, initialCategory]);
+
const handleSubmit = () => {
if (!categoryName.trim()) {
alert("カテゴリ名を入力してください。");
diff --git a/frontend/src/components/documents/DocumentForm.tsx b/frontend/src/components/documents/DocumentForm.tsx
index 1fb1745..3ac944c 100644
--- a/frontend/src/components/documents/DocumentForm.tsx
+++ b/frontend/src/components/documents/DocumentForm.tsx
@@ -13,6 +13,29 @@ export default function DocumentForm({
onSubmit: () => void;
editingDocument: CosmosItem | null;
}) {
+ const handleSubmit = async () => {
+ if (!text.trim()) {
+ alert("テキストを入力してください。");
+ return;
+ }
+
+ if (editingDocument) {
+ // ドキュメントを更新
+ await fetch(`/api/document`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ id: editingDocument.id,
+ category_id: editingDocument.category_id, // 必要なカテゴリIDを含める
+ content: text,
+ }),
+ });
+ } else {
+ // 新規ドキュメントを登録
+ onSubmit();
+ }
+ };
+
return (
@@ -28,7 +51,7 @@ export default function DocumentForm({