start project
This commit is contained in:
@@ -64,6 +64,9 @@ INSTALLED_APPS = [
|
||||
"django_filters",
|
||||
"corsheaders",
|
||||
"drf_spectacular",
|
||||
# Serves the Swagger UI / ReDoc bundles from our own origin instead of a
|
||||
# CDN, which is what keeps the strict CSP in frontend/nginx.conf working.
|
||||
"drf_spectacular_sidecar",
|
||||
# Local
|
||||
"apps.core",
|
||||
"apps.accounts",
|
||||
@@ -212,8 +215,21 @@ SPECTACULAR_SETTINGS = {
|
||||
"COMPONENT_SPLIT_REQUEST": True,
|
||||
"SCHEMA_PATH_PREFIX": "/api/v1",
|
||||
"SERVE_PERMISSIONS": ["rest_framework.permissions.AllowAny"],
|
||||
# Serve the UI assets from this origin (drf-spectacular-sidecar) rather
|
||||
# than the default cdn.jsdelivr.net "@latest" URLs: the CSP blocks
|
||||
# third-party scripts, so the CDN version renders a blank page.
|
||||
"SWAGGER_UI_DIST": "SIDECAR",
|
||||
"SWAGGER_UI_FAVICON_HREF": "SIDECAR",
|
||||
"REDOC_DIST": "SIDECAR",
|
||||
}
|
||||
|
||||
# Whether /api/docs/, /api/redoc/ and /api/schema/ are routed at all. They
|
||||
# describe every endpoint, parameter and response shape in the service, so
|
||||
# production keeps them off by default and opts in explicitly (behind an
|
||||
# authenticating proxy) rather than publishing the full API surface to
|
||||
# anonymous callers - see config/urls.py.
|
||||
SERVE_API_DOCS = env.bool("SERVE_API_DOCS", default=True)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CORS / CSRF
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -7,7 +7,7 @@ back to an insecure default.
|
||||
"""
|
||||
|
||||
from .base import * # noqa: F403
|
||||
from .base import DEBUG, SECRET_KEY, env
|
||||
from .base import DATABASES, DEBUG, SECRET_KEY, env
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fail fast on unsafe configuration
|
||||
@@ -18,11 +18,36 @@ if DEBUG: # pragma: no cover - configuration guard
|
||||
if len(SECRET_KEY) < 50: # pragma: no cover - configuration guard
|
||||
raise RuntimeError("DJANGO_SECRET_KEY must be at least 50 characters in production.")
|
||||
|
||||
# A length check alone is false comfort: the placeholder shipped in
|
||||
# .env.example is 68 characters long and would sail straight through it.
|
||||
_PLACEHOLDER_MARKERS = ("change-me", "changeme", "build-only", "not-a-real-secret")
|
||||
|
||||
|
||||
def _reject_placeholder(name: str, value: str) -> None: # pragma: no cover - guard
|
||||
lowered = value.lower()
|
||||
if any(marker in lowered for marker in _PLACEHOLDER_MARKERS):
|
||||
raise RuntimeError(
|
||||
f"{name} still holds a placeholder value from .env.example. "
|
||||
"Generate a real secret before deploying: "
|
||||
'python -c "import secrets; print(secrets.token_urlsafe(64))"'
|
||||
)
|
||||
|
||||
|
||||
_reject_placeholder("DJANGO_SECRET_KEY", SECRET_KEY)
|
||||
_reject_placeholder("POSTGRES_PASSWORD", DATABASES["default"].get("PASSWORD") or "")
|
||||
_reject_placeholder("ELASTICSEARCH_PASSWORD", env("ELASTICSEARCH_PASSWORD", default=""))
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Transport security
|
||||
# ---------------------------------------------------------------------------
|
||||
SECURE_SSL_REDIRECT = env.bool("SECURE_SSL_REDIRECT", default=True)
|
||||
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
||||
|
||||
# Container/orchestrator probes hit the API over plain HTTP on localhost, so
|
||||
# without an exemption every probe gets a 301 to https. `curl -fsS` treats a
|
||||
# 3xx as success, which means a completely broken container would still report
|
||||
# itself healthy - the probe has to reach the real view to be worth anything.
|
||||
SECURE_REDIRECT_EXEMPT = [r"^api/v1/health/"]
|
||||
SECURE_HSTS_SECONDS = 31_536_000 # one year
|
||||
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
||||
SECURE_HSTS_PRELOAD = True
|
||||
@@ -40,3 +65,8 @@ ADMIN_URL = env("DJANGO_ADMIN_URL", default="admin/")
|
||||
|
||||
# Deny by default; CORS_ALLOWED_ORIGINS must be set explicitly.
|
||||
CORS_ALLOW_ALL_ORIGINS = False
|
||||
|
||||
# The interactive docs describe every endpoint and payload in the service and
|
||||
# are served unauthenticated. Off unless the operator explicitly opts in (for
|
||||
# an internal-only origin, say) - see config/urls.py.
|
||||
SERVE_API_DOCS = env.bool("SERVE_API_DOCS", default=False)
|
||||
|
||||
+15
-8
@@ -25,16 +25,23 @@ api_v1 = [
|
||||
urlpatterns = [
|
||||
path(getattr(settings, "ADMIN_URL", "admin/"), admin.site.urls),
|
||||
path("api/v1/", include((api_v1, "api"), namespace="v1")),
|
||||
# OpenAPI schema and the two documentation renderers.
|
||||
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
|
||||
path(
|
||||
"api/docs/",
|
||||
SpectacularSwaggerView.as_view(url_name="schema"),
|
||||
name="swagger-ui",
|
||||
),
|
||||
path("api/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
|
||||
]
|
||||
|
||||
# OpenAPI schema and the two documentation renderers. They are served without
|
||||
# authentication, and together they document every endpoint, parameter and
|
||||
# response shape in the service - useful everywhere except on a public
|
||||
# production origin, where SERVE_API_DOCS defaults to false.
|
||||
if getattr(settings, "SERVE_API_DOCS", True):
|
||||
urlpatterns += [
|
||||
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
|
||||
path(
|
||||
"api/docs/",
|
||||
SpectacularSwaggerView.as_view(url_name="schema"),
|
||||
name="swagger-ui",
|
||||
),
|
||||
path("api/redoc/", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
|
||||
]
|
||||
|
||||
admin.site.site_header = "LinkedIn Profile Search"
|
||||
admin.site.site_title = "LinkedIn Profile Search"
|
||||
admin.site.index_title = "Operations"
|
||||
|
||||
Reference in New Issue
Block a user