Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 33 additions & 60 deletions backend/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,103 +5,76 @@
from dotenv import load_dotenv
import traceback

load_dotenv()
# Load environment variables
def get_database_url():
env_path = os.getenv("ENV_PATH")
if env_path:
print(f"[auth.py] Loading environment from: {env_path}")
load_dotenv(env_path)
return os.getenv("DATABASE_URL", "").replace("+asyncpg", "").replace("@db", "@localhost")
else:
print("[auth.py] Loading from default .env file")
load_dotenv(".env")
return os.getenv("DATABASE_URL", "").replace("+asyncpg", "")

DATABASE_URL = get_database_url()

# Ensure required environment variables are present
if not os.getenv("JWT_SECRET"):
raise ValueError("JWT_SECRET environment variable is not set")

SECRET_KEY = os.getenv("JWT_SECRET")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 * 30 # 30 days

def create_access_token(data: dict):
def create_jwt_token(data: dict):
"""Create a new JWT token with expiration"""
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
jwt_token = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
print(f"[auth.py] Created JWT token: {jwt_token[:20]}...")
return jwt_token

def create_access_token(data: dict):
"""Alias for create_jwt_token for backward compatibility"""
return create_jwt_token(data)

def verify_token(token: str):
"""Verify and decode a JWT token"""
print(f"[auth.py] Verifying token: {token[:20]}...")

try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
print(f"[auth.py] Token decoded successfully: {payload}")

if "exp" not in payload:
print("[auth.py] Token missing 'exp' field")
return None

# Check if token has expired
expiration_timestamp = payload["exp"]
try:
# Print both values for comparison
current_timestamp = datetime.utcnow().timestamp()
print(f"[auth.py] Current time: {current_timestamp}, Token expiration: {expiration_timestamp}")

# Check expiration
if current_timestamp > expiration_timestamp:
print("[auth.py] Token has expired")
return None

except Exception as e:
print(f"[auth.py] Error comparing timestamps: {str(e)}")
if datetime.utcnow() > datetime.fromtimestamp(payload["exp"]):
return None

return payload
except JWTError as e:
print(f"[auth.py] JWT verification error: {str(e)}")
print(f"JWT verification error: {str(e)}")
return None
except Exception as e:
print(f"[auth.py] Token verification error: {str(e)}")
print(traceback.format_exc())
print(f"Token verification error: {str(e)}")

return None

async def get_current_user(authorization: str = Header(...)):
"""Get current user from authorization header"""
try:
print(f"[auth.py] Authorization header: {authorization[:30]}...")

if not authorization or not authorization.startswith("Bearer "):
print(f"[auth.py] Invalid authorization header format: {authorization[:15]}...")
raise HTTPException(
status_code=401,
detail="Invalid authorization header format"
)


token = authorization.replace("Bearer ", "")
print(f"[auth.py] Processing token: {token[:20]}... (length: {len(token)})")

if not token:
print("[auth.py] Empty token after Bearer prefix")
raise HTTPException(
status_code=401,
detail="Empty token"
)

payload = verify_token(token)
if not payload:
print(f"[auth.py] Token verification failed for token: {token[:20]}...")

raise HTTPException(
status_code=401,
detail="Invalid or expired token"
)

# Verify essential fields
if "github_id" not in payload:
print(f"[auth.py] Missing required field 'github_id' in token payload: {payload}")
raise HTTPException(
status_code=401,
detail="Token missing required fields"
)

print(f"[auth.py] Authenticated user with GitHub ID: {payload.get('github_id')}")

return payload
except HTTPException:
# Re-raise HTTP exceptions
raise
except Exception as e:
print(f"[auth.py] Authentication error: {str(e)}")
print(traceback.format_exc())
raise HTTPException(
status_code=401,
detail=f"Authorization failed: {str(e)}"
)

18 changes: 8 additions & 10 deletions backend/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,18 @@
import os
import sys

load_dotenv()
# env load
env_path = os.getenv("ENV_PATH")
if env_path:
print(f"[database.py] Loading custom ENV_PATH: {env_path}")
load_dotenv(env_path)
else:
print("[database.py] Loading default .env/.env.railway")
load_dotenv(".env") # Railway default

DATABASE_URL = os.getenv("DATABASE_URL")
print(f"[database.py] Loaded DATABASE_URL: {DATABASE_URL}", flush=True)

# try:
# engine = create_async_engine(DATABASE_URL, future=True, echo=True)
# except Exception as e:
# print(f"[database.py] Failed to create engine: {e}", flush=True)
# raise

# SessionLocal = sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
# Base = declarative_base()

if "alembic" in sys.argv[0]:
engine = create_engine(DATABASE_URL.replace("+asyncpg", ""), future=True, echo=True)
SessionLocal = sessionmaker(bind=engine)
Expand Down
135 changes: 105 additions & 30 deletions backend/github_oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,55 +14,130 @@
logger.error(f"GITHUB_CLIENT_SECRET: {'present' if GITHUB_CLIENT_SECRET else 'missing'}")
raise ValueError("❌ GitHub OAuth credentials not found")

print(f"🔑 GitHub OAuth Configuration: Client ID: {GITHUB_CLIENT_ID[:5]}...")

async def exchange_code_for_token(code: str):
if not code:
raise HTTPException(status_code=400, detail="GitHub code is required")


"""Exchange GitHub OAuth code for token data"""
try:
async with httpx.AsyncClient() as client:
logger.info(f"Exchanging code for token with GitHub")
print(f"🚀 Exchanging code for token with GitHub...")
print(f"Using code: {code[:10]}...")

# Log credentials being used (partially obscured for security)
print(f"🔑 Using Client ID: {GITHUB_CLIENT_ID[:5]}...")
print(f"🔑 Using Client Secret: {GITHUB_CLIENT_SECRET[:5]}...")

response = await client.post(
"https://github.com/login/oauth/access_token",
headers={"Accept": "application/json"},
headers={
"Accept": "application/json"
},

data={
"client_id": GITHUB_CLIENT_ID,
"client_secret": GITHUB_CLIENT_SECRET,
"code": code
}
)
response.raise_for_status()
data = response.json()
logger.info("Successfully exchanged code for token")
return data
except httpx.HTTPError as e:
logger.error(f"HTTP error during token exchange: {str(e)}")
logger.error(f"Response status: {e.response.status_code if hasattr(e, 'response') else 'unknown'}")
logger.error(f"Response body: {e.response.text if hasattr(e, 'response') else 'unknown'}")
raise HTTPException(status_code=500, detail=f"GitHub API error: {str(e)}")

print(f"✅ GitHub token exchange status: {response.status_code}")
print(f"Response headers: {dict(response.headers)}")

try:
response_text = response.text
print(f"Raw response text: {response_text}")
data = response.json()
print(f"Parsed response data keys: {list(data.keys())}")
except Exception as e:
print(f"❌ Failed to parse JSON response: {response_text}")
raise Exception(f"Failed to parse GitHub response: {str(e)}")

if "error" in data:
error_description = data.get("error_description", "No description provided")
print(f"❌ GitHub OAuth error: {data['error']} - {error_description}")
raise Exception(f"GitHub OAuth error: {data['error']} - {error_description}")

if "access_token" not in data:
print("❌ Access token is missing from response")
print(f"Available fields in response: {list(data.keys())}")
raise Exception("No access token in GitHub response")

# Ensure token is properly stored and formatted
access_token = data.get("access_token", "").strip()
if not access_token:
print("❌ Access token is empty")
raise Exception("Empty access token received from GitHub")

# Create a new clean data object to avoid any issues
token_data = {
"access_token": access_token,
"token_type": data.get("token_type", "bearer"),
"scope": data.get("scope", "")
}

print(f"🔑 Successfully obtained token data with fields: {list(token_data.keys())}")
print(f"🔑 Access token value: {access_token[:10]}...")

# Directly log the full token JUST FOR DEBUGGING (would remove in production)
print(f"🔑 FULL TOKEN FOR DEBUG: {access_token}")

# Verify token data is correctly formed
if not token_data.get("access_token"):
print("⚠️ WARNING: access_token field is empty or missing in final token_data")
# Try once more to ensure it's set
token_data["access_token"] = access_token

return token_data

except Exception as e:
logger.exception("Error exchanging code for token:")
raise HTTPException(status_code=500, detail=str(e))
print(f"🔥 Error in token exchange: {str(e)}")
raise

async def get_user_info(access_token: str):
if not access_token:
raise HTTPException(status_code=400, detail="Access token is required")

"""Get GitHub user information using access token"""
try:
async with httpx.AsyncClient() as client:
logger.info("Fetching user info from GitHub")
print(f"👤 Fetching user info from GitHub...")
print(f"👤 Using token: {access_token[:10]}...")

# Try both authentication methods
headers = {
"Authorization": f"token {access_token}", # GitHub preferred format
"Accept": "application/vnd.github.v3+json",
"User-Agent": "LIT1337-App"
}

response = await client.get(
"https://api.github.com/user",
headers={"Authorization": f"Bearer {access_token}"}
headers=headers
)
response.raise_for_status()

if response.status_code != 200:
# Try alternative Bearer format if token format fails
print(f"First attempt failed with status {response.status_code}, trying Bearer format")
alt_headers = {
"Authorization": f"Bearer {access_token}", # OAuth standard
"Accept": "application/vnd.github.v3+json",
"User-Agent": "LIT1337-App"
}

response = await client.get(
"https://api.github.com/user",
headers=alt_headers
)

print(f"GitHub API response status: {response.status_code}")

if response.status_code != 200:
error_data = response.json()
print(f"❌ GitHub API error response: {error_data}")
raise Exception(f"GitHub API error: {response.status_code} - {error_data.get('message', 'Unknown error')}")

data = response.json()
logger.info("Successfully fetched user info")
print(f"✅ GitHub user info received for: {data.get('login')}")
return data
except httpx.HTTPError as e:
logger.error(f"HTTP error fetching user info: {str(e)}")
logger.error(f"Response status: {e.response.status_code if hasattr(e, 'response') else 'unknown'}")
logger.error(f"Response body: {e.response.text if hasattr(e, 'response') else 'unknown'}")
raise HTTPException(status_code=500, detail=f"GitHub API error: {str(e)}")

except Exception as e:
logger.exception("Error fetching user info:")
raise HTTPException(status_code=500, detail=str(e))
print(f"❌ Error getting user info: {str(e)}")
raise
Loading