diff --git a/backend/app/main.py b/backend/app/main.py index c126ca5..e010032 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,4 +1,5 @@ from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware from .routes import chat, upload, files, auth, history from .database import connect_to_mongo, close_mongo_connection @@ -23,6 +24,15 @@ allow_headers=["*"], ) +# Mount static files +storage_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "storage")) +if not os.path.exists(storage_dir): + try: + os.makedirs(storage_dir) + except Exception: + pass +app.mount("/static", StaticFiles(directory=storage_dir), name="static") + # Event Handlers app.add_event_handler("startup", connect_to_mongo) app.add_event_handler("shutdown", close_mongo_connection) diff --git a/backend/app/routes/auth.py b/backend/app/routes/auth.py index 0a17f8a..67e1b04 100644 --- a/backend/app/routes/auth.py +++ b/backend/app/routes/auth.py @@ -1,10 +1,13 @@ -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File from fastapi.security import OAuth2PasswordRequestForm from datetime import timedelta from ..auth import create_access_token, ACCESS_TOKEN_EXPIRE_MINUTES, get_current_user from ..users import User from pydantic import BaseModel, EmailStr -from typing import Dict, Any +from typing import Dict, Any, Optional +import os +import shutil +import uuid router = APIRouter() @@ -13,6 +16,10 @@ class UserCreate(BaseModel): password: str full_name: str = None +class UserUpdate(BaseModel): + full_name: Optional[str] = None + username: Optional[str] = None + class Token(BaseModel): access_token: str token_type: str @@ -28,7 +35,7 @@ async def register(user: UserCreate): print(f"Registration error: {e}") raise HTTPException(status_code=500, detail=str(e)) -@router.post("/login", response_model=Token) +@router.post("/login", response_model=Dict[str, Any]) async def login(form_data: OAuth2PasswordRequestForm = Depends()): user = await User.get_by_email(form_data.username) if not user or not User.verify_password(form_data.password, user["hashed_password"]): @@ -42,7 +49,17 @@ async def login(form_data: OAuth2PasswordRequestForm = Depends()): access_token = create_access_token( data={"sub": user["email"]}, expires_delta=access_token_expires ) - return {"access_token": access_token, "token_type": "bearer"} + + # Prepare user data + user_data = {k: v for k, v in user.items() if k != "hashed_password"} + if "_id" in user_data: + user_data["_id"] = str(user_data["_id"]) + + return { + "access_token": access_token, + "token_type": "bearer", + "user": user_data + } @router.get("/me") async def read_users_me(current_user = Depends(get_current_user)): @@ -50,3 +67,63 @@ async def read_users_me(current_user = Depends(get_current_user)): if "_id" in current_user: current_user["_id"] = str(current_user["_id"]) return current_user + +@router.put("/profile") +async def update_profile(user_update: UserUpdate, current_user = Depends(get_current_user)): + try: + email = current_user["email"] + update_data = {k: v for k, v in user_update.dict().items() if v is not None} + + if not update_data: + return current_user + + success = await User.update_user(email, update_data) + if not success: + raise HTTPException(status_code=400, detail="Failed to update profile") + + # Refetch updated user + updated_user = await User.get_by_email(email) + if "_id" in updated_user: + updated_user["_id"] = str(updated_user["_id"]) + + # Remove password + if "hashed_password" in updated_user: + del updated_user["hashed_password"] + + return updated_user + except Exception as e: + print(f"Profile update error: {e}") + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/profile/avatar") +async def upload_avatar(file: UploadFile = File(...), current_user = Depends(get_current_user)): + try: + email = current_user["email"] + + # Ensure uploads directory exists + current_dir = os.path.dirname(os.path.abspath(__file__)) + storage_dir = os.path.abspath(os.path.join(current_dir, "..", "..", "storage", "avatars")) + os.makedirs(storage_dir, exist_ok=True) + + # Generate unique filename + file_extension = os.path.splitext(file.filename)[1] + filename = f"{uuid.uuid4()}{file_extension}" + file_path = os.path.join(storage_dir, filename) + + # Save file + with open(file_path, "wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + # Update user avatar URL + # URL should be relative path served by static + avatar_url = f"/static/avatars/{filename}" + + success = await User.update_user(email, {"avatar_url": avatar_url}) + if not success: + raise HTTPException(status_code=400, detail="Failed to update user avatar") + + return {"avatar_url": avatar_url} + + except Exception as e: + print(f"Avatar upload error: {e}") + raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/app/users.py b/backend/app/users.py index 67d08b6..c9f6abf 100644 --- a/backend/app/users.py +++ b/backend/app/users.py @@ -38,3 +38,13 @@ async def create_user(email: str, password: str, full_name: str = None): result = await db.db.users.insert_one(user_doc) user_doc["_id"] = result.inserted_id return user_doc + + @staticmethod + async def update_user(email: str, update_data: dict): + if db.db is None: raise Exception("Database not connected") + + result = await db.db.users.update_one( + {"email": email}, + {"$set": update_data} + ) + return result.modified_count > 0 diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 8978251..700da71 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -7,7 +7,9 @@ import { Auth } from "./components/Auth"; import LandingPage from "./components/LandingPage"; import { Sheet, SheetContent, SheetTrigger } from "./components/ui/sheet"; import { DialogTitle, DialogDescription } from "./components/ui/dialog"; -import { PanelLeft, X, Database, LogOut, SquarePen } from "lucide-react"; +import UserMenu from "./components/UserMenu"; +import ProfileModal from "./components/ProfileModal"; +import { PanelLeft, X, Database, LogOut, SquarePen, Sun, Moon } from "lucide-react"; function App() { const [token, setToken] = useState(localStorage.getItem("token") || null); @@ -28,6 +30,7 @@ function App() { const [showSidebar, setShowSidebar] = useState(true); const [indexedDocs, setIndexedDocs] = useState(0); const [isDarkMode, setIsDarkMode] = useState(true); + const [isProfileOpen, setIsProfileOpen] = useState(false); // Real Chat History @@ -55,15 +58,48 @@ function App() { useEffect(() => { if (token) { fetchHistory(); + + // Refresh user data if missing or incomplete + if (!user || !user.email) { + fetch(`${API_BASE_URL}/api/auth/me`, { + headers: { 'Authorization': `Bearer ${token}` } + }) + .then(res => res.ok ? res.json() : null) + .then(u => { + if (u) { + setUser(u); + localStorage.setItem("user", JSON.stringify(u)); + } + }) + .catch(err => console.error("Failed to refresh user", err)); + } } }, [token]); - const handleLogin = (data) => { - setToken(data.access_token); - setUser(data.user); - localStorage.setItem("token", data.access_token); - if (data.user) { - localStorage.setItem("user", JSON.stringify(data.user)); + const handleLogin = async (data) => { + let userData = data.user; + const accessToken = data.access_token; + + setToken(accessToken); + localStorage.setItem("token", accessToken); + + if (!userData) { + // Fallback: fetch user me + try { + const res = await fetch(`${API_BASE_URL}/api/auth/me`, { + headers: { 'Authorization': `Bearer ${accessToken}` } + }); + if (res.ok) { + userData = await res.json(); + } + } catch (e) { + console.error("Failed to fetch user info", e); + } + } + + setUser(userData); + if (userData) { + localStorage.setItem("user", JSON.stringify(userData)); } else { localStorage.removeItem("user"); } @@ -79,6 +115,28 @@ function App() { setShowLanding(true); }; + const handleUpdateProfile = async (updatedData) => { + // Optimistic update + const newUser = { ...user, ...updatedData }; + setUser(newUser); + localStorage.setItem("user", JSON.stringify(newUser)); + + // Attempt backend update if endpoint exists (mocked for now as we don't have PUT /api/auth/me confirmed) + // If backend supports it: + /* + try { + await fetch(`${API_BASE_URL}/api/auth/me`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(updatedData) + }); + } catch(e) { console.error(e); } + */ + }; + const fetchHistory = async () => { try { const res = await fetch(`${API_BASE_URL}/api/history`, { @@ -290,18 +348,8 @@ function App() { {/* User Area */} -
{user?.full_name || "User"}
+{user?.email}
+