Harden runtime configuration and container security defaults
This commit is contained in:
@ -1,10 +1,14 @@
|
|||||||
# Django
|
# Django
|
||||||
DJANGO_SETTINGS_MODULE=config.settings.development
|
DJANGO_SETTINGS_MODULE=config.settings.development
|
||||||
|
DJANGO_ENV=development
|
||||||
|
# Required to be a strong, unique value when DJANGO_DEBUG=0.
|
||||||
DJANGO_SECRET_KEY=change-me-in-production
|
DJANGO_SECRET_KEY=change-me-in-production
|
||||||
DJANGO_DEBUG=1
|
DJANGO_DEBUG=1
|
||||||
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
|
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
|
||||||
DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost,http://127.0.0.1
|
DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost,http://127.0.0.1
|
||||||
DJANGO_TIME_ZONE=UTC
|
DJANGO_TIME_ZONE=UTC
|
||||||
|
DJANGO_LOG_LEVEL=INFO
|
||||||
|
DJANGO_LOG_SQL=0
|
||||||
DJANGO_SUPERUSER_USERNAME=admin
|
DJANGO_SUPERUSER_USERNAME=admin
|
||||||
DJANGO_SUPERUSER_EMAIL=admin@example.com
|
DJANGO_SUPERUSER_EMAIL=admin@example.com
|
||||||
DJANGO_SUPERUSER_PASSWORD=adminpass
|
DJANGO_SUPERUSER_PASSWORD=adminpass
|
||||||
@ -29,6 +33,10 @@ AUTO_COLLECTSTATIC=1
|
|||||||
AUTO_BUILD_TAILWIND=1
|
AUTO_BUILD_TAILWIND=1
|
||||||
GUNICORN_WORKERS=3
|
GUNICORN_WORKERS=3
|
||||||
|
|
||||||
|
# Production-minded security toggles
|
||||||
|
DJANGO_SECURE_SSL_REDIRECT=1
|
||||||
|
DJANGO_SECURE_HSTS_SECONDS=31536000
|
||||||
|
|
||||||
# Providers / ingestion
|
# Providers / ingestion
|
||||||
PROVIDER_BACKEND=demo
|
PROVIDER_BACKEND=demo
|
||||||
PROVIDER_NAMESPACE_DEMO=mvp_demo
|
PROVIDER_NAMESPACE_DEMO=mvp_demo
|
||||||
|
|||||||
13
Dockerfile
13
Dockerfile
@ -24,7 +24,10 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
|||||||
PYTHONUNBUFFERED=1 \
|
PYTHONUNBUFFERED=1 \
|
||||||
PIP_NO_CACHE_DIR=1 \
|
PIP_NO_CACHE_DIR=1 \
|
||||||
VIRTUAL_ENV=/opt/venv \
|
VIRTUAL_ENV=/opt/venv \
|
||||||
PATH="/opt/venv/bin:${PATH}"
|
PATH="/opt/venv/bin:/home/app/.local/bin:${PATH}" \
|
||||||
|
APP_USER=app \
|
||||||
|
APP_UID=10001 \
|
||||||
|
APP_GID=10001
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@ -32,6 +35,10 @@ RUN apt-get update \
|
|||||||
&& apt-get install -y --no-install-recommends libpq5 postgresql-client curl nodejs npm \
|
&& apt-get install -y --no-install-recommends libpq5 postgresql-client curl nodejs npm \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN groupadd --gid "${APP_GID}" "${APP_USER}" \
|
||||||
|
&& useradd --uid "${APP_UID}" --gid "${APP_GID}" --create-home --shell /usr/sbin/nologin "${APP_USER}"
|
||||||
|
RUN printf '%s\n' 'export PATH="/opt/venv/bin:/home/app/.local/bin:$PATH"' > /etc/profile.d/hoopscout-path.sh
|
||||||
|
|
||||||
COPY --from=builder /opt/venv /opt/venv
|
COPY --from=builder /opt/venv /opt/venv
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
|
||||||
@ -39,7 +46,9 @@ RUN if [ -f package.json ]; then npm install --no-audit --no-fund; fi
|
|||||||
RUN if [ -f package.json ]; then npm run build; fi
|
RUN if [ -f package.json ]; then npm run build; fi
|
||||||
|
|
||||||
RUN chmod +x /app/entrypoint.sh
|
RUN chmod +x /app/entrypoint.sh
|
||||||
RUN mkdir -p /app/staticfiles /app/media /app/runtime
|
RUN mkdir -p /app/staticfiles /app/media /app/runtime /app/node_modules /app/static/vendor \
|
||||||
|
&& chown -R "${APP_UID}:${APP_GID}" /app /opt/venv
|
||||||
|
|
||||||
|
USER ${APP_UID}:${APP_GID}
|
||||||
ENTRYPOINT ["/app/entrypoint.sh"]
|
ENTRYPOINT ["/app/entrypoint.sh"]
|
||||||
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
|
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
|
||||||
|
|||||||
25
README.md
25
README.md
@ -95,6 +95,7 @@ docker compose exec web python manage.py createsuperuser
|
|||||||
|
|
||||||
- `web` service starts through `entrypoint.sh` and waits for PostgreSQL readiness.
|
- `web` service starts through `entrypoint.sh` and waits for PostgreSQL readiness.
|
||||||
- `web` service also builds Tailwind CSS before `collectstatic` when `AUTO_BUILD_TAILWIND=1`.
|
- `web` service also builds Tailwind CSS before `collectstatic` when `AUTO_BUILD_TAILWIND=1`.
|
||||||
|
- `web`, `celery_worker`, `celery_beat`, and `tailwind` run as a non-root user inside the image.
|
||||||
- `celery_worker` executes background sync work.
|
- `celery_worker` executes background sync work.
|
||||||
- `celery_beat` supports scheduled jobs (future scheduling strategy can be added per provider).
|
- `celery_beat` supports scheduled jobs (future scheduling strategy can be added per provider).
|
||||||
- `tailwind` service runs watch mode for development (`npm run dev`).
|
- `tailwind` service runs watch mode for development (`npm run dev`).
|
||||||
@ -156,6 +157,30 @@ docker compose up tailwind
|
|||||||
```
|
```
|
||||||
|
|
||||||
Source CSS lives in `static/src/tailwind.css` and compiles to `static/css/main.css`.
|
Source CSS lives in `static/src/tailwind.css` and compiles to `static/css/main.css`.
|
||||||
|
HTMX is served from local static assets (`static/vendor/htmx.min.js`) instead of a CDN dependency.
|
||||||
|
|
||||||
|
## Production Configuration
|
||||||
|
|
||||||
|
Use production settings in deployed environments:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DJANGO_SETTINGS_MODULE=config.settings.production
|
||||||
|
DJANGO_DEBUG=0
|
||||||
|
DJANGO_ENV=production
|
||||||
|
```
|
||||||
|
|
||||||
|
When `DJANGO_DEBUG=0`, startup fails fast unless:
|
||||||
|
|
||||||
|
- `DJANGO_SECRET_KEY` is a real non-default value
|
||||||
|
- `DJANGO_ALLOWED_HOSTS` is set
|
||||||
|
- `DJANGO_CSRF_TRUSTED_ORIGINS` is set (for production settings)
|
||||||
|
|
||||||
|
Production settings enable hardened defaults such as:
|
||||||
|
|
||||||
|
- secure cookies
|
||||||
|
- HSTS
|
||||||
|
- security headers
|
||||||
|
- `ManifestStaticFilesStorage` for static asset integrity/versioning
|
||||||
|
|
||||||
## Superuser and Auth
|
## Superuser and Auth
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import os
|
import os
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||||
|
|
||||||
@ -16,6 +17,7 @@ def env_list(key: str, default: str = "") -> list[str]:
|
|||||||
return [item.strip() for item in value.split(",") if item.strip()]
|
return [item.strip() for item in value.split(",") if item.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
DJANGO_ENV = os.getenv("DJANGO_ENV", "development").strip().lower()
|
||||||
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "insecure-development-secret")
|
SECRET_KEY = os.getenv("DJANGO_SECRET_KEY", "insecure-development-secret")
|
||||||
DEBUG = env_bool("DJANGO_DEBUG", False)
|
DEBUG = env_bool("DJANGO_DEBUG", False)
|
||||||
ALLOWED_HOSTS = env_list("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1")
|
ALLOWED_HOSTS = env_list("DJANGO_ALLOWED_HOSTS", "localhost,127.0.0.1")
|
||||||
@ -23,6 +25,11 @@ CSRF_TRUSTED_ORIGINS = env_list(
|
|||||||
"DJANGO_CSRF_TRUSTED_ORIGINS", "http://localhost,http://127.0.0.1"
|
"DJANGO_CSRF_TRUSTED_ORIGINS", "http://localhost,http://127.0.0.1"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not DEBUG and SECRET_KEY in {"", "insecure-development-secret", "change-me-in-production"}:
|
||||||
|
raise ImproperlyConfigured("DJANGO_SECRET_KEY must be set to a strong value when DEBUG=0.")
|
||||||
|
if not DEBUG and not ALLOWED_HOSTS:
|
||||||
|
raise ImproperlyConfigured("DJANGO_ALLOWED_HOSTS must not be empty when DEBUG=0.")
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
"django.contrib.admin",
|
"django.contrib.admin",
|
||||||
"django.contrib.auth",
|
"django.contrib.auth",
|
||||||
@ -96,12 +103,12 @@ TIME_ZONE = os.getenv("DJANGO_TIME_ZONE", "UTC")
|
|||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
STATIC_URL = "/static/"
|
STATIC_URL = os.getenv("DJANGO_STATIC_URL", "/static/")
|
||||||
STATIC_ROOT = BASE_DIR / "staticfiles"
|
STATIC_ROOT = Path(os.getenv("DJANGO_STATIC_ROOT", str(BASE_DIR / "staticfiles")))
|
||||||
STATICFILES_DIRS = [BASE_DIR / "static"]
|
STATICFILES_DIRS = [BASE_DIR / "static"]
|
||||||
|
|
||||||
MEDIA_URL = "/media/"
|
MEDIA_URL = os.getenv("DJANGO_MEDIA_URL", "/media/")
|
||||||
MEDIA_ROOT = BASE_DIR / "media"
|
MEDIA_ROOT = Path(os.getenv("DJANGO_MEDIA_ROOT", str(BASE_DIR / "media")))
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
@ -141,6 +148,35 @@ PROVIDER_BALLDONTLIE_SEASONS = [
|
|||||||
if value.strip().isdigit()
|
if value.strip().isdigit()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
LOG_LEVEL = os.getenv("DJANGO_LOG_LEVEL", "INFO").upper()
|
||||||
|
LOG_SQL = env_bool("DJANGO_LOG_SQL", False)
|
||||||
|
LOGGING = {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"formatters": {
|
||||||
|
"standard": {
|
||||||
|
"format": "%(asctime)s %(levelname)s %(name)s %(message)s",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers": {
|
||||||
|
"console": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"formatter": "standard",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": LOG_LEVEL,
|
||||||
|
},
|
||||||
|
"loggers": {
|
||||||
|
"django.db.backends": {
|
||||||
|
"handlers": ["console"],
|
||||||
|
"level": "DEBUG" if LOG_SQL else "WARNING",
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
"DEFAULT_PERMISSION_CLASSES": [
|
"DEFAULT_PERMISSION_CLASSES": [
|
||||||
"apps.api.permissions.ReadOnlyOrDeny",
|
"apps.api.permissions.ReadOnlyOrDeny",
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
from .base import * # noqa: F403,F401
|
from .base import * # noqa: F403,F401
|
||||||
|
|
||||||
DEBUG = True
|
DEBUG = True
|
||||||
|
SECURE_SSL_REDIRECT = False
|
||||||
|
SESSION_COOKIE_SECURE = False
|
||||||
|
CSRF_COOKIE_SECURE = False
|
||||||
|
|||||||
@ -1,13 +1,33 @@
|
|||||||
from .base import * # noqa: F403,F401
|
from .base import * # noqa: F403,F401
|
||||||
import os
|
import os
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
|
|
||||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||||
|
USE_X_FORWARDED_HOST = True
|
||||||
|
SECURE_SSL_REDIRECT = os.getenv("DJANGO_SECURE_SSL_REDIRECT", "1") == "1"
|
||||||
SESSION_COOKIE_SECURE = True
|
SESSION_COOKIE_SECURE = True
|
||||||
CSRF_COOKIE_SECURE = True
|
CSRF_COOKIE_SECURE = True
|
||||||
SECURE_BROWSER_XSS_FILTER = True
|
|
||||||
SECURE_CONTENT_TYPE_NOSNIFF = True
|
SECURE_CONTENT_TYPE_NOSNIFF = True
|
||||||
|
SECURE_REFERRER_POLICY = "same-origin"
|
||||||
|
X_FRAME_OPTIONS = "DENY"
|
||||||
SECURE_HSTS_SECONDS = int(os.getenv("DJANGO_SECURE_HSTS_SECONDS", "31536000"))
|
SECURE_HSTS_SECONDS = int(os.getenv("DJANGO_SECURE_HSTS_SECONDS", "31536000"))
|
||||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||||
SECURE_HSTS_PRELOAD = True
|
SECURE_HSTS_PRELOAD = True
|
||||||
|
CSRF_COOKIE_HTTPONLY = True
|
||||||
|
SESSION_COOKIE_HTTPONLY = True
|
||||||
|
SESSION_COOKIE_SAMESITE = os.getenv("DJANGO_SESSION_COOKIE_SAMESITE", "Lax")
|
||||||
|
CSRF_COOKIE_SAMESITE = os.getenv("DJANGO_CSRF_COOKIE_SAMESITE", "Lax")
|
||||||
|
|
||||||
|
if not CSRF_TRUSTED_ORIGINS: # noqa: F405
|
||||||
|
raise ImproperlyConfigured("DJANGO_CSRF_TRUSTED_ORIGINS must be set for production.")
|
||||||
|
|
||||||
|
STORAGES = {
|
||||||
|
"default": {
|
||||||
|
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||||
|
},
|
||||||
|
"staticfiles": {
|
||||||
|
"BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
@ -10,11 +10,16 @@ services:
|
|||||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
||||||
- static_data:/var/www/static:ro
|
- static_data:/var/www/static:ro
|
||||||
- media_data:/var/www/media:ro
|
- media_data:/var/www/media:ro
|
||||||
|
read_only: true
|
||||||
|
tmpfs:
|
||||||
|
- /var/cache/nginx
|
||||||
|
- /var/run
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/health/ || exit 1"]
|
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/health/ || exit 1"]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
web:
|
web:
|
||||||
@ -28,7 +33,7 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
command: gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers ${GUNICORN_WORKERS:-3}
|
command: gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers ${GUNICORN_WORKERS:-3} --access-logfile - --error-logfile -
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
- .:/app
|
||||||
- node_modules_data:/app/node_modules
|
- node_modules_data:/app/node_modules
|
||||||
@ -42,6 +47,7 @@ services:
|
|||||||
interval: 15s
|
interval: 15s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 8
|
retries: 8
|
||||||
|
start_period: 20s
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
tailwind:
|
tailwind:
|
||||||
@ -76,6 +82,7 @@ services:
|
|||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
celery_beat:
|
celery_beat:
|
||||||
@ -98,12 +105,15 @@ services:
|
|||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
|
start_period: 20s
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
env_file:
|
environment:
|
||||||
- .env
|
POSTGRES_DB: ${POSTGRES_DB}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
@ -8,6 +8,11 @@ done
|
|||||||
|
|
||||||
echo "PostgreSQL is available."
|
echo "PostgreSQL is available."
|
||||||
|
|
||||||
|
if [ "${DJANGO_SETTINGS_MODULE:-}" = "config.settings.production" ] && [ "$1" = "gunicorn" ]; then
|
||||||
|
echo "Running Django deployment checks..."
|
||||||
|
python manage.py check --deploy --fail-level WARNING
|
||||||
|
fi
|
||||||
|
|
||||||
if [ "${AUTO_APPLY_MIGRATIONS:-0}" = "1" ] && [ "$1" = "gunicorn" ]; then
|
if [ "${AUTO_APPLY_MIGRATIONS:-0}" = "1" ] && [ "$1" = "gunicorn" ]; then
|
||||||
echo "Applying database migrations..."
|
echo "Applying database migrations..."
|
||||||
python manage.py migrate --noinput
|
python manage.py migrate --noinput
|
||||||
@ -15,11 +20,12 @@ fi
|
|||||||
|
|
||||||
if [ "${AUTO_COLLECTSTATIC:-0}" = "1" ] && [ "$1" = "gunicorn" ]; then
|
if [ "${AUTO_COLLECTSTATIC:-0}" = "1" ] && [ "$1" = "gunicorn" ]; then
|
||||||
if [ "${AUTO_BUILD_TAILWIND:-1}" = "1" ] && [ -f /app/package.json ]; then
|
if [ "${AUTO_BUILD_TAILWIND:-1}" = "1" ] && [ -f /app/package.json ]; then
|
||||||
|
if [ -x /app/node_modules/.bin/tailwindcss ]; then
|
||||||
echo "Building Tailwind assets..."
|
echo "Building Tailwind assets..."
|
||||||
if [ ! -d /app/node_modules ]; then
|
|
||||||
npm install --no-audit --no-fund
|
|
||||||
fi
|
|
||||||
npm run build
|
npm run build
|
||||||
|
else
|
||||||
|
echo "Tailwind dependencies missing; skipping AUTO_BUILD_TAILWIND."
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Collecting static files..."
|
echo "Collecting static files..."
|
||||||
|
|||||||
@ -19,16 +19,21 @@ http {
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-Frame-Options "DENY" always;
|
||||||
|
add_header Referrer-Policy "same-origin" always;
|
||||||
|
|
||||||
location /static/ {
|
location /static/ {
|
||||||
alias /var/www/static/;
|
alias /var/www/static/;
|
||||||
expires 30d;
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, max-age=2592000, immutable";
|
||||||
access_log off;
|
access_log off;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /media/ {
|
location /media/ {
|
||||||
alias /var/www/media/;
|
alias /var/www/media/;
|
||||||
expires 30d;
|
expires 7d;
|
||||||
|
add_header Cache-Control "public, max-age=604800";
|
||||||
access_log off;
|
access_log off;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
package-lock.json
generated
8
package-lock.json
generated
@ -7,6 +7,9 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "hoopscout-frontend",
|
"name": "hoopscout-frontend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"htmx.org": "^1.9.12"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tailwindcss": "^3.4.17"
|
"tailwindcss": "^3.4.17"
|
||||||
}
|
}
|
||||||
@ -275,6 +278,11 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/htmx.org": {
|
||||||
|
"version": "1.9.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-1.9.12.tgz",
|
||||||
|
"integrity": "sha512-VZAohXyF7xPGS52IM8d1T1283y+X4D+Owf3qY1NZ9RuBypyu9l8cGsxUMAG5fEAb/DhT7rDoJ9Hpu5/HxFD3cw=="
|
||||||
|
},
|
||||||
"node_modules/is-binary-path": {
|
"node_modules/is-binary-path": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
@ -4,8 +4,12 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"description": "Tailwind pipeline for HoopScout Django templates",
|
"description": "Tailwind pipeline for HoopScout Django templates",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tailwindcss -c tailwind.config.js -i ./static/src/tailwind.css -o ./static/css/main.css --minify",
|
"build:vendor": "mkdir -p ./static/vendor && cp ./node_modules/htmx.org/dist/htmx.min.js ./static/vendor/htmx.min.js",
|
||||||
"dev": "tailwindcss -c tailwind.config.js -i ./static/src/tailwind.css -o ./static/css/main.css --watch"
|
"build": "npm run build:vendor && tailwindcss -c tailwind.config.js -i ./static/src/tailwind.css -o ./static/css/main.css --minify",
|
||||||
|
"dev": "npm run build:vendor && tailwindcss -c tailwind.config.js -i ./static/src/tailwind.css -o ./static/css/main.css --watch"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"htmx.org": "^1.9.12"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tailwindcss": "^3.4.17"
|
"tailwindcss": "^3.4.17"
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
[pytest]
|
[pytest]
|
||||||
DJANGO_SETTINGS_MODULE = config.settings.development
|
DJANGO_SETTINGS_MODULE = config.settings.development
|
||||||
python_files = tests.py test_*.py *_tests.py
|
python_files = tests.py test_*.py *_tests.py
|
||||||
|
cache_dir = /tmp/.pytest_cache
|
||||||
|
|||||||
1
static/vendor/htmx.min.js
vendored
Normal file
1
static/vendor/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -6,7 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{% block title %}HoopScout{% endblock %}</title>
|
<title>{% block title %}HoopScout{% endblock %}</title>
|
||||||
<link rel="stylesheet" href="{% static 'css/main.css' %}">
|
<link rel="stylesheet" href="{% static 'css/main.css' %}">
|
||||||
<script src="https://unpkg.com/htmx.org@1.9.12" defer></script>
|
<script src="{% static 'vendor/htmx.min.js' %}" defer></script>
|
||||||
</head>
|
</head>
|
||||||
<body class="min-h-full bg-slate-100 text-slate-900">
|
<body class="min-h-full bg-slate-100 text-slate-900">
|
||||||
<header class="border-b border-slate-200 bg-white">
|
<header class="border-b border-slate-200 bg-white">
|
||||||
|
|||||||
Reference in New Issue
Block a user