Author SHA1 Message Date
westfarn eed8852897 Fix logout 403 by setting CSRF trusted origins (#18)
CI / test (pull_request) Successful in 11s
Unit Tests / test (pull_request) Successful in 10s
Django 5 rejects HTTPS POSTs without CSRF_TRUSTED_ORIGINS; derive them from ALLOWED_HOSTS and trust the reverse-proxy TLS headers in prod/beta.
2026-07-10 13:12:17 -05:00
westfarn c97bd16445 Require auth for /preview_email/ (#16) (#17)
Unit Tests / test (push) Successful in 10s
## Summary
- Closes #16
- Confirms `/preview_email/<pk>/` requires an authenticated user (`@login_required` already on the view)
- Adds unit tests for unauthenticated redirect and authenticated preview access

## Test plan
- [x] `python manage.py test public.tests.PreviewEmailAuthTests`
- [ ] Manually hit `/preview_email/1/` logged out → redirect to `/accounts/login/`
- [ ] Log in and hit same URL → email preview renders

Reviewed-on: #17
2026-07-10 10:57:48 -07:00
westfarn 8ccf17655d updateing deploy for westfarn user
Unit Tests / test (push) Successful in 11s
2026-07-08 12:26:07 -05:00
westfarn 04d842d799 Test who runs the job
Unit Tests / test (push) Successful in 10s
2026-07-08 12:21:50 -05:00
westfarn ae0f8a8bc5 Updated deploy to use act_runner keys
Deploy Company Site / test (push) Successful in 10s
Unit Tests / test (push) Successful in 10s
Deploy Company Site / docker (push) Successful in 16s
Deploy Company Site / deploy (push) Failing after 0s
2026-07-08 11:51:33 -05:00
westfarn 16dfb3faae Updated deploy to use act_runner keys
Unit Tests / test (push) Successful in 10s
2026-07-08 11:46:43 -05:00
westfarn a5fa08d4a0 update docker unittests
Unit Tests / test (push) Successful in 10s
2026-07-08 06:12:21 -05:00
westfarn 9a383c0ee9 Update workflow to use server-infra
Unit Tests / test (push) Successful in 10s
2026-07-08 06:05:11 -05:00
westfarn 6f97f6084d Adding images
Unit Tests / test (push) Successful in 11s
2026-07-07 15:28:45 -05:00
westfarn 426cc82f04 Dockerize Django app with dev/beta/prod env config and uv (#6)
Unit Tests / test (push) Successful in 10s
## Summary

- Containerize the Django app with Docker and docker-compose (dev + production)
- Refactor settings into `dev` / `beta` / `prod` environments driven by environment variables
- Connect to PostgreSQL via `DATABASE_URL` or `DB_*` vars
- Migrate package management from pip to uv (`pyproject.toml`, `uv.lock`)
- Split Gitea workflows: PRs run unit tests only; pushes to `master` run tests, Docker validation, and deploy
- Update deploy script to rsync code, preserve server `.env`, validate config, and run Docker compose

Closes #4

## Test plan

- [x] `uv run python manage.py test` passes locally (10/10)
- [x] `DJANGO_ENV=beta` and `DJANGO_ENV=prod` load with correct logging levels
- [x] `scripts/validate-env.sh` rejects missing production variables
- [ ] `docker compose up --build` starts app + Postgres locally
- [ ] Containerized unit tests pass in CI Docker job
- [ ] Server `.env` created from `.env.prod.example` before first production deploy
- [ ] CI workflow runs on this PR (tests only, no deploy)

Reviewed-on: #6
2026-07-07 11:23:50 -07:00
10 changed files with 135 additions and 30 deletions
+2
View File
@@ -4,6 +4,8 @@ DJANGO_ENV=dev
DJANGO_DEBUG=true
DJANGO_SECRET_KEY=change-me-for-local-development
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
# Optional; when unset, http:// origins are derived for local hosts.
# DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost:8000,http://127.0.0.1:8000
# Database (docker-compose sets DATABASE_URL for the web service)
DATABASE_URL=postgres://company_site:company_site@db:5432/company_site
+2
View File
@@ -7,6 +7,8 @@ DJANGO_ENV=prod
DJANGO_DEBUG=false
DJANGO_SECRET_KEY=replace-with-a-long-random-secret
DJANGO_ALLOWED_HOSTS=aimloperations.com,www.aimloperations.com
# Optional override; when unset, https:// origins are derived from DJANGO_ALLOWED_HOSTS.
# DJANGO_CSRF_TRUSTED_ORIGINS=https://aimloperations.com,https://www.aimloperations.com
# Logging (optional override; defaults: dev=DEBUG, beta=INFO, prod=WARNING)
# DJANGO_LOG_LEVEL=WARNING
+5 -25
View File
@@ -1,6 +1,6 @@
name: Deploy Company Site
# Deploy pipeline runs only on pushes to master (never on pull requests).
# Runs after Unit Tests completes on master. Direct pushes only (not PRs).
on:
workflow_run:
workflows: [Unit Tests]
@@ -8,34 +8,14 @@ on:
branches: [master]
jobs:
test:
runs-on: self-hosted
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Install dependencies
run: uv sync --frozen
- name: Run unit tests
env:
DJANGO_ENV: dev
DJANGO_SECRET_KEY: test-secret-key
run: |
cd company_site
uv run python manage.py test
docker:
if: gitea.event.workflow_run.conclusion == 'success' && gitea.event.workflow_run.event == 'push'
runs-on: self-hosted
needs: test
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ gitea.event.workflow_run.head_sha }}
- name: Build Docker image
run: docker compose build
@@ -53,7 +33,7 @@ jobs:
deploy:
if: gitea.event.workflow_run.conclusion == 'success' && gitea.event.workflow_run.event == 'push'
runs-on: self-hosted
needs: [test, docker]
needs: docker
env:
SERVER_INFRA_ROOT: /home/westfarn/Documents/repos/server-infra
steps:
@@ -82,6 +82,34 @@ WEBMCP_ENABLED = env_bool("WEBMCP_ENABLED", False)
allowed_hosts = env_list("DJANGO_ALLOWED_HOSTS", "*")
ALLOWED_HOSTS = allowed_hosts if allowed_hosts else ["*"]
def build_csrf_trusted_origins(
allowed_hosts: list[str], explicit: list[str] | None = None
) -> list[str]:
"""Build CSRF_TRUSTED_ORIGINS for Django 4+ Origin checks on HTTPS POSTs.
Prefer DJANGO_CSRF_TRUSTED_ORIGINS when set. Otherwise derive from ALLOWED_HOSTS:
https for public hosts, http for local loopback hosts.
"""
if explicit:
return explicit
local_hosts = {"localhost", "127.0.0.1", "0.0.0.0"}
origins: list[str] = []
for host in allowed_hosts:
if not host or host == "*" or host.startswith("."):
continue
hostname = host.split(":")[0]
scheme = "http" if hostname in local_hosts else "https"
origins.append(f"{scheme}://{host}")
return origins
CSRF_TRUSTED_ORIGINS = build_csrf_trusted_origins(
ALLOWED_HOSTS,
env_list("DJANGO_CSRF_TRUSTED_ORIGINS"),
)
INSTALLED_APPS = [
"public.apps.PublicConfig",
"financial.apps.FinancialConfig",
@@ -11,4 +11,10 @@ if DEBUG:
warnings.warn("DEBUG is enabled in beta environment.", stacklevel=1)
# Same reverse-proxy assumptions as production when TLS is terminated upstream.
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
USE_X_FORWARDED_HOST = True
SESSION_COOKIE_SECURE = not DEBUG
CSRF_COOKIE_SECURE = not DEBUG
LOGGING = build_logging_config(logging_level_for_env("beta"), "beta")
@@ -9,4 +9,10 @@ TIANJI_ENABLED = env_bool("TIANJI_ENABLED", True) # noqa: F405
if not env("DJANGO_SECRET_KEY"): # noqa: F405
raise ValueError("DJANGO_SECRET_KEY must be set in production.")
# App sits behind a reverse proxy that terminates TLS (docker :8000).
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
USE_X_FORWARDED_HOST = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
LOGGING = build_logging_config(logging_level_for_env("prod"), "prod")
Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

+79 -1
View File
@@ -1,12 +1,90 @@
from unittest.mock import patch
from django.contrib.auth.models import User
from django.test import Client, TestCase, override_settings
from django.urls import reverse
from .models import Contact
from company_site.settings.base import build_csrf_trusted_origins
from .models import Contact, EmailMessage
from .seo import SERVICE_URL_NAMES, get_service_entries
class CsrfTrustedOriginsTests(TestCase):
def test_derives_https_origins_from_public_hosts(self):
origins = build_csrf_trusted_origins(
["aimloperations.com", "www.aimloperations.com"]
)
self.assertEqual(
origins,
[
"https://aimloperations.com",
"https://www.aimloperations.com",
],
)
def test_derives_http_origins_for_local_hosts(self):
origins = build_csrf_trusted_origins(["localhost", "127.0.0.1"])
self.assertEqual(origins, ["http://localhost", "http://127.0.0.1"])
def test_explicit_origins_win(self):
origins = build_csrf_trusted_origins(
["aimloperations.com"],
["https://custom.example"],
)
self.assertEqual(origins, ["https://custom.example"])
class LogoutCsrfTests(TestCase):
def setUp(self):
self.client = Client(enforce_csrf_checks=True)
self.user = User.objects.create_user(username="logout_user", password="pass")
def test_logout_post_with_csrf_succeeds(self):
self.client.login(username="logout_user", password="pass")
self.client.get("/")
csrf = self.client.cookies["csrftoken"].value
response = self.client.post(
reverse("logout"),
{"csrfmiddlewaretoken": csrf},
)
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, "/")
self.assertNotIn("_auth_user_id", self.client.session)
class PreviewEmailAuthTests(TestCase):
def setUp(self):
self.client = Client()
self.user = User.objects.create_user(username="previewer", password="pass")
self.email = EmailMessage.objects.create(
subject="Preview subject",
body="Preview body content",
recipient="recipient@example.com",
)
self.url = reverse("preview_email", kwargs={"pk": self.email.pk})
def test_unauthenticated_user_is_redirected_to_login(self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 302)
self.assertIn("/accounts/login/", response.url)
def test_authenticated_user_can_preview_email(self):
self.client.login(username="previewer", password="pass")
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Preview subject")
self.assertContains(response, "Preview body content")
@override_settings(
DEBUG=True,
EMAIL_BACKEND="django.core.mail.backends.locmem.EmailBackend",
+7 -4
View File
@@ -18,11 +18,14 @@ services:
build: .
ports:
- "8000:8000"
env_file:
- .env
# No required env_file — CI has no .env. Defaults below; for local secrets:
# docker compose --env-file .env up
environment:
DJANGO_ENV: dev
DATABASE_URL: postgres://company_site:company_site@db:5432/company_site
DJANGO_ENV: ${DJANGO_ENV:-dev}
DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:-dev-only-change-me}
DJANGO_DEBUG: ${DJANGO_DEBUG:-true}
DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS:-localhost,127.0.0.1,0.0.0.0}
DATABASE_URL: ${DATABASE_URL:-postgres://company_site:company_site@db:5432/company_site}
depends_on:
db:
condition: service_healthy