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.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"]
)

View File

@ -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
);

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"""
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()

View File

@ -4,7 +4,7 @@
<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>">
<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 -->
<script src="/env.js"></script>
</head>

View File

@ -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);
}

View File

@ -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 && (
<div className="user-greeting-header">
שלום, {user.username} 👋
שלום, {user.display_name || user.username} 👋
</div>
)}
@ -313,10 +320,7 @@ function App() {
/>
) : (
<Register
onSuccess={() => {
addToast("נרשמת בהצלחה! כעת התחבר", "success");
setAuthView("login");
}}
onSuccess={handleLoginSuccess}
onSwitchToLogin={() => setAuthView("login")}
/>
)}
@ -430,6 +434,17 @@ function App() {
onCancel={handleCancelDelete}
/>
<Modal
isOpen={logoutModal}
title="התנתקות"
message="האם אתה בטוח שברצונך להתנתק?"
confirmText="התנתק"
cancelText="ביטול"
isDangerous={false}
onConfirm={confirmLogout}
onCancel={() => setLogoutModal(false)}
/>
<ToastContainer toasts={toasts} onRemove={removeToast} />
</div>
);

View File

@ -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();

View File

@ -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 && <div className="error-banner">{error}</div>}
<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
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
placeholder="בחר שם משתמש"
placeholder="username (English only)"
autoComplete="username"
minLength={3}
pattern="[a-zA-Z0-9_-]+"
title="שם משתמש יכול להכיל רק אותיות באנגלית, מספרים, _ ו-"
/>
</div>
<div className="field">
<label>אימייל</label>
<label>אימייל *</label>
<input
type="email"
value={email}
@ -72,7 +118,7 @@ function Register({ onSuccess, onSwitchToLogin }) {
</div>
<div className="field">
<label>סיסמה</label>
<label>סיסמה *</label>
<input
type="password"
value={password}
@ -85,7 +131,7 @@ function Register({ onSuccess, onSwitchToLogin }) {
</div>
<div className="field">
<label>אימות סיסמה</label>
<label>אימות סיסמה *</label>
<input
type="password"
value={confirmPassword}