Style register and sign in and create confirmation when sign out

This commit is contained in:
dvirlabs 2025-12-08 08:12:35 +02:00
parent e160357256
commit 53ca792988
10 changed files with 147 additions and 34 deletions

View File

@ -4,7 +4,7 @@ from datetime import timedelta
from fastapi import FastAPI, HTTPException, Query, Depends from fastapi import FastAPI, HTTPException, Query, Depends
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, EmailStr from pydantic import BaseModel, EmailStr, field_validator
import os import os
import uvicorn import uvicorn
@ -62,6 +62,18 @@ class UserRegister(BaseModel):
username: str username: str
email: EmailStr email: EmailStr
password: str 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): class UserLogin(BaseModel):
@ -78,6 +90,9 @@ class UserResponse(BaseModel):
id: int id: int
username: str username: str
email: str email: str
first_name: Optional[str] = None
last_name: Optional[str] = None
display_name: str
app = FastAPI( app = FastAPI(
@ -277,13 +292,23 @@ def register(user: UserRegister):
print(f"[REGISTER] Hashing password...") print(f"[REGISTER] Hashing password...")
password_hash = hash_password(user.password) password_hash = hash_password(user.password)
print(f"[REGISTER] Creating user in database...") 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']}") print(f"[REGISTER] User created successfully: {new_user['id']}")
return UserResponse( return UserResponse(
id=new_user["id"], id=new_user["id"],
username=new_user["username"], 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( return UserResponse(
id=user["id"], id=user["id"],
username=user["username"], 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"]
) )

View File

@ -4,6 +4,9 @@ CREATE TABLE IF NOT EXISTS users (
username TEXT UNIQUE NOT NULL, username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL, email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL, password_hash TEXT NOT NULL,
first_name TEXT,
last_name TEXT,
display_name TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );

View File

@ -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""" """Create a new user"""
conn = get_db_connection() conn = get_db_connection()
cur = conn.cursor(cursor_factory=RealDictCursor) cur = conn.cursor(cursor_factory=RealDictCursor)
try: try:
# Use display_name if provided, otherwise use username
final_display_name = display_name if display_name else username
cur.execute( cur.execute(
""" """
INSERT INTO users (username, email, password_hash) INSERT INTO users (username, email, password_hash, first_name, last_name, display_name)
VALUES (%s, %s, %s) VALUES (%s, %s, %s, %s, %s, %s)
RETURNING id, username, email, created_at 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() user = cur.fetchone()
conn.commit() conn.commit()
@ -41,7 +44,7 @@ def get_user_by_username(username: str):
cur = conn.cursor(cursor_factory=RealDictCursor) cur = conn.cursor(cursor_factory=RealDictCursor)
try: try:
cur.execute( 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,) (username,)
) )
user = cur.fetchone() user = cur.fetchone()
@ -57,7 +60,7 @@ def get_user_by_email(email: str):
cur = conn.cursor(cursor_factory=RealDictCursor) cur = conn.cursor(cursor_factory=RealDictCursor)
try: try:
cur.execute( 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,) (email,)
) )
user = cur.fetchone() user = cur.fetchone()
@ -73,7 +76,7 @@ def get_user_by_id(user_id: int):
cur = conn.cursor(cursor_factory=RealDictCursor) cur = conn.cursor(cursor_factory=RealDictCursor)
try: try:
cur.execute( 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_id,)
) )
user = cur.fetchone() user = cur.fetchone()

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍽️</text></svg>"> <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🍽️</text></svg>">
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Recipes | מתכונים שלי</title> <title>My Recipes | המתכונים שלי</title>
<!-- Load environment variables before app starts --> <!-- Load environment variables before app starts -->
<script src="/env.js"></script> <script src="/env.js"></script>
</head> </head>

View File

@ -765,7 +765,7 @@ body {
} }
[data-theme="light"] 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); color: var(--text-main);
} }
@ -1292,17 +1292,17 @@ html {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 2rem; padding: 2rem;
background: var(--bg); background: radial-gradient(circle at top, #0f172a 0, #020617 55%);
} }
.auth-card { .auth-card {
width: 100%; width: 100%;
max-width: 420px; max-width: 420px;
background: var(--card); background: #020617;
border: 1px solid var(--border); border: 1px solid var(--border-subtle);
border-radius: 16px; border-radius: 16px;
padding: 2rem; 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 { .auth-title {
@ -1349,3 +1349,14 @@ html {
.full-width { .full-width {
width: 100%; 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);
}

View File

@ -41,6 +41,7 @@ function App() {
const [editingRecipe, setEditingRecipe] = useState(null); const [editingRecipe, setEditingRecipe] = useState(null);
const [deleteModal, setDeleteModal] = useState({ isOpen: false, recipeId: null, recipeName: "" }); const [deleteModal, setDeleteModal] = useState({ isOpen: false, recipeId: null, recipeName: "" });
const [logoutModal, setLogoutModal] = useState(false);
const [toasts, setToasts] = useState([]); const [toasts, setToasts] = useState([]);
const [theme, setTheme] = useState(() => { const [theme, setTheme] = useState(() => {
try { try {
@ -249,11 +250,17 @@ function App() {
}; };
const handleLogout = () => { const handleLogout = () => {
setLogoutModal(true);
};
const confirmLogout = () => {
removeToken(); removeToken();
setUser(null); setUser(null);
setIsAuthenticated(false); setIsAuthenticated(false);
setRecipes([]); setRecipes([]);
setSelectedRecipe(null); setSelectedRecipe(null);
setLogoutModal(false);
addToast('התנתקת בהצלחה', 'success');
}; };
// Show loading state while checking auth // Show loading state while checking auth
@ -275,7 +282,7 @@ function App() {
{/* User greeting above TopBar */} {/* User greeting above TopBar */}
{isAuthenticated && user && ( {isAuthenticated && user && (
<div className="user-greeting-header"> <div className="user-greeting-header">
שלום, {user.username} 👋 שלום, {user.display_name || user.username} 👋
</div> </div>
)} )}
@ -313,10 +320,7 @@ function App() {
/> />
) : ( ) : (
<Register <Register
onSuccess={() => { onSuccess={handleLoginSuccess}
addToast("נרשמת בהצלחה! כעת התחבר", "success");
setAuthView("login");
}}
onSwitchToLogin={() => setAuthView("login")} onSwitchToLogin={() => setAuthView("login")}
/> />
)} )}
@ -430,6 +434,17 @@ function App() {
onCancel={handleCancelDelete} onCancel={handleCancelDelete}
/> />
<Modal
isOpen={logoutModal}
title="התנתקות"
message="האם אתה בטוח שברצונך להתנתק?"
confirmText="התנתק"
cancelText="ביטול"
isDangerous={false}
onConfirm={confirmLogout}
onCancel={() => setLogoutModal(false)}
/>
<ToastContainer toasts={toasts} onRemove={removeToast} /> <ToastContainer toasts={toasts} onRemove={removeToast} />
</div> </div>
); );

View File

@ -8,11 +8,18 @@ const getApiBase = () => {
const API_BASE = 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`, { const res = await fetch(`${API_BASE}/auth/register`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, 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) { if (!res.ok) {
const error = await res.json(); const error = await res.json();

View File

@ -1,11 +1,14 @@
import { useState } from "react"; import { useState } from "react";
import { register } from "../authApi"; import { register, login, saveToken } from "../authApi";
function Register({ onSuccess, onSwitchToLogin }) { function Register({ onSuccess, onSwitchToLogin }) {
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [email, setEmail] = useState(""); const [email, setEmail] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState("");
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [displayName, setDisplayName] = useState("");
const [error, setError] = useState(""); const [error, setError] = useState("");
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -24,12 +27,21 @@ function Register({ onSuccess, onSwitchToLogin }) {
return; return;
} }
if (!displayName.trim()) {
setError("שם תצוגה הוא שדה חובה");
return;
}
setLoading(true); setLoading(true);
try { try {
await register(username, email, password); // Register the user
// After successful registration, switch to login await register(username, email, password, firstName, lastName, displayName);
onSwitchToLogin();
// Automatically login after successful registration
const response = await login(username, password);
saveToken(response.access_token);
onSuccess();
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
} finally { } finally {
@ -47,20 +59,54 @@ function Register({ onSuccess, onSwitchToLogin }) {
{error && <div className="error-banner">{error}</div>} {error && <div className="error-banner">{error}</div>}
<div className="field"> <div className="field">
<label>שם משתמש</label> <label>שם פרטי</label>
<input
type="text"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
placeholder="שם פרטי (אופציונלי)"
/>
</div>
<div className="field">
<label>שם משפחה</label>
<input
type="text"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
placeholder="שם משפחה (אופציונלי)"
/>
</div>
<div className="field">
<label>שם תצוגה *</label>
<input
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
required
placeholder="איך תרצה שיופיע שמך?"
minLength={2}
/>
</div>
<div className="field">
<label>שם משתמש * (אנגלית בלבד)</label>
<input <input
type="text" type="text"
value={username} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
required required
placeholder="בחר שם משתמש" placeholder="username (English only)"
autoComplete="username" autoComplete="username"
minLength={3} minLength={3}
pattern="[a-zA-Z0-9_-]+"
title="שם משתמש יכול להכיל רק אותיות באנגלית, מספרים, _ ו-"
/> />
</div> </div>
<div className="field"> <div className="field">
<label>אימייל</label> <label>אימייל *</label>
<input <input
type="email" type="email"
value={email} value={email}
@ -72,7 +118,7 @@ function Register({ onSuccess, onSwitchToLogin }) {
</div> </div>
<div className="field"> <div className="field">
<label>סיסמה</label> <label>סיסמה *</label>
<input <input
type="password" type="password"
value={password} value={password}
@ -85,7 +131,7 @@ function Register({ onSuccess, onSwitchToLogin }) {
</div> </div>
<div className="field"> <div className="field">
<label>אימות סיסמה</label> <label>אימות סיסמה *</label>
<input <input
type="password" type="password"
value={confirmPassword} value={confirmPassword}