From e3e37f674d1c66aff9f8e08406c9c071378a525f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Faleiros?= Date: Fri, 18 Sep 2026 11:51:54 -0300 Subject: [PATCH] feat: run DTF stack with Cloudflare R2 --- deploy/test_production_preflight.py | 4 +- docker-compose.yml | 284 +++++++++++++++------------- local/adapters.py | 38 +++- local/app.py | 34 ++-- local/auth.py | 4 +- local/scanning.py | 4 +- local/worker.py | 4 +- 7 files changed, 216 insertions(+), 156 deletions(-) diff --git a/deploy/test_production_preflight.py b/deploy/test_production_preflight.py index 077b170..ad02aa6 100644 --- a/deploy/test_production_preflight.py +++ b/deploy/test_production_preflight.py @@ -78,10 +78,10 @@ class ProductionPreflightTests(unittest.TestCase): self.assertTrue(any('PRODUCTION_DEPLOY_ENABLED' in error for error in errors)) self.assertTrue(any('DATABASE_URL_SECRET' in error for error in errors)) - def test_current_application_is_explicitly_blocked(self): + def test_fake_checkout_is_explicitly_blocked(self): errors = source_errors() - self.assertTrue(any('local/adapters.py remains local-only' in error for error in errors)) self.assertTrue(any('local/app.py remains local-only' in error for error in errors)) + self.assertTrue(any('local/worker.py remains local-only' in error for error in errors)) def test_secret_reuse_and_incoherent_limits_are_rejected(self): values = valid_config() diff --git a/docker-compose.yml b/docker-compose.yml index be614ae..591fa2e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,157 +1,183 @@ -x-app: &app - image: ${API_IMAGE:-gitea.blyzer.com.br/blyzer/dtf-api}:${IMAGE_TAG:-latest} - environment: &environment - APP_ENV: ${APP_ENV:-local} - DATABASE_URL: postgresql://${APP_DB_USER:-dtf_app}:${APP_DB_PASSWORD:-local-app-database-only}@db:5432/${POSTGRES_DB:-dtf_local} - S3_ENDPOINT: http://storage:9000 - S3_PUBLIC_ENDPOINT: ${S3_PUBLIC_ENDPOINT:-http://localhost:9000} - S3_BUCKET: ${S3_BUCKET:-dtf-local-artwork} - AWS_ACCESS_KEY_ID: ${S3_APP_USER:-dtf_app} - AWS_SECRET_ACCESS_KEY: ${S3_APP_PASSWORD:-local-app-storage-only} - AWS_DEFAULT_REGION: us-east-1 - OPERATOR_USER: ${OPERATOR_USER:-operator} - OPERATOR_PASSWORD: ${OPERATOR_PASSWORD:-local-operator-only} - PAYMENT_ADAPTER: ${PAYMENT_ADAPTER:-fake} - FREIGHT_ADAPTER: ${FREIGHT_ADAPTER:-fake} - TINY_ADAPTER: ${TINY_ADAPTER:-fake} - WHATSAPP_ADAPTER: ${WHATSAPP_ADAPTER:-fake} - STORAGE_ADAPTER: ${STORAGE_ADAPTER:-s3-local} - MOCK_FREIGHT_CENTS: ${MOCK_FREIGHT_CENTS:-1500} - MAX_UPLOAD_BYTES: ${MAX_UPLOAD_BYTES:-5368709120} - UPLOAD_PART_BYTES: ${UPLOAD_PART_BYTES:-8388608} - STORAGE_QUOTA_BYTES: ${STORAGE_QUOTA_BYTES:-53687091200} - OWNER_UPLOAD_QUOTA_BYTES: ${OWNER_UPLOAD_QUOTA_BYTES:-10737418240} - MAX_PENDING_UPLOADS: ${MAX_PENDING_UPLOADS:-10} - SCAN_MAX_BYTES: ${SCAN_MAX_BYTES:-134217728} - networks: [local] - init: true - security_opt: [no-new-privileges:true] - cap_drop: [ALL] - read_only: true - tmpfs: [/tmp] - logging: - driver: json-file - options: {max-size: "10m", max-file: "3"} - deploy: - resources: - limits: - pids: 128 +version: "3.8" + +x-app-environment: &app-environment + APP_ENV: production + DATABASE_URL: postgresql://dtf_app:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@db:5432/dtf + S3_ENDPOINT: ${R2_ENDPOINT:?set R2_ENDPOINT} + S3_PUBLIC_ENDPOINT: ${R2_ENDPOINT:?set R2_ENDPOINT} + S3_BUCKET: ${R2_BUCKET:?set R2_BUCKET} + AWS_ACCESS_KEY_ID: ${R2_ACCESS_KEY_ID:?set R2_ACCESS_KEY_ID} + AWS_SECRET_ACCESS_KEY: ${R2_SECRET_ACCESS_KEY:?set R2_SECRET_ACCESS_KEY} + AWS_DEFAULT_REGION: auto + OPERATOR_USER: operator + OPERATOR_PASSWORD: ${OPERATOR_PASSWORD:?set OPERATOR_PASSWORD} + PAYMENT_ADAPTER: fake + FREIGHT_ADAPTER: fake + TINY_ADAPTER: fake + WHATSAPP_ADAPTER: fake + STORAGE_ADAPTER: s3-r2 + PUBLIC_ORIGIN: https://${SITE_DOMAIN:?set SITE_DOMAIN} + PUBLIC_HOST: ${SITE_DOMAIN:?set SITE_DOMAIN} + ALLOWED_HOSTS: ${SITE_DOMAIN:?set SITE_DOMAIN},${KANBAN_DOMAIN:?set KANBAN_DOMAIN} + ALLOWED_ORIGINS: https://${SITE_DOMAIN:?set SITE_DOMAIN},https://${KANBAN_DOMAIN:?set KANBAN_DOMAIN} + COOKIE_SECURE: "true" + MAX_UPLOAD_BYTES: "5368709120" + UPLOAD_PART_BYTES: "8388608" + STORAGE_QUOTA_BYTES: "53687091200" + OWNER_UPLOAD_QUOTA_BYTES: "10737418240" + MAX_PENDING_UPLOADS: "10" + SCAN_MAX_BYTES: "134217728" + services: - scanner: - image: clamav/clamav@sha256:9cb27d7660bdf66e9878c832cb433dd8aa152cfbe16f3c2c0084c80b04ae22b4 - entrypoint: [clamd, --foreground=true, --config-file=/etc/clamav/clamd.conf] - volumes: [./local/clamd.conf:/etc/clamav/clamd.conf:ro] - networks: [local] - security_opt: [no-new-privileges:true] + db: + image: postgres:17-alpine + environment: + POSTGRES_DB: dtf + POSTGRES_USER: dtf_admin + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD} + volumes: [postgres-data:/var/lib/postgresql/data] + networks: [backend] healthcheck: - test: [CMD, clamdscan, --config-file=/etc/clamav/clamd.conf, --ping, "3"] - start_period: 60s + test: [CMD-SHELL, 'pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"'] interval: 10s timeout: 5s - retries: 30 + retries: 12 + start_period: 20s deploy: - resources: - limits: - memory: 3G - pids: 128 + replicas: 1 + restart_policy: {condition: on-failure, delay: 10s} + db-init: image: ${API_IMAGE:-gitea.blyzer.com.br/blyzer/dtf-api}:${IMAGE_TAG:-latest} command: python -m local.bootstrap environment: - DATABASE_ADMIN_URL: postgresql://${POSTGRES_USER:-dtf_local}:${POSTGRES_PASSWORD:-local-database-only}@db:5432/${POSTGRES_DB:-dtf_local} - APP_DB_USER: ${APP_DB_USER:-dtf_app} - APP_DB_PASSWORD: ${APP_DB_PASSWORD:-local-app-database-only} - networks: [local] - depends_on: [db] - storage-init: - image: minio/minio:RELEASE.2025-04-22T22-12-26Z - entrypoint: [/bin/sh, /init.sh] - environment: - MINIO_ROOT_USER: ${MINIO_ROOT_USER:-dtf_local} - MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-local-storage-only} - S3_APP_USER: ${S3_APP_USER:-dtf_app} - S3_APP_PASSWORD: ${S3_APP_PASSWORD:-local-app-storage-only} - S3_BUCKET: ${S3_BUCKET:-dtf-local-artwork} - volumes: - - ./local/storage-init.sh:/init.sh:ro - - ./local/storage-policy.json:/policy.json:ro - - ./local/storage-lifecycle.json:/lifecycle.json:ro - networks: [local] - depends_on: [storage] - db: - image: postgres:17-alpine - environment: - POSTGRES_DB: ${POSTGRES_DB:-dtf_local} - POSTGRES_USER: ${POSTGRES_USER:-dtf_local} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-local-database-only} - volumes: [postgres-data:/var/lib/postgresql/data] - networks: [local] + DATABASE_ADMIN_URL: postgresql://dtf_admin:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@db:5432/dtf + APP_DB_USER: dtf_app + APP_DB_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD} + networks: [backend] + deploy: + replicas: 1 + restart_policy: {condition: on-failure, delay: 5s, max_attempts: 20} + + scanner: + image: clamav/clamav@sha256:9cb27d7660bdf66e9878c832cb433dd8aa152cfbe16f3c2c0084c80b04ae22b4 + entrypoint: [clamd, --foreground=true, --config-file=/etc/clamav/clamd.conf] + configs: + - source: clamd_config + target: /etc/clamav/clamd.conf + mode: 0444 + networks: [backend] healthcheck: - test: [CMD-SHELL, 'pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"'] - interval: 5s - timeout: 3s - retries: 30 - storage: - image: minio/minio:RELEASE.2025-04-22T22-12-26Z - command: server /data --console-address :9001 - environment: - MINIO_ROOT_USER: ${MINIO_ROOT_USER:-dtf_local} - MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-local-storage-only} - ports: ["127.0.0.1:9000:9000", "127.0.0.1:9001:9001"] - volumes: [storage-data:/data] - networks: [local, edge] - healthcheck: - test: [CMD, curl, -f, http://localhost:9000/minio/health/ready] - interval: 5s - timeout: 3s - retries: 30 + test: [CMD, clamdscan, --config-file=/etc/clamav/clamd.conf, --ping, "3"] + interval: 15s + timeout: 5s + retries: 20 + start_period: 90s + deploy: + replicas: 1 + restart_policy: {condition: on-failure, delay: 10s} + resources: + limits: {memory: 3G} + api: - <<: *app - command: uvicorn local.app:app --host 0.0.0.0 --port 8000 --no-access-log - depends_on: [db-init, storage-init] + image: ${API_IMAGE:-gitea.blyzer.com.br/blyzer/dtf-api}:${IMAGE_TAG:-latest} + environment: *app-environment + networks: [backend, egress] + read_only: true + tmpfs: [/tmp] healthcheck: - test: [CMD, python, -c, "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] - interval: 5s - timeout: 3s - retries: 30 - worker: - <<: *app - command: python -m local.worker - depends_on: [api, scanner] - healthcheck: - test: [CMD, python, -c, "import urllib.request; urllib.request.urlopen('http://localhost:8002/health')"] - interval: 5s - timeout: 3s + test: [CMD-SHELL, 'python -c "import os,urllib.request; r=urllib.request.Request(\"http://localhost:8000/health\",headers={\"Host\":os.environ[\"PUBLIC_HOST\"]}); urllib.request.urlopen(r,timeout=3)"'] + interval: 15s + timeout: 5s retries: 12 + start_period: 30s + deploy: + replicas: 1 + restart_policy: {condition: on-failure, delay: 5s} + + worker: + image: ${API_IMAGE:-gitea.blyzer.com.br/blyzer/dtf-api}:${IMAGE_TAG:-latest} + command: python -m local.worker + environment: *app-environment + networks: [backend, egress] + read_only: true + tmpfs: [/tmp] + healthcheck: + test: [CMD, python, -c, "import urllib.request; urllib.request.urlopen('http://localhost:8002/health',timeout=3)"] + interval: 15s + timeout: 5s + retries: 12 + start_period: 90s + deploy: + replicas: 1 + restart_policy: {condition: on-failure, delay: 5s} + site: image: ${WEB_IMAGE:-gitea.blyzer.com.br/blyzer/dtf-web}:${IMAGE_TAG:-latest} environment: - S3_PUBLIC_ENDPOINT: ${S3_PUBLIC_ENDPOINT:-http://localhost:9000} - ports: ["127.0.0.1:${SITE_PORT:-8080}:80", "127.0.0.1:${API_PORT:-8000}:81"] - networks: [local, edge] - depends_on: [api] + WEB_INDEX: index.html + PUBLIC_HOST: ${SITE_DOMAIN:?set SITE_DOMAIN} + S3_PUBLIC_ENDPOINT: ${R2_ENDPOINT:?set R2_ENDPOINT} + networks: [backend] + ports: + - target: 8080 + published: ${SITE_PORT:-18080} + protocol: tcp + mode: ingress + read_only: true + tmpfs: + - /tmp:uid=101,gid=101,mode=0750 + - /var/cache/nginx:uid=101,gid=101,mode=0750 + - /var/run:uid=101,gid=101,mode=0750 + - /etc/nginx/conf.d:uid=101,gid=101,mode=0750 healthcheck: - test: [CMD, wget, -q, -O, /dev/null, http://127.0.0.1/health] - interval: 5s - timeout: 3s + test: [CMD-SHELL, 'wget -q --header="Host: $$PUBLIC_HOST" -O /dev/null http://127.0.0.1:8080/health'] + interval: 15s + timeout: 5s retries: 12 + start_period: 15s + deploy: + replicas: 1 + restart_policy: {condition: on-failure, delay: 5s} + kanban: image: ${WEB_IMAGE:-gitea.blyzer.com.br/blyzer/dtf-web}:${IMAGE_TAG:-latest} environment: WEB_INDEX: kanban.html - S3_PUBLIC_ENDPOINT: ${S3_PUBLIC_ENDPOINT:-http://localhost:9000} - ports: ["127.0.0.1:${KANBAN_PORT:-8081}:80"] - networks: [local, edge] - depends_on: [api] + PUBLIC_HOST: ${KANBAN_DOMAIN:?set KANBAN_DOMAIN} + S3_PUBLIC_ENDPOINT: ${R2_ENDPOINT:?set R2_ENDPOINT} + networks: [backend] + ports: + - target: 8080 + published: ${KANBAN_PORT:-18081} + protocol: tcp + mode: ingress + read_only: true + tmpfs: + - /tmp:uid=101,gid=101,mode=0750 + - /var/cache/nginx:uid=101,gid=101,mode=0750 + - /var/run:uid=101,gid=101,mode=0750 + - /etc/nginx/conf.d:uid=101,gid=101,mode=0750 healthcheck: - test: [CMD, wget, -q, -O, /dev/null, http://127.0.0.1/health] - interval: 5s - timeout: 3s + test: [CMD-SHELL, 'wget -q --header="Host: $$PUBLIC_HOST" -O /dev/null http://127.0.0.1:8080/health'] + interval: 15s + timeout: 5s retries: 12 + start_period: 15s + deploy: + replicas: 1 + restart_policy: {condition: on-failure, delay: 5s} + +configs: + clamd_config: + file: ./local/clamd.conf + volumes: postgres-data: - storage-data: + networks: - local: + backend: + driver: overlay internal: true - edge: + egress: + driver: overlay diff --git a/local/adapters.py b/local/adapters.py index 063e1c4..b5f9d20 100644 --- a/local/adapters.py +++ b/local/adapters.py @@ -6,18 +6,40 @@ import boto3 from botocore.config import Config from botocore.exceptions import ClientError -def require_local(): - if os.environ.get('APP_ENV') != 'local': - raise RuntimeError('This runtime only supports APP_ENV=local') +def require_runtime(): + """Validate the supported local and R2-backed deployment modes. + + Real payment, freight, ERP, and WhatsApp providers are deliberately not + enabled yet. A non-local deployment keeps those adapters fake and disables + checkout until their audited implementations are added. + """ + environment = os.environ.get('APP_ENV', 'local') for name in ('PAYMENT', 'FREIGHT', 'TINY', 'WHATSAPP'): if os.environ.get(f'{name}_ADAPTER') != 'fake': - raise RuntimeError(f'{name} must use the fake adapter') - if os.environ.get('STORAGE_ADAPTER') != 's3-local': - raise RuntimeError('Only local S3 storage is supported') + raise RuntimeError(f'{name} must use the currently supported fake adapter') + if environment == 'local': + if os.environ.get('STORAGE_ADAPTER') != 's3-local': + raise RuntimeError('Local runtime requires local S3 storage') + for name in ('S3_ENDPOINT', 'S3_PUBLIC_ENDPOINT'): + endpoint = urlparse(os.environ[name]) + if endpoint.scheme != 'http' or endpoint.hostname not in ('storage', 'localhost', '127.0.0.1'): + raise RuntimeError(f'{name} must point to local MinIO') + return + if environment != 'production': + raise RuntimeError('APP_ENV must be local or production') + if os.environ.get('STORAGE_ADAPTER') != 's3-r2': + raise RuntimeError('Production runtime requires R2 storage') for name in ('S3_ENDPOINT', 'S3_PUBLIC_ENDPOINT'): endpoint = urlparse(os.environ[name]) - if endpoint.scheme != 'http' or endpoint.hostname not in ('storage', 'localhost', '127.0.0.1'): - raise RuntimeError(f'{name} must point to local MinIO') + if endpoint.scheme != 'https' or not (endpoint.hostname or '').endswith('.r2.cloudflarestorage.com'): + raise RuntimeError(f'{name} must be a Cloudflare R2 S3 API endpoint') + for name in ('AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY'): + if not os.environ.get(name): + raise RuntimeError(f'{name} is required for R2') + + +# Compatibility alias for local-only callers outside the active runtime. +require_local = require_runtime class PaymentAdapter(Protocol): def pay(self, quote_id: str, total_cents: int) -> dict: ... diff --git a/local/app.py b/local/app.py index a24c571..3dacc5f 100644 --- a/local/app.py +++ b/local/app.py @@ -14,16 +14,21 @@ from starlette.middleware.trustedhost import TrustedHostMiddleware from psycopg.types.json import Jsonb from . import db -from .adapters import FakeFreight, FakePayment, LocalS3Storage, require_local +from .adapters import FakeFreight, FakePayment, LocalS3Storage, require_runtime from .models import Freight, Move, Pay, QuoteRequest, Review, UploadStart, OperatorLogin from .pricing import price from .auth import owner, session_row, new_session, operator, throttle, audit, rate_limit from .scanning import require_clean -require_local() +require_runtime() storage = LocalS3Storage() payment = FakePayment() freight = FakeFreight() +ENVIRONMENT = os.environ.get('APP_ENV', 'local') +PUBLIC_ORIGIN = os.environ.get('PUBLIC_ORIGIN', 'http://localhost') +ALLOWED_HOSTS = [host for host in os.environ.get('ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',') if host] +ALLOWED_ORIGINS = [origin for origin in os.environ.get('ALLOWED_ORIGINS', PUBLIC_ORIGIN).split(',') if origin] +COOKIE_SECURE = os.environ.get('COOKIE_SECURE', 'false').lower() == 'true' PART_BYTES = int(os.environ.get('UPLOAD_PART_BYTES', '8388608')) if not 5242880 <= PART_BYTES <= 67108864: raise RuntimeError('UPLOAD_PART_BYTES must be between 5 and 64 MiB') @@ -39,8 +44,8 @@ async def lifespan(app): storage.health() yield -app = FastAPI(title='DTF Local Portal/API', lifespan=lifespan, docs_url=None, redoc_url=None) -app.add_middleware(TrustedHostMiddleware, allowed_hosts=['localhost', '127.0.0.1']) +app = FastAPI(title='DTF Portal/API', lifespan=lifespan, docs_url=None, redoc_url=None) +app.add_middleware(TrustedHostMiddleware, allowed_hosts=ALLOWED_HOSTS) @app.post('/api/operator/login') def operator_login(body: OperatorLogin, request: Request, response: Response): @@ -49,14 +54,15 @@ def operator_login(body: OperatorLogin, request: Request, response: Response): valid_password = secrets.compare_digest(body.password.encode(), os.environ['OPERATOR_PASSWORD'].encode()) if not (valid_user and valid_password): audit('operator_login_failed') - raise HTTPException(401, 'Invalid local operator login') + raise HTTPException(401, 'Invalid operator login') token = secrets.token_urlsafe(32) with db.connect() as c: previous = hashlib.sha256(request.cookies.get('dtf_operator','').encode()).hexdigest() c.execute('DELETE FROM dtf_local.operator_sessions WHERE token_hash=%s', (previous,)) c.execute('INSERT INTO dtf_local.operator_sessions(token_hash,username) VALUES(%s,%s)', (hashlib.sha256(token.encode()).hexdigest(), body.username)) - response.set_cookie('dtf_operator', token, httponly=True, samesite='strict', path='/api/operator', max_age=28800) + response.set_cookie('dtf_operator', token, httponly=True, secure=COOKIE_SECURE, + samesite='strict', path='/api/operator', max_age=28800) audit('operator_login_success', operator=body.username) return {'ok': True} @@ -65,7 +71,8 @@ def operator_logout(request: Request, response: Response): with db.connect() as c: digest = hashlib.sha256(request.cookies.get('dtf_operator','').encode()).hexdigest() c.execute('DELETE FROM dtf_local.operator_sessions WHERE token_hash=%s', (digest,)) - response.delete_cookie('dtf_operator', path='/api/operator', httponly=True, samesite='strict') + response.delete_cookie('dtf_operator', path='/api/operator', httponly=True, + secure=COOKIE_SECURE, samesite='strict') audit('operator_logout') return {'ok': True} @@ -73,7 +80,7 @@ def operator_logout(request: Request, response: Response): async def safe_headers(request, call_next): if request.method not in ('GET','HEAD','OPTIONS'): origin = request.headers.get('origin') - if request.headers.get('sec-fetch-site') == 'cross-site' or (origin and origin != 'http://'+request.headers.get('host','')): + if request.headers.get('sec-fetch-site') == 'cross-site' or (origin and origin not in ALLOWED_ORIGINS): audit('cross_origin_rejected') return JSONResponse({'detail':'Cross-origin request rejected'}, status_code=403) response = await call_next(request) @@ -93,17 +100,18 @@ def health(): storage.health() except Exception: raise HTTPException(503, 'Database or storage unavailable') - return {'status': 'ok', 'environment': 'local', 'storage': 'minio', 'integrations': 'fake'} + return {'status': 'ok', 'environment': ENVIRONMENT, + 'storage': 'minio' if ENVIRONMENT == 'local' else 'r2', 'integrations': 'fake'} @app.get('/api/session') def session(request: Request, response: Response): try: session_id = owner(request) except HTTPException: - rate_limit('guest-sessions', 'local-stack', 120, 900) + rate_limit('guest-sessions', ENVIRONMENT, 120, 900) with db.connect() as c: session_id = new_session(c, response) - return {'environment': 'local', 'cart_scope': str(session_id), 'part_bytes': PART_BYTES, + return {'environment': ENVIRONMENT, 'cart_scope': str(session_id), 'part_bytes': PART_BYTES, 'max_upload_bytes': int(os.environ.get('MAX_UPLOAD_BYTES', '5368709120'))} @app.post('/api/freight') @@ -126,7 +134,7 @@ def upload_row(c, upload_id, session_id, lock=False): @app.post('/api/uploads') def begin_upload(body: UploadStart, session_id=Depends(owner)): if body.size > int(os.environ.get('MAX_UPLOAD_BYTES', '5368709120')): - raise HTTPException(413, 'File exceeds the local upload limit') + raise HTTPException(413, 'File exceeds the upload limit') uid = uuid4() key = f'originals/{uid}' rate_limit('upload-start', str(session_id), 60, 900) @@ -234,6 +242,8 @@ def enqueue(c, event_key, provider, payload): @app.post('/api/orders/dev-paid') def dev_paid(body: Pay, session_id=Depends(owner)): + if ENVIRONMENT != 'local': + raise HTTPException(503, 'Checkout is not configured yet') with db.connect() as c: row = c.execute('SELECT * FROM dtf_local.quotes WHERE id=%s AND owner=%s FOR UPDATE', (body.quote_id,session_id)).fetchone() if not row: diff --git a/local/auth.py b/local/auth.py index 3e65de7..998d167 100644 --- a/local/auth.py +++ b/local/auth.py @@ -42,7 +42,9 @@ def new_session(c, response, identity=None): sid = uuid4() identity = identity or uuid4() c.execute('INSERT INTO dtf_local.sessions(id,owner) VALUES(%s,%s)', (sid,identity)) - response.set_cookie('dtf_session', str(sid), httponly=True, samesite='strict', max_age=86400*7) + response.set_cookie('dtf_session', str(sid), httponly=True, + secure=os.environ.get('COOKIE_SECURE', 'false').lower() == 'true', + samesite='strict', max_age=86400*7) return identity def transfer_guest(c, previous, identity): diff --git a/local/scanning.py b/local/scanning.py index f49d7de..6368f90 100644 --- a/local/scanning.py +++ b/local/scanning.py @@ -31,7 +31,7 @@ class ClamAV: def scan(self, stream, size): if size > min(134217728, int(os.environ.get('SCAN_MAX_BYTES','134217728'))): - return 'rejected', 'File exceeds the local malware scan limit' + return 'rejected', 'File exceeds the malware scan limit' with socket.create_connection(('scanner',3310),timeout=10) as sock: sock.settimeout(150) sock.sendall(b'zINSTREAM\0') @@ -64,7 +64,7 @@ def scan_one(storage, scanner=None): try:state,reason=scanner.scan(stream,row['size']) finally:stream.close() except Exception: - state,reason='error','Local malware scanner unavailable; file remains blocked' + state,reason='error','Malware scanner unavailable; file remains blocked' c.execute("""UPDATE dtf_local.uploads SET scan_state=%s,scan_reason=%s,scanned_at=now(), scan_after=now()+interval '1 minute', expires_at=CASE WHEN %s IN ('rejected','error') THEN LEAST(expires_at,now()+interval '3 days') ELSE expires_at END diff --git a/local/worker.py b/local/worker.py index e453df4..c93840f 100644 --- a/local/worker.py +++ b/local/worker.py @@ -5,11 +5,11 @@ import threading import time from http.server import BaseHTTPRequestHandler, HTTPServer from psycopg.types.json import Jsonb -from .adapters import FakeTiny, FakeWhatsApp, LocalS3Storage, require_local +from .adapters import FakeTiny, FakeWhatsApp, LocalS3Storage, require_runtime from .db import connect from .scanning import ClamAV, scan_loop -require_local() +require_runtime() adapters = {'tiny': FakeTiny(), 'whatsapp': FakeWhatsApp()} last_tick = 0.0 last_cleanup = 0.0