Spaces:
Runtime error
Runtime error
| """ | |
| FastAPIベースのセッション管理サーバー | |
| HttpOnlyクッキーによるセキュアなセッション管理を提供 | |
| """ | |
| from fastapi import FastAPI, Request, Response, HTTPException | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import RedirectResponse, JSONResponse | |
| import requests | |
| import uuid | |
| import json | |
| import os | |
| import time | |
| import secrets | |
| import base64 | |
| from huggingface_hub import attach_huggingface_oauth, parse_huggingface_oauth | |
| from datetime import datetime, timedelta | |
| from typing import Optional, Dict, Any | |
| import logging | |
| # ログ設定 | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| app = FastAPI(title="Mari Session API", version="1.0.0") | |
| # CORS設定(Streamlitからのアクセスを許可) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=[ | |
| "http://localhost:8501", | |
| "https://localhost:8501", | |
| "http://127.0.0.1:8501", | |
| "https://127.0.0.1:8501", | |
| "*" # Hugging Face Spacesでの実行を考慮 | |
| ], | |
| allow_credentials=True, | |
| allow_methods=["GET", "POST", "PUT", "DELETE"], | |
| allow_headers=["*"], | |
| ) | |
| attach_huggingface_oauth(app) | |
| def greet_json(request: Request): | |
| oauth_info = parse_huggingface_oauth(request) | |
| if oauth_info is None: | |
| return {"msg": "Not logged in!"} | |
| return {"msg": f"Hello, {oauth_info.user_info.preferred_username}!"} | |
| class SessionManager: | |
| """セッション管理クラス""" | |
| def __init__(self, storage_path: str = "session_data"): | |
| self.storage_path = storage_path | |
| self.cookie_name = "mari_session_id" | |
| self.session_duration_days = 7 | |
| self.cleanup_interval_hours = 24 | |
| # ストレージディレクトリを作成 | |
| os.makedirs(self.storage_path, exist_ok=True) | |
| # 最後のクリーンアップ時刻を記録するファイル | |
| self.cleanup_file = os.path.join(self.storage_path, "last_cleanup.json") | |
| def create_session(self) -> str: | |
| """新しいセッションを作成""" | |
| session_id = str(uuid.uuid4()) | |
| session_data = { | |
| 'session_id': session_id, | |
| 'created_at': datetime.now().isoformat(), | |
| 'last_access': datetime.now().isoformat(), | |
| 'user_data': {} | |
| } | |
| # セッションファイルに保存 | |
| session_file = os.path.join(self.storage_path, f"{session_id}.json") | |
| with open(session_file, 'w', encoding='utf-8') as f: | |
| json.dump(session_data, f, ensure_ascii=False, indent=2) | |
| logger.info(f"新規セッション作成: {session_id[:8]}...") | |
| return session_id | |
| def get_session(self, session_id: str) -> Optional[Dict[str, Any]]: | |
| """セッションデータを取得""" | |
| try: | |
| session_file = os.path.join(self.storage_path, f"{session_id}.json") | |
| if not os.path.exists(session_file): | |
| return None | |
| with open(session_file, 'r', encoding='utf-8') as f: | |
| session_data = json.load(f) | |
| # 期限チェック | |
| last_access = datetime.fromisoformat(session_data.get('last_access', '')) | |
| expiry_time = last_access + timedelta(days=self.session_duration_days) | |
| if datetime.now() > expiry_time: | |
| # 期限切れセッションを削除 | |
| os.remove(session_file) | |
| logger.info(f"期限切れセッション削除: {session_id[:8]}...") | |
| return None | |
| return session_data | |
| except Exception as e: | |
| logger.error(f"セッション取得エラー: {e}") | |
| return None | |
| def update_session_access(self, session_id: str) -> bool: | |
| """セッションの最終アクセス時刻を更新""" | |
| try: | |
| session_file = os.path.join(self.storage_path, f"{session_id}.json") | |
| if not os.path.exists(session_file): | |
| return False | |
| with open(session_file, 'r', encoding='utf-8') as f: | |
| session_data = json.load(f) | |
| session_data['last_access'] = datetime.now().isoformat() | |
| with open(session_file, 'w', encoding='utf-8') as f: | |
| json.dump(session_data, f, ensure_ascii=False, indent=2) | |
| return True | |
| except Exception as e: | |
| logger.error(f"セッションアクセス時刻更新エラー: {e}") | |
| return False | |
| def delete_session(self, session_id: str) -> bool: | |
| """セッションを削除""" | |
| try: | |
| session_file = os.path.join(self.storage_path, f"{session_id}.json") | |
| if os.path.exists(session_file): | |
| os.remove(session_file) | |
| logger.info(f"セッション削除: {session_id[:8]}...") | |
| return True | |
| return False | |
| except Exception as e: | |
| logger.error(f"セッション削除エラー: {e}") | |
| return False | |
| def cleanup_expired_sessions(self): | |
| """期限切れセッションのクリーンアップ""" | |
| try: | |
| current_time = datetime.now() | |
| cleaned_count = 0 | |
| for filename in os.listdir(self.storage_path): | |
| if filename.endswith('.json') and filename != 'last_cleanup.json': | |
| session_file = os.path.join(self.storage_path, filename) | |
| try: | |
| with open(session_file, 'r', encoding='utf-8') as f: | |
| session_data = json.load(f) | |
| last_access = datetime.fromisoformat(session_data.get('last_access', '')) | |
| expiry_time = last_access + timedelta(days=self.session_duration_days) | |
| if current_time > expiry_time: | |
| os.remove(session_file) | |
| cleaned_count += 1 | |
| except Exception as e: | |
| logger.warning(f"セッションファイル処理エラー {filename}: {e}") | |
| if cleaned_count > 0: | |
| logger.info(f"期限切れセッション {cleaned_count}件を削除") | |
| except Exception as e: | |
| logger.error(f"セッションクリーンアップエラー: {e}") | |
| # セッションマネージャーのインスタンス | |
| session_manager = SessionManager() | |
| async def create_session(response: Response): | |
| """新しいセッションを作成してCookieを設定""" | |
| try: | |
| # 期限切れセッションのクリーンアップ | |
| session_manager.cleanup_expired_sessions() | |
| # 新しいセッションを作成 | |
| session_id = session_manager.create_session() | |
| # HttpOnlyクッキーを設定(Hugging Face Spaces対応) | |
| is_production = os.getenv("SPACE_ID") is not None # Hugging Face Spacesの環境変数 | |
| response.set_cookie( | |
| key=session_manager.cookie_name, | |
| value=session_id, | |
| max_age=session_manager.session_duration_days * 24 * 60 * 60, # 7日間 | |
| httponly=True, | |
| secure=is_production, # 本番環境(HTTPS)でのみSecure属性を有効 | |
| samesite="lax" if is_production else "strict" # 本番環境では緩和 | |
| ) | |
| return { | |
| "status": "success", | |
| "session_id": session_id, | |
| "message": "セッションが作成されました" | |
| } | |
| except Exception as e: | |
| logger.error(f"セッション作成エラー: {e}") | |
| raise HTTPException(status_code=500, detail="セッション作成に失敗しました") | |
| async def get_session_info(request: Request): | |
| """現在のセッション情報を取得""" | |
| try: | |
| # CookieからセッションIDを取得 | |
| session_id = request.cookies.get(session_manager.cookie_name) | |
| if not session_id: | |
| raise HTTPException(status_code=401, detail="セッションが見つかりません") | |
| # セッションデータを取得 | |
| session_data = session_manager.get_session(session_id) | |
| if not session_data: | |
| raise HTTPException(status_code=401, detail="無効なセッションです") | |
| # アクセス時刻を更新 | |
| session_manager.update_session_access(session_id) | |
| return { | |
| "status": "success", | |
| "session_id": session_id, | |
| "created_at": session_data.get('created_at'), | |
| "last_access": session_data.get('last_access') | |
| } | |
| except HTTPException: | |
| raise | |
| except Exception as e: | |
| logger.error(f"セッション情報取得エラー: {e}") | |
| raise HTTPException(status_code=500, detail="セッション情報の取得に失敗しました") | |
| async def validate_session(request: Request): | |
| """セッションの有効性を検証""" | |
| try: | |
| # CookieからセッションIDを取得 | |
| session_id = request.cookies.get(session_manager.cookie_name) | |
| if not session_id: | |
| return {"valid": False, "message": "セッションが見つかりません"} | |
| # セッションデータを取得 | |
| session_data = session_manager.get_session(session_id) | |
| if not session_data: | |
| return {"valid": False, "message": "無効なセッションです"} | |
| # アクセス時刻を更新 | |
| session_manager.update_session_access(session_id) | |
| return { | |
| "valid": True, | |
| "session_id": session_id, | |
| "message": "有効なセッションです" | |
| } | |
| except Exception as e: | |
| logger.error(f"セッション検証エラー: {e}") | |
| return {"valid": False, "message": "セッション検証に失敗しました"} | |
| async def delete_session(request: Request, response: Response): | |
| """セッションを削除""" | |
| try: | |
| # CookieからセッションIDを取得 | |
| session_id = request.cookies.get(session_manager.cookie_name) | |
| if session_id: | |
| # セッションファイルを削除 | |
| session_manager.delete_session(session_id) | |
| # Cookieを削除(Hugging Face Spaces対応) | |
| is_production = os.getenv("SPACE_ID") is not None | |
| response.delete_cookie( | |
| key=session_manager.cookie_name, | |
| httponly=True, | |
| secure=is_production, | |
| samesite="lax" if is_production else "strict" | |
| ) | |
| return { | |
| "status": "success", | |
| "message": "セッションが削除されました" | |
| } | |
| except Exception as e: | |
| logger.error(f"セッション削除エラー: {e}") | |
| raise HTTPException(status_code=500, detail="セッション削除に失敗しました") | |
| async def health_check(): | |
| """ヘルスチェック""" | |
| return {"status": "healthy", "timestamp": datetime.now().isoformat()} | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="127.0.0.1", port=8000, log_level="info") |