This commit is contained in:
Iliyan Angelov
2025-11-26 22:32:20 +02:00
commit ed94dd22dd
150 changed files with 14058 additions and 0 deletions

View File

16
fraud_platform/asgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
ASGI config for fraud_platform project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'fraud_platform.settings')
application = get_asgi_application()

View File

@@ -0,0 +1,71 @@
"""
Context processors for SEO and site-wide data.
"""
from django.conf import settings
from reports.models import SiteSettings
def seo_context(request):
"""
Provides SEO-related context variables for all templates.
"""
site_url = request.build_absolute_uri('/').rstrip('/')
# Get site settings
site_settings = SiteSettings.get_settings()
# Default SEO values
default_seo = {
'site_name': 'Портал за Докладване на Измами',
'site_description': 'Портал за докладване на измами. Защита на гражданите от онлайн измами.',
'site_keywords': 'измами, киберпрестъпления, докладване измами, защита потребители, България, официален портал, анти-измами, сигурност онлайн',
'site_author': 'Официален Портал - Република България',
'site_language': 'bg',
'site_url': site_url,
'site_image': f'{site_url}/static/images/logo.svg',
'twitter_site': '@fraudplatformbg',
'twitter_creator': '@fraudplatformbg',
}
# Get page-specific SEO from view context if available
page_seo = {
'page_title': getattr(request, 'seo_title', None),
'page_description': getattr(request, 'seo_description', None),
'page_keywords': getattr(request, 'seo_keywords', None),
'page_image': getattr(request, 'seo_image', None),
'page_type': getattr(request, 'seo_type', 'website'),
'canonical_url': getattr(request, 'canonical_url', request.build_absolute_uri()),
}
# Merge defaults with page-specific
seo = {**default_seo, **{k: v for k, v in page_seo.items() if v}}
# Build full title
if seo.get('page_title'):
seo['full_title'] = f"{seo['page_title']} | {seo['site_name']}"
else:
seo['full_title'] = seo['site_name']
# Use page image or default
seo['og_image'] = seo.get('page_image') or seo['site_image']
return {
'seo': seo,
'site_settings': site_settings,
}
def email_settings(request):
"""
Provides email settings context (for use in settings if needed).
"""
from reports.models import SiteSettings
site_settings = SiteSettings.get_settings()
return {
'email_settings': {
'default_from_email': site_settings.default_from_email,
'contact_email': site_settings.contact_email,
}
}

View File

@@ -0,0 +1,9 @@
import os
from .base import *
# Import environment-specific settings
if os.environ.get('DJANGO_ENV') == 'production':
from .production import *
else:
from .development import *

View File

@@ -0,0 +1,241 @@
"""
Base settings for fraud_platform project.
"""
import os
from pathlib import Path
import environ
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent.parent
# Initialize environment variables
env = environ.Env(
DEBUG=(bool, False)
)
# Read .env file if it exists
environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env('SECRET_KEY', default='django-insecure-change-this-in-production')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env('DEBUG', default=False)
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[])
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# MFA/OTP
'django_otp',
'django_otp.plugins.otp_totp',
'django_otp.plugins.otp_static',
# Local apps
'accounts',
'reports',
'osint',
'moderation',
'analytics',
'legal',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'accounts.middleware.SecurityHeadersMiddleware', # Security headers
'accounts.middleware.RateLimitMiddleware', # Rate limiting
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django_otp.middleware.OTPMiddleware', # MFA middleware
'accounts.middleware.SessionSecurityMiddleware', # Session security (after auth)
'accounts.middleware.SecurityLoggingMiddleware', # Security logging
'accounts.middleware.IPWhitelistMiddleware', # IP filtering (admin)
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'fraud_platform.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates'],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'fraud_platform.context_processors.seo_context',
],
},
},
]
WSGI_APPLICATION = 'fraud_platform.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': env('DB_NAME', default='fraud_platform_db'),
'USER': env('DB_USER', default='postgres'),
'PASSWORD': env('DB_PASSWORD', default=''),
'HOST': env('DB_HOST', default='localhost'),
'PORT': env('DB_PORT', default='5432'),
}
}
# Custom User Model
AUTH_USER_MODEL = 'accounts.User'
# Password validation - Enhanced security
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
'OPTIONS': {
'user_attributes': ('username', 'email', 'first_name', 'last_name'),
'max_similarity': 0.7,
}
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
'OPTIONS': {
'min_length': 12,
}
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Additional password requirements
PASSWORD_MIN_LENGTH = 12
PASSWORD_REQUIRE_UPPERCASE = True
PASSWORD_REQUIRE_LOWERCASE = True
PASSWORD_REQUIRE_DIGITS = True
PASSWORD_REQUIRE_SPECIAL = True
# Internationalization
LANGUAGE_CODE = 'bg' # Bulgarian
TIME_ZONE = 'Europe/Sofia'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
STATIC_URL = '/static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
BASE_DIR / 'static',
]
# Media files
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
# Default primary key field type
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# Security Settings
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
SECURE_HSTS_SECONDS = 31536000 if not DEBUG else 0
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
SECURE_SSL_REDIRECT = not DEBUG
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Session Security
SESSION_COOKIE_SECURE = not DEBUG
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = 'Lax'
SESSION_COOKIE_AGE = 86400 # 24 hours
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
SESSION_SAVE_EVERY_REQUEST = True # Extend session on activity
# CSRF Security
CSRF_COOKIE_SECURE = not DEBUG
CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SAMESITE = 'Lax'
CSRF_USE_SESSIONS = True # Store CSRF token in session instead of cookie
CSRF_FAILURE_VIEW = 'accounts.views.csrf_failure'
# Password Security
PASSWORD_HASHERS = [
'django.contrib.auth.hashers.Argon2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
]
# Encryption Key (should be in environment in production)
ENCRYPTION_KEY = env('ENCRYPTION_KEY', default=None)
# Rate Limiting
RATELIMIT_ENABLE = True
RATELIMIT_USE_CACHE = 'default'
# Security Logging
SECURITY_LOG_FAILED_LOGINS = True
SECURITY_LOG_SUSPICIOUS_ACTIVITY = True
# Login URLs
LOGIN_URL = '/accounts/login/'
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'
# Email Configuration
# Uses custom backend that reads from SiteSettings model
# Falls back to environment variables if SiteSettings not configured
EMAIL_BACKEND = env('EMAIL_BACKEND', default='reports.email_backend.SiteSettingsEmailBackend')
EMAIL_HOST = env('EMAIL_HOST', default='')
EMAIL_PORT = env('EMAIL_PORT', default=587)
EMAIL_USE_TLS = env('EMAIL_USE_TLS', default=True)
EMAIL_USE_SSL = env('EMAIL_USE_SSL', default=False)
EMAIL_HOST_USER = env('EMAIL_HOST_USER', default='')
EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD', default='')
# DEFAULT_FROM_EMAIL is now managed via SiteSettings model
# Fallback to env variable if SiteSettings not configured
DEFAULT_FROM_EMAIL = env('DEFAULT_FROM_EMAIL', default='noreply@fraudplatform.bg')
EMAIL_TIMEOUT = env('EMAIL_TIMEOUT', default=10)
# File Upload Settings - Security
FILE_UPLOAD_MAX_MEMORY_SIZE = 10485760 # 10MB
DATA_UPLOAD_MAX_MEMORY_SIZE = 10485760 # 10MB
FILE_UPLOAD_PERMISSIONS = 0o644
DATA_UPLOAD_MAX_NUMBER_FIELDS = 1000 # Prevent DoS via form fields
# Allowed file types for uploads
ALLOWED_FILE_EXTENSIONS = ['.pdf', '.jpg', '.jpeg', '.png', '.txt', '.doc', '.docx']
ALLOWED_MIME_TYPES = [
'application/pdf',
'image/jpeg',
'image/png',
'text/plain',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
]
# Database Security
if 'default' in DATABASES:
DATABASES['default']['CONN_MAX_AGE'] = 600 # Connection pooling
DATABASES['default']['OPTIONS'] = {
'connect_timeout': 10,
'options': '-c statement_timeout=30000' # 30 second query timeout
}

View File

@@ -0,0 +1,33 @@
"""
Development settings for fraud_platform project.
"""
from .base import *
import os
DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
# Use SQLite for development if PostgreSQL is not available
if os.environ.get('USE_SQLITE', 'True').lower() == 'true':
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Development-specific settings (django_extensions is optional)
# INSTALLED_APPS += [
# 'django_extensions', # Optional: for development tools
# ]
# Use SiteSettings email backend (will use console if SMTP not configured)
# EMAIL_BACKEND is set in base.py to use SiteSettingsEmailBackend
# This allows admin to configure SMTP even in development
# Disable security features for development
SECURE_SSL_REDIRECT = False
SESSION_COOKIE_SECURE = False
CSRF_COOKIE_SECURE = False

View File

@@ -0,0 +1,54 @@
"""
Production settings for fraud_platform project.
"""
from .base import *
DEBUG = False
# Production security settings
SECURE_SSL_REDIRECT = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True
# Logging
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'verbose': {
'format': '{levelname} {asctime} {module} {message}',
'style': '{',
},
},
'handlers': {
'file': {
'level': 'INFO',
'class': 'logging.FileHandler',
'filename': BASE_DIR / 'logs' / 'django.log',
'formatter': 'verbose',
},
'security_file': {
'level': 'WARNING',
'class': 'logging.FileHandler',
'filename': BASE_DIR / 'logs' / 'security.log',
'formatter': 'verbose',
},
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'INFO',
'propagate': True,
},
'django.security': {
'handlers': ['security_file'],
'level': 'WARNING',
'propagate': True,
},
},
}

View File

@@ -0,0 +1,54 @@
"""
Sitemap configuration for SEO.
"""
from django.contrib.sitemaps import Sitemap
from django.urls import reverse
from reports.models import ScamReport
class StaticViewSitemap(Sitemap):
"""Sitemap for static pages."""
priority = 1.0
changefreq = 'monthly'
def items(self):
return [
'reports:home',
'reports:list',
'reports:create',
'reports:contact',
'reports:search',
'legal:privacy',
'legal:terms',
]
def location(self, item):
return reverse(item)
class ScamReportSitemap(Sitemap):
"""Sitemap for scam reports."""
changefreq = 'weekly'
priority = 0.8
def items(self):
# Only include public, verified reports
return ScamReport.objects.filter(
is_public=True,
status='verified'
).order_by('-created_at')
def lastmod(self, obj):
return obj.updated_at or obj.created_at
def location(self, obj):
from django.urls import reverse
return reverse('reports:detail', kwargs={'pk': obj.pk})
# Combine sitemaps
sitemaps = {
'static': StaticViewSitemap,
'reports': ScamReportSitemap,
}

57
fraud_platform/urls.py Normal file
View File

@@ -0,0 +1,57 @@
"""
URL configuration for fraud_platform project.
"""
from django.contrib import admin
from django.contrib.sitemaps.views import sitemap
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from django.http import HttpResponse, Http404
from django.views.decorators.cache import cache_control
from .sitemaps import sitemaps
import os
@cache_control(max_age=86400) # Cache for 1 day
def favicon_view(request):
"""Serve favicon.ico"""
favicon_path = os.path.join(settings.BASE_DIR, 'static', 'favicon.ico')
if os.path.exists(favicon_path):
with open(favicon_path, 'rb') as f:
return HttpResponse(f.read(), content_type='image/x-icon')
raise Http404("Favicon not found")
@cache_control(max_age=86400) # Cache for 1 day
def robots_txt(request):
"""Serve robots.txt"""
content = """User-agent: *
Allow: /
Disallow: /admin/
Disallow: /accounts/
Disallow: /moderation/
Disallow: /analytics/
Disallow: /osint/admin-dashboard/
Disallow: /api/
Sitemap: {}/sitemap.xml
""".format(request.build_absolute_uri('/').rstrip('/'))
return HttpResponse(content, content_type='text/plain')
urlpatterns = [
path('admin/', admin.site.urls),
path('accounts/', include('accounts.urls')),
path('osint/', include('osint.urls')),
path('moderation/', include('moderation.urls')),
path('analytics/', include('analytics.urls')),
path('legal/', include('legal.urls')),
path('', include('reports.urls')), # Home page and reports (reports:home, reports:list, etc.)
# SEO
path('sitemap.xml', sitemap, {'sitemaps': sitemaps}, name='django.contrib.sitemaps.views.sitemap'),
path('robots.txt', robots_txt, name='robots'),
# Favicon
path('favicon.ico', favicon_view, name='favicon'),
]
# Serve media files in development
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

16
fraud_platform/wsgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
WSGI config for fraud_platform project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'fraud_platform.settings')
application = get_wsgi_application()