first commit

This commit is contained in:
2025-11-30 00:07:24 +01:00
commit b5e642aecb
78 changed files with 15162 additions and 0 deletions

47
app/core/__init__.py Normal file
View File

@ -0,0 +1,47 @@
"""
Core Module
Provides core functionality for the application:
- database: Database configuration and session management
- security: Password hashing, JWT tokens, and security utilities
- logging_config: Structured logging configuration
Usage:
from app.core.database import db
from app.core.security import hash_password, verify_password
from app.core.logging_config import get_logger
"""
from app.core.database import db, init_db, get_db_session
from app.core.security import (
hash_password,
verify_password,
create_jwt_token,
decode_jwt_token,
create_id_token,
generate_secure_token,
generate_client_secret,
validate_password_strength
)
from app.core.logging_config import setup_logging, get_logger
__all__ = [
# Database
'db',
'init_db',
'get_db_session',
# Security
'hash_password',
'verify_password',
'create_jwt_token',
'decode_jwt_token',
'create_id_token',
'generate_secure_token',
'generate_client_secret',
'validate_password_strength',
# Logging
'setup_logging',
'get_logger',
]

56
app/core/database.py Normal file
View File

@ -0,0 +1,56 @@
"""
Database Configuration and Session Management
Provides database initialization, session management, and base models
following the Python Quick Start Guide best practices.
"""
from flask_sqlalchemy import SQLAlchemy
from typing import Generator
from sqlalchemy.orm import Session
# Database instance
db = SQLAlchemy()
def init_db(app) -> None:
"""
Initialize database with Flask app.
Args:
app: Flask application instance
"""
db.init_app(app)
def get_db_session() -> Session:
"""
Get current database session.
Returns:
SQLAlchemy session instance
Note:
This is a Flask-SQLAlchemy session, managed automatically.
Use db.session throughout the application.
"""
return db.session
# For FastAPI-style dependency injection (if migrating to FastAPI later)
def get_db() -> Generator[Session, None, None]:
"""
Get database session for dependency injection.
Yields:
Database session
Usage:
def some_function(db: Session = Depends(get_db)):
# Use db session
"""
try:
yield db.session
finally:
# Flask-SQLAlchemy handles cleanup automatically
pass

255
app/core/logging_config.py Normal file
View File

@ -0,0 +1,255 @@
"""
Logging Configuration
Provides structured logging setup for the application.
Based on Python Quick Start Guide best practices.
"""
import logging
import sys
from typing import Optional
from datetime import datetime
def setup_logging(
log_level: str = "INFO",
environment: str = "development"
) -> None:
"""
Configure application logging.
Args:
log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
environment: Environment name (development, production)
Usage:
setup_logging(log_level="INFO", environment="production")
"""
level = getattr(logging, log_level.upper(), logging.INFO)
# Clear existing handlers
root_logger = logging.getLogger()
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
# Console handler
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(level)
# Format based on environment
if environment == "development":
# Human-readable format for development
formatter = logging.Formatter(
fmt='%(asctime)s | %(levelname)-8s | %(name)s:%(funcName)s:%(lineno)d | %(message)s',
datefmt='%H:%M:%S'
)
else:
# Structured format for production (easier to parse)
formatter = logging.Formatter(
fmt='%(asctime)s | %(levelname)s | %(name)s | %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
console_handler.setFormatter(formatter)
root_logger.addHandler(console_handler)
root_logger.setLevel(level)
# Suppress noisy loggers
logging.getLogger('werkzeug').setLevel(logging.WARNING)
logging.getLogger('urllib3').setLevel(logging.WARNING)
def get_logger(name: str) -> logging.Logger:
"""
Get a logger instance.
Args:
name: Logger name (typically __name__)
Returns:
Logger instance
Example:
logger = get_logger(__name__)
logger.info("User logged in", extra={"user_id": 123})
"""
return logging.getLogger(name)
# ==========================================
# Structured Logging Helpers
# ==========================================
def log_audit_event(
logger: logging.Logger,
action: str,
user_id: Optional[int] = None,
ip_address: Optional[str] = None,
**kwargs
) -> None:
"""
Log an audit event with structured data.
Args:
logger: Logger instance
action: Action performed (e.g., "login_success", "user_created")
user_id: User ID performing action
ip_address: IP address of request
**kwargs: Additional context data
Example:
log_audit_event(
logger,
action="login_success",
user_id=123,
ip_address="192.168.1.1",
username="admin"
)
"""
extra_data = {
'action': action,
'user_id': user_id,
'ip_address': ip_address,
'timestamp': datetime.utcnow().isoformat(),
**kwargs
}
# Filter out None values
extra_data = {k: v for k, v in extra_data.items() if v is not None}
logger.info(f"AUDIT: {action}", extra=extra_data)
def log_security_event(
logger: logging.Logger,
event_type: str,
severity: str = "warning",
**kwargs
) -> None:
"""
Log a security-related event.
Args:
logger: Logger instance
event_type: Type of security event (e.g., "failed_login", "rate_limit_exceeded")
severity: Severity level (debug, info, warning, error, critical)
**kwargs: Additional context data
Example:
log_security_event(
logger,
event_type="failed_login",
severity="warning",
username="admin",
ip_address="192.168.1.1",
attempts=3
)
"""
extra_data = {
'event_type': event_type,
'timestamp': datetime.utcnow().isoformat(),
**kwargs
}
log_method = getattr(logger, severity.lower(), logger.warning)
log_method(f"SECURITY: {event_type}", extra=extra_data)
# ==========================================
# Request Logging Helpers
# ==========================================
def log_request(
logger: logging.Logger,
method: str,
path: str,
status_code: int,
duration_ms: float,
user_id: Optional[int] = None
) -> None:
"""
Log an HTTP request.
Args:
logger: Logger instance
method: HTTP method (GET, POST, etc.)
path: Request path
status_code: HTTP status code
duration_ms: Request duration in milliseconds
user_id: Authenticated user ID (if any)
Example:
log_request(
logger,
method="POST",
path="/api/users",
status_code=201,
duration_ms=45.2,
user_id=123
)
"""
extra_data = {
'method': method,
'path': path,
'status_code': status_code,
'duration_ms': round(duration_ms, 2),
'user_id': user_id
}
# Filter out None values
extra_data = {k: v for k, v in extra_data.items() if v is not None}
# Use different log levels based on status code
if status_code >= 500:
logger.error(f"{method} {path} {status_code}", extra=extra_data)
elif status_code >= 400:
logger.warning(f"{method} {path} {status_code}", extra=extra_data)
else:
logger.info(f"{method} {path} {status_code}", extra=extra_data)
# ==========================================
# Error Logging Helpers
# ==========================================
def log_exception(
logger: logging.Logger,
error: Exception,
context: Optional[str] = None,
**kwargs
) -> None:
"""
Log an exception with context.
Args:
logger: Logger instance
error: Exception instance
context: Additional context about where error occurred
**kwargs: Additional context data
Example:
try:
# Some operation
except Exception as e:
log_exception(
logger,
error=e,
context="Failed to create user",
user_id=123
)
"""
extra_data = {
'error_type': type(error).__name__,
'error_message': str(error),
'context': context,
**kwargs
}
# Filter out None values
extra_data = {k: v for k, v in extra_data.items() if v is not None}
logger.error(
f"Exception: {type(error).__name__}: {str(error)}",
extra=extra_data,
exc_info=True
)

237
app/core/security.py Normal file
View File

@ -0,0 +1,237 @@
"""
Security Utilities
Provides password hashing, JWT token management, and other security functions
following the Python Quick Start Guide best practices.
"""
import bcrypt
import jwt
import secrets
from datetime import datetime, timedelta
from typing import Dict, Optional, Any
# ==========================================
# Password Hashing (bcrypt)
# ==========================================
def hash_password(password: str) -> str:
"""
Hash a password using bcrypt.
Args:
password: Plain text password
Returns:
Hashed password as string
Example:
hashed = hash_password("mypassword123")
"""
password_bytes = password.encode('utf-8')
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password_bytes, salt)
return hashed.decode('utf-8')
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""
Verify a password against a hash.
Args:
plain_password: Plain text password to check
hashed_password: Hashed password to compare against
Returns:
True if password matches, False otherwise
Example:
if verify_password("mypassword123", user.password_hash):
# Password is correct
"""
password_bytes = plain_password.encode('utf-8')
hash_bytes = hashed_password.encode('utf-8')
return bcrypt.checkpw(password_bytes, hash_bytes)
# ==========================================
# JWT Token Management
# ==========================================
def create_jwt_token(
payload: Dict[str, Any],
secret_key: str,
algorithm: str = 'HS256',
expires_in: int = 3600
) -> str:
"""
Create a JWT token with expiration.
Args:
payload: Token payload data
secret_key: Secret key for signing
algorithm: JWT algorithm (HS256, RS256, etc.)
expires_in: Token lifetime in seconds
Returns:
Encoded JWT token string
Example:
token = create_jwt_token(
payload={'user_id': 123, 'role': 'admin'},
secret_key=app.config['SECRET_KEY'],
expires_in=3600
)
"""
payload = payload.copy()
expire = datetime.utcnow() + timedelta(seconds=expires_in)
payload.update({'exp': expire, 'iat': datetime.utcnow()})
return jwt.encode(payload, secret_key, algorithm=algorithm)
def decode_jwt_token(
token: str,
secret_key: str,
algorithm: str = 'HS256'
) -> Optional[Dict[str, Any]]:
"""
Decode and verify a JWT token.
Args:
token: JWT token string
secret_key: Secret key for verification
algorithm: JWT algorithm used
Returns:
Decoded payload dict, or None if invalid
Example:
payload = decode_jwt_token(token, app.config['SECRET_KEY'])
if payload:
user_id = payload['user_id']
"""
try:
payload = jwt.decode(token, secret_key, algorithms=[algorithm])
return payload
except jwt.ExpiredSignatureError:
# Token has expired
return None
except jwt.InvalidTokenError:
# Token is invalid
return None
def create_id_token(
user_data: Dict[str, Any],
client_id: str,
issuer: str,
private_key: str,
algorithm: str = 'RS256',
expires_in: int = 3600
) -> str:
"""
Create an OIDC ID Token (JWT).
Args:
user_data: User information (sub, email, name, etc.)
client_id: OAuth client ID (aud claim)
issuer: OIDC issuer URL (iss claim)
private_key: Private key for RS256 signing
algorithm: JWT algorithm (should be RS256 for OIDC)
expires_in: Token lifetime in seconds
Returns:
Encoded ID token string
Example:
id_token = create_id_token(
user_data={'sub': 'user-123', 'email': 'user@example.com'},
client_id='my-app',
issuer='https://auth.example.com',
private_key=app.config['OIDC_JWT_PRIVATE_KEY']
)
"""
now = datetime.utcnow()
payload = {
'iss': issuer,
'sub': user_data.get('sub'),
'aud': client_id,
'exp': now + timedelta(seconds=expires_in),
'iat': now,
**user_data # Include all user claims
}
return jwt.encode(payload, private_key, algorithm=algorithm)
# ==========================================
# Token Generation
# ==========================================
def generate_secure_token(length: int = 32) -> str:
"""
Generate a cryptographically secure random token.
Args:
length: Token length in bytes (default 32)
Returns:
URL-safe token string
Example:
auth_code = generate_secure_token(32)
access_token = generate_secure_token(64)
"""
return secrets.token_urlsafe(length)
def generate_client_secret() -> str:
"""
Generate a secure client secret for OIDC clients.
Returns:
URL-safe client secret string
Example:
client_secret = generate_client_secret()
"""
return secrets.token_urlsafe(32)
# ==========================================
# Password Validation
# ==========================================
def validate_password_strength(password: str) -> tuple[bool, Optional[str]]:
"""
Validate password strength.
Args:
password: Password to validate
Returns:
Tuple of (is_valid, error_message)
Rules:
- Minimum 8 characters
- At least one digit (optional but recommended)
Example:
is_valid, error = validate_password_strength("password123")
if not is_valid:
raise ValueError(error)
"""
if len(password) < 8:
return False, "Password must be at least 8 characters long"
# Optional: Check for digit
# if not any(char.isdigit() for char in password):
# return False, "Password must contain at least one digit"
# Optional: Check for uppercase
# if not any(char.isupper() for char in password):
# return False, "Password must contain at least one uppercase letter"
return True, None