From 02d257fa0f0e7157b83d80f24786531bfff9c29d Mon Sep 17 00:00:00 2001 From: "open-swe[bot]" <215916821+open-swe[bot]@users.noreply.github.com> Date: Wed, 23 Jul 2025 19:37:05 +0000 Subject: [PATCH] feat: Add API Key Banner for Unauthorized Users (#488) * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * Apply patch * cr * cr * cr * cr --------- Co-authored-by: open-swe[bot] Co-authored-by: bracesproul --- .../src/routes/github/issue-webhook.ts | 2 +- apps/open-swe/src/security/auth.ts | 2 +- apps/open-swe/src/utils/model-manager.ts | 2 +- apps/web/src/components/api-key-banner.tsx | 105 ++++++++++++++++++ apps/web/src/components/v2/default-view.tsx | 2 + apps/web/src/hooks/useUser.ts | 38 +++++++ .../shared/src}/github/allowed-users.ts | 3 +- 7 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 apps/web/src/components/api-key-banner.tsx create mode 100644 apps/web/src/hooks/useUser.ts rename {apps/open-swe/src/utils => packages/shared/src}/github/allowed-users.ts (90%) diff --git a/apps/open-swe/src/routes/github/issue-webhook.ts b/apps/open-swe/src/routes/github/issue-webhook.ts index c1dfc9e2..9fa5f8a0 100644 --- a/apps/open-swe/src/routes/github/issue-webhook.ts +++ b/apps/open-swe/src/routes/github/issue-webhook.ts @@ -20,7 +20,7 @@ import { } from "../../utils/github/label.js"; import { ManagerGraphUpdate } from "@open-swe/shared/open-swe/manager/types"; import { RequestSource } from "../../constants.js"; -import { isAllowedUser } from "../../utils/github/allowed-users.js"; +import { isAllowedUser } from "@open-swe/shared/github/allowed-users"; const logger = createLogger(LogLevel.INFO, "GitHubIssueWebhook"); diff --git a/apps/open-swe/src/security/auth.ts b/apps/open-swe/src/security/auth.ts index 096ec02b..6192baeb 100644 --- a/apps/open-swe/src/security/auth.ts +++ b/apps/open-swe/src/security/auth.ts @@ -17,7 +17,7 @@ import { verifyGitHubWebhookOrThrow } from "./github.js"; import { createWithOwnerMetadata, createOwnerFilter } from "./utils.js"; import { LANGGRAPH_USER_PERMISSIONS } from "../constants.js"; import { getGitHubPatFromRequest } from "../utils/github-pat.js"; -import { isAllowedUser } from "../utils/github/allowed-users.js"; +import { isAllowedUser } from "@open-swe/shared/github/allowed-users"; import { validate } from "uuid"; // TODO: Export from LangGraph SDK diff --git a/apps/open-swe/src/utils/model-manager.ts b/apps/open-swe/src/utils/model-manager.ts index 4e8d9af9..04625d47 100644 --- a/apps/open-swe/src/utils/model-manager.ts +++ b/apps/open-swe/src/utils/model-manager.ts @@ -5,7 +5,7 @@ import { import { GraphConfig } from "@open-swe/shared/open-swe/types"; import { createLogger, LogLevel } from "./logger.js"; import { Task } from "./load-model.js"; -import { isAllowedUser } from "./github/allowed-users.js"; +import { isAllowedUser } from "@open-swe/shared/github/allowed-users"; import { decryptSecret } from "@open-swe/shared/crypto"; const logger = createLogger(LogLevel.INFO, "ModelManager"); diff --git a/apps/web/src/components/api-key-banner.tsx b/apps/web/src/components/api-key-banner.tsx new file mode 100644 index 00000000..56b432bc --- /dev/null +++ b/apps/web/src/components/api-key-banner.tsx @@ -0,0 +1,105 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { X, Key } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; +import { useUser } from "@/hooks/useUser"; +import { useConfigStore, DEFAULT_CONFIG_KEY } from "@/hooks/useConfigStore"; +import { isAllowedUser } from "@open-swe/shared/github/allowed-users"; +import Link from "next/link"; + +const API_KEY_BANNER_DISMISSED_KEY = "api_key_banner_dismissed"; + +export function ApiKeyBanner() { + const { user, isLoading } = useUser(); + const { getConfig } = useConfigStore(); + const config = getConfig(DEFAULT_CONFIG_KEY); + const [dismissed, setDismissed] = useState(false); + + useEffect(() => { + if (typeof window === "undefined") { + return; + } + // Check if user has previously dismissed the banner + const hasDismissed = localStorage.getItem(API_KEY_BANNER_DISMISSED_KEY); + if (hasDismissed === "true") { + setDismissed(true); + } + }, []); + + // Don't show banner if: + // - Still loading user data + // - User is not authenticated + // - User has dismissed the banner + if (isLoading || !user || dismissed) { + return null; + } + + // Check if user is in the allowed list + const userIsAllowed = isAllowedUser(user.login); + + // If user is allowed, they don't need API keys + if (userIsAllowed) { + return null; + } + + // Check if user has any API keys configured + const apiKeys = config.apiKeys || {}; + const hasApiKeys = + apiKeys.anthropicApiKey || apiKeys.openaiApiKey || apiKeys.googleApiKey; + + // If user has API keys, don't show banner + if (hasApiKeys) { + return null; + } + + const handleDismiss = () => { + if (typeof window === "undefined") { + return; + } + setDismissed(true); + localStorage.setItem(API_KEY_BANNER_DISMISSED_KEY, "true"); + }; + + return ( +
+
+ +
+

+ API Key Required +

+

+ You need to add an API key to use Open SWE. Add your Anthropic, + OpenAI, or Google API key to get started. +

+
+
+
+ + + + +
+
+ ); +} diff --git a/apps/web/src/components/v2/default-view.tsx b/apps/web/src/components/v2/default-view.tsx index aaf48862..58ef16b2 100644 --- a/apps/web/src/components/v2/default-view.tsx +++ b/apps/web/src/components/v2/default-view.tsx @@ -18,6 +18,7 @@ import { ContentBlocksPreview } from "../thread/ContentBlocksPreview"; import { ThemeToggle } from "../theme-toggle"; import { ThreadCard, ThreadCardLoading } from "./thread-card"; import { GitHubInstallationBanner } from "../github/installation-banner"; +import { ApiKeyBanner } from "../api-key-banner"; import { QuickActions } from "./quick-actions"; import { DraftsSection } from "./drafts-section"; import { GitHubLogoutButton } from "../github/github-oauth-button"; @@ -116,6 +117,7 @@ export function DefaultView({ threads, threadsLoading }: DefaultViewProps) {
+ {/* Terminal Chat Input */} void; +} + +async function fetchUser(): Promise { + const response = await fetch("/api/auth/user"); + if (!response.ok) { + throw new Error("Failed to fetch user data"); + } + const data: UserResponse = await response.json(); + return data.user; +} + +export function useUser(): UseUserResult { + const { data, error, isLoading, mutate } = useSWR( + "user", + fetchUser, + ); + + return { user: data || null, isLoading, error, mutate }; +} diff --git a/apps/open-swe/src/utils/github/allowed-users.ts b/packages/shared/src/github/allowed-users.ts similarity index 90% rename from apps/open-swe/src/utils/github/allowed-users.ts rename to packages/shared/src/github/allowed-users.ts index 67dedac0..a70616f4 100644 --- a/apps/open-swe/src/utils/github/allowed-users.ts +++ b/packages/shared/src/github/allowed-users.ts @@ -59,7 +59,8 @@ export const ALLOWED_USERS = [ // HACK: Until we setup proper support for API credits, we will only allow users to self host Open SWE export function isAllowedUser(username: string): boolean { const restrictToLangChainAuth = - process.env.RESTRICT_TO_LANGCHAIN_AUTH === "true"; + process.env.RESTRICT_TO_LANGCHAIN_AUTH === "true" || + process.env.NEXT_PUBLIC_RESTRICT_TO_LANGCHAIN_AUTH === "true"; if (!restrictToLangChainAuth) { return true; }