Renecto commited on
Commit
a4bd58d
·
verified ·
1 Parent(s): e575259

feat: add /reset-password page

Browse files
Files changed (3) hide show
  1. app.py +158 -2
  2. bootstrap.py +117 -117
  3. requirements.txt +1 -0
app.py CHANGED
@@ -12,8 +12,8 @@ import logging
12
 
13
  logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
14
  from pathlib import Path
15
- from fastapi import FastAPI, Request, Depends, HTTPException
16
- from fastapi.responses import RedirectResponse, JSONResponse
17
  from fastapi.staticfiles import StaticFiles
18
  from starlette.middleware.base import BaseHTTPMiddleware
19
  import gradio as gr
@@ -370,6 +370,162 @@ async def healthz():
370
  print(f"[HEALTHZ] {status}")
371
  return JSONResponse(content=status)
372
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  print("[ROUTES] Root, logout, and healthz routes registered")
374
 
375
  # --- Mount Gradio UIs ---
 
12
 
13
  logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
14
  from pathlib import Path
15
+ from fastapi import FastAPI, Request, Depends, HTTPException, Form
16
+ from fastapi.responses import RedirectResponse, JSONResponse, HTMLResponse
17
  from fastapi.staticfiles import StaticFiles
18
  from starlette.middleware.base import BaseHTTPMiddleware
19
  import gradio as gr
 
370
  print(f"[HEALTHZ] {status}")
371
  return JSONResponse(content=status)
372
 
373
+ _RESET_PASSWORD_HTML = """<!DOCTYPE html>
374
+ <html lang="ja">
375
+ <head>
376
+ <meta charset="UTF-8">
377
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
378
+ <title>パスワード再設定</title>
379
+ <style>
380
+ * {{ box-sizing: border-box; margin: 0; padding: 0; }}
381
+ body {{
382
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
383
+ background: #f5f5f5;
384
+ display: flex;
385
+ align-items: center;
386
+ justify-content: center;
387
+ min-height: 100vh;
388
+ }}
389
+ .card {{
390
+ background: #fff;
391
+ border-radius: 12px;
392
+ box-shadow: 0 2px 16px rgba(0,0,0,0.1);
393
+ padding: 40px;
394
+ width: 100%;
395
+ max-width: 400px;
396
+ }}
397
+ h1 {{ font-size: 1.4rem; margin-bottom: 24px; color: #333; }}
398
+ label {{ display: block; font-size: 0.85rem; color: #555; margin-bottom: 6px; }}
399
+ input[type=password] {{
400
+ width: 100%;
401
+ padding: 10px 14px;
402
+ border: 1px solid #ddd;
403
+ border-radius: 8px;
404
+ font-size: 1rem;
405
+ margin-bottom: 16px;
406
+ outline: none;
407
+ transition: border 0.2s;
408
+ }}
409
+ input[type=password]:focus {{ border-color: #f97316; }}
410
+ button {{
411
+ width: 100%;
412
+ padding: 12px;
413
+ background: #f97316;
414
+ color: #fff;
415
+ border: none;
416
+ border-radius: 8px;
417
+ font-size: 1rem;
418
+ cursor: pointer;
419
+ transition: background 0.2s;
420
+ }}
421
+ button:hover {{ background: #ea6c0a; }}
422
+ .msg {{ margin-top: 16px; font-size: 0.9rem; color: #e53e3e; text-align: center; }}
423
+ .msg.success {{ color: #38a169; }}
424
+ </style>
425
+ </head>
426
+ <body>
427
+ <div class="card">
428
+ <h1>🔑 パスワード再設定</h1>
429
+ <form method="post" action="/reset-password" id="resetForm">
430
+ <input type="hidden" name="access_token" id="access_token">
431
+ <input type="hidden" name="refresh_token" id="refresh_token">
432
+ <label for="new_password">新しいパスワード</label>
433
+ <input type="password" name="new_password" id="new_password" placeholder="8文字以上" required minlength="8">
434
+ <label for="confirm_password">確認(再入力)</label>
435
+ <input type="password" id="confirm_password" placeholder="同じパスワードを入力" required minlength="8">
436
+ <button type="submit">パスワードを変更する</button>
437
+ </form>
438
+ <div class="msg" id="msg">{message}</div>
439
+ </div>
440
+ <script>
441
+ // URLフラグメントからSupabaseのトークンを取得してhidden inputにセット
442
+ (function() {{
443
+ var hash = window.location.hash.substring(1);
444
+ var params = {{}};
445
+ hash.split('&').forEach(function(part) {{
446
+ var kv = part.split('=');
447
+ if (kv.length === 2) params[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1]);
448
+ }});
449
+ if (params.access_token) {{
450
+ document.getElementById('access_token').value = params.access_token;
451
+ }}
452
+ if (params.refresh_token) {{
453
+ document.getElementById('refresh_token').value = params.refresh_token;
454
+ }}
455
+ if (!params.access_token && !params.refresh_token) {{
456
+ document.getElementById('msg').textContent = '⚠️ 無効なリンクです。パスワードリセットメールを再送してください。';
457
+ }}
458
+ }})();
459
+
460
+ // パスワード一致チェック
461
+ document.getElementById('resetForm').addEventListener('submit', function(e) {{
462
+ var pw = document.getElementById('new_password').value;
463
+ var cpw = document.getElementById('confirm_password').value;
464
+ if (pw !== cpw) {{
465
+ e.preventDefault();
466
+ document.getElementById('msg').textContent = '❌ パスワードが一致しません。';
467
+ }}
468
+ }});
469
+ </script>
470
+ </body>
471
+ </html>"""
472
+
473
+ @app.get("/reset-password")
474
+ async def reset_password_page():
475
+ """パスワード再設定フォームを表示"""
476
+ return HTMLResponse(_RESET_PASSWORD_HTML.format(message=""))
477
+
478
+
479
+ @app.post("/reset-password")
480
+ async def reset_password_submit(
481
+ access_token: str = Form(default=""),
482
+ refresh_token: str = Form(default=""),
483
+ new_password: str = Form(...),
484
+ ):
485
+ """パスワード再設定を実行"""
486
+ if not access_token:
487
+ html = _RESET_PASSWORD_HTML.format(message="❌ トークンが取得できませんでした。メールのリンクを再度クリックしてください。")
488
+ return HTMLResponse(html, status_code=400)
489
+
490
+ if len(new_password) < 8:
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>
501
+ <meta charset="UTF-8">
502
+ <meta http-equiv="refresh" content="3;url=/login/">
503
+ <title>パスワード変更完了</title>
504
+ <style>
505
+ body {{ font-family: -apple-system, sans-serif; display: flex; align-items: center;
506
+ justify-content: center; min-height: 100vh; background: #f5f5f5; }}
507
+ .card {{ background: #fff; border-radius: 12px; padding: 40px; text-align: center;
508
+ box-shadow: 0 2px 16px rgba(0,0,0,0.1); max-width: 360px; width: 100%; }}
509
+ h1 {{ color: #38a169; margin-bottom: 12px; }}
510
+ p {{ color: #555; font-size: 0.95rem; }}
511
+ </style>
512
+ </head>
513
+ <body>
514
+ <div class="card">
515
+ <h1>✅ パスワードを変更しました</h1>
516
+ <p>3秒後にログイン画面に移動します...</p>
517
+ <p><a href="/login/">今すぐログイン画面へ</a></p>
518
+ </div>
519
+ </body>
520
+ </html>"""
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
+
528
+
529
  print("[ROUTES] Root, logout, and healthz routes registered")
530
 
531
  # --- Mount Gradio UIs ---
bootstrap.py CHANGED
@@ -1,117 +1,117 @@
1
- #!/usr/bin/env python3
2
- """
3
- Bootstrap module - Downloads ver20 from private DLPO/habadashi Space
4
- """
5
-
6
- import os
7
- from pathlib import Path
8
- from huggingface_hub import snapshot_download
9
-
10
- def get_hf_token():
11
- """Get HF_TOKEN from environment variable"""
12
- token = os.environ.get("HF_TOKEN")
13
- if not token:
14
- print("[BOOTSTRAP_ERROR] HF_TOKEN not found in environment")
15
- raise ValueError(
16
- "HF_TOKEN not found. Please set HF_TOKEN environment variable "
17
- "in HF Space Secrets to access private repository."
18
- )
19
- print(f"[BOOTSTRAP] HF_TOKEN found (length: {len(token)}, 末尾3文字: ...{token[-3:]})")
20
- return token
21
-
22
- def download_private_app(force_download: bool = False):
23
- """
24
- Download ver20 application from private DLPO/habadashi Space
25
-
26
- Args:
27
- force_download: If True, re-download even if already cached
28
-
29
- Returns:
30
- Path: Local directory containing downloaded ver20 files
31
- """
32
- repo_id = "DLPO/habadashi"
33
- repo_type = "space"
34
- local_dir = Path("./private_app")
35
-
36
- print(f"[BOOTSTRAP] Starting download")
37
- print(f"[BOOTSTRAP] repo_id={repo_id} repo_type={repo_type}")
38
- print(f"[BOOTSTRAP] local_dir={local_dir} force_download={force_download}")
39
- print(f"[BOOTSTRAP] local_dir.exists()={local_dir.exists()}")
40
-
41
- # Check if already downloaded (skip if exists and not forced)
42
- if local_dir.exists() and not force_download:
43
- # Verify essential files exist
44
- app_py = local_dir / "app.py"
45
- print(f"[BOOTSTRAP] Checking cache: app.py exists={app_py.exists()}")
46
- if app_py.exists():
47
- print(f"[BOOTSTRAP] Using cached download: {local_dir}")
48
- # Verify structure
49
- _verify_downloaded_structure(local_dir)
50
- return local_dir
51
- else:
52
- print(f"[BOOTSTRAP] Cache invalid (app.py missing), will re-download")
53
-
54
- print(f"[BOOTSTRAP] Starting fresh download from {repo_id}...")
55
-
56
- try:
57
- token = get_hf_token()
58
-
59
- # Download entire Space repository (removed local_dir_use_symlinks - deprecated)
60
- print(f"[BOOTSTRAP] Calling snapshot_download...")
61
- snapshot_download(
62
- repo_id=repo_id,
63
- repo_type=repo_type,
64
- local_dir=str(local_dir),
65
- token=token,
66
- )
67
-
68
- print(f"[BOOTSTRAP] Download complete: {local_dir}")
69
-
70
- # Verify essential structure
71
- _verify_downloaded_structure(local_dir)
72
-
73
- return local_dir
74
-
75
- except Exception as e:
76
- print(f"[BOOTSTRAP_ERROR] Download failed: {e}")
77
- import traceback
78
- print(f"[TRACEBACK]\n{traceback.format_exc()}")
79
- raise
80
-
81
- def _verify_downloaded_structure(local_dir: Path):
82
- """Verify downloaded structure has essential components"""
83
- print(f"[BOOTSTRAP_VERIFY] Checking downloaded structure...")
84
-
85
- essential_files = [
86
- "app.py",
87
- "requirements.txt",
88
- ]
89
-
90
- essential_dirs = [
91
- "lib",
92
- "exec",
93
- "core",
94
- "presentation",
95
- ]
96
-
97
- for file_name in essential_files:
98
- file_path = local_dir / file_name
99
- exists = file_path.exists()
100
- print(f"[BOOTSTRAP_VERIFY] {file_name}: {'EXISTS' if exists else 'MISSING'}")
101
- if not exists and file_name == "app.py":
102
- raise FileNotFoundError(
103
- f"Critical file missing: {file_name}. "
104
- f"Expected at: {file_path}"
105
- )
106
-
107
- for dir_name in essential_dirs:
108
- dir_path = local_dir / dir_name
109
- exists = dir_path.exists() and dir_path.is_dir()
110
- print(f"[BOOTSTRAP_VERIFY] {dir_name}/: {'EXISTS' if exists else 'MISSING'}")
111
-
112
- print(f"[BOOTSTRAP_VERIFY] Verification complete")
113
-
114
- if __name__ == "__main__":
115
- # Test download
116
- download_private_app()
117
- print("🎉 Bootstrap test successful!")
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Bootstrap module - Downloads ver20 from private DLPO/habadashi Space
4
+ """
5
+
6
+ import os
7
+ from pathlib import Path
8
+ from huggingface_hub import snapshot_download
9
+
10
+ def get_hf_token():
11
+ """Get HF_TOKEN from environment variable"""
12
+ token = os.environ.get("HF_TOKEN")
13
+ if not token:
14
+ print("[BOOTSTRAP_ERROR] HF_TOKEN not found in environment")
15
+ raise ValueError(
16
+ "HF_TOKEN not found. Please set HF_TOKEN environment variable "
17
+ "in HF Space Secrets to access private repository."
18
+ )
19
+ print(f"[BOOTSTRAP] HF_TOKEN found (length: {len(token)}, 末尾3文字: ...{token[-3:]})")
20
+ return token
21
+
22
+ def download_private_app(force_download: bool = False):
23
+ """
24
+ Download ver20 application from private DLPO/habadashi Space
25
+
26
+ Args:
27
+ force_download: If True, re-download even if already cached
28
+
29
+ Returns:
30
+ Path: Local directory containing downloaded ver20 files
31
+ """
32
+ repo_id = "DLPO/habadashi"
33
+ repo_type = "space"
34
+ local_dir = Path("./private_app")
35
+
36
+ print(f"[BOOTSTRAP] Starting download")
37
+ print(f"[BOOTSTRAP] repo_id={repo_id} repo_type={repo_type}")
38
+ print(f"[BOOTSTRAP] local_dir={local_dir} force_download={force_download}")
39
+ print(f"[BOOTSTRAP] local_dir.exists()={local_dir.exists()}")
40
+
41
+ # Check if already downloaded (skip if exists and not forced)
42
+ if local_dir.exists() and not force_download:
43
+ # Verify essential files exist
44
+ app_py = local_dir / "app.py"
45
+ print(f"[BOOTSTRAP] Checking cache: app.py exists={app_py.exists()}")
46
+ if app_py.exists():
47
+ print(f"[BOOTSTRAP] Using cached download: {local_dir}")
48
+ # Verify structure
49
+ _verify_downloaded_structure(local_dir)
50
+ return local_dir
51
+ else:
52
+ print(f"[BOOTSTRAP] Cache invalid (app.py missing), will re-download")
53
+
54
+ print(f"[BOOTSTRAP] Starting fresh download from {repo_id}...")
55
+
56
+ try:
57
+ token = get_hf_token()
58
+
59
+ # Download entire Space repository (removed local_dir_use_symlinks - deprecated)
60
+ print(f"[BOOTSTRAP] Calling snapshot_download...")
61
+ snapshot_download(
62
+ repo_id=repo_id,
63
+ repo_type=repo_type,
64
+ local_dir=str(local_dir),
65
+ token=token,
66
+ )
67
+
68
+ print(f"[BOOTSTRAP] Download complete: {local_dir}")
69
+
70
+ # Verify essential structure
71
+ _verify_downloaded_structure(local_dir)
72
+
73
+ return local_dir
74
+
75
+ except Exception as e:
76
+ print(f"[BOOTSTRAP_ERROR] Download failed: {e}")
77
+ import traceback
78
+ print(f"[TRACEBACK]\n{traceback.format_exc()}")
79
+ raise
80
+
81
+ def _verify_downloaded_structure(local_dir: Path):
82
+ """Verify downloaded structure has essential components"""
83
+ print(f"[BOOTSTRAP_VERIFY] Checking downloaded structure...")
84
+
85
+ essential_files = [
86
+ "app.py",
87
+ "requirements.txt",
88
+ ]
89
+
90
+ essential_dirs = [
91
+ "lib",
92
+ "exec",
93
+ "core",
94
+ "presentation",
95
+ ]
96
+
97
+ for file_name in essential_files:
98
+ file_path = local_dir / file_name
99
+ exists = file_path.exists()
100
+ print(f"[BOOTSTRAP_VERIFY] {file_name}: {'EXISTS' if exists else 'MISSING'}")
101
+ if not exists and file_name == "app.py":
102
+ raise FileNotFoundError(
103
+ f"Critical file missing: {file_name}. "
104
+ f"Expected at: {file_path}"
105
+ )
106
+
107
+ for dir_name in essential_dirs:
108
+ dir_path = local_dir / dir_name
109
+ exists = dir_path.exists() and dir_path.is_dir()
110
+ print(f"[BOOTSTRAP_VERIFY] {dir_name}/: {'EXISTS' if exists else 'MISSING'}")
111
+
112
+ print(f"[BOOTSTRAP_VERIFY] Verification complete")
113
+
114
+ if __name__ == "__main__":
115
+ # Test download
116
+ download_private_app()
117
+ print("🎉 Bootstrap test successful!")
requirements.txt CHANGED
@@ -5,6 +5,7 @@ gradio>=5.34.1
5
  supabase
6
  python-multipart
7
  huggingface_hub
 
8
 
9
  # ver20 dependencies (from ver20/requirements.txt)
10
  gradio_client
 
5
  supabase
6
  python-multipart
7
  huggingface_hub
8
+ python-dotenv
9
 
10
  # ver20 dependencies (from ver20/requirements.txt)
11
  gradio_client