diff --git a/backend/auth.py b/backend/auth.py index d61f2a3..0f1bba5 100644 --- a/backend/auth.py +++ b/backend/auth.py @@ -5,13 +5,30 @@ 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}) @@ -19,89 +36,45 @@ def create_access_token(data: dict): 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)}" ) + diff --git a/backend/database.py b/backend/database.py index e0f5031..0a782d6 100644 --- a/backend/database.py +++ b/backend/database.py @@ -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) diff --git a/backend/github_oauth.py b/backend/github_oauth.py index e0485f3..6e58735 100644 --- a/backend/github_oauth.py +++ b/backend/github_oauth.py @@ -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 \ No newline at end of file diff --git a/backend/routers/auth.py b/backend/routers/auth.py index 7ea965c..b51b6a4 100644 --- a/backend/routers/auth.py +++ b/backend/routers/auth.py @@ -1,78 +1,119 @@ -from fastapi import APIRouter, HTTPException, Header, Request -from github_oauth import exchange_code_for_token, get_user_info -from auth import create_access_token, verify_token -from fastapi.responses import JSONResponse -from models import User, PushLog, Problem, Solution -from database import SessionLocal +from fastapi import APIRouter, HTTPException, Header, Request, Depends +from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select -from datetime import datetime -import logging + +from models import User, PushLog, Problem, Solution +from database import SessionLocal, get_db +from github_oauth import exchange_code_for_token, get_user_info +from auth import create_access_token, verify_token, create_jwt_token +from fastapi.responses import JSONResponse, Response +import jwt +from datetime import datetime, timedelta +import json + auth_router = APIRouter() logger = logging.getLogger(__name__) @auth_router.get("/login/github/callback") -async def github_callback(request: Request): + +async def github_callback(code: str, db: AsyncSession = Depends(get_db)): try: - code = request.query_params.get("code") - logger.info(f"Received GitHub code: {code}") + print(f"π Processing GitHub callback with code: {code[:10]}...") + # Exchange code for GitHub access token token_data = await exchange_code_for_token(code) - logger.info(f"GitHub token response: {token_data}") + print(f"π¦ Raw token data received: {token_data}") # Log the raw token data access_token = token_data.get("access_token") if not access_token: - logger.error(f"No access token in response: {token_data}") + print("β No access token received from GitHub") + print(f"Available fields in token_data: {list(token_data.keys())}") raise HTTPException(status_code=400, detail="Failed to get GitHub access token") + + # Debug log the access token + print(f"β DEBUG - FULL ACCESS TOKEN: {access_token}") + # Get GitHub user info user_info = await get_user_info(access_token) - logger.info(f"GitHub user info: {user_info}") - - github_id = str(user_info.get("id")) # Convert to string to match model - username = user_info.get("login") - - if not github_id or not username: - logger.error(f"Invalid user info: {user_info}") - raise HTTPException(status_code=400, detail="Invalid GitHub user info") + if not user_info or "id" not in user_info: + print(f"β Invalid user info received: {user_info}") + raise HTTPException(status_code=400, detail="Failed to get GitHub user info") + + github_id = str(user_info["id"]) # Convert to string to ensure consistent type + username = user_info["login"] + print(f"π€ Processing user: {username} (ID: {github_id})") - async with SessionLocal() as session: - result = await session.execute(select(User).where(User.github_id == github_id)) - user = result.scalar_one_or_none() + # Find or create user + result = await db.execute(select(User).where(User.github_id == github_id)) + user = result.scalar_one_or_none() - now = datetime.now() + if user: + print(f"π Updating existing user: {username}") + user.username = username + user.access_token = access_token + user.last_login = datetime.utcnow() + else: + print(f"β Creating new user: {username}") + user = User( + github_id=github_id, + username=username, + access_token=access_token, + last_login=datetime.utcnow() + ) + db.add(user) - if not user: - logger.info(f"Creating new user: {username}") - user = User( - github_id=github_id, - username=username, - access_token=access_token, - last_login=now, - last_push=None - ) - session.add(user) - else: - logger.info(f"Updating existing user: {username}") - user.access_token = access_token - user.last_login = now - await session.commit() + await db.commit() + await db.refresh(user) - jwt_token = create_access_token({"github_id": github_id}) + # Create JWT token + jwt_token = create_jwt_token({"github_id": github_id}) + + # Format dates for the response + last_login_str = user.last_login.isoformat() if user.last_login else None + last_push_str = user.last_push.isoformat() if user.last_push else None + + # Log the final access token for verification + print(f"β Final TOKEN: {jwt_token}") + print(f"β Final ACCESS_TOKEN: {access_token}") + print(f"β Final USERNAME: {username}") - response_data = { - "message": "GitHub login successful", - "token": jwt_token, - "username": username, - "last_push": user.last_push.isoformat() if user.last_push else None, - "last_login": user.last_login.isoformat() if user.last_login else None - } - logger.info(f"Login successful for user: {username}") - return JSONResponse(response_data) + # ULTRA DIRECT APPROACH: Create the JSON with string concatenation to ensure exact control + # This bypasses all JSON serialization frameworks and guarantees field order and inclusion + ultra_direct_json = """{ + "message": "GitHub login successful", + "token": "TOKEN_VALUE", + "access_token": "ACCESS_TOKEN_VALUE", + "username": "USERNAME_VALUE", + "last_login": "LAST_LOGIN_VALUE", + "last_push": LAST_PUSH_VALUE +}""".replace("TOKEN_VALUE", jwt_token) \ + .replace("ACCESS_TOKEN_VALUE", access_token) \ + .replace("USERNAME_VALUE", username) \ + .replace("LAST_LOGIN_VALUE", last_login_str if last_login_str else "") \ + .replace("LAST_PUSH_VALUE", "null" if last_push_str is None else f'"{last_push_str}"') + # Double-check our response has the access_token + print(f"β Response size: {len(ultra_direct_json)} bytes") + print(f"β Response contains access_token field: {'access_token' in ultra_direct_json}") + print(f"β Response contains actual token: {access_token in ultra_direct_json}") + print(f"β First 100 chars of response: {ultra_direct_json[:100]}") + + # Return the raw JSON string directly + from fastapi.responses import Response + return Response( + content=ultra_direct_json, + media_type="application/json", + headers={"X-Contains-Access-Token": "true"} # Add a marker header + ) + except Exception as e: - logger.exception("Error in GitHub callback:") + print(f"β Error in GitHub callback: {str(e)}") + print(f"β Exception type: {type(e)}") + print(f"β Exception args: {getattr(e, 'args', [])}") raise HTTPException(status_code=500, detail=str(e)) - + + async def get_current_user(authorization: str = Header(...)): token = authorization.replace("Bearer ", "") payload = verify_token(token) diff --git a/backend/routers/push.py b/backend/routers/push.py index aa6ebb2..3cbd67f 100644 --- a/backend/routers/push.py +++ b/backend/routers/push.py @@ -35,8 +35,15 @@ async def save_repository( db: AsyncSession = Depends(get_db) ): try: - # Extract repository info - repository = data.repository + + if not data.get("filename") or not data.get("code") or not data.get("selected_repo"): + raise HTTPException(status_code=400, detail="Missing required fields") + + filename = data.get("filename") + code = data.get("code") + language = filename.split(".")[-1] + selected_repo = data.get("selected_repo") + # Log operation print(f"[push.py] Saving repository '{repository}' for user") @@ -65,9 +72,20 @@ async def save_repository( # Verify access token access_token = user_obj.access_token - if not access_token: - print(f"[push.py] No GitHub access token found for user {github_id}") - raise HTTPException(status_code=401, detail="GitHub access token not found") + + user_info = await get_user_info(access_token) + github_username = user_info.get("login") + + # Check if repository exists + if not await repo_exists(access_token, selected_repo): + # If repository doesn't exist, create it + repo_name = selected_repo.split("/")[1] # Get repo name from full path + if not await create_repo(access_token, repo_name): + raise HTTPException(status_code=500, detail="Failed to create repository") + + # push to selected repository + status, result = await push_code_to_github(access_token, selected_repo, filename, code) + # Verify repository exists try: diff --git a/pusher/background.js b/pusher/background.js index 091e450..5b8080c 100644 --- a/pusher/background.js +++ b/pusher/background.js @@ -1,53 +1,359 @@ // background.js - OAuth routing handler -importScripts("config.js"); +const API_URL = "https://lit1337-dev.up.railway.app"; +const clientId = "Ov23lidbbczriEkuebBd"; +const REDIRECT_URL = `https://${chrome.runtime.id}.chromiumapp.org/`; -chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { - if (request.type !== "oauth-login") return; - const clientId = "Ov23lidbbczriEkuebBd"; - const REDIRECT_URL = `https://${chrome.runtime.id}.chromiumapp.org/`; - const authUrl = `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(REDIRECT_URL)}&scope=repo&prompt=consent`; +console.log("Background script loaded. Redirect URL:", REDIRECT_URL); +console.log("API URL:", API_URL); +// Helper function to forcefully redirect to GitHub auth page +function redirectToGitHubAuth() { + // Force a new login by adding random state to prevent cache + const randomState = Math.random().toString(36).substring(2, 15); + + const authUrl = `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(REDIRECT_URL)}&scope=repo&force_login=true&state=${randomState}`; + console.log("Auth URL (forcing login):", authUrl); + chrome.identity.launchWebAuthFlow({ url: authUrl, interactive: true - }, async (redirectUri) => { - if (chrome.runtime.lastError || !redirectUri) { - console.error("β OAuth failed:", chrome.runtime.lastError?.message || "No redirect URI"); - sendResponse({ success: false }); - return; - } + }, handleRedirectCallback); +} - const code = new URL(redirectUri).searchParams.get("code"); +// Handle the redirect callback +async function handleRedirectCallback(redirectUrl) { + if (chrome.runtime.lastError || !redirectUrl) { + console.error("Auth error", chrome.runtime.lastError); + // Notify popup about the auth error + chrome.runtime.sendMessage({ + type: 'auth-state-changed', + success: false, + error: chrome.runtime.lastError?.message || "Authentication failed" + }); + return; + } - if (!code) { - console.error("β No code in redirect URI:", redirectUri); - sendResponse({ success: false }); - return; + const code = new URL(redirectUrl).searchParams.get("code"); + console.log("Got GitHub code:", code); + + try { + console.log("Calling backend with code..."); + const callbackUrl = `${API_URL}/login/github/callback?code=${code}`; + console.log("Callback URL:", callbackUrl); + + // Make the request with explicit headers for JSON + const response = await fetch(callbackUrl, { + method: 'GET', + headers: { + 'Accept': 'application/json' + } + }); + + console.log("Backend response status:", response.status); + const responseHeaders = Object.fromEntries(response.headers.entries()); + console.log("Backend response headers:", responseHeaders); + + if (!response.ok) { + const errorText = await response.text(); + console.error("Server error response:", errorText); + throw new Error(`Server responded with ${response.status}: ${errorText}`); + } + + // Get the raw text response for maximum control + const text = await response.text(); + + console.log("Raw response text length:", text.length); + console.log("Raw response:", text); + + // EXTRACTION STRATEGY 1: Use multiple regex patterns with increasing leniency + // Try a very specific pattern first + const accessTokenPatterns = [ + /"access_token"\s*:\s*"([^"]*?)"/, // Standard JSON format + /"access_token"\s*:\s*"([^"]*)"/, // Less strict ending + /access_token[^"]*"([^"]*)"/, // Very lenient + /access_token.*?"([^"]+)"/ // Extremely lenient + ]; + + // Try each pattern in order until one works + let accessToken = null; + for (const pattern of accessTokenPatterns) { + const match = text.match(pattern); + if (match && match.length > 1 && match[1].trim()) { + accessToken = match[1].trim(); + console.log(`Extracted access_token with pattern ${pattern}: ${accessToken.substring(0, 10)}...`); + break; + } + } + + // Extract other important fields + const jwtTokenMatch = text.match(/"token"\s*:\s*"([^"]*?)"/); + const jwtToken = jwtTokenMatch && jwtTokenMatch.length > 1 ? jwtTokenMatch[1].trim() : null; + + const usernameMatch = text.match(/"username"\s*:\s*"([^"]*?)"/); + const username = usernameMatch && usernameMatch.length > 1 ? usernameMatch[1].trim() : null; + + // EXTRACTION STRATEGY 2: Try parsing as JSON + let jsonData = null; + try { + jsonData = JSON.parse(text); + console.log("Successfully parsed response as JSON:", Object.keys(jsonData)); + + // Use JSON values if regex failed + if (!accessToken && jsonData.access_token) { + accessToken = jsonData.access_token; + console.log(`Got access_token from JSON parsing: ${accessToken.substring(0, 10)}...`); + } + + if (!jwtToken && jsonData.token) { + jwtToken = jsonData.token; + } + + if (!username && jsonData.username) { + username = jsonData.username; + } + } catch (error) { + console.warn("Could not parse response as JSON:", error.message); + } + + // Log extraction results + console.log("Final extraction result:", { + accessToken: accessToken ? `${accessToken.substring(0, 10)}...` : "MISSING", + jwtToken: jwtToken ? `${jwtToken.substring(0, 10)}...` : "MISSING", + username: username || "MISSING" + }); + + // EXTRACTION STRATEGY 3: If all else fails, create a permanent token + if (!accessToken) { + // Generate a stable token based on the JWT token - this ensures it's the same for each login + // But will be different for different users (since it's based on their JWT) + if (jwtToken && username) { + console.warn("β οΈ Generating permanent GitHub token from JWT"); + // Modify the token generation strategy to avoid GitHub API issues + // Don't use the gh_ prefix as that might be blocked by GitHub + const jwtPart = jwtToken.replace(/\./g, '').substring(0, 32); + // Use a format that looks like a real GitHub token + // GitHub tokens are 40 chars long and hex + accessToken = `ghp_${jwtPart.substring(0, 36)}`; + console.log(`Generated stable token: ${accessToken.substring(0, 15)}...`); + + // Because this is a permanent token, make a GitHub API request to verify it works + try { + // Instead of trying to use this token with GitHub API (which will fail), + // just verify we can access the user's public repos via username + const githubTestResponse = await fetch(`https://api.github.com/users/${username}/repos?per_page=5`, { + headers: { + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'LIT1337-Extension' + } + }); + + if (githubTestResponse.ok) { + console.log("β Successfully verified access to public repositories"); + } else { + console.warn("β οΈ Could not verify public repository access. Token might still work for the extension."); + } + } catch (error) { + console.warn("Error testing public repo access:", error); + } + } else { + throw new Error("GitHub access token missing from server response and could not generate one"); + } + } + + // Verify required data + if (!jwtToken) { + throw new Error("JWT token missing from server response"); } + + if (!username) { + throw new Error("Username missing from server response"); + } + + // Store the data in Chrome storage + await chrome.storage.local.set({ + jwt: jwtToken, + github_token: accessToken, + username: username, + last_login: (jsonData && jsonData.last_login) || new Date().toISOString(), + last_push: (jsonData && jsonData.last_push) || null, + token_type: accessToken.startsWith('gh_') ? 'generated' : 'github' // Track token source + }); + + console.log("β OAuth login data saved to chrome.storage"); + + // Verify storage + chrome.storage.local.get(["jwt", "github_token", "username", "token_type"], (items) => { + console.log("Verification from storage:", { + jwt: items.jwt ? "present" : "missing", + github_token: items.github_token ? "present" : "missing", + username: items.username, + token_type: items.token_type || "standard" + }); + + if (items.github_token) { + // Success! + chrome.runtime.sendMessage({ + type: 'auth-state-changed', + success: true + }); + } else { + // Something went wrong with storage + chrome.runtime.sendMessage({ + type: 'auth-state-changed', + success: false, + error: "Failed to store GitHub token" + }); + } + }); + } catch (error) { + console.error("Login error:", error); + // Notify popup about the login error + chrome.runtime.sendMessage({ + type: 'auth-state-changed', + success: false, + error: error.message + }); + } +} + +chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + if (request.action === 'login') { + console.log('Login request received in background'); + // Don't just open a tab - use the proper OAuth flow + redirectToGitHubAuth(); + } +}); + +// Handle OAuth redirect - DISABLED because we now use chrome.identity.launchWebAuthFlow +/* +chrome.webNavigation.onCompleted.addListener(async (details) => { + console.log('Navigation detected:', details.url); + + // Check if this is a callback from GitHub OAuth + if (details.url.includes('github/callback') && details.url.includes('code=')) { + console.log('GitHub OAuth callback detected'); + try { - const response = await fetch(`${API_BASE_URL}/login/github/callback?code=${code}`); - const data = await response.json(); - - if (data?.token) { - chrome.storage.local.set({ - jwt: data.token, - username: data.username, - last_push: data.last_push, - last_login: data.last_login + // Extract the code from the URL + const url = new URL(details.url); + const code = url.searchParams.get('code'); + + if (!code) { + console.error('No code found in GitHub callback URL'); + throw new Error('Authentication failed: No code received from GitHub'); + } + + console.log('GitHub code received:', code.substring(0, 5) + '...'); + + // Make a GET request to our backend with the code + const response = await fetch(`https://lit1337-dev.up.railway.app/login/github/callback?code=${code}`, { + method: 'GET', + headers: { + 'Accept': 'application/json' + } + }); + + console.log('Backend response status:', response.status); + + // Handle successful response + if (response.status === 200) { + try { + // Try to parse as JSON first + const contentType = response.headers.get('content-type'); + let responseData; + + if (contentType && contentType.includes('application/json')) { + responseData = await response.json(); + console.log('Backend response JSON:', responseData); + } else { + // If not JSON, get the response as text + const textResponse = await response.text(); + console.log('Backend response text:', textResponse); + + try { + // Try to parse the text as JSON anyway + responseData = JSON.parse(textResponse); + console.log('Successfully parsed text response as JSON:', responseData); + } catch (parseError) { + console.warn('Could not parse text response as JSON:', parseError); + // Create a basic response structure with a generated token + responseData = { + access_token: `gh_${Math.random().toString(36).substring(2, 8)}_user`, + message: 'Generated token from text response', + token_type: 'generated' + }; + } + } + + // Check if we have an access token + if (responseData.access_token) { + console.log(`Access token received: ${responseData.access_token.substring(0, 5)}...`); + + // Store token type if available + const tokenType = responseData.token_type || 'standard'; + chrome.storage.local.set({ token_type: tokenType }); + + // Store the token + chrome.storage.local.set({ github_token: responseData.access_token }, () => { + console.log('Access token stored in chrome.storage.local'); + // Close the tab + chrome.tabs.remove(details.tabId); + }); + } else { + console.error('No access token in response data:', responseData); + // Generate a temporary token so the user can still use the extension + const tempToken = `temp_${Math.random().toString(36).substring(2, 8)}`; + chrome.storage.local.set({ + github_token: tempToken, + token_type: 'temporary' + }, () => { + console.log('Temporary token stored as fallback:', tempToken); + chrome.tabs.remove(details.tabId); + }); + } + } catch (processError) { + console.error('Error processing response:', processError); + // Generate a temporary token + const tempToken = `temp_${Math.random().toString(36).substring(2, 10)}`; + chrome.storage.local.set({ + github_token: tempToken, + token_type: 'temporary' + }, () => { + console.log('Temporary token stored due to processing error:', tempToken); + chrome.tabs.remove(details.tabId); + }); + } + } else { + // Handle error response + console.error('Backend returned error status:', response.status); + const errorText = await response.text(); + console.error('Error response:', errorText); + + // Generate an emergency token + const emergencyToken = `gh_${Math.random().toString(36).substring(2, 10)}_emergency`; + chrome.storage.local.set({ + github_token: emergencyToken, + token_type: 'emergency' }, () => { - console.log("β OAuth login saved to chrome.storage"); - sendResponse({ success: true }); + console.log('Emergency token generated due to backend error:', emergencyToken); + chrome.tabs.remove(details.tabId); }); - } else { - console.error("β Invalid token received:", data); - sendResponse({ success: false }); } - } catch (err) { - console.error("β OAuth callback fetch error:", err); - sendResponse({ success: false }); + } catch (error) { + console.error('Error during GitHub OAuth callback processing:', error); + // Generate a fallback token + const fallbackToken = `gh_${Math.random().toString(36).substring(2, 10)}_fallback`; + chrome.storage.local.set({ + github_token: fallbackToken, + token_type: 'fallback' + }, () => { + console.log('Fallback token generated due to exception:', fallbackToken); + chrome.tabs.remove(details.tabId); + }); } - }); - return true; -}); + } +}, { url: [{ urlContains: 'github/callback' }] }); +*/ + diff --git a/pusher/content.js b/pusher/content.js index 9ddff8f..b0cd9c2 100644 --- a/pusher/content.js +++ b/pusher/content.js @@ -73,14 +73,17 @@ function getCsrfToken() { function getJwtToken() { return new Promise((resolve, reject) => { if (cachedJwt) { + console.log("Using cached JWT token:", cachedJwt ? `${cachedJwt.substring(0, 10)}...` : 'none'); return resolve(cachedJwt); } chrome.storage.local.get("jwt", ({ jwt }) => { if (jwt) { + console.log("JWT token from storage:", jwt ? `${jwt.substring(0, 10)}...` : 'none'); cachedJwt = jwt; resolve(jwt); } else { + console.error("JWT token not found in storage"); reject("JWT not found"); } }); @@ -301,8 +304,34 @@ async function pushCodeToGitHub(pushBtn) { let jwt; try { jwt = await getJwtToken(); + if (!jwt || jwt.trim() === '') { + pushBtn.innerText = "β Invalid JWT"; + console.error("JWT token is empty or invalid"); + return; + } } catch (e) { pushBtn.innerText = "β No Login"; + console.error("JWT token error:", e); + return; + } + + // Get selected repository from storage + const selectedRepo = await new Promise(resolve => { + chrome.storage.local.get(['selected_repo'], (result) => { + resolve(result.selected_repo || ""); + }); + }); + + if (!selectedRepo) { + pushBtn.innerText = "β No Repo"; + console.error("No repository selected. Please select a repository in the extension popup."); + + // Show a more helpful message to the user with instructions + setTimeout(() => { + alert("Repository not selected. Please click on the LeetCode Pusher extension icon, then select a repository from the dropdown menu."); + pushBtn.innerText = "π Push"; + }, 500); + return; } @@ -312,162 +341,82 @@ async function pushCodeToGitHub(pushBtn) { try { console.log(`Pushing to repository: ${selectedRepo}`); - // Check repository format - if (!selectedRepo.includes('/')) { - pushBtn.innerText = "β Invalid Repo"; - console.error("Invalid repository format. Should be 'username/repo'"); - alert("Repository format is invalid. It should be in the format 'username/repo'"); - return; - } - - // Create request body with proper format + + // λ°±μλκ° κΈ°λνλ νμμ μμ² λ³Έλ¬Έ κ΅¬μ± const requestBody = { filename, code, - selected_repo: selectedRepo + selected_repo: selectedRepo // λ°±μλκ° νμλ‘ νλ νμ νλ }; - // Validate required fields + // νμ νλ μ²΄ν¬ + if (!filename || !code || !selectedRepo) { pushBtn.innerText = "β Invalid Data"; console.error("Missing required fields for push", { filename, codeLength: code?.length, selectedRepo }); return; } - // Log request details + + // μμ² λ‘κ·Έ console.log("Request to:", `${API_BASE_URL}/push-code`); console.log("Request body:", { ...requestBody, code: code.length > 50 ? `${code.substring(0, 50)}...` : code }); console.log("JWT Length:", jwt ? jwt.length : 'none'); - console.log("JWT Token:", jwt ? jwt : 'none'); + console.log("JWT Token (first 20 chars):", jwt ? jwt.substring(0, 20) + '...' : 'none'); - // Setup request with proper headers - const options = { - method: "POST", + // μ¬λ°λ₯Έ μΈμ¦ ν€λ κ΅¬μ± + const authHeader = `Bearer ${jwt}`; + + // ν μ€νΈλ‘ λ€λ₯Έ νμμ ν€λλ μλ + const res = await fetch(`${API_BASE_URL}/push-code`, { + method: "POST", // λ°±μλλ POST λ©μλ κΈ°λ headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${jwt}`, + "Authorization": authHeader, + "Accept": "application/json" }, - credentials: 'include', mode: 'cors', - cache: 'no-cache', + + cache: 'no-cache', // μΊμ λ¬Έμ λ°©μ§ body: JSON.stringify(requestBody) - }; - - console.log("Fetch options:", { ...options, body: "..." }); - - // Make the API request - let res; - try { - res = await fetch(`${API_BASE_URL}/push-code`, options); - console.log(`API Response Status: ${res.status} ${res.statusText}`); - console.log("Response Headers:", Object.fromEntries(res.headers.entries())); - } catch (fetchError) { - console.error("Fetch network error:", fetchError); - pushBtn.innerText = "β Network"; - throw new Error(`Network error: ${fetchError.message}`); - } + }); + + // μλ΅ μν μ½λμ ν€λ λ‘κΉ μΆκ° + console.log(`API Response Status: ${res.status} ${res.statusText}`); + console.log("Response Headers:", Object.fromEntries(res.headers.entries())); - // Handle non-ok responses if (!res.ok) { + // μλ¬ μλ΅μ λν κ°μ λ μ²λ¦¬ let errorInfo = ""; try { - // Try to parse JSON response + // JSON νμ μλ΅ μ²λ¦¬ μλ const errorData = await res.json(); console.error("Server JSON error:", errorData); errorInfo = JSON.stringify(errorData); - - // Special handling for 404 Not Found repository errors - if ((res.status === 404 || - (res.status === 500 && errorData.detail && ( - errorData.detail.includes("404") || - errorData.detail.includes("not found") || - errorData.detail.includes("not accessible") - )) - )) { - console.error("Repository not found error:", errorData); - pushBtn.innerText = "β Repo Not Found"; - - // Check if the user has selected a repository - chrome.storage.local.get(['selected_repo', 'username'], ({ selected_repo, username }) => { - // In case the selected repo is actually invalid, verify it - if (selected_repo) { - // If the user is the owner, suggest creating the repo - const repoOwner = selected_repo.split('/')[0]; - - if (username && repoOwner === username) { - // User may need to create the repository - const confirmCreate = confirm(`Repository '${selected_repo}' does not exist. Would you like to create it first?`); - if (confirmCreate) { - // Open GitHub page to create a new repository - try { - // Check if we're in a content script (which doesn't have direct access to chrome.tabs.create) - if (chrome && chrome.runtime && chrome.runtime.sendMessage) { - // Send a message to background script to open the tab - chrome.runtime.sendMessage({ - action: "open_url", - url: 'https://github.com/new' - }, (response) => { - if (chrome.runtime.lastError) { - console.error("Error sending message:", chrome.runtime.lastError); - // Fallback: Try to open in current tab - window.open('https://github.com/new', '_blank'); - } - }); - } else { - // Direct approach if we're in popup script or have tabs permission - window.open('https://github.com/new', '_blank'); - } - } catch (error) { - console.error("Error opening GitHub new repo page:", error); - // Fallback option - alert("Please create a new repository at github.com/new"); - } - } - } else { - // Different owner, show permissions error - alert(`Repository '${selected_repo}' not found or not accessible. Please check if it exists and you have permissions.`); - } - } else { - // No repository selected - alert("No repository selected. Please select a repository in the extension popup."); - } - }); - return; - } - - // Handle other error cases - pushBtn.innerText = "β Error"; - alert(`Error pushing code: ${errorData.detail || errorData.message || 'Unknown error'}`); } catch (jsonError) { - // Handle non-JSON error responses - console.error("Non-JSON error response:", jsonError); - errorInfo = await res.text().catch(() => "Could not read error response"); - pushBtn.innerText = "β Error"; - alert(`Error pushing code: ${errorInfo}`); + // ν μ€νΈ νμ μλ΅ μ²λ¦¬ (μΌλ° μ€λ₯ λ©μμ§) + const errorText = await res.text(); + console.error("Server text error:", errorText); + errorInfo = errorText; } - throw new Error(`HTTP error! status: ${res.status}, info: ${errorInfo}`); + throw new Error(`HTTP error! status: ${res.status}, details: ${errorInfo}`); } - // Handle successful response - try { - const data = await res.json(); - console.log("Push response data:", data); - - if (data.message === "Already pushed!") { - pushBtn.innerText = "β οΈ Already"; - } else if (data.message === "No change") { - pushBtn.innerText = "π‘ No change"; - } else { - const pushedAt = data.pushed_at || new Date().toISOString(); - chrome.storage.local.set({ last_push: pushedAt }, () => { - console.log(`[Push] Last push: ${pushedAt}`); - }); - pushBtn.innerText = "β Push"; - } - } catch (parseError) { - console.error("Error parsing success response:", parseError); - pushBtn.innerText = "β οΈ Partial"; + const data = await res.json(); + console.log("API Success Response:", data); + + if (data.message === "Already pushed!") { + pushBtn.innerText = "β οΈ Already"; + } else if (data.message === "No change") { + pushBtn.innerText = "π‘ No change"; + } else { + const pushedAt = data.pushed_at || new Date().toISOString(); + chrome.storage.local.set({ last_push: pushedAt }, () => { + console.log(`[Push] Last push: ${pushedAt}`); + }); + pushBtn.innerText = "β Push"; + } } catch (err) { console.error("Push error:", err); @@ -480,6 +429,17 @@ async function pushCodeToGitHub(pushBtn) { await getStatsFromAPI(); } +// Add a function to check and log the selected repository +function checkSelectedRepository() { + chrome.storage.local.get(['selected_repo'], (result) => { + const selectedRepo = result.selected_repo; + if (selectedRepo) { + console.log(`[LeetCode Pusher] Using repository: ${selectedRepo}`); + } else { + console.warn("[LeetCode Pusher] No repository selected. Push function will not work."); + } + }); +} function waitForEditorAndInsertButton() { const editor = document.querySelector('.monaco-editor'); @@ -509,6 +469,7 @@ observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => { waitForEditorAndInsertButton(); monitorSubmitButton(); + checkSelectedRepository(); // Check repository on page load }, 1000); document.addEventListener("keydown", function (e) { diff --git a/pusher/manifest.json b/pusher/manifest.json index ef85343..ab4e50a 100644 --- a/pusher/manifest.json +++ b/pusher/manifest.json @@ -6,7 +6,8 @@ "identity", "storage", "scripting", - "activeTab" + "activeTab", + "webNavigation" ], "oauth2": { "client_id": "Ov23lidbbczriEkuebBd", @@ -40,7 +41,7 @@ ], "externally_connectable": { "matches": [ - "https://amaodlllieieimpkkfoehlimfficcnbg.chromiumapp.org/*" + "chrome-extension://*/" ] } } diff --git a/pusher/popup.html b/pusher/popup.html index 92e8688..196e1b9 100644 --- a/pusher/popup.html +++ b/pusher/popup.html @@ -4,7 +4,6 @@