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] <open-swe@users.noreply.github.com>
Co-authored-by: bracesproul <braceasproul@gmail.com>
This commit is contained in:
open-swe[bot] 2025-07-23 19:37:05 +00:00 • committed by GitHub
parent 219c280644
commit 02d257fa0f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 150 additions and 4 deletions

View file

@ -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");

View file

@ -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

View file

@ -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");

View file

@ -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 (
<div
className={cn(
"rounded-md border border-blue-200 bg-blue-50 p-4 dark:border-blue-800 dark:bg-blue-950/20",
"flex items-center justify-between gap-6",
)}
>
<div className="flex items-center gap-3">
<Key className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<div>
<h3 className="mb-1 font-medium text-blue-800 dark:text-blue-200">
API Key Required
</h3>
<p className="text-sm text-blue-700 dark:text-blue-300">
You need to add an API key to use Open SWE. Add your Anthropic,
OpenAI, or Google API key to get started.
</p>
</div>
</div>
<div className="flex items-center gap-2">
<Link href="/settings?tab=api-keys">
<Button
variant="default"
size="sm"
className="border-blue-600 bg-blue-600 text-white hover:bg-blue-700"
>
Add API Keys
</Button>
</Link>
<Button
variant="ghost"
size="sm"
onClick={handleDismiss}
className="h-8 w-8 p-0 text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-200"
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
);
}

View file

@ -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) {
<div className="flex-1 overflow-y-auto">
<div className="mx-auto max-w-4xl space-y-6 p-4">
<GitHubInstallationBanner />
<ApiKeyBanner />
{/* Terminal Chat Input */}
<Card
className={cn(

View file

@ -0,0 +1,38 @@
import useSWR from "swr";
interface UserData {
login: string;
avatar_url: string;
html_url: string;
name: string | null;
email: string | null;
}
interface UserResponse {
user: UserData;
}
interface UseUserResult {
user: UserData | null;
isLoading: boolean;
error: Error | null;
mutate: () => void;
}
async function fetchUser(): Promise<UserData> {
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<UserData>(
"user",
fetchUser,
);
return { user: data || null, isLoading, error, mutate };
}

View file

@ -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;
}