update to python fastpi

This commit is contained in:
Iliyan Angelov
2025-11-16 15:59:05 +02:00
parent 93d4c1df80
commit 98ccd5b6ff
4464 changed files with 773233 additions and 13740 deletions

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