diff --git a/frontend/src/hooks/useTicketWebSocket.ts b/frontend/src/hooks/useTicketWebSocket.ts index a098299..f78fec0 100644 --- a/frontend/src/hooks/useTicketWebSocket.ts +++ b/frontend/src/hooks/useTicketWebSocket.ts @@ -2,6 +2,8 @@ import { useEffect, useRef, useCallback } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { toast } from 'react-hot-toast'; import { useCurrentUser } from './useAuth'; +import { getBaseDomain } from '../utils/domain'; +import { getCookie } from '../utils/cookies'; interface TicketWebSocketMessage { type: 'new_ticket' | 'ticket_update' | 'ticket_deleted' | 'new_comment' | 'ticket_assigned' | 'ticket_status_changed'; @@ -111,10 +113,16 @@ export const useTicketWebSocket = (options: UseTicketWebSocketOptions = {}) => { return; } - // Determine WebSocket URL - connect to backend on port 8000 + // Determine WebSocket URL using same logic as API config + const baseDomain = getBaseDomain(); const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsHost = window.location.hostname + ':8000'; - const wsUrl = `${protocol}//${wsHost}/ws/tickets/`; + + // For localhost or lvh.me, use port 8000. In production, no port (Traefik handles it) + const isDev = baseDomain === 'localhost' || baseDomain === 'lvh.me'; + const port = isDev ? ':8000' : ''; + + const token = getCookie('access_token'); + const wsUrl = `${protocol}//api.${baseDomain}${port}/ws/tickets/?token=${token}`; console.log('Connecting to ticket WebSocket:', wsUrl); diff --git a/smoothschedule/compose/local/django/start b/smoothschedule/compose/local/django/start index ba96db4..88caf5b 100644 --- a/smoothschedule/compose/local/django/start +++ b/smoothschedule/compose/local/django/start @@ -6,4 +6,6 @@ set -o nounset python manage.py migrate -exec python manage.py runserver_plus 0.0.0.0:8000 +# Use python -m daphne to avoid PATH issues +echo "Starting Daphne..." +exec python -m daphne -b 0.0.0.0 -p 8000 config.asgi:application diff --git a/smoothschedule/config/settings/base.py b/smoothschedule/config/settings/base.py index ec62acd..f266ac3 100644 --- a/smoothschedule/config/settings/base.py +++ b/smoothschedule/config/settings/base.py @@ -103,6 +103,7 @@ LOCAL_APPS = [ "payments", "platform_admin.apps.PlatformAdminConfig", "notifications", # New: Generic notification app + "tickets", # New: Support tickets app # Your stuff: custom apps go here ] # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps @@ -313,7 +314,7 @@ REST_FRAMEWORK = { } # django-cors-headers - https://github.com/adamchainz/django-cors-headers#setup -CORS_URLS_REGEX = r"^/(api|auth)/.*$" +# CORS_URLS_REGEX removed to allow CORS on all paths (API endpoints now at root level via api.* subdomain) from corsheaders.defaults import default_headers # CORS allowed origins (configurable via environment variables) diff --git a/smoothschedule/config/settings/local.py b/smoothschedule/config/settings/local.py index 6f1b2db..e669ed5 100644 --- a/smoothschedule/config/settings/local.py +++ b/smoothschedule/config/settings/local.py @@ -58,6 +58,10 @@ ALLOWED_HOSTS = ["localhost", "0.0.0.0", "127.0.0.1", ".lvh.me", "lvh.me"] # no # CORS and CSRF are configured in base.py with environment variable overrides # Local development uses the .env file to set DJANGO_CORS_ALLOWED_ORIGINS +# Cookie configuration for cross-subdomain auth in development (lvh.me) +SESSION_COOKIE_DOMAIN = ".lvh.me" +CSRF_COOKIE_DOMAIN = ".lvh.me" + # CACHES # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#caches @@ -78,7 +82,7 @@ EMAIL_BACKEND = env( # WhiteNoise # ------------------------------------------------------------------------------ # http://whitenoise.evans.io/en/latest/django.html#using-whitenoise-in-development -INSTALLED_APPS = ["whitenoise.runserver_nostatic", *INSTALLED_APPS] +# INSTALLED_APPS = ["whitenoise.runserver_nostatic", *INSTALLED_APPS] # django-debug-toolbar @@ -109,12 +113,16 @@ if env("USE_DOCKER") == "yes": # ------------------------------------------------------------------------------ # https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration INSTALLED_APPS += ["django_extensions"] -# Celery +# CELERY # ------------------------------------------------------------------------------ # https://docs.celeryq.dev/en/stable/userguide/configuration.html#task-eager-propagates CELERY_TASK_EAGER_PROPAGATES = True +# ASGI +# ------------------------------------------------------------------------------ +ASGI_APPLICATION = "config.asgi.application" + # LOGGING - Development # ------------------------------------------------------------------------------ # Extend the base LOGGING configuration with development settings @@ -124,6 +132,13 @@ LOGGING["formatters"]["verbose"] = { } LOGGING["handlers"]["console"]["formatter"] = "verbose" LOGGING["root"] = {"level": "DEBUG", "handlers": ["console"]} +LOGGING["loggers"] = { + "watchfiles": {"level": "WARNING"}, + "watchfiles.main": {"level": "WARNING"}, + "watchfiles.watcher": {"level": "WARNING"}, + "watchdog": {"level": "WARNING"}, + "inotify_buffer": {"level": "WARNING"}, +} # Your stuff... # ------------------------------------------------------------------------------ diff --git a/smoothschedule/config/settings/multitenancy.py b/smoothschedule/config/settings/multitenancy.py index c0c2967..acf40d3 100644 --- a/smoothschedule/config/settings/multitenancy.py +++ b/smoothschedule/config/settings/multitenancy.py @@ -33,6 +33,7 @@ SHARED_APPS = [ 'rest_framework.authtoken', 'corsheaders', 'drf_spectacular', + 'channels', # WebSockets 'allauth', 'allauth.account', 'allauth.mfa', diff --git a/smoothschedule/tickets/middleware.py b/smoothschedule/tickets/middleware.py new file mode 100644 index 0000000..01cc464 --- /dev/null +++ b/smoothschedule/tickets/middleware.py @@ -0,0 +1,67 @@ +from channels.db import database_sync_to_async +from django.contrib.auth.models import AnonymousUser +from rest_framework.authtoken.models import Token +from django.db import close_old_connections + +@database_sync_to_async +def get_user(token_key): + try: + token = Token.objects.select_related('user').get(key=token_key) + return token.user + except Token.DoesNotExist: + return AnonymousUser() + +class TokenAuthMiddleware: + """ + Custom middleware to authenticate users via 'access_token' cookie + using Django REST Framework's TokenAuthentication. + """ + def __init__(self, inner): + self.inner = inner + + async def __call__(self, scope, receive, send): + print(f"TokenAuthMiddleware: Processing request for {scope.get('path')}", flush=True) + # Close old database connections to prevent usage of timed out connections + close_old_connections() + + # Check if user is already authenticated (e.g. by SessionMiddleware) + if scope.get("user") and scope["user"].is_authenticated: + print(f"TokenAuthMiddleware: User already authenticated: {scope['user']}", flush=True) + return await self.inner(scope, receive, send) + + token_key = None + + # Try to authenticate with token from query string + query_string = scope.get('query_string', b'').decode() + if 'token=' in query_string: + for param in query_string.split('&'): + if param.startswith('token='): + token_key = param.split('=')[1] + print(f"TokenAuthMiddleware: Found token in query string: {token_key}", flush=True) + break + + # Try to authenticate with token from cookie if not found in query string + if not token_key: + headers = dict(scope['headers']) + if b'cookie' in headers: + cookies = headers[b'cookie'].decode().split('; ') + print(f"TokenAuthMiddleware: Found cookies: {cookies}", flush=True) + for cookie in cookies: + if cookie.strip().startswith('access_token='): + token_key = cookie.strip().split('=')[1] + break + else: + print("TokenAuthMiddleware: No cookie header found.", flush=True) + + if token_key: + print(f"TokenAuthMiddleware: Found token key: {token_key}", flush=True) + user = await get_user(token_key) + if user: + print(f"TokenAuthMiddleware: Authenticated user: {user}", flush=True) + scope['user'] = user + else: + print("TokenAuthMiddleware: User not found for token.", flush=True) + else: + print("TokenAuthMiddleware: No access_token cookie or query param found.", flush=True) + + return await self.inner(scope, receive, send) diff --git a/smoothschedule/tickets/routing.py b/smoothschedule/tickets/routing.py index 5d8d6b6..fa4615d 100644 --- a/smoothschedule/tickets/routing.py +++ b/smoothschedule/tickets/routing.py @@ -3,6 +3,6 @@ from django.urls import re_path from . import consumers websocket_urlpatterns = [ - re_path(r"ws/tickets/$", consumers.TicketConsumer.as_asgi()), - re_path(r"ws/notifications/$", consumers.NotificationConsumer.as_asgi()), + re_path(r"^/?ws/tickets/?$", consumers.TicketConsumer.as_asgi()), + re_path(r"^/?ws/notifications/?$", consumers.NotificationConsumer.as_asgi()), ] \ No newline at end of file