diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..fb989c4 --- /dev/null +++ b/config/__init__.py @@ -0,0 +1,3 @@ +from .celery import app as celery_app + +__all__ = ('celery_app',) diff --git a/config/api.py b/config/api.py new file mode 100644 index 0000000..18cb509 --- /dev/null +++ b/config/api.py @@ -0,0 +1,5 @@ +from rest_framework.routers import DefaultRouter + +router = DefaultRouter() + +urlpatterns = router.urls diff --git a/config/asgi.py b/config/asgi.py new file mode 100644 index 0000000..b7de924 --- /dev/null +++ b/config/asgi.py @@ -0,0 +1,20 @@ +import os + +from django.core.asgi import get_asgi_application + +from channels.auth import AuthMiddlewareStack +from channels.routing import ProtocolTypeRouter, URLRouter +from channels.security.websocket import AllowedHostsOriginValidator + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + +django_asgi_app = get_asgi_application() + +application = ProtocolTypeRouter({ + "http": django_asgi_app, + "websocket": AllowedHostsOriginValidator( + AuthMiddlewareStack( + URLRouter([]) + ) + ) +}) diff --git a/config/celery.py b/config/celery.py new file mode 100644 index 0000000..5e2b5f9 --- /dev/null +++ b/config/celery.py @@ -0,0 +1,8 @@ +from celery import Celery +import os + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + +app = Celery('config') +app.config_from_object('django.conf:settings', namespace='CELERY') +app.autodiscover_tasks() \ No newline at end of file diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000..3a3d4eb --- /dev/null +++ b/config/settings.py @@ -0,0 +1,200 @@ +""" +Django settings will use prefix of DJANGO_ for environment variables. +""" + +import os +from pathlib import Path +import sys +from dotenv import load_dotenv + +BASE_DIR = Path(__file__).resolve().parent.parent + +load_dotenv(dotenv_path = BASE_DIR / '.env') + +FRONT_DIR = os.getenv('DJANGO_FRONT_DIR', BASE_DIR / 'front') +MODEL_DIR = os.getenv('DJANGO_MODEL_DIR', BASE_DIR / 'model') + +SECRET_KEY = os.getenv('DJANGO_SECRET_KEY') +DEBUG = str(os.getenv('DJANGO_DEBUG')).lower() in ('1', 'true', 'yes', 'on') + +DOMAIN_NAME = os.getenv('DJANGO_DOMAIN_NAME', 'localhost') +ALLOWED_HOSTS = [stripped_host for host in os.getenv('DJANGO_ALLOWED_HOSTS', 'localhost').split(',') if (stripped_host:=host.strip())] + +PARENT_NAME = Path(__file__).resolve().parent.name + +DJANGO_CELERY_BROKER_URL = os.getenv('DJANGO_CELERY_BROKER_URL', 'redis://localhost:6379/0') + +STATIC_URL = os.getenv('DJANGO_STATIC_URL', '/static/') +MEDIA_URL = os.getenv('DJANGO_MEDIA_URL', '/media/') +STATIC_ROOT = os.getenv('DJANGO_STATIC_ROOT', BASE_DIR / 'static') +MEDIA_ROOT = os.getenv('DJANGO_MEDIA_ROOT', BASE_DIR / 'media') + +DB_ENGINE = os.getenv('DJANGO_DB_ENGINE', 'django.db.backends.sqlite3') +DB_NAME = os.getenv('DJANGO_POSTGRES_DB', BASE_DIR / 'db.sqlite3') +DB_USER = os.getenv('DJANGO_POSTGRES_USER') +DB_PASSWORD = os.getenv('DJANGO_POSTGRES_PASSWORD') +DB_HOST = os.getenv('DJANGO_POSTGRES_HOST') +DB_PORT = os.getenv('DJANGO_POSTGRES_PORT', 5432) + +if any(arg.startswith('test') for arg in sys.argv): + DB_ENGINE = 'django.db.backends.sqlite3' + DB_NAME = ':memory:' + DB_USER = None + DB_PASSWORD = None + DB_HOST = None + DB_PORT = None + +OVERRIDE_APPS = [ + 'jazzmin', + 'daphne', +] +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', + 'channels', + 'django_celery_results', + 'django_celery_beat', + 'corsheaders', +] +LOCAL_APPS = [ + 'apps.users', +] +INSTALLED_APPS = OVERRIDE_APPS + DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS + +AUTH_USER_MODEL = 'users.User' +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', + 'corsheaders.middleware.CorsMiddleware', + '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 = f'{PARENT_NAME}.urls' +WSGI_APPLICATION = f'{PARENT_NAME}.wsgi.application' +ASGI_APPLICATION = f'{PARENT_NAME}.asgi.application' + +CHANNEL_LAYERS = { + 'default': { + 'BACKEND': 'channels_redis.core.RedisChannelLayer', + 'CONFIG': { + 'hosts': [DJANGO_CELERY_BROKER_URL], + }, + }, +} + +SESSION_ENGINE = 'django.contrib.sessions.backends.db' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +DATABASES = { + 'default': { + 'ENGINE': DB_ENGINE, + 'NAME': DB_NAME, + } if DB_ENGINE == 'django.db.backends.sqlite3' else { + 'ENGINE': DB_ENGINE, + 'NAME': DB_NAME, + 'USER': DB_USER, + 'PASSWORD': DB_PASSWORD, + 'HOST': DB_HOST, + 'PORT': DB_PORT, + 'CONN_MAX_AGE': 600, + } +} + +STORAGES = { + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage", + }, +} + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +LANGUAGE_CODE = 'en-uk' +TIME_ZONE = 'UTC' +USE_I18N = True +USE_TZ = True + +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': [ + 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.BasicAuthentication', + ], + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.AllowAny', + ], +} + +CELERY_BROKER_URL = DJANGO_CELERY_BROKER_URL +CELERY_RESULT_BACKEND = 'django-db' +CELERY_CACHE_BACKEND = 'django-cache' +CELERY_ACCEPT_CONTENT = ['json'] +CELERY_TASK_SERIALIZER = 'json' +CELERY_RESULT_SERIALIZER = 'json' +CELERY_TIMEZONE = 'UTC' +CELERY_TASK_TRACK_STARTED = True +CELERY_TASK_TIME_LIMIT = 30 * 60 + +X_FRAME_OPTIONS = 'SAMEORIGIN' +CORS_ALLOW_CREDENTIALS = True +CORS_ALLOWED_ORIGINS = [ + f'http://{DOMAIN_NAME}', + f'https://{DOMAIN_NAME}', +] +CSRF_TRUSTED_ORIGINS = [ + f'http://{DOMAIN_NAME}', + f'https://{DOMAIN_NAME}', +] +CSRF_COOKIE_HTTPONLY = False +CSRF_COOKIE_SECURE = not DEBUG +CSRF_COOKIE_SAMESITE = 'Lax' +SESSION_COOKIE_SAMESITE = 'Lax' +SESSION_COOKIE_HTTPONLY = True +SESSION_COOKIE_SECURE = not DEBUG +SESSION_COOKIE_AGE = 1209600 +SESSION_SAVE_EVERY_REQUEST = True + +if DEBUG: + CORS_ALLOWED_ORIGINS.append(f'http://{DOMAIN_NAME}:5173') + CORS_ALLOWED_ORIGINS.append(f'http://{DOMAIN_NAME}:8000') + CSRF_TRUSTED_ORIGINS.append(f'http://{DOMAIN_NAME}:5173') + CSRF_TRUSTED_ORIGINS.append(f'http://{DOMAIN_NAME}:8000') diff --git a/config/urls.py b/config/urls.py new file mode 100644 index 0000000..fed3f71 --- /dev/null +++ b/config/urls.py @@ -0,0 +1,14 @@ +from django.contrib import admin +from django.urls import path, include, re_path +from django.conf import settings +from django.conf.urls.static import static + +from .views import serve_frontend + +urlpatterns = [ + path('admin/', admin.site.urls), + path('api/', include('config.api')), + re_path(r'^(?!static/|media/)(?P.*)$', serve_frontend, {'document_root': settings.FRONT_DIR}), + *static(settings.STATIC_URL, document_root=settings.STATIC_ROOT), + *static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT), +] \ No newline at end of file diff --git a/config/views.py b/config/views.py new file mode 100644 index 0000000..a164f33 --- /dev/null +++ b/config/views.py @@ -0,0 +1,14 @@ +from django.utils._os import safe_join +from django.views.static import serve as static_serve +from django.views.decorators.csrf import ensure_csrf_cookie +import posixpath +from pathlib import Path + +@ensure_csrf_cookie +def serve_frontend(request, path, document_root = None): + path = posixpath.normpath(path).lstrip("/") + fullpath = Path(safe_join(document_root, path)) + if fullpath.is_file(): + return static_serve(request, path, document_root) + else: + return static_serve(request, "index.html", document_root) \ No newline at end of file diff --git a/config/wsgi.py b/config/wsgi.py new file mode 100644 index 0000000..160e932 --- /dev/null +++ b/config/wsgi.py @@ -0,0 +1,5 @@ +import os +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') +application = get_wsgi_application() \ No newline at end of file diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..8e7ac79 --- /dev/null +++ b/manage.py @@ -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', 'config.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()