start project
CI / Backend (lint, types, tests) (push) Has been cancelled
CI / Frontend (lint, types, build) (push) Has been cancelled
CI / Docker images build (push) Has been cancelled

This commit is contained in:
2026-09-04 12:52:21 +03:30
parent a464e550c9
commit 212074c5f5
40 changed files with 2804 additions and 164 deletions
+16
View File
@@ -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
# ---------------------------------------------------------------------------
+31 -1
View File
@@ -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
View File
@@ -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"