update
This commit is contained in:
Binary file not shown.
Binary file not shown.
82
Backend/src/services/audit_service.py
Normal file
82
Backend/src/services/audit_service.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
Audit logging service for tracking important actions
|
||||
"""
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
from ..models.audit_log import AuditLog
|
||||
from ..config.logging_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AuditService:
|
||||
"""Service for creating audit log entries"""
|
||||
|
||||
@staticmethod
|
||||
async def log_action(
|
||||
db: Session,
|
||||
action: str,
|
||||
resource_type: str,
|
||||
user_id: Optional[int] = None,
|
||||
resource_id: Optional[int] = None,
|
||||
ip_address: Optional[str] = None,
|
||||
user_agent: Optional[str] = None,
|
||||
request_id: Optional[str] = None,
|
||||
details: Optional[Dict[str, Any]] = None,
|
||||
status: str = "success",
|
||||
error_message: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
Create an audit log entry
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
action: Action performed (e.g., "user.created", "booking.cancelled")
|
||||
resource_type: Type of resource (e.g., "user", "booking")
|
||||
user_id: ID of user who performed the action
|
||||
resource_id: ID of the resource affected
|
||||
ip_address: IP address of the request
|
||||
user_agent: User agent string
|
||||
request_id: Request ID for tracing
|
||||
details: Additional context as dictionary
|
||||
status: Status of the action (success, failed, error)
|
||||
error_message: Error message if action failed
|
||||
"""
|
||||
try:
|
||||
audit_log = AuditLog(
|
||||
user_id=user_id,
|
||||
action=action,
|
||||
resource_type=resource_type,
|
||||
resource_id=resource_id,
|
||||
ip_address=ip_address,
|
||||
user_agent=user_agent,
|
||||
request_id=request_id,
|
||||
details=details,
|
||||
status=status,
|
||||
error_message=error_message
|
||||
)
|
||||
|
||||
db.add(audit_log)
|
||||
db.commit()
|
||||
|
||||
logger.info(
|
||||
f"Audit log created: {action} on {resource_type}",
|
||||
extra={
|
||||
"action": action,
|
||||
"resource_type": resource_type,
|
||||
"resource_id": resource_id,
|
||||
"user_id": user_id,
|
||||
"status": status,
|
||||
"request_id": request_id
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create audit log: {str(e)}", exc_info=True)
|
||||
db.rollback()
|
||||
# Don't raise exception - audit logging failures shouldn't break the app
|
||||
|
||||
|
||||
# Global audit service instance
|
||||
audit_service = AuditService()
|
||||
|
||||
@@ -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,
|
||||
|
||||
98
Backend/src/services/privacy_admin_service.py
Normal file
98
Backend/src/services/privacy_admin_service.py
Normal file
@@ -0,0 +1,98 @@
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..models.cookie_policy import CookiePolicy
|
||||
from ..models.cookie_integration_config import CookieIntegrationConfig
|
||||
from ..models.user import User
|
||||
from ..schemas.admin_privacy import (
|
||||
CookieIntegrationSettings,
|
||||
CookiePolicySettings,
|
||||
PublicPrivacyConfig,
|
||||
)
|
||||
|
||||
|
||||
class PrivacyAdminService:
|
||||
"""
|
||||
Service layer for admin-controlled cookie policy and integrations.
|
||||
"""
|
||||
|
||||
# Policy
|
||||
@staticmethod
|
||||
def get_or_create_policy(db: Session) -> CookiePolicy:
|
||||
policy = db.query(CookiePolicy).first()
|
||||
if policy:
|
||||
return policy
|
||||
|
||||
policy = CookiePolicy()
|
||||
db.add(policy)
|
||||
db.commit()
|
||||
db.refresh(policy)
|
||||
return policy
|
||||
|
||||
@staticmethod
|
||||
def get_policy_settings(db: Session) -> CookiePolicySettings:
|
||||
policy = PrivacyAdminService.get_or_create_policy(db)
|
||||
return CookiePolicySettings(
|
||||
analytics_enabled=policy.analytics_enabled,
|
||||
marketing_enabled=policy.marketing_enabled,
|
||||
preferences_enabled=policy.preferences_enabled,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def update_policy(
|
||||
db: Session, settings: CookiePolicySettings, updated_by: User | None
|
||||
) -> CookiePolicy:
|
||||
policy = PrivacyAdminService.get_or_create_policy(db)
|
||||
policy.analytics_enabled = settings.analytics_enabled
|
||||
policy.marketing_enabled = settings.marketing_enabled
|
||||
policy.preferences_enabled = settings.preferences_enabled
|
||||
if updated_by:
|
||||
policy.updated_by_id = updated_by.id
|
||||
db.add(policy)
|
||||
db.commit()
|
||||
db.refresh(policy)
|
||||
return policy
|
||||
|
||||
# Integrations
|
||||
@staticmethod
|
||||
def get_or_create_integrations(db: Session) -> CookieIntegrationConfig:
|
||||
config = db.query(CookieIntegrationConfig).first()
|
||||
if config:
|
||||
return config
|
||||
config = CookieIntegrationConfig()
|
||||
db.add(config)
|
||||
db.commit()
|
||||
db.refresh(config)
|
||||
return config
|
||||
|
||||
@staticmethod
|
||||
def get_integration_settings(db: Session) -> CookieIntegrationSettings:
|
||||
cfg = PrivacyAdminService.get_or_create_integrations(db)
|
||||
return CookieIntegrationSettings(
|
||||
ga_measurement_id=cfg.ga_measurement_id,
|
||||
fb_pixel_id=cfg.fb_pixel_id,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def update_integrations(
|
||||
db: Session, settings: CookieIntegrationSettings, updated_by: User | None
|
||||
) -> CookieIntegrationConfig:
|
||||
cfg = PrivacyAdminService.get_or_create_integrations(db)
|
||||
cfg.ga_measurement_id = settings.ga_measurement_id
|
||||
cfg.fb_pixel_id = settings.fb_pixel_id
|
||||
if updated_by:
|
||||
cfg.updated_by_id = updated_by.id
|
||||
db.add(cfg)
|
||||
db.commit()
|
||||
db.refresh(cfg)
|
||||
return cfg
|
||||
|
||||
@staticmethod
|
||||
def get_public_privacy_config(db: Session) -> PublicPrivacyConfig:
|
||||
policy = PrivacyAdminService.get_policy_settings(db)
|
||||
integrations = PrivacyAdminService.get_integration_settings(db)
|
||||
return PublicPrivacyConfig(policy=policy, integrations=integrations)
|
||||
|
||||
|
||||
privacy_admin_service = PrivacyAdminService()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user