File size: 11,233 Bytes
a73fa4e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
"""
FastAPIベースのセッション管理サーバー
HttpOnlyクッキーによるセキュアなセッション管理を提供
"""
from fastapi import FastAPI, Request, Response, HTTPException
from fastapi.middleware.cors import CORSMiddleware
import uuid
import json
import os
import time
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=["*"],
)

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")