Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ class User(UserBase):
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True)
hashed_password = Column(String)
reset_token = Column(String, nullable=True)
reset_token_expires = Column(DateTime, nullable=True)
created_at = Column(DateTime, default=datetime.utcnow)

# Event Database Model
Expand Down
56 changes: 55 additions & 1 deletion backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from pydantic import BaseModel
from typing import Optional, List
import os
import secrets

# Import database models and dependencies
from database import User, Event, get_user_db, get_event_db, init_event_db
Expand Down Expand Up @@ -40,6 +41,16 @@ class EventResponse(BaseModel):
venue: str
image_url: Optional[str] = None

class ForgotPasswordRequest(BaseModel):
email: str

class ResetPasswordRequest(BaseModel):
token: str
new_password: str

class MessageResponse(BaseModel):
message: str

# Initialize event database if needed
init_event_db()

Expand All @@ -49,7 +60,7 @@ class EventResponse(BaseModel):
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:3000"], # React dev server
allow_origins=["http://localhost:3000", "http://localhost:3001"], # React dev server
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
Expand All @@ -74,6 +85,16 @@ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt

def generate_reset_token():
return secrets.token_urlsafe(32)

def send_reset_email(email: str, reset_token: str):
reset_link = f"http://localhost:3000/reset-password?token={reset_token}"
print(f"Password reset email for {email}:")
print(f"Reset link: {reset_link}")
print("(In production, this would be sent via email service)")
return True

def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security), db = Depends(get_user_db)):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
Expand Down Expand Up @@ -145,6 +166,39 @@ async def login(user: UserLogin, db = Depends(get_user_db)):

return {"access_token": access_token, "token_type": "bearer"}

@app.post("/auth/forgot-password", response_model=MessageResponse)
async def forgot_password(request: ForgotPasswordRequest, db = Depends(get_user_db)):
user = db.query(User).filter(User.email == request.email).first()
if not user:
return {"message": "If an account with that email exists, a password reset link has been sent."}

reset_token = generate_reset_token()
reset_expires = datetime.utcnow() + timedelta(hours=1)

user.reset_token = reset_token
user.reset_token_expires = reset_expires
db.commit()

send_reset_email(user.email, reset_token)

return {"message": "If an account with that email exists, a password reset link has been sent."}

@app.post("/auth/reset-password", response_model=MessageResponse)
async def reset_password(request: ResetPasswordRequest, db = Depends(get_user_db)):
user = db.query(User).filter(User.reset_token == request.token).first()
if not user or not user.reset_token_expires or user.reset_token_expires < datetime.utcnow():
raise HTTPException(
status_code=400,
detail="Invalid or expired reset token"
)

user.hashed_password = get_password_hash(request.new_password)
user.reset_token = None
user.reset_token_expires = None
db.commit()

return {"message": "Password has been reset successfully."}

@app.get("/events", response_model=List[EventResponse])
async def get_events(db = Depends(get_event_db)):
"""Get all events from the database"""
Expand Down
37 changes: 37 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,43 @@
text-decoration: underline;
}

.error-message {
background-color: #fee;
color: #c33;
padding: 12px;
border-radius: 6px;
font-size: 14px;
margin-bottom: 16px;
border: 1px solid #fcc;
}

.success-message {
background-color: #efe;
color: #363;
padding: 12px;
border-radius: 6px;
font-size: 14px;
margin-bottom: 16px;
border: 1px solid #cfc;
}

.return-to-signin {
color: #1c74cc;
background: none;
border: none;
padding: 0;
font-size: 14px;
cursor: pointer;
text-decoration: none;
margin-top: 16px;
width: 100%;
text-align: center;
}

.return-to-signin:hover {
text-decoration: underline;
}

/* Homepage Styles */
.homepage {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
Expand Down
224 changes: 154 additions & 70 deletions src/Login.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [stayLoggedIn, setStayLoggedIn] = useState(false);
const [showForgotPassword, setShowForgotPassword] = useState(false);
const [forgotPasswordEmail, setForgotPasswordEmail] = useState('');
const [forgotPasswordMessage, setForgotPasswordMessage] = useState('');
const [forgotPasswordError, setForgotPasswordError] = useState('');
const [isSubmittingReset, setIsSubmittingReset] = useState(false);

const isFormValid = email.trim() !== '' && password.trim() !== '';
const isForgotPasswordValid = forgotPasswordEmail.trim() !== '';

const handleSubmit = (e) => {
e.preventDefault();
Expand All @@ -19,83 +25,161 @@ function Login() {
}
};

const handleForgotPasswordSubmit = async (e) => {
e.preventDefault();
if (!isForgotPasswordValid) return;

setIsSubmittingReset(true);
setForgotPasswordError('');
setForgotPasswordMessage('');

try {
const response = await fetch('http://localhost:8000/auth/forgot-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email: forgotPasswordEmail }),
});

const data = await response.json();

if (response.ok) {
setForgotPasswordMessage(data.message);
} else {
setForgotPasswordError(data.detail || 'An error occurred');
}
} catch (error) {
setForgotPasswordError('Network error. Please try again.');
} finally {
setIsSubmittingReset(false);
}
};

const handleReturnToSignIn = () => {
setShowForgotPassword(false);
setForgotPasswordEmail('');
setForgotPasswordMessage('');
setForgotPasswordError('');
};

return (
<div className="App">
<div className="login-container">
<div className="logo-container">
<img src={logo} alt="StubHub" className="logo" />
</div>

<h1 className="login-title">Sign in to StubHub</h1>

<form onSubmit={handleSubmit} className="login-form">
<div className="input-group">
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="input-field"
/>
</div>

<div className="input-group">
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="input-field"
/>
</div>

<div className="checkbox-row">
<label className="checkbox-label">
<input
type="checkbox"
checked={stayLoggedIn}
onChange={(e) => setStayLoggedIn(e.target.checked)}
className="checkbox"
/>
Stay logged in
</label>
<button type="button" className="forgot-password" onClick={() => console.log('Forgot password clicked')}>Forgot Password</button>
</div>

<button
type="submit"
className={`sign-in-btn ${!isFormValid ? 'disabled' : ''}`}
disabled={!isFormValid}
>
Sign in
</button>
</form>

<button className="email-code-btn">
Sign in with Email Code
</button>

<div className="social-login">
<button className="facebook-btn">
<img src={facebookLogo} alt="Facebook" className="social-icon" />
Log in with Facebook
</button>

<button className="apple-btn">
<img src={appleLogo} alt="Apple" className="social-icon" />
Sign in with Apple
</button>

<button className="google-btn">
<img src={googleLogo} alt="Google" className="social-icon" />
Sign in with Google
</button>
</div>

<div className="create-account">
<span>New to StubHub? </span>
<button type="button" className="create-account-link" onClick={() => console.log('Create account clicked')}>Create account</button>
</div>
{!showForgotPassword ? (
<>
<h1 className="login-title">Sign in to StubHub</h1>

<form onSubmit={handleSubmit} className="login-form">
<div className="input-group">
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="input-field"
/>
</div>

<div className="input-group">
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="input-field"
/>
</div>

<div className="checkbox-row">
<label className="checkbox-label">
<input
type="checkbox"
checked={stayLoggedIn}
onChange={(e) => setStayLoggedIn(e.target.checked)}
className="checkbox"
/>
Stay logged in
</label>
<button type="button" className="forgot-password" onClick={() => setShowForgotPassword(true)}>Forgot Password</button>
</div>

<button
type="submit"
className={`sign-in-btn ${!isFormValid ? 'disabled' : ''}`}
disabled={!isFormValid}
>
Sign in
</button>
</form>

<button className="email-code-btn">
Sign in with Email Code
</button>

<div className="social-login">
<button className="facebook-btn">
<img src={facebookLogo} alt="Facebook" className="social-icon" />
Log in with Facebook
</button>

<button className="apple-btn">
<img src={appleLogo} alt="Apple" className="social-icon" />
Sign in with Apple
</button>

<button className="google-btn">
<img src={googleLogo} alt="Google" className="social-icon" />
Sign in with Google
</button>
</div>

<div className="create-account">
<span>New to StubHub? </span>
<button type="button" className="create-account-link" onClick={() => console.log('Create account clicked')}>Create account</button>
</div>
</>
) : (
<>
<h1 className="login-title">Forgot Password</h1>

<form onSubmit={handleForgotPasswordSubmit} className="login-form">
<div className="input-group">
<input
type="email"
placeholder="Email"
value={forgotPasswordEmail}
onChange={(e) => setForgotPasswordEmail(e.target.value)}
className="input-field"
/>
</div>

{forgotPasswordError && (
<div className="error-message">{forgotPasswordError}</div>
)}

{forgotPasswordMessage && (
<div className="success-message">{forgotPasswordMessage}</div>
)}

<button
type="submit"
className={`sign-in-btn ${!isForgotPasswordValid || isSubmittingReset ? 'disabled' : ''}`}
disabled={!isForgotPasswordValid || isSubmittingReset}
>
{isSubmittingReset ? 'Sending...' : 'Send Reset Link'}
</button>
</form>

<button type="button" className="return-to-signin" onClick={handleReturnToSignIn}>
Return to sign in
</button>
</>
)}
</div>
</div>
);
Expand Down