update to python fastpi
This commit is contained in:
BIN
Backend/src/services/__pycache__/auth_service.cpython-312.pyc
Normal file
BIN
Backend/src/services/__pycache__/auth_service.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Backend/src/services/__pycache__/room_service.cpython-312.pyc
Normal file
BIN
Backend/src/services/__pycache__/room_service.cpython-312.pyc
Normal file
Binary file not shown.
350
Backend/src/services/auth_service.py
Normal file
350
Backend/src/services/auth_service.py
Normal file
@@ -0,0 +1,350 @@
|
||||
from jose import jwt
|
||||
import bcrypt
|
||||
from datetime import datetime, timedelta
|
||||
import secrets
|
||||
import hashlib
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
|
||||
from ..models.user import User
|
||||
from ..models.refresh_token import RefreshToken
|
||||
from ..models.password_reset_token import PasswordResetToken
|
||||
from ..models.role import Role
|
||||
from ..utils.mailer import send_email
|
||||
import os
|
||||
|
||||
|
||||
class AuthService:
|
||||
def __init__(self):
|
||||
self.jwt_secret = os.getenv("JWT_SECRET")
|
||||
self.jwt_refresh_secret = os.getenv("JWT_REFRESH_SECRET")
|
||||
self.jwt_expires_in = os.getenv("JWT_EXPIRES_IN", "1h")
|
||||
self.jwt_refresh_expires_in = os.getenv("JWT_REFRESH_EXPIRES_IN", "7d")
|
||||
|
||||
def generate_tokens(self, user_id: int) -> dict:
|
||||
"""Generate JWT tokens"""
|
||||
access_token = jwt.encode(
|
||||
{"userId": user_id},
|
||||
self.jwt_secret,
|
||||
algorithm="HS256"
|
||||
)
|
||||
|
||||
refresh_token = jwt.encode(
|
||||
{"userId": user_id},
|
||||
self.jwt_refresh_secret,
|
||||
algorithm="HS256"
|
||||
)
|
||||
|
||||
return {"accessToken": access_token, "refreshToken": refresh_token}
|
||||
|
||||
def verify_access_token(self, token: str) -> dict:
|
||||
"""Verify JWT access token"""
|
||||
return jwt.decode(token, self.jwt_secret, algorithms=["HS256"])
|
||||
|
||||
def verify_refresh_token(self, token: str) -> dict:
|
||||
"""Verify JWT refresh token"""
|
||||
return jwt.decode(token, self.jwt_refresh_secret, algorithms=["HS256"])
|
||||
|
||||
def hash_password(self, password: str) -> str:
|
||||
"""Hash password using bcrypt"""
|
||||
# bcrypt has 72 byte limit, but it handles truncation automatically
|
||||
password_bytes = password.encode('utf-8')
|
||||
# Generate salt and hash password
|
||||
salt = bcrypt.gensalt()
|
||||
hashed = bcrypt.hashpw(password_bytes, salt)
|
||||
return hashed.decode('utf-8')
|
||||
|
||||
def verify_password(self, plain_password: str, hashed_password: str) -> bool:
|
||||
"""Verify password using bcrypt"""
|
||||
try:
|
||||
password_bytes = plain_password.encode('utf-8')
|
||||
hashed_bytes = hashed_password.encode('utf-8')
|
||||
return bcrypt.checkpw(password_bytes, hashed_bytes)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def format_user_response(self, user: User) -> dict:
|
||||
"""Format user response"""
|
||||
return {
|
||||
"id": user.id,
|
||||
"name": user.full_name,
|
||||
"email": user.email,
|
||||
"phone": user.phone,
|
||||
"role": user.role.name if user.role else "customer",
|
||||
"createdAt": user.created_at.isoformat() if user.created_at else None,
|
||||
"updatedAt": user.updated_at.isoformat() if user.updated_at else None,
|
||||
}
|
||||
|
||||
async def register(self, db: Session, name: str, email: str, password: str, phone: Optional[str] = None) -> dict:
|
||||
"""Register new user"""
|
||||
# Check if email exists
|
||||
existing_user = db.query(User).filter(User.email == email).first()
|
||||
if existing_user:
|
||||
raise ValueError("Email already registered")
|
||||
|
||||
# Hash password
|
||||
hashed_password = self.hash_password(password)
|
||||
|
||||
# Create user (default role_id = 3 for customer)
|
||||
user = User(
|
||||
full_name=name,
|
||||
email=email,
|
||||
password=hashed_password,
|
||||
phone=phone,
|
||||
role_id=3 # Customer role
|
||||
)
|
||||
db.add(user)
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
# Load role
|
||||
user.role = db.query(Role).filter(Role.id == user.role_id).first()
|
||||
|
||||
# Generate tokens
|
||||
tokens = self.generate_tokens(user.id)
|
||||
|
||||
# Save refresh token (expires in 7 days)
|
||||
expires_at = datetime.utcnow() + timedelta(days=7)
|
||||
refresh_token = RefreshToken(
|
||||
user_id=user.id,
|
||||
token=tokens["refreshToken"],
|
||||
expires_at=expires_at
|
||||
)
|
||||
db.add(refresh_token)
|
||||
db.commit()
|
||||
|
||||
# Send welcome email (non-blocking)
|
||||
try:
|
||||
client_url = os.getenv("CLIENT_URL", "http://localhost:5173")
|
||||
await send_email(
|
||||
to=user.email,
|
||||
subject="Welcome to Hotel Booking",
|
||||
html=f"""
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<h2 style="color: #4F46E5;">Welcome {user.full_name}!</h2>
|
||||
<p>Thank you for registering an account at <strong>Hotel Booking</strong>.</p>
|
||||
<p>Your account has been successfully created with email: <strong>{user.email}</strong></p>
|
||||
<div style="background-color: #F3F4F6; padding: 20px; border-radius: 8px; margin: 20px 0;">
|
||||
<p style="margin: 0;"><strong>You can:</strong></p>
|
||||
<ul style="margin-top: 10px;">
|
||||
<li>Search and book hotel rooms</li>
|
||||
<li>Manage your bookings</li>
|
||||
<li>Update your personal information</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p>
|
||||
<a href="{client_url}/login" style="background-color: #4F46E5; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">
|
||||
Login Now
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Failed to send welcome email: {e}")
|
||||
|
||||
return {
|
||||
"user": self.format_user_response(user),
|
||||
"token": tokens["accessToken"],
|
||||
"refreshToken": tokens["refreshToken"]
|
||||
}
|
||||
|
||||
async def login(self, db: Session, email: str, password: str, remember_me: bool = False) -> dict:
|
||||
"""Login user"""
|
||||
# Find user with role and password
|
||||
user = db.query(User).filter(User.email == email).first()
|
||||
if not user:
|
||||
raise ValueError("Invalid email or password")
|
||||
|
||||
# Load role
|
||||
user.role = db.query(Role).filter(Role.id == user.role_id).first()
|
||||
|
||||
# Check password
|
||||
if not self.verify_password(password, user.password):
|
||||
raise ValueError("Invalid email or password")
|
||||
|
||||
# Generate tokens
|
||||
tokens = self.generate_tokens(user.id)
|
||||
|
||||
# Calculate expiry based on remember_me
|
||||
expiry_days = 7 if remember_me else 1
|
||||
expires_at = datetime.utcnow() + timedelta(days=expiry_days)
|
||||
|
||||
# Save refresh token
|
||||
refresh_token = RefreshToken(
|
||||
user_id=user.id,
|
||||
token=tokens["refreshToken"],
|
||||
expires_at=expires_at
|
||||
)
|
||||
db.add(refresh_token)
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"user": self.format_user_response(user),
|
||||
"token": tokens["accessToken"],
|
||||
"refreshToken": tokens["refreshToken"]
|
||||
}
|
||||
|
||||
async def refresh_access_token(self, db: Session, refresh_token_str: str) -> dict:
|
||||
"""Refresh access token"""
|
||||
if not refresh_token_str:
|
||||
raise ValueError("Refresh token is required")
|
||||
|
||||
# Verify refresh token
|
||||
decoded = self.verify_refresh_token(refresh_token_str)
|
||||
|
||||
# Check if refresh token exists in database
|
||||
stored_token = db.query(RefreshToken).filter(
|
||||
RefreshToken.token == refresh_token_str,
|
||||
RefreshToken.user_id == decoded["userId"]
|
||||
).first()
|
||||
|
||||
if not stored_token:
|
||||
raise ValueError("Invalid refresh token")
|
||||
|
||||
# Check if token is expired
|
||||
if datetime.utcnow() > stored_token.expires_at:
|
||||
db.delete(stored_token)
|
||||
db.commit()
|
||||
raise ValueError("Refresh token expired")
|
||||
|
||||
# Generate new access token
|
||||
access_token = jwt.encode(
|
||||
{"userId": decoded["userId"]},
|
||||
self.jwt_secret,
|
||||
algorithm="HS256"
|
||||
)
|
||||
|
||||
return {"token": access_token}
|
||||
|
||||
async def logout(self, db: Session, refresh_token_str: str) -> bool:
|
||||
"""Logout user"""
|
||||
if refresh_token_str:
|
||||
db.query(RefreshToken).filter(RefreshToken.token == refresh_token_str).delete()
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
async def get_profile(self, db: Session, user_id: int) -> dict:
|
||||
"""Get user profile"""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise ValueError("User not found")
|
||||
|
||||
# Load role
|
||||
user.role = db.query(Role).filter(Role.id == user.role_id).first()
|
||||
|
||||
return self.format_user_response(user)
|
||||
|
||||
def generate_reset_token(self) -> tuple:
|
||||
"""Generate reset token"""
|
||||
reset_token = secrets.token_hex(32)
|
||||
hashed_token = hashlib.sha256(reset_token.encode()).hexdigest()
|
||||
return reset_token, hashed_token
|
||||
|
||||
async def forgot_password(self, db: Session, email: str) -> dict:
|
||||
"""Forgot Password - Send reset link"""
|
||||
# Find user by email
|
||||
user = db.query(User).filter(User.email == email).first()
|
||||
|
||||
# Always return success to prevent email enumeration
|
||||
if not user:
|
||||
return {
|
||||
"success": True,
|
||||
"message": "If email exists, reset link has been sent"
|
||||
}
|
||||
|
||||
# Generate reset token
|
||||
reset_token, hashed_token = self.generate_reset_token()
|
||||
|
||||
# Delete old tokens
|
||||
db.query(PasswordResetToken).filter(PasswordResetToken.user_id == user.id).delete()
|
||||
|
||||
# Save token (expires in 1 hour)
|
||||
expires_at = datetime.utcnow() + timedelta(hours=1)
|
||||
reset_token_obj = PasswordResetToken(
|
||||
user_id=user.id,
|
||||
token=hashed_token,
|
||||
expires_at=expires_at
|
||||
)
|
||||
db.add(reset_token_obj)
|
||||
db.commit()
|
||||
|
||||
# Build reset URL
|
||||
client_url = os.getenv("CLIENT_URL", "http://localhost:5173")
|
||||
reset_url = f"{client_url}/reset-password/{reset_token}"
|
||||
|
||||
# Try to send email
|
||||
try:
|
||||
await send_email(
|
||||
to=user.email,
|
||||
subject="Reset password - Hotel Booking",
|
||||
html=f"""
|
||||
<p>You (or someone) has requested to reset your password.</p>
|
||||
<p>Click the link below to reset your password (expires in 1 hour):</p>
|
||||
<p><a href="{reset_url}">{reset_url}</a></p>
|
||||
"""
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Failed to send reset email: {e}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Password reset link has been sent to your email"
|
||||
}
|
||||
|
||||
async def reset_password(self, db: Session, token: str, password: str) -> dict:
|
||||
"""Reset Password - Update password with token"""
|
||||
if not token or not password:
|
||||
raise ValueError("Token and password are required")
|
||||
|
||||
# Hash the token to compare
|
||||
hashed_token = hashlib.sha256(token.encode()).hexdigest()
|
||||
|
||||
# Find valid token
|
||||
reset_token = db.query(PasswordResetToken).filter(
|
||||
PasswordResetToken.token == hashed_token,
|
||||
PasswordResetToken.expires_at > datetime.utcnow(),
|
||||
PasswordResetToken.used == False
|
||||
).first()
|
||||
|
||||
if not reset_token:
|
||||
raise ValueError("Invalid or expired reset token")
|
||||
|
||||
# Find user
|
||||
user = db.query(User).filter(User.id == reset_token.user_id).first()
|
||||
if not user:
|
||||
raise ValueError("User not found")
|
||||
|
||||
# Check if new password matches old password
|
||||
if self.verify_password(password, user.password):
|
||||
raise ValueError("New password must be different from the old password")
|
||||
|
||||
# Hash new password
|
||||
hashed_password = self.hash_password(password)
|
||||
|
||||
# Update password
|
||||
user.password = hashed_password
|
||||
db.commit()
|
||||
|
||||
# Mark token as used
|
||||
reset_token.used = True
|
||||
db.commit()
|
||||
|
||||
# Send confirmation email (non-blocking)
|
||||
try:
|
||||
await send_email(
|
||||
to=user.email,
|
||||
subject="Password Changed",
|
||||
html=f"<p>The password for account {user.email} has been changed successfully.</p>"
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"Failed to send confirmation email: {e}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Password has been reset successfully"
|
||||
}
|
||||
|
||||
|
||||
auth_service = AuthService()
|
||||
|
||||
145
Backend/src/services/room_service.py
Normal file
145
Backend/src/services/room_service.py
Normal file
@@ -0,0 +1,145 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, and_, or_
|
||||
from typing import Optional, List, Dict
|
||||
from datetime import datetime
|
||||
import os
|
||||
|
||||
from ..models.room import Room, RoomStatus
|
||||
from ..models.room_type import RoomType
|
||||
from ..models.review import Review, ReviewStatus
|
||||
|
||||
|
||||
def normalize_images(images, base_url: str) -> List[str]:
|
||||
"""Normalize image paths to absolute URLs"""
|
||||
if not images:
|
||||
return []
|
||||
|
||||
imgs = images
|
||||
if isinstance(images, str):
|
||||
try:
|
||||
import json
|
||||
imgs = json.loads(images)
|
||||
except:
|
||||
imgs = [s.strip() for s in images.split(',') if s.strip()]
|
||||
|
||||
if not isinstance(imgs, list):
|
||||
return []
|
||||
|
||||
result = []
|
||||
for img in imgs:
|
||||
if not img:
|
||||
continue
|
||||
if img.startswith('http://') or img.startswith('https://'):
|
||||
result.append(img)
|
||||
else:
|
||||
path_part = img if img.startswith('/') else f"/{img}"
|
||||
result.append(f"{base_url}{path_part}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_base_url(request) -> str:
|
||||
"""Get base URL for image normalization"""
|
||||
return os.getenv("SERVER_URL") or f"http://{request.headers.get('host', 'localhost:3000')}"
|
||||
|
||||
|
||||
async def get_rooms_with_ratings(
|
||||
db: Session,
|
||||
rooms: List[Room],
|
||||
base_url: str
|
||||
) -> List[Dict]:
|
||||
"""Get rooms with calculated ratings"""
|
||||
result = []
|
||||
|
||||
for room in rooms:
|
||||
# Get review stats
|
||||
review_stats = db.query(
|
||||
func.avg(Review.rating).label('average_rating'),
|
||||
func.count(Review.id).label('total_reviews')
|
||||
).filter(
|
||||
and_(
|
||||
Review.room_id == room.id,
|
||||
Review.status == ReviewStatus.approved
|
||||
)
|
||||
).first()
|
||||
|
||||
room_dict = {
|
||||
"id": room.id,
|
||||
"room_type_id": room.room_type_id,
|
||||
"room_number": room.room_number,
|
||||
"floor": room.floor,
|
||||
"status": room.status.value if isinstance(room.status, RoomStatus) else room.status,
|
||||
"price": float(room.price) if room.price else 0.0,
|
||||
"featured": room.featured,
|
||||
"description": room.description,
|
||||
"amenities": room.amenities,
|
||||
"created_at": room.created_at.isoformat() if room.created_at else None,
|
||||
"updated_at": room.updated_at.isoformat() if room.updated_at else None,
|
||||
"average_rating": round(float(review_stats.average_rating or 0), 1) if review_stats and review_stats.average_rating else None,
|
||||
"total_reviews": review_stats.total_reviews or 0 if review_stats else 0,
|
||||
}
|
||||
|
||||
# Normalize images
|
||||
try:
|
||||
room_dict["images"] = normalize_images(room.images, base_url)
|
||||
except:
|
||||
room_dict["images"] = []
|
||||
|
||||
# Add room type info
|
||||
if room.room_type:
|
||||
room_dict["room_type"] = {
|
||||
"id": room.room_type.id,
|
||||
"name": room.room_type.name,
|
||||
"description": room.room_type.description,
|
||||
"base_price": float(room.room_type.base_price) if room.room_type.base_price else 0.0,
|
||||
"capacity": room.room_type.capacity,
|
||||
"amenities": room.room_type.amenities,
|
||||
"images": [] # RoomType doesn't have images column in DB
|
||||
}
|
||||
|
||||
result.append(room_dict)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def get_amenities_list(db: Session) -> List[str]:
|
||||
"""Get all unique amenities from room types and rooms"""
|
||||
all_amenities = []
|
||||
|
||||
# Get from room types
|
||||
room_types = db.query(RoomType.amenities).all()
|
||||
for rt in room_types:
|
||||
if rt.amenities:
|
||||
if isinstance(rt.amenities, list):
|
||||
all_amenities.extend([str(a).strip() for a in rt.amenities])
|
||||
elif isinstance(rt.amenities, str):
|
||||
try:
|
||||
import json
|
||||
parsed = json.loads(rt.amenities)
|
||||
if isinstance(parsed, list):
|
||||
all_amenities.extend([str(a).strip() for a in parsed])
|
||||
else:
|
||||
all_amenities.extend([s.strip() for s in rt.amenities.split(',')])
|
||||
except:
|
||||
all_amenities.extend([s.strip() for s in rt.amenities.split(',')])
|
||||
|
||||
# Get from rooms
|
||||
rooms = db.query(Room.amenities).all()
|
||||
for r in rooms:
|
||||
if r.amenities:
|
||||
if isinstance(r.amenities, list):
|
||||
all_amenities.extend([str(a).strip() for a in r.amenities])
|
||||
elif isinstance(r.amenities, str):
|
||||
try:
|
||||
import json
|
||||
parsed = json.loads(r.amenities)
|
||||
if isinstance(parsed, list):
|
||||
all_amenities.extend([str(a).strip() for a in parsed])
|
||||
else:
|
||||
all_amenities.extend([s.strip() for s in r.amenities.split(',')])
|
||||
except:
|
||||
all_amenities.extend([s.strip() for s in r.amenities.split(',')])
|
||||
|
||||
# Return unique, non-empty values
|
||||
return sorted(list(set([a for a in all_amenities if a])))
|
||||
|
||||
Reference in New Issue
Block a user