Initial commit: Ebook Translation System with Docker setup
This commit is contained in:
30
ebook_backend&admin_panel/admin-backend/utils/auth.py
Normal file
30
ebook_backend&admin_panel/admin-backend/utils/auth.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import declarative_base
|
||||
from passlib.context import CryptContext
|
||||
|
||||
# Load environment variables
|
||||
load_dotenv()
|
||||
|
||||
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://postgres:postgres@localhost:5432/postgres")
|
||||
|
||||
engine = create_engine(DATABASE_URL)
|
||||
SessionLocal = sessionmaker(bind=engine)
|
||||
Base = declarative_base()
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def hash_password(pw: str) -> str:
|
||||
return pwd_context.hash(pw)
|
||||
|
||||
def verify_password(pw: str, hashed: str) -> bool:
|
||||
return pwd_context.verify(pw, hashed)
|
||||
@@ -0,0 +1,8 @@
|
||||
import random
|
||||
import string
|
||||
|
||||
# def generate_coupon(length: int = 6) -> str:
|
||||
# return ''.join(random.choices(string.ascii_uppercase + string.digits, k=length))
|
||||
|
||||
def generate_coupon():
|
||||
return ''.join(random.choices(string.ascii_uppercase + string.digits, k=10))
|
||||
211
ebook_backend&admin_panel/admin-backend/utils/exceptions.py
Normal file
211
ebook_backend&admin_panel/admin-backend/utils/exceptions.py
Normal file
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
Custom exceptions for the Ebook Coupon Management System
|
||||
Provides structured error handling with proper error codes and messages.
|
||||
"""
|
||||
from typing import Dict, Any, Optional
|
||||
from fastapi import HTTPException
|
||||
|
||||
|
||||
class APIException(HTTPException):
|
||||
"""Base API exception with structured error information"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
status_code: int,
|
||||
detail: str,
|
||||
error_code: str,
|
||||
extra_data: Optional[Dict[str, Any]] = None
|
||||
):
|
||||
super().__init__(status_code=status_code, detail=detail)
|
||||
self.error_code = error_code
|
||||
self.extra_data = extra_data or {}
|
||||
|
||||
|
||||
class AuthenticationError(APIException):
|
||||
"""Authentication related errors"""
|
||||
|
||||
def __init__(self, detail: str = "Authentication failed"):
|
||||
super().__init__(
|
||||
status_code=401,
|
||||
detail=detail,
|
||||
error_code="AUTHENTICATION_ERROR"
|
||||
)
|
||||
|
||||
|
||||
class AuthorizationError(APIException):
|
||||
"""Authorization related errors"""
|
||||
|
||||
def __init__(self, detail: str = "Access denied"):
|
||||
super().__init__(
|
||||
status_code=403,
|
||||
detail=detail,
|
||||
error_code="AUTHORIZATION_ERROR"
|
||||
)
|
||||
|
||||
|
||||
class NotFoundError(APIException):
|
||||
"""Resource not found errors"""
|
||||
|
||||
def __init__(self, resource: str, detail: Optional[str] = None):
|
||||
if detail is None:
|
||||
detail = f"{resource} not found"
|
||||
super().__init__(
|
||||
status_code=404,
|
||||
detail=detail,
|
||||
error_code="NOT_FOUND_ERROR"
|
||||
)
|
||||
|
||||
|
||||
class ValidationError(APIException):
|
||||
"""Validation related errors"""
|
||||
|
||||
def __init__(self, detail: str, field: Optional[str] = None):
|
||||
if field:
|
||||
detail = f"Validation error in field '{field}': {detail}"
|
||||
super().__init__(
|
||||
status_code=422,
|
||||
detail=detail,
|
||||
error_code="VALIDATION_ERROR"
|
||||
)
|
||||
|
||||
|
||||
class ConflictError(APIException):
|
||||
"""Resource conflict errors"""
|
||||
|
||||
def __init__(self, detail: str):
|
||||
super().__init__(
|
||||
status_code=409,
|
||||
detail=detail,
|
||||
error_code="CONFLICT_ERROR"
|
||||
)
|
||||
|
||||
|
||||
class RateLimitError(APIException):
|
||||
"""Rate limiting errors"""
|
||||
|
||||
def __init__(self, detail: str = "Rate limit exceeded"):
|
||||
super().__init__(
|
||||
status_code=429,
|
||||
detail=detail,
|
||||
error_code="RATE_LIMIT_ERROR"
|
||||
)
|
||||
|
||||
|
||||
class DatabaseError(APIException):
|
||||
"""Database related errors"""
|
||||
|
||||
def __init__(self, detail: str = "Database operation failed"):
|
||||
super().__init__(
|
||||
status_code=500,
|
||||
detail=detail,
|
||||
error_code="DATABASE_ERROR"
|
||||
)
|
||||
|
||||
|
||||
class FileUploadError(APIException):
|
||||
"""File upload related errors"""
|
||||
|
||||
def __init__(self, detail: str):
|
||||
super().__init__(
|
||||
status_code=400,
|
||||
detail=detail,
|
||||
error_code="FILE_UPLOAD_ERROR"
|
||||
)
|
||||
|
||||
|
||||
class CouponError(APIException):
|
||||
"""Coupon related errors"""
|
||||
|
||||
def __init__(self, detail: str, error_code: str = "COUPON_ERROR"):
|
||||
super().__init__(
|
||||
status_code=400,
|
||||
detail=detail,
|
||||
error_code=error_code
|
||||
)
|
||||
|
||||
|
||||
def handle_api_exception(exc: APIException, path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Handle API exception and return structured error response
|
||||
|
||||
Args:
|
||||
exc: API exception instance
|
||||
path: Request path
|
||||
|
||||
Returns:
|
||||
Structured error response
|
||||
"""
|
||||
return {
|
||||
"success": False,
|
||||
"error": exc.detail,
|
||||
"error_code": exc.error_code,
|
||||
"timestamp": None, # Will be set by exception handler
|
||||
"path": path,
|
||||
**exc.extra_data
|
||||
}
|
||||
|
||||
|
||||
# Coupon specific exceptions
|
||||
class CouponNotFoundError(NotFoundError):
|
||||
"""Coupon not found error"""
|
||||
|
||||
def __init__(self, code: str):
|
||||
super().__init__("coupon", f"Coupon code '{code}' not found")
|
||||
|
||||
|
||||
class CouponAlreadyUsedError(CouponError):
|
||||
"""Coupon already used error"""
|
||||
|
||||
def __init__(self, code: str):
|
||||
super().__init__(
|
||||
f"Coupon code '{code}' has already been used",
|
||||
"COUPON_ALREADY_USED"
|
||||
)
|
||||
|
||||
|
||||
class CouponBlockedError(CouponError):
|
||||
"""Coupon blocked error"""
|
||||
|
||||
def __init__(self, code: str, remaining_minutes: int):
|
||||
super().__init__(
|
||||
f"Coupon code '{code}' is blocked. Try again in {remaining_minutes} minutes",
|
||||
"COUPON_BLOCKED"
|
||||
)
|
||||
|
||||
|
||||
class CouponLimitExceededError(CouponError):
|
||||
"""Coupon usage limit exceeded error"""
|
||||
|
||||
def __init__(self, code: str, limit: int):
|
||||
super().__init__(
|
||||
f"Coupon code '{code}' usage limit ({limit}) exceeded",
|
||||
"COUPON_LIMIT_EXCEEDED"
|
||||
)
|
||||
|
||||
|
||||
# File upload specific exceptions
|
||||
class FileTypeError(FileUploadError):
|
||||
"""Invalid file type error"""
|
||||
|
||||
def __init__(self, allowed_types: list):
|
||||
super().__init__(
|
||||
f"Invalid file type. Allowed types: {', '.join(allowed_types)}"
|
||||
)
|
||||
|
||||
|
||||
class FileSizeError(FileUploadError):
|
||||
"""File too large error"""
|
||||
|
||||
def __init__(self, max_size_mb: int):
|
||||
super().__init__(
|
||||
f"File too large. Maximum size: {max_size_mb}MB"
|
||||
)
|
||||
|
||||
|
||||
class FileExistsError(FileUploadError):
|
||||
"""File already exists error"""
|
||||
|
||||
def __init__(self, filename: str):
|
||||
super().__init__(
|
||||
f"File '{filename}' already exists. Please delete it first."
|
||||
)
|
||||
157
ebook_backend&admin_panel/admin-backend/utils/logger.py
Normal file
157
ebook_backend&admin_panel/admin-backend/utils/logger.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
Professional logging utility for the Ebook Coupon Management System
|
||||
Provides structured logging with proper formatting and log levels.
|
||||
"""
|
||||
import logging
|
||||
import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from typing import Optional, Any, Dict
|
||||
import json
|
||||
|
||||
class SafeJSONEncoder(json.JSONEncoder):
|
||||
"""Custom JSON encoder that handles non-serializable objects safely"""
|
||||
|
||||
def default(self, obj):
|
||||
"""Handle non-serializable objects by converting them to strings"""
|
||||
if hasattr(obj, '__dict__'):
|
||||
return str(obj)
|
||||
elif hasattr(obj, '__str__'):
|
||||
return str(obj)
|
||||
else:
|
||||
return f"<{type(obj).__name__} object>"
|
||||
|
||||
class StructuredFormatter(logging.Formatter):
|
||||
"""Custom formatter for structured logging"""
|
||||
|
||||
def format(self, record: logging.LogRecord) -> str:
|
||||
"""Format log record with structured data"""
|
||||
log_entry = {
|
||||
"timestamp": datetime.utcnow().isoformat(),
|
||||
"level": record.levelname,
|
||||
"logger": record.name,
|
||||
"message": record.getMessage(),
|
||||
"module": record.module,
|
||||
"function": record.funcName,
|
||||
"line": record.lineno
|
||||
}
|
||||
|
||||
# Add extra fields if present
|
||||
if hasattr(record, 'request_id'):
|
||||
log_entry['request_id'] = record.request_id
|
||||
if hasattr(record, 'method'):
|
||||
log_entry['method'] = record.method
|
||||
if hasattr(record, 'path'):
|
||||
log_entry['path'] = record.path
|
||||
if hasattr(record, 'status_code'):
|
||||
log_entry['status_code'] = record.status_code
|
||||
if hasattr(record, 'process_time'):
|
||||
log_entry['process_time'] = record.process_time
|
||||
if hasattr(record, 'client_ip'):
|
||||
log_entry['client_ip'] = record.client_ip
|
||||
if hasattr(record, 'user_agent'):
|
||||
log_entry['user_agent'] = record.user_agent
|
||||
if hasattr(record, 'error'):
|
||||
log_entry['error'] = record.error
|
||||
if hasattr(record, 'exception_type'):
|
||||
log_entry['exception_type'] = record.exception_type
|
||||
if hasattr(record, 'exception_message'):
|
||||
log_entry['exception_message'] = record.exception_message
|
||||
if hasattr(record, 'errors'):
|
||||
# Handle errors list safely
|
||||
try:
|
||||
if isinstance(record.errors, list):
|
||||
log_entry['errors'] = [str(error) if not isinstance(error, (dict, str, int, float, bool)) else error for error in record.errors]
|
||||
else:
|
||||
log_entry['errors'] = str(record.errors)
|
||||
except Exception:
|
||||
log_entry['errors'] = str(record.errors)
|
||||
if hasattr(record, 'app_name'):
|
||||
log_entry['app_name'] = record.app_name
|
||||
if hasattr(record, 'version'):
|
||||
log_entry['version'] = record.version
|
||||
if hasattr(record, 'environment'):
|
||||
log_entry['environment'] = record.environment
|
||||
if hasattr(record, 'debug'):
|
||||
log_entry['debug'] = record.debug
|
||||
|
||||
# Add exception info if present
|
||||
if record.exc_info:
|
||||
log_entry['exception'] = self.formatException(record.exc_info)
|
||||
|
||||
return json.dumps(log_entry, ensure_ascii=False, cls=SafeJSONEncoder)
|
||||
|
||||
def setup_logger(name: str, level: Optional[str] = None) -> logging.Logger:
|
||||
"""
|
||||
Setup a logger with proper configuration
|
||||
|
||||
Args:
|
||||
name: Logger name
|
||||
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
|
||||
Returns:
|
||||
Configured logger instance
|
||||
"""
|
||||
# Get log level from environment or use default
|
||||
log_level = level or os.getenv("LOG_LEVEL", "INFO").upper()
|
||||
|
||||
# Create logger
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(getattr(logging, log_level))
|
||||
|
||||
# Avoid duplicate handlers
|
||||
if logger.handlers:
|
||||
return logger
|
||||
|
||||
# Create formatters
|
||||
structured_formatter = StructuredFormatter()
|
||||
console_formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
# Console handler
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setLevel(logging.DEBUG)
|
||||
console_handler.setFormatter(console_formatter)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
# File handler for structured logs
|
||||
log_dir = "logs"
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
|
||||
file_handler = logging.handlers.RotatingFileHandler(
|
||||
os.path.join(log_dir, "app.log"),
|
||||
maxBytes=10*1024*1024, # 10MB
|
||||
backupCount=5
|
||||
)
|
||||
file_handler.setLevel(logging.INFO)
|
||||
file_handler.setFormatter(structured_formatter)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
# Error file handler
|
||||
error_handler = logging.handlers.RotatingFileHandler(
|
||||
os.path.join(log_dir, "error.log"),
|
||||
maxBytes=10*1024*1024, # 10MB
|
||||
backupCount=5
|
||||
)
|
||||
error_handler.setLevel(logging.ERROR)
|
||||
error_handler.setFormatter(structured_formatter)
|
||||
logger.addHandler(error_handler)
|
||||
|
||||
return logger
|
||||
|
||||
def get_logger(name: str) -> logging.Logger:
|
||||
"""
|
||||
Get a logger instance
|
||||
|
||||
Args:
|
||||
name: Logger name
|
||||
|
||||
Returns:
|
||||
Logger instance
|
||||
"""
|
||||
return logging.getLogger(name)
|
||||
|
||||
# Create default logger
|
||||
default_logger = setup_logger("ebook_coupon_system")
|
||||
@@ -0,0 +1,8 @@
|
||||
from fastapi.templating import Jinja2Templates
|
||||
import os
|
||||
|
||||
BASE_DIR = os.path.dirname(__file__)
|
||||
PARENT_DIR = os.path.abspath(os.path.join(BASE_DIR, "..", ".."))
|
||||
TEMPLATE_DIR = os.path.join(PARENT_DIR, "admin-frontend")
|
||||
|
||||
templates = Jinja2Templates(directory=TEMPLATE_DIR)
|
||||
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
Timezone utilities for CEST/CET conversion
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
import pytz
|
||||
|
||||
def get_cest_timezone():
|
||||
"""Get CEST/CET timezone (Europe/Berlin)"""
|
||||
return pytz.timezone('Europe/Berlin')
|
||||
|
||||
def get_server_timezone():
|
||||
"""Get server's local timezone (IST)"""
|
||||
return pytz.timezone('Asia/Kolkata')
|
||||
|
||||
def utc_to_cest(utc_datetime):
|
||||
"""
|
||||
Convert UTC datetime to CEST/CET timezone
|
||||
|
||||
Args:
|
||||
utc_datetime: UTC datetime object
|
||||
|
||||
Returns:
|
||||
datetime object in CEST/CET timezone
|
||||
"""
|
||||
if utc_datetime is None:
|
||||
return None
|
||||
|
||||
# Ensure the datetime is timezone-aware
|
||||
if utc_datetime.tzinfo is None:
|
||||
utc_datetime = utc_datetime.replace(tzinfo=timezone.utc)
|
||||
|
||||
cest_tz = get_cest_timezone()
|
||||
return utc_datetime.astimezone(cest_tz)
|
||||
|
||||
def local_to_cest(local_datetime):
|
||||
"""
|
||||
Convert local server time (IST) to CEST/CET timezone
|
||||
|
||||
Args:
|
||||
local_datetime: Local datetime object (from server)
|
||||
|
||||
Returns:
|
||||
datetime object in CEST/CET timezone
|
||||
"""
|
||||
if local_datetime is None:
|
||||
return None
|
||||
|
||||
# First, make the local datetime timezone-aware
|
||||
ist_tz = get_server_timezone()
|
||||
if local_datetime.tzinfo is None:
|
||||
local_datetime = ist_tz.localize(local_datetime)
|
||||
|
||||
# Convert to CEST/CET
|
||||
cest_tz = get_cest_timezone()
|
||||
return local_datetime.astimezone(cest_tz)
|
||||
|
||||
def format_cest_datetime(utc_datetime, format_str="%Y-%m-%d %H:%M:%S"):
|
||||
"""
|
||||
Format UTC datetime to CEST/CET timezone string
|
||||
|
||||
Args:
|
||||
utc_datetime: UTC datetime object
|
||||
format_str: Format string for datetime
|
||||
|
||||
Returns:
|
||||
Formatted string in CEST/CET timezone
|
||||
"""
|
||||
if utc_datetime is None:
|
||||
return None
|
||||
|
||||
# Convert local server time to CEST/CET
|
||||
cest_datetime = local_to_cest(utc_datetime)
|
||||
return cest_datetime.strftime(format_str)
|
||||
|
||||
def now_cest():
|
||||
"""
|
||||
Get current time in CEST/CET timezone
|
||||
|
||||
Returns:
|
||||
datetime object in CEST/CET timezone
|
||||
"""
|
||||
cest_tz = get_cest_timezone()
|
||||
return datetime.now(cest_tz)
|
||||
Reference in New Issue
Block a user