diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..02a19eec --- /dev/null +++ b/.gitignore @@ -0,0 +1,164 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ +static/ +nginx-dev.conf \ No newline at end of file diff --git a/Arman-challenge-design.png b/Arman-challenge-design.png new file mode 100644 index 00000000..4bd36283 Binary files /dev/null and b/Arman-challenge-design.png differ diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 00000000..dd10a834 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,3 @@ +.* +!.coveragerc +!.env diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 00000000..18644955 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,32 @@ +FROM python:3.10 +ENV PYTHONUNBUFFERED 1 + +WORKDIR /backend + +COPY ./requirements.txt requirements.txt +RUN pip --no-cache-dir --timeout=1000 install -r requirements.txt + +COPY . /backend/ + +# Install supervisor +RUN apt-get update && apt-get install -y supervisor + +# Install netcat +RUN apt-get update && apt-get install -y netcat-openbsd + +# Copy supervisor configuration +COPY scripts/supervisord.conf /etc/supervisor/conf.d/supervisord.conf + +# Ensure the entrypoint script is executable +RUN chmod +x scripts/entrypoint.sh + +RUN mkdir -p /var/log +RUN chown -R nobody:nogroup /var/log + +EXPOSE 8000 + +# Set the entrypoint script +ENTRYPOINT ["scripts/entrypoint.sh"] + +# Default command to run supervisord +CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 00000000..d50329d9 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,38 @@ +# Django & DRF & JWT +django==5.0 +djangorestframework==3.15.2 +djangorestframework-simplejwt==5.3.1 +# Control Cors headers +django_cors_headers==4.4.0 +# Postgres client and dependencies +psycopg==3.2.1 +psycopg-binary==3.2.1 +psycopg-pool==3.2.2 +# Load dotenv files +python-dotenv==1.0.1 +django-environ==0.11.2 +# uvicorn server +uvicorn==0.30.6 +# Colorize test outputs +redgreenunittest==0.1.1 +# Django redis client +django-redis==5.4.0 +# Celery +celery==5.4.0 +redis==5.0.8 +amqp==5.2.0 +msgpack==1.0.8 +django-celery-results==2.5.1 +django-celery-beat==2.7.0 +flower==2.0.1 +# SMSProvider +kavenegar==1.1.2 +# Admin UI +django-jazzmin==3.0.0 +# Formatting +ruff==0.6.4 +mypy==1.11.2 +# Swagger +drf-yasg==1.21.7 +# Load testing +locust==2.31.6 diff --git a/backend/scripts/entrypoint.sh b/backend/scripts/entrypoint.sh new file mode 100644 index 00000000..b632d56f --- /dev/null +++ b/backend/scripts/entrypoint.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# Wait for the database and other services to start +echo "Waiting for PostgreSQL..." +while ! nc -z postgres 5432; do + sleep 0.1 +done +echo "PostgreSQL started" + +# Wait for the cache backend and message broker services to start +echo "Waiting for RabbitMQ..." +while ! nc -z rabbitmq 5672; do + sleep 0.1 +done +echo "RabbitMQ started" + +echo "Waiting for Redis..." +while ! nc -z redis 6379; do + sleep 0.1 +done +echo "Redis started" + +# Lint and Format code +echo "Running linter..." +ruff format +ruff check --fix + +# Apply migrations +echo "Applying migrations..." +python /backend/manage.py migrate + +# Run tests if in a testing environment +if [ "$DJANGO_ENV" == "testing" ]; then + echo "Running tests..." + python /backend/manage.py test +fi + +# Collect static files +echo "Collecting static files..." +python /backend/manage.py collectstatic --noinput + +# Start supervisord, which manages Django and Celery +echo "Starting supervisord..." +exec supervisord -c /etc/supervisor/conf.d/supervisord.conf diff --git a/backend/scripts/supervisord.conf b/backend/scripts/supervisord.conf new file mode 100644 index 00000000..324befb0 --- /dev/null +++ b/backend/scripts/supervisord.conf @@ -0,0 +1,18 @@ +[supervisord] +nodaemon=true + +[program:django] +command=uvicorn apps.server:app +directory=/backend +autostart=true +autorestart=true +stdout_logfile=/var/log/django.log +stderr_logfile=/var/log/django_err.log + +[program:celery] +command=celery -A apps.core worker -B --loglevel=INFO +directory=/backend +autostart=true +autorestart=true +stdout_logfile=/var/log/celery.log +stderr_logfile=/var/log/celery_err.log diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..9b18423e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,110 @@ +services: + backend: + image: python:3.10 + restart: always + build: + context: ./backend + dockerfile: Dockerfile + container_name: backend + volumes: + - ./backend:/backend + - ./backend/logs:/var/log/arman + - static:/backend/static + expose: + - "8000" + networks: + - arman-network + env_file: + - .env + depends_on: + - postgres + - redis + + postgres: + image: postgres:latest + container_name: postgres + restart: always + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - postgres_data:/var/lib/postgresql/data + - ./postgres/init:/docker-entrypoint-initdb.d + ports: + - "5432:5432" + networks: + - arman-network + + redis: + image: redis:latest + restart: always + container_name: redis + ports: + - "6379:6379" + networks: + - arman-network + + rabbit: + image: rabbitmq:management + container_name: rabbitmq + restart: always + environment: + RABBITMQ_DEFAULT_USER: ${RABBITMQ_DEFAULT_USER} + RABBITMQ_DEFAULT_PASS: ${RABBITMQ_DEFAULT_PASS} + RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_DEFAULT_VHOST} + ports: + - "5672:5672" + - "15672:15672" + networks: + - arman-network + + nginx: + image: nginx:latest + container_name: nginx + restart: always + ports: + - "80:80" + - "443:443" + environment: + - ENV=${ENV} + volumes: + - ./nginx/nginx-${ENV}.conf:/etc/nginx/nginx.conf + - ./frontend:/usr/share/nginx/html + - static:/usr/share/nginx/static:ro + - ./certbot/www:/var/www/certbot/:ro + - ./certbot/conf/:/etc/nginx/ssl/:ro + networks: + - arman-network + depends_on: + - backend + + pgadmin: + image: dpage/pgadmin4:latest + container_name: pgadmin + environment: + PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL} + PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD} + expose: + - "8090" + volumes: + - pgadmin-data:/var/lib/pgadmin + depends_on: + - postgres + networks: + - arman-network + restart: always + + certbot: + image: certbot/certbot:latest + volumes: + - ./certbot/www/:/var/www/certbot/:rw + - ./certbot/conf/:/etc/letsencrypt/:rw + +volumes: + postgres_data: + static: + pgadmin-data: + +networks: + arman-network: diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 00000000..4243db71 --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,5 @@ +FROM nginx:latest + +COPY nginx.conf /etc/nginx/nginx.conf + +EXPOSE 80 diff --git a/nginx/nginx-prod.conf b/nginx/nginx-prod.conf new file mode 100644 index 00000000..631333fa --- /dev/null +++ b/nginx/nginx-prod.conf @@ -0,0 +1,115 @@ +events { + worker_connections 4096; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Frontend HTTP server block with exception for Certbot + server { + listen 80; + listen [::]:80; + server_name arman.dev www.arman.dev; + server_tokens off; + + # Serve Certbot challenge files over HTTP + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Redirect all other HTTP traffic to HTTPS + location / { + return 301 https://$host$request_uri; + } + } + + # Backend HTTP server block with exception for Certbot + server { + listen 80; + listen [::]:80; + server_name api.arman.dev www.api.arman.dev; + server_tokens off; + + # Serve Certbot challenge files over HTTP + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Redirect all other HTTP traffic to HTTPS + location / { + return 301 https://$host$request_uri; + } + } + + # Frontend HTTPS server block + server { + listen 443 ssl; + listen [::]:443 ssl; + server_name arman.dev www.arman.dev; + server_tokens off; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers off; # Prefer the client's cipher suite + ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256"; + + ssl_certificate /etc/nginx/ssl/live/arman.dev/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/live/arman.dev/privkey.pem; + + # Serve Certbot challenge files over HTTPS as well + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Proxy all other traffic to the frontend service + location / { + proxy_pass http://frontend:4200/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } + + # Backend HTTPS server block + server { + listen 443 ssl; + listen [::]:443 ssl; + server_name api.arman.dev www.api.arman.dev; + server_tokens off; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_prefer_server_ciphers off; # Prefer the client's cipher suite + ssl_ciphers "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256"; + + ssl_certificate /etc/nginx/ssl/live/api.arman.dev/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/live/api.arman.dev/privkey.pem; + + # Serve Certbot challenge files over HTTPS as well + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + # Proxy all other traffic to the backend service + location / { + proxy_pass http://backend:8000/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /rabbit/ { + proxy_pass http://rabbit:15672/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /static/ { + alias /usr/share/nginx/static/; + } + + } +} diff --git a/postgres/init/init.sql b/postgres/init/init.sql new file mode 100644 index 00000000..b1ab39f2 --- /dev/null +++ b/postgres/init/init.sql @@ -0,0 +1,17 @@ +-- Create database +CREATE DATABASE arman; + +-- Create user and set password +CREATE USER arman_admin WITH PASSWORD 'fghgj