mari-chat-3 / session_api_server.py
sirochild's picture
Upload session_api_server.py
84064db verified
"""
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)
@app.get("/")
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()
@app.post("/session/create")
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="セッション作成に失敗しました")
@app.get("/session/info")
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="セッション情報の取得に失敗しました")
@app.post("/session/validate")
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": "セッション検証に失敗しました"}
@app.delete("/session/delete")
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="セッション削除に失敗しました")
@app.get("/health")
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")