feat: add /reset-password page
Browse files- app.py +50 -8
- login.py +4 -29
- supabase_logger.py +20 -6
app.py
CHANGED
|
@@ -22,7 +22,7 @@ from supabase import create_client, Client
|
|
| 22 |
# Import bootstrap to download private app
|
| 23 |
from bootstrap import download_private_app
|
| 24 |
from login import create_login_ui
|
| 25 |
-
from supabase_logger import init_logger, log_event, set_user_context, get_user_context
|
| 26 |
|
| 27 |
# --- Startup Meta Info ---
|
| 28 |
print("=" * 80)
|
|
@@ -94,6 +94,23 @@ print("[PHASE] fastapi_init_end")
|
|
| 94 |
_user_profile_cache: dict = {}
|
| 95 |
|
| 96 |
# --- Request Logging Middleware ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
| 98 |
async def dispatch(self, request: Request, call_next):
|
| 99 |
start_time = time.time()
|
|
@@ -105,6 +122,10 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
| 105 |
set_user_context(user_info)
|
| 106 |
user_tag = f" user={user_info['email']}" if user_info else ""
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
# print(f"[REQUEST] method={method} path={path}{user_tag}")
|
| 109 |
# log_event("request", f"{method} {path}", metadata={"method": method, "path": path})
|
| 110 |
|
|
@@ -132,6 +153,7 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
|
| 132 |
raise
|
| 133 |
finally:
|
| 134 |
set_user_context(None)
|
|
|
|
| 135 |
|
| 136 |
@staticmethod
|
| 137 |
def _resolve_user(request: Request):
|
|
@@ -183,10 +205,13 @@ app.add_middleware(RequestLoggingMiddleware)
|
|
| 183 |
print("[MIDDLEWARE] RequestLoggingMiddleware added")
|
| 184 |
|
| 185 |
# --- Authentication Handler (for login UI) ---
|
| 186 |
-
def handle_login(email, password):
|
| 187 |
"""Handle login attempt via Supabase"""
|
| 188 |
print(f"[AUTH] Login attempt for: {email}")
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
| 190 |
try:
|
| 191 |
res = supabase.auth.sign_in_with_password({"email": email, "password": password})
|
| 192 |
if res.session:
|
|
@@ -194,7 +219,8 @@ def handle_login(email, password):
|
|
| 194 |
user_ctx = {"user_id": str(res.user.id), "email": email}
|
| 195 |
log_event(
|
| 196 |
"auth", "login_success",
|
| 197 |
-
user_override=user_ctx,
|
|
|
|
| 198 |
)
|
| 199 |
return (
|
| 200 |
gr.update(visible=False),
|
|
@@ -205,7 +231,9 @@ def handle_login(email, password):
|
|
| 205 |
print(f"[AUTH] Login failed for {email}: {e}")
|
| 206 |
log_event(
|
| 207 |
"auth", "login_failure",
|
| 208 |
-
level="WARNING",
|
|
|
|
|
|
|
| 209 |
)
|
| 210 |
return gr.update(), gr.update(value=f"❌ エラー: {str(e)}"), None
|
| 211 |
|
|
@@ -270,13 +298,18 @@ if private_app_dir:
|
|
| 270 |
def bridge_logger(event_type: str, message: str, metadata=None):
|
| 271 |
"""Ver20からのログイベントをSupabaseに転送"""
|
| 272 |
user_override = None
|
|
|
|
| 273 |
clean_metadata = None
|
| 274 |
if metadata:
|
| 275 |
clean_metadata = dict(metadata)
|
| 276 |
user_ctx = clean_metadata.pop("_user_context", None)
|
| 277 |
if user_ctx and isinstance(user_ctx, dict):
|
| 278 |
user_override = user_ctx
|
| 279 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
|
| 281 |
set_logger_callback(bridge_logger)
|
| 282 |
print("[LOGGING] Connected ver20 logging to Supabase")
|
|
@@ -491,10 +524,17 @@ async def reset_password_submit(
|
|
| 491 |
html = _RESET_PASSWORD_HTML.format(message="❌ パスワードは8文字以上で設定してください。")
|
| 492 |
return HTMLResponse(html, status_code=400)
|
| 493 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
try:
|
| 495 |
supabase.auth.set_session(access_token, refresh_token)
|
| 496 |
supabase.auth.update_user({"password": new_password})
|
| 497 |
-
log_event("auth", "password_reset_success")
|
| 498 |
success_html = """<!DOCTYPE html>
|
| 499 |
<html lang="ja">
|
| 500 |
<head>
|
|
@@ -521,7 +561,9 @@ async def reset_password_submit(
|
|
| 521 |
return HTMLResponse(success_html)
|
| 522 |
except Exception as e:
|
| 523 |
print(f"[AUTH] Password reset failed: {e}")
|
| 524 |
-
log_event("auth", "password_reset_failure",
|
|
|
|
|
|
|
| 525 |
html = _RESET_PASSWORD_HTML.format(message=f"❌ エラーが発生しました: {str(e)}")
|
| 526 |
return HTMLResponse(html, status_code=400)
|
| 527 |
|
|
|
|
| 22 |
# Import bootstrap to download private app
|
| 23 |
from bootstrap import download_private_app
|
| 24 |
from login import create_login_ui
|
| 25 |
+
from supabase_logger import init_logger, log_event, set_user_context, get_user_context, set_request_source
|
| 26 |
|
| 27 |
# --- Startup Meta Info ---
|
| 28 |
print("=" * 80)
|
|
|
|
| 94 |
_user_profile_cache: dict = {}
|
| 95 |
|
| 96 |
# --- Request Logging Middleware ---
|
| 97 |
+
def _resolve_source(request: Request) -> dict:
|
| 98 |
+
"""リクエストヘッダから流入元を判定して source_channel / source_domain を返す。
|
| 99 |
+
判定優先順:
|
| 100 |
+
1. Referer に huggingface.co/spaces/ を含む → hf_embed(HF UI 経由の埋め込み表示)
|
| 101 |
+
2. ホストが *.hf.space → hf_space(Space への直接アクセス)
|
| 102 |
+
3. それ以外 → unknown
|
| 103 |
+
"""
|
| 104 |
+
headers = request.headers
|
| 105 |
+
referer = (headers.get("referer") or "").lower()
|
| 106 |
+
host = (headers.get("x-forwarded-host") or headers.get("host") or "").lower()
|
| 107 |
+
if "huggingface.co/spaces/" in referer:
|
| 108 |
+
return {"source_channel": "hf_embed", "source_domain": "huggingface.co"}
|
| 109 |
+
if host.endswith(".hf.space"):
|
| 110 |
+
return {"source_channel": "hf_space", "source_domain": host}
|
| 111 |
+
return {"source_channel": "unknown", "source_domain": host or None}
|
| 112 |
+
|
| 113 |
+
|
| 114 |
class RequestLoggingMiddleware(BaseHTTPMiddleware):
|
| 115 |
async def dispatch(self, request: Request, call_next):
|
| 116 |
start_time = time.time()
|
|
|
|
| 122 |
set_user_context(user_info)
|
| 123 |
user_tag = f" user={user_info['email']}" if user_info else ""
|
| 124 |
|
| 125 |
+
# Resolve source (flow-in channel) and store in contextvars
|
| 126 |
+
source_info = _resolve_source(request)
|
| 127 |
+
set_request_source(source_info)
|
| 128 |
+
|
| 129 |
# print(f"[REQUEST] method={method} path={path}{user_tag}")
|
| 130 |
# log_event("request", f"{method} {path}", metadata={"method": method, "path": path})
|
| 131 |
|
|
|
|
| 153 |
raise
|
| 154 |
finally:
|
| 155 |
set_user_context(None)
|
| 156 |
+
set_request_source(None)
|
| 157 |
|
| 158 |
@staticmethod
|
| 159 |
def _resolve_user(request: Request):
|
|
|
|
| 205 |
print("[MIDDLEWARE] RequestLoggingMiddleware added")
|
| 206 |
|
| 207 |
# --- Authentication Handler (for login UI) ---
|
| 208 |
+
def handle_login(request: gr.Request, email, password):
|
| 209 |
"""Handle login attempt via Supabase"""
|
| 210 |
print(f"[AUTH] Login attempt for: {email}")
|
| 211 |
+
source = _resolve_source(request)
|
| 212 |
+
log_event("auth", "login_attempt",
|
| 213 |
+
user_override={"email": email},
|
| 214 |
+
metadata=source)
|
| 215 |
try:
|
| 216 |
res = supabase.auth.sign_in_with_password({"email": email, "password": password})
|
| 217 |
if res.session:
|
|
|
|
| 219 |
user_ctx = {"user_id": str(res.user.id), "email": email}
|
| 220 |
log_event(
|
| 221 |
"auth", "login_success",
|
| 222 |
+
user_override=user_ctx,
|
| 223 |
+
metadata=source,
|
| 224 |
)
|
| 225 |
return (
|
| 226 |
gr.update(visible=False),
|
|
|
|
| 231 |
print(f"[AUTH] Login failed for {email}: {e}")
|
| 232 |
log_event(
|
| 233 |
"auth", "login_failure",
|
| 234 |
+
level="WARNING",
|
| 235 |
+
user_override={"email": email},
|
| 236 |
+
metadata={"error": str(e), **source},
|
| 237 |
)
|
| 238 |
return gr.update(), gr.update(value=f"❌ エラー: {str(e)}"), None
|
| 239 |
|
|
|
|
| 298 |
def bridge_logger(event_type: str, message: str, metadata=None):
|
| 299 |
"""Ver20からのログイベントをSupabaseに転送"""
|
| 300 |
user_override = None
|
| 301 |
+
session_id = None
|
| 302 |
clean_metadata = None
|
| 303 |
if metadata:
|
| 304 |
clean_metadata = dict(metadata)
|
| 305 |
user_ctx = clean_metadata.pop("_user_context", None)
|
| 306 |
if user_ctx and isinstance(user_ctx, dict):
|
| 307 |
user_override = user_ctx
|
| 308 |
+
session_id = clean_metadata.pop("session_id", None)
|
| 309 |
+
log_event(event_type, message,
|
| 310 |
+
metadata=clean_metadata,
|
| 311 |
+
user_override=user_override,
|
| 312 |
+
session_id=session_id)
|
| 313 |
|
| 314 |
set_logger_callback(bridge_logger)
|
| 315 |
print("[LOGGING] Connected ver20 logging to Supabase")
|
|
|
|
| 524 |
html = _RESET_PASSWORD_HTML.format(message="❌ パスワードは8文字以上で設定してください。")
|
| 525 |
return HTMLResponse(html, status_code=400)
|
| 526 |
|
| 527 |
+
reset_user_ctx = None
|
| 528 |
+
try:
|
| 529 |
+
_res = supabase.auth.get_user(access_token)
|
| 530 |
+
reset_user_ctx = {"user_id": str(_res.user.id), "email": _res.user.email}
|
| 531 |
+
except Exception:
|
| 532 |
+
pass
|
| 533 |
+
|
| 534 |
try:
|
| 535 |
supabase.auth.set_session(access_token, refresh_token)
|
| 536 |
supabase.auth.update_user({"password": new_password})
|
| 537 |
+
log_event("auth", "password_reset_success", user_override=reset_user_ctx)
|
| 538 |
success_html = """<!DOCTYPE html>
|
| 539 |
<html lang="ja">
|
| 540 |
<head>
|
|
|
|
| 561 |
return HTMLResponse(success_html)
|
| 562 |
except Exception as e:
|
| 563 |
print(f"[AUTH] Password reset failed: {e}")
|
| 564 |
+
log_event("auth", "password_reset_failure",
|
| 565 |
+
level="WARNING", user_override=reset_user_ctx,
|
| 566 |
+
metadata={"error": str(e)})
|
| 567 |
html = _RESET_PASSWORD_HTML.format(message=f"❌ エラーが発生しました: {str(e)}")
|
| 568 |
return HTMLResponse(html, status_code=400)
|
| 569 |
|
login.py
CHANGED
|
@@ -56,7 +56,8 @@ def create_login_ui(handle_login_fn):
|
|
| 56 |
Create Gradio login UI (チャットはコメントアウト済み).
|
| 57 |
|
| 58 |
Args:
|
| 59 |
-
handle_login_fn: Function to handle login (email, password) -> (form_update, status_update, token)
|
|
|
|
| 60 |
|
| 61 |
Returns:
|
| 62 |
Gradio Blocks UI for login
|
|
@@ -76,34 +77,8 @@ def create_login_ui(handle_login_fn):
|
|
| 76 |
# Hidden textbox to store token and trigger cookie setting via JS
|
| 77 |
token_storage = gr.Textbox(visible=False, elem_id="token_storage")
|
| 78 |
|
| 79 |
-
#
|
| 80 |
-
# gr.
|
| 81 |
-
# with gr.Row():
|
| 82 |
-
# input_url = gr.Textbox(label="URL", placeholder="https://example.com", scale=3)
|
| 83 |
-
# input_industry = gr.Textbox(label="業界 / カテゴリ", placeholder="EC、人材、金融など", scale=2)
|
| 84 |
-
# input_cvr = gr.Number(label="CVR (%)", value=None, minimum=0, maximum=100, scale=1)
|
| 85 |
-
# apply_btn = gr.Button("チャットに反映", variant="secondary")
|
| 86 |
-
# gr.Markdown("### 💬 広告改善提案チャット")
|
| 87 |
-
# chat_frame = gr.HTML(build_chat_html({}))
|
| 88 |
-
# def on_load(request: gr.Request):
|
| 89 |
-
# params = dict(request.query_params)
|
| 90 |
-
# url_val = params.get("url", "")
|
| 91 |
-
# industry_val = params.get("industry", "")
|
| 92 |
-
# cvr_val = float(params["cvr"]) if params.get("cvr") else None
|
| 93 |
-
# return url_val, industry_val, cvr_val, build_chat_html(params)
|
| 94 |
-
# ui.load(on_load, inputs=None, outputs=[input_url, input_industry, input_cvr, chat_frame])
|
| 95 |
-
# def update_chat(url, industry, cvr):
|
| 96 |
-
# params = {}
|
| 97 |
-
# if url:
|
| 98 |
-
# params["url"] = url
|
| 99 |
-
# if industry:
|
| 100 |
-
# params["industry"] = industry
|
| 101 |
-
# if cvr is not None:
|
| 102 |
-
# params["cvr"] = cvr
|
| 103 |
-
# return build_chat_html(params)
|
| 104 |
-
# apply_btn.click(update_chat, inputs=[input_url, input_industry, input_cvr], outputs=chat_frame)
|
| 105 |
-
|
| 106 |
-
# External login handler (from app.py) is bound here
|
| 107 |
login_btn.click(
|
| 108 |
handle_login_fn,
|
| 109 |
inputs=[email_input, pass_input],
|
|
|
|
| 56 |
Create Gradio login UI (チャットはコメントアウト済み).
|
| 57 |
|
| 58 |
Args:
|
| 59 |
+
handle_login_fn: Function to handle login (request, email, password) -> (form_update, status_update, token)
|
| 60 |
+
First argument must be gr.Request for source detection.
|
| 61 |
|
| 62 |
Returns:
|
| 63 |
Gradio Blocks UI for login
|
|
|
|
| 77 |
# Hidden textbox to store token and trigger cookie setting via JS
|
| 78 |
token_storage = gr.Textbox(visible=False, elem_id="token_storage")
|
| 79 |
|
| 80 |
+
# External login handler (from app.py) is bound here.
|
| 81 |
+
# gr.Request は Gradio が第1引数に自動注入するため inputs に含めない。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
login_btn.click(
|
| 83 |
handle_login_fn,
|
| 84 |
inputs=[email_input, pass_input],
|
supabase_logger.py
CHANGED
|
@@ -2,16 +2,17 @@
|
|
| 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
|
| 6 |
|
| 7 |
# At startup
|
| 8 |
init_logger(supabase_client)
|
| 9 |
|
| 10 |
# In middleware / auth
|
| 11 |
-
set_user_context({"user_id": "...", "email": "..."
|
|
|
|
| 12 |
|
| 13 |
# Anywhere
|
| 14 |
-
log_event("
|
| 15 |
"""
|
| 16 |
|
| 17 |
from __future__ import annotations
|
|
@@ -25,9 +26,13 @@ from typing import Any, Dict, Optional
|
|
| 25 |
from supabase import Client
|
| 26 |
|
| 27 |
_supabase: Optional[Client] = None
|
|
|
|
| 28 |
_current_user: ContextVar[Optional[Dict[str, Any]]] = ContextVar(
|
| 29 |
"current_user", default=None
|
| 30 |
)
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
|
| 33 |
def init_logger(client: Client) -> None:
|
|
@@ -43,6 +48,11 @@ def get_user_context() -> Optional[Dict[str, Any]]:
|
|
| 43 |
return _current_user.get()
|
| 44 |
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
def log_event(
|
| 47 |
event_type: str,
|
| 48 |
message: str,
|
|
@@ -50,14 +60,16 @@ def log_event(
|
|
| 50 |
level: str = "INFO",
|
| 51 |
metadata: Optional[Dict[str, Any]] = None,
|
| 52 |
user_override: Optional[Dict[str, Any]] = None,
|
|
|
|
| 53 |
) -> None:
|
| 54 |
"""
|
| 55 |
-
Insert a structured log row into Supabase ``
|
| 56 |
|
| 57 |
Falls back to stdout if the insert fails so the main request is
|
| 58 |
never blocked by a logging error.
|
| 59 |
"""
|
| 60 |
user = user_override or _current_user.get()
|
|
|
|
| 61 |
|
| 62 |
row = {
|
| 63 |
"event_type": event_type,
|
|
@@ -65,7 +77,9 @@ def log_event(
|
|
| 65 |
"message": message,
|
| 66 |
"user_id": user.get("user_id") if user else None,
|
| 67 |
"user_email": user.get("email") if user else None,
|
| 68 |
-
"
|
|
|
|
|
|
|
| 69 |
"metadata": metadata or {},
|
| 70 |
"created_at": datetime.now(timezone.utc).isoformat(),
|
| 71 |
}
|
|
@@ -73,7 +87,7 @@ def log_event(
|
|
| 73 |
def _insert():
|
| 74 |
try:
|
| 75 |
if _supabase is not None:
|
| 76 |
-
_supabase.table("
|
| 77 |
except Exception:
|
| 78 |
print(f"[SUPABASE_LOG_ERROR] insert failed: {traceback.format_exc()}")
|
| 79 |
print(f"[SUPABASE_LOG_ERROR] row: {row}")
|
|
|
|
| 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_channel": "hf_space", "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
|
|
|
|
| 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 |
def init_logger(client: Client) -> None:
|
|
|
|
| 48 |
return _current_user.get()
|
| 49 |
|
| 50 |
|
| 51 |
+
def set_request_source(source: Optional[Dict[str, str]]) -> None:
|
| 52 |
+
"""リクエストの流入元情報をセット(FastAPI middleware から呼ぶ)"""
|
| 53 |
+
_request_source.set(source)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
def log_event(
|
| 57 |
event_type: str,
|
| 58 |
message: str,
|
|
|
|
| 60 |
level: str = "INFO",
|
| 61 |
metadata: Optional[Dict[str, Any]] = None,
|
| 62 |
user_override: Optional[Dict[str, Any]] = None,
|
| 63 |
+
session_id: Optional[str] = None,
|
| 64 |
) -> None:
|
| 65 |
"""
|
| 66 |
+
Insert a structured log row into Supabase ``activity_logs``.
|
| 67 |
|
| 68 |
Falls back to stdout if the insert fails so the main request is
|
| 69 |
never blocked by a logging error.
|
| 70 |
"""
|
| 71 |
user = user_override or _current_user.get()
|
| 72 |
+
source = _request_source.get()
|
| 73 |
|
| 74 |
row = {
|
| 75 |
"event_type": event_type,
|
|
|
|
| 77 |
"message": message,
|
| 78 |
"user_id": user.get("user_id") if user else None,
|
| 79 |
"user_email": user.get("email") if user else None,
|
| 80 |
+
"session_id": session_id,
|
| 81 |
+
"source_channel": source.get("source_channel") if source else None,
|
| 82 |
+
"source_domain": source.get("source_domain") if source else None,
|
| 83 |
"metadata": metadata or {},
|
| 84 |
"created_at": datetime.now(timezone.utc).isoformat(),
|
| 85 |
}
|
|
|
|
| 87 |
def _insert():
|
| 88 |
try:
|
| 89 |
if _supabase is not None:
|
| 90 |
+
_supabase.table("activity_logs").insert(row).execute()
|
| 91 |
except Exception:
|
| 92 |
print(f"[SUPABASE_LOG_ERROR] insert failed: {traceback.format_exc()}")
|
| 93 |
print(f"[SUPABASE_LOG_ERROR] row: {row}")
|