init
This commit is contained in:
37
backend/Dockerfile
Normal file
37
backend/Dockerfile
Normal file
@@ -0,0 +1,37 @@
|
||||
FROM python:3.11-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Установка системных зависимостей
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libpq-dev \
|
||||
gcc \
|
||||
gettext \
|
||||
vim \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Копирование и установка Python зависимостей
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Дополнительно устанавливаем daphne если его нет в requirements.txt
|
||||
RUN pip install daphne
|
||||
|
||||
# Создание непривилегированного пользователя с определенным UID/GID
|
||||
RUN groupadd -r appuser -g 1000 && useradd -r -g appuser -u 1000 appuser
|
||||
|
||||
# Копирование исходного кода
|
||||
COPY . .
|
||||
|
||||
# Создание директорий для статических файлов, медиа и логов с правильными правами
|
||||
RUN mkdir -p /app/staticfiles /app/media /app/logs && \
|
||||
chown -R appuser:appuser /app && \
|
||||
chmod -R 755 /app/logs
|
||||
|
||||
# Переключение на непривилегированного пользователя
|
||||
USER appuser
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
# Команда по умолчанию - запуск через daphne для поддержки WebSocket
|
||||
CMD ["daphne", "-b", "0.0.0.0", "-p", "8000", "videocall_app.asgi:application"]
|
||||
0
backend/apps/__init__.py
Normal file
0
backend/apps/__init__.py
Normal file
0
backend/apps/authentication/__init__.py
Normal file
0
backend/apps/authentication/__init__.py
Normal file
3
backend/apps/authentication/admin.py
Normal file
3
backend/apps/authentication/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
backend/apps/authentication/apps.py
Normal file
6
backend/apps/authentication/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuthenticationConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.authentication'
|
||||
0
backend/apps/authentication/migrations/__init__.py
Normal file
0
backend/apps/authentication/migrations/__init__.py
Normal file
3
backend/apps/authentication/models.py
Normal file
3
backend/apps/authentication/models.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
3
backend/apps/authentication/tests.py
Normal file
3
backend/apps/authentication/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
11
backend/apps/authentication/urls.py
Normal file
11
backend/apps/authentication/urls.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# authentication/urls.py - Authentication URL patterns
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'authentication'
|
||||
|
||||
urlpatterns = [
|
||||
path('login/', views.login_view, name='login'),
|
||||
path('logout/', views.logout_view, name='logout'),
|
||||
path('check/', views.check_auth_view, name='check'),
|
||||
]
|
||||
127
backend/apps/authentication/views.py
Normal file
127
backend/apps/authentication/views.py
Normal file
@@ -0,0 +1,127 @@
|
||||
# authentication/views.py - Authentication API views
|
||||
import hashlib
|
||||
from django.contrib.auth import login, logout
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.sessions.models import Session
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
from django_ratelimit.decorators import ratelimit
|
||||
from apps.core.models import SystemSettings
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([AllowAny])
|
||||
@csrf_exempt
|
||||
@ratelimit(key='ip', rate='10/min', method='POST', block=True)
|
||||
def login_view(request):
|
||||
"""
|
||||
Authenticate user with system password.
|
||||
Rate limited to prevent brute force attacks.
|
||||
"""
|
||||
try:
|
||||
password = request.data.get('password')
|
||||
|
||||
if not password:
|
||||
return Response(
|
||||
{'error': 'Password is required'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
settings_obj = SystemSettings.get_settings()
|
||||
|
||||
if settings_obj.check_password(password):
|
||||
# Create or get session
|
||||
if not request.session.session_key:
|
||||
request.session.create()
|
||||
|
||||
# Store authentication in session
|
||||
request.session['authenticated'] = True
|
||||
request.session['auth_timestamp'] = timezone.now().isoformat()
|
||||
request.session['client_ip'] = get_client_ip(request)
|
||||
request.session.save()
|
||||
|
||||
logger.info(f"Successful login from IP: {get_client_ip(request)}")
|
||||
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': 'Authentication successful',
|
||||
'session_key': request.session.session_key
|
||||
})
|
||||
else:
|
||||
logger.warning(f"Failed login attempt from IP: {get_client_ip(request)}")
|
||||
return Response(
|
||||
{'error': 'Invalid password'},
|
||||
status=status.HTTP_401_UNAUTHORIZED
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Login failed: {e}")
|
||||
return Response(
|
||||
{'error': 'Authentication failed'},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([AllowAny])
|
||||
@csrf_exempt
|
||||
def logout_view(request):
|
||||
"""Clear user session and log out"""
|
||||
try:
|
||||
request.session.flush()
|
||||
logger.info("User logged out successfully")
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': 'Logged out successfully'
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Logout failed: {e}")
|
||||
return Response(
|
||||
{'error': 'Logout failed'},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([AllowAny])
|
||||
@csrf_exempt
|
||||
def check_auth_view(request):
|
||||
"""Check if user is authenticated"""
|
||||
is_authenticated = request.session.get('authenticated', False)
|
||||
|
||||
if is_authenticated:
|
||||
# Optional: Check session age for additional security
|
||||
auth_timestamp = request.session.get('auth_timestamp')
|
||||
if auth_timestamp:
|
||||
try:
|
||||
auth_time = timezone.datetime.fromisoformat(auth_timestamp)
|
||||
if timezone.now() - auth_time > timedelta(hours=24):
|
||||
request.session.flush()
|
||||
is_authenticated = False
|
||||
except (ValueError, TypeError):
|
||||
request.session.flush()
|
||||
is_authenticated = False
|
||||
|
||||
return Response({
|
||||
'authenticated': is_authenticated,
|
||||
'session_key': request.session.session_key if is_authenticated else None
|
||||
})
|
||||
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
|
||||
0
backend/apps/rooms/__init__.py
Normal file
0
backend/apps/rooms/__init__.py
Normal file
3
backend/apps/rooms/admin.py
Normal file
3
backend/apps/rooms/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
backend/apps/rooms/apps.py
Normal file
6
backend/apps/rooms/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class RoomsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'apps.rooms'
|
||||
319
backend/apps/rooms/consumers.py
Normal file
319
backend/apps/rooms/consumers.py
Normal file
@@ -0,0 +1,319 @@
|
||||
# rooms/consumers.py - WebSocket consumer for WebRTC signaling
|
||||
import json
|
||||
import logging
|
||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
from channels.db import database_sync_to_async
|
||||
from django.utils import timezone
|
||||
from apps.rooms.models import RoomManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VideoCallConsumer(AsyncWebsocketConsumer):
|
||||
"""
|
||||
WebSocket consumer for handling WebRTC signaling and room management.
|
||||
Implements secure peer-to-peer connection establishment.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.room_id = None
|
||||
self.room_group_name = None
|
||||
self.participant_id = None
|
||||
|
||||
async def connect(self):
|
||||
"""Handle WebSocket connection"""
|
||||
try:
|
||||
# Extract room ID from URL
|
||||
self.room_id = self.scope['url_route']['kwargs']['room_id']
|
||||
self.room_group_name = f'room_{self.room_id}'
|
||||
|
||||
# Get participant ID from session or generate one
|
||||
session = self.scope.get('session', {})
|
||||
self.participant_id = session.get('session_key') or f'temp_{timezone.now().timestamp()}'
|
||||
|
||||
# Verify room exists and user can join
|
||||
room_data = await self.get_room_data(self.room_id)
|
||||
|
||||
if not room_data:
|
||||
await self.close(code=4004) # Room not found
|
||||
return
|
||||
|
||||
# Check room capacity
|
||||
participants = room_data.get('participants', [])
|
||||
max_participants = room_data.get('max_participants', 2)
|
||||
|
||||
if len(participants) >= max_participants and self.participant_id not in participants:
|
||||
await self.close(code=4003) # Room is full
|
||||
return
|
||||
|
||||
# Join the room group
|
||||
await self.channel_layer.group_add(
|
||||
self.room_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
# Accept the WebSocket connection
|
||||
await self.accept()
|
||||
|
||||
# Notify other participants about new user
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
{
|
||||
'type': 'user_joined',
|
||||
'participant_id': self.participant_id,
|
||||
'timestamp': timezone.now().isoformat()
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(f"User {self.participant_id} connected to room {self.room_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"WebSocket connection error: {e}")
|
||||
await self.close(code=4000)
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
"""Handle WebSocket disconnection"""
|
||||
try:
|
||||
if self.room_group_name and self.participant_id:
|
||||
# Notify other participants about user leaving
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
{
|
||||
'type': 'user_left',
|
||||
'participant_id': self.participant_id,
|
||||
'timestamp': timezone.now().isoformat()
|
||||
}
|
||||
)
|
||||
|
||||
# Remove user from room group
|
||||
await self.channel_layer.group_discard(
|
||||
self.room_group_name,
|
||||
self.channel_name
|
||||
)
|
||||
|
||||
# Update room data
|
||||
await self.leave_room(self.room_id, self.participant_id)
|
||||
|
||||
logger.info(f"User {self.participant_id} disconnected from room {self.room_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"WebSocket disconnect error: {e}")
|
||||
|
||||
async def receive(self, text_data):
|
||||
"""Handle incoming WebSocket messages"""
|
||||
try:
|
||||
data = json.loads(text_data)
|
||||
message_type = data.get('type')
|
||||
|
||||
# Validate message structure
|
||||
if not message_type:
|
||||
await self.send_error('Message type is required')
|
||||
return
|
||||
|
||||
# Handle different message types
|
||||
if message_type == 'offer':
|
||||
await self.handle_webrtc_offer(data)
|
||||
elif message_type == 'answer':
|
||||
await self.handle_webrtc_answer(data)
|
||||
elif message_type == 'ice_candidate':
|
||||
await self.handle_ice_candidate(data)
|
||||
elif message_type == 'ping':
|
||||
await self.handle_ping()
|
||||
elif message_type == 'media_state':
|
||||
await self.handle_media_state(data)
|
||||
else:
|
||||
await self.send_error(f'Unknown message type: {message_type}')
|
||||
|
||||
except json.JSONDecodeError:
|
||||
await self.send_error('Invalid JSON format')
|
||||
except Exception as e:
|
||||
logger.error(f"WebSocket receive error: {e}")
|
||||
await self.send_error('Message processing failed')
|
||||
|
||||
async def handle_webrtc_offer(self, data):
|
||||
"""Handle WebRTC offer from peer"""
|
||||
try:
|
||||
target_participant = data.get('target')
|
||||
offer = data.get('offer')
|
||||
|
||||
if not offer:
|
||||
await self.send_error('Offer data is required')
|
||||
return
|
||||
|
||||
# Forward offer to target participant or broadcast to room
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
{
|
||||
'type': 'webrtc_offer',
|
||||
'offer': offer,
|
||||
'sender': self.participant_id,
|
||||
'target': target_participant,
|
||||
'timestamp': timezone.now().isoformat()
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"WebRTC offer handling error: {e}")
|
||||
await self.send_error('Failed to process offer')
|
||||
|
||||
async def handle_webrtc_answer(self, data):
|
||||
"""Handle WebRTC answer from peer"""
|
||||
try:
|
||||
target_participant = data.get('target')
|
||||
answer = data.get('answer')
|
||||
|
||||
if not answer:
|
||||
await self.send_error('Answer data is required')
|
||||
return
|
||||
|
||||
# Forward answer to target participant
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
{
|
||||
'type': 'webrtc_answer',
|
||||
'answer': answer,
|
||||
'sender': self.participant_id,
|
||||
'target': target_participant,
|
||||
'timestamp': timezone.now().isoformat()
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"WebRTC answer handling error: {e}")
|
||||
await self.send_error('Failed to process answer')
|
||||
|
||||
async def handle_ice_candidate(self, data):
|
||||
"""Handle ICE candidate exchange"""
|
||||
try:
|
||||
target_participant = data.get('target')
|
||||
candidate = data.get('candidate')
|
||||
|
||||
if not candidate:
|
||||
await self.send_error('ICE candidate data is required')
|
||||
return
|
||||
|
||||
# Forward ICE candidate to target participant
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
{
|
||||
'type': 'ice_candidate',
|
||||
'candidate': candidate,
|
||||
'sender': self.participant_id,
|
||||
'target': target_participant,
|
||||
'timestamp': timezone.now().isoformat()
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"ICE candidate handling error: {e}")
|
||||
await self.send_error('Failed to process ICE candidate')
|
||||
|
||||
async def handle_ping(self):
|
||||
"""Handle ping message for connection health check"""
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'pong',
|
||||
'timestamp': timezone.now().isoformat()
|
||||
}))
|
||||
|
||||
async def handle_media_state(self, data):
|
||||
"""Handle media state changes (mute/unmute, video on/off)"""
|
||||
try:
|
||||
media_state = data.get('state', {})
|
||||
|
||||
# Broadcast media state to other participants
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
{
|
||||
'type': 'media_state_update',
|
||||
'participant_id': self.participant_id,
|
||||
'state': media_state,
|
||||
'timestamp': timezone.now().isoformat()
|
||||
}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Media state handling error: {e}")
|
||||
await self.send_error('Failed to process media state')
|
||||
|
||||
# Group message handlers
|
||||
async def user_joined(self, event):
|
||||
"""Send user joined notification"""
|
||||
if event['participant_id'] != self.participant_id:
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'user_joined',
|
||||
'participant_id': event['participant_id'],
|
||||
'timestamp': event['timestamp']
|
||||
}))
|
||||
|
||||
async def user_left(self, event):
|
||||
"""Send user left notification"""
|
||||
if event['participant_id'] != self.participant_id:
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'user_left',
|
||||
'participant_id': event['participant_id'],
|
||||
'timestamp': event['timestamp']
|
||||
}))
|
||||
|
||||
async def webrtc_offer(self, event):
|
||||
"""Forward WebRTC offer to client"""
|
||||
# Only send to target participant or broadcast if no target specified
|
||||
if not event.get('target') or event['target'] == self.participant_id:
|
||||
if event['sender'] != self.participant_id:
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'webrtc_offer',
|
||||
'offer': event['offer'],
|
||||
'sender': event['sender'],
|
||||
'timestamp': event['timestamp']
|
||||
}))
|
||||
|
||||
async def webrtc_answer(self, event):
|
||||
"""Forward WebRTC answer to client"""
|
||||
if event.get('target') == self.participant_id:
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'webrtc_answer',
|
||||
'answer': event['answer'],
|
||||
'sender': event['sender'],
|
||||
'timestamp': event['timestamp']
|
||||
}))
|
||||
|
||||
async def ice_candidate(self, event):
|
||||
"""Forward ICE candidate to client"""
|
||||
# Only send to target participant or broadcast if no target specified
|
||||
if not event.get('target') or event['target'] == self.participant_id:
|
||||
if event['sender'] != self.participant_id:
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'ice_candidate',
|
||||
'candidate': event['candidate'],
|
||||
'sender': event['sender'],
|
||||
'timestamp': event['timestamp']
|
||||
}))
|
||||
|
||||
async def media_state_update(self, event):
|
||||
"""Forward media state update to client"""
|
||||
if event['participant_id'] != self.participant_id:
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'media_state_update',
|
||||
'participant_id': event['participant_id'],
|
||||
'state': event['state'],
|
||||
'timestamp': event['timestamp']
|
||||
}))
|
||||
|
||||
# Helper methods
|
||||
async def send_error(self, error_message):
|
||||
"""Send error message to client"""
|
||||
await self.send(text_data=json.dumps({
|
||||
'type': 'error',
|
||||
'message': error_message,
|
||||
'timestamp': timezone.now().isoformat()
|
||||
}))
|
||||
|
||||
@database_sync_to_async
|
||||
def get_room_data(self, room_id):
|
||||
"""Get room data from Redis"""
|
||||
return RoomManager.get_room_by_id(room_id)
|
||||
|
||||
@database_sync_to_async
|
||||
def leave_room(self, room_id, participant_id):
|
||||
"""Remove participant from room"""
|
||||
return RoomManager.leave_room(room_id, participant_id)
|
||||
0
backend/apps/rooms/migrations/__init__.py
Normal file
0
backend/apps/rooms/migrations/__init__.py
Normal file
223
backend/apps/rooms/models.py
Normal file
223
backend/apps/rooms/models.py
Normal file
@@ -0,0 +1,223 @@
|
||||
# rooms/models.py - Room management models
|
||||
import uuid
|
||||
import string
|
||||
import secrets
|
||||
from datetime import timedelta
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class RoomManager:
|
||||
"""
|
||||
Manager class for room operations using Redis for temporary storage.
|
||||
Implements all CRUD operations for video call rooms.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _get_redis_client():
|
||||
"""Get Redis client instance"""
|
||||
from django.core.cache import cache
|
||||
return cache
|
||||
|
||||
@classmethod
|
||||
def generate_short_code(cls, length=None):
|
||||
"""Generate a unique short code for room access"""
|
||||
length = length or getattr(settings, 'SHORT_CODE_LENGTH', 6)
|
||||
characters = string.ascii_uppercase + string.digits
|
||||
|
||||
# Ensure uniqueness by checking existing codes
|
||||
cache = cls._get_redis_client()
|
||||
max_attempts = 100
|
||||
|
||||
for _ in range(max_attempts):
|
||||
code = ''.join(secrets.choice(characters) for _ in range(length))
|
||||
if not cache.get(f'room_code_{code}'):
|
||||
return code
|
||||
|
||||
raise ValueError("Unable to generate unique short code")
|
||||
|
||||
@classmethod
|
||||
def create_room(cls, creator_ip=None):
|
||||
"""Create a new video call room"""
|
||||
cache = cls._get_redis_client()
|
||||
|
||||
room_data = {
|
||||
'room_id': str(uuid.uuid4()),
|
||||
'short_code': cls.generate_short_code(),
|
||||
'created_at': timezone.now().isoformat(),
|
||||
'participants': [],
|
||||
'is_active': True,
|
||||
'expires_at': (
|
||||
timezone.now() +
|
||||
timedelta(hours=getattr(settings, 'ROOM_EXPIRY_HOURS', 24))
|
||||
).isoformat(),
|
||||
'creator_ip': creator_ip,
|
||||
'max_participants': getattr(settings, 'MAX_PARTICIPANTS_PER_ROOM', 2)
|
||||
}
|
||||
|
||||
# Store room data with expiration
|
||||
cache.set(
|
||||
f'room_{room_data["room_id"]}',
|
||||
room_data,
|
||||
timeout=getattr(settings, 'ROOM_EXPIRY_HOURS', 24) * 3600
|
||||
)
|
||||
|
||||
# Create code mapping for easy lookup
|
||||
cache.set(
|
||||
f'room_code_{room_data["short_code"]}',
|
||||
room_data["room_id"],
|
||||
timeout=getattr(settings, 'ROOM_EXPIRY_HOURS', 24) * 3600
|
||||
)
|
||||
|
||||
# Log room creation
|
||||
from apps.core.models import RoomActivityLog
|
||||
RoomActivityLog.objects.create(
|
||||
room_id=room_data["room_id"],
|
||||
action='created',
|
||||
ip_address=creator_ip
|
||||
)
|
||||
|
||||
return room_data
|
||||
|
||||
@classmethod
|
||||
def get_room_by_id(cls, room_id):
|
||||
"""Retrieve room data by room ID"""
|
||||
cache = cls._get_redis_client()
|
||||
return cache.get(f'room_{room_id}')
|
||||
|
||||
@classmethod
|
||||
def get_room_by_code(cls, short_code):
|
||||
"""Retrieve room data by short code"""
|
||||
cache = cls._get_redis_client()
|
||||
room_id = cache.get(f'room_code_{short_code}')
|
||||
|
||||
if room_id:
|
||||
return cls.get_room_by_id(room_id)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def join_room(cls, room_identifier, participant_id, participant_ip=None):
|
||||
"""
|
||||
Add participant to room.
|
||||
room_identifier can be either room_id or short_code
|
||||
"""
|
||||
cache = cls._get_redis_client()
|
||||
|
||||
# Try to get room by ID first, then by code
|
||||
room_data = cls.get_room_by_id(room_identifier)
|
||||
if not room_data:
|
||||
room_data = cls.get_room_by_code(room_identifier)
|
||||
|
||||
if not room_data:
|
||||
return None, "Room not found"
|
||||
|
||||
# Check if room is active and not expired
|
||||
if not room_data.get('is_active', False):
|
||||
return None, "Room is not active"
|
||||
|
||||
expires_at = timezone.datetime.fromisoformat(
|
||||
room_data['expires_at'].replace('Z', '+00:00')
|
||||
)
|
||||
if timezone.now() > expires_at:
|
||||
cls.delete_room(room_data['room_id'])
|
||||
return None, "Room has expired"
|
||||
|
||||
# Check participant limit
|
||||
current_participants = room_data.get('participants', [])
|
||||
max_participants = room_data.get('max_participants', 2)
|
||||
|
||||
if len(current_participants) >= max_participants:
|
||||
return None, "Room is full"
|
||||
|
||||
# Add participant if not already in room
|
||||
if participant_id not in current_participants:
|
||||
current_participants.append(participant_id)
|
||||
room_data['participants'] = current_participants
|
||||
|
||||
# Update room data
|
||||
cache.set(
|
||||
f'room_{room_data["room_id"]}',
|
||||
room_data,
|
||||
timeout=getattr(settings, 'ROOM_EXPIRY_HOURS', 24) * 3600
|
||||
)
|
||||
|
||||
# Log participant join
|
||||
from apps.core.models import RoomActivityLog
|
||||
RoomActivityLog.objects.create(
|
||||
room_id=room_data["room_id"],
|
||||
action='joined',
|
||||
participant_count=len(current_participants),
|
||||
ip_address=participant_ip
|
||||
)
|
||||
|
||||
return room_data, "Successfully joined room"
|
||||
|
||||
@classmethod
|
||||
def leave_room(cls, room_id, participant_id):
|
||||
"""Remove participant from room"""
|
||||
cache = cls._get_redis_client()
|
||||
room_data = cls.get_room_by_id(room_id)
|
||||
|
||||
if not room_data:
|
||||
return False
|
||||
|
||||
participants = room_data.get('participants', [])
|
||||
if participant_id in participants:
|
||||
participants.remove(participant_id)
|
||||
room_data['participants'] = participants
|
||||
|
||||
# Update room data
|
||||
cache.set(
|
||||
f'room_{room_id}',
|
||||
room_data,
|
||||
timeout=getattr(settings, 'ROOM_EXPIRY_HOURS', 24) * 3600
|
||||
)
|
||||
|
||||
# Log participant leave
|
||||
from apps.core.models import RoomActivityLog
|
||||
RoomActivityLog.objects.create(
|
||||
room_id=room_id,
|
||||
action='left',
|
||||
participant_count=len(participants)
|
||||
)
|
||||
|
||||
# Delete room if no participants left
|
||||
if not participants:
|
||||
cls.delete_room(room_id)
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def delete_room(cls, room_id):
|
||||
"""Delete room and clean up all associated data"""
|
||||
cache = cls._get_redis_client()
|
||||
room_data = cls.get_room_by_id(room_id)
|
||||
|
||||
if room_data:
|
||||
# Remove code mapping
|
||||
short_code = room_data.get('short_code')
|
||||
if short_code:
|
||||
cache.delete(f'room_code_{short_code}')
|
||||
|
||||
# Remove room data
|
||||
cache.delete(f'room_{room_id}')
|
||||
|
||||
# Log room deletion
|
||||
from apps.core.models import RoomActivityLog
|
||||
RoomActivityLog.objects.create(
|
||||
room_id=room_id,
|
||||
action='deleted'
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def cleanup_expired_rooms(cls):
|
||||
"""Clean up expired rooms (to be called by scheduled task)"""
|
||||
# This would typically be implemented as a management command
|
||||
# or scheduled task using Django-Q or Celery
|
||||
pass
|
||||
7
backend/apps/rooms/routing.py
Normal file
7
backend/apps/rooms/routing.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# rooms/routing.py - WebSocket URL routing
|
||||
from django.urls import path
|
||||
from apps.rooms import consumers
|
||||
|
||||
websocket_urlpatterns = [
|
||||
path('ws/room/<str:room_id>/', consumers.VideoCallConsumer.as_asgi()),
|
||||
]
|
||||
3
backend/apps/rooms/tests.py
Normal file
3
backend/apps/rooms/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
14
backend/apps/rooms/urls.py
Normal file
14
backend/apps/rooms/urls.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# rooms/urls.py - Room management URL patterns
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
app_name = 'rooms'
|
||||
|
||||
urlpatterns = [
|
||||
path('create/', views.create_room, name='create'),
|
||||
path('join/', views.join_room, name='join'),
|
||||
path('<str:room_id>/', views.get_room, name='get'),
|
||||
path('<str:room_id>/leave/', views.leave_room, name='leave'),
|
||||
path('<str:room_id>/delete/', views.delete_room, name='delete'),
|
||||
]
|
||||
|
||||
293
backend/apps/rooms/views.py
Normal file
293
backend/apps/rooms/views.py
Normal file
@@ -0,0 +1,293 @@
|
||||
# rooms/views.py - Room management API views
|
||||
from django.utils import timezone
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from django_ratelimit.decorators import ratelimit
|
||||
from apps.rooms.models import RoomManager
|
||||
from apps.core.views import get_client_ip
|
||||
import qrcode
|
||||
import io
|
||||
import base64
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def require_auth(view_func):
|
||||
"""Decorator to require authentication for views"""
|
||||
def wrapper(request, *args, **kwargs):
|
||||
if not request.session.get('authenticated'):
|
||||
logger.warning(f"Unauthenticated access attempt to {request.path}")
|
||||
return Response(
|
||||
{'error': 'Authentication required'},
|
||||
status=status.HTTP_401_UNAUTHORIZED
|
||||
)
|
||||
return view_func(request, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([AllowAny])
|
||||
@csrf_exempt
|
||||
@require_auth
|
||||
@ratelimit(key='ip', rate='30/min', method='POST', block=True)
|
||||
def create_room(request):
|
||||
"""Create a new video call room"""
|
||||
try:
|
||||
client_ip = get_client_ip(request)
|
||||
logger.info(f"Creating room for IP: {client_ip}")
|
||||
|
||||
room_data = RoomManager.create_room(creator_ip=client_ip)
|
||||
|
||||
# Generate QR code for the room
|
||||
room_url = f"{request.build_absolute_uri('/')}join/{room_data['short_code']}"
|
||||
|
||||
qr = qrcode.QRCode(version=1, box_size=10, border=5)
|
||||
qr.add_data(room_url)
|
||||
qr.make(fit=True)
|
||||
|
||||
qr_image = qr.make_image(fill_color="black", back_color="white")
|
||||
buffer = io.BytesIO()
|
||||
qr_image.save(buffer, format='PNG')
|
||||
qr_code_data = base64.b64encode(buffer.getvalue()).decode()
|
||||
|
||||
response_data = {
|
||||
'room_id': room_data['room_id'],
|
||||
'short_code': room_data['short_code'],
|
||||
'room_url': room_url,
|
||||
'qr_code': f"data:image/png;base64,{qr_code_data}",
|
||||
'expires_at': room_data['expires_at'],
|
||||
'max_participants': room_data['max_participants']
|
||||
}
|
||||
|
||||
logger.info(f"Room created successfully: {room_data['room_id']}")
|
||||
return Response(response_data, status=status.HTTP_201_CREATED)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Room creation failed: {e}")
|
||||
return Response(
|
||||
{'error': 'Failed to create room'},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([AllowAny])
|
||||
@csrf_exempt
|
||||
@require_auth
|
||||
def get_room(request, room_id):
|
||||
"""Get room information by room ID"""
|
||||
try:
|
||||
logger.info(f"Getting room info for: {room_id}")
|
||||
room_data = RoomManager.get_room_by_id(room_id)
|
||||
|
||||
if not room_data:
|
||||
logger.warning(f"Room not found: {room_id}")
|
||||
return Response(
|
||||
{'error': 'Room not found'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
# Check if room has expired
|
||||
expires_at = timezone.datetime.fromisoformat(
|
||||
room_data['expires_at'].replace('Z', '+00:00')
|
||||
)
|
||||
if timezone.now() > expires_at:
|
||||
logger.info(f"Room expired, deleting: {room_id}")
|
||||
RoomManager.delete_room(room_id)
|
||||
return Response(
|
||||
{'error': 'Room has expired'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
response_data = {
|
||||
'room_id': room_data['room_id'],
|
||||
'short_code': room_data['short_code'],
|
||||
'is_active': room_data['is_active'],
|
||||
'participant_count': len(room_data.get('participants', [])),
|
||||
'max_participants': room_data.get('max_participants', 2),
|
||||
'expires_at': room_data['expires_at']
|
||||
}
|
||||
|
||||
logger.info(f"Room info retrieved: {room_id}, participants: {len(room_data.get('participants', []))}")
|
||||
return Response(response_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to retrieve room {room_id}: {e}")
|
||||
return Response(
|
||||
{'error': 'Failed to retrieve room'},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([AllowAny])
|
||||
@csrf_exempt
|
||||
@require_auth
|
||||
@ratelimit(key='ip', rate='60/min', method='POST', block=True)
|
||||
def join_room(request):
|
||||
"""Join a room by room ID or short code"""
|
||||
try:
|
||||
room_identifier = request.data.get('room_identifier')
|
||||
|
||||
# Ensure we have a session
|
||||
if not request.session.session_key:
|
||||
request.session.create()
|
||||
|
||||
participant_id = request.session.session_key
|
||||
|
||||
if not room_identifier:
|
||||
return Response(
|
||||
{'error': 'Room identifier is required'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
logger.info(f"Joining room: {room_identifier} with participant: {participant_id}")
|
||||
|
||||
client_ip = get_client_ip(request)
|
||||
room_data, message = RoomManager.join_room(
|
||||
room_identifier,
|
||||
participant_id,
|
||||
client_ip
|
||||
)
|
||||
|
||||
if not room_data:
|
||||
logger.warning(f"Failed to join room: {room_identifier}, reason: {message}")
|
||||
return Response(
|
||||
{'error': message},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
response_data = {
|
||||
'success': True,
|
||||
'message': message,
|
||||
'room_id': room_data['room_id'],
|
||||
'short_code': room_data['short_code'],
|
||||
'participant_count': len(room_data.get('participants', [])),
|
||||
'participant_id': participant_id
|
||||
}
|
||||
|
||||
logger.info(f"User joined room: {room_data['room_id']}, participant: {participant_id}")
|
||||
return Response(response_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to join room: {e}")
|
||||
return Response(
|
||||
{'error': 'Failed to join room'},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([AllowAny])
|
||||
@csrf_exempt
|
||||
@require_auth
|
||||
def leave_room(request, room_id):
|
||||
"""Leave a room"""
|
||||
try:
|
||||
# Ensure we have a session
|
||||
if not request.session.session_key:
|
||||
logger.warning(f"No session key when trying to leave room: {room_id}")
|
||||
return Response(
|
||||
{'success': True, # Return success even if no session, as user wasn't in room anyway
|
||||
'message': 'No active session found'})
|
||||
|
||||
participant_id = request.session.session_key
|
||||
logger.info(f"Leaving room: {room_id} with participant: {participant_id}")
|
||||
|
||||
# Check if room exists first
|
||||
room_data = RoomManager.get_room_by_id(room_id)
|
||||
if not room_data:
|
||||
logger.info(f"Room {room_id} not found when trying to leave")
|
||||
return Response({
|
||||
'success': True, # Return success as room doesn't exist anyway
|
||||
'message': 'Room not found'
|
||||
})
|
||||
|
||||
# Check if participant is actually in the room
|
||||
participants = room_data.get('participants', [])
|
||||
if participant_id not in participants:
|
||||
logger.info(f"Participant {participant_id} not in room {room_id}")
|
||||
return Response({
|
||||
'success': True, # Return success as user wasn't in room anyway
|
||||
'message': 'Not in room'
|
||||
})
|
||||
|
||||
success = RoomManager.leave_room(room_id, participant_id)
|
||||
|
||||
if success:
|
||||
logger.info(f"User left room: {room_id}, participant: {participant_id}")
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': 'Left room successfully'
|
||||
})
|
||||
else:
|
||||
logger.warning(f"Failed to leave room: {room_id}, participant: {participant_id}")
|
||||
return Response({
|
||||
'success': True, # Still return success to avoid client errors
|
||||
'message': 'Room leave processed'
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to leave room {room_id}: {e}")
|
||||
return Response(
|
||||
{'error': 'Failed to leave room'},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
|
||||
@api_view(['DELETE'])
|
||||
@permission_classes([AllowAny])
|
||||
@csrf_exempt
|
||||
@require_auth
|
||||
def delete_room(request, room_id):
|
||||
"""Delete a room (only creator or admin can delete)"""
|
||||
try:
|
||||
logger.info(f"Deleting room: {room_id}")
|
||||
|
||||
# Check if room exists
|
||||
room_data = RoomManager.get_room_by_id(room_id)
|
||||
if not room_data:
|
||||
return Response(
|
||||
{'error': 'Room not found'},
|
||||
status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
# Additional permission check could be added here
|
||||
success = RoomManager.delete_room(room_id)
|
||||
|
||||
if success:
|
||||
logger.info(f"Room deleted: {room_id}")
|
||||
return Response({
|
||||
'success': True,
|
||||
'message': 'Room deleted successfully'
|
||||
})
|
||||
else:
|
||||
return Response(
|
||||
{'error': 'Failed to delete room'},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete room {room_id}: {e}")
|
||||
return Response(
|
||||
{'error': 'Failed to delete room'},
|
||||
status=status.HTTP_500_INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([AllowAny])
|
||||
@csrf_exempt
|
||||
@require_auth
|
||||
def health_check(request):
|
||||
"""API health check endpoint"""
|
||||
return Response({
|
||||
'status': 'healthy',
|
||||
'timestamp': timezone.now().isoformat(),
|
||||
'authenticated': True,
|
||||
'session_key': request.session.session_key
|
||||
})
|
||||
22
backend/manage.py
Executable file
22
backend/manage.py
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'videocall_app.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
23
backend/requirements.txt
Normal file
23
backend/requirements.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
asgiref==3.9.1
|
||||
cffi==1.17.1
|
||||
channels==4.0.0
|
||||
channels-redis==4.1.0
|
||||
cryptography==41.0.7
|
||||
Django==5.2.5
|
||||
django-cors-headers==4.3.1
|
||||
django-ratelimit==4.1.0
|
||||
django-redis==5.4.0
|
||||
djangorestframework==3.14.0
|
||||
msgpack==1.1.1
|
||||
pillow==11.3.0
|
||||
psycopg2-binary==2.9.10
|
||||
pycparser==2.22
|
||||
pypng==0.20220715.0
|
||||
python-decouple==3.8
|
||||
pytz==2025.2
|
||||
qrcode==7.4.2
|
||||
redis==5.0.1
|
||||
sqlparse==0.5.3
|
||||
typing_extensions==4.15.0
|
||||
uuid==1.30
|
||||
websockets==12.0
|
||||
72
backend/run_server.py
Normal file
72
backend/run_server.py
Normal file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Server startup script with WebSocket support
|
||||
Use this instead of 'python manage.py runserver' for development
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import django
|
||||
from pathlib import Path
|
||||
|
||||
# Add the project directory to Python path
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(BASE_DIR))
|
||||
|
||||
# Set Django settings module
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'videocall_app.settings')
|
||||
|
||||
# Setup Django
|
||||
django.setup()
|
||||
|
||||
def check_daphne_installed():
|
||||
"""Check if Daphne is installed"""
|
||||
try:
|
||||
import daphne
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
def run_with_daphne():
|
||||
"""Run server with Daphne (supports WebSocket)"""
|
||||
print("🚀 Starting server with Daphne (WebSocket support enabled)")
|
||||
print(" Backend: http://localhost:8000")
|
||||
print(" WebSocket: ws://localhost:8000/ws/")
|
||||
print(" Press Ctrl+C to stop")
|
||||
print()
|
||||
|
||||
os.system('daphne -b 0.0.0.0 -p 8000 videocall_app.asgi:application')
|
||||
|
||||
def run_with_runserver():
|
||||
"""Run server with standard runserver (no WebSocket support)"""
|
||||
print("⚠️ Running with standard Django runserver")
|
||||
print(" WebSocket connections will NOT work!")
|
||||
print(" Install Daphne for WebSocket support: pip install daphne")
|
||||
print(" Backend: http://localhost:8000")
|
||||
print(" Press Ctrl+C to stop")
|
||||
print()
|
||||
|
||||
os.system('python manage.py runserver')
|
||||
|
||||
def main():
|
||||
"""Main function"""
|
||||
print("🔌 Video Call Application Server")
|
||||
print("=" * 40)
|
||||
|
||||
if check_daphne_installed():
|
||||
run_with_daphne()
|
||||
else:
|
||||
print("❌ Daphne not found - WebSocket support disabled")
|
||||
print(" Install it with: pip install daphne")
|
||||
print(" Or install from requirements: pip install -r requirements.txt")
|
||||
print()
|
||||
|
||||
response = input("Continue with standard runserver? (y/N): ")
|
||||
if response.lower() == 'y':
|
||||
run_with_runserver()
|
||||
else:
|
||||
print("Please install Daphne and try again.")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
0
backend/videocall_app/__init__.py
Normal file
0
backend/videocall_app/__init__.py
Normal file
20
backend/videocall_app/asgi.py
Normal file
20
backend/videocall_app/asgi.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# videocall_app/asgi.py - ASGI configuration for WebSocket support
|
||||
import os
|
||||
from channels.auth import AuthMiddlewareStack
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
from channels.security.websocket import AllowedHostsOriginValidator
|
||||
from django.core.asgi import get_asgi_application
|
||||
from apps.rooms.routing import websocket_urlpatterns
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'videocall_app.settings')
|
||||
|
||||
django_asgi_app = get_asgi_application()
|
||||
|
||||
application = ProtocolTypeRouter({
|
||||
"http": django_asgi_app,
|
||||
"websocket": AllowedHostsOriginValidator(
|
||||
AuthMiddlewareStack(
|
||||
URLRouter(websocket_urlpatterns)
|
||||
)
|
||||
),
|
||||
})
|
||||
303
backend/videocall_app/settings.py
Normal file
303
backend/videocall_app/settings.py
Normal file
@@ -0,0 +1,303 @@
|
||||
# videocall_app/settings.py - Django main settings configuration
|
||||
import os
|
||||
from decouple import config
|
||||
from pathlib import Path
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
# Security settings
|
||||
SECRET_KEY = config('SECRET_KEY', default='your-secret-key-here-change-in-production')
|
||||
DEBUG = config('DEBUG', default=False, cast=bool)
|
||||
|
||||
# ALLOWED_HOSTS - только ваши настоящие домены
|
||||
allowed_hosts_default = 'yourdomain.com,www.yourdomain.com,localhost,127.0.0.1'
|
||||
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default=allowed_hosts_default).split(',')
|
||||
|
||||
# Убираем пустые строки и пробелы
|
||||
ALLOWED_HOSTS = [host.strip() for host in ALLOWED_HOSTS if host.strip()]
|
||||
|
||||
# В DEBUG режиме разрешаем localhost для разработки
|
||||
if DEBUG:
|
||||
ALLOWED_HOSTS.extend(['localhost', '127.0.0.1', '0.0.0.0'])
|
||||
|
||||
# Application definition
|
||||
DJANGO_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
]
|
||||
|
||||
THIRD_PARTY_APPS = [
|
||||
'rest_framework',
|
||||
'corsheaders',
|
||||
'channels',
|
||||
'django_ratelimit',
|
||||
]
|
||||
|
||||
LOCAL_APPS = [
|
||||
'apps.core',
|
||||
'apps.rooms',
|
||||
'apps.authentication',
|
||||
]
|
||||
|
||||
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
|
||||
|
||||
MIDDLEWARE = [
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'videocall_app.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [os.path.join(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',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'videocall_app.wsgi.application'
|
||||
ASGI_APPLICATION = 'videocall_app.asgi.application'
|
||||
|
||||
# Database configuration
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': config('DB_NAME', default='videocall_db'),
|
||||
'USER': config('DB_USER', default='postgres'),
|
||||
'PASSWORD': config('DB_PASSWORD', default=''),
|
||||
'HOST': config('DB_HOST', default='localhost'),
|
||||
'PORT': config('DB_PORT', default='5432'),
|
||||
}
|
||||
}
|
||||
|
||||
# Redis configuration
|
||||
REDIS_URL = config('REDIS_URL', default='redis://localhost:6379/0')
|
||||
|
||||
# Channels configuration for WebSockets
|
||||
CHANNEL_LAYERS = {
|
||||
'default': {
|
||||
'BACKEND': 'channels_redis.core.RedisChannelLayer',
|
||||
'CONFIG': {
|
||||
'hosts': [REDIS_URL],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Cache configuration
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django_redis.cache.RedisCache',
|
||||
'LOCATION': REDIS_URL,
|
||||
'OPTIONS': {
|
||||
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Session configuration
|
||||
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
|
||||
SESSION_CACHE_ALIAS = 'default'
|
||||
SESSION_COOKIE_AGE = 86400 # 24 hours
|
||||
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
|
||||
SESSION_SAVE_EVERY_REQUEST = True
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
SESSION_COOKIE_SECURE = not DEBUG # Use secure cookies in production
|
||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||
|
||||
# CSRF Configuration
|
||||
CSRF_COOKIE_HTTPONLY = False # Allow JavaScript to read CSRF token
|
||||
CSRF_COOKIE_SECURE = not DEBUG # Use secure cookies in production
|
||||
CSRF_COOKIE_SAMESITE = 'Lax'
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
'http://localhost:3000',
|
||||
'http://127.0.0.1:3000',
|
||||
]
|
||||
|
||||
if not DEBUG:
|
||||
# Add your production domains
|
||||
CSRF_TRUSTED_ORIGINS.extend([
|
||||
'https://yourdomain.com',
|
||||
'https://www.yourdomain.com',
|
||||
])
|
||||
|
||||
# REST Framework configuration
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
],
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
'rest_framework.permissions.AllowAny', # Changed to allow custom auth
|
||||
],
|
||||
'DEFAULT_THROTTLE_CLASSES': [
|
||||
'rest_framework.throttling.AnonRateThrottle',
|
||||
'rest_framework.throttling.UserRateThrottle'
|
||||
],
|
||||
'DEFAULT_THROTTLE_RATES': {
|
||||
'anon': '100/hour',
|
||||
'user': '1000/hour'
|
||||
}
|
||||
}
|
||||
|
||||
# CORS settings
|
||||
CORS_ALLOWED_ORIGINS = config(
|
||||
'CORS_ALLOWED_ORIGINS',
|
||||
default='http://localhost:3000,http://127.0.0.1:3000'
|
||||
).split(',')
|
||||
|
||||
CORS_ALLOW_CREDENTIALS = True
|
||||
CORS_ALLOW_ALL_ORIGINS = DEBUG # Only in development
|
||||
|
||||
# WebSocket origins for Channels
|
||||
ALLOWED_HOSTS_INCLUDE_WEBSOCKET = True
|
||||
|
||||
# Additional CORS headers for development
|
||||
if DEBUG:
|
||||
CORS_ALLOW_HEADERS = [
|
||||
'accept',
|
||||
'accept-encoding',
|
||||
'authorization',
|
||||
'content-type',
|
||||
'dnt',
|
||||
'origin',
|
||||
'user-agent',
|
||||
'x-csrftoken',
|
||||
'x-requested-with',
|
||||
]
|
||||
|
||||
# Django Ratelimit settings
|
||||
RATELIMIT_USE_CACHE = 'default'
|
||||
RATELIMIT_ENABLE = True
|
||||
|
||||
# В файле backend/videocall_app/settings.py замените секцию LOGGING на:
|
||||
|
||||
# Logging configuration
|
||||
import os
|
||||
|
||||
# Создаем директорию для логов если её нет
|
||||
LOGS_DIR = BASE_DIR / 'logs'
|
||||
LOGS_DIR.mkdir(exist_ok=True)
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'formatters': {
|
||||
'verbose': {
|
||||
'format': '[{levelname}] {asctime} {name}: {message}',
|
||||
'style': '{',
|
||||
},
|
||||
'simple': {
|
||||
'format': '{levelname} {message}',
|
||||
'style': '{',
|
||||
},
|
||||
},
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'verbose',
|
||||
},
|
||||
'file': {
|
||||
'class': 'logging.FileHandler',
|
||||
'filename': LOGS_DIR / 'django.log',
|
||||
'formatter': 'verbose',
|
||||
} if os.access(LOGS_DIR, os.W_OK) else {
|
||||
# Fallback to console if can't write to file
|
||||
'class': 'logging.StreamHandler',
|
||||
'formatter': 'verbose',
|
||||
},
|
||||
},
|
||||
'root': {
|
||||
'handlers': ['console'],
|
||||
'level': 'INFO',
|
||||
},
|
||||
'loggers': {
|
||||
'django': {
|
||||
'handlers': ['console'],
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
'apps.authentication': {
|
||||
'handlers': ['console'],
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
'apps.rooms': {
|
||||
'handlers': ['console'],
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Internationalization
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
TIME_ZONE = 'UTC'
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
STATIC_URL = '/static/'
|
||||
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
||||
|
||||
# Additional locations of static files
|
||||
STATICFILES_DIRS = []
|
||||
if (BASE_DIR / 'static').exists():
|
||||
STATICFILES_DIRS.append(BASE_DIR / 'static')
|
||||
|
||||
# Static files finders
|
||||
STATICFILES_FINDERS = [
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
]
|
||||
|
||||
# Media files
|
||||
MEDIA_URL = '/media/'
|
||||
MEDIA_ROOT = BASE_DIR / 'media'
|
||||
|
||||
# Application-specific settings
|
||||
ROOM_EXPIRY_HOURS = 24
|
||||
MAX_PARTICIPANTS_PER_ROOM = 2
|
||||
SHORT_CODE_LENGTH = 6
|
||||
|
||||
# Default primary key field type
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
# Security settings for production
|
||||
if not DEBUG:
|
||||
# Trust proxy headers from nginx
|
||||
USE_X_FORWARDED_HOST = True
|
||||
USE_X_FORWARDED_PORT = True
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
|
||||
# ВАЖНО: НЕ ВКЛЮЧАЕМ принудительное перенаправление на HTTPS
|
||||
# так как это делает nginx
|
||||
SECURE_SSL_REDIRECT = False
|
||||
|
||||
# Остальные настройки безопасности
|
||||
SECURE_BROWSER_XSS_FILTER = True
|
||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||
SECURE_HSTS_SECONDS = 31536000
|
||||
SESSION_COOKIE_SECURE = True
|
||||
CSRF_COOKIE_SECURE = True
|
||||
|
||||
# Дополнительные заголовки безопасности
|
||||
SECURE_REFERRER_POLICY = 'same-origin'
|
||||
17
backend/videocall_app/urls.py
Normal file
17
backend/videocall_app/urls.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# videocall_app/urls.py - Main URL configuration
|
||||
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),
|
||||
path('api/auth/', include('apps.authentication.urls')),
|
||||
path('api/rooms/', include('apps.rooms.urls')),
|
||||
path('api/', include('apps.core.urls')),
|
||||
]
|
||||
|
||||
# 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
backend/videocall_app/wsgi.py
Normal file
16
backend/videocall_app/wsgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for videocall_app 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', 'videocall_app.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
Reference in New Issue
Block a user