fix: A29 + A42(partiel) — publish partagé, fuite password_hash, data_dir (v7.5.0)
- A29 — `app/services/publish.py` : slugify titré unique (fallback aléatoire),
404 si la page n'existe pas, événements centralisés. Les 3 paires
publish/unpublish déléguent (sharing = front, board, v2) :
· board : mise à jour aveugle → 404 + contrôle de session ajouté
· board : perd `share_mode='anyone'` en bonus, v2 : perd `is_shared=1` —
le share dialog reste l'unique propriétaire de ces drapeaux
· v2 : slug fourni conservé, slug vidé aussi à la dépublication (avant : laissé)
· `/users/me` ×2 et listings collections ×3 = contrats versionnés distincts,
décision documentée (on garde)
- Byproduct sécurité — `GET /api/users/me` (v1) et le contexte de `/accounts`
faisaient `SELECT *` sur users → password_hash / login_attempts / locked_until
exposés → colonnes whitelistées (liste v2)
- A42 (partiel) — 9 copies de `Path(os.environ.get("FLOWDECK_DATA_DIR", "/data"))`
→ `settings.data_dir` (property : lecture à chaque accès, les tests
monkeypatchent l'env) ; cache Gitea : évacuation des entrées expirées à chaque
écriture. Reste : client httpx partagé (52 créations, cache par event loop)
tests : test_publish_service_shared_and_safe, test_users_me_no_secret_columns,
test_gitea_cache_evicts_expired
suite **1034/1034** · `ruff check app tests` OK · OpenAPI 511 chemins / 7.5.0
docs (ROADMAP/CHANGELOG/WORKLOAD/VERSION) à jour
This commit is contained in:
+6
-17
@@ -31,6 +31,7 @@ from app.services.api_v2_helpers import ( # noqa: F401 — require_scope est ut
|
||||
validate_scopes_input,
|
||||
)
|
||||
from app.services.automations import fire_event as _fire_event
|
||||
from app.services.publish import fire_published, fire_unpublished, publish, unpublish
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/v2", tags=["api-v2"])
|
||||
@@ -1633,30 +1634,18 @@ async def publish_page_v2(page_id: int, request: Request, authorization: str | N
|
||||
body = await request.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
slug = (body.get("slug") or body.get("publish_slug") or f"p-{page_id}-{secrets.token_urlsafe(6)}").strip()
|
||||
with get_conn() as conn:
|
||||
if not conn.execute("SELECT id FROM pages WHERE id=?", (page_id,)).fetchone():
|
||||
raise HTTPException(404, "Page not found")
|
||||
conn.execute("UPDATE pages SET is_published=1, publish_slug=?, is_shared=1 WHERE id=?", (slug, page_id))
|
||||
conn.commit()
|
||||
slug_in = (body.get("slug") or body.get("publish_slug") or "").strip() or None
|
||||
slug, _title = publish(page_id, explicit_slug=slug_in)
|
||||
audit_log(user, "page.publish", "page", page_id, slug, request)
|
||||
try:
|
||||
await _fire_event("page.published", {"page_id": page_id, "slug": slug})
|
||||
except Exception:
|
||||
logger.exception("publish_page_v2")
|
||||
await fire_published(page_id, slug)
|
||||
return {"page_id": page_id, "slug": slug, "url": f"/p/{slug}", "status": "published"}
|
||||
|
||||
@router.delete("/pages/{page_id}/publish")
|
||||
async def unpublish_page_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)):
|
||||
user = require_scope("write")(request, authorization)
|
||||
_v2_rate_check(request, user)
|
||||
with get_conn() as conn:
|
||||
conn.execute("UPDATE pages SET is_published=0 WHERE id=?", (page_id,))
|
||||
conn.commit()
|
||||
try:
|
||||
await _fire_event("page.unpublished", {"page_id": page_id})
|
||||
except Exception:
|
||||
logger.exception("unpublish_page_v2")
|
||||
unpublish(page_id)
|
||||
await fire_unpublished(page_id)
|
||||
return {"page_id": page_id, "status": "unpublished"}
|
||||
|
||||
# ── History ───────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user