Renecto commited on
Commit
02ee0b7
·
verified ·
1 Parent(s): b61607c

Deploy habadashi_login gateway

Browse files
Files changed (3) hide show
  1. Dockerfile +16 -16
  2. app.py +312 -296
  3. login.py +136 -136
Dockerfile CHANGED
@@ -1,16 +1,16 @@
1
- FROM python:3.12-slim
2
-
3
- WORKDIR /app
4
-
5
- RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*
6
-
7
- COPY requirements.txt .
8
- RUN pip install --no-cache-dir -r requirements.txt
9
-
10
- COPY app.py bootstrap.py login.py supabase_logger.py ./
11
-
12
- ENV PYTHONPATH=/app
13
-
14
- EXPOSE 7860
15
-
16
- CMD ["python", "app.py"]
 
1
+ FROM python:3.12-slim
2
+
3
+ WORKDIR /app
4
+
5
+ RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*
6
+
7
+ COPY requirements.txt .
8
+ RUN pip install --no-cache-dir -r requirements.txt
9
+
10
+ COPY app.py bootstrap.py login.py supabase_logger.py ./
11
+
12
+ ENV PYTHONPATH=/app
13
+
14
+ EXPOSE 7860
15
+
16
+ CMD ["python", "app.py"]
app.py CHANGED
@@ -1,296 +1,312 @@
1
- #!/usr/bin/env python3
2
- """
3
- Habadashi Login Gateway - Main entry point
4
- Public Space that loads private ver20 app dynamically
5
- """
6
-
7
- import os
8
- import sys
9
- import time
10
- import traceback
11
- from pathlib import Path
12
- from fastapi import FastAPI, Request, Depends, HTTPException
13
- from fastapi.responses import RedirectResponse, JSONResponse
14
- from starlette.middleware.base import BaseHTTPMiddleware
15
- import gradio as gr
16
- from supabase import create_client, Client
17
-
18
- # Import bootstrap to download private app
19
- from bootstrap import download_private_app
20
- from login import create_login_ui
21
- from supabase_logger import init_logger, log_event, set_user_context, get_user_context
22
-
23
- # --- Startup Meta Info ---
24
- print("=" * 80)
25
- print("🚀 Starting Habadashi Login Gateway")
26
- print("=" * 80)
27
- print(f"[STARTUP_META] Python version: {sys.version}")
28
- print(f"[STARTUP_META] CWD: {os.getcwd()}")
29
- print(f"[STARTUP_META] PORT: {os.environ.get('PORT', 'not set')}")
30
- print(f"[STARTUP_META] SPACE_ID: {os.environ.get('SPACE_ID', 'not set')}")
31
- print(f"[STARTUP_META] SPACE_HOST: {os.environ.get('SPACE_HOST', 'not set')}")
32
- print(f"[STARTUP_META] GRADIO_SERVER_NAME: {os.environ.get('GRADIO_SERVER_NAME', 'not set')}")
33
- print(f"[STARTUP_META] GRADIO_SERVER_PORT: {os.environ.get('GRADIO_SERVER_PORT', 'not set')}")
34
- print(f"[STARTUP_META] HF_TOKEN: {'***set***' if os.environ.get('HF_TOKEN') else 'NOT SET'}")
35
- print(f"[STARTUP_META] SUPABASE_URL: {'***set***' if os.environ.get('SUPABASE_URL') else 'NOT SET'}")
36
- print(f"[STARTUP_META] SUPABASE_KEY: {'***set***' if os.environ.get('SUPABASE_KEY') else 'NOT SET'}")
37
- print("=" * 80)
38
-
39
- # --- Bootstrap: Download private app at startup ---
40
- print("[PHASE] bootstrap_start")
41
- try:
42
- private_app_dir = download_private_app()
43
-
44
- # Add private app to Python path so we can import it
45
- private_app_path = str(private_app_dir.resolve())
46
- if private_app_path not in sys.path:
47
- sys.path.insert(0, private_app_path)
48
- print(f"[PHASE] bootstrap_end success=true path={private_app_path}")
49
-
50
- except Exception as e:
51
- print(f"[PHASE] bootstrap_end success=false")
52
- print(f"[ERROR] Bootstrap failed: {e}")
53
- print(f"[TRACEBACK]\n{traceback.format_exc()}")
54
- print("⚠️ Application will start but /app/ route will not work")
55
- private_app_dir = None
56
-
57
- # --- Supabase Setup ---
58
- print("[PHASE] supabase_init_start")
59
- SUPABASE_URL = os.environ.get("SUPABASE_URL")
60
- SUPABASE_KEY = os.environ.get("SUPABASE_KEY")
61
-
62
- if not SUPABASE_URL or not SUPABASE_KEY:
63
- print("[ERROR] SUPABASE_URL and/or SUPABASE_KEY not set")
64
- raise ValueError(
65
- "SUPABASE_URL and SUPABASE_KEY must be set in environment variables. "
66
- "Please configure them in HF Space Secrets."
67
- )
68
-
69
- try:
70
- supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
71
- init_logger(supabase)
72
- print(f"[PHASE] supabase_init_end success=true")
73
- except Exception as e:
74
- print(f"[PHASE] supabase_init_end success=false")
75
- print(f"[ERROR] Supabase init failed: {e}")
76
- print(f"[TRACEBACK]\n{traceback.format_exc()}")
77
- raise
78
-
79
- # --- FastAPI App ---
80
- print("[PHASE] fastapi_init_start")
81
- app = FastAPI()
82
- print("[PHASE] fastapi_init_end")
83
-
84
- # --- Request Logging Middleware ---
85
- class RequestLoggingMiddleware(BaseHTTPMiddleware):
86
- async def dispatch(self, request: Request, call_next):
87
- start_time = time.time()
88
- path = request.url.path
89
- method = request.method
90
-
91
- # Resolve user from cookie and store in contextvars
92
- user_info = self._resolve_user(request)
93
- set_user_context(user_info)
94
- user_tag = f" user={user_info['email']}" if user_info else ""
95
-
96
- print(f"[REQUEST] method={method} path={path}{user_tag}")
97
- log_event("request", f"{method} {path}", metadata={"method": method, "path": path})
98
-
99
- try:
100
- response = await call_next(request)
101
- duration = time.time() - start_time
102
- print(f"[RESPONSE] method={method} path={path} status={response.status_code} duration={duration:.3f}s{user_tag}")
103
- if response.status_code >= 400:
104
- log_event(
105
- "response_error",
106
- f"{method} {path} -> {response.status_code}",
107
- level="WARNING",
108
- metadata={"method": method, "path": path, "status": response.status_code, "duration": round(duration, 3)},
109
- )
110
- return response
111
- except Exception as e:
112
- duration = time.time() - start_time
113
- print(f"[RESPONSE] method={method} path={path} status=500 duration={duration:.3f}s error={e}{user_tag}")
114
- log_event(
115
- "response_error",
116
- f"{method} {path} -> 500: {e}",
117
- level="ERROR",
118
- metadata={"method": method, "path": path, "status": 500, "duration": round(duration, 3)},
119
- )
120
- raise
121
- finally:
122
- set_user_context(None)
123
-
124
- @staticmethod
125
- def _resolve_user(request: Request):
126
- """Lightweight user resolution from cookie (no profile fetch)."""
127
- token = request.cookies.get("sb_access_token")
128
- if not token:
129
- return None
130
- try:
131
- res = supabase.auth.get_user(token)
132
- return {"user_id": str(res.user.id), "email": res.user.email}
133
- except Exception:
134
- return None
135
-
136
- app.add_middleware(RequestLoggingMiddleware)
137
- print("[MIDDLEWARE] RequestLoggingMiddleware added")
138
-
139
- # --- Authentication Handler (for login UI) ---
140
- def handle_login(email, password):
141
- """Handle login attempt via Supabase"""
142
- print(f"[AUTH] Login attempt for: {email}")
143
- log_event("login_attempt", f"Login attempt: {email}", metadata={"email": email})
144
- try:
145
- res = supabase.auth.sign_in_with_password({"email": email, "password": password})
146
- if res.session:
147
- print(f"[AUTH] Login successful: {email}")
148
- user_ctx = {"user_id": str(res.user.id), "email": email}
149
- log_event(
150
- "login_success", f"Login success: {email}",
151
- user_override=user_ctx, metadata={"email": email},
152
- )
153
- return (
154
- gr.update(visible=False),
155
- gr.update(visible=True, value=f"### ✅ ログイン成功: {email}"),
156
- res.session.access_token
157
- )
158
- except Exception as e:
159
- print(f"[AUTH] Login failed for {email}: {e}")
160
- log_event(
161
- "login_failure", f"Login failed: {email} - {e}",
162
- level="WARNING", metadata={"email": email, "error": str(e)},
163
- )
164
- return gr.update(), gr.update(value=f"❌ エラー: {str(e)}"), None
165
-
166
- # --- Authentication Dependency ---
167
- def get_current_user(request: Request):
168
- """Verify token from cookie and fetch user profile"""
169
- token = request.cookies.get("sb_access_token")
170
-
171
- print(f"[AUTH_CHECK] Token present: {bool(token)}")
172
-
173
- if not token:
174
- return None
175
-
176
- try:
177
- # Verify token with Supabase
178
- res = supabase.auth.get_user(token)
179
- user_id = res.user.id
180
-
181
- # Fetch profile from profiles table (with organization name)
182
- profile_res = supabase.from_("profiles").select(
183
- "email, org_id, role, display_name, organizations(name)"
184
- ).eq("id", user_id).single().execute()
185
-
186
- profile_data = profile_res.data
187
- user_dict = {
188
- "user_id": user_id,
189
- "email": profile_data.get("email"),
190
- "display_name": profile_data.get("display_name"),
191
- "role": profile_data.get("role"),
192
- "org_name": (profile_data.get("organizations") or {}).get("name")
193
- }
194
-
195
- print(f"[AUTH_CHECK] Success: user={user_dict['email']} role={user_dict['role']}")
196
- log_event(
197
- "auth_check", f"Authenticated: {user_dict['email']}",
198
- user_override=user_dict,
199
- metadata={"role": user_dict.get("role"), "org_name": user_dict.get("org_name")},
200
- )
201
- return user_dict
202
-
203
- except Exception as e:
204
- print(f"[AUTH_CHECK] Failed: {e}")
205
- log_event("auth_check_fail", f"Auth check failed: {e}", level="WARNING")
206
- return None
207
-
208
- # --- Create UI instances ---
209
- print("[PHASE] create_ui_start")
210
- login_ui = create_login_ui(handle_login)
211
- print("[PHASE] create_ui_end component=login_ui")
212
-
213
- # Import ver20 app (Gradio Blocks)
214
- print("[PHASE] import_ver20_start")
215
- ver20_app = None
216
- if private_app_dir:
217
- try:
218
- # Import app module from downloaded private app
219
- from app import app as ver20_blocks
220
- ver20_app = ver20_blocks
221
- print(f"[PHASE] import_ver20_end success=true type={type(ver20_app)}")
222
- except Exception as e:
223
- print(f"[PHASE] import_ver20_end success=false")
224
- print(f"[ERROR] Failed to import ver20 app: {e}")
225
- print(f"[TRACEBACK]\n{traceback.format_exc()}")
226
- else:
227
- print(f"[PHASE] import_ver20_end success=false reason=bootstrap_failed")
228
-
229
- # --- Routes ---
230
- @app.get("/")
231
- async def root(user=Depends(get_current_user)):
232
- """Root route - redirect to login or app based on auth status"""
233
- print(f"[ROUTE] / accessed, user_authenticated={isinstance(user, dict) and user.get('user_id')}")
234
- if isinstance(user, dict) and user.get("user_id"):
235
- return RedirectResponse(url="/app/")
236
- return RedirectResponse(url="/login/")
237
-
238
- @app.get("/logout")
239
- async def logout():
240
- """Logout route - clear cookie and redirect to login"""
241
- user = get_user_context()
242
- print(f"[ROUTE] /logout accessed")
243
- log_event("logout", "User logged out", user_override=user)
244
- response = RedirectResponse(url="/login/")
245
- response.delete_cookie("sb_access_token")
246
- return response
247
-
248
- @app.get("/healthz")
249
- async def healthz():
250
- """Health check endpoint"""
251
- status = {
252
- "ok": True,
253
- "ver20_loaded": ver20_app is not None,
254
- "private_app_dir": str(private_app_dir) if private_app_dir else None
255
- }
256
- print(f"[HEALTHZ] {status}")
257
- return JSONResponse(content=status)
258
-
259
- print("[ROUTES] Root, logout, and healthz routes registered")
260
-
261
- # --- Mount Gradio UIs ---
262
- print("[PHASE] mount_login_start")
263
- app = gr.mount_gradio_app(app, login_ui, path="/login")
264
- print("[PHASE] mount_login_end path=/login")
265
-
266
- # Ver20 App (protected)
267
- print("[PHASE] mount_app_start")
268
- if ver20_app:
269
- app = gr.mount_gradio_app(app, ver20_app, path="/app", auth_dependency=get_current_user)
270
- print("[PHASE] mount_app_end path=/app protected=true ver20=true")
271
- else:
272
- # Fallback: simple placeholder if ver20 failed to load
273
- with gr.Blocks() as fallback_ui:
274
- gr.Markdown("# ⚠️ Application Not Available")
275
- gr.Markdown("The private application failed to load. Please check logs.")
276
-
277
- app = gr.mount_gradio_app(app, fallback_ui, path="/app", auth_dependency=get_current_user)
278
- print("[PHASE] mount_app_end path=/app protected=true ver20=false fallback=true")
279
-
280
- print("=" * 80)
281
- print("🎉 Habadashi Login Gateway Ready!")
282
- print("=" * 80)
283
- print(f"[STARTUP_COMPLETE] All phases completed successfully")
284
- print(f"[STARTUP_COMPLETE] Access URLs:")
285
- print(f" - Root: /")
286
- print(f" - Login: /login/")
287
- print(f" - App: /app/")
288
- print(f" - Health: /healthz")
289
- print(f" - Logout: /logout")
290
- print("=" * 80)
291
-
292
- if __name__ == "__main__":
293
- import uvicorn
294
- port = int(os.environ.get("PORT", 7860))
295
- print(f"[ENTRYPOINT] Starting uvicorn on 0.0.0.0:{port}")
296
- uvicorn.run(app, host="0.0.0.0", port=port, log_level="info")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Habadashi Login Gateway - Main entry point
4
+ Public Space that loads private ver20 app dynamically
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import time
10
+ import traceback
11
+ from pathlib import Path
12
+ from fastapi import FastAPI, Request, Depends, HTTPException
13
+ from fastapi.responses import RedirectResponse, JSONResponse
14
+ from starlette.middleware.base import BaseHTTPMiddleware
15
+ import gradio as gr
16
+ from supabase import create_client, Client
17
+
18
+ # Import bootstrap to download private app
19
+ from bootstrap import download_private_app
20
+ from login import create_login_ui
21
+ from supabase_logger import init_logger, log_event, set_user_context, get_user_context
22
+
23
+ # --- Startup Meta Info ---
24
+ print("=" * 80)
25
+ print("🚀 Starting Habadashi Login Gateway")
26
+ print("=" * 80)
27
+ print(f"[STARTUP_META] Python version: {sys.version}")
28
+ print(f"[STARTUP_META] CWD: {os.getcwd()}")
29
+ print(f"[STARTUP_META] PORT: {os.environ.get('PORT', 'not set')}")
30
+ print(f"[STARTUP_META] SPACE_ID: {os.environ.get('SPACE_ID', 'not set')}")
31
+ print(f"[STARTUP_META] SPACE_HOST: {os.environ.get('SPACE_HOST', 'not set')}")
32
+ print(f"[STARTUP_META] GRADIO_SERVER_NAME: {os.environ.get('GRADIO_SERVER_NAME', 'not set')}")
33
+ print(f"[STARTUP_META] GRADIO_SERVER_PORT: {os.environ.get('GRADIO_SERVER_PORT', 'not set')}")
34
+ print(f"[STARTUP_META] HF_TOKEN: {'***set***' if os.environ.get('HF_TOKEN') else 'NOT SET'}")
35
+ print(f"[STARTUP_META] SUPABASE_URL: {'***set***' if os.environ.get('SUPABASE_URL') else 'NOT SET'}")
36
+ print(f"[STARTUP_META] SUPABASE_KEY: {'***set***' if os.environ.get('SUPABASE_KEY') else 'NOT SET'}")
37
+ print("=" * 80)
38
+
39
+ # --- Bootstrap: Download private app at startup ---
40
+ print("[PHASE] bootstrap_start")
41
+ try:
42
+ private_app_dir = download_private_app()
43
+
44
+ # Add private app to Python path so we can import it
45
+ private_app_path = str(private_app_dir.resolve())
46
+ if private_app_path not in sys.path:
47
+ sys.path.insert(0, private_app_path)
48
+ print(f"[PHASE] bootstrap_end success=true path={private_app_path}")
49
+
50
+ except Exception as e:
51
+ print(f"[PHASE] bootstrap_end success=false")
52
+ print(f"[ERROR] Bootstrap failed: {e}")
53
+ print(f"[TRACEBACK]\n{traceback.format_exc()}")
54
+ print("⚠️ Application will start but /app/ route will not work")
55
+ private_app_dir = None
56
+
57
+ # --- Supabase Setup ---
58
+ print("[PHASE] supabase_init_start")
59
+ SUPABASE_URL = os.environ.get("SUPABASE_URL")
60
+ SUPABASE_KEY = os.environ.get("SUPABASE_KEY")
61
+
62
+ if not SUPABASE_URL or not SUPABASE_KEY:
63
+ print("[ERROR] SUPABASE_URL and/or SUPABASE_KEY not set")
64
+ raise ValueError(
65
+ "SUPABASE_URL and SUPABASE_KEY must be set in environment variables. "
66
+ "Please configure them in HF Space Secrets."
67
+ )
68
+
69
+ try:
70
+ supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
71
+ init_logger(supabase)
72
+ print(f"[PHASE] supabase_init_end success=true")
73
+ except Exception as e:
74
+ print(f"[PHASE] supabase_init_end success=false")
75
+ print(f"[ERROR] Supabase init failed: {e}")
76
+ print(f"[TRACEBACK]\n{traceback.format_exc()}")
77
+ raise
78
+
79
+ # --- FastAPI App ---
80
+ print("[PHASE] fastapi_init_start")
81
+ app = FastAPI()
82
+ print("[PHASE] fastapi_init_end")
83
+
84
+ # --- Request Logging Middleware ---
85
+ class RequestLoggingMiddleware(BaseHTTPMiddleware):
86
+ async def dispatch(self, request: Request, call_next):
87
+ start_time = time.time()
88
+ path = request.url.path
89
+ method = request.method
90
+
91
+ # Resolve user from cookie and store in contextvars
92
+ user_info = self._resolve_user(request)
93
+ set_user_context(user_info)
94
+ user_tag = f" user={user_info['email']}" if user_info else ""
95
+
96
+ # print(f"[REQUEST] method={method} path={path}{user_tag}")
97
+ # log_event("request", f"{method} {path}", metadata={"method": method, "path": path})
98
+
99
+ try:
100
+ response = await call_next(request)
101
+ duration = time.time() - start_time
102
+ # print(f"[RESPONSE] method={method} path={path} status={response.status_code} duration={duration:.3f}s{user_tag}")
103
+ if response.status_code >= 400:
104
+ log_event(
105
+ "response_error",
106
+ f"{method} {path} -> {response.status_code}",
107
+ level="WARNING",
108
+ metadata={"method": method, "path": path, "status": response.status_code, "duration": round(duration, 3)},
109
+ )
110
+ return response
111
+ except Exception as e:
112
+ duration = time.time() - start_time
113
+ print(f"[RESPONSE] method={method} path={path} status=500 duration={duration:.3f}s error={e}{user_tag}")
114
+ log_event(
115
+ "response_error",
116
+ f"{method} {path} -> 500: {e}",
117
+ level="ERROR",
118
+ metadata={"method": method, "path": path, "status": 500, "duration": round(duration, 3)},
119
+ )
120
+ raise
121
+ finally:
122
+ set_user_context(None)
123
+
124
+ @staticmethod
125
+ def _resolve_user(request: Request):
126
+ """Lightweight user resolution from cookie (no profile fetch)."""
127
+ token = request.cookies.get("sb_access_token")
128
+ if not token:
129
+ return None
130
+ try:
131
+ res = supabase.auth.get_user(token)
132
+ return {"user_id": str(res.user.id), "email": res.user.email}
133
+ except Exception:
134
+ return None
135
+
136
+ app.add_middleware(RequestLoggingMiddleware)
137
+ print("[MIDDLEWARE] RequestLoggingMiddleware added")
138
+
139
+ # --- Authentication Handler (for login UI) ---
140
+ def handle_login(email, password):
141
+ """Handle login attempt via Supabase"""
142
+ print(f"[AUTH] Login attempt for: {email}")
143
+ log_event("login_attempt", f"Login attempt: {email}", metadata={"email": email})
144
+ try:
145
+ res = supabase.auth.sign_in_with_password({"email": email, "password": password})
146
+ if res.session:
147
+ print(f"[AUTH] Login successful: {email}")
148
+ user_ctx = {"user_id": str(res.user.id), "email": email}
149
+ log_event(
150
+ "login_success", f"Login success: {email}",
151
+ user_override=user_ctx, metadata={"email": email},
152
+ )
153
+ return (
154
+ gr.update(visible=False),
155
+ gr.update(visible=True, value=f"### ✅ ログイン成功: {email}"),
156
+ res.session.access_token
157
+ )
158
+ except Exception as e:
159
+ print(f"[AUTH] Login failed for {email}: {e}")
160
+ log_event(
161
+ "login_failure", f"Login failed: {email} - {e}",
162
+ level="WARNING", metadata={"email": email, "error": str(e)},
163
+ )
164
+ return gr.update(), gr.update(value=f"❌ エラー: {str(e)}"), None
165
+
166
+ # --- Authentication Dependency ---
167
+ def get_current_user(request: Request):
168
+ """Verify token from cookie and fetch user profile"""
169
+ token = request.cookies.get("sb_access_token")
170
+
171
+ print(f"[AUTH_CHECK] Token present: {bool(token)}")
172
+
173
+ if not token:
174
+ return None
175
+
176
+ try:
177
+ # Verify token with Supabase
178
+ res = supabase.auth.get_user(token)
179
+ user_id = res.user.id
180
+
181
+ # Fetch profile from profiles table (with organization name)
182
+ profile_res = supabase.from_("profiles").select(
183
+ "email, org_id, role, display_name, organizations(name)"
184
+ ).eq("id", user_id).single().execute()
185
+
186
+ profile_data = profile_res.data
187
+ user_dict = {
188
+ "user_id": user_id,
189
+ "email": profile_data.get("email"),
190
+ "display_name": profile_data.get("display_name"),
191
+ "role": profile_data.get("role"),
192
+ "org_name": (profile_data.get("organizations") or {}).get("name")
193
+ }
194
+
195
+ print(f"[AUTH_CHECK] Success: user={user_dict['email']} role={user_dict['role']}")
196
+ log_event(
197
+ "auth_check", f"Authenticated: {user_dict['email']}",
198
+ user_override=user_dict,
199
+ metadata={"role": user_dict.get("role"), "org_name": user_dict.get("org_name")},
200
+ )
201
+ return user_dict
202
+
203
+ except Exception as e:
204
+ print(f"[AUTH_CHECK] Failed: {e}")
205
+ log_event("auth_check_fail", f"Auth check failed: {e}", level="WARNING")
206
+ return None
207
+
208
+ # --- Create UI instances ---
209
+ print("[PHASE] create_ui_start")
210
+ login_ui = create_login_ui(handle_login)
211
+ print("[PHASE] create_ui_end component=login_ui")
212
+
213
+ # Import ver20 app (Gradio Blocks)
214
+ print("[PHASE] import_ver20_start")
215
+ ver20_app = None
216
+ if private_app_dir:
217
+ try:
218
+ # Import app module from downloaded private app
219
+ from app import app as ver20_blocks
220
+
221
+ # --- Inject Logging Callback ---
222
+ try:
223
+ from lib.logging import set_logger_callback
224
+
225
+ def bridge_logger(event_type: str, message: str, metadata=None):
226
+ """Ver20からのログイベントをSupabaseに転送"""
227
+ # 現在のスレッド/コンテキストのuser情報を自動的に使用
228
+ log_event(event_type, message, metadata=metadata)
229
+
230
+ set_logger_callback(bridge_logger)
231
+ print("[LOGGING] Connected ver20 logging to Supabase")
232
+ except ImportError as e:
233
+ print(f"[LOGGING] Could not import lib.logging or set_logger_callback: {e}")
234
+ # -------------------------------
235
+
236
+ ver20_app = ver20_blocks
237
+ print(f"[PHASE] import_ver20_end success=true type={type(ver20_app)}")
238
+ except Exception as e:
239
+ print(f"[PHASE] import_ver20_end success=false")
240
+ print(f"[ERROR] Failed to import ver20 app: {e}")
241
+ print(f"[TRACEBACK]\n{traceback.format_exc()}")
242
+ else:
243
+ print(f"[PHASE] import_ver20_end success=false reason=bootstrap_failed")
244
+
245
+ # --- Routes ---
246
+ @app.get("/")
247
+ async def root(user=Depends(get_current_user)):
248
+ """Root route - redirect to login or app based on auth status"""
249
+ print(f"[ROUTE] / accessed, user_authenticated={isinstance(user, dict) and user.get('user_id')}")
250
+ if isinstance(user, dict) and user.get("user_id"):
251
+ return RedirectResponse(url="/app/")
252
+ return RedirectResponse(url="/login/")
253
+
254
+ @app.get("/logout")
255
+ async def logout():
256
+ """Logout route - clear cookie and redirect to login"""
257
+ user = get_user_context()
258
+ print(f"[ROUTE] /logout accessed")
259
+ log_event("logout", "User logged out", user_override=user)
260
+ response = RedirectResponse(url="/login/")
261
+ response.delete_cookie("sb_access_token")
262
+ return response
263
+
264
+ @app.get("/healthz")
265
+ async def healthz():
266
+ """Health check endpoint"""
267
+ status = {
268
+ "ok": True,
269
+ "ver20_loaded": ver20_app is not None,
270
+ "private_app_dir": str(private_app_dir) if private_app_dir else None
271
+ }
272
+ print(f"[HEALTHZ] {status}")
273
+ return JSONResponse(content=status)
274
+
275
+ print("[ROUTES] Root, logout, and healthz routes registered")
276
+
277
+ # --- Mount Gradio UIs ---
278
+ print("[PHASE] mount_login_start")
279
+ app = gr.mount_gradio_app(app, login_ui, path="/login")
280
+ print("[PHASE] mount_login_end path=/login")
281
+
282
+ # Ver20 App (protected)
283
+ print("[PHASE] mount_app_start")
284
+ if ver20_app:
285
+ app = gr.mount_gradio_app(app, ver20_app, path="/app", auth_dependency=get_current_user)
286
+ print("[PHASE] mount_app_end path=/app protected=true ver20=true")
287
+ else:
288
+ # Fallback: simple placeholder if ver20 failed to load
289
+ with gr.Blocks() as fallback_ui:
290
+ gr.Markdown("# ⚠️ Application Not Available")
291
+ gr.Markdown("The private application failed to load. Please check logs.")
292
+
293
+ app = gr.mount_gradio_app(app, fallback_ui, path="/app", auth_dependency=get_current_user)
294
+ print("[PHASE] mount_app_end path=/app protected=true ver20=false fallback=true")
295
+
296
+ print("=" * 80)
297
+ print("🎉 Habadashi Login Gateway Ready!")
298
+ print("=" * 80)
299
+ print(f"[STARTUP_COMPLETE] All phases completed successfully")
300
+ print(f"[STARTUP_COMPLETE] Access URLs:")
301
+ print(f" - Root: /")
302
+ print(f" - Login: /login/")
303
+ print(f" - App: /app/")
304
+ print(f" - Health: /healthz")
305
+ print(f" - Logout: /logout")
306
+ print("=" * 80)
307
+
308
+ if __name__ == "__main__":
309
+ import uvicorn
310
+ port = int(os.environ.get("PORT", 7860))
311
+ print(f"[ENTRYPOINT] Starting uvicorn on 0.0.0.0:{port}")
312
+ uvicorn.run(app, host="0.0.0.0", port=port, log_level="info")
login.py CHANGED
@@ -1,136 +1,136 @@
1
- import os
2
- import urllib.parse
3
- import gradio as gr
4
-
5
- # Level Bridge Chat の Space URL(環境変数で上書き可能)
6
- CHAT_SPACE_URL = os.environ.get(
7
- "LEVEL_BRIDGE_CHAT_URL",
8
- "https://dlpo-level-bridge-chat.hf.space",
9
- )
10
-
11
- # チャットiframeに転送するパラメータ名一覧
12
- # ログインページのURLに ?url=...&industry=EC のように付けると転送される
13
- FORWARD_PARAMS = ["url", "industry", "campaign_name", "cvr", "ctr", "cpa"]
14
-
15
-
16
- def build_chat_html(params: dict) -> str:
17
- """URLパラメータをチャットiframe srcに付与してHTML生成"""
18
- chat_params = {k: v for k, v in params.items() if k in FORWARD_PARAMS and v}
19
- if chat_params:
20
- src = CHAT_SPACE_URL + "?" + urllib.parse.urlencode(chat_params)
21
- else:
22
- src = CHAT_SPACE_URL
23
- return f"""
24
- <div style="
25
- border: 1px solid #e0e0e0;
26
- border-radius: 12px;
27
- overflow: hidden;
28
- height: 1000px;
29
- background: #fafafa;
30
- ">
31
- <iframe
32
- src="{src}"
33
- style="width:100%; height:100%; border:none;"
34
- allow="clipboard-write"
35
- ></iframe>
36
- </div>
37
- <p style="font-size:11px; color:#999; margin-top:4px; text-align:right;">
38
- Powered by Level Bridge Chat
39
- </p>
40
- """
41
-
42
-
43
- def create_login_ui(handle_login_fn):
44
- """
45
- Create Gradio login UI with embedded Level Bridge Chat.
46
- URLパラメータ (url, industry, campaign_name, cvr, ctr, cpa) をチャットに転送する。
47
-
48
- 例: /login/?url=https://example.com&industry=EC&cvr=2.1
49
-
50
- Args:
51
- handle_login_fn: Function to handle login (email, password) -> (form_update, status_update, token)
52
-
53
- Returns:
54
- Gradio Blocks UI for login
55
- """
56
- with gr.Blocks(title="Login") as ui:
57
-
58
- # --- 上段: ログインフォーム ---
59
- gr.Markdown("# 🔐 Habadashi Login")
60
-
61
- with gr.Column(visible=True) as login_form:
62
- email_input = gr.Textbox(label="Email")
63
- pass_input = gr.Textbox(label="Password", type="password")
64
- login_btn = gr.Button("Login", variant="primary")
65
-
66
- status_msg = gr.Markdown("")
67
-
68
- # Hidden textbox to store token and trigger cookie setting via JS
69
- token_storage = gr.Textbox(visible=False, elem_id="token_storage")
70
-
71
- # --- 中段: チャット初期化フォーム ---
72
- gr.Markdown("### 🔧 チャット初期化パラメータ(任意)")
73
- with gr.Row():
74
- input_url = gr.Textbox(label="URL", placeholder="https://example.com", scale=3)
75
- input_industry = gr.Textbox(label="業界 / カテゴリ", placeholder="EC、人材、金融など", scale=2)
76
- input_cvr = gr.Number(label="CVR (%)", value=None, minimum=0, maximum=100, scale=1)
77
- apply_btn = gr.Button("チャットに反映", variant="secondary")
78
-
79
- # --- 下段: チャット ---
80
- gr.Markdown("### 💬 広告改善提案チャット")
81
- chat_frame = gr.HTML(build_chat_html({}))
82
-
83
- # ページ読み込み時にURLパラメータでフォームとiframeを初期化
84
- def on_load(request: gr.Request):
85
- params = dict(request.query_params)
86
- url_val = params.get("url", "")
87
- industry_val = params.get("industry", "")
88
- cvr_val = float(params["cvr"]) if params.get("cvr") else None
89
- return url_val, industry_val, cvr_val, build_chat_html(params)
90
-
91
- ui.load(on_load, inputs=None, outputs=[input_url, input_industry, input_cvr, chat_frame])
92
-
93
- # フォーム変更時にiframeを動的更新
94
- def update_chat(url, industry, cvr):
95
- params = {}
96
- if url:
97
- params["url"] = url
98
- if industry:
99
- params["industry"] = industry
100
- if cvr is not None:
101
- params["cvr"] = cvr
102
- return build_chat_html(params)
103
-
104
- apply_btn.click(
105
- update_chat,
106
- inputs=[input_url, input_industry, input_cvr],
107
- outputs=chat_frame,
108
- )
109
-
110
- # External login handler (from app.py) is bound here
111
- login_btn.click(
112
- handle_login_fn,
113
- inputs=[email_input, pass_input],
114
- outputs=[login_form, status_msg, token_storage],
115
- )
116
-
117
- # When token is set, use JavaScript to save cookie and redirect
118
- token_storage.change(
119
- None,
120
- inputs=[token_storage],
121
- js="""(token) => {
122
- if (!token || token === "") return;
123
-
124
- // Set cookie
125
- document.cookie = `sb_access_token=${token}; path=/; max-age=3600; SameSite=None; Secure;`;
126
-
127
- console.log("Cookie set, redirecting...");
128
-
129
- // Wait briefly then redirect
130
- setTimeout(() => {
131
- window.location.href = '/app/';
132
- }, 200);
133
- }""",
134
- )
135
-
136
- return ui
 
1
+ import os
2
+ import urllib.parse
3
+ import gradio as gr
4
+
5
+ # Level Bridge Chat の Space URL(環境変数で上書き可能)
6
+ CHAT_SPACE_URL = os.environ.get(
7
+ "LEVEL_BRIDGE_CHAT_URL",
8
+ "https://dlpo-level-bridge-chat.hf.space",
9
+ )
10
+
11
+ # チャットiframeに転送するパラメータ名一覧
12
+ # ログインページのURLに ?url=...&industry=EC のように付けると転送される
13
+ FORWARD_PARAMS = ["url", "industry", "campaign_name", "cvr", "ctr", "cpa"]
14
+
15
+
16
+ def build_chat_html(params: dict) -> str:
17
+ """URLパラメータをチャットiframe srcに付与してHTML生成"""
18
+ chat_params = {k: v for k, v in params.items() if k in FORWARD_PARAMS and v}
19
+ if chat_params:
20
+ src = CHAT_SPACE_URL + "?" + urllib.parse.urlencode(chat_params)
21
+ else:
22
+ src = CHAT_SPACE_URL
23
+ return f"""
24
+ <div style="
25
+ border: 1px solid #e0e0e0;
26
+ border-radius: 12px;
27
+ overflow: hidden;
28
+ height: 1000px;
29
+ background: #fafafa;
30
+ ">
31
+ <iframe
32
+ src="{src}"
33
+ style="width:100%; height:100%; border:none;"
34
+ allow="clipboard-write"
35
+ ></iframe>
36
+ </div>
37
+ <p style="font-size:11px; color:#999; margin-top:4px; text-align:right;">
38
+ Powered by Level Bridge Chat
39
+ </p>
40
+ """
41
+
42
+
43
+ def create_login_ui(handle_login_fn):
44
+ """
45
+ Create Gradio login UI with embedded Level Bridge Chat.
46
+ URLパラメータ (url, industry, campaign_name, cvr, ctr, cpa) をチャットに転送する。
47
+
48
+ 例: /login/?url=https://example.com&industry=EC&cvr=2.1
49
+
50
+ Args:
51
+ handle_login_fn: Function to handle login (email, password) -> (form_update, status_update, token)
52
+
53
+ Returns:
54
+ Gradio Blocks UI for login
55
+ """
56
+ with gr.Blocks(title="Login") as ui:
57
+
58
+ # --- 上段: ログインフォーム ---
59
+ gr.Markdown("# 🔐 Habadashi Login")
60
+
61
+ with gr.Column(visible=True) as login_form:
62
+ email_input = gr.Textbox(label="Email")
63
+ pass_input = gr.Textbox(label="Password", type="password")
64
+ login_btn = gr.Button("Login", variant="primary")
65
+
66
+ status_msg = gr.Markdown("")
67
+
68
+ # Hidden textbox to store token and trigger cookie setting via JS
69
+ token_storage = gr.Textbox(visible=False, elem_id="token_storage")
70
+
71
+ # --- 中段: チャット初期化フォーム ---
72
+ gr.Markdown("### 🔧 チャット初期化パラメータ(任意)")
73
+ with gr.Row():
74
+ input_url = gr.Textbox(label="URL", placeholder="https://example.com", scale=3)
75
+ input_industry = gr.Textbox(label="業界 / カテゴリ", placeholder="EC、人材、金融など", scale=2)
76
+ input_cvr = gr.Number(label="CVR (%)", value=None, minimum=0, maximum=100, scale=1)
77
+ apply_btn = gr.Button("チャットに反映", variant="secondary")
78
+
79
+ # --- 下段: チャット ---
80
+ gr.Markdown("### 💬 広告改善提案チャット")
81
+ chat_frame = gr.HTML(build_chat_html({}))
82
+
83
+ # ページ読み込み時にURLパラメータでフォームとiframeを初期化
84
+ def on_load(request: gr.Request):
85
+ params = dict(request.query_params)
86
+ url_val = params.get("url", "")
87
+ industry_val = params.get("industry", "")
88
+ cvr_val = float(params["cvr"]) if params.get("cvr") else None
89
+ return url_val, industry_val, cvr_val, build_chat_html(params)
90
+
91
+ ui.load(on_load, inputs=None, outputs=[input_url, input_industry, input_cvr, chat_frame])
92
+
93
+ # フォーム変更時にiframeを動的更新
94
+ def update_chat(url, industry, cvr):
95
+ params = {}
96
+ if url:
97
+ params["url"] = url
98
+ if industry:
99
+ params["industry"] = industry
100
+ if cvr is not None:
101
+ params["cvr"] = cvr
102
+ return build_chat_html(params)
103
+
104
+ apply_btn.click(
105
+ update_chat,
106
+ inputs=[input_url, input_industry, input_cvr],
107
+ outputs=chat_frame,
108
+ )
109
+
110
+ # External login handler (from app.py) is bound here
111
+ login_btn.click(
112
+ handle_login_fn,
113
+ inputs=[email_input, pass_input],
114
+ outputs=[login_form, status_msg, token_storage],
115
+ )
116
+
117
+ # When token is set, use JavaScript to save cookie and redirect
118
+ token_storage.change(
119
+ None,
120
+ inputs=[token_storage],
121
+ js="""(token) => {
122
+ if (!token || token === "") return;
123
+
124
+ // Set cookie
125
+ document.cookie = `sb_access_token=${token}; path=/; max-age=3600; SameSite=None; Secure;`;
126
+
127
+ console.log("Cookie set, redirecting...");
128
+
129
+ // Wait briefly then redirect
130
+ setTimeout(() => {
131
+ window.location.href = '/app/';
132
+ }, 200);
133
+ }""",
134
+ )
135
+
136
+ return ui