first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 22:20:45 +03:00
commit d50f14bcb1
681 changed files with 65020 additions and 0 deletions

9
core/Permission.py Normal file
View File

@@ -0,0 +1,9 @@
from rest_framework.permissions import BasePermission, SAFE_METHODS
class ReadOnly(BasePermission):
"""
Yalnızca okuma işlemlerine izin verir.
"""
def has_permission(self, request, view):
# SAFE_METHODS: ('GET', 'HEAD', 'OPTIONS')
return request.method in SAFE_METHODS

5
core/__init__.py Normal file
View File

@@ -0,0 +1,5 @@
# Celery app'i import et, böylece Django başladığında her zaman yüklenir
from .celery import app as celery_app
__all__ = ('celery_app',)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

16
core/asgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
ASGI config for core 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/6.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
application = get_asgi_application()

19
core/celery.py Normal file
View File

@@ -0,0 +1,19 @@
import os
from celery import Celery
# Django settings modülünü ayarla
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
app = Celery('core')
# Django settings'ten celery yapılandırmasını yükle
app.config_from_object('django.conf:settings', namespace='CELERY')
# Tüm Django app'lerden tasks.py dosyalarını otomatik olarak keşfet
app.autodiscover_tasks()
@app.task(bind=True, ignore_result=True)
def debug_task(self):
print(f'Request: {self.request!r}')

452
core/settings.py Normal file
View File

@@ -0,0 +1,452 @@
"""
Django settings for core project.
Generated by 'django-admin startproject' using Django 6.0.
For more information on this file, see
https://docs.djangoproject.com/en/6.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.0/ref/settings/
"""
from pathlib import Path
import os
import environ
BASE_DIR = Path(__file__).resolve().parent.parent
env = environ.Env()
environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env('SECRET_KEY', default='django-insecure-slih+3-7gn0b04-2wm4zq)rp*kz1jnt&bf9o3i3*8jhz*n9=2k')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = env.bool('DEBUG', default=True)
ALLOWED_HOSTS = ['api.denizogur.com.tr', 'localhost', '127.0.0.1', '[::1]']
CSRF_TRUSTED_ORIGINS = ['https://api.denizogur.com.tr', 'https://*.denizogur.com.tr']
CSRF_COOKIE_DOMAIN = '.denizogur.com.tr'
# Site URL - .env dosyasından alınır
SITE_URL = os.getenv('SITE_URL', 'http://127.0.0.1:8000')
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# 3. Parti
'rest_framework',
'rest_framework_simplejwt',
'django_celery_results',
# celery -A [project-name] worker --beat --scheduler django --loglevel=info
'django_celery_beat',
'djoser',
'corsheaders',
'social_django',
'imagekit',
'django_cleanup',
'colorfield',
'autoslug',
'tinymce',
'django.contrib.sites', # Added for Djoser domain resolution
# Local Apps
# 'blog',
'accounts',
'settings',
'backup',
'home',
'portfolio',
'contact',
]
SITE_ID = 1
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
"whitenoise.middleware.WhiteNoiseMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'accounts.middleware.SocialAuthExceptionMiddleware',
]
ROOT_URLCONF = 'core.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / 'templates']
,
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
# Social auth context processors
'social_django.context_processors.backends',
'social_django.context_processors.login_redirect',
],
},
},
]
WSGI_APPLICATION = 'core.wsgi.application'
# Database
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
# Docker ortamında veya environment değişkeni varsa PostgreSQL kullan
USE_POSTGRES = env.bool('USE_POSTGRES', default=True)
if USE_POSTGRES:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': env('POSTGRES_DB', default='beyhan_blog'),
'USER': env('POSTGRES_USER', default='beyhan_blog'),
'PASSWORD': env('POSTGRES_PASSWORD', default='1923btO**'),
'HOST': env('POSTGRES_HOST', default='212.64.215.243'),
'PORT': env('POSTGRES_PORT', default='5432'),
'OPTIONS': {
'options': '-c search_path=public'
},
}
}
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# ==============================================================================
# REDIS CACHE CONFIGURATION
# ==============================================================================
CACHES = {
'default': {
'BACKEND': 'django_redis.cache.RedisCache',
# 'LOCATION': 'redis://default:1923btO**@ares-redis-xrot7z:6379',
'LOCATION': os.getenv('CELERY_BROKER_URL', 'redis://default:8KNa2T3ceGkrYPpt@212.64.215.243:6379/3'),
# 'LOCATION': os.getenv('REDIS_URL', 'redis://127.0.0.1:6379/1'),
'OPTIONS': {
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
},
'KEY_PREFIX': 'ata_',
'TIMEOUT': 300, # 5 dakika default timeout
}
}
# ==============================================================================
# CELERY CONFIGURATION
# ==============================================================================
# CELERY_BROKER_URL = 'redis://default:1923btO**@10.80.80.70:6379/5'
CELERY_BROKER_URL = os.getenv('CELERY_BROKER_URL', 'redis://default:8KNa2T3ceGkrYPpt@212.64.215.243:6379/5')
CELERY_RESULT_BACKEND = os.getenv('CELERY_RESULT_BACKEND', 'django-db')
CELERY_ACCEPT_CONTENT = ['application/json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'Europe/Istanbul' # Türkiye timezone
CELERY_ENABLE_UTC = True # UTC zaman kullan
# Password validation
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/6.0/topics/i18n/
LANGUAGE_CODE = 'tr'
TIME_ZONE = 'Europe/Istanbul'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.0/howto/static-files/
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
BASE_DIR / 'static',
]
# Media files (User uploaded files)
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
# Default primary key field type
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# ==============================================================================
# CUSTOM USER MODEL
# ==============================================================================
AUTH_USER_MODEL = 'accounts.CustomUser'
# ==============================================================================
# REST FRAMEWORK CONFIGURATION
# ==============================================================================
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
'rest_framework.authentication.SessionAuthentication', # For social auth
),
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
# Throttling for security - api.denizogur.com.tr için bypass edildi
'DEFAULT_THROTTLE_CLASSES': [
'core.throttling.CustomAnonRateThrottle',
'core.throttling.CustomUserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '100/hour', # Anonymous users
'user': '1000/hour', # Authenticated users
},
}
# ==============================================================================
# SIMPLE JWT CONFIGURATION
# ==============================================================================
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=30),
'REFRESH_TOKEN_LIFETIME': timedelta(days=120),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
'UPDATE_LAST_LOGIN': True,
'ALGORITHM': 'HS256',
'SIGNING_KEY': SECRET_KEY,
'VERIFYING_KEY': None,
'AUDIENCE': None,
'ISSUER': None,
'AUTH_HEADER_TYPES': ('Bearer',),
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',
'JTI_CLAIM': 'jti',
}
# ==============================================================================
# DJOSER CONFIGURATION
# ==============================================================================
DJOSER = {
# Domain for email links (YOUR FRONTEND URL)
# Djoser combines DOMAIN + ACTIVATION_URL to create the full link
'DOMAIN': 'localhost:3000', # IMPORTANT: Change this to your frontend's domain
'SITE_NAME': 'Django Auth API',
# Registration & Activation
'SEND_ACTIVATION_EMAIL': True,
'ACTIVATION_URL': 'activate/{uid}/{token}', # Frontend route, e.g., http://localhost:3000/activate/MQ/token/
# Password Reset
'SEND_CONFIRMATION_EMAIL': True,
'PASSWORD_RESET_CONFIRM_URL': 'password-reset/{uid}/{token}', # Frontend route
'PASSWORD_RESET_SHOW_EMAIL_NOT_FOUND': False,
# Username Reset
'USERNAME_RESET_CONFIRM_URL': 'username-reset/{uid}/{token}', # Frontend route
# Email confirmations
'PASSWORD_CHANGED_EMAIL_CONFIRMATION': True,
'USERNAME_CHANGED_EMAIL_CONFIRMATION': True,
# User settings
'USER_CREATE_PASSWORD_RETYPE': True,
'SET_PASSWORD_RETYPE': True,
'PASSWORD_RESET_CONFIRM_RETYPE': True,
'LOGIN_FIELD': 'email',
# Serializers
'SERIALIZERS': {
'user_create': 'accounts.serializers.CustomUserCreateSerializer',
'user': 'accounts.serializers.CustomUserSerializer',
'current_user': 'accounts.serializers.CustomUserSerializer',
},
}
# ==============================================================================
# EMAIL CONFIGURATION
# ==============================================================================
# Development: Using MailPit (local email testing tool)
# MailPit default runs on localhost:1025 for SMTP and localhost:8025 for web UI
# SITE_URL = os.getenv('SITE_URL', 'http://127.0.0.1:8000')
EMAIL_BACKEND = os.getenv('EMAIL_BACKEND','django.core.mail.backends.smtp.EmailBackend')
EMAIL_HOST = os.getenv('EMAIL_HOST','212.64.215.243')
EMAIL_PORT = os.getenv('EMAIL_PORT',1025)
# EMAIL_USE_TLS = os.getenv('EMAIL_USE_TLS',False)
# EMAIL_USE_SSL = os.getenv('EMAIL_USE_SSL',True)
EMAIL_HOST_USER = os.getenv('EMAIL_HOST_USER','')
EMAIL_HOST_PASSWORD = os.getenv('EMAIL_HOST_PASSWORD','')
DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL','noreply@localhost')
# Production: Uncomment and configure these with environment variables
# EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
# EMAIL_HOST = os.environ.get('EMAIL_HOST', 'smtp.gmail.com')
# EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 587))
# EMAIL_USE_TLS = os.environ.get('EMAIL_USE_TLS', 'True') == 'True'
# EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER')
# EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD')
# DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'noreply@yourdomain.com')
# ==============================================================================
# CORS CONFIGURATION
# ==============================================================================
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000", # Next.js default
"http://127.0.0.1:3000",
"http://localhost:5173", # Vite default
"http://127.0.0.1:5173",
"http://localhost:8080", # Vue/Nuxt alternative port
"http://127.0.0.1:8080",
]
# For development only - be careful in production!
CORS_ORIGIN_ALLOW_ALL = True
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_HEADERS = [
'accept',
'accept-encoding',
'authorization',
'content-type',
'dnt',
'origin',
'user-agent',
'x-csrftoken',
'x-requested-with',
]
# ==============================================================================
# SOCIAL AUTH CONFIGURATION (Python Social Auth)
# ==============================================================================
AUTHENTICATION_BACKENDS = (
# Social auth backends
'social_core.backends.google.GoogleOAuth2',
'social_core.backends.github.GithubOAuth2',
'social_core.backends.facebook.FacebookOAuth2',
# Add more providers as needed
# Django default
'django.contrib.auth.backends.ModelBackend',
)
# Social Auth Settings
SOCIAL_AUTH_JSONFIELD_ENABLED = True
SOCIAL_AUTH_URL_NAMESPACE = 'social'
# Pipeline - custom pipeline to set is_active=True for social users
SOCIAL_AUTH_PIPELINE = (
'social_core.pipeline.social_auth.social_details',
'social_core.pipeline.social_auth.social_uid',
'social_core.pipeline.social_auth.auth_allowed',
'social_core.pipeline.social_auth.social_user',
'social_core.pipeline.user.get_username',
'social_core.pipeline.user.create_user',
'social_core.pipeline.social_auth.associate_user',
'social_core.pipeline.social_auth.load_extra_data',
'social_core.pipeline.user.user_details',
'accounts.pipeline.activate_user', # Custom pipeline to set is_active=True
)
# User model
SOCIAL_AUTH_USER_MODEL = 'accounts.CustomUser'
SOCIAL_AUTH_USERNAME_IS_REQUIRED = False
SOCIAL_AUTH_USER_FIELDS = ['email', 'first_name', 'last_name']
# Strategy
SOCIAL_AUTH_STRATEGY = 'social_django.strategy.DjangoStrategy'
SOCIAL_AUTH_STORAGE = 'social_django.models.DjangoStorage'
# Google OAuth2 Configuration
# Get credentials from: https://console.developers.google.com/
SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = '915364976256-691m0s87as2r5vdbqr96f6humblseobt.apps.googleusercontent.com' # Your Google Client ID
SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = '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' # Your Google Client Secret
SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
]
SOCIAL_AUTH_GOOGLE_OAUTH2_EXTRA_DATA = ['first_name', 'last_name']
# GitHub OAuth2 Configuration
# Get credentials from: https://github.com/settings/developers
SOCIAL_AUTH_GITHUB_KEY = 'Ov23liUt9B61O46Mdfm4' # Your GitHub Client ID
SOCIAL_AUTH_GITHUB_SECRET = 'c7fc8dcb1b2c8f22120608425d07d5efd995baaf' # Your GitHub Client Secret
SOCIAL_AUTH_GITHUB_SCOPE = ['user:email']
# Facebook OAuth2 Configuration
# Get credentials from: https://developers.facebook.com/
SOCIAL_AUTH_FACEBOOK_KEY = '' # Your Facebook App ID
SOCIAL_AUTH_FACEBOOK_SECRET = '' # Your Facebook App Secret
SOCIAL_AUTH_FACEBOOK_SCOPE = ['email']
SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = {
'fields': 'id, name, email, first_name, last_name'
}
# Redirect URLs (customize for your frontend)
LOGIN_URL = '/api/v1/spa/'
LOGIN_REDIRECT_URL = '/api/v1/auth/social/callback/'
SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/api/v1/auth/social/callback/'
SOCIAL_AUTH_NEW_USER_REDIRECT_URL = '/api/v1/auth/social/callback/'
SOCIAL_AUTH_INACTIVE_USER_URL = '/api/v1/auth/social/error/'
SOCIAL_AUTH_LOGIN_ERROR_URL = '/api/v1/auth/social/error/'
TASKS = {"default": {"BACKEND": "django.tasks.backends.immediate.ImmediateBackend"}}
# ==============================================================================
# SECURITY SETTINGS FOR SPA/JWT
# ==============================================================================
# Since we're using JWT tokens (not session cookies), we can relax CSRF for API endpoints
# But keep it enabled for Django admin
"""
CSRF_COOKIE_HTTPONLY = False
CSRF_COOKIE_SECURE = False # Set to True in production with HTTPS
SESSION_COOKIE_SECURE = False # Set to True in production with HTTPS
CSRF_TRUSTED_ORIGINS = [
"http://localhost:3000",
"http://localhost:5173",
]
"""

347
core/throttling.py Normal file
View File

@@ -0,0 +1,347 @@
"""
Custom throttling classes for API rate limiting
"""
import logging
import ipaddress
from rest_framework.throttling import AnonRateThrottle, UserRateThrottle
# Logger yapılandırması
logger = logging.getLogger('throttling')
logger.setLevel(logging.INFO)
# File handler ekle (logs dizinine yaz)
import os
import sys
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
LOG_DIR = BASE_DIR / 'logs'
LOG_DIR.mkdir(exist_ok=True)
# Formatter oluştur
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# File handler oluştur (dosyaya yaz)
file_handler = logging.FileHandler(LOG_DIR / 'throttling.log', encoding='utf-8')
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(formatter)
# Stream handler oluştur (konsola yaz - Docker için)
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(formatter)
# Handler'ları logger'a ekle (duplicate handler'ları önlemek için)
if not logger.handlers:
logger.addHandler(file_handler)
logger.addHandler(console_handler)
def is_cloudflare_ip(ip):
"""
IP adresinin Cloudflare IP aralığında olup olmadığını kontrol eder
Cloudflare IP aralıkları: https://www.cloudflare.com/ips/
"""
try:
ip_obj = ipaddress.ip_address(ip)
# Cloudflare IPv4 aralıkları (en yaygın olanlar)
cloudflare_ranges = [
ipaddress.ip_network('173.245.48.0/20'),
ipaddress.ip_network('103.21.244.0/22'),
ipaddress.ip_network('103.22.200.0/22'),
ipaddress.ip_network('103.31.4.0/22'),
ipaddress.ip_network('141.101.64.0/18'),
ipaddress.ip_network('108.162.192.0/18'),
ipaddress.ip_network('190.93.240.0/20'),
ipaddress.ip_network('188.114.96.0/20'),
ipaddress.ip_network('197.234.240.0/22'),
ipaddress.ip_network('198.41.128.0/17'),
ipaddress.ip_network('162.158.0.0/15'),
ipaddress.ip_network('104.16.0.0/13'),
ipaddress.ip_network('104.24.0.0/14'),
ipaddress.ip_network('172.64.0.0/13'),
ipaddress.ip_network('131.0.72.0/22'),
]
# Cloudflare IPv6 aralıkları
cloudflare_ranges_v6 = [
ipaddress.ip_network('2400:cb00::/32'),
ipaddress.ip_network('2606:4700::/32'),
ipaddress.ip_network('2803:f800::/32'),
ipaddress.ip_network('2405:b500::/32'),
ipaddress.ip_network('2405:8100::/32'),
ipaddress.ip_network('2a06:98c0::/29'),
ipaddress.ip_network('2c0f:f248::/32'),
]
all_ranges = cloudflare_ranges + cloudflare_ranges_v6
return any(ip_obj in network for network in all_ranges)
except (ValueError, ipaddress.AddressValueError):
return False
class CustomAnonRateThrottle(AnonRateThrottle):
"""
Belirli IP'ler için throttling'i bypass eder
"""
EXEMPT_IPS = [
'127.0.0.1',
'localhost',
'::1',
'212.64.215.243',
'162.158.210.254',
'188.132.232.119',
]
def get_client_ip(self, request):
"""
Client IP adresini al
Cloudflare kullanıldığında CF-Connecting-IP header'ını öncelikli kullanır
"""
# Cloudflare gerçek client IP'si (en güvenilir)
cf_connecting_ip = request.META.get('HTTP_CF_CONNECTING_IP')
if cf_connecting_ip:
return cf_connecting_ip.strip()
# True-Client-IP (bazı Cloudflare yapılandırmalarında)
true_client_ip = request.META.get('HTTP_TRUE_CLIENT_IP')
if true_client_ip:
return true_client_ip.strip()
# X-Forwarded-For (fallback)
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
# İlk IP genellikle gerçek client IP'sidir
ip = x_forwarded_for.split(',')[0].strip()
return ip
# Son çare: REMOTE_ADDR
remote_addr = request.META.get('REMOTE_ADDR', 'unknown')
# Eğer REMOTE_ADDR Cloudflare IP'si ise, gerçek IP bulunamadı demektir
if is_cloudflare_ip(remote_addr):
logger.warning(
f"[IP DETECTION] Cloudflare IP tespit edildi ({remote_addr}) "
f"ama gerçek client IP bulunamadı. CF-Connecting-IP header'ı eksik olabilir."
)
return remote_addr
def get_cache_key(self, request, view):
"""
Cache key'i gerçek client IP'ye göre oluşturur
Cloudflare kullanıldığında gerçek IP'yi kullanır
"""
# Gerçek client IP'yi al
ident = self.get_client_ip(request)
# Cache key format: throttle_anon_{ip}
return self.cache_format % {
'scope': self.scope,
'ident': ident
}
def allow_request(self, request, view):
# Get client IP
ip = self.get_client_ip(request)
remote_addr = request.META.get('REMOTE_ADDR', 'unknown')
host = request.get_host().split(':')[0]
path = request.path
method = request.method
user_agent = request.META.get('HTTP_USER_AGENT', 'unknown')
# View bilgisi
view_name = getattr(view, '__class__', None)
view_name = view_name.__name__ if view_name else 'unknown'
# Cloudflare kontrolü
is_from_cloudflare = is_cloudflare_ip(remote_addr)
cf_info = f"CF-IP: {remote_addr}" if is_from_cloudflare else ""
# Belirtilen IP'lerden geliyorsa throttling yapma
if ip in self.EXEMPT_IPS:
logger.info(
f"[ANON THROTTLE - BYPASS] Real-IP: {ip} | {cf_info} | Host: {host} | "
f"Path: {path} | Method: {method} | View: {view_name} | "
f"User-Agent: {user_agent[:100]}"
)
return True
# Normal throttling kurallarını uygula
allowed = super().allow_request(request, view)
# Throttle durumunu kontrol et
if allowed:
# Rate limit bilgilerini al
throttle_scope = getattr(view, 'throttle_scope', None) or 'anon'
rate = self.get_rate()
num_requests, duration = self.parse_rate(rate)
# Cache key'den kalan istek sayısını tahmin et
cache_key = self.get_cache_key(request, view)
history = self.cache.get(cache_key, [])
remaining = max(0, num_requests - len(history))
logger.info(
f"[ANON THROTTLE - ALLOWED] Real-IP: {ip} | {cf_info} | Host: {host} | "
f"Path: {path} | Method: {method} | View: {view_name} | "
f"Rate: {rate} | Remaining: {remaining}/{num_requests} | "
f"User-Agent: {user_agent[:100]}"
)
else:
# Throttle limit aşıldı
rate = self.get_rate()
num_requests, duration = self.parse_rate(rate)
logger.warning(
f"[ANON THROTTLE - BLOCKED] Real-IP: {ip} | {cf_info} | Host: {host} | "
f"Path: {path} | Method: {method} | View: {view_name} | "
f"Rate: {rate} | Limit: {num_requests}/{duration} | "
f"User-Agent: {user_agent[:100]}"
)
return allowed
class CustomUserRateThrottle(UserRateThrottle):
"""
Belirli kullanıcılar veya domainler için throttling'i bypass eder
"""
EXEMPT_HOSTS = [
'api.denizogur.com.tr',
'denizogur.com.tr',
'denizour.com.tr',
'localhost',
'127.0.0.1',
]
def get_client_ip(self, request):
"""
Client IP adresini al
Cloudflare kullanıldığında CF-Connecting-IP header'ını öncelikli kullanır
"""
# Cloudflare gerçek client IP'si (en güvenilir)
cf_connecting_ip = request.META.get('HTTP_CF_CONNECTING_IP')
if cf_connecting_ip:
return cf_connecting_ip.strip()
# True-Client-IP (bazı Cloudflare yapılandırmalarında)
true_client_ip = request.META.get('HTTP_TRUE_CLIENT_IP')
if true_client_ip:
return true_client_ip.strip()
# X-Forwarded-For (fallback)
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
# İlk IP genellikle gerçek client IP'sidir
ip = x_forwarded_for.split(',')[0].strip()
return ip
# Son çare: REMOTE_ADDR
remote_addr = request.META.get('REMOTE_ADDR', 'unknown')
# Eğer REMOTE_ADDR Cloudflare IP'si ise, gerçek IP bulunamadı demektir
if is_cloudflare_ip(remote_addr):
logger.warning(
f"[IP DETECTION] Cloudflare IP tespit edildi ({remote_addr}) "
f"ama gerçek client IP bulunamadı. CF-Connecting-IP header'ı eksik olabilir."
)
return remote_addr
def get_cache_key(self, request, view):
"""
Cache key'i gerçek client IP'ye göre oluşturur
Cloudflare kullanıldığında gerçek IP'yi kullanır
UserRateThrottle için user ID de eklenir
"""
# Authenticated kullanıcı için user ID kullan
if request.user and request.user.is_authenticated:
ident = request.user.pk
else:
# Anonymous kullanıcı için gerçek client IP'yi kullan
ident = self.get_client_ip(request)
# Cache key format: throttle_user_{user_id} veya throttle_anon_{ip}
return self.cache_format % {
'scope': self.scope,
'ident': ident
}
def allow_request(self, request, view):
# Get client IP
ip = self.get_client_ip(request)
remote_addr = request.META.get('REMOTE_ADDR', 'unknown')
host = request.get_host().split(':')[0]
path = request.path
method = request.method
user_agent = request.META.get('HTTP_USER_AGENT', 'unknown')
# View bilgisi
view_name = getattr(view, '__class__', None)
view_name = view_name.__name__ if view_name else 'unknown'
# User bilgisi
user_info = 'anonymous'
if request.user and request.user.is_authenticated:
user_info = f"user_id:{request.user.id} | email:{getattr(request.user, 'email', 'N/A')} | staff:{request.user.is_staff}"
# Cloudflare kontrolü
is_from_cloudflare = is_cloudflare_ip(remote_addr)
cf_info = f"CF-IP: {remote_addr}" if is_from_cloudflare else ""
# Host kontrolü
if host in self.EXEMPT_HOSTS:
logger.info(
f"[USER THROTTLE - BYPASS (HOST)] Real-IP: {ip} | {cf_info} | Host: {host} | "
f"Path: {path} | Method: {method} | View: {view_name} | "
f"User: {user_info} | User-Agent: {user_agent[:100]}"
)
return True
# Authenticated kullanıcı için throttling yapma (staff users)
if request.user and request.user.is_authenticated and request.user.is_staff:
logger.info(
f"[USER THROTTLE - BYPASS (STAFF)] Real-IP: {ip} | {cf_info} | Host: {host} | "
f"Path: {path} | Method: {method} | View: {view_name} | "
f"User: {user_info} | User-Agent: {user_agent[:100]}"
)
return True
# Normal throttling kurallarını uygula
allowed = super().allow_request(request, view)
# Throttle durumunu kontrol et
if allowed:
# Rate limit bilgilerini al
throttle_scope = getattr(view, 'throttle_scope', None) or 'user'
rate = self.get_rate()
num_requests, duration = self.parse_rate(rate)
# Cache key'den kalan istek sayısını tahmin et
cache_key = self.get_cache_key(request, view)
history = self.cache.get(cache_key, [])
remaining = max(0, num_requests - len(history))
logger.info(
f"[USER THROTTLE - ALLOWED] Real-IP: {ip} | {cf_info} | Host: {host} | "
f"Path: {path} | Method: {method} | View: {view_name} | "
f"User: {user_info} | Rate: {rate} | Remaining: {remaining}/{num_requests} | "
f"User-Agent: {user_agent[:100]}"
)
else:
# Throttle limit aşıldı
rate = self.get_rate()
num_requests, duration = self.parse_rate(rate)
logger.warning(
f"[USER THROTTLE - BLOCKED] Real-IP: {ip} | {cf_info} | Host: {host} | "
f"Path: {path} | Method: {method} | View: {view_name} | "
f"User: {user_info} | Rate: {rate} | Limit: {num_requests}/{duration} | "
f"User-Agent: {user_agent[:100]}"
)
return allowed

38
core/urls.py Normal file
View File

@@ -0,0 +1,38 @@
"""
URL configuration for core project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/6.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
# Main API routes (includes SPA pages)
# path('api/v1/', include('accounts.urls')),
path('api/v1/', include('settings.urls')),
path('api/v1/', include('home.urls')),
path('api/v1/', include('portfolio.urls')),
path('api/v1/', include('contact.urls')),
path('api/v1/auth/', include('djoser.urls.jwt')),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)

40
core/utils.py Normal file
View File

@@ -0,0 +1,40 @@
import os
import uuid
from django.utils.deconstruct import deconstructible
from imagekit.processors import ResizeToFill
class ConvertToRGBA(object):
"""Converts an image to RGBA mode."""
def process(self, img):
if img.mode not in ('RGBA', 'LA'):
img = img.convert('RGBA')
return img
@deconstructible
class UniquePathAndRename(object):
def __init__(self, upload_to):
self.upload_to = upload_to
def __call__(self, instance, filename):
ext = filename.split('.')[-1]
new_filename = f"{uuid.uuid4().hex}.{ext}"
return os.path.join(self.upload_to, new_filename)
def image_optimizer(upload_to, width, height, quality, img_format):
"""
ProcessedImageField için gerekli olan `upload_to`, `processors`, `format`
ve `options` parametrelerini dinamik olarak oluşturur.
"""
processors = [ResizeToFill(width, height)]
if img_format == 'PNG':
processors.insert(0, ConvertToRGBA())
return {
'upload_to': UniquePathAndRename(upload_to),
'processors': processors,
'format': img_format,
'options': {'quality': quality}
}

16
core/wsgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
WSGI config for core 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/6.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
application = get_wsgi_application()