Style register and sign in and create confirmation when sign out
This commit is contained in:
parent
e160357256
commit
53ca792988
Binary file not shown.
Binary file not shown.
@ -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"]
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
);
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user