This commit is contained in:
september
2025-09-03 06:51:24 -07:00
commit 54a81cbdcf
95 changed files with 17637 additions and 0 deletions

View File

180
backend/apps/core/admin.py Normal file
View 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 = 'Управление системой видеозвонков'

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.core'

View 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')],
},
),
]

View File

@@ -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'},
),
]

View File

View 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')}"

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

10
backend/apps/core/urls.py Normal file
View 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
View 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