fix: A30 + A37 + A39 + A40 + A41 — fin du P2/P3 XS/S (v7.4.0)
- A30 — `require_scope()` câblé : 69 sites stricts de api_v2.py passent par la factory (Bearer + scope en 1 appel, contrôle manuel supprimé) ; sémantique alignée sur celle des handlers (pas de default "read" → 0 changement de comportement) ; 12 top-level morts supprimés (0 ref app ET tests) : unsync_block, find_referring, _b64url, strip_markdown, format_number, get_auto_property_value, get_next_unique_id, local_date_in_tz, verify_device_token, _get_dynamic_groups, _require_user_gitea, validate_upload_request - A37 — CORS sans `*` : origines = app_base_url + allow_origin_regex (localhost/dev, origines d'extension pour le Web Clipper), méthodes et entêtes minutées, allow_credentials explicite + test test_cors_no_star - A39 — htmx : décision « rien » documentée (32 attributs hx-* réels sur 6 templates, conversion = refonte du view-switching sans test E2E) - A40 — version d'assets à source unique : ENV.globals["asset_version"] lu au boot depuis le fichier VERSION ; littéraux `?v=` de base.html éliminés ; test test_asset_version_single_source - A41 — app.css : 91 règles mortes purgées (-10 274 octets, 121 618 → 111 344), scan templates/JS/CSS/Python à 0 référence suite **1031/1031** · `ruff check app tests` OK · OpenAPI 511 chemins / 7.4.0 docs (ROADMAP/CHANGELOG/WORKLOAD/VERSION) à jour
This commit is contained in:
@@ -214,9 +214,13 @@ def get_bearer_user(request: Request, authorization: str | None = Header(default
|
||||
return user
|
||||
|
||||
def require_scope(required: str):
|
||||
"""A30 : la factory de scopes, AVOIR utilisée — les handlers faisaient
|
||||
`has_scope(...)` à la main (69 sites dans api_v2.py)."""
|
||||
def _dep(request: Request, authorization: str | None = Header(default=None)) -> dict:
|
||||
user = get_bearer_user(request, authorization)
|
||||
scopes = user.get("_token_scopes") or "read"
|
||||
# Pas de default "read" : identique au contrôle manuel des handlers
|
||||
# (un jeton sans scope est refusé, quel que soit le scope demandé).
|
||||
scopes = user.get("_token_scopes")
|
||||
if not has_scope(scopes, required):
|
||||
raise HTTPException(status_code=403, detail=f"Insufficient scope. Required: {required}, token scopes: {scopes}")
|
||||
return user
|
||||
|
||||
@@ -98,9 +98,3 @@ def coerce_tags(value: Any) -> list[str]:
|
||||
return [str(value)]
|
||||
|
||||
|
||||
def strip_markdown(text: str) -> str:
|
||||
text = re.sub(r"`{1,3}([^`]*)`{1,3}", r"\1", text)
|
||||
text = re.sub(r"!\[[^\]]*\]\([^)]*\)", "", text)
|
||||
text = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", text)
|
||||
text = re.sub(r"[*_~#>]+", "", text)
|
||||
return text.strip()
|
||||
|
||||
@@ -247,13 +247,6 @@ def user_ref(user: dict | None) -> dict | None:
|
||||
}
|
||||
|
||||
|
||||
def get_auto_property_value(prop_type: str, user: dict | None = None) -> Any:
|
||||
"""Compute the value of an auto-property."""
|
||||
if prop_type == "created_time" or prop_type == "last_edited_time":
|
||||
return datetime.now(UTC).isoformat()
|
||||
if prop_type == "created_by" or prop_type == "last_edited_by":
|
||||
return user_ref(user)
|
||||
return None
|
||||
|
||||
|
||||
def apply_auto_properties(
|
||||
@@ -291,28 +284,5 @@ def apply_auto_properties(
|
||||
return values
|
||||
|
||||
|
||||
def get_next_unique_id(collection_id: int, conn) -> int:
|
||||
"""Get the next unique_id for a collection (max + 1)."""
|
||||
row = conn.execute(
|
||||
"""SELECT COALESCE(MAX(CAST(json_extract(property_values_json, '$.unique_id') AS INTEGER)), 0) + 1
|
||||
FROM collection_pages WHERE collection_id=?""",
|
||||
(collection_id,),
|
||||
).fetchone()
|
||||
return row[0] if row else 1
|
||||
|
||||
|
||||
def format_number(value: float, fmt: str = "number") -> str:
|
||||
"""Format a number value for display."""
|
||||
if value is None:
|
||||
return ""
|
||||
if fmt == "percent":
|
||||
return f"{value}%"
|
||||
elif fmt == "dollar":
|
||||
return f"${value:,.2f}"
|
||||
elif fmt == "euro":
|
||||
return f"€{value:,.2f}"
|
||||
elif fmt == "pound":
|
||||
return f"£{value:,.2f}"
|
||||
elif fmt == "yen":
|
||||
return f"¥{value:,.0f}"
|
||||
return str(value)
|
||||
|
||||
@@ -196,9 +196,6 @@ def now_in_tz(tz_name: str | None = None) -> dt.datetime:
|
||||
return dt.datetime.now(dt.UTC)
|
||||
|
||||
|
||||
def local_date_in_tz(tz_name: str | None = None) -> dt.date:
|
||||
"""'Today' from the point of view of ``tz_name`` (fallback UTC)."""
|
||||
return now_in_tz(tz_name).date()
|
||||
|
||||
|
||||
def _zone_dt(d: dt.date, time_str: str, tz_name: str | None):
|
||||
|
||||
@@ -306,14 +306,3 @@ def mark_synced_block_deleted(synced_id: int, page_ids: list[int]) -> None:
|
||||
|
||||
# ── Unsync: convert synced block to independent copy ──────────────
|
||||
|
||||
def unsync_block(page_id: int, synced_block_id: int) -> list[dict] | None:
|
||||
"""Remove a page's sync reference and return the current content
|
||||
so the caller can turn it into an independent block."""
|
||||
sb = get_synced_block(synced_block_id)
|
||||
if not sb:
|
||||
return None
|
||||
remove_page_synced(page_id, synced_block_id)
|
||||
try:
|
||||
return json.loads(sb["content"])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return None
|
||||
|
||||
@@ -384,19 +384,6 @@ def register_device(user_id: int, device_id: str, device_name: str = "", extensi
|
||||
return {"id": cur.lastrowid, "device_id": device_id, "token": token, "existing": False}
|
||||
|
||||
|
||||
def verify_device_token(device_id: str, token: str) -> dict | None:
|
||||
"""Verify a device token, returns device row or None."""
|
||||
thash = _hash_token(token)
|
||||
with get_conn() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT * FROM extension_devices WHERE device_id=? AND token_hash=? AND revoked=0",
|
||||
(device_id, thash),
|
||||
).fetchone()
|
||||
if row:
|
||||
conn.execute("UPDATE extension_devices SET last_used_at=CURRENT_TIMESTAMP WHERE id=?", (row["id"],))
|
||||
conn.commit()
|
||||
return dict(row)
|
||||
return None
|
||||
|
||||
|
||||
def log_clip(user_id: int, device_id: str, clip_type: str, source_url: str, target_page_id: int, workspace_id: int, title: str):
|
||||
|
||||
@@ -83,8 +83,3 @@ def resolve_tokens_html(content: str, titles: dict[str, str]) -> str:
|
||||
return s
|
||||
|
||||
|
||||
def find_referring(content: str, page_id: int) -> bool:
|
||||
"""True when the content references ``page_id`` (anchor or wiki token)."""
|
||||
if not content:
|
||||
return False
|
||||
return (f"/pages/{page_id}" in content) or (f"[[fdpage:{page_id}]]" in content)
|
||||
|
||||
Reference in New Issue
Block a user