From 2f03f46c363936be90620b380e0b8542fab52fa5 Mon Sep 17 00:00:00 2001 From: faizahmad-khan Date: Wed, 29 Jul 2026 20:56:13 +0530 Subject: [PATCH 1/2] fix(backend): detect stale/revoked GitHub tokens and prompt re-auth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - github_service.py: catch GithubException 401 in commit_files and return {error: 'invalid_token'} instead of falling through to unknown - supabase_service.py: add clear_github_token() to wipe dead credentials - bot.py: handle invalid_token in commit_confirm and force_commit callbacks — clear the token from DB and send a targeted /auth re-prompt - routes/staged_files.py: return HTTP 401 (not 500) on invalid_token from /commit-direct so VS Code extension can show a clear error Fixes #YOUR_ISSUE_NUMBER --- backend/bot.py | 41 +++++++++++++++++++++++++--------- backend/github_service.py | 2 ++ backend/routes/staged_files.py | 5 +++++ backend/supabase_service.py | 12 ++++++++++ 4 files changed, 50 insertions(+), 10 deletions(-) diff --git a/backend/bot.py b/backend/bot.py index a8711b2..8aa43d9 100644 --- a/backend/bot.py +++ b/backend/bot.py @@ -35,6 +35,7 @@ from supabase_service import ( ban_user, clear_all_staged, + clear_github_token, count_stats, get_all_users, get_pending_files, @@ -1130,13 +1131,23 @@ async def commit_now_callback(update: Update, context: ContextTypes.DEFAULT_TYPE return WAITING_PROTECTED_BRANCH_NAME else: - error_msg = result.get('message', 'Unknown error') - await channel_logger.log_commit_failed(telegram_id, active_repo, error_msg) - await query.edit_message_text( - f"[X] *Commit failed.*\n\n" - f"Error: {error_msg}\n\n" - f"Your staged files are safe. Try /files again." - ) + if result.get("error") == "invalid_token": + clear_github_token(telegram_id) + await channel_logger.log_commit_failed(telegram_id, active_repo, "invalid_token") + await query.edit_message_text( + "🔑 *Your GitHub token has expired or been revoked.*\n\n" + "Your staged files are safe — no changes were made.\n\n" + "Please run /auth to reconnect your GitHub account and try again.", + parse_mode=ParseMode.MARKDOWN + ) + else: + error_msg = result.get('message', 'Unknown error') + await channel_logger.log_commit_failed(telegram_id, active_repo, error_msg) + await query.edit_message_text( + f"[X] *Commit failed.*\n\n" + f"Error: {error_msg}\n\n" + f"Your staged files are safe. Try /files again." + ) context.user_data.clear() return ConversationHandler.END @@ -1211,9 +1222,19 @@ async def commit_force_callback(update: Update, context: ContextTypes.DEFAULT_TY parse_mode=ParseMode.MARKDOWN ) else: - error_msg = result.get('message', 'Unknown error') - await channel_logger.log_commit_failed(telegram_id, active_repo, error_msg) - await query.edit_message_text(f"[X] Force commit failed: {error_msg}") + if result.get("error") == "invalid_token": + clear_github_token(telegram_id) + await channel_logger.log_commit_failed(telegram_id, active_repo, "invalid_token") + await query.edit_message_text( + "🔑 *Your GitHub token has expired or been revoked.*\n\n" + "Your staged files are safe — no changes were made.\n\n" + "Please run /auth to reconnect your GitHub account and try again.", + parse_mode=ParseMode.MARKDOWN + ) + else: + error_msg = result.get('message', 'Unknown error') + await channel_logger.log_commit_failed(telegram_id, active_repo, error_msg) + await query.edit_message_text(f"[X] Force commit failed: {error_msg}") context.user_data.clear() return ConversationHandler.END diff --git a/backend/github_service.py b/backend/github_service.py index 7458469..118c949 100644 --- a/backend/github_service.py +++ b/backend/github_service.py @@ -240,6 +240,8 @@ def commit_files( } except GithubException as e: + if e.status == 401: + return {"ok": False, "error": "invalid_token", "message": "GitHub token is invalid or expired. Please re-authenticate via /auth."} if e.status == 409: return {"ok": False, "error": "conflict", "message": "SHA conflict on GitHub"} if e.status == 422: diff --git a/backend/routes/staged_files.py b/backend/routes/staged_files.py index 6baf0ea..0da9d0d 100644 --- a/backend/routes/staged_files.py +++ b/backend/routes/staged_files.py @@ -169,6 +169,11 @@ async def commit_direct(payload: DirectCommitPayload, telegram_id: str = Depends status_code=409, detail=f"Conflict in: {', '.join(conflict_files)}. Use /files in Telegram \u2192 Force Commit.", ) + if result.get("error") == "invalid_token": + raise HTTPException( + status_code=401, + detail="GitHub token is invalid or expired. Please re-authenticate via /auth in Telegram.", + ) raise HTTPException( status_code=500, detail=result.get("message", "GitHub commit failed."), diff --git a/backend/supabase_service.py b/backend/supabase_service.py index 9bd8256..9981d99 100644 --- a/backend/supabase_service.py +++ b/backend/supabase_service.py @@ -441,3 +441,15 @@ def update_github_token(telegram_id: str, token: str) -> bool: print(f"[supabase] update_github_token error: {e}") return False + +def clear_github_token(telegram_id: str) -> bool: + """Clear a stale or revoked GitHub token so the user is prompted to re-authenticate.""" + try: + get_client().table("users") \ + .update({"github_token": None}) \ + .eq("telegram_id", telegram_id) \ + .execute() + return True + except Exception as e: + print(f"[supabase] clear_github_token error: {e}") + return False From 2fa055079aeb54c42627a476e7158bb2968567d4 Mon Sep 17 00:00:00 2001 From: faizahmad-khan Date: Wed, 29 Jul 2026 21:07:42 +0530 Subject: [PATCH 2/2] fix(merge): include upstream update_github_username and get_user_by_github_username functions --- backend/supabase_service.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/backend/supabase_service.py b/backend/supabase_service.py index 9981d99..290c31a 100644 --- a/backend/supabase_service.py +++ b/backend/supabase_service.py @@ -453,3 +453,31 @@ def clear_github_token(telegram_id: str) -> bool: except Exception as e: print(f"[supabase] clear_github_token error: {e}") return False + + +def update_github_username(telegram_id: str, github_username: str) -> None: + """Persist the user's GitHub login so webhooks can map assignee -> telegram_id.""" + if not github_username: + return + try: + get_client().table("users") \ + .update({"github_username": github_username.lower()}) \ + .eq("telegram_id", telegram_id) \ + .execute() + except Exception as e: + print(f"[supabase] update_github_username error: {e}") + + +def get_user_by_github_username(github_username: str) -> dict | None: + """Reverse lookup: GitHub login -> user row. Case-insensitive. None if unmapped.""" + if not github_username: + return None + try: + result = get_client().table("users") \ + .select("*") \ + .ilike("github_username", github_username) \ + .execute() + return result.data[0] if result.data else None + except Exception as e: + print(f"[supabase] get_user_by_github_username error: {e}") + return None