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 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"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user