11import json
22import os
33import asyncio
4+ import logging
45from fastapi import FastAPI , Depends , HTTPException , Response , Request
56from fastapi .responses import RedirectResponse , JSONResponse
67from fastapi .middleware .cors import CORSMiddleware
2930OLLAMA_FALLBACK_MODELS = [
3031 m .strip () for m in os .getenv ("OLLAMA_FALLBACK_MODELS" , "" ).split ("," ) if m .strip ()
3132]
32-
33+ FRONTEND_URL = os .getenv ("FRONTEND_URL" , "http://localhost:5173" )
34+ SESSION_TTL_SECONDS = int (os .getenv ("SESSION_TTL_SECONDS" , "3600" ))
35+ COOKIE_SECURE = APP_ENV != "development"
36+ COOKIE_SAMESITE = "Lax" if APP_ENV == "development" else "Strict"
3337app = FastAPI ()
38+ logger = logging .getLogger ("codescribe" )
39+ if not logger .handlers :
40+ logging .basicConfig (level = logging .INFO , format = "%(asctime)s %(levelname)s %(message)s" )
3441
3542# ---- Session persistence ----
3643SESSIONS_FILE = "sessions.json"
@@ -86,6 +93,15 @@ def save_sessions():
8693 allow_headers = ["*" ]
8794)
8895
96+ @app .middleware ("http" )
97+ async def add_request_context (request : Request , call_next ):
98+ req_id = request .headers .get ("x-request-id" ) or str (uuid4 ())
99+ start = time .perf_counter ()
100+ response = await call_next (request )
101+ elapsed_ms = int ((time .perf_counter () - start ) * 1000 )
102+ response .headers ["X-Request-ID" ] = req_id
103+ logger .info ("%s %s -> %s (%sms) req_id=%s" , request .method , request .url .path , response .status_code , elapsed_ms , req_id )
104+ return response
89105# ---- Env vars ----
90106GITHUB_CLIENT_ID = os .getenv ("GITHUB_CLIENT_ID" )
91107GITHUB_CLIENT_SECRET = os .getenv ("GITHUB_CLIENT_SECRET" )
@@ -99,6 +115,20 @@ def ensure_github_oauth_config():
99115 )
100116
101117
118+ def validate_oauth_state (request : Request , state : Optional [str ]):
119+ if not state :
120+ raise HTTPException (status_code = 400 , detail = "Missing state parameter" )
121+ signed_state = request .cookies .get ("oauth_state" )
122+ if not signed_state :
123+ raise HTTPException (status_code = 400 , detail = "Missing OAuth state cookie" )
124+ try :
125+ expected_state = serializer .loads (signed_state ).get ("state" )
126+ except Exception :
127+ raise HTTPException (status_code = 400 , detail = "Invalid OAuth state cookie" )
128+ if expected_state != state :
129+ raise HTTPException (status_code = 400 , detail = "OAuth state mismatch" )
130+
131+
102132# ---------- NEW: tiny in-memory TTL cache for GitHub responses ----------
103133from functools import lru_cache
104134from collections import defaultdict
@@ -203,13 +233,24 @@ async def test_auth(user=Depends(get_current_user)):
203233async def github_login ():
204234 ensure_github_oauth_config ()
205235 state = os .urandom (16 ).hex ()
206- return RedirectResponse (
236+ response = RedirectResponse (
207237 f"https://github.com/login/oauth/authorize?"
208238 f"client_id={ GITHUB_CLIENT_ID } &state={ state } &scope=repo,user"
209239 )
240+ response .set_cookie (
241+ key = "oauth_state" ,
242+ value = serializer .dumps ({"state" : state }),
243+ httponly = True ,
244+ samesite = COOKIE_SAMESITE ,
245+ secure = COOKIE_SECURE ,
246+ max_age = 300 ,
247+ )
248+ return response
210249
211250@app .get ("/auth/github/callback" )
212- async def github_callback (code : str , state : Optional [str ] = None ):
251+ async def github_callback (request : Request , code : str , state : Optional [str ] = None ):
252+ ensure_github_oauth_config ()
253+ validate_oauth_state (request , state )
213254 ensure_github_oauth_config ()
214255 if not state :
215256 raise HTTPException (status_code = 400 , detail = "Missing state parameter" )
@@ -235,19 +276,21 @@ async def github_callback(code: str, state: Optional[str] = None):
235276 "access_token" : token_data ["access_token" ],
236277 "user" : user_data ["login" ],
237278 "user_id" : user_data ["id" ],
238- "expires" : time .time () + 3600
279+ "expires" : time .time () + SESSION_TTL_SECONDS
239280 }
240281 save_sessions ()
241282
242283 signed_cookie = serializer .dumps ({"session_id" : session_id })
243- response = RedirectResponse (url = "http://localhost:5173" )
284+ response = RedirectResponse (url = FRONTEND_URL )
244285 response .set_cookie (
245286 key = "session_id" ,
246287 value = signed_cookie ,
247288 httponly = True ,
248- samesite = "Lax" ,
249- secure = False ,
289+ samesite = COOKIE_SAMESITE ,
290+ secure = COOKIE_SECURE ,
291+ max_age = SESSION_TTL_SECONDS ,
250292 )
293+ response .delete_cookie ("oauth_state" , samesite = COOKIE_SAMESITE , secure = COOKIE_SECURE )
251294 return response
252295
253296async def get_repo_default_branch (owner : str , repo : str ) -> str :
@@ -872,7 +915,7 @@ async def logout(request: Request, user=Depends(get_current_user)):
872915
873916 # Clear cookie
874917 response = JSONResponse ({"message" : "Logged out" })
875- response .delete_cookie ("session_id" )
918+ response .delete_cookie ("session_id" , samesite = COOKIE_SAMESITE , secure = COOKIE_SECURE )
876919 return response
877920
878921@app .get ("/repos/{owner}/{repo}/file-content" )
@@ -1000,3 +1043,13 @@ async def get_task_status(task_id: str):
10001043
10011044
10021045
1046+
1047+
1048+
1049+
1050+
1051+
1052+
1053+
1054+
1055+
0 commit comments