From 4e79d7c1de3259649acd9cff587a5ed4b451f5cb Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Mon, 26 May 2025 23:40:57 +0200 Subject: [PATCH] add superuser creation command and update settings for environment variables --- .env.example | 9 +++ .gitignore | 4 +- Dockerfile | 18 ++++++ api/management/__init__.py | 0 api/management/commands/__init__.py | 0 .../commands/createsuperuser_if_not_exists.py | 18 ++++++ docker-compose.yml | 19 ++++++ mail_management/settings.py | 18 ++++-- pyproject.toml | 2 + requirements.txt | 60 +++++++++++++++++++ uv.lock | 26 ++++++++ 11 files changed, 168 insertions(+), 6 deletions(-) create mode 100644 .env.example create mode 100644 Dockerfile create mode 100644 api/management/__init__.py create mode 100644 api/management/commands/__init__.py create mode 100644 api/management/commands/createsuperuser_if_not_exists.py create mode 100644 docker-compose.yml create mode 100644 requirements.txt diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..32f6b37 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +# Django settings +DJANGO_SUPERUSER_USERNAME=admin +DJANGO_SUPERUSER_EMAIL=admin@example.com +DJANGO_SUPERUSER_PASSWORD=adminpassword +DEBUG=False + +SECRET_KEY=your-secret-key +ALLOWED_HOSTS=localhost,127.0.0.1 +CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 38b110d..46703e3 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,6 @@ build/ # Logs and temporary files *.log *.tmp -*.swp \ No newline at end of file +*.swp + +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f789528 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM python:3.13-slim + +WORKDIR /app + + +COPY requirements.txt /app/ + +# Install dependencies +RUN pip install --upgrade pip \ + && pip install -r requirements.txt \ + && pip install gunicorn + +# Copy the rest of the project +COPY . /app/ + +EXPOSE 8000 + +CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"] diff --git a/api/management/__init__.py b/api/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/management/commands/__init__.py b/api/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/management/commands/createsuperuser_if_not_exists.py b/api/management/commands/createsuperuser_if_not_exists.py new file mode 100644 index 0000000..779265e --- /dev/null +++ b/api/management/commands/createsuperuser_if_not_exists.py @@ -0,0 +1,18 @@ +from django.core.management.base import BaseCommand +from django.contrib.auth import get_user_model +import os + +class Command(BaseCommand): + help = "Create a superuser if it doesn't already exist" + + def handle(self, *args, **kwargs): + User = get_user_model() + username = os.getenv("DJANGO_SUPERUSER_USERNAME") + email = os.getenv("DJANGO_SUPERUSER_EMAIL") + password = os.getenv("DJANGO_SUPERUSER_PASSWORD") + + if not User.objects.filter(username=username).exists(): + User.objects.create_superuser(username=username, email=email, password=password) + self.stdout.write(self.style.SUCCESS(f"Superuser {username} created")) + else: + self.stdout.write(self.style.WARNING(f"Superuser {username} already exists")) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..dca1f4b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +services: + web: + build: . + command: > + sh -c "python manage.py migrate && + python manage.py createsuperuser_if_not_exists && + python manage.py collectstatic --noinput && + gunicorn mail_management.wsgi:application --bind 0.0.0.0:8000" + volumes: + - static_volume:/app/staticfiles + - media_volume:/app/media + ports: + - "8000:8000" + env_file: + - .env + +volumes: + static_volume: + media_volume: \ No newline at end of file diff --git a/mail_management/settings.py b/mail_management/settings.py index 1266bea..f57173c 100644 --- a/mail_management/settings.py +++ b/mail_management/settings.py @@ -11,7 +11,7 @@ https://docs.djangoproject.com/en/5.2/ref/settings/ """ from pathlib import Path - +import os # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -20,13 +20,13 @@ BASE_DIR = Path(__file__).resolve().parent.parent # See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'django-insecure-u^7rwn!f0*brki9f46&lu8_lal=3_(1kykx#84dp^hx62923qr' +SECRET_KEY = os.getenv("SECRET_KEY", 'django-insecure-u^7rwn!f0*brki9f46&lu8_lal=3_(1kykx#84dp^hx62923qr') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = [] +DEBUG = os.getenv('DEBUG', 'false').lower() == 'true' +ALLOWED_HOSTS: list[str] = os.getenv('ALLOWED_HOSTS', '').split(',') if os.getenv('ALLOWED_HOSTS') else ['*'] +CORS_ALLOWED_ORIGINS = os.getenv('CORS_ALLOWED_ORIGINS', '').split(',') if os.getenv('CORS_ALLOWED_ORIGINS') else [] # Application definition @@ -39,17 +39,20 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', 'ninja_extra', 'ninja_jwt', + 'corsheaders', 'api' ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', ] ROOT_URLCONF = 'mail_management.urls' @@ -119,6 +122,11 @@ USE_TZ = True # https://docs.djangoproject.com/en/5.2/howto/static-files/ STATIC_URL = 'static/' +MEDIA_URL = '/media/' +STATIC_ROOT = BASE_DIR / "staticfiles" +MEDIA_ROOT = BASE_DIR / "media" + +STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage" # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field diff --git a/pyproject.toml b/pyproject.toml index 0b14ae9..afcdc15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,8 +5,10 @@ description = "Add your description here" requires-python = ">=3.13" dependencies = [ "django>=5.2.1", + "django-cors-headers>=4.7.0", "django-ninja>=1.4.1", "django-ninja-jwt>=5.3.7", "mypy>=1.15.0", "ruff>=0.11.10", + "whitenoise>=6.9.0", ] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0aba6c1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,60 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile pyproject.toml -o requirements.txt +annotated-types==0.7.0 + # via pydantic +asgiref==3.8.1 + # via + # django + # django-cors-headers + # django-ninja-extra +cffi==1.17.1 + # via cryptography +contextlib2==21.6.0 + # via django-ninja-extra +cryptography==45.0.3 + # via pyjwt +django==5.2.1 + # via + # mail-management (pyproject.toml) + # django-cors-headers + # django-ninja + # django-ninja-extra + # django-ninja-jwt +django-cors-headers==4.7.0 + # via mail-management (pyproject.toml) +django-ninja==1.4.1 + # via + # mail-management (pyproject.toml) + # django-ninja-extra +django-ninja-extra==0.30.0 + # via django-ninja-jwt +django-ninja-jwt==5.3.7 + # via mail-management (pyproject.toml) +injector==0.22.0 + # via django-ninja-extra +mypy==1.15.0 + # via mail-management (pyproject.toml) +mypy-extensions==1.1.0 + # via mypy +pycparser==2.22 + # via cffi +pydantic==2.11.5 + # via django-ninja +pydantic-core==2.33.2 + # via pydantic +pyjwt==2.10.1 + # via django-ninja-jwt +ruff==0.11.11 + # via mail-management (pyproject.toml) +sqlparse==0.5.3 + # via django +typing-extensions==4.13.2 + # via + # mypy + # pydantic + # pydantic-core + # typing-inspection +typing-inspection==0.4.1 + # via pydantic +whitenoise==6.9.0 + # via mail-management (pyproject.toml) diff --git a/uv.lock b/uv.lock index b9f2dc3..992252a 100644 --- a/uv.lock +++ b/uv.lock @@ -100,6 +100,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/90/92/7448697b5838b3a1c6e1d2d6a673e908d0398e84dc4f803a2ce11e7ffc0f/django-5.2.1-py3-none-any.whl", hash = "sha256:a9b680e84f9a0e71da83e399f1e922e1ab37b2173ced046b541c72e1589a5961", size = 8301833 }, ] +[[package]] +name = "django-cors-headers" +version = "4.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "django" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/6c/16f6cb6064c63074fd5b2bd494eb319afd846236d9c1a6c765946df2c289/django_cors_headers-4.7.0.tar.gz", hash = "sha256:6fdf31bf9c6d6448ba09ef57157db2268d515d94fc5c89a0a1028e1fc03ee52b", size = 21037 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/a2/7bcfff86314bd9dd698180e31ba00604001606efb518a06cca6833a54285/django_cors_headers-4.7.0-py3-none-any.whl", hash = "sha256:f1c125dcd58479fe7a67fe2499c16ee38b81b397463cf025f0e2c42937421070", size = 12794 }, +] + [[package]] name = "django-ninja" version = "1.4.1" @@ -158,19 +171,23 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "django" }, + { name = "django-cors-headers" }, { name = "django-ninja" }, { name = "django-ninja-jwt" }, { name = "mypy" }, { name = "ruff" }, + { name = "whitenoise" }, ] [package.metadata] requires-dist = [ { name = "django", specifier = ">=5.2.1" }, + { name = "django-cors-headers", specifier = ">=4.7.0" }, { name = "django-ninja", specifier = ">=1.4.1" }, { name = "django-ninja-jwt", specifier = ">=5.3.7" }, { name = "mypy", specifier = ">=1.15.0" }, { name = "ruff", specifier = ">=0.11.10" }, + { name = "whitenoise", specifier = ">=6.9.0" }, ] [[package]] @@ -330,3 +347,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be76 wheels = [ { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, ] + +[[package]] +name = "whitenoise" +version = "6.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/cf/c15c2f21aee6b22a9f6fc9be3f7e477e2442ec22848273db7f4eb73d6162/whitenoise-6.9.0.tar.gz", hash = "sha256:8c4a7c9d384694990c26f3047e118c691557481d624f069b7f7752a2f735d609", size = 25920 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b2/2ce9263149fbde9701d352bda24ea1362c154e196d2fda2201f18fc585d7/whitenoise-6.9.0-py3-none-any.whl", hash = "sha256:c8a489049b7ee9889617bb4c274a153f3d979e8f51d2efd0f5b403caf41c57df", size = 20161 }, +]