update
This commit is contained in:
137
Backend/src/shared/config/settings.py
Normal file
137
Backend/src/shared/config/settings.py
Normal file
@@ -0,0 +1,137 @@
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from pydantic import Field
|
||||
from typing import List
|
||||
import os
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8', case_sensitive=False, extra='ignore')
|
||||
APP_NAME: str = Field(default='Hotel Booking API', description='Application name')
|
||||
APP_VERSION: str = Field(default='1.0.0', description='Application version')
|
||||
ENVIRONMENT: str = Field(default='development', description='Environment: development, staging, production')
|
||||
DEBUG: bool = Field(default=False, description='Debug mode')
|
||||
API_V1_PREFIX: str = Field(default='/api/v1', description='API v1 prefix')
|
||||
HOST: str = Field(default='0.0.0.0', description='Server host')
|
||||
PORT: int = Field(default=8000, description='Server port')
|
||||
DB_USER: str = Field(default='root', description='Database user')
|
||||
DB_PASS: str = Field(default='', description='Database password')
|
||||
DB_NAME: str = Field(default='hotel_db', description='Database name')
|
||||
DB_HOST: str = Field(default='localhost', description='Database host')
|
||||
DB_PORT: str = Field(default='3306', description='Database port')
|
||||
JWT_SECRET: str = Field(default='dev-secret-key-change-in-production-12345', description='JWT secret key')
|
||||
JWT_ALGORITHM: str = Field(default='HS256', description='JWT algorithm')
|
||||
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=30, description='JWT access token expiration in minutes')
|
||||
JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = Field(default=3, description='JWT refresh token expiration in days (reduced from 7 for better security)')
|
||||
MAX_LOGIN_ATTEMPTS: int = Field(default=5, description='Maximum failed login attempts before account lockout')
|
||||
ACCOUNT_LOCKOUT_DURATION_MINUTES: int = Field(default=30, description='Account lockout duration in minutes after max failed attempts')
|
||||
ENCRYPTION_KEY: str = Field(default='', description='Base64-encoded encryption key for data encryption at rest')
|
||||
CLIENT_URL: str = Field(default='http://localhost:5173', description='Frontend client URL')
|
||||
CORS_ORIGINS: List[str] = Field(default_factory=lambda: ['http://localhost:5173', 'http://localhost:3000', 'http://127.0.0.1:5173'], description='Allowed CORS origins')
|
||||
RATE_LIMIT_ENABLED: bool = Field(default=True, description='Enable rate limiting')
|
||||
RATE_LIMIT_PER_MINUTE: int = Field(default=60, description='Requests per minute per IP')
|
||||
CSRF_PROTECTION_ENABLED: bool = Field(default=True, description='Enable CSRF protection')
|
||||
HSTS_PRELOAD_ENABLED: bool = Field(default=False, description='Enable HSTS preload directive (requires domain submission to hstspreload.org)')
|
||||
LOG_LEVEL: str = Field(default='INFO', description='Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL')
|
||||
LOG_FILE: str = Field(default='logs/app.log', description='Log file path')
|
||||
LOG_MAX_BYTES: int = Field(default=10485760, description='Max log file size (10MB)')
|
||||
LOG_BACKUP_COUNT: int = Field(default=5, description='Number of backup log files')
|
||||
SMTP_HOST: str = Field(default='smtp.gmail.com', description='SMTP host')
|
||||
SMTP_PORT: int = Field(default=587, description='SMTP port')
|
||||
SMTP_USER: str = Field(default='', description='SMTP username')
|
||||
SMTP_PASSWORD: str = Field(default='', description='SMTP password')
|
||||
SMTP_FROM_EMAIL: str = Field(default='', description='From email address')
|
||||
SMTP_FROM_NAME: str = Field(default='Hotel Booking', description='From name')
|
||||
UPLOAD_DIR: str = Field(default='uploads', description='Upload directory')
|
||||
MAX_UPLOAD_SIZE: int = Field(default=5242880, description='Max upload size in bytes (5MB)')
|
||||
MAX_REQUEST_BODY_SIZE: int = Field(default=10485760, description='Max request body size in bytes (10MB)')
|
||||
ALLOWED_EXTENSIONS: List[str] = Field(default_factory=lambda: ['jpg', 'jpeg', 'png', 'gif', 'webp'], description='Allowed file extensions')
|
||||
REDIS_ENABLED: bool = Field(default=False, description='Enable Redis caching')
|
||||
REDIS_HOST: str = Field(default='localhost', description='Redis host')
|
||||
REDIS_PORT: int = Field(default=6379, description='Redis port')
|
||||
REDIS_DB: int = Field(default=0, description='Redis database number')
|
||||
REDIS_PASSWORD: str = Field(default='', description='Redis password')
|
||||
REQUEST_TIMEOUT: int = Field(default=30, description='Request timeout in seconds')
|
||||
HEALTH_CHECK_INTERVAL: int = Field(default=30, description='Health check interval in seconds')
|
||||
STRIPE_SECRET_KEY: str = Field(default='', description='Stripe secret key')
|
||||
STRIPE_PUBLISHABLE_KEY: str = Field(default='', description='Stripe publishable key')
|
||||
STRIPE_WEBHOOK_SECRET: str = Field(default='', description='Stripe webhook secret')
|
||||
PAYPAL_CLIENT_ID: str = Field(default='', description='PayPal client ID')
|
||||
PAYPAL_CLIENT_SECRET: str = Field(default='', description='PayPal client secret')
|
||||
PAYPAL_MODE: str = Field(default='sandbox', description='PayPal mode: sandbox or live')
|
||||
BORICA_TERMINAL_ID: str = Field(default='', description='Borica Terminal ID')
|
||||
BORICA_MERCHANT_ID: str = Field(default='', description='Borica Merchant ID')
|
||||
BORICA_PRIVATE_KEY_PATH: str = Field(default='', description='Borica private key file path')
|
||||
BORICA_CERTIFICATE_PATH: str = Field(default='', description='Borica certificate file path')
|
||||
BORICA_GATEWAY_URL: str = Field(default='https://3dsgate-dev.borica.bg/cgi-bin/cgi_link', description='Borica gateway URL (test or production)')
|
||||
BORICA_MODE: str = Field(default='test', description='Borica mode: test or production')
|
||||
|
||||
@property
|
||||
def database_url(self) -> str:
|
||||
"""Generate database URL with proper credential escaping to prevent injection."""
|
||||
from urllib.parse import quote_plus
|
||||
# Properly escape credentials to handle special characters
|
||||
user = quote_plus(self.DB_USER)
|
||||
password = quote_plus(self.DB_PASS)
|
||||
host = quote_plus(self.DB_HOST)
|
||||
port = str(self.DB_PORT)
|
||||
name = quote_plus(self.DB_NAME)
|
||||
return f'mysql+pymysql://{user}:{password}@{host}:{port}/{name}'
|
||||
|
||||
@property
|
||||
def is_production(self) -> bool:
|
||||
return self.ENVIRONMENT.lower() == 'production'
|
||||
|
||||
@property
|
||||
def is_development(self) -> bool:
|
||||
return self.ENVIRONMENT.lower() == 'development'
|
||||
|
||||
@property
|
||||
def redis_url(self) -> str:
|
||||
if self.REDIS_PASSWORD:
|
||||
return f'redis://:{self.REDIS_PASSWORD}@{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}'
|
||||
return f'redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}'
|
||||
|
||||
IP_WHITELIST_ENABLED: bool = Field(default=False, description='Enable IP whitelisting for admin endpoints')
|
||||
ADMIN_IP_WHITELIST: List[str] = Field(default_factory=list, description='List of allowed IP addresses/CIDR ranges for admin endpoints')
|
||||
|
||||
def validate_encryption_key(self) -> None:
|
||||
"""
|
||||
Validate encryption key is properly configured.
|
||||
Raises ValueError if key is missing or invalid in production.
|
||||
"""
|
||||
if not self.ENCRYPTION_KEY:
|
||||
if self.is_production:
|
||||
raise ValueError(
|
||||
'CRITICAL: ENCRYPTION_KEY is not configured in production. '
|
||||
'Please set ENCRYPTION_KEY environment variable to a base64-encoded 32-byte key.'
|
||||
)
|
||||
else:
|
||||
# In development, warn but don't fail
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning(
|
||||
'ENCRYPTION_KEY is not configured. Encryption operations may fail. '
|
||||
'Please set ENCRYPTION_KEY environment variable.'
|
||||
)
|
||||
return
|
||||
|
||||
# Validate base64 encoding and key length (32 bytes = 44 base64 chars)
|
||||
try:
|
||||
import base64
|
||||
decoded = base64.b64decode(self.ENCRYPTION_KEY)
|
||||
if len(decoded) != 32:
|
||||
raise ValueError(
|
||||
f'ENCRYPTION_KEY must be a base64-encoded 32-byte key. '
|
||||
f'Received {len(decoded)} bytes after decoding.'
|
||||
)
|
||||
except Exception as e:
|
||||
if self.is_production:
|
||||
raise ValueError(
|
||||
f'Invalid ENCRYPTION_KEY format: {str(e)}. '
|
||||
'Must be a valid base64-encoded 32-byte key.'
|
||||
)
|
||||
else:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning(f'Invalid ENCRYPTION_KEY format: {str(e)}')
|
||||
|
||||
settings = Settings()
|
||||
Reference in New Issue
Block a user