mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 08:03:15 +00:00
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:
parent
219c280644
commit
02d257fa0f
7 changed files with 150 additions and 4 deletions
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
105
apps/web/src/components/api-key-banner.tsx
Normal file
105
apps/web/src/components/api-key-banner.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
38
apps/web/src/hooks/useUser.ts
Normal file
38
apps/web/src/hooks/useUser.ts
Normal 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 };
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue