All checks were successful
Build and deploy / Validate source (push) Successful in 1m25s
Build and deploy / Integration suite on a real stack (push) Successful in 3m16s
Build and deploy / Secret scan and release gate (push) Successful in 13s
Build and deploy / Publish images (push) Successful in 1m29s
The customer's browser sends the small picture it already makes of each file (at most 300 KB, WebP/JPEG/PNG read from the bytes, own uploads only); it goes when the file's bytes go. Board cards, quote rows, the order panel and the quote detail show it, with the layout drawing as a second view and as the fallback for files without a picture. Cotações gets a customer filter (e-mail, CNPJ, WhatsApp) and product, layout and resolution-warning chips, with the page bar always shown. The board adds Entrega, Retirada and stalled-order chips. The top search now covers orders in any stage and unpaid quotes; CNPJ and phone match by digits only when nothing but digits and punctuation was typed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
50 lines
2.1 KiB
Python
50 lines
2.1 KiB
Python
"""The DTF Portal/API service: assembly only.
|
|
|
|
Configuration and shared helpers are in local.runtime; every route lives in a
|
|
router under local.api. This module creates the application, applies the
|
|
cross-cutting middleware, and includes them.
|
|
"""
|
|
from contextlib import asynccontextmanager
|
|
|
|
from fastapi import FastAPI
|
|
from fastapi.responses import JSONResponse
|
|
from starlette.middleware.trustedhost import TrustedHostMiddleware
|
|
|
|
from .core import db
|
|
from .core.auth import audit, client_ip
|
|
from .runtime import ALLOWED_HOSTS, ALLOWED_ORIGINS, storage
|
|
from .api import artwork, customer, health, operator, orders, payments, quotes, uploads
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app):
|
|
with db.connect() as c:
|
|
c.execute('SELECT 1 FROM dtf_local.operator_sessions LIMIT 1')
|
|
storage.health()
|
|
yield
|
|
|
|
|
|
app = FastAPI(title='DTF Portal/API', lifespan=lifespan, docs_url=None, redoc_url=None)
|
|
app.add_middleware(TrustedHostMiddleware, allowed_hosts=ALLOWED_HOSTS)
|
|
|
|
@app.middleware('http')
|
|
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 not in ALLOWED_ORIGINS):
|
|
audit('cross_origin_rejected', ip=client_ip(request))
|
|
return JSONResponse({'detail':'Cross-origin request rejected'}, status_code=403)
|
|
response = await call_next(request)
|
|
if response.status_code in (401,403,429) or response.status_code>=500:
|
|
audit('http_security_event', method=request.method, status=response.status_code, ip=client_ip(request))
|
|
# Nothing is cached unless a route says so (the Kanban's artwork pictures).
|
|
response.headers.setdefault('Cache-Control', 'no-store')
|
|
response.headers['X-Content-Type-Options'] = 'nosniff'
|
|
response.headers['Referrer-Policy'] = 'no-referrer'
|
|
return response
|
|
|
|
|
|
# Order is not significant: no two routers declare the same path.
|
|
for module in (health, uploads, quotes, orders, payments, operator, customer, artwork):
|
|
app.include_router(module.router)
|