init
This commit is contained in:
0
backend/apps/core/__init__.py
Normal file
0
backend/apps/core/__init__.py
Normal file
180
backend/apps/core/admin.py
Normal file
180
backend/apps/core/admin.py
Normal file
@@ -0,0 +1,180 @@
|
||||
# apps/core/admin.py - Админка для управления системными настройками
|
||||
from django import forms
|
||||
from django.contrib import admin
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.hashers import make_password
|
||||
from django.utils.html import format_html
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.core.exceptions import ValidationError
|
||||
from .models import SystemSettings, RoomActivityLog
|
||||
|
||||
|
||||
class SystemSettingsForm(forms.ModelForm):
|
||||
"""Кастомная форма для системных настроек"""
|
||||
new_password = forms.CharField(
|
||||
widget=forms.PasswordInput,
|
||||
required=False,
|
||||
label='Новый пароль',
|
||||
help_text='Введите новый пароль для входа в систему (минимум 6 символов)'
|
||||
)
|
||||
confirm_password = forms.CharField(
|
||||
widget=forms.PasswordInput,
|
||||
required=False,
|
||||
label='Подтвердите пароль',
|
||||
help_text='Повторите новый пароль'
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = SystemSettings
|
||||
fields = ['is_active']
|
||||
|
||||
def clean(self):
|
||||
"""Валидация формы"""
|
||||
cleaned_data = super().clean()
|
||||
new_password = cleaned_data.get('new_password')
|
||||
confirm_password = cleaned_data.get('confirm_password')
|
||||
|
||||
if new_password or confirm_password:
|
||||
if not new_password:
|
||||
raise ValidationError('Введите новый пароль')
|
||||
|
||||
if not confirm_password:
|
||||
raise ValidationError('Подтвердите пароль')
|
||||
|
||||
if new_password != confirm_password:
|
||||
raise ValidationError('Пароли не совпадают')
|
||||
|
||||
if len(new_password) < 6:
|
||||
raise ValidationError('Пароль должен содержать минимум 6 символов')
|
||||
|
||||
return cleaned_data
|
||||
|
||||
|
||||
@admin.register(SystemSettings)
|
||||
class SystemSettingsAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Админ-интерфейс для управления системными настройками
|
||||
"""
|
||||
form = SystemSettingsForm
|
||||
list_display = ('id', 'is_active', 'created_at', 'updated_at', 'password_status')
|
||||
list_filter = ('is_active', 'created_at', 'updated_at')
|
||||
readonly_fields = ('created_at', 'updated_at', 'password_hash_preview')
|
||||
|
||||
fieldsets = (
|
||||
('Основные настройки', {
|
||||
'fields': ('is_active',)
|
||||
}),
|
||||
('Безопасность', {
|
||||
'fields': ('new_password', 'confirm_password', 'password_hash_preview'),
|
||||
'description': 'Установите новый пароль для доступа к системе'
|
||||
}),
|
||||
('Информация', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
})
|
||||
)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""Обработка сохранения с проверкой пароля"""
|
||||
new_password = form.cleaned_data.get('new_password')
|
||||
|
||||
if new_password:
|
||||
obj.set_password(new_password)
|
||||
self.message_user(
|
||||
request,
|
||||
'Пароль успешно изменен',
|
||||
level=messages.SUCCESS
|
||||
)
|
||||
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
def password_status(self, obj):
|
||||
"""Статус пароля"""
|
||||
if obj.access_password_hash:
|
||||
return format_html(
|
||||
'<span style="color: green;">✓ Установлен</span>'
|
||||
)
|
||||
return format_html(
|
||||
'<span style="color: red;">✗ Не установлен</span>'
|
||||
)
|
||||
password_status.short_description = 'Статус пароля'
|
||||
|
||||
def password_hash_preview(self, obj):
|
||||
"""Превью хеша пароля"""
|
||||
if obj.access_password_hash:
|
||||
hash_preview = obj.access_password_hash[:20] + '...' if len(obj.access_password_hash) > 20 else obj.access_password_hash
|
||||
return format_html(
|
||||
'<code style="background: #f8f9fa; padding: 2px 6px; border-radius: 3px;">{}</code>',
|
||||
hash_preview
|
||||
)
|
||||
return 'Пароль не установлен'
|
||||
password_hash_preview.short_description = 'Хеш пароля'
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Ограничиваем создание - должен быть только один экземпляр настроек"""
|
||||
if SystemSettings.objects.exists():
|
||||
return False
|
||||
return True
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
"""Запрещаем удаление системных настроек"""
|
||||
return False
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Возвращаем queryset"""
|
||||
return super().get_queryset(request)
|
||||
|
||||
class Media:
|
||||
css = {
|
||||
'all': ('admin/css/custom_admin.css',)
|
||||
}
|
||||
|
||||
|
||||
@admin.register(RoomActivityLog)
|
||||
class RoomActivityLogAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Админ-интерфейс для просмотра логов активности комнат
|
||||
"""
|
||||
list_display = ('room_id_short', 'action', 'participant_count', 'timestamp', 'ip_address')
|
||||
list_filter = ('action', 'timestamp')
|
||||
search_fields = ('room_id',)
|
||||
readonly_fields = ('room_id', 'action', 'timestamp', 'participant_count', 'ip_address', 'user_agent_hash')
|
||||
date_hierarchy = 'timestamp'
|
||||
ordering = ('-timestamp',)
|
||||
|
||||
def room_id_short(self, obj):
|
||||
"""Короткое отображение ID комнаты"""
|
||||
if obj.room_id:
|
||||
return f"{obj.room_id[:8]}..."
|
||||
return 'N/A'
|
||||
room_id_short.short_description = 'Room ID'
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""Запрещаем ручное добавление логов"""
|
||||
return False
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
"""Запрещаем изменение логов"""
|
||||
return False
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
"""Разрешаем удаление только суперпользователям"""
|
||||
return request.user.is_superuser
|
||||
|
||||
# Дополнительные действия
|
||||
actions = ['delete_old_logs']
|
||||
|
||||
def delete_old_logs(self, request, queryset):
|
||||
"""Удаление старых логов"""
|
||||
from datetime import datetime, timedelta
|
||||
old_date = datetime.now() - timedelta(days=30)
|
||||
count = RoomActivityLog.objects.filter(timestamp__lt=old_date).delete()[0]
|
||||
self.message_user(request, f'Удалено {count} старых записей (старше 30 дней)')
|
||||
delete_old_logs.short_description = 'Удалить логи старше 30 дней'
|
||||
|
||||
|
||||
# Кастомизация админки
|
||||
admin.site.site_header = 'Video Call Administration'
|
||||
admin.site.site_title = 'Video Call Admin'
|
||||
admin.site.index_title = 'Управление системой видеозвонков'
|
||||
6
backend/apps/core/apps.py
Normal file
6
backend/apps/core/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CoreConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.core'
|
||||
44
backend/apps/core/migrations/0001_initial.py
Normal file
44
backend/apps/core/migrations/0001_initial.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# Generated by Django 5.2.5 on 2025-09-01 07:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SystemSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('access_password_hash', models.CharField(help_text='Hashed password for system access', max_length=255)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'System Settings',
|
||||
'verbose_name_plural': 'System Settings',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RoomActivityLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('room_id', models.CharField(db_index=True, max_length=36)),
|
||||
('action', models.CharField(choices=[('created', 'Room Created'), ('joined', 'User Joined'), ('left', 'User Left'), ('expired', 'Room Expired'), ('deleted', 'Room Deleted')], max_length=20)),
|
||||
('timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('participant_count', models.PositiveIntegerField(default=0)),
|
||||
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
|
||||
('user_agent_hash', models.CharField(blank=True, help_text='Hashed user agent for basic analytics', max_length=64, null=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-timestamp'],
|
||||
'indexes': [models.Index(fields=['room_id', '-timestamp'], name='core_roomac_room_id_3e3535_idx'), models.Index(fields=['action', '-timestamp'], name='core_roomac_action_9fbe2f_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2.5 on 2025-09-01 08:30
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='roomactivitylog',
|
||||
options={'ordering': ['-timestamp'], 'verbose_name': 'Room Activity Log', 'verbose_name_plural': 'Room Activity Logs'},
|
||||
),
|
||||
]
|
||||
0
backend/apps/core/migrations/__init__.py
Normal file
0
backend/apps/core/migrations/__init__.py
Normal file
92
backend/apps/core/models.py
Normal file
92
backend/apps/core/models.py
Normal file
@@ -0,0 +1,92 @@
|
||||
# core/models.py - Core system models for video call application
|
||||
from django.db import models
|
||||
from django.contrib.auth.hashers import make_password, check_password
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class SystemSettings(models.Model):
|
||||
"""
|
||||
Model for storing system-wide settings like access password.
|
||||
Follows singleton pattern - only one instance should exist.
|
||||
"""
|
||||
access_password_hash = models.CharField(
|
||||
max_length=255,
|
||||
help_text="Hashed password for system access"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "System Settings"
|
||||
verbose_name_plural = "System Settings"
|
||||
|
||||
def set_password(self, raw_password):
|
||||
"""Set and hash the access password"""
|
||||
self.access_password_hash = make_password(raw_password)
|
||||
|
||||
def check_password(self, raw_password):
|
||||
"""Check if provided password matches stored hash"""
|
||||
return check_password(raw_password, self.access_password_hash)
|
||||
|
||||
@classmethod
|
||||
def get_settings(cls):
|
||||
"""Get or create system settings instance"""
|
||||
settings, created = cls.objects.get_or_create(
|
||||
pk=1,
|
||||
defaults={
|
||||
'access_password_hash': make_password('admin123'),
|
||||
'is_active': True
|
||||
}
|
||||
)
|
||||
return settings
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Ensure only one settings instance exists"""
|
||||
self.pk = 1
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
"""Prevent deletion of system settings"""
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return f"System Settings (Updated: {self.updated_at.strftime('%Y-%m-%d %H:%M')})"
|
||||
|
||||
|
||||
class RoomActivityLog(models.Model):
|
||||
"""
|
||||
Model for logging room activities without storing personal data.
|
||||
Used for analytics and system monitoring.
|
||||
"""
|
||||
ACTION_CHOICES = [
|
||||
('created', 'Room Created'),
|
||||
('joined', 'User Joined'),
|
||||
('left', 'User Left'),
|
||||
('expired', 'Room Expired'),
|
||||
('deleted', 'Room Deleted'),
|
||||
]
|
||||
|
||||
room_id = models.CharField(max_length=36, db_index=True) # UUID
|
||||
action = models.CharField(max_length=20, choices=ACTION_CHOICES)
|
||||
timestamp = models.DateTimeField(auto_now_add=True)
|
||||
participant_count = models.PositiveIntegerField(default=0)
|
||||
ip_address = models.GenericIPAddressField(null=True, blank=True)
|
||||
user_agent_hash = models.CharField(
|
||||
max_length=64,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Hashed user agent for basic analytics"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-timestamp']
|
||||
indexes = [
|
||||
models.Index(fields=['room_id', '-timestamp']),
|
||||
models.Index(fields=['action', '-timestamp']),
|
||||
]
|
||||
verbose_name = "Room Activity Log"
|
||||
verbose_name_plural = "Room Activity Logs"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_action_display()} - {self.room_id[:8]}... at {self.timestamp.strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
3
backend/apps/core/tests.py
Normal file
3
backend/apps/core/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
10
backend/apps/core/urls.py
Normal file
10
backend/apps/core/urls.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# core/urls.py - Core application URL patterns
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'core'
|
||||
|
||||
urlpatterns = [
|
||||
path('health/', views.health_check, name='health'),
|
||||
path('csrf/', views.get_csrf_token, name='csrf'),
|
||||
]
|
||||
243
backend/apps/core/views.py
Normal file
243
backend/apps/core/views.py
Normal file
@@ -0,0 +1,243 @@
|
||||
# apps/core/views.py - Core application views including health check
|
||||
from django.http import JsonResponse
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
|
||||
from django.utils import timezone
|
||||
from django.db import connection
|
||||
from django.core.cache import cache
|
||||
from django.conf import settings
|
||||
from django.middleware.csrf import get_token
|
||||
import logging
|
||||
import sys
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
@csrf_exempt
|
||||
def health_check(request):
|
||||
"""
|
||||
Health check endpoint for monitoring system status.
|
||||
Returns detailed health information about various components.
|
||||
"""
|
||||
try:
|
||||
health_data = {
|
||||
'status': 'healthy',
|
||||
'timestamp': timezone.now().isoformat(),
|
||||
'version': getattr(settings, 'VERSION', '1.0.0'),
|
||||
'environment': 'production' if not settings.DEBUG else 'development',
|
||||
'checks': {}
|
||||
}
|
||||
|
||||
overall_status = True
|
||||
|
||||
# Database health check
|
||||
try:
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT 1")
|
||||
cursor.fetchone()
|
||||
health_data['checks']['database'] = {
|
||||
'status': 'healthy',
|
||||
'message': 'Database connection successful'
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Database health check failed: {e}")
|
||||
health_data['checks']['database'] = {
|
||||
'status': 'unhealthy',
|
||||
'message': f'Database connection failed: {str(e)}'
|
||||
}
|
||||
overall_status = False
|
||||
|
||||
# Redis/Cache health check
|
||||
try:
|
||||
cache.set('health_check', 'test', 30)
|
||||
cached_value = cache.get('health_check')
|
||||
if cached_value == 'test':
|
||||
health_data['checks']['cache'] = {
|
||||
'status': 'healthy',
|
||||
'message': 'Cache (Redis) connection successful'
|
||||
}
|
||||
else:
|
||||
raise Exception("Cache test failed")
|
||||
except Exception as e:
|
||||
logger.error(f"Cache health check failed: {e}")
|
||||
health_data['checks']['cache'] = {
|
||||
'status': 'unhealthy',
|
||||
'message': f'Cache connection failed: {str(e)}'
|
||||
}
|
||||
overall_status = False
|
||||
|
||||
# System resources check
|
||||
try:
|
||||
import psutil
|
||||
memory_usage = psutil.virtual_memory().percent
|
||||
cpu_usage = psutil.cpu_percent(interval=1)
|
||||
disk_usage = psutil.disk_usage('/').percent
|
||||
|
||||
health_data['checks']['system'] = {
|
||||
'status': 'healthy' if memory_usage < 90 and cpu_usage < 90 and disk_usage < 90 else 'warning',
|
||||
'memory_usage': f"{memory_usage}%",
|
||||
'cpu_usage': f"{cpu_usage}%",
|
||||
'disk_usage': f"{disk_usage}%"
|
||||
}
|
||||
|
||||
if memory_usage > 95 or cpu_usage > 95 or disk_usage > 95:
|
||||
overall_status = False
|
||||
except ImportError:
|
||||
health_data['checks']['system'] = {
|
||||
'status': 'unavailable',
|
||||
'message': 'psutil not installed - system metrics unavailable'
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"System health check failed: {e}")
|
||||
health_data['checks']['system'] = {
|
||||
'status': 'unhealthy',
|
||||
'message': f'System check failed: {str(e)}'
|
||||
}
|
||||
|
||||
# WebSocket/Channels check
|
||||
try:
|
||||
from channels.layers import get_channel_layer
|
||||
channel_layer = get_channel_layer()
|
||||
if channel_layer:
|
||||
health_data['checks']['websocket'] = {
|
||||
'status': 'healthy',
|
||||
'message': 'WebSocket layer available'
|
||||
}
|
||||
else:
|
||||
raise Exception("Channel layer not configured")
|
||||
except Exception as e:
|
||||
logger.error(f"WebSocket health check failed: {e}")
|
||||
health_data['checks']['websocket'] = {
|
||||
'status': 'unhealthy',
|
||||
'message': f'WebSocket check failed: {str(e)}'
|
||||
}
|
||||
overall_status = False
|
||||
|
||||
# Update overall status
|
||||
health_data['status'] = 'healthy' if overall_status else 'unhealthy'
|
||||
|
||||
# Return appropriate HTTP status code
|
||||
status_code = 200 if overall_status else 503
|
||||
|
||||
return JsonResponse(health_data, status=status_code)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Health check endpoint failed: {e}")
|
||||
return JsonResponse({
|
||||
'status': 'error',
|
||||
'message': 'Health check failed',
|
||||
'error': str(e),
|
||||
'timestamp': timezone.now().isoformat()
|
||||
}, status=500)
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
@csrf_exempt
|
||||
def system_info(request):
|
||||
"""
|
||||
System information endpoint for monitoring and debugging.
|
||||
Only available in DEBUG mode or to authenticated users.
|
||||
"""
|
||||
# Security check - only allow in development or for authenticated requests
|
||||
if not settings.DEBUG and not request.session.get('authenticated'):
|
||||
return JsonResponse({'error': 'Access denied'}, status=403)
|
||||
|
||||
try:
|
||||
system_data = {
|
||||
'python_version': sys.version,
|
||||
'django_version': __import__('django').get_version(),
|
||||
'debug_mode': settings.DEBUG,
|
||||
'timezone': str(timezone.get_current_timezone()),
|
||||
'database_engine': settings.DATABASES['default']['ENGINE'],
|
||||
'installed_apps': list(settings.INSTALLED_APPS),
|
||||
'middleware': list(settings.MIDDLEWARE),
|
||||
}
|
||||
|
||||
# Add more detailed info in DEBUG mode
|
||||
if settings.DEBUG:
|
||||
system_data.update({
|
||||
'secret_key_length': len(settings.SECRET_KEY),
|
||||
'allowed_hosts': settings.ALLOWED_HOSTS,
|
||||
'static_url': settings.STATIC_URL,
|
||||
'media_url': settings.MEDIA_URL,
|
||||
})
|
||||
|
||||
return JsonResponse(system_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"System info endpoint failed: {e}")
|
||||
return JsonResponse({
|
||||
'error': 'Failed to retrieve system information',
|
||||
'message': str(e)
|
||||
}, status=500)
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
@csrf_exempt
|
||||
def metrics(request):
|
||||
"""
|
||||
Basic metrics endpoint for monitoring.
|
||||
"""
|
||||
try:
|
||||
from apps.core.models import RoomActivityLog
|
||||
from datetime import timedelta
|
||||
|
||||
# Calculate basic metrics
|
||||
now = timezone.now()
|
||||
last_hour = now - timedelta(hours=1)
|
||||
last_day = now - timedelta(days=1)
|
||||
|
||||
metrics_data = {
|
||||
'timestamp': now.isoformat(),
|
||||
'rooms': {
|
||||
'created_last_hour': RoomActivityLog.objects.filter(
|
||||
action='created',
|
||||
timestamp__gte=last_hour
|
||||
).count(),
|
||||
'created_last_day': RoomActivityLog.objects.filter(
|
||||
action='created',
|
||||
timestamp__gte=last_day
|
||||
).count(),
|
||||
'total_created': RoomActivityLog.objects.filter(
|
||||
action='created'
|
||||
).count(),
|
||||
},
|
||||
'users': {
|
||||
'joined_last_hour': RoomActivityLog.objects.filter(
|
||||
action='joined',
|
||||
timestamp__gte=last_hour
|
||||
).count(),
|
||||
'joined_last_day': RoomActivityLog.objects.filter(
|
||||
action='joined',
|
||||
timestamp__gte=last_day
|
||||
).count(),
|
||||
}
|
||||
}
|
||||
|
||||
return JsonResponse(metrics_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Metrics endpoint failed: {e}")
|
||||
return JsonResponse({
|
||||
'error': 'Failed to retrieve metrics',
|
||||
'message': str(e)
|
||||
}, status=500)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(["GET"])
|
||||
def get_csrf_token(request):
|
||||
"""Get CSRF token for API requests"""
|
||||
token = get_token(request)
|
||||
return JsonResponse({'csrfToken': token})
|
||||
|
||||
|
||||
def get_client_ip(request):
|
||||
"""Extract client IP address from request"""
|
||||
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
|
||||
if x_forwarded_for:
|
||||
ip = x_forwarded_for.split(',')[0].strip()
|
||||
else:
|
||||
ip = request.META.get('REMOTE_ADDR')
|
||||
return ip
|
||||
Reference in New Issue
Block a user