--- since: 1.2.1 --- # DjangoPlay — `paystream` (Main Django Project) > Doc generated inspecting every file under `webapp/paystream/` (settings package, `app_settings/` `security/` including all fraud/abuse infra services, `custom_site/`, `urlconf/`, `hosts.py`, `integrations/issuetracker/` — the full adapter layer, `services/`, `celery.py`, `apps.py`, `asgi.py`/`wsgi.py`), plus `webapp/manage.py` and the root `pyproject.toml` for how the project is actually bootstrapped and versioned. ## 1. Summary `paystream` is not a domain app — it **is the Django project itself**. It owns: 1. **The settings package** (`settings/` + `app_settings/`) — a layered, environment-aware, secrets-encrypted configuration system that every other app's `apps.py`/`AppConfig` and every `docs/system/apps/*.md` doc in this series ultimately depends on. 2. **URL/host routing** (`urlconf/`, `hosts.py`) — the top-level `ROOT_URLCONF`, plus `django-hosts`-based subdomain routing that gives DjangoPlay its three public surfaces: the main app, `issues.`, and `docs.`. 3. **The custom Django Admin Site and console** (`custom_site/`) — `PaystreamAdminSite`, the single `AdminSite` instance every app's `ModelAdmin` registers against (via `utilities.admin.mixins.BaseAdminPage`), plus the view functions behind the custom `/console/` UI that `utilities.admin.app_builder`/`dashboard_metadata` feed into. 4. **The Issue Tracker integration** (`integrations/issuetracker/`) — DjangoPlay's adapter layer on top of the third-party `genericissuetracker` package: identity resolution, RBAC visibility, audit-event bridging, a protected attachment-download endpoint, and the UI/API views served on the `issues.` subdomain. 5. **Security infrastructure** (`security/`) — Fernet-based secrets encryption/decryption for the whole project, plus a signup-fraud-scoring pipeline (AbuseIPDB, disposable-email detection, garbage-name heuristics, Cloudflare Turnstile) consumed by `users`' signup flow. 6. **Background job wiring** (`celery.py` at both project root and `app_settings/celery.py`) and **structured logging** (`app_settings/logging.py`) for the whole platform. 7. **Small project-level services**: runtime `Site`/`SocialApp` bootstrapping (`services/`), a wireless/connectivity check endpoint, and the template context processors that inject site/subdomain URLs into every page. Every other app in this documentation series is, from `paystream`'s point of view, an entry in `INSTALLED_APPS` and a set of `include()`d URLs. This doc is the one place that shows how they're actually wired together into one running Django project — `INSTALLED_APPS`, middleware order, the subdomain topology, and the settings layering that decides what "dev" vs "staging" vs "prod" actually means. --- ## 2. Architecture ### 2.1 Directory map ``` paystream/ ├── settings/ Environment-layered Django settings (§3) │ ├── base.py Assembles all app_settings/*.py into one namespace │ ├── dev.py / staging.py / prod.py Environment overrides (loaded via DJANGO_SETTINGS_MODULE) │ └── validation.py Fail-fast startup checks (SITE_PROTOCOL/HOST/PORT/SECRET_KEY) ├── app_settings/ 23 single-concern settings modules (§3.2), imported by base.py ├── security/ Fernet secrets encryption + signup-fraud infra services (§5) ├── custom_site/ PaystreamAdminSite + custom console admin views (§4) ├── urlconf/ ROOT_URLCONF (default.py→base.py) + per-subdomain urlconfs (§2.2) ├── hosts.py django-hosts ROOT_HOSTCONF — issues./docs./default routing (§2.2) ├── integrations/issuetracker/ Adapter layer over the third-party genericissuetracker package (§6) ├── services/ Runtime Site/SocialApp bootstrap + context processors (§7) ├── apps.py PaystreamConfig.ready() — startup hooks (§2.3) ├── celery.py Celery app instance (project root, imported by __init__.py-style entry points) ├── asgi.py / wsgi.py ASGI/WSGI entrypoints └── views/wireless_check.py Trivial connectivity-check endpoint, mounted on both default and issues hosts ``` ### 2.2 Host/subdomain routing (`hosts.py` + `urlconf/`) DjangoPlay uses `django-hosts` (`ROOT_HOSTCONF = "paystream.hosts"`, set in `app_settings/core.py`) to route by subdomain **before** Django's normal `ROOT_URLCONF` resolution: ```python host_patterns = patterns( "", host(r"issues", "paystream.urlconf.subdomains.issues", name="issues"), host(r"docs", "paystream.urlconf.subdomains.docs", name="docs"), host(r"", "paystream.urlconf.default", name="default"), ) ``` - **`issues.`** → `urlconf/subdomains/issues.py` — mounts the Issue Tracker's UI (`integrations/issuetracker/views/ui`) at the subdomain root, its API/downloads under `api/`, `helpdesk`'s public support form, a cross-subdomain session-check/relogin pair (`IssueSessionCheckView`/`IssueReLoginView` — see §6.4), and the shared `wireless_check` endpoint. - **`docs.`** → `urlconf/subdomains/docs.py` — serves a static documentation site from `settings.DOCS_ROOT` (a filesystem path, not a Django app) via `django.views.static.serve`, with a catch-all `serve_docs` view that falls back to `view.html` for client-side markdown routing (i.e., the docs site is a static SPA-like bundle, not server-rendered per-page). - **default host** (`""`) → `urlconf/default.py` → `urlconf/base.py` — the main application, documented app-by-app throughout this series; `urlconf/base.py` is where every domain app's `include()` lives (see the `utilities` app doc §2.5 for the full table of what's mounted where) plus the custom console/admin routes (§4). `MIDDLEWARE` (`app_settings/middleware.py`) puts `django_hosts.middleware.HostsRequestMiddleware` **first** and `HostsResponseMiddleware` **last**, which is required for `django-hosts` to correctly rewrite `{% host_url %}` template tags and reverse URLs across subdomains. `SESSION_COOKIE_DOMAIN`/`CSRF_COOKIE_DOMAIN` are both set to `.{SITE_HOST}` in every environment (`dev.py`/`staging.py`/`prod.py`) — this is what makes a single login session valid across the main app, `issues.`, and `docs.` subdomains simultaneously (the actual mechanism the Issue Tracker's cross-subdomain session-check endpoints, §6.4, depend on). ### 2.3 App startup (`apps.py::PaystreamConfig.ready()`) Runs two independent, defensively-guarded startup routines: 1. **Asset generation** — calls `mailer.engine.asset_builder.build_logo_assets()` inside a bare `try/except`; a failure here only logs a warning and does not block startup. 2. **Runtime `Site`/`SocialApp` bootstrap** — but only if the `django_site` table already exists (checked via raw `connection.introspection.table_names()`, swallowing `OperationalError`/`ProgrammingError` — i.e., this is a no-op on a fresh, unmigrated database, which is what lets `manage.py migrate` itself run without `apps.ready()` crashing). If the table exists, calls `services.runtime_site.ensure_runtime_site()` (creates/reuses a `django.contrib.sites.Site` row matching `SITE_HOST`/`SITE_PORT` and pins `settings.SITE_ID` to it at runtime — notably **mutating `settings.SITE_ID` after Django has already started**, not something set once at import time) then `services.socialapps.ensure_google_socialapp()` (idempotently creates/updates exactly one `allauth` Google `SocialApp`, deleting duplicates if more than one exists, and hard-fails with `RuntimeError` if the `email` OAuth scope is ever missing from its config). --- ## 3. Settings System ### 3.1 Environment selection and secrets resolution - **`manage.py`** defaults `DJANGO_SETTINGS_MODULE` to `paystream.settings.dev` if not already set in the environment. - **`wsgi.py`/`asgi.py`** default it to `paystream.settings` instead — the *package*, whose `__init__.py` is deliberately empty (docstring: *"Do NOT place Celery or settings logic here"*). In a deployment where `DJANGO_SETTINGS_MODULE` isn't explicitly exported (e.g. to `paystream.settings.prod`), WSGI/ASGI boot would import an empty settings module with none of `DEBUG`/`DATABASES`/`INSTALLED_APPS` defined — see §9, point 1. - **Secrets resolution** (`app_settings/common.py::get_decrypted_value`) checks, in order: `os.environ` → project `.env` file (via `django-environ`, plaintext or Fernet-encrypted) → `~/.dplay/.secrets` (a plaintext KEY=VALUE file, described as the "source of truth" for sensitive keys in `authx.py`'s module docstring). Any value beginning with the literal string `gAAAAA` (the Fernet token prefix) is decrypted with `ENCRYPTION_KEY` (`security/crypto.py`, `Fernet`-based); `ENCRYPTION_KEY` itself is resolved the same env→secrets-file fallback chain and the process **hard-fails at import time** (`ValueError`) if it can't be found — same pattern for `SECRET_KEY` in `app_settings/core.py`. `security/bootstrap_secrets.py::bootstrap_secrets()` / `common.py::load_all_decrypted_values()` decrypts every `gAAAAA…`-prefixed value in `os.environ` **in place**, used by the Celery bootstrap path so worker processes don't need to re-resolve secrets per-task. - **`SITE_URL` construction** (`app_settings/site.py::build_site_url`) is deliberately strict: raises if protocol isn't exactly `"http"`/`"https"`, and omits the port only when it's blank/`80`/`443` — every environment file (`dev.py`/`staging.py`/`prod.py`) calls this to compute the canonical `SITE_URL` after resolving `SITE_PROTOCOL`/`SITE_HOST`/`SITE_PORT`. - **`settings/validation.py::validate_settings`** runs at the end of `base.py` and raises `ImproperlyConfigured` for an invalid `SITE_PROTOCOL`, missing `SITE_HOST`, non-numeric `SITE_PORT`, or missing `SECRET_KEY` — a fail-fast guard against half-configured environments. ### 3.2 `app_settings/` — one file per concern `settings/base.py` assembles the whole `INSTALLED_APPS`/middleware/etc. namespace via `from paystream.app_settings.X import *` for 20 modules, in this fixed order: `ai`, `authx`, `cache`, `celery`, `common`, `core`, `database`, `drf_spectacular` (imported non-wildcard, as `SPECTACULAR_SETTINGS`), `email`, `genericissuetracker`, `jwt`, `link_expiry`, `logging`, `middleware`, `rest_framework`, `security`, `site`, `static_media`, `templates`, `werkzeug`, then `settings/validation.py`, and finally (after `SITE_PROTOCOL` is resolved) `allauth.py`. `public.py` exists but is currently empty. | Module | Owns | |---|---| | `core.py` | `SECRET_KEY` resolution, the full `INSTALLED_APPS` list (§3.3), `AUTH_USER_MODEL = "users.UserIdentity"`, `ROOT_URLCONF`/`WSGI_APPLICATION`/`ASGI_APPLICATION`, `ROOT_HOSTCONF`/`DEFAULT_HOST` | | `database.py` | Single `postgresql` `DATABASES["default"]` entry, fully secret-resolved (no SQLite fallback anywhere) | | `cache.py` | `django_redis`-backed `CACHES["default"]` (zlib compression, 200-connection pool, 60s ping interval) — this is the cache Django's own `cache` framework calls resolve to, distinct from the *raw* `core.utils.redis_client` several apps (`utilities`, `mailer`) talk to directly instead | | `celery.py` (app_settings) | Redis-backed broker/result-backend URL construction, `CELERY_BEAT_SCHEDULE` (currently exactly one entry — `audit.tasks.cleanup_expired_audit_events`, daily at 02:00), platform-conditional worker pool (`solo` on macOS, `prefork` elsewhere), and `configure_celery_settings()` (injects all `CELERY_*` globals into Django's settings namespace — called explicitly, wrapped in try/except-raise, at the end of `base.py`) | | `rest_framework.py` | Global DRF defaults: JWT + session auth, `IsAuthenticated` by default, `UserRateThrottle` (100/hr) plus the named scopes `custom`/`custom_search`/`token` that `utilities.api.rate_limits` implements, `drf_spectacular` as the schema class, a custom `EXCEPTION_HANDLER` (`users.views.ui.errors.custom_401`) | | `jwt.py` | `SIMPLE_JWT` — 2-hour access tokens, 1-day refresh (1-minute if "remember me" is off — see the inline comment, though as written both keys coexist: `REFRESH_TOKEN_LIFETIME` and `REFRESH_TOKEN_LIFETIME_REMEMBER_ME`, and only `users` app code, not shown here, decides which one is actually applied), rotation + blacklisting enabled | | `authx.py` | Configuration for `authx-identity` — **an external microservice**, not an installed Django app (§8) | | `allauth.py` | `AUTHENTICATION_BACKENDS`, all `ACCOUNT_*`/`SOCIALACCOUNT_*` settings, points at `users`' custom adapters (`CustomAccountAdapter`, `CustomSocialAccountAdapter`) | | `security.py` | Session/cookie defaults (1-hour `SESSION_COOKIE_AGE`, `cached_db` session engine), `AUTH_PASSWORD_VALIDATORS`, and the three-environment Cloudflare Turnstile key set (`TURNSTILE_*_DEV/STAGING/PROD`) plus `ABUSEIPDB_API_KEY` | | `email.py` | SMTP/console/dummy backend selection via `EMAIL_MODE`, the bug-report feature flag + URL allowlist (`REPORT_BUG_ENABLED_ON_URLS` — consumed by `utilities.context_processors.report_bug`), and `EMAIL_FLOW_LIMITS` loading with a three-tier fallback (env JSON → `configs/email_flow_limits.json` → hardcoded `FALLBACK_EMAIL_FLOW_LIMITS`), validated by `validate_limits()` | | `link_expiry.py` | Central day-based expiry constants for verification/reset/unsubscribe links | | `drf_spectacular.py` | `SPECTACULAR_SETTINGS` — title/description/version (hardcoded `"1.2.0"`, independent of `pyproject.toml`'s `APP_VERSION` — see §9), JWT bearer security scheme, pre/post-processing hooks (`apidocs.hooks.*` plus a local `inject_servers` hook that reads `settings.OPENAPI_SERVERS`, itself set per-environment in `dev.py`/`prod.py`) | | `genericissuetracker.py` | Settings for the third-party `genericissuetracker` package — page size, anonymous-reporting toggle, the identity-resolver dotted path (`users.services.issuetracker_identity_resolver...`), default permission class and transition policy (both pointing into `paystream.integrations.issuetracker.access_control.permissions`, §6.2), the internal-visibility allowed-roles list (`CEO`, `DJGO`, `SSO`), and a 5000-char comment length cap (below the DB's hard 10000-char cap, per the inline comment) | | `middleware.py` | The full `MIDDLEWARE` list (§3.4) | | `templates.py` | `TEMPLATES["DIRS"]` (`frontend/templates`), the context-processor list (four of which are `paystream`'s own — §7.2), and a cached template loader | | `static_media.py` | `STATIC_URL`/`STATIC_ROOT`/`STATICFILES_DIRS` (`ManifestStaticFilesStorage` for hashed prod filenames), `MEDIA_URL`/`MEDIA_ROOT`, and — oddly placed here rather than in an admin-specific module — `AUTO_LOAD_THRESHOLD = 500` (the changelist auto-load threshold `utilities.admin.base_admin_page.BaseAdminPage.get_auto_load_threshold` reads) | | `logging.py` | Full structured logging config (§3.5) | | `ai.py` | `aicore` app's provider-agnostic LLM config — xAI/OpenAI/custom(Ollama-compatible) switch, temperature/token/timeout/rate-limit knobs | | `werkzeug.py` | `RUNSERVER_PLUS` dev-only debugger config | | `common.py` | The secrets-resolution primitives described in §3.1, plus a grab-bag of directly-exported site-identity settings (`SITE_NAME`, `SUPPORT_EMAIL`/`PHONE`/`LOCATION`, `GITHUB_URL`/`LINKEDIN_URL`, `SUPERUSER_EMAIL` — default `"redstar@djangoplay.org"`, the same identity referenced by three other independent hardcoded-admin checks across the platform, §9 point 3 — and hardcoded founder bio constants used somewhere in `frontend`), and `APP_VERSION` read directly from the repo's root `pyproject.toml` at import time | ### 3.3 `INSTALLED_APPS` (verbatim order, from `app_settings/core.py`) ``` mptt, dal, dal_select2, django_hosts, django.contrib.{admin, auth, contenttypes, sessions, messages, staticfiles, sites, humanize}, django_extensions, drf_spectacular, audit, policyengine, frontend, apidocs, paystream, fincore, devtools, rest_framework, core, utilities, mailer, users, teamcentral, helpdesk, locations, industries, entities, invoices, aicore, allauth, allauth.account, allauth.socialaccount, allauth.socialaccount.providers.google, rest_framework_simplejwt.token_blacklist, simple_history, compressor, genericissuetracker, paystream.integrations.issuetracker ``` This is the definitive list of every installed Django app in the project — useful as a cross-check against any other doc in this series that references "every app" or "all installed apps." Note `mptt` (tree-structured models — likely used by one of the domain apps for hierarchical data, e.g. `locations`' region/subregion/city hierarchy or a category tree, not independently verified in this pass), `dal`/`dal_select2` (django-autocomplete-light — likely superseded in practice by `utilities`'s own hand-rolled AJAX form-options/filter-options endpoints, §2.5 of the `utilities` doc, though both being installed simultaneously is worth confirming isn't redundant), and `django_extensions` (dev tooling, e.g. `runserver_plus`/`shell_plus`) are present but not analyzed in depth in this pass. ### 3.4 Middleware order (`app_settings/middleware.py`) ``` django_hosts.HostsRequestMiddleware core.middleware.request_id.RequestIDMiddleware core.middleware.client_ip.ClientIPMiddleware django.middleware.security.SecurityMiddleware whitenoise.middleware.WhiteNoiseMiddleware django.contrib.sessions.middleware.SessionMiddleware django.middleware.common.CommonMiddleware django.middleware.csrf.CsrfViewMiddleware django.contrib.auth.middleware.AuthenticationMiddleware django.contrib.messages.middleware.MessageMiddleware allauth.account.middleware.AccountMiddleware core.middleware.timezone.TimezoneMiddleware core.execution_context.middleware.ExecutionContextMiddleware django.middleware.clickjacking.XFrameOptionsMiddleware simple_history.middleware.HistoryRequestMiddleware core.middleware.template_syntax.TemplateSyntaxErrorLoggingMiddleware core.middleware.api_request_logging.APIRequestLoggingMiddleware django_hosts.HostsResponseMiddleware ``` **There is no login-enforcement middleware anywhere in this list** — `AuthenticationMiddleware` only attaches `request.user`, it doesn't require authentication. Every view in the project is responsible for its own access control (`@login_required`, DRF `permission_classes`, or nothing at all) — this is directly relevant to the unauthenticated-AJAX-endpoint finding documented in the `utilities` app doc §5, and to the Issue Tracker's deliberately-open read endpoints (§6.2 below). ### 3.5 Logging (`app_settings/logging.py`) Structured (JSON, via `pythonjsonlogger`) file logging plus human-readable console output, built programmatically rather than as one static dict: - **Formatters**: `ConsoleLogFormatter` (human-readable) and `StructuredLogFormatter` (JSON; an `include_traceback` variant exists for `.errors`-suffixed loggers). - **Filters**: `ErrorOnlyFilter`/`NonErrorFilter` split console output between the `console` and `console_errors` handlers; `RequestTimezoneFilter` attaches the request's resolved timezone (via `core.request_context.get_timezone`) to every log record. - **Per-app rotating file handlers** are generated in a loop over `LOG_FILES` (5MB, 5 backups each) — but **only for apps listed in `AUTO_LOG_APPS`**: `users`, `locations`, `industries`, `entities`, `invoices`, `frontend`, `apidocs`, `policyengine`, `mailer` (+ `mailer.events`/`mailer.errors`/`mailer.audit`), `audit`, `django`, `django_redis`, `data_sync`. **Apps not in this list — `helpdesk`, `teamcentral`, `fincore`, `aicore`, `paystream` itself, `utilities`, `devtools`, `core`, `genericissuetracker`, and the issue-tracker integration — get no dedicated log file** and fall through to the root logger (console only) unless they configure their own logger elsewhere (not found in this pass). Worth confirming with the team whether this is intentional or an oversight, especially for `paystream` and `core`, which carry security-relevant code (signup fraud scoring, secrets handling). - **`ENABLE_ASYNC_LOGGING`** (env-flag, default off) swaps `console`+file handlers for a single `QueueHandler`+`console_errors` pair per logger — but the per-app file handler that was just built for that same app is **not** included in the async handler list, meaning turning on async logging silently stops writing to the per-app rotating files for every non-`.errors` logger (§9, point 5). --- ## 4. Custom Admin Site & Console (`custom_site/`) ### 4.1 `PaystreamAdminSite` (`custom_site/admin_site.py`) The single `admin_site = PaystreamAdminSite(name='console')` instance that `utilities.admin.mixins.decorators.AdminIconDecorator` and every app's `ModelAdmin` registration ultimately targets instead of Django's default site. Notable overrides: - **`has_permission(request)`** = `request.user.is_active and request.user.is_authenticated` for non-POST requests (POST always passes, to let CSRF middleware handle the rejection instead). **This omits Django's normal `is_staff` requirement entirely** — any active, authenticated user clears the `AdminSite` gate; per-model access is still enforced downstream (Django `has_view_permission`/registry checks — see the `utilities` doc §2.3/§2.1), but the "must be `is_staff` to even reach the admin" layer that ships with Django by default does not exist in this project. Worth confirming this is intentional given how many other layers already gate access (§9, point 2). - **`index()`** and **`login()`** redirect to the custom `/console/dashboard/` and `account_login` routes respectively rather than rendering Django's default admin index/login pages — the built-in `AdminSite` UI is effectively bypassed entirely in favor of the custom console (`utilities.admin.app_builder`/`dashboard_metadata`, `admin_console_views.py` below). - **`admin_view()`** wraps every admin view so a raised `PermissionDenied` renders `errors/base.html` (via `permission_denied()`) instead of Django's default 403 page, while explicitly letting already-formed 401/403 HTTP responses (e.g. CSRF failures) pass through untouched. - **`catch_all_view()`** and **`app_index()`** redirect Django admin's own built-in URL patterns (`/admin/`, `/admin//`) toward the custom `generic_404` and `/console/apps//` respectively — i.e., Django's native admin URL surface is intentionally routed away from wherever possible, in favor of the parallel `/console/` URL space. ### 4.2 Custom console views (`custom_site/admin_console_views.py`) Three `@login_required` view functions — `app_index_view`, `single_app_view`, `custom_changelist_view` — that back the `/console/apps/...` URLs (mounted in `urlconf/base.py`, reverse-named `admin_single_app`/`admin_custom_changelist`, and the functions `utilities.admin.app_builder.build_app_list()`/`dashboard_metadata.py` build links against). Each layers the same sequence of checks: 1. **A hardcoded "users app" special case**: if `app_label == "users"`, access is denied outright to everyone whose `username != "redstar"` — **a fourth, independent hardcoded-identity access-control mechanism**, this time username-based, alongside `utilities.services.is_admin.PlatformAdminService`'s two-email allowlist, `configs/admin_registry.json`'s `"users": {"enabled": false}` entry, and `common.py`'s `SUPERUSER_EMAIL` default (all four ultimately point at the same conceptual "founder/owner" account but are three structurally different checks against three different identity attributes — username, email-set membership, and a registry flag — see §9 point 3). 2. **Registry check** (`utilities.admin.app_registry.is_app_enabled_for_user`) — same `"redstar"`-username bypass as above. 3. **Model resolution + Django-native permission check** (`admin_instance.has_view_permission(request)`) — this is the actual Django `Group`/`Permission` layer (`policyengine`'s output) being consulted, and per `custom_changelist_view`'s own inline comment, is treated as the "source of truth" for whether the page renders, layered *after* the registry and username checks have already run. `custom_changelist_view` additionally wraps `admin_instance.changelist_view()` in a bare `try/except` that prints a full traceback (`traceback.print_exc()`) before re-raising — a debug-oriented pattern that will dump full stack traces to stdout/stderr in production unless caught by the deployment's own error monitoring. --- ## 5. Security Infrastructure (`security/`) ### 5.1 Secrets encryption (`crypto.py`, `encrypt_env.py`, `decrypt_env.py`, `generate_key.py`, `bootstrap_secrets.py`) Symmetric encryption via `cryptography.fernet.Fernet`. `generate_key.py` is a one-shot key-generation script; `crypto.py` exposes `generate_encryption_key`/`encrypt_value`/`decrypt_value` (the latter raising `ValueError` on `InvalidToken` — wrong key or corrupted ciphertext). `encrypt_env.py`/`decrypt_env.py` (354/353 lines — not fully enumerated line-by-line in this pass, but their role is confirmed by `authx.py`'s docstring and `common.py`'s resolution chain) are the operator-facing tooling for converting a plaintext `~/.dplay/.secrets` file into the Fernet-encrypted `.env` values `get_decrypted_value()` reads at runtime. `bootstrap_secrets.py` is the single function (`bootstrap_secrets()`) that decrypts everything into `os.environ` in place before Celery/Django settings load — used specifically so Celery worker processes (which may not go through the same Django settings-import path as the web process) still get plaintext secrets. ### 5.2 Signup abuse scoring (`security/infra/`) Four independent signal sources, composed by `SignupAbuseService.analyze()` into a single weighted score and an `allow` / `challenge` / `block` / `hard_block` verdict: | Service | Signal | Score weight | |---|---|---| | `GarbageNameService` | Heuristic detection of nonsense/garbage first/last names | +20 | | `DisposableEmailService` | Domain membership in `configs/disposable_domains.txt` (loaded once, process-lifetime cache via a class-level `LOADED` flag) | +30 | | `AbuseIPDBService` | `abuseipdb.com` reputation lookup (`abuseConfidenceScore >= 75`), Redis/Django-cache-backed for 24h per IP to control API spend; **fails open** (returns `not abusive` on any request/parsing exception) | +50 | | Datacenter-IP detection (inline in `SignupAbuseService`, reusing the AbuseIPDB response's `isp`/`usage_type` fields) | Substring match against a hardcoded keyword set (`amazon`, `aws`, `digitalocean`, `ovh`, `hetzner`, `linode`, `vultr`, `google cloud`, `microsoft azure`) | +40 | | Suspicious User-Agent (inline) | Substring match against a hardcoded keyword set (`python-requests`, `curl`, `wget`, `scrapy`, `aiohttp`, `httpclient`, `bot`, `crawler`, `spider`) | +25 | Thresholds: `score <= 30` → `allow`; `<= 60` → `challenge`; `<= 100` → `block`; `> 100` → `hard_block`. This service is defined here but *consumed* by `users`' signup flow (not independently re-verified against `users`' own app doc in this pass — worth cross-checking that doc for exactly where in the signup pipeline `analyze()` is called and what each verdict actually does). ### 5.3 Cloudflare Turnstile (`infra/turnstile_service.py`) Server-side CAPTCHA verification against Cloudflare's `siteverify` endpoint. Notably validates the returned `hostname` against `settings.ALLOWED_TURNSTILE_HOSTNAMES` (a different allowlist per environment — `dev.py`/`staging.py`/`prod.py`) as a **second** check on top of Cloudflare's own `success` flag — this defends against a token solved on/for a different site being replayed here. Fails closed on any exception (returns `success: False`), unlike `AbuseIPDBService`'s fail-open behavior — an intentional (if unstated) asymmetry between the two: a CAPTCHA-verification outage blocks signups, while a reputation-lookup outage doesn't. --- ## 6. Issue Tracker Integration (`integrations/issuetracker/`) This is DjangoPlay's own code — an **adapter/integration layer** wired on top of the separately-installed third-party `genericissuetracker` package (present as its own `INSTALLED_APPS` entry, own models, own `settings.get_setting()` config accessor, own Django signals). `paystream.integrations.issuetracker` is where DjangoPlay-specific identity, RBAC, audit, and UI concerns get bridged onto that generic package without modifying it directly. ### 6.1 Startup (`apps.py::IssueTrackerIntegrationConfig.ready()`) Two things, both wrapped in a blanket `try/except` (comment: *"Avoid breaking app startup if DB isn't ready"*, so this silently no-ops on a fresh/unmigrated DB): 1. Imports `signals.py` (registers the receivers below). 2. Calls `IssueLabelBootstrapService.ensure_labels_exist()` (idempotent `get_or_create` of two canonical labels, `bug-internal`/`bug-public`, process-lifetime-cached via a class-level `_bootstrapped` flag) and registers the tracker's three models (`Issue`, `IssueComment`, `IssueAttachment`) into `audit.lifecycle.registry.AUDIT_TRACKED_MODELS` — i.e., **this is where the issue tracker opts itself into the platform's audit system**, done imperatively at startup rather than via static config (see the `audit` app doc for what `AUDIT_TRACKED_MODELS` membership actually triggers). ### 6.2 Access control (`access_control/permissions.py`) Two classes, both configured into `genericissuetracker`'s settings (`app_settings/genericissuetracker.py`) so the third-party package calls back into DjangoPlay-specific logic: - **`IssueTrackerAccessPermission`** (`GENERIC_ISSUETRACKER_DEFAULT_PERMISSION_CLASSES`) — **all `SAFE_METHODS` (GET/HEAD/OPTIONS) are allowed unconditionally**, including for anonymous users; write access requires either anonymous+`POST`+`ALLOW_ANONYMOUS_REPORTING` (bug reports from the public), or a fully validated identity (`UnifiedLoginService.validate_user` — the same login-policy service `users` uses elsewhere — plus an `IdentityQueryService` snapshot check for `is_active`/`is_verified`) and, if `ISSUE_INTERNAL_ALLOWED_ROLES` is non-empty, role membership. Superusers bypass the login-policy check but **not** the active/verified snapshot check. - **`IssueStateTransitionOwnerPolicy`** (`GENERIC_ISSUETRACKER_TRANSITION_POLICY`) — governs status-change transitions specifically: superuser bypass, then issue-owner bypass (`issue.reporter_user_id == user_id`), then role-based check against the same `ISSUE_INTERNAL_ALLOWED_ROLES` setting (`CEO`, `DJGO`, `SSO`) — reading the role off `user.employmentprofile.role.code`, the same `teamcentral`-owned field `policyengine` reads (see the `policyengine` app doc §5). ### 6.3 Visibility (`services/visibility.py`) `IssueVisibilityService` is the **queryset-level** enforcement layer (separate from the permission classes above, which gate the request but not which *rows* come back): non-privileged identities (not superuser, not in `ISSUE_INTERNAL_ALLOWED_ROLES`) get every issue/comment/attachment queryset filtered to `is_public=True` (or `issue__is_public=True` for the child models). This is what makes the read-endpoints-open-to-everyone design in §6.2 safe in practice — an anonymous or low-privilege caller can hit any read endpoint, but will only ever see rows already marked public. ### 6.4 Cross-subdomain session bridging (`views/ui/session.py`) `IssueSessionCheckView` (GET, polls whether the shared session — valid across the main app and `issues.` subdomain via the shared `SESSION_COOKIE_DOMAIN`, §2.2 — is still authenticated/unexpired) and `IssueReLoginView` (POST, re-authenticates by username-or-email + password without leaving the `issues.` subdomain) are both decorated `@csrf_exempt`. This is a deliberate accommodation for cross-subdomain JS polling/XHR (a same-origin CSRF token from the main app's session wouldn't otherwise be usable for XHR calls originating on `issues.`), but it means `IssueReLoginView` — a credential-checking login endpoint — has **no CSRF protection and no visible rate-limiting/throttle** in the view itself; brute-force protection, if any, would have to come from elsewhere (e.g. Django's `AUTHENTICATION_BACKENDS`/account-lockout logic in `users`, not verified in this pass). Worth confirming with the team. ### 6.5 Attachment downloads (`views/downloads/attachment.py`) `protected_attachment_download` — resolves an `IssueAttachment` by its public `number` (not the PK), re-checks visibility via `IssueVisibilityService.filter_attachment_queryset(...).exists()` (masking a visibility failure as a generic `Http404`, not a `403`, deliberately per the module's own docstring — *"404 masking"*), checks the file still exists on disk, logs the access, then streams it via `FileResponse(..., as_attachment=True)`. ### 6.6 Domain-event bridging (`signals.py`) Five receivers (`issue_created`, `issue_updated`, `issue_deleted`, `issue_status_changed`, `issue_commented` — all Django signals defined and fired by the third-party `genericissuetracker` package itself) each build a metadata dict and call `core.events.helpers.emit_event(..., category=EVENT_CATEGORY_DATA_GOVERNANCE)`. Every receiver is wrapped in its own `try/except Exception: logger.exception(...)` — the module docstring states this is deliberate ("Never raises") so a failure in the platform's own event/audit pipeline can never break the underlying issue-tracker operation the user actually asked for. --- ## 7. Project-Level Services (`services/`) ### 7.1 Runtime bootstrap `runtime_site.py::ensure_runtime_site()` / `socialapps.py::ensure_google_socialapp()` — both process-lifetime-idempotent (module-level `_initialized` flag), both called only from `PaystreamConfig.ready()` (§2.3). `ensure_runtime_site` computes a `domain` string from `SITE_HOST`(:`SITE_PORT`), raises if it would exceed the `Site.domain` field's 100-char DB limit, and **reassigns `settings.SITE_ID` at runtime** to whatever `Site` row it finds/creates — a pattern worth being aware of since `SITE_ID` is normally treated as a static setting. `ensure_google_socialapp` additionally hard-fails (`RuntimeError`) if the resulting `SocialApp.settings` config is ever missing the `email` OAuth scope, since that scope is required for `allauth`'s SSO signup flow elsewhere in the platform. ### 7.2 Template context processors (`services/context_processors/`) Four processors registered in `TEMPLATES["OPTIONS"]["context_processors"]` (`app_settings/templates.py`): - **`app_version`** — injects `APP_VERSION` (from `pyproject.toml`) into every template. - **`site_context`** — the most complex: derives `CURRENT_SUBDOMAIN` from the request's `Host` header, maps it to a `HOME_URL` via a hardcoded `SUBDOMAIN_HOME_MAP` (`issues`→`issues_ui:list`, `docs`→`docs`, else→`console_dashboard`), and spreads every configured subdomain (from `utilities.admin.url_utils.get_all_subdomains()` — cross-referenced in the `utilities` app doc §2.6/§4.6) into `{NAME}_URL` template variables. Also injects `SITE_URL`/`SITE_NAME`/`WEBSITE_URL`/`AI_ENABLED`. - **`global_urls`** — injects the DRF-Spectacular schema URL (`apidocs:schema`, degrading to `""` on `NoReverseMatch`) and `TURNSTILE_SITE_KEY`. - **`admin_urls`** — injects `console_dashboard_url`. --- ## 8. Third-Party Integrations & External Services This section consolidates every external dependency/service `paystream` itself configures or talks to directly (as opposed to being installed purely for another app's use): | Integration | Nature | Where configured/used | |---|---|---| | **`authx-identity`** | **External HTTP microservice**, not a Django app — its own base URL (`AUTHX_BASE_URL`, default `http://localhost:8100`), its own Postgres database (`AUTHX_DATABASE_URL*` — explicitly noted in `authx.py` as "AuthX service only — not Django's DB"), its own JWT signing keypair (RS256, `AUTHX_PUBLIC_KEY`/`AUTHX_PRIVATE_KEY`), issuer/audience claims (`https://auth.djangoplay.org` / `djangoplay`), and a service-to-service token (`AUTHX_SERVICE_TOKEN`) for DjangoPlay→AuthX `/internal/*` calls. This is the "third-party identity" system referenced in the platform overview; `paystream/app_settings/authx.py` only configures the *client* side — the actual integration code (HTTP calls, JWT verification) lives in `users`, not `paystream` (see the `users` app doc for that half). | | **`genericissuetracker`** | Third-party **installed Django app** (pip package), with `paystream.integrations.issuetracker` as DjangoPlay's adapter (§6). Configuration lives in `app_settings/genericissuetracker.py`. | | **Google OAuth (via `allauth`)** | Standard `allauth.socialaccount.providers.google`; credentials resolved per-protocol (`GOOGLE_CLIENT_ID_HTTPS`/`_HTTP`) in `settings/base.py` *before* `allauth.py` is imported (order-sensitive, called out in an inline comment); runtime-bootstrapped as a singleton `SocialApp` row by `services/socialapps.py`. | | **Cloudflare Turnstile** | CAPTCHA, HTTP call to `challenges.cloudflare.com`, three independent key-pairs per environment (§3.2, §5.3). | | **AbuseIPDB** | IP reputation HTTP API, one key (`ABUSEIPDB_API_KEY`), Django-cache-backed (§5.2). | | **Redis** | Two independent roles: (1) Django's `CACHES["default"]` via `django_redis` (`app_settings/cache.py`), (2) Celery broker/result-backend (`app_settings/celery.py`). Same physical Redis config values (`REDIS_HOST`/`PORT`/`PASSWORD`/`SSL`) resolved independently in both files rather than shared from one source — worth checking they're kept in sync if Redis connection settings ever change (§9). | | **PostgreSQL** | Sole database backend (`database.py`) — no SQLite anywhere, including presumably for tests (not independently verified). | | **xAI / OpenAI / Ollama-compatible custom endpoint** | `aicore` app's LLM provider, configured here (`ai.py`) but implemented in `aicore` (see that app's doc). | | **WhiteNoise** | Static file serving middleware (`whitenoise.middleware.WhiteNoiseMiddleware` in `MIDDLEWARE`) — no separate static-file CDN/service configured in this pass. | | **`django-hosts`, `django-mptt`, `django-autocomplete-light` (`dal`/`dal_select2`), `django-simple-history`, `django-compressor`, `drf-spectacular`, `djangorestframework-simplejwt`, `django-extensions`, `django-environ`, `cryptography`, `requests`** | Installed third-party Python packages inferred directly from `INSTALLED_APPS` and import statements across this app — not an exhaustive dependency audit (that would require reading `pyproject.toml`'s full dependency list, not done in this pass). | --- ## 9. Quick Reference — Tracing a Request End to End For a new engineer asking "what actually happens between a browser hitting `https://issues.djangoplay.org/some/path` and a response coming back": 1. **`django-hosts`' `HostsRequestMiddleware`** (first in `MIDDLEWARE`) inspects the `Host` header against `paystream.hosts.host_patterns`, matches `issues`, and routes to `paystream.urlconf.subdomains.issues` for the rest of request handling (not `urlconf/default.py`). 2. Standard middleware runs (security headers, WhiteNoise for static files, session — shared across subdomains via `SESSION_COOKIE_DOMAIN`, CSRF, auth — attaches `request.user` but doesn't require it, `django-simple-history`'s `HistoryRequestMiddleware` for change attribution, request-ID/client-IP/timezone/execution-context middleware from `core`). 3. `urlconf/subdomains/issues.py` resolves the path — e.g. into `paystream.integrations.issuetracker.views.ui` for a UI page, or `paystream.integrations.issuetracker.urls` under `api/` for an API call. 4. If it's an Issue Tracker API/UI view backed by `genericissuetracker`, the third-party package's own view logic runs, but permission-gates through `IssueTrackerAccessPermission`/`IssueStateTransitionOwnerPolicy` (§6.2) — both DjangoPlay-specific classes injected via `GENERIC_ISSUETRACKER_*` settings — and queryset-filters through `IssueVisibilityService` (§6.3). 5. Any domain event the third-party package fires (`issue_created`, etc.) is picked up by `paystream.integrations.issuetracker.signals` and forwarded into `core.events.helpers.emit_event` for the platform's audit pipeline (§6.6) — failures here are swallowed, never surfacing to the original request. 6. `django-hosts`' `HostsResponseMiddleware` (last in `MIDDLEWARE`) does any final host-aware response rewriting before the response is returned. For the equivalent trace through the **default host** and the **custom console/admin UI** specifically, see §4.2 (the three-layer `admin_console_views.py` check sequence) and the `utilities` app doc §2.3/§7 (the registry/permission resolution those views call into).