diff --git a/backend/__pycache__/main.cpython-313.pyc b/backend/__pycache__/main.cpython-313.pyc index 72cc141..be7bf31 100644 Binary files a/backend/__pycache__/main.cpython-313.pyc and b/backend/__pycache__/main.cpython-313.pyc differ diff --git a/backend/__pycache__/user_db_utils.cpython-313.pyc b/backend/__pycache__/user_db_utils.cpython-313.pyc index e6126ee..c500bba 100644 Binary files a/backend/__pycache__/user_db_utils.cpython-313.pyc and b/backend/__pycache__/user_db_utils.cpython-313.pyc differ diff --git a/backend/main.py b/backend/main.py index e6a755e..a52d7db 100644 --- a/backend/main.py +++ b/backend/main.py @@ -4,7 +4,7 @@ from datetime import timedelta from fastapi import FastAPI, HTTPException, Query, Depends from fastapi.middleware.cors import CORSMiddleware -from pydantic import BaseModel, EmailStr +from pydantic import BaseModel, EmailStr, field_validator import os import uvicorn @@ -62,6 +62,18 @@ class UserRegister(BaseModel): username: str email: EmailStr password: str + first_name: Optional[str] = None + last_name: Optional[str] = None + display_name: str + + @field_validator('username') + @classmethod + def username_must_be_english(cls, v: str) -> str: + if not v.isascii(): + raise ValueError('שם משתמש חייב להיות באנגלית בלבד') + if not all(c.isalnum() or c in '_-' for c in v): + raise ValueError('שם משתמש יכול להכיל רק אותיות, מספרים, _ ו-') + return v class UserLogin(BaseModel): @@ -78,6 +90,9 @@ class UserResponse(BaseModel): id: int username: str email: str + first_name: Optional[str] = None + last_name: Optional[str] = None + display_name: str app = FastAPI( @@ -277,13 +292,23 @@ def register(user: UserRegister): print(f"[REGISTER] Hashing password...") password_hash = hash_password(user.password) print(f"[REGISTER] Creating user in database...") - new_user = create_user(user.username, user.email, password_hash) + new_user = create_user( + user.username, + user.email, + password_hash, + user.first_name, + user.last_name, + user.display_name + ) print(f"[REGISTER] User created successfully: {new_user['id']}") return UserResponse( id=new_user["id"], username=new_user["username"], - email=new_user["email"] + email=new_user["email"], + first_name=new_user.get("first_name"), + last_name=new_user.get("last_name"), + display_name=new_user["display_name"] ) @@ -326,7 +351,10 @@ def get_me(current_user: dict = Depends(get_current_user)): return UserResponse( id=user["id"], username=user["username"], - email=user["email"] + email=user["email"], + first_name=user.get("first_name"), + last_name=user.get("last_name"), + display_name=user["display_name"] ) diff --git a/backend/schema.sql b/backend/schema.sql index 76916b5..88bfd34 100644 --- a/backend/schema.sql +++ b/backend/schema.sql @@ -4,6 +4,9 @@ CREATE TABLE IF NOT EXISTS users ( username TEXT UNIQUE NOT NULL, email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, + first_name TEXT, + last_name TEXT, + display_name TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); diff --git a/backend/user_db_utils.py b/backend/user_db_utils.py index fd37d36..264da28 100644 --- a/backend/user_db_utils.py +++ b/backend/user_db_utils.py @@ -14,18 +14,21 @@ def get_db_connection(): ) -def create_user(username: str, email: str, password_hash: str): +def create_user(username: str, email: str, password_hash: str, first_name: str = None, last_name: str = None, display_name: str = None): """Create a new user""" conn = get_db_connection() cur = conn.cursor(cursor_factory=RealDictCursor) try: + # Use display_name if provided, otherwise use username + final_display_name = display_name if display_name else username + cur.execute( """ - INSERT INTO users (username, email, password_hash) - VALUES (%s, %s, %s) - RETURNING id, username, email, created_at + INSERT INTO users (username, email, password_hash, first_name, last_name, display_name) + VALUES (%s, %s, %s, %s, %s, %s) + RETURNING id, username, email, first_name, last_name, display_name, created_at """, - (username, email, password_hash) + (username, email, password_hash, first_name, last_name, final_display_name) ) user = cur.fetchone() conn.commit() @@ -41,7 +44,7 @@ def get_user_by_username(username: str): cur = conn.cursor(cursor_factory=RealDictCursor) try: cur.execute( - "SELECT id, username, email, password_hash, created_at FROM users WHERE username = %s", + "SELECT id, username, email, password_hash, first_name, last_name, display_name, created_at FROM users WHERE username = %s", (username,) ) user = cur.fetchone() @@ -57,7 +60,7 @@ def get_user_by_email(email: str): cur = conn.cursor(cursor_factory=RealDictCursor) try: cur.execute( - "SELECT id, username, email, password_hash, created_at FROM users WHERE email = %s", + "SELECT id, username, email, password_hash, first_name, last_name, display_name, created_at FROM users WHERE email = %s", (email,) ) user = cur.fetchone() @@ -73,7 +76,7 @@ def get_user_by_id(user_id: int): cur = conn.cursor(cursor_factory=RealDictCursor) try: cur.execute( - "SELECT id, username, email, created_at FROM users WHERE id = %s", + "SELECT id, username, email, first_name, last_name, display_name, created_at FROM users WHERE id = %s", (user_id,) ) user = cur.fetchone() diff --git a/frontend/index.html b/frontend/index.html index 259fcb7..577d29f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - My Recipes | מתכונים שלי + My Recipes | המתכונים שלי diff --git a/frontend/src/App.css b/frontend/src/App.css index 4780b9d..b88d521 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -765,7 +765,7 @@ body { } [data-theme="light"] body { - background: linear-gradient(180deg, #ac8d75 0%, #f6f8fa 100%); + background: linear-gradient(180deg, #ac8d75 0%, #ede9e5 100%); color: var(--text-main); } @@ -1292,17 +1292,17 @@ html { align-items: center; justify-content: center; padding: 2rem; - background: var(--bg); + background: radial-gradient(circle at top, #0f172a 0, #020617 55%); } .auth-card { width: 100%; max-width: 420px; - background: var(--card); - border: 1px solid var(--border); + background: #020617; + border: 1px solid var(--border-subtle); border-radius: 16px; padding: 2rem; - box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3); + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.7); } .auth-title { @@ -1349,3 +1349,14 @@ html { .full-width { width: 100%; } + +/* Light mode auth styles */ +[data-theme="light"] .auth-container { + background: linear-gradient(180deg, #ac8d75 0%, #ede9e5 100%); +} + +[data-theme="light"] .auth-card { + background: #d1b29b; + border: 1px solid rgba(107, 114, 128, 0.2); + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.08); +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 5948a98..467216d 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -41,6 +41,7 @@ function App() { const [editingRecipe, setEditingRecipe] = useState(null); const [deleteModal, setDeleteModal] = useState({ isOpen: false, recipeId: null, recipeName: "" }); + const [logoutModal, setLogoutModal] = useState(false); const [toasts, setToasts] = useState([]); const [theme, setTheme] = useState(() => { try { @@ -249,11 +250,17 @@ function App() { }; const handleLogout = () => { + setLogoutModal(true); + }; + + const confirmLogout = () => { removeToken(); setUser(null); setIsAuthenticated(false); setRecipes([]); setSelectedRecipe(null); + setLogoutModal(false); + addToast('התנתקת בהצלחה', 'success'); }; // Show loading state while checking auth @@ -275,7 +282,7 @@ function App() { {/* User greeting above TopBar */} {isAuthenticated && user && (
- שלום, {user.username} 👋 + שלום, {user.display_name || user.username} 👋
)} @@ -313,10 +320,7 @@ function App() { /> ) : ( { - addToast("נרשמת בהצלחה! כעת התחבר", "success"); - setAuthView("login"); - }} + onSuccess={handleLoginSuccess} onSwitchToLogin={() => setAuthView("login")} /> )} @@ -430,6 +434,17 @@ function App() { onCancel={handleCancelDelete} /> + setLogoutModal(false)} + /> + ); diff --git a/frontend/src/authApi.js b/frontend/src/authApi.js index 626dd1d..e6ae1dc 100644 --- a/frontend/src/authApi.js +++ b/frontend/src/authApi.js @@ -8,11 +8,18 @@ const getApiBase = () => { const API_BASE = getApiBase(); -export async function register(username, email, password) { +export async function register(username, email, password, firstName, lastName, displayName) { const res = await fetch(`${API_BASE}/auth/register`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ username, email, password }), + body: JSON.stringify({ + username, + email, + password, + first_name: firstName, + last_name: lastName, + display_name: displayName + }), }); if (!res.ok) { const error = await res.json(); diff --git a/frontend/src/components/Register.jsx b/frontend/src/components/Register.jsx index 01d197f..40d1bdc 100644 --- a/frontend/src/components/Register.jsx +++ b/frontend/src/components/Register.jsx @@ -1,11 +1,14 @@ import { useState } from "react"; -import { register } from "../authApi"; +import { register, login, saveToken } from "../authApi"; function Register({ onSuccess, onSwitchToLogin }) { const [username, setUsername] = useState(""); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [displayName, setDisplayName] = useState(""); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); @@ -24,12 +27,21 @@ function Register({ onSuccess, onSwitchToLogin }) { return; } + if (!displayName.trim()) { + setError("שם תצוגה הוא שדה חובה"); + return; + } + setLoading(true); try { - await register(username, email, password); - // After successful registration, switch to login - onSwitchToLogin(); + // Register the user + await register(username, email, password, firstName, lastName, displayName); + + // Automatically login after successful registration + const response = await login(username, password); + saveToken(response.access_token); + onSuccess(); } catch (err) { setError(err.message); } finally { @@ -47,20 +59,54 @@ function Register({ onSuccess, onSwitchToLogin }) { {error &&
{error}
}
- + + setFirstName(e.target.value)} + placeholder="שם פרטי (אופציונלי)" + /> +
+ +
+ + setLastName(e.target.value)} + placeholder="שם משפחה (אופציונלי)" + /> +
+ +
+ + setDisplayName(e.target.value)} + required + placeholder="איך תרצה שיופיע שמך?" + minLength={2} + /> +
+ +
+ setUsername(e.target.value)} required - placeholder="בחר שם משתמש" + placeholder="username (English only)" autoComplete="username" minLength={3} + pattern="[a-zA-Z0-9_-]+" + title="שם משתמש יכול להכיל רק אותיות באנגלית, מספרים, _ ו-" />
- +
- +
- +