update
This commit is contained in:
@@ -5,19 +5,29 @@ import secrets
|
||||
import hashlib
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
import logging
|
||||
|
||||
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
|
||||
from ..utils.email_templates import (
|
||||
welcome_email_template,
|
||||
password_reset_email_template,
|
||||
password_changed_email_template
|
||||
)
|
||||
from ..config.settings import settings
|
||||
import os
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AuthService:
|
||||
def __init__(self):
|
||||
self.jwt_secret = os.getenv("JWT_SECRET")
|
||||
self.jwt_refresh_secret = os.getenv("JWT_REFRESH_SECRET")
|
||||
# Use settings, fallback to env vars, then to defaults for development
|
||||
self.jwt_secret = getattr(settings, 'JWT_SECRET', None) or os.getenv("JWT_SECRET", "dev-secret-key-change-in-production-12345")
|
||||
self.jwt_refresh_secret = os.getenv("JWT_REFRESH_SECRET") or (self.jwt_secret + "-refresh")
|
||||
self.jwt_expires_in = os.getenv("JWT_EXPIRES_IN", "1h")
|
||||
self.jwt_refresh_expires_in = os.getenv("JWT_REFRESH_EXPIRES_IN", "7d")
|
||||
|
||||
@@ -70,6 +80,7 @@ class AuthService:
|
||||
"name": user.full_name,
|
||||
"email": user.email,
|
||||
"phone": user.phone,
|
||||
"avatar": user.avatar,
|
||||
"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,
|
||||
@@ -115,33 +126,16 @@ class AuthService:
|
||||
|
||||
# Send welcome email (non-blocking)
|
||||
try:
|
||||
client_url = os.getenv("CLIENT_URL", "http://localhost:5173")
|
||||
client_url = settings.CLIENT_URL or os.getenv("CLIENT_URL", "http://localhost:5173")
|
||||
email_html = welcome_email_template(user.full_name, user.email, client_url)
|
||||
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>
|
||||
"""
|
||||
html=email_html
|
||||
)
|
||||
logger.info(f"Welcome email sent successfully to {user.email}")
|
||||
except Exception as e:
|
||||
print(f"Failed to send welcome email: {e}")
|
||||
logger.error(f"Failed to send welcome email to {user.email}: {type(e).__name__}: {str(e)}", exc_info=True)
|
||||
|
||||
return {
|
||||
"user": self.format_user_response(user),
|
||||
@@ -170,14 +164,42 @@ class AuthService:
|
||||
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()
|
||||
# Delete old/expired refresh tokens for this user to prevent duplicates
|
||||
# This ensures we don't have multiple active tokens and prevents unique constraint violations
|
||||
try:
|
||||
db.query(RefreshToken).filter(
|
||||
RefreshToken.user_id == user.id
|
||||
).delete()
|
||||
db.flush() # Flush to ensure deletion happens before insert
|
||||
|
||||
# Save new refresh token
|
||||
refresh_token = RefreshToken(
|
||||
user_id=user.id,
|
||||
token=tokens["refreshToken"],
|
||||
expires_at=expires_at
|
||||
)
|
||||
db.add(refresh_token)
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error saving refresh token for user {user.id}: {str(e)}", exc_info=True)
|
||||
# If there's still a duplicate, try to delete and retry once
|
||||
try:
|
||||
db.query(RefreshToken).filter(
|
||||
RefreshToken.token == tokens["refreshToken"]
|
||||
).delete()
|
||||
db.flush()
|
||||
refresh_token = RefreshToken(
|
||||
user_id=user.id,
|
||||
token=tokens["refreshToken"],
|
||||
expires_at=expires_at
|
||||
)
|
||||
db.add(refresh_token)
|
||||
db.commit()
|
||||
except Exception as retry_error:
|
||||
db.rollback()
|
||||
logger.error(f"Retry failed for refresh token: {str(retry_error)}", exc_info=True)
|
||||
raise ValueError("Failed to create session. Please try again.")
|
||||
|
||||
return {
|
||||
"user": self.format_user_response(user),
|
||||
@@ -235,6 +257,53 @@ class AuthService:
|
||||
|
||||
return self.format_user_response(user)
|
||||
|
||||
async def update_profile(
|
||||
self,
|
||||
db: Session,
|
||||
user_id: int,
|
||||
full_name: Optional[str] = None,
|
||||
email: Optional[str] = None,
|
||||
phone_number: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
current_password: Optional[str] = None
|
||||
) -> dict:
|
||||
"""Update user profile"""
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
raise ValueError("User not found")
|
||||
|
||||
# If password is being changed, verify current password
|
||||
if password:
|
||||
if not current_password:
|
||||
raise ValueError("Current password is required to change password")
|
||||
if not self.verify_password(current_password, user.password):
|
||||
raise ValueError("Current password is incorrect")
|
||||
# Hash new password
|
||||
user.password = self.hash_password(password)
|
||||
|
||||
# Update other fields
|
||||
if full_name is not None:
|
||||
user.full_name = full_name
|
||||
if email is not None:
|
||||
# Check if email is already taken by another user
|
||||
existing_user = db.query(User).filter(
|
||||
User.email == email,
|
||||
User.id != user_id
|
||||
).first()
|
||||
if existing_user:
|
||||
raise ValueError("Email already registered")
|
||||
user.email = email
|
||||
if phone_number is not None:
|
||||
user.phone = phone_number
|
||||
|
||||
db.commit()
|
||||
db.refresh(user)
|
||||
|
||||
# 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)
|
||||
@@ -270,22 +339,41 @@ class AuthService:
|
||||
db.commit()
|
||||
|
||||
# Build reset URL
|
||||
client_url = os.getenv("CLIENT_URL", "http://localhost:5173")
|
||||
client_url = settings.CLIENT_URL or os.getenv("CLIENT_URL", "http://localhost:5173")
|
||||
reset_url = f"{client_url}/reset-password/{reset_token}"
|
||||
|
||||
# Try to send email
|
||||
try:
|
||||
logger.info(f"Attempting to send password reset email to {user.email}")
|
||||
logger.info(f"Reset URL: {reset_url}")
|
||||
email_html = password_reset_email_template(reset_url)
|
||||
|
||||
# Create plain text version for better email deliverability
|
||||
plain_text = f"""
|
||||
Password Reset Request
|
||||
|
||||
You (or someone) has requested to reset your password for your Hotel Booking account.
|
||||
|
||||
Click the link below to reset your password. This link will expire in 1 hour:
|
||||
|
||||
{reset_url}
|
||||
|
||||
If you did not request this, please ignore this email.
|
||||
|
||||
Best regards,
|
||||
Hotel Booking Team
|
||||
""".strip()
|
||||
|
||||
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>
|
||||
"""
|
||||
html=email_html,
|
||||
text=plain_text
|
||||
)
|
||||
logger.info(f"Password reset email sent successfully to {user.email} with reset URL: {reset_url}")
|
||||
except Exception as e:
|
||||
print(f"Failed to send reset email: {e}")
|
||||
logger.error(f"Failed to send password reset email to {user.email}: {type(e).__name__}: {str(e)}", exc_info=True)
|
||||
# Still return success to prevent email enumeration, but log the error
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
@@ -332,13 +420,16 @@ class AuthService:
|
||||
|
||||
# Send confirmation email (non-blocking)
|
||||
try:
|
||||
logger.info(f"Attempting to send password changed confirmation email to {user.email}")
|
||||
email_html = password_changed_email_template(user.email)
|
||||
await send_email(
|
||||
to=user.email,
|
||||
subject="Password Changed",
|
||||
html=f"<p>The password for account {user.email} has been changed successfully.</p>"
|
||||
html=email_html
|
||||
)
|
||||
logger.info(f"Password changed confirmation email sent successfully to {user.email}")
|
||||
except Exception as e:
|
||||
print(f"Failed to send confirmation email: {e}")
|
||||
logger.error(f"Failed to send password changed confirmation email to {user.email}: {type(e).__name__}: {str(e)}", exc_info=True)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
|
||||
Reference in New Issue
Block a user