Renecto commited on
Commit
175d049
·
verified ·
1 Parent(s): 52279ac

feat: add /reset-password page

Browse files
Files changed (3) hide show
  1. app.py +50 -8
  2. login.py +4 -29
  3. 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
- log_event("auth", "login_attempt", metadata={"email": email})
 
 
 
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, metadata={"email": email},
 
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", metadata={"email": email, "error": str(e)},
 
 
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
- log_event(event_type, message, metadata=clean_metadata, user_override=user_override)
 
 
 
 
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", level="WARNING", metadata={"error": str(e)})
 
 
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.Markdown("### 🔧 チャット初期化パラメータ(任意)")
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": "...", "role": "..."})
 
12
 
13
  # Anywhere
14
- log_event("step_executed", "Step01 completed", metadata={"session_id": "abc"})
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 ``app_logs``.
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
- "user_role": user.get("role") if user else None,
 
 
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("app_logs").insert(row).execute()
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}")