feat: add /reset-password page
Browse files- app.py +19 -9
- supabase_logger.py +133 -123
app.py
CHANGED
|
@@ -96,22 +96,31 @@ _user_profile_cache: dict = {}
|
|
| 96 |
# 最後に解決された source を保持(Gradio WebSocket スレッドは ContextVar が伝播しないため)
|
| 97 |
_last_known_source: dict = {}
|
| 98 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
# --- Request Logging Middleware ---
|
| 100 |
-
def _resolve_source(request: Request) -> dict:
|
| 101 |
-
"""リクエストヘッダから流入元を判定して
|
|
|
|
| 102 |
判定優先順:
|
| 103 |
-
1.
|
| 104 |
-
2.
|
| 105 |
-
3.
|
|
|
|
| 106 |
"""
|
| 107 |
headers = request.headers
|
| 108 |
referer = (headers.get("referer") or "").lower()
|
| 109 |
host = (headers.get("x-forwarded-host") or headers.get("host") or "").lower()
|
|
|
|
|
|
|
| 110 |
if "huggingface.co/spaces/" in referer:
|
| 111 |
-
return {"
|
| 112 |
if host.endswith(".hf.space"):
|
| 113 |
-
return {"
|
| 114 |
-
return {"
|
| 115 |
|
| 116 |
|
| 117 |
class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
@@ -128,7 +137,8 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
| 128 |
# Resolve source (flow-in channel) and store in contextvars
|
| 129 |
source_info = _resolve_source(request)
|
| 130 |
set_request_source(source_info)
|
| 131 |
-
|
|
|
|
| 132 |
|
| 133 |
# print(f"[REQUEST] method={method} path={path}{user_tag}")
|
| 134 |
# log_event("request", f"{method} {path}", metadata={"method": method, "path": path})
|
|
|
|
| 96 |
# 最後に解決された source を保持(Gradio WebSocket スレッドは ContextVar が伝播しないため)
|
| 97 |
_last_known_source: dict = {}
|
| 98 |
|
| 99 |
+
# 内部プロキシホスト(ログ対象外)
|
| 100 |
+
_INTERNAL_PROXY_DOMAINS = {
|
| 101 |
+
"proxy.spaces.internal.huggingface.tech",
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
# --- Request Logging Middleware ---
|
| 105 |
+
def _resolve_source(request: Request) -> dict | None:
|
| 106 |
+
"""リクエストヘッダから流入元を判定して source_domain を返す。
|
| 107 |
+
内部プロキシホストの場合は None を返してログを除外する。
|
| 108 |
判定優先順:
|
| 109 |
+
1. ホストが内部プロキシ → None(ログ除外)
|
| 110 |
+
2. Referer に huggingface.co/spaces/ を含む → source_domain="huggingface.co"
|
| 111 |
+
3. ホストが *.hf.space → source_domain=host
|
| 112 |
+
4. それ以外 → source_domain=host or None
|
| 113 |
"""
|
| 114 |
headers = request.headers
|
| 115 |
referer = (headers.get("referer") or "").lower()
|
| 116 |
host = (headers.get("x-forwarded-host") or headers.get("host") or "").lower()
|
| 117 |
+
if host in _INTERNAL_PROXY_DOMAINS:
|
| 118 |
+
return None
|
| 119 |
if "huggingface.co/spaces/" in referer:
|
| 120 |
+
return {"source_domain": "huggingface.co"}
|
| 121 |
if host.endswith(".hf.space"):
|
| 122 |
+
return {"source_domain": host}
|
| 123 |
+
return {"source_domain": host or None}
|
| 124 |
|
| 125 |
|
| 126 |
class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
|
|
| 137 |
# Resolve source (flow-in channel) and store in contextvars
|
| 138 |
source_info = _resolve_source(request)
|
| 139 |
set_request_source(source_info)
|
| 140 |
+
if source_info is not None:
|
| 141 |
+
_last_known_source.update(source_info)
|
| 142 |
|
| 143 |
# print(f"[REQUEST] method={method} path={path}{user_tag}")
|
| 144 |
# log_event("request", f"{method} {path}", metadata={"method": method, "path": path})
|
supabase_logger.py
CHANGED
|
@@ -1,123 +1,133 @@
|
|
| 1 |
-
"""
|
| 2 |
-
Supabase structured logger with per-request user context.
|
| 3 |
-
|
| 4 |
-
Usage:
|
| 5 |
-
from supabase_logger import init_logger, log_event, set_user_context, get_user_context, set_request_source
|
| 6 |
-
|
| 7 |
-
# At startup
|
| 8 |
-
init_logger(supabase_client)
|
| 9 |
-
|
| 10 |
-
# In middleware / auth
|
| 11 |
-
set_user_context({"user_id": "...", "email": "..."})
|
| 12 |
-
set_request_source({"
|
| 13 |
-
|
| 14 |
-
# Anywhere
|
| 15 |
-
log_event("user", "click_step01", session_id="abc123", metadata={"force": False})
|
| 16 |
-
"""
|
| 17 |
-
|
| 18 |
-
from __future__ import annotations
|
| 19 |
-
|
| 20 |
-
import threading
|
| 21 |
-
import traceback
|
| 22 |
-
from contextvars import ContextVar
|
| 23 |
-
from datetime import datetime, timezone
|
| 24 |
-
from typing import Any, Dict, Optional
|
| 25 |
-
|
| 26 |
-
from supabase import Client
|
| 27 |
-
|
| 28 |
-
_supabase: Optional[Client] = None
|
| 29 |
-
|
| 30 |
-
_current_user: ContextVar[Optional[Dict[str, Any]]] = ContextVar(
|
| 31 |
-
"current_user", default=None
|
| 32 |
-
)
|
| 33 |
-
_request_source: ContextVar[Optional[Dict[str, str]]] = ContextVar(
|
| 34 |
-
"request_source", default=None
|
| 35 |
-
)
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
def
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Supabase structured logger with per-request user context.
|
| 3 |
+
|
| 4 |
+
Usage:
|
| 5 |
+
from supabase_logger import init_logger, log_event, set_user_context, get_user_context, set_request_source
|
| 6 |
+
|
| 7 |
+
# At startup
|
| 8 |
+
init_logger(supabase_client)
|
| 9 |
+
|
| 10 |
+
# In middleware / auth
|
| 11 |
+
set_user_context({"user_id": "...", "email": "..."})
|
| 12 |
+
set_request_source({"source_domain": "dlpo-mbok-dev.hf.space"})
|
| 13 |
+
|
| 14 |
+
# Anywhere
|
| 15 |
+
log_event("user", "click_step01", session_id="abc123", metadata={"force": False})
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import threading
|
| 21 |
+
import traceback
|
| 22 |
+
from contextvars import ContextVar
|
| 23 |
+
from datetime import datetime, timezone
|
| 24 |
+
from typing import Any, Dict, Optional
|
| 25 |
+
|
| 26 |
+
from supabase import Client
|
| 27 |
+
|
| 28 |
+
_supabase: Optional[Client] = None
|
| 29 |
+
|
| 30 |
+
_current_user: ContextVar[Optional[Dict[str, Any]]] = ContextVar(
|
| 31 |
+
"current_user", default=None
|
| 32 |
+
)
|
| 33 |
+
_request_source: ContextVar[Optional[Dict[str, str]]] = ContextVar(
|
| 34 |
+
"request_source", default=None
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
# Domains whose events are never persisted (internal HF proxy health checks etc.)
|
| 38 |
+
_BLOCKED_SOURCE_DOMAINS = {
|
| 39 |
+
"proxy.spaces.internal.huggingface.tech",
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def init_logger(client: Client) -> None:
|
| 44 |
+
global _supabase
|
| 45 |
+
_supabase = client
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def set_user_context(user: Optional[Dict[str, Any]]) -> None:
|
| 49 |
+
_current_user.set(user)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def get_user_context() -> Optional[Dict[str, Any]]:
|
| 53 |
+
return _current_user.get()
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def set_request_source(source: Optional[Dict[str, str]]) -> None:
|
| 57 |
+
"""リクエストの流入元情報をセット(FastAPI middleware から呼ぶ)"""
|
| 58 |
+
_request_source.set(source)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def log_event(
|
| 62 |
+
event_type: str,
|
| 63 |
+
message: str,
|
| 64 |
+
*,
|
| 65 |
+
level: str = "INFO",
|
| 66 |
+
metadata: Optional[Dict[str, Any]] = None,
|
| 67 |
+
user_override: Optional[Dict[str, Any]] = None,
|
| 68 |
+
session_id: Optional[str] = None,
|
| 69 |
+
source: Optional[Dict[str, str]] = None,
|
| 70 |
+
) -> None:
|
| 71 |
+
"""
|
| 72 |
+
Insert a structured log row into Supabase ``activity_logs``.
|
| 73 |
+
|
| 74 |
+
Source resolution priority:
|
| 75 |
+
1. Explicit ``source`` argument
|
| 76 |
+
2. ContextVar set by middleware (``set_request_source``)
|
| 77 |
+
3. ``source_domain`` key inside ``metadata``
|
| 78 |
+
|
| 79 |
+
If the resolved ``source_domain`` is an internal proxy domain the event is
|
| 80 |
+
silently dropped (not persisted).
|
| 81 |
+
|
| 82 |
+
Falls back to stdout if the insert fails so the main request is
|
| 83 |
+
never blocked by a logging error.
|
| 84 |
+
"""
|
| 85 |
+
user = user_override or _current_user.get()
|
| 86 |
+
|
| 87 |
+
# --- source resolution ---
|
| 88 |
+
clean_meta: Dict[str, Any] = dict(metadata) if metadata else {}
|
| 89 |
+
resolved_source: Dict[str, Any] = {}
|
| 90 |
+
|
| 91 |
+
if source:
|
| 92 |
+
resolved_source = source
|
| 93 |
+
elif _request_source.get():
|
| 94 |
+
resolved_source = _request_source.get() # type: ignore[assignment]
|
| 95 |
+
else:
|
| 96 |
+
# Fall back to key embedded in metadata
|
| 97 |
+
dm = clean_meta.pop("source_domain", None)
|
| 98 |
+
if dm:
|
| 99 |
+
resolved_source = {"source_domain": dm}
|
| 100 |
+
|
| 101 |
+
# Strip source_domain from metadata to avoid duplication
|
| 102 |
+
clean_meta.pop("source_domain", None)
|
| 103 |
+
# Also strip legacy source_channel if still present in older metadata
|
| 104 |
+
clean_meta.pop("source_channel", None)
|
| 105 |
+
|
| 106 |
+
source_domain = resolved_source.get("source_domain")
|
| 107 |
+
|
| 108 |
+
# Drop events from internal proxy domains
|
| 109 |
+
if source_domain in _BLOCKED_SOURCE_DOMAINS:
|
| 110 |
+
return
|
| 111 |
+
|
| 112 |
+
row = {
|
| 113 |
+
"event_type": event_type,
|
| 114 |
+
"level": level,
|
| 115 |
+
"message": message,
|
| 116 |
+
"user_id": user.get("user_id") if user else None,
|
| 117 |
+
"user_email": user.get("email") if user else None,
|
| 118 |
+
"session_id": session_id,
|
| 119 |
+
"source_domain": source_domain,
|
| 120 |
+
"metadata": clean_meta,
|
| 121 |
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
def _insert():
|
| 125 |
+
try:
|
| 126 |
+
if _supabase is not None:
|
| 127 |
+
_supabase.table("activity_logs").insert(row).execute()
|
| 128 |
+
except Exception:
|
| 129 |
+
print(f"[SUPABASE_LOG_ERROR] insert failed: {traceback.format_exc()}")
|
| 130 |
+
print(f"[SUPABASE_LOG_ERROR] row: {row}")
|
| 131 |
+
|
| 132 |
+
# Fire-and-forget so the request is never delayed
|
| 133 |
+
threading.Thread(target=_insert, daemon=True).start()
|