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 @@ LeetCode Pusher +
@@ -133,31 +182,35 @@

LeetCode Pusher

-

-

-
+ + +
- - +
-
Loading...
+
+ Loading... +
diff --git a/pusher/popup.js b/pusher/popup.js index 74bc478..beaa9e5 100644 --- a/pusher/popup.js +++ b/pusher/popup.js @@ -1,63 +1,84 @@ -// Wait for DOM to be fully loaded -document.addEventListener('DOMContentLoaded', function() { - const clientId = "Ov23lidbbczriEkuebBd"; - const REDIRECT_URL = `https://${chrome.runtime.id}.chromiumapp.org/`; - // API_BASE_URL is imported from config.js +const clientId = "Ov23lidbbczriEkuebBd"; +const REDIRECT_URL = `https://${chrome.runtime.id}.chromiumapp.org/`; - const loginBtn = document.getElementById("login-btn"); - const logoutBtn = document.getElementById("logout-btn"); - const statusEl = document.getElementById("status"); - const repoEl = document.getElementById("repo"); - const lastPushEl = document.getElementById("last-push"); - const lastLoginEl = document.getElementById("last-login"); - const loadingEl = document.getElementById("loading"); - const githubBtn = document.getElementById("github-btn"); - const repoSelect = document.getElementById("repo-select"); - const setRepoBtn = document.getElementById("set-repo-btn"); +const loginBtn = document.getElementById("login-btn"); +const logoutBtn = document.getElementById("logout-btn"); +const statusEl = document.getElementById("status"); +const repoEl = document.getElementById("repo"); +const lastPushEl = document.getElementById("last-push"); +const lastLoginEl = document.getElementById("last-login"); +const loadingEl = document.getElementById("loading"); +const githubBtn = document.getElementById("github-btn"); +const repoSelect = document.getElementById("repo-select"); - // Function to fetch user's repositories - async function fetchUserRepos(github_token) { - try { - console.log('Fetching repositories with token:', github_token ? github_token.substring(0, 5) + '...' : 'missing'); - - if (!github_token) { - throw new Error("GitHub token is missing. Please login again."); - } - - // Get the token type from storage - const tokenType = await new Promise(resolve => { - chrome.storage.local.get(['token_type'], (result) => { - resolve(result.token_type || 'standard'); - }); +// Function to fetch user's repositories +async function fetchUserRepos(github_token) { + try { + console.log('Fetching repositories with token:', github_token ? github_token.substring(0, 5) + '...' : 'missing'); + + if (!github_token) { + throw new Error("GitHub token is missing. Please login again."); + } + + // Get the token type from storage + const tokenType = await new Promise(resolve => { + chrome.storage.local.get(['token_type'], (result) => { + resolve(result.token_type || 'standard'); }); - - console.log(`Token type: ${tokenType}`); - - // Get username from storage - we'll need this for fallback and API calls - const username = await new Promise(resolve => { - chrome.storage.local.get(['username'], (result) => { - resolve(result.username || ""); - }); + }); + + console.log(`Token type: ${tokenType}`); + + // Get username from storage - we'll need this for fallback and API calls + const username = await new Promise(resolve => { + chrome.storage.local.get(['username'], (result) => { + resolve(result.username || ""); }); + }); + + console.log(`Username: ${username}`); + + // Handle temporary tokens + if (github_token.startsWith('temp_')) { + console.warn("Using temporary token - will try to fetch repos anyway"); + } + + // Try to fetch real repositories first in all cases + let success = false; + let repos = []; + + // First attempt: Use GitHub API to get user's repositories + try { + console.log("Fetching repositories from GitHub API..."); - console.log(`Username: ${username}`); + // Try to fetch using username - this doesn't require auth + const response = await fetch(`https://api.github.com/users/${username}/repos?per_page=100&sort=updated`, { + headers: { + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'LIT1337-Chrome-Extension' + } + }); - // Handle temporary tokens - if (github_token.startsWith('temp_')) { - console.warn("Using temporary token - will try to fetch repos anyway"); + if (response.ok) { + success = true; + const data = await response.json(); + console.log(`Successfully fetched ${data.length} repositories for user ${username}`); + repos = data; + } else { + console.log(`Public repo fetch failed with status ${response.status}`); } - - // Try to fetch real repositories first in all cases - let success = false; - let repos = []; - - // First attempt: Use GitHub API to get user's repositories + } catch (error) { + console.error("Error fetching public repos:", error); + } + + // If that failed, try with token auth + if (!success && !github_token.startsWith('temp_')) { + // Method 1: Standard GitHub API token format + console.log("Trying GitHub API with 'token' prefix..."); try { - console.log("Fetching repositories from GitHub API..."); - - // Try to fetch using username - this doesn't require auth - const response = await fetch(`https://api.github.com/users/${username}/repos?per_page=100&sort=updated`, { + const response = await fetch('https://api.github.com/user/repos?per_page=100&sort=updated', { headers: { + 'Authorization': `token ${github_token}`, 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'LIT1337-Chrome-Extension' } @@ -66,23 +87,22 @@ document.addEventListener('DOMContentLoaded', function() { if (response.ok) { success = true; const data = await response.json(); - console.log(`Successfully fetched ${data.length} repositories for user ${username}`); + console.log(`Successfully fetched ${data.length} repositories with token format`); repos = data; } else { - console.log(`Public repo fetch failed with status ${response.status}`); + console.log(`Token format failed with status ${response.status}`); } } catch (error) { - console.error("Error fetching public repos:", error); + console.error("Error with token format:", error); } - // If that failed, try with token auth - if (!success && !github_token.startsWith('temp_')) { - // Method 1: Standard GitHub API token format - console.log("Trying GitHub API with 'token' prefix..."); + // Method 2: Try Bearer format + if (!success) { + console.log("Trying GitHub API with 'Bearer' prefix..."); try { const response = await fetch('https://api.github.com/user/repos?per_page=100&sort=updated', { headers: { - 'Authorization': `token ${github_token}`, + 'Authorization': `Bearer ${github_token}`, 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'LIT1337-Chrome-Extension' } @@ -91,486 +111,275 @@ document.addEventListener('DOMContentLoaded', function() { if (response.ok) { success = true; const data = await response.json(); - console.log(`Successfully fetched ${data.length} repositories with token format`); + console.log(`Successfully fetched ${data.length} repositories with Bearer format`); repos = data; } else { - console.log(`Token format failed with status ${response.status}`); + console.log(`Bearer format failed with status ${response.status}`); } } catch (error) { - console.error("Error with token format:", error); - } - - // Method 2: Try Bearer format - if (!success) { - console.log("Trying GitHub API with 'Bearer' prefix..."); - try { - const response = await fetch('https://api.github.com/user/repos?per_page=100&sort=updated', { - headers: { - 'Authorization': `Bearer ${github_token}`, - 'Accept': 'application/vnd.github.v3+json', - 'User-Agent': 'LIT1337-Chrome-Extension' - } - }); - - if (response.ok) { - success = true; - const data = await response.json(); - console.log(`Successfully fetched ${data.length} repositories with Bearer format`); - repos = data; - } else { - console.log(`Bearer format failed with status ${response.status}`); - } - } catch (error) { - console.error("Error with Bearer format:", error); - } + console.error("Error with Bearer format:", error); } } - - // If all attempts failed, use fallback demo repositories - if (!success) { - console.log("All API attempts failed, using fallback demo repositories"); - repos = [ - { name: "example-repo-1", owner: { login: username || "user" } }, - { name: "example-repo-2", owner: { login: username || "user" } }, - { name: "leetcode-solutions", owner: { login: username || "user" } } - ]; - } - - return Array.isArray(repos) ? repos : []; - } catch (error) { - console.error('Error fetching repos:', error); - throw error; // Rethrow to handle in the caller } - } - - // Function to populate repository select dropdown - async function populateRepoSelect(github_token) { - try { - console.log('Starting repository population...'); - if (!github_token) { - console.error('Missing GitHub token for repo population'); - repoSelect.innerHTML = ''; - statusEl.innerText = "Authentication required. Please login with GitHub."; - loginBtn.style.display = "inline-block"; - return; - } - - statusEl.innerText = "Loading repositories..."; - repoEl.innerText = "Fetching repositories..."; - - const repos = await fetchUserRepos(github_token); - if (repos.length === 0) { - repoSelect.innerHTML = ''; - repoEl.innerText = "No repositories found. Please check your GitHub account."; - return; - } - - // Sort repositories alphabetically - repos.sort((a, b) => a.name.localeCompare(b.name)); - - repoSelect.innerHTML = ''; - repos.forEach(repo => { - const option = document.createElement('option'); - option.value = `${repo.owner.login}/${repo.name}`; - option.textContent = repo.name; - repoSelect.appendChild(option); - }); - console.log(`Populated ${repos.length} repositories`); - repoEl.innerText = "Please select a repository"; - } catch (error) { - console.error('Error populating repo select:', error); - repoSelect.innerHTML = ''; - statusEl.innerText = `Error: ${error.message}`; - - // If token related error, show login button - if (error.message.includes("login") || error.message.includes("token") || error.message.includes("auth")) { - loginBtn.style.display = "inline-block"; - } + + // If all attempts failed, use fallback demo repositories + if (!success) { + console.log("All API attempts failed, using fallback demo repositories"); + repos = [ + { name: "example-repo-1", owner: { login: username || "user" } }, + { name: "example-repo-2", owner: { login: username || "user" } }, + { name: "leetcode-solutions", owner: { login: username || "user" } } + ]; } - repoSelect.style.display = 'block'; + + return Array.isArray(repos) ? repos : []; + } catch (error) { + console.error('Error fetching repos:', error); + throw error; // Rethrow to handle in the caller } +} - // Listen for auth state changes from background script - chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - console.log('Message received in popup:', message); +// Function to populate repository select dropdown +async function populateRepoSelect(github_token) { + try { + console.log('Starting repository population...'); + if (!github_token) { + console.error('Missing GitHub token for repo population'); + repoSelect.innerHTML = ''; + statusEl.innerText = "Authentication required. Please login with GitHub."; + loginBtn.style.display = "inline-block"; + return; + } - if (message.type === 'auth-state-changed') { - console.log('Auth state changed:', message); - - // Clear login timeout if it exists - if (window.loginTimeoutId) { - clearTimeout(window.loginTimeoutId); - window.loginTimeoutId = null; - } - - loadingEl.classList.remove("show"); - - if (!message.success) { - loginBtn.style.display = "inline-block"; - statusEl.className = "badge error"; - statusEl.innerText = `Login failed: ${message.error || 'Unknown error'}`; - return; - } - - // Check if we have the required data before reloading - chrome.storage.local.get(["jwt", "github_token", "username"], - ({ jwt, github_token, username }) => { - console.log("Storage check after login:", { - jwt: jwt ? "exists" : "missing", - github_token: github_token ? "exists" : "missing", - username: username || "missing", - tokenLength: github_token ? github_token.length : 0 - }); - - if (jwt && github_token && username) { - console.log("Login successful, reloading popup"); - location.reload(); - } else { - console.error("Login completed but missing required data"); - statusEl.className = "badge error"; - statusEl.innerText = "Login incomplete - missing data. Try again."; - loginBtn.style.display = "inline-block"; - } - }); + statusEl.innerText = "Loading repositories..."; + repoEl.innerText = "Fetching repositories..."; + + const repos = await fetchUserRepos(github_token); + if (repos.length === 0) { + repoSelect.innerHTML = ''; + repoEl.innerText = "No repositories found. Please check your GitHub account."; + return; } - }); - // initial render: check JWT + check if user exists on server - chrome.storage.local.get(["jwt", "github_token", "username", "last_push", "last_login", "selected_repo", "token_type"], - async ({ jwt, github_token, username, last_push, last_login, selected_repo, token_type }) => { - console.log('Retrieved from storage:', { - jwt: jwt ? `${jwt.substring(0, 10)}...` : 'missing', - github_token: github_token ? `${github_token.substring(0, 10)}...` : 'missing', - username, - token_type: token_type || 'unknown', - last_login: last_login ? new Date(last_login).toLocaleString() : 'missing', - last_push: last_push ? new Date(last_push).toLocaleString() : 'missing', - selected_repo: selected_repo || 'none' + // Sort repositories alphabetically + repos.sort((a, b) => a.name.localeCompare(b.name)); + + repoSelect.innerHTML = ''; + repos.forEach(repo => { + const option = document.createElement('option'); + option.value = `${repo.owner.login}/${repo.name}`; + option.textContent = repo.name; + repoSelect.appendChild(option); }); + console.log(`Populated ${repos.length} repositories`); + repoEl.innerText = "Please select a repository"; + } catch (error) { + console.error('Error populating repo select:', error); + repoSelect.innerHTML = ''; + statusEl.innerText = `Error: ${error.message}`; - loadingEl.classList.remove("show"); // Always hide loading on initial render - - if (jwt && github_token && username) { - try { - console.log('User is logged in, updating UI...'); - - // Fetch user data from backend to get the latest info - try { - statusEl.innerText = "Fetching user data..."; - const response = await fetch(`${API_BASE_URL}/me`, { - headers: { - 'Authorization': `Bearer ${jwt}` - } - }); - - if (response.ok) { - const userData = await response.json(); - console.log('User data from backend:', userData); - - // Update local storage with backend data - chrome.storage.local.set({ - last_login: userData.last_login || last_login, - last_push: userData.last_push || last_push, - selected_repo: userData.selected_repo || selected_repo - }, () => { - console.log('Updated storage with backend data'); - // Use the backend data for selected_repo if available - selected_repo = userData.selected_repo || selected_repo; - }); - } else { - console.warn('Failed to fetch user data from backend:', await response.text()); - } - } catch (error) { - console.error('Error fetching user data:', error); - } - - updateUI(username, last_push, last_login, selected_repo); - - // Populate repos after updating UI so user sees they're logged in - await populateRepoSelect(github_token); - if (selected_repo) { - repoSelect.value = selected_repo; - repoEl.innerText = `Connected repo: ${selected_repo}`; - githubBtn.style.display = "inline-block"; - // Show the set repo button for the selected repo - setRepoBtn.style.display = "inline-block"; - } else { - // Display warning if no repository is selected - repoEl.innerText = "⚠️ Please select a repository to push code"; - repoEl.style.color = "#ff6b00"; - statusEl.innerText = `Welcome, ${username}! Select a repo`; - setRepoBtn.style.display = "none"; - } - } catch (error) { - console.error('Error during initialization:', error); - statusEl.className = "badge error"; - statusEl.innerText = `Error: ${error.message}`; - - // Only clear storage if there's a token problem, not for other errors - if (error.message.includes('token') || error.message.includes('auth')) { - console.log("Clearing storage due to token error"); - chrome.storage.local.clear(() => { - loginBtn.style.display = "inline-block"; - logoutBtn.style.display = "none"; - githubBtn.style.display = "none"; - setRepoBtn.style.display = "none"; - lastPushEl.style.display = "none"; - repoSelect.style.display = "none"; - }); - } - } - } else { - // if not logged in - console.log('User is not logged in'); - statusEl.innerText = "πŸ”’ Not logged in"; + // If token related error, show login button + if (error.message.includes("login") || error.message.includes("token") || error.message.includes("auth")) { loginBtn.style.display = "inline-block"; - logoutBtn.style.display = "none"; - githubBtn.style.display = "none"; - setRepoBtn.style.display = "none"; - lastPushEl.style.display = "none"; - repoSelect.style.display = "none"; } - }); + } + repoSelect.style.display = 'block'; +} - // click login button - loginBtn.addEventListener("click", () => { - console.log("Login button clicked"); - loginBtn.style.display = "none"; - statusEl.className = "badge"; - statusEl.innerText = "Connecting to GitHub..."; - loadingEl.classList.add("show"); +// Listen for auth state changes from background script +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + console.log('Message received in popup:', message); + + if (message.type === 'auth-state-changed') { + console.log('Auth state changed:', message); - // Set a timeout to prevent infinite loading - const loginTimeout = setTimeout(() => { - console.log("Login timeout reached - 30 seconds with no response"); - loadingEl.classList.remove("show"); + // Clear login timeout if it exists + if (window.loginTimeoutId) { + clearTimeout(window.loginTimeoutId); + window.loginTimeoutId = null; + } + + loadingEl.classList.remove("show"); + + if (!message.success) { loginBtn.style.display = "inline-block"; statusEl.className = "badge error"; - statusEl.innerText = "Login timed out. Please try again."; - }, 30000); - - // Store the timeout ID so it can be cleared on success - window.loginTimeoutId = loginTimeout; + statusEl.innerText = `Login failed: ${message.error || 'Unknown error'}`; + return; + } - // Force clear any existing tokens before login attempt - chrome.storage.local.remove(["github_token", "token_type"], () => { - console.log("Cleared existing GitHub token before login"); - chrome.runtime.sendMessage({ action: "login" }); + // Check if we have the required data before reloading + chrome.storage.local.get(["jwt", "github_token", "username"], + ({ jwt, github_token, username }) => { + console.log("Storage check after login:", { + jwt: jwt ? "exists" : "missing", + github_token: github_token ? "exists" : "missing", + username: username || "missing", + tokenLength: github_token ? github_token.length : 0 + }); + + if (jwt && github_token && username) { + console.log("Login successful, reloading popup"); + location.reload(); + } else { + console.error("Login completed but missing required data"); + statusEl.className = "badge error"; + statusEl.innerText = "Login incomplete - missing data. Try again."; + loginBtn.style.display = "inline-block"; + } }); - }); + } +}); - // click logout button - logoutBtn.addEventListener("click", () => { - chrome.storage.local.clear(() => { - statusEl.innerText = "Logged out."; - loginBtn.style.display = "inline-block"; - logoutBtn.style.display = "none"; - githubBtn.style.display = "none"; - repoEl.innerText = ""; - lastPushEl.innerText = ""; - lastPushEl.style.display = "none"; - repoSelect.style.display = "none"; - }); +// initial render: check JWT + check if user exists on server +chrome.storage.local.get(["jwt", "github_token", "username", "last_push", "last_login", "selected_repo", "token_type"], + async ({ jwt, github_token, username, last_push, last_login, selected_repo, token_type }) => { + console.log('Retrieved from storage:', { + jwt: jwt ? `${jwt.substring(0, 10)}...` : 'missing', + github_token: github_token ? `${github_token.substring(0, 10)}...` : 'missing', + username, + token_type: token_type || 'unknown', + last_login: last_login ? new Date(last_login).toLocaleString() : 'missing', + last_push: last_push ? new Date(last_push).toLocaleString() : 'missing', + selected_repo: selected_repo || 'none' }); - - // click github button - githubBtn.addEventListener("click", () => { - chrome.storage.local.get(["selected_repo"], ({ selected_repo }) => { + + loadingEl.classList.remove("show"); // Always hide loading on initial render + + if (jwt && github_token && username) { + try { + console.log('User is logged in, updating UI...'); + updateUI(username, last_push, last_login, selected_repo); + + // Populate repos after updating UI so user sees they're logged in + await populateRepoSelect(github_token); if (selected_repo) { - const repoUrl = `https://github.com/${selected_repo}`; - chrome.tabs.create({ url: repoUrl }); + repoSelect.value = selected_repo; + repoEl.innerText = `Connected repo: ${selected_repo}`; + githubBtn.style.display = "inline-block"; } else { - console.error("No repository selected."); - statusEl.innerText = "Please select a repository first."; + // Display warning if no repository is selected + repoEl.innerText = "⚠️ Please select a repository to push code"; + repoEl.style.color = "#ff6b00"; + statusEl.innerText = `Welcome, ${username}! Select a repo`; } - }); - }); - - // Add function to save repository to backend - async function saveRepositoryToBackend(selectedRepo) { - try { - statusEl.innerText = "Saving repository..."; + } catch (error) { + console.error('Error during initialization:', error); + statusEl.className = "badge error"; + statusEl.innerText = `Error: ${error.message}`; - // Get JWT token for authentication - const { jwt } = await new Promise(resolve => { - chrome.storage.local.get(["jwt"], (result) => { - resolve(result); + // Only clear storage if there's a token problem, not for other errors + if (error.message.includes('token') || error.message.includes('auth')) { + console.log("Clearing storage due to token error"); + chrome.storage.local.clear(() => { + loginBtn.style.display = "inline-block"; + logoutBtn.style.display = "none"; + githubBtn.style.display = "none"; + lastPushEl.style.display = "none"; + repoSelect.style.display = "none"; }); - }); - - if (!jwt) { - throw new Error("Authentication required. Please login first."); } - - const response = await fetch(`${API_BASE_URL}/save-repository`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${jwt}` - }, - body: JSON.stringify({ repository: selectedRepo }) - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.detail || `Server error: ${response.status}`); - } - - const data = await response.json(); - console.log("Repository saved to backend:", data); - - return data; - } catch (error) { - console.error("Error saving repository to backend:", error); - throw error; } + } else { + // if not logged in + console.log('User is not logged in'); + statusEl.innerText = "πŸ”’ Not logged in"; + loginBtn.style.display = "inline-block"; + logoutBtn.style.display = "none"; + githubBtn.style.display = "none"; + lastPushEl.style.display = "none"; + repoSelect.style.display = "none"; } +}); + +// click login button +loginBtn.addEventListener("click", () => { + console.log("Login button clicked"); + loginBtn.style.display = "none"; + statusEl.className = "badge"; + statusEl.innerText = "Connecting to GitHub..."; + loadingEl.classList.add("show"); + + // Set a timeout to prevent infinite loading + const loginTimeout = setTimeout(() => { + console.log("Login timeout reached - 30 seconds with no response"); + loadingEl.classList.remove("show"); + loginBtn.style.display = "inline-block"; + statusEl.className = "badge error"; + statusEl.innerText = "Login timed out. Please try again."; + }, 30000); + + // Store the timeout ID so it can be cleared on success + window.loginTimeoutId = loginTimeout; + + // Force clear any existing tokens before login attempt + chrome.storage.local.remove(["github_token", "token_type"], () => { + console.log("Cleared existing GitHub token before login"); + chrome.runtime.sendMessage({ action: "login" }); - // Repository selection change handler - repoSelect.addEventListener('change', (e) => { - const selectedRepo = e.target.value; - if (selectedRepo) { - console.log(`Selected repository: ${selectedRepo}`); - - // Store selected repo in Chrome storage - chrome.storage.local.set({ selected_repo: selectedRepo }, () => { - console.log(`Repository saved to storage: ${selectedRepo}`); - - // Show visual confirmation - repoEl.innerText = `Selected repo: ${selectedRepo} (not saved to backend yet)`; - repoEl.style.color = "#ff9800"; // Orange color to indicate pending status - githubBtn.style.display = "inline-block"; - setRepoBtn.style.display = "inline-block"; - - // Notify user about successful selection - statusEl.innerText = `Repository selected. Click "Set Repository" button to save.`; - statusEl.className = "badge"; - - // Log the updated storage state to verify - chrome.storage.local.get(["selected_repo"], (result) => { - console.log(`Storage verification - selected_repo: ${result.selected_repo}`); - }); - }); - } else { - console.warn("No repository selected"); - repoEl.innerText = "⚠️ No repository selected"; - repoEl.style.color = "#ff6b00"; // Warning color - githubBtn.style.display = "none"; - setRepoBtn.style.display = "none"; - } }); - // Set Repository button click handler - setRepoBtn.addEventListener('click', async () => { - // Get selected repository from storage - const { selected_repo } = await new Promise(resolve => { - chrome.storage.local.get(["selected_repo"], (result) => { - resolve(result); - }); - }); - - if (!selected_repo) { - statusEl.innerText = "Please select a repository first"; - statusEl.className = "badge error"; - return; - } - - try { - setRepoBtn.disabled = true; - setRepoBtn.innerHTML = ' Saving...'; - - // Save repository to backend - await saveRepositoryToBackend(selected_repo); - - // Update UI to show success - repoEl.innerText = `Connected repo: ${selected_repo}`; - repoEl.style.color = "#4caf50"; // Green color to indicate success - statusEl.innerText = "Repository saved successfully!"; - statusEl.className = "badge success"; - - setTimeout(() => { - statusEl.innerText = "Welcome!"; - statusEl.className = "badge"; - }, 3000); - } catch (error) { - console.error("Error saving repository:", error); - statusEl.innerText = `Error: ${error.message}`; - statusEl.className = "badge error"; - } finally { - setRepoBtn.disabled = false; - setRepoBtn.innerHTML = ' Set Repository'; + + }); + + +// click github button +githubBtn.addEventListener("click", () => { + chrome.storage.local.get(["selected_repo"], ({ selected_repo }) => { + if (selected_repo) { + const repoUrl = `https://github.com/${selected_repo}`; + chrome.tabs.create({ url: repoUrl }); + } else { + console.error("No repository selected."); + statusEl.innerText = "Please select a repository first."; + } }); - // Add a refresh repositories button - const refreshBtn = document.createElement("button"); - refreshBtn.innerText = "πŸ”„ Refresh Repos"; - refreshBtn.style = ` - margin-top: 10px; - background-color: #007bff; - color: white; - border: none; - border-radius: 4px; - padding: 5px 10px; - cursor: pointer; - font-size: 12px; - `; - refreshBtn.addEventListener("click", async () => { - const status = document.createElement("span"); - status.innerText = " (refreshing...)"; - status.style.fontSize = "12px"; - refreshBtn.appendChild(status); + +// Repository selection change handler +repoSelect.addEventListener('change', (e) => { + const selectedRepo = e.target.value; + if (selectedRepo) { + console.log(`Selected repository: ${selectedRepo}`); - // Get token from storage - chrome.storage.local.get(["github_token"], async ({ github_token }) => { - if (github_token) { - try { - // Clear and repopulate repository selection - repoSelect.innerHTML = ''; - await populateRepoSelect(github_token); - - // Attempt to restore previous selection - chrome.storage.local.get(["selected_repo"], ({ selected_repo }) => { - if (selected_repo) { - console.log(`Attempting to restore selection: ${selected_repo}`); - // Check if the option exists in the dropdown - const options = Array.from(repoSelect.options); - const matchingOption = options.find(opt => opt.value === selected_repo); - - if (matchingOption) { - repoSelect.value = selected_repo; - repoEl.innerText = `Connected repo: ${selected_repo}`; - githubBtn.style.display = "inline-block"; - console.log(`Selection restored: ${selected_repo}`); - } else { - console.warn(`Previously selected repo ${selected_repo} not found in current list`); - repoEl.innerText = "⚠️ Please select a repository"; - repoEl.style.color = "#ff6b00"; - } - } - }); - - statusEl.innerText = "Repositories refreshed!"; - setTimeout(() => { - statusEl.innerText = "Welcome!"; - }, 2000); - } catch (error) { - console.error("Error refreshing repositories:", error); - statusEl.innerText = `Error: ${error.message}`; - } - } else { - statusEl.innerText = "Please login first"; - } + // Store selected repo in Chrome storage + chrome.storage.local.set({ selected_repo: selectedRepo }, () => { + console.log(`Repository saved to storage: ${selectedRepo}`); - // Remove the status indicator - refreshBtn.removeChild(status); + // Update UI with selected repo + chrome.storage.local.get(["username", "last_push", "last_login"], + ({ username, last_push, last_login }) => { + updateUI(username, last_push, last_login, selectedRepo); + }); }); - }); + } else { + console.warn("No repository selected"); + } +}); + +// UI update function +function updateUI(username, last_push, last_login, selected_repo) { + statusEl.innerText = `Welcome, ${username}!`; + loginBtn.style.display = "none"; + logoutBtn.style.display = "inline-block"; + repoSelect.style.display = "block"; + + if (selected_repo) { + repoEl.innerText = `Connected repo: ${selected_repo}`; + githubBtn.style.display = "inline-block"; + } else { + repoEl.innerText = "Please select a repository"; + githubBtn.style.display = "none"; + } + + if (last_push) { + lastPushEl.style.display = "inline-block"; + const pushDate = new Date(last_push); + lastPushEl.innerText = `Last push: ${pushDate.getFullYear()}-${(pushDate.getMonth() + 1).toString().padStart(2, '0')}-${pushDate.getDate().toString().padStart(2, '0')} ${pushDate.getHours().toString().padStart(2, '0')}:${pushDate.getMinutes().toString().padStart(2, '0')}`; + } else { + lastPushEl.style.display = "none"; - // Add the refresh button to the page after repository selection - const container = document.querySelector('.container'); - if (container) { - container.appendChild(refreshBtn); } // UI update function @@ -605,6 +414,5 @@ document.addEventListener('DOMContentLoaded', function() { } } - // Make the Set Repository button initially hidden - setRepoBtn.style.display = "none"; -}); +} +