diff --git a/backend/.env b/backend/.env index 9bcf84f..de1b663 100644 --- a/backend/.env +++ b/backend/.env @@ -4,3 +4,16 @@ DB_USER=recipes_user DB_NAME=recipes_db DB_HOST=localhost DB_PORT=5432 + +# Email Configuration +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USER=dvirlabs@gmail.com +SMTP_PASSWORD=agaanrhbbazbdytv +SMTP_FROM=dvirlabs@gmail.com + +# Google OAuth +GOOGLE_CLIENT_ID=143092846986-hsi59m0on2c9rb5qrdoejfceieao2ioc.apps.googleusercontent.com +GOOGLE_CLIENT_SECRET=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 +GOOGLE_REDIRECT_URI=http://localhost:8001/auth/google/callback +FRONTEND_URL=http://localhost:5174 \ No newline at end of file diff --git a/backend/__pycache__/auth_utils.cpython-312.pyc b/backend/__pycache__/auth_utils.cpython-312.pyc new file mode 100644 index 0000000..681b898 Binary files /dev/null and b/backend/__pycache__/auth_utils.cpython-312.pyc differ diff --git a/backend/__pycache__/db_utils.cpython-312.pyc b/backend/__pycache__/db_utils.cpython-312.pyc index e7a1dfa..49e25cf 100644 Binary files a/backend/__pycache__/db_utils.cpython-312.pyc and b/backend/__pycache__/db_utils.cpython-312.pyc differ diff --git a/backend/__pycache__/email_utils.cpython-312.pyc b/backend/__pycache__/email_utils.cpython-312.pyc new file mode 100644 index 0000000..519a5fc Binary files /dev/null and b/backend/__pycache__/email_utils.cpython-312.pyc differ diff --git a/backend/__pycache__/grocery_db_utils.cpython-312.pyc b/backend/__pycache__/grocery_db_utils.cpython-312.pyc new file mode 100644 index 0000000..6a65603 Binary files /dev/null and b/backend/__pycache__/grocery_db_utils.cpython-312.pyc differ diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc index f3ea2d5..ac62b4a 100644 Binary files a/backend/__pycache__/main.cpython-312.pyc and b/backend/__pycache__/main.cpython-312.pyc differ diff --git a/backend/__pycache__/notification_db_utils.cpython-312.pyc b/backend/__pycache__/notification_db_utils.cpython-312.pyc new file mode 100644 index 0000000..2722871 Binary files /dev/null and b/backend/__pycache__/notification_db_utils.cpython-312.pyc differ diff --git a/backend/__pycache__/oauth_utils.cpython-312.pyc b/backend/__pycache__/oauth_utils.cpython-312.pyc new file mode 100644 index 0000000..6ba3367 Binary files /dev/null and b/backend/__pycache__/oauth_utils.cpython-312.pyc differ diff --git a/backend/__pycache__/user_db_utils.cpython-312.pyc b/backend/__pycache__/user_db_utils.cpython-312.pyc new file mode 100644 index 0000000..2b2f41f Binary files /dev/null and b/backend/__pycache__/user_db_utils.cpython-312.pyc differ diff --git a/backend/email_utils.py b/backend/email_utils.py new file mode 100644 index 0000000..e721713 --- /dev/null +++ b/backend/email_utils.py @@ -0,0 +1,106 @@ +import os +import random +import aiosmtplib +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart +from datetime import datetime, timedelta +from dotenv import load_dotenv + +load_dotenv() + +# In-memory storage for verification codes (in production, use Redis or database) +verification_codes = {} + +def generate_verification_code(): + """Generate a 6-digit verification code""" + return str(random.randint(100000, 999999)) + +async def send_verification_email(email: str, code: str, purpose: str = "password_change"): + """Send verification code via email""" + smtp_host = os.getenv("SMTP_HOST", "smtp.gmail.com") + smtp_port = int(os.getenv("SMTP_PORT", "587")) + smtp_user = os.getenv("SMTP_USER") + smtp_password = os.getenv("SMTP_PASSWORD") + smtp_from = os.getenv("SMTP_FROM", smtp_user) + + if not smtp_user or not smtp_password: + raise Exception("SMTP credentials not configured") + + # Create message + message = MIMEMultipart("alternative") + message["Subject"] = "קוד אימות - מתכונים שלי" + message["From"] = smtp_from + message["To"] = email + + # Email content + if purpose == "password_change": + text = f""" + שלום, + + קוד האימות שלך לשינוי סיסמה הוא: {code} + + הקוד תקף ל-10 דקות. + + אם לא ביקשת לשנות את הסיסמה, התעלם מהודעה זו. + + בברכה, + צוות מתכונים שלי + """ + + html = f""" + +
+קוד האימות שלך הוא:
+הקוד תקף ל-10 דקות.
++ אם לא ביקשת לשנות את הסיסמה, התעלם מהודעה זו. +
+ + + """ + + part1 = MIMEText(text, "plain") + part2 = MIMEText(html, "html") + message.attach(part1) + message.attach(part2) + + # Send email + await aiosmtplib.send( + message, + hostname=smtp_host, + port=smtp_port, + username=smtp_user, + password=smtp_password, + start_tls=True, + ) + +def store_verification_code(user_id: int, code: str): + """Store verification code with expiry""" + expiry = datetime.now() + timedelta(minutes=10) + verification_codes[user_id] = { + "code": code, + "expiry": expiry + } + +def verify_code(user_id: int, code: str) -> bool: + """Verify if code is correct and not expired""" + if user_id not in verification_codes: + return False + + stored = verification_codes[user_id] + + # Check if expired + if datetime.now() > stored["expiry"]: + del verification_codes[user_id] + return False + + # Check if code matches + if stored["code"] != code: + return False + + # Code is valid, remove it + del verification_codes[user_id] + return True diff --git a/backend/main.py b/backend/main.py index db939f9..1c6b64d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -2,8 +2,9 @@ import random from typing import List, Optional from datetime import timedelta -from fastapi import FastAPI, HTTPException, Query, Depends, Response +from fastapi import FastAPI, HTTPException, Query, Depends, Response, Request from fastapi.middleware.cors import CORSMiddleware +from starlette.middleware.sessions import SessionMiddleware from pydantic import BaseModel, EmailStr, field_validator import os @@ -53,6 +54,15 @@ from notification_db_utils import ( delete_notification, ) +from email_utils import ( + generate_verification_code, + send_verification_email, + store_verification_code, + verify_code, +) + +from oauth_utils import oauth + class RecipeBase(BaseModel): name: str @@ -118,6 +128,16 @@ class UserResponse(BaseModel): is_admin: bool = False +class RequestPasswordChangeCode(BaseModel): + pass # No fields needed, uses current user from token + + +class ChangePasswordRequest(BaseModel): + verification_code: str + current_password: str + new_password: str + + # Grocery List models class GroceryListCreate(BaseModel): name: str @@ -180,6 +200,13 @@ app = FastAPI( description="API פשוט לבחירת מתכונים לצהריים / ערב / כל ארוחה 😋", ) +# Add session middleware for OAuth (must be before other middleware) +app.add_middleware( + SessionMiddleware, + secret_key=os.getenv("SECRET_KEY", "your-secret-key-change-in-production-min-32-chars"), + max_age=3600, # 1 hour +) + # Allow CORS from frontend domains allowed_origins = [ "http://localhost:5173", @@ -458,6 +485,159 @@ def get_me(current_user: dict = Depends(get_current_user)): ) +@app.post("/auth/request-password-change-code") +async def request_password_change_code( + current_user: dict = Depends(get_current_user) +): + """Send verification code to user's email for password change""" + from user_db_utils import get_user_by_id + + # Get user from database + user = get_user_by_id(current_user["user_id"]) + if not user: + raise HTTPException(status_code=404, detail="משתמש לא נמצא") + + # Generate verification code + code = generate_verification_code() + + # Store code + store_verification_code(current_user["user_id"], code) + + # Send email + try: + await send_verification_email(user["email"], code, "password_change") + except Exception as e: + raise HTTPException(status_code=500, detail=f"שגיאה בשליחת אימייל: {str(e)}") + + return {"message": "קוד אימות נשלח לכתובת המייל שלך"} + + +@app.post("/auth/change-password") +def change_password( + request: ChangePasswordRequest, + current_user: dict = Depends(get_current_user) +): + """Change user password after verifying code and current password""" + from user_db_utils import get_user_by_id + + # Get user from database + user = get_user_by_id(current_user["user_id"]) + if not user: + raise HTTPException(status_code=404, detail="משתמש לא נמצא") + + # Verify code + if not verify_code(current_user["user_id"], request.verification_code): + raise HTTPException(status_code=401, detail="קוד אימות שגוי או פג תוקף") + + # Verify current password + if not verify_password(request.current_password, user["password_hash"]): + raise HTTPException(status_code=401, detail="סיסמה נוכחית שגויה") + + # Hash new password + new_password_hash = hash_password(request.new_password) + + # Update password in database + conn = get_conn() + cur = conn.cursor() + try: + cur.execute( + "UPDATE users SET password_hash = %s WHERE id = %s", + (new_password_hash, current_user["user_id"]) + ) + conn.commit() + except Exception as e: + conn.rollback() + raise HTTPException(status_code=500, detail=f"שגיאה בעדכון סיסמה: {str(e)}") + finally: + cur.close() + conn.close() + + return {"message": "הסיסמה עודכנה בהצלחה"} + + +# ============= Google OAuth Endpoints ============= + +@app.get("/auth/google/login") +async def google_login(request: Request): + """Redirect to Google OAuth login""" + redirect_uri = os.getenv("GOOGLE_REDIRECT_URI", "http://localhost:5174/auth/google/callback") + return await oauth.google.authorize_redirect(request, redirect_uri) + + +@app.get("/auth/google/callback") +async def google_callback(request: Request): + """Handle Google OAuth callback""" + try: + # Get token from Google + token = await oauth.google.authorize_access_token(request) + + # Get user info from Google + user_info = token.get('userinfo') + if not user_info: + raise HTTPException(status_code=400, detail="Failed to get user info from Google") + + email = user_info.get('email') + google_id = user_info.get('sub') + name = user_info.get('name', '') + given_name = user_info.get('given_name', '') + family_name = user_info.get('family_name', '') + + if not email: + raise HTTPException(status_code=400, detail="Email not provided by Google") + + # Check if user exists + existing_user = get_user_by_email(email) + + if existing_user: + # User exists, log them in + user_id = existing_user["id"] + username = existing_user["username"] + else: + # Create new user + # Generate username from email or name + username = email.split('@')[0] + + # Check if username exists, add number if needed + base_username = username + counter = 1 + while get_user_by_username(username): + username = f"{base_username}{counter}" + counter += 1 + + # Create user with random password (they'll use Google login) + import secrets + random_password = secrets.token_urlsafe(32) + password_hash = hash_password(random_password) + + new_user = create_user( + username=username, + email=email, + password_hash=password_hash, + first_name=given_name if given_name else None, + last_name=family_name if family_name else None, + display_name=name if name else username, + is_admin=False + ) + user_id = new_user["id"] + + # Create JWT token + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token( + data={"sub": str(user_id), "username": username}, + expires_delta=access_token_expires + ) + + # Redirect to frontend with token + frontend_url = os.getenv("FRONTEND_URL", "http://localhost:5174") + return Response( + status_code=302, + headers={"Location": f"{frontend_url}?token={access_token}"} + ) + + except Exception as e: + raise HTTPException(status_code=400, detail=f"Google authentication failed: {str(e)}") + + # ============= Grocery Lists Endpoints ============= @app.get("/grocery-lists", response_model=List[GroceryList]) @@ -742,4 +922,4 @@ def delete_notification_endpoint( if __name__ == "__main__": - uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True) \ No newline at end of file + uvicorn.run("main:app", host="0.0.0.0", port=8001, reload=True) \ No newline at end of file diff --git a/backend/oauth_utils.py b/backend/oauth_utils.py new file mode 100644 index 0000000..78547fc --- /dev/null +++ b/backend/oauth_utils.py @@ -0,0 +1,20 @@ +import os +from authlib.integrations.starlette_client import OAuth +from starlette.config import Config + +# Load config +config = Config('.env') + +# Initialize OAuth +oauth = OAuth(config) + +# Register Google OAuth +oauth.register( + name='google', + client_id=os.getenv('GOOGLE_CLIENT_ID'), + client_secret=os.getenv('GOOGLE_CLIENT_SECRET'), + server_metadata_url='https://accounts.google.com/.well-known/openid-configuration', + client_kwargs={ + 'scope': 'openid email profile' + } +) diff --git a/backend/requirements.txt b/backend/requirements.txt index 955dfbe..c86cbd8 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -12,3 +12,11 @@ python-jose[cryptography]==3.3.0 passlib[bcrypt]==1.7.4 python-multipart==0.0.9 bcrypt==4.1.2 + +# Email +aiosmtplib==3.0.2 + +# OAuth +authlib==1.3.0 +httpx==0.27.0 +itsdangerous==2.1.2 diff --git a/backend/reset_admin_password.py b/backend/reset_admin_password.py new file mode 100644 index 0000000..0b9aba2 --- /dev/null +++ b/backend/reset_admin_password.py @@ -0,0 +1,41 @@ +import psycopg2 +import bcrypt +import os +from dotenv import load_dotenv + +load_dotenv() + +# New password for admin +new_password = "admin123" # Change this to whatever you want + +# Hash the password +salt = bcrypt.gensalt() +password_hash = bcrypt.hashpw(new_password.encode('utf-8'), salt).decode('utf-8') + +# Update in database +conn = psycopg2.connect(os.getenv('DATABASE_URL')) +cur = conn.cursor() + +# Update admin password +cur.execute( + "UPDATE users SET password_hash = %s WHERE username = %s", + (password_hash, 'admin') +) +conn.commit() + +# Verify +cur.execute("SELECT username, email, is_admin FROM users WHERE username = 'admin'") +user = cur.fetchone() +if user: + print(f"✓ Admin password updated successfully!") + print(f" Username: {user[0]}") + print(f" Email: {user[1]}") + print(f" Is Admin: {user[2]}") + print(f"\nYou can now login with:") + print(f" Username: admin") + print(f" Password: {new_password}") +else: + print("✗ Admin user not found!") + +cur.close() +conn.close() diff --git a/backend/user_db_utils.py b/backend/user_db_utils.py index 9944e84..fd4af0c 100644 --- a/backend/user_db_utils.py +++ b/backend/user_db_utils.py @@ -76,7 +76,7 @@ def get_user_by_id(user_id: int): cur = conn.cursor(cursor_factory=RealDictCursor) try: cur.execute( - "SELECT id, username, email, first_name, last_name, display_name, is_admin, created_at FROM users WHERE id = %s", + "SELECT id, username, email, password_hash, first_name, last_name, display_name, is_admin, created_at FROM users WHERE id = %s", (user_id,) ) user = cur.fetchone() diff --git a/demo-recipes.sql b/demo-recipes.sql new file mode 100644 index 0000000..3559a84 --- /dev/null +++ b/demo-recipes.sql @@ -0,0 +1,178 @@ +-- Demo recipes for user dvir (id=3) + +-- Recipe 1: שקשוקה +INSERT INTO recipes (name, meal_type, time_minutes, tags, ingredients, steps, image, made_by, user_id) +VALUES ( + 'שקשוקה', + 'breakfast', + 25, + '["מהיר", "בריא", "צמחוני"]'::jsonb, + '["4 ביצים", "4 עגבניות", "1 בצל", "2 שיני שום", "פלפל חריף", "כמון", "מלח", "שמן זית"]'::jsonb, + '[ + "לחתוך את הבצל והשום דק", + "לחמם שמן בסיר ולטגן את הבצל עד שקוף", + "להוסיף שום ופלפל חריף ולטגן דקה", + "לקצוץ עגבניות ולהוסיף לסיר", + "לתבל בכמון ומלח, לבשל 10 דקות", + "לפתוח גומות ברוטב ולשבור ביצה בכל גומה", + "לכסות ולבשל עד שהביצים מתקשות" + ]'::jsonb, + 'https://images.unsplash.com/photo-1525351484163-7529414344d8?w=500', + 'דביר', + 3 +); + +-- Recipe 2: פסטה ברוטב עגבניות +INSERT INTO recipes (name, meal_type, time_minutes, tags, ingredients, steps, image, made_by, user_id) +VALUES ( + 'פסטה ברוטב עגבניות', + 'lunch', + 20, + '["מהיר", "צמחוני", "ילדים אוהבים"]'::jsonb, + '["500 גרם פסטה", "רסק עגבניות", "בזיליקום טרי", "3 שיני שום", "שמן זית", "מלח", "פלפל"]'::jsonb, + '[ + "להרתיח מים מלוחים ולבשל את הפסטה לפי ההוראות", + "בינתיים, לחמם שמן בסיר", + "לטגן שום כתוש דקה", + "להוסיף רסק עגבניות ולתבל", + "לבשל על אש בינונית 10 דקות", + "להוסיף בזיליקום קרוע", + "לערבב את הפסטה המסוננת עם הרוטב" + ]'::jsonb, + 'https://images.unsplash.com/photo-1621996346565-e3dbc646d9a9?w=500', + 'דביר', + 3 +); + +-- Recipe 3: סלט ישראלי +INSERT INTO recipes (name, meal_type, time_minutes, tags, ingredients, steps, image, made_by, user_id) +VALUES ( + 'סלט ישראלי', + 'snack', + 10, + '["מהיר", "בריא", "טבעוני", "צמחוני"]'::jsonb, + '["4 עגבניות", "2 מלפפונים", "1 בצל", "פטרוזיליה", "לימון", "שמן זית", "מלח"]'::jsonb, + '[ + "לחתוך עגבניות ומלפפונים לקוביות קטנות", + "לקצוץ בצל דק", + "לקצוץ פטרוזיליה", + "לערבב הכל בקערה", + "להוסיף מיץ לימון ושמן זית", + "לתבל במלח ולערבב היטב" + ]'::jsonb, + 'https://images.unsplash.com/photo-1512621776951-a57141f2eefd?w=500', + 'דביר', + 3 +); + +-- Recipe 4: חביתה עם ירקות +INSERT INTO recipes (name, meal_type, time_minutes, tags, ingredients, steps, image, made_by, user_id) +VALUES ( + 'חביתה עם ירקות', + 'breakfast', + 15, + '["מהיר", "בריא", "חלבוני", "צמחוני"]'::jsonb, + '["3 ביצים", "1 בצל", "1 פלפל", "עגבניה", "גבינה צהובה", "מלח", "פלפל שחור", "שמן"]'::jsonb, + '[ + "לקצוץ את הירקות לקוביות קטנות", + "לטגן את הירקות בשמן עד שמתרככים", + "להקציף את הביצים במזלג", + "לשפוך את הביצים על הירקות", + "לפזר גבינה קצוצה", + "לבשל עד שהתחתית מוזהבת", + "להפוך או לקפל לחצי ולהגיש" + ]'::jsonb, + 'https://images.unsplash.com/photo-1525351159099-122d10e7960e?w=500', + 'דביר', + 3 +); + +-- Recipe 5: עוף בתנור עם תפוחי אדמה +INSERT INTO recipes (name, meal_type, time_minutes, tags, ingredients, steps, image, made_by, user_id) +VALUES ( + 'עוף בתנור עם תפוחי אדמה', + 'dinner', + 60, + '["משפחתי", "חגיגי"]'::jsonb, + '["1 עוף שלם", "1 ק״ג תפוחי אדמה", "פפריקה", "כורכום", "שום", "מלח", "פלפל", "שמן זית", "לימון"]'::jsonb, + '[ + "לחמם תנור ל-200 מעלות", + "לחתוך תפוחי אדמה לרבעים", + "לשפשף את העוף בתבלינים, שמן ומיץ לימון", + "לסדר תפוחי אדמה בתבנית", + "להניח את העוף על התפוחי אדמה", + "לאפות כשעה עד שהעוף מוזהב", + "להוציא, לחתוך ולהגיש עם הירקות" + ]'::jsonb, + 'https://images.unsplash.com/photo-1598103442097-8b74394b95c6?w=500', + 'דביר', + 3 +); + +-- Recipe 6: סנדוויץ טונה +INSERT INTO recipes (name, meal_type, time_minutes, tags, ingredients, steps, image, made_by, user_id) +VALUES ( + 'סנדוויץ טונה', + 'lunch', + 5, + '["מהיר", "קר", "חלבוני"]'::jsonb, + '["קופסת טונה", "2 פרוסות לחם", "מיונז", "חסה", "עגבניה", "מלפפון חמוץ", "מלח", "פלפל"]'::jsonb, + '[ + "לסנן את הטונה", + "לערבב את הטונה עם מיונז", + "לתבל במלח ופלפל", + "למרוח על פרוסת לחם", + "להוסיף חסה, עגבניה ומלפפון", + "לכסות בפרוסת לחם שנייה" + ]'::jsonb, + 'https://images.unsplash.com/photo-1528735602780-2552fd46c7af?w=500', + 'דביר', + 3 +); + +-- Recipe 7: בראוניז שוקולד +INSERT INTO recipes (name, meal_type, time_minutes, tags, ingredients, steps, image, made_by, user_id) +VALUES ( + 'בראוניז שוקולד', + 'snack', + 35, + '["קינוח", "שוקולד", "אפייה"]'::jsonb, + '["200 גרם שוקולד מריר", "150 גרם חמאה", "3 ביצים", "כוס סוכר", "חצי כוס קמח", "אבקת קקאו", "וניל"]'::jsonb, + '[ + "לחמם תנור ל-180 מעלות", + "להמיס שוקולד וחמאה במיקרוגל", + "להקציף ביצים וסוכר", + "להוסיף את תערובת השוקולד", + "להוסיף קמח וקקאו ולערבב", + "לשפוך לתבנית משומנת", + "לאפות 25 דקות", + "להוציא ולהניח להתקרר לפני חיתוך" + ]'::jsonb, + 'https://images.unsplash.com/photo-1606313564200-e75d5e30476c?w=500', + 'דביר', + 3 +); + +-- Recipe 8: מרק עדשים +INSERT INTO recipes (name, meal_type, time_minutes, tags, ingredients, steps, image, made_by, user_id) +VALUES ( + 'מרק עדשים', + 'dinner', + 40, + '["בריא", "צמחוני", "טבעוני", "חם"]'::jsonb, + '["2 כוסות עדשים כתומות", "בצל", "גזר", "3 שיני שום", "כמון", "כורכום", "מלח", "לימון"]'::jsonb, + '[ + "לשטוף את העדשים", + "לקצוץ בצל, גזר ושום", + "לטגן את הבצל עד שקוף", + "להוסיף שום ותבלינים", + "להוסיף גזר ועדשים", + "להוסיף 6 כוסות מים", + "לבשל 30 דקות עד שהעדשים רכים", + "לטחון חלק מהמרק לקבלת מרקם עבה", + "להוסיף מיץ לימון לפני הגשה" + ]'::jsonb, + 'https://images.unsplash.com/photo-1547592166-23ac45744acd?w=500', + 'דביר', + 3 +); diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index cb69b6a..405eb50 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -12,6 +12,7 @@ import ToastContainer from "./components/ToastContainer"; import ThemeToggle from "./components/ThemeToggle"; import Login from "./components/Login"; import Register from "./components/Register"; +import ChangePassword from "./components/ChangePassword"; import { getRecipes, getRandomRecipe, createRecipe, updateRecipe, deleteRecipe } from "./api"; import { getToken, removeToken, getMe } from "./authApi"; @@ -51,6 +52,7 @@ function App() { const [deleteModal, setDeleteModal] = useState({ isOpen: false, recipeId: null, recipeName: "" }); const [logoutModal, setLogoutModal] = useState(false); + const [changePasswordModal, setChangePasswordModal] = useState(false); const [toasts, setToasts] = useState([]); const [theme, setTheme] = useState(() => { try { @@ -324,7 +326,13 @@ function App() { ) : ( -+ קוד אימות יישלח לכתובת המייל שלך. הקוד תקף ל-10 דקות. +
+ +
עדיין אין לך חשבון?{" "}
diff --git a/frontend/src/components/TopBar.jsx b/frontend/src/components/TopBar.jsx
index 65ee762..9b65525 100644
--- a/frontend/src/components/TopBar.jsx
+++ b/frontend/src/components/TopBar.jsx
@@ -1,6 +1,6 @@
import NotificationBell from "./NotificationBell";
-function TopBar({ onAddClick, user, onLogout, onShowToast }) {
+function TopBar({ onAddClick, user, onLogout, onChangePassword, onShowToast }) {
return (