mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 09:13:14 +00:00
feat: Auth from Stream --> Middleware (#280)
* init auth stream --> middleware * minor cleanup * fix build * fix github token * minor format * handle access token + GH install key * add prompt to install GH app * fix build * fix gh install token passing * remove console logs * CR * CR * move dep, rm private key string replace * CR minor fixes * Update apps/web/src/providers/client.ts --------- Co-authored-by: Brace Sproul <braceasproul@gmail.com>
This commit is contained in:
parent
7a281ad4c1
commit
d778021da3
28 changed files with 534 additions and 372 deletions
|
|
@ -1,5 +1,8 @@
|
|||
import { Auth, HTTPException } from "@langchain/langgraph-sdk/auth";
|
||||
import { verifyGithubUser, GithubUser } from "./github-auth.js";
|
||||
import {
|
||||
verifyGithubUser,
|
||||
GithubUser,
|
||||
} from "@open-swe/shared/github/verify-user";
|
||||
import {
|
||||
GITHUB_INSTALLATION_TOKEN_COOKIE,
|
||||
GITHUB_TOKEN_COOKIE,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import type { Endpoints } from "@octokit/types";
|
||||
import type { RestEndpointMethodTypes } from "@octokit/rest";
|
||||
|
||||
export type GitHubIssue =
|
||||
Endpoints["GET /repos/{owner}/{repo}/issues/{issue_number}"]["response"]["data"];
|
||||
RestEndpointMethodTypes["issues"]["get"]["response"]["data"];
|
||||
|
||||
export type GitHubIssueComment =
|
||||
Endpoints["GET /repos/{owner}/{repo}/issues/{issue_number}/comments"]["response"]["data"][number];
|
||||
RestEndpointMethodTypes["issues"]["listComments"]["response"]["data"][number];
|
||||
|
||||
export type GitHubPullRequest =
|
||||
Endpoints["POST /repos/{owner}/{repo}/pulls"]["response"]["data"];
|
||||
RestEndpointMethodTypes["pulls"]["create"]["response"]["data"];
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { initApiPassthrough } from "langgraph-nextjs-api-passthrough";
|
||||
import {
|
||||
GITHUB_TOKEN_COOKIE,
|
||||
GITHUB_INSTALLATION_ID_COOKIE,
|
||||
GITHUB_INSTALLATION_TOKEN_COOKIE,
|
||||
} from "@open-swe/shared/constants";
|
||||
import { encryptGitHubToken } from "@open-swe/shared/crypto";
|
||||
import { NextRequest } from "next/server";
|
||||
import { getInstallationToken } from "@/utils/github";
|
||||
import { GITHUB_INSTALLATION_ID_COOKIE } from "@/lib/auth";
|
||||
|
||||
function getGitHubAccessTokenOrThrow(
|
||||
req: NextRequest,
|
||||
|
|
@ -15,7 +15,9 @@ function getGitHubAccessTokenOrThrow(
|
|||
const token = req.cookies.get(GITHUB_TOKEN_COOKIE)?.value ?? "";
|
||||
|
||||
if (!token) {
|
||||
throw new Error("No GitHub access token cookie found.");
|
||||
throw new Error(
|
||||
"No GitHub access token found. User must authenticate first.",
|
||||
);
|
||||
}
|
||||
|
||||
return encryptGitHubToken(token, encryptionKey);
|
||||
|
|
@ -28,12 +30,16 @@ async function getGitHubInstallationTokenOrThrow(
|
|||
const installationIdCookie = req.cookies.get(
|
||||
GITHUB_INSTALLATION_ID_COOKIE,
|
||||
)?.value;
|
||||
|
||||
if (!installationIdCookie) {
|
||||
throw new Error("No GitHub installation ID cookie found.");
|
||||
throw new Error(
|
||||
"No GitHub installation ID found. GitHub App must be installed first.",
|
||||
);
|
||||
}
|
||||
|
||||
const appId = process.env.GITHUB_APP_ID;
|
||||
const privateAppKey = process.env.GITHUB_APP_PRIVATE_KEY;
|
||||
|
||||
if (!appId || !privateAppKey) {
|
||||
throw new Error("GitHub App ID or Private App Key is not configured.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ import {
|
|||
GITHUB_AUTH_STATE_COOKIE,
|
||||
GITHUB_INSTALLATION_ID_COOKIE,
|
||||
GITHUB_TOKEN_TYPE_COOKIE,
|
||||
} from "@/lib/auth";
|
||||
GITHUB_TOKEN_COOKIE,
|
||||
} from "@open-swe/shared/constants";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { GITHUB_TOKEN_COOKIE } from "@open-swe/shared/constants";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
|
|
@ -81,9 +81,7 @@ export async function GET(request: NextRequest) {
|
|||
}
|
||||
|
||||
// Create the success response
|
||||
const response = NextResponse.redirect(
|
||||
new URL("/?auth=success", request.url),
|
||||
);
|
||||
const response = NextResponse.redirect(new URL("/chat", request.url));
|
||||
|
||||
// Clear the state cookie as it's no longer needed
|
||||
response.cookies.set(GITHUB_AUTH_STATE_COOKIE, "", {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { GITHUB_AUTH_STATE_COOKIE } from "@/lib/auth";
|
||||
import { GITHUB_AUTH_STATE_COOKIE } from "@open-swe/shared/constants";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function GET(_request: NextRequest) {
|
||||
|
|
|
|||
34
apps/web/src/app/api/auth/user/route.ts
Normal file
34
apps/web/src/app/api/auth/user/route.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getGitHubToken } from "@/lib/auth";
|
||||
import { verifyGithubUser } from "@open-swe/shared/github/verify-user";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const token = getGitHubToken(request);
|
||||
if (!token || !token.access_token) {
|
||||
return NextResponse.json({ error: "Not authenticated" }, { status: 401 });
|
||||
}
|
||||
const user = await verifyGithubUser(token.access_token);
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid GitHub token" },
|
||||
{ status: 401 },
|
||||
);
|
||||
}
|
||||
// Only return safe fields
|
||||
return NextResponse.json({
|
||||
user: {
|
||||
login: user.login,
|
||||
avatar_url: user.avatar_url,
|
||||
html_url: user.html_url,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to fetch user info" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { GITHUB_INSTALLATION_ID_COOKIE } from "@open-swe/shared/constants";
|
||||
import {
|
||||
GITHUB_INSTALLATION_ID_COOKIE,
|
||||
GITHUB_INSTALLATION_RETURN_TO_COOKIE,
|
||||
GITHUB_INSTALLATION_STATE_COOKIE,
|
||||
} from "@/lib/auth";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { GITHUB_INSTALLATION_ID_COOKIE } from "@/lib/auth";
|
||||
import { GITHUB_INSTALLATION_ID_COOKIE } from "@open-swe/shared/constants";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getInstallationToken } from "../../../../../utils/github"; // Adjusted path
|
||||
import { GITHUB_INSTALLATION_ID_COOKIE } from "@/lib/auth";
|
||||
import { GITHUB_INSTALLATION_ID_COOKIE } from "@open-swe/shared/constants";
|
||||
|
||||
const GITHUB_API_URL = "https://api.github.com";
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import {
|
|||
getInstallationRepositories,
|
||||
Repository,
|
||||
} from "@/utils/github";
|
||||
import { GITHUB_INSTALLATION_ID_COOKIE } from "@/lib/auth";
|
||||
import { GITHUB_INSTALLATION_ID_COOKIE } from "@open-swe/shared/constants";
|
||||
|
||||
/**
|
||||
* Fetches repositories accessible to the GitHub App installation
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getInstallationToken } from "@/utils/github";
|
||||
import { GITHUB_INSTALLATION_ID_COOKIE } from "@/lib/auth";
|
||||
import { GITHUB_INSTALLATION_ID_COOKIE } from "@open-swe/shared/constants";
|
||||
|
||||
/**
|
||||
* Returns a GitHub installation token that can be used for Git operations
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export default function GitHubPage() {
|
|||
)}
|
||||
|
||||
{!isInstalled ? (
|
||||
<div className="mb-6 rounded-lg bg-white p-6 shadow-md">
|
||||
<div className="mb-6 rounded-lg bg-white p-6 shadow-md dark:bg-gray-900">
|
||||
<h2 className="mb-4 text-xl font-semibold">Install GitHub App</h2>
|
||||
<p className="mb-4">
|
||||
To access your GitHub repositories, you need to install our GitHub
|
||||
|
|
@ -76,7 +76,7 @@ export default function GitHubPage() {
|
|||
</p>
|
||||
<button
|
||||
onClick={handleInstall}
|
||||
className="rounded bg-black px-4 py-2 text-white hover:bg-gray-800"
|
||||
className="rounded bg-black px-4 py-2 text-white hover:bg-gray-800 dark:bg-white dark:text-black dark:hover:bg-gray-200"
|
||||
>
|
||||
Install GitHub App
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import { Thread } from "@/components/thread";
|
||||
import { StreamProvider } from "@/providers/Stream";
|
||||
import { ThreadProvider } from "@/providers/Thread";
|
||||
import { StreamProvider } from "@/providers/Stream";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import React from "react";
|
||||
import { GitHubAppProvider } from "@/providers/GitHubApp";
|
||||
|
|
|
|||
262
apps/web/src/components/github/auth-status.tsx
Normal file
262
apps/web/src/components/github/auth-status.tsx
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
"use client";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { GitHubSVG } from "@/components/icons/github";
|
||||
import { LogOut } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
|
||||
const GITHUB_APP_INSTALL_URL = "/api/github/installation";
|
||||
const GITHUB_LOGIN_URL = "/api/auth/github/login";
|
||||
const GITHUB_LOGOUT_URL = "/api/auth/logout";
|
||||
|
||||
interface GitHubUser {
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
html_url: string;
|
||||
name?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export default function AuthStatus() {
|
||||
const [isAuth, setIsAuth] = useState<boolean | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isInstalled, setIsInstalled] = useState<boolean | null>(null);
|
||||
const [isInstallLoading, setIsInstallLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [user, setUser] = useState<GitHubUser | null>(null);
|
||||
const [userLoading, setUserLoading] = useState(false);
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
checkAuthStatus();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuth) {
|
||||
checkAppInstallation();
|
||||
fetchGitHubUser();
|
||||
} else {
|
||||
setIsInstalled(null);
|
||||
setUser(null);
|
||||
}
|
||||
}, [isAuth]);
|
||||
|
||||
const checkAuthStatus = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const response = await fetch("/api/auth/status");
|
||||
const data = await response.json();
|
||||
setIsAuth(data.authenticated);
|
||||
} catch (err) {
|
||||
setIsAuth(false);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const checkAppInstallation = async () => {
|
||||
setIsInstallLoading(true);
|
||||
try {
|
||||
const response = await fetch("/api/github/repositories");
|
||||
if (response.ok) {
|
||||
setIsInstalled(true);
|
||||
} else {
|
||||
const data = await response.json();
|
||||
if (data.error && data.error.includes("installation")) {
|
||||
setIsInstalled(false);
|
||||
} else {
|
||||
setError(data.error || "Unknown error");
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
setError("Failed to check GitHub App installation status");
|
||||
} finally {
|
||||
setIsInstallLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchGitHubUser = async () => {
|
||||
setUserLoading(true);
|
||||
try {
|
||||
const response = await fetch("/api/auth/user");
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setUser(data.user);
|
||||
} else {
|
||||
setUser(null);
|
||||
}
|
||||
} catch {
|
||||
setUser(null);
|
||||
} finally {
|
||||
setUserLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogin = () => {
|
||||
window.location.href = GITHUB_LOGIN_URL;
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await fetch(GITHUB_LOGOUT_URL, { method: "POST" });
|
||||
window.location.href = "/";
|
||||
} catch {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInstall = () => {
|
||||
window.location.href = GITHUB_APP_INSTALL_URL;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{isLoading ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled
|
||||
>
|
||||
<GitHubSVG
|
||||
width="16"
|
||||
height="16"
|
||||
/>{" "}
|
||||
Checking...
|
||||
</Button>
|
||||
) : isAuth ? (
|
||||
<Popover
|
||||
open={popoverOpen}
|
||||
onOpenChange={setPopoverOpen}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex items-center gap-2"
|
||||
onClick={() => setPopoverOpen((open) => !open)}
|
||||
>
|
||||
{userLoading ? (
|
||||
<span className="h-5 w-5 animate-pulse rounded-full bg-gray-200" />
|
||||
) : user && user.avatar_url ? (
|
||||
<img
|
||||
src={user.avatar_url}
|
||||
alt={user.login}
|
||||
className="h-5 w-5 rounded-full border border-gray-300"
|
||||
/>
|
||||
) : (
|
||||
<GitHubSVG
|
||||
width="16"
|
||||
height="16"
|
||||
/>
|
||||
)}
|
||||
{user && user.login ? (
|
||||
<span className="font-mono text-xs">{user.login}</span>
|
||||
) : (
|
||||
<span className="font-mono text-xs">GitHub User</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="end"
|
||||
className="w-64 p-3"
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
{userLoading ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-8 w-8 animate-pulse rounded-full bg-gray-200" />
|
||||
<div className="flex-1">
|
||||
<div className="mb-1 h-3 w-20 animate-pulse rounded bg-gray-200" />
|
||||
<div className="h-3 w-32 animate-pulse rounded bg-gray-100" />
|
||||
</div>
|
||||
</div>
|
||||
) : user ? (
|
||||
<div className="mb-2 flex items-center gap-3">
|
||||
<img
|
||||
src={user.avatar_url}
|
||||
alt={user.login}
|
||||
className="h-8 w-8 rounded-full border border-gray-300"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-semibold">
|
||||
{user.name || user.login}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{user.email || user.login}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{isInstallLoading ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled
|
||||
>
|
||||
<GitHubSVG
|
||||
width="16"
|
||||
height="16"
|
||||
/>{" "}
|
||||
Checking App...
|
||||
</Button>
|
||||
) : isInstalled === false ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleInstall}
|
||||
disabled={isInstallLoading}
|
||||
className="w-full"
|
||||
>
|
||||
<GitHubSVG
|
||||
width="16"
|
||||
height="16"
|
||||
/>{" "}
|
||||
Install GitHub App
|
||||
</Button>
|
||||
) : isInstalled === true ? (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="flex w-full items-center justify-center gap-1"
|
||||
>
|
||||
<GitHubSVG
|
||||
width="14"
|
||||
height="14"
|
||||
/>{" "}
|
||||
App Installed
|
||||
</Badge>
|
||||
) : null}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleLogout}
|
||||
disabled={isLoading}
|
||||
className="flex w-full items-center gap-2"
|
||||
>
|
||||
<LogOut className="size-4" /> Logout
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
) : (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleLogin}
|
||||
disabled={isLoading}
|
||||
size="sm"
|
||||
>
|
||||
<GitHubSVG
|
||||
width="16"
|
||||
height="16"
|
||||
/>{" "}
|
||||
Connect GitHub
|
||||
</Button>
|
||||
)}
|
||||
{error && <span className="ml-2 text-xs text-red-500">{error}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
56
apps/web/src/components/github/installation-banner.tsx
Normal file
56
apps/web/src/components/github/installation-banner.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
"use client";
|
||||
|
||||
import { useGitHubAppProvider } from "@/providers/GitHubApp";
|
||||
import { InstallationPrompt } from "./installation-prompt";
|
||||
import { useState, useEffect } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function GitHubInstallationBanner() {
|
||||
const { isInstalled, isLoading } = useGitHubAppProvider();
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
const [isNewUser, setIsNewUser] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Check if this might be a new user (no installation history in localStorage)
|
||||
const hasSeenInstallation = localStorage.getItem(
|
||||
"github_installation_seen",
|
||||
);
|
||||
if (!hasSeenInstallation && !isInstalled && !isLoading) {
|
||||
setIsNewUser(true);
|
||||
localStorage.setItem("github_installation_seen", "true");
|
||||
}
|
||||
}, [isInstalled, isLoading]);
|
||||
|
||||
// Don't show banner if:
|
||||
// - Still loading installation status
|
||||
// - App is already installed
|
||||
// - User has dismissed the banner
|
||||
if (isLoading || isInstalled || dismissed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleDismiss = () => {
|
||||
setDismissed(true);
|
||||
setIsNewUser(false);
|
||||
};
|
||||
|
||||
// Enhanced messaging for new users
|
||||
const title = isNewUser
|
||||
? "🎉 Welcome to Open SWE! Complete your setup"
|
||||
: "Complete your setup to start coding";
|
||||
|
||||
const description = isNewUser
|
||||
? "You're just one step away from AI-powered development! Install our GitHub App to connect your repositories and start coding with AI assistance."
|
||||
: "Install our GitHub App to grant access to your repositories and enable AI-powered development.";
|
||||
|
||||
return (
|
||||
<InstallationPrompt
|
||||
title={title}
|
||||
description={description}
|
||||
variant="banner"
|
||||
showDismiss={true}
|
||||
onDismiss={handleDismiss}
|
||||
className={cn(isNewUser && "border-2 border-amber-300 shadow-lg")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
76
apps/web/src/components/github/installation-prompt.tsx
Normal file
76
apps/web/src/components/github/installation-prompt.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
"use client";
|
||||
|
||||
import { InstallAppButton } from "./install-app-button";
|
||||
import { X } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface InstallationPromptProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
showDismiss?: boolean;
|
||||
onDismiss?: () => void;
|
||||
className?: string;
|
||||
variant?: "default" | "banner";
|
||||
}
|
||||
|
||||
export function InstallationPrompt({
|
||||
title = "GitHub App Not Installed",
|
||||
description = "You need to install our GitHub App to grant access to your repositories.",
|
||||
showDismiss = false,
|
||||
onDismiss,
|
||||
className = "",
|
||||
variant = "default",
|
||||
}: InstallationPromptProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-md border border-amber-200 bg-amber-50 p-4 dark:border-amber-800 dark:bg-amber-950/20",
|
||||
variant === "banner" && "flex items-center justify-between",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{variant === "banner" ? (
|
||||
<>
|
||||
<div>
|
||||
<h3 className="mb-1 font-medium text-amber-800 dark:text-amber-200">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="text-sm text-amber-700 dark:text-amber-300">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<InstallAppButton
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="border-amber-600 bg-amber-600 text-white hover:bg-amber-700"
|
||||
>
|
||||
Install GitHub App
|
||||
</InstallAppButton>
|
||||
{showDismiss && onDismiss && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onDismiss}
|
||||
className="h-8 w-8 p-0 text-amber-600 hover:text-amber-800 dark:text-amber-400 dark:hover:text-amber-200"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<h3 className="mb-2 font-medium text-amber-800 dark:text-amber-200">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="mb-4 text-sm text-amber-700 dark:text-amber-300">
|
||||
{description}
|
||||
</p>
|
||||
<InstallAppButton>Install GitHub App</InstallAppButton>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -16,9 +16,9 @@ import { Check, ChevronsUpDown } from "lucide-react";
|
|||
import { cn } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
import type { TargetRepository } from "@open-swe/shared/open-swe/types";
|
||||
import { useGitHubAppProvider } from "@/providers/GitHubApp";
|
||||
import type { Repository } from "@/utils/github";
|
||||
import { GitHubSVG } from "@/components/icons/github";
|
||||
import { Repository } from "@/utils/github";
|
||||
import { useGitHubAppProvider } from "@/providers/GitHubApp";
|
||||
|
||||
interface RepositorySelectorProps {
|
||||
disabled?: boolean;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useGitHubAppProvider } from "@/providers/GitHubApp";
|
||||
import { InstallAppButton } from "./install-app-button";
|
||||
import { InstallationPrompt } from "./installation-prompt";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { RefreshCw } from "lucide-react";
|
||||
|
||||
|
|
@ -49,14 +48,7 @@ export function RepositoryList({ className = "" }: RepositoryListProps) {
|
|||
if (!isInstalled) {
|
||||
return (
|
||||
<div className={`p-4 ${className}`}>
|
||||
<div className="mb-4 rounded-md border border-amber-200 bg-amber-50 p-4">
|
||||
<h3 className="mb-2 font-medium">GitHub App Not Installed</h3>
|
||||
<p className="mb-4 text-sm text-amber-800">
|
||||
You need to install our GitHub App to grant access to your
|
||||
repositories.
|
||||
</p>
|
||||
<InstallAppButton>Install GitHub App</InstallAppButton>
|
||||
</div>
|
||||
<InstallationPrompt />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TooltipIconButton } from "@/components/ui/tooltip-icon-button";
|
||||
import AuthStatus from "@/components/github/auth-status";
|
||||
|
||||
interface SidebarButtonsProps {
|
||||
historyOpen: boolean;
|
||||
|
|
@ -17,6 +18,7 @@ interface SidebarButtonsProps {
|
|||
setConfigOpen: (open: boolean) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// TODO: Implement Using this component in the thread component instead of current implementation
|
||||
export const SidebarButtons = forwardRef<HTMLDivElement, SidebarButtonsProps>(
|
||||
(
|
||||
|
|
@ -104,6 +106,7 @@ export const SidebarButtons = forwardRef<HTMLDivElement, SidebarButtonsProps>(
|
|||
>
|
||||
<History className="size-5" />
|
||||
</TooltipIconButton>
|
||||
<AuthStatus />
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import {
|
|||
PanelRightOpen,
|
||||
PanelRightClose,
|
||||
SquarePen,
|
||||
XIcon,
|
||||
Settings,
|
||||
FilePlus2,
|
||||
} from "lucide-react";
|
||||
|
|
@ -30,7 +29,6 @@ import { useMediaQuery } from "@/hooks/useMediaQuery";
|
|||
import { Label } from "../ui/label";
|
||||
import { useFileUpload } from "@/hooks/useFileUpload";
|
||||
import { ContentBlocksPreview } from "./ContentBlocksPreview";
|
||||
import { GitHubOAuthButton } from "../github/github-oauth-button";
|
||||
import { useGitHubAppProvider } from "@/providers/GitHubApp";
|
||||
import TaskList from "../task-list";
|
||||
import { ConfigurationSidebar } from "../configuration-sidebar";
|
||||
|
|
@ -54,6 +52,7 @@ import {
|
|||
mapCustomEventsToSteps,
|
||||
} from "@open-swe/shared/open-swe/custom-node-events";
|
||||
import { DO_NOT_RENDER_ID_PREFIX } from "@open-swe/shared/constants";
|
||||
import AuthStatus from "../github/auth-status";
|
||||
|
||||
function StickyToBottomContent(props: {
|
||||
content: ReactNode;
|
||||
|
|
@ -403,6 +402,7 @@ export function Thread() {
|
|||
)}
|
||||
</div>
|
||||
<div className="absolute top-2 right-4 flex items-center gap-2 text-gray-700">
|
||||
<AuthStatus />
|
||||
<TooltipIconButton
|
||||
tooltip="Configuration"
|
||||
variant="ghost"
|
||||
|
|
@ -413,7 +413,6 @@ export function Thread() {
|
|||
<Settings className="size-4" />
|
||||
</TooltipIconButton>
|
||||
<ThemeToggle />
|
||||
<GitHubOAuthButton />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -467,7 +466,7 @@ export function Thread() {
|
|||
</div>
|
||||
|
||||
<div className="col-span-2 flex items-center justify-end gap-2 text-gray-700">
|
||||
<GitHubOAuthButton />
|
||||
<AuthStatus />
|
||||
<OpenPRButton />
|
||||
<TooltipIconButton
|
||||
tooltip="Configuration"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
|
|
@ -8,17 +7,7 @@ import {
|
|||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Loader2,
|
||||
GitBranch,
|
||||
GitPullRequest,
|
||||
Bug,
|
||||
FilePlus2,
|
||||
Archive,
|
||||
} from "lucide-react";
|
||||
import { FilePlus2, Archive } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ThreadDisplayInfo } from "./types";
|
||||
import { TerminalInput } from "./terminal-input";
|
||||
|
|
@ -34,6 +23,7 @@ import { Label } from "../ui/label";
|
|||
import { ContentBlocksPreview } from "../thread/ContentBlocksPreview";
|
||||
import { ThemeToggle } from "../theme-toggle";
|
||||
import { ThreadCard, ThreadCardLoading } from "./thread-card";
|
||||
import { GitHubInstallationBanner } from "../github/installation-banner";
|
||||
import { QuickActions } from "./quick-actions";
|
||||
import { useState } from "react";
|
||||
|
||||
|
|
@ -86,6 +76,7 @@ export function DefaultView({ threads, threadsLoading }: DefaultViewProps) {
|
|||
{/* Main Content */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="mx-auto max-w-4xl space-y-6 p-4">
|
||||
<GitHubInstallationBanner />
|
||||
{/* Terminal Chat Input */}
|
||||
<Card
|
||||
className={cn(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { GITHUB_TOKEN_COOKIE } from "@open-swe/shared/constants";
|
||||
import {
|
||||
GITHUB_TOKEN_COOKIE,
|
||||
GITHUB_TOKEN_TYPE_COOKIE,
|
||||
GITHUB_INSTALLATION_ID_COOKIE,
|
||||
} from "@open-swe/shared/constants";
|
||||
|
||||
export const GITHUB_TOKEN_TYPE_COOKIE = "github_token_type";
|
||||
export const GITHUB_INSTALLATION_ID_COOKIE = "github_installation_id";
|
||||
export const GITHUB_AUTH_STATE_COOKIE = "github_auth_state";
|
||||
export const GITHUB_INSTALLATION_STATE_COOKIE = "github_installation_state";
|
||||
export const GITHUB_INSTALLATION_RETURN_TO_COOKIE = "installation_return_to";
|
||||
|
||||
|
|
|
|||
18
apps/web/src/middleware.ts
Normal file
18
apps/web/src/middleware.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { GITHUB_TOKEN_COOKIE } from "@open-swe/shared/constants";
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
if (request.nextUrl.pathname === "/") {
|
||||
const token = request.cookies.get(GITHUB_TOKEN_COOKIE)?.value;
|
||||
if (token && token.length > 0) {
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = "/chat";
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
}
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/"],
|
||||
};
|
||||
|
|
@ -1,10 +1,4 @@
|
|||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
ReactNode,
|
||||
useState,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import React, { createContext, useContext, ReactNode, useState } from "react";
|
||||
import { useStream } from "@langchain/langgraph-sdk/react";
|
||||
import {
|
||||
uiMessageReducer,
|
||||
|
|
@ -14,14 +8,7 @@ import {
|
|||
type RemoveUIMessage,
|
||||
} from "@langchain/langgraph-sdk/react-ui";
|
||||
import { useQueryState } from "nuqs";
|
||||
import { LangGraphLogoSVG } from "@/components/icons/langgraph";
|
||||
import { useThreadsContext } from "./Thread";
|
||||
import { TooltipIconButton } from "@/components/ui/tooltip-icon-button";
|
||||
import { Copy, CopyCheck, ArrowRight } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { GitHubSVG } from "@/components/icons/github";
|
||||
import { useGitHubToken } from "@/hooks/useGitHubToken";
|
||||
import { GraphState, GraphUpdate } from "@open-swe/shared/open-swe/types";
|
||||
import {
|
||||
CustomNodeEvent,
|
||||
|
|
@ -45,23 +32,22 @@ async function sleep(ms = 4000) {
|
|||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
const StreamSession = ({
|
||||
children,
|
||||
apiUrl,
|
||||
assistantId,
|
||||
githubToken,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
apiUrl: string;
|
||||
assistantId: string;
|
||||
githubToken: string;
|
||||
}) => {
|
||||
const StreamSession = ({ children }: { children: ReactNode }) => {
|
||||
const [threadId, setThreadId] = useQueryState("threadId");
|
||||
const [customEvents, setCustomEvents] = useState<CustomNodeEvent[]>([]);
|
||||
const { refreshThreads } = useThreadsContext();
|
||||
|
||||
const streamValue = useTypedStream({
|
||||
apiUrl,
|
||||
assistantId,
|
||||
apiUrl:
|
||||
process.env.NEXT_PUBLIC_API_URL ??
|
||||
(() => {
|
||||
throw new Error("NEXT_PUBLIC_API_URL is required");
|
||||
})(),
|
||||
assistantId:
|
||||
process.env.NEXT_PUBLIC_MANAGER_ASSISTANT_ID ??
|
||||
(() => {
|
||||
throw new Error("NEXT_PUBLIC_MANAGER_ASSISTANT_ID is required");
|
||||
})(),
|
||||
reconnectOnMount: true,
|
||||
threadId: threadId ?? null,
|
||||
onCustomEvent: (event, options) => {
|
||||
|
|
@ -77,7 +63,6 @@ const StreamSession = ({
|
|||
},
|
||||
onThreadId: (id) => {
|
||||
setThreadId(id);
|
||||
|
||||
sleep().then(() => {
|
||||
refreshThreads().catch(console.error);
|
||||
});
|
||||
|
|
@ -96,285 +81,10 @@ const StreamSession = ({
|
|||
);
|
||||
};
|
||||
|
||||
export const StreamProvider: React.FC<{ children: ReactNode }> = ({
|
||||
children,
|
||||
}) => {
|
||||
const baseCopyTooltipText = "Copy environment variables";
|
||||
const [copyTooltipText, setCopyTooltipText] = useState(baseCopyTooltipText);
|
||||
|
||||
const apiUrl: string | undefined = process.env.NEXT_PUBLIC_API_URL ?? "";
|
||||
const assistantId: string | undefined =
|
||||
process.env.NEXT_PUBLIC_ASSISTANT_ID ?? "";
|
||||
|
||||
const [isAuth, setIsAuth] = useState<boolean | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const [hasGitHubAppInstalled, setHasGitHubAppInstalled] = useState<
|
||||
boolean | null
|
||||
>(() => {
|
||||
if (typeof window === "undefined") return null;
|
||||
const cached = localStorage.getItem("github_app_installed");
|
||||
return cached === "true" ? true : cached === "false" ? false : null;
|
||||
});
|
||||
|
||||
const [isCheckingAppInstallation, setIsCheckingAppInstallation] =
|
||||
useState(false);
|
||||
const {
|
||||
token: githubToken,
|
||||
fetchToken: fetchGitHubToken,
|
||||
isLoading: isTokenLoading,
|
||||
} = useGitHubToken();
|
||||
|
||||
useEffect(() => {
|
||||
checkAuthStatus();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuth) {
|
||||
const cachedInstallationStatus = localStorage.getItem(
|
||||
"github_app_installed",
|
||||
);
|
||||
if (cachedInstallationStatus === "true") {
|
||||
setHasGitHubAppInstalled(true);
|
||||
// Fetch token if we don't have one yet
|
||||
if (!githubToken && !isTokenLoading) {
|
||||
fetchGitHubToken();
|
||||
}
|
||||
} else if (cachedInstallationStatus === "false") {
|
||||
setHasGitHubAppInstalled(false);
|
||||
} else {
|
||||
checkGitHubAppInstallation();
|
||||
}
|
||||
}
|
||||
}, [isAuth, githubToken]);
|
||||
|
||||
const checkAuthStatus = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/auth/status");
|
||||
const data = await response.json();
|
||||
setIsAuth(data.authenticated);
|
||||
} catch (error) {
|
||||
console.error("Error checking auth status:", error);
|
||||
setIsAuth(false);
|
||||
}
|
||||
};
|
||||
|
||||
const checkGitHubAppInstallation = async () => {
|
||||
setIsCheckingAppInstallation(true);
|
||||
try {
|
||||
const response = await fetch("/api/github/repositories");
|
||||
if (response.ok) {
|
||||
setHasGitHubAppInstalled(true);
|
||||
localStorage.setItem("github_app_installed", "true");
|
||||
|
||||
await fetchGitHubToken();
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
if (errorData.error.includes("installation")) {
|
||||
setHasGitHubAppInstalled(false);
|
||||
localStorage.setItem("github_app_installed", "false");
|
||||
} else {
|
||||
setHasGitHubAppInstalled(false);
|
||||
localStorage.setItem("github_app_installed", "false");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking GitHub App installation:", error);
|
||||
setHasGitHubAppInstalled(false);
|
||||
localStorage.setItem("github_app_installed", "false");
|
||||
} finally {
|
||||
setIsCheckingAppInstallation(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogin = () => {
|
||||
setIsLoading(true);
|
||||
window.location.href = "/api/auth/github/login";
|
||||
};
|
||||
|
||||
const handleInstallGitHubApp = () => {
|
||||
setIsLoading(true);
|
||||
|
||||
localStorage.removeItem("github_app_installed");
|
||||
window.location.href = "/api/github/installation";
|
||||
};
|
||||
|
||||
if (!apiUrl || !assistantId) {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full items-center justify-center p-4">
|
||||
<div className="animate-in fade-in-0 zoom-in-95 flex max-w-3xl flex-col rounded-lg border bg-red-50 shadow-lg">
|
||||
<div className="flex flex-col gap-4 border-b p-6">
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<LangGraphLogoSVG className="h-7" />
|
||||
<h1 className="text-xl font-semibold tracking-tight">
|
||||
Environment Variables Missing
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
Whoops, looks like you don't have an API URL or assistant ID
|
||||
set in your environment variables. Please make sure you have both
|
||||
of these set before continuing.
|
||||
</p>
|
||||
<div className="relative">
|
||||
<TooltipIconButton
|
||||
onClick={() => {
|
||||
const textToCopy = `NEXT_PUBLIC_API_URL=${apiUrl}\nNEXT_PUBLIC_ASSISTANT_ID=${assistantId}`;
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
setCopyTooltipText("Copied!");
|
||||
setTimeout(
|
||||
() => setCopyTooltipText(baseCopyTooltipText),
|
||||
2000,
|
||||
);
|
||||
});
|
||||
}}
|
||||
className="absolute top-2 right-2 cursor-pointer"
|
||||
tooltip={copyTooltipText}
|
||||
>
|
||||
{copyTooltipText === baseCopyTooltipText ? (
|
||||
<motion.div
|
||||
key="check"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<Copy />
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="copy"
|
||||
initial={{ opacity: 0, scale: 0.8 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.8 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
>
|
||||
<CopyCheck className="text-green-500" />
|
||||
</motion.div>
|
||||
)}
|
||||
</TooltipIconButton>
|
||||
<code className="bg-muted flex flex-col gap-2 rounded-md border border-red-200 px-4 py-3 text-sm">
|
||||
<span>NEXT_PUBLIC_API_URL={apiUrl}</span>
|
||||
<span>NEXT_PUBLIC_ASSISTANT_ID={assistantId}</span>
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAuth) {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full items-center justify-center p-4">
|
||||
<div className="animate-in fade-in-0 zoom-in-95 flex w-full max-w-3xl flex-col rounded-lg border shadow-lg">
|
||||
<div className="flex flex-col gap-4 border-b p-6">
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<LangGraphLogoSVG className="h-7" />
|
||||
<h1 className="text-xl font-semibold tracking-tight">
|
||||
Get started
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
Connect your GitHub account to get started with Open SWE.
|
||||
</p>
|
||||
<Button
|
||||
onClick={handleLogin}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<GitHubSVG
|
||||
width="16"
|
||||
height="16"
|
||||
/>
|
||||
{isLoading ? "Connecting..." : "Connect GitHub"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Step 2: GitHub App Installation (only show if authenticated but app not installed)
|
||||
// Only show modal if we've definitively determined the app is not installed
|
||||
// Don't show while we're still loading or have cached positive status
|
||||
if (isAuth && hasGitHubAppInstalled === false && !isTokenLoading) {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full items-center justify-center p-4">
|
||||
<div className="animate-in fade-in-0 zoom-in-95 flex w-full max-w-3xl flex-col rounded-lg border shadow-lg">
|
||||
<div className="flex flex-col gap-4 border-b p-6">
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<LangGraphLogoSVG className="h-7" />
|
||||
<h1 className="text-xl font-semibold tracking-tight">
|
||||
One more step
|
||||
</h1>
|
||||
</div>
|
||||
<div className="text-muted-foreground flex items-center gap-2 text-sm">
|
||||
<span className="rounded-full bg-green-100 px-2 py-1 text-xs font-medium text-green-800">
|
||||
1. GitHub Login ✓
|
||||
</span>
|
||||
<ArrowRight className="h-3 w-3" />
|
||||
<span className="rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-800">
|
||||
2. Repository Access
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
Great! Now we need access to your GitHub repositories. Install our
|
||||
GitHub App to grant access to specific repositories.
|
||||
</p>
|
||||
<div className="rounded-md border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800">
|
||||
<p>
|
||||
You'll be redirected to GitHub where you can select which
|
||||
repositories to grant access to.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleInstallGitHubApp}
|
||||
disabled={isLoading || isCheckingAppInstallation}
|
||||
className="bg-black hover:bg-gray-800"
|
||||
>
|
||||
<GitHubSVG
|
||||
width="16"
|
||||
height="16"
|
||||
/>
|
||||
{isLoading || isCheckingAppInstallation
|
||||
? "Loading..."
|
||||
: "Install GitHub App"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Only render StreamSession when we have a valid token
|
||||
if (!githubToken) {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full items-center justify-center p-4">
|
||||
<div className="animate-in fade-in-0 zoom-in-95 flex w-full max-w-3xl flex-col rounded-lg border shadow-lg">
|
||||
<div className="flex flex-col gap-4 border-b p-6">
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<LangGraphLogoSVG className="h-7" />
|
||||
<h1 className="text-xl font-semibold tracking-tight">
|
||||
Loading...
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-muted-foreground">
|
||||
Setting up your GitHub integration...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StreamSession
|
||||
apiUrl={apiUrl}
|
||||
assistantId={assistantId}
|
||||
githubToken={githubToken}
|
||||
>
|
||||
{children}
|
||||
</StreamSession>
|
||||
);
|
||||
export const StreamProvider: React.FC<{
|
||||
children: ReactNode;
|
||||
}> = ({ children }) => {
|
||||
return <StreamSession>{children}</StreamSession>;
|
||||
};
|
||||
|
||||
export const useStreamContext = (): StreamContextType => {
|
||||
|
|
|
|||
|
|
@ -20,11 +20,13 @@
|
|||
"@langchain/core": "^0.3.56",
|
||||
"@langchain/langgraph": "^0.3.3",
|
||||
"@langchain/langgraph-sdk": "^0.0.83",
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"zod": "^3.25.32"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.1.0",
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@octokit/types": "^12.0.0",
|
||||
"@tsconfig/recommended": "^1.0.8",
|
||||
"@types/node": "^22.13.5",
|
||||
"dotenv": "^16.4.7",
|
||||
|
|
|
|||
|
|
@ -9,3 +9,6 @@ export const GITHUB_TOKEN_COOKIE = "x-github-access-token";
|
|||
export const GITHUB_INSTALLATION_TOKEN_COOKIE = "x-github-installation-token";
|
||||
|
||||
export const DO_NOT_RENDER_ID_PREFIX = "do-not-render-";
|
||||
export const GITHUB_AUTH_STATE_COOKIE = "github_auth_state";
|
||||
export const GITHUB_INSTALLATION_ID_COOKIE = "github_installation_id";
|
||||
export const GITHUB_TOKEN_TYPE_COOKIE = "github_token_type";
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import { Octokit } from "@octokit/rest";
|
||||
import { Endpoints } from "@octokit/types";
|
||||
import { createLogger, LogLevel } from "../utils/logger.js";
|
||||
|
||||
const logger = createLogger(LogLevel.INFO, "GithubAuth");
|
||||
|
||||
export type GithubUser = Endpoints["GET /user"]["response"]["data"];
|
||||
|
||||
|
|
@ -20,19 +17,12 @@ export async function verifyGithubUser(
|
|||
|
||||
try {
|
||||
const octokit = new Octokit({ auth: accessToken });
|
||||
|
||||
const { data: user } = await octokit.users.getAuthenticated();
|
||||
|
||||
if (!user || !user.login) {
|
||||
logger.error(
|
||||
"GitHub token is invalid or user information could not be retrieved.",
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return user;
|
||||
} catch (error) {
|
||||
logger.error("An error occurred during GitHub user verification:", error);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
18
yarn.lock
18
yarn.lock
|
|
@ -2130,6 +2130,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/openapi-types@npm:^20.0.0":
|
||||
version: 20.0.0
|
||||
resolution: "@octokit/openapi-types@npm:20.0.0"
|
||||
checksum: 23ff7613750f8b5790a0cbed5a2048728a7909e50d726932831044908357a932c7fc0613fb7b86430a49d31b3d03a180632ea5dd936535bfbc1176391a199e96
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/openapi-types@npm:^25.1.0":
|
||||
version: 25.1.0
|
||||
resolution: "@octokit/openapi-types@npm:25.1.0"
|
||||
|
|
@ -2202,6 +2209,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/types@npm:^12.0.0":
|
||||
version: 12.6.0
|
||||
resolution: "@octokit/types@npm:12.6.0"
|
||||
dependencies:
|
||||
"@octokit/openapi-types": ^20.0.0
|
||||
checksum: 850235f425584499a2266d5c585c1c2462ae11e25c650567142f3342cb9ce589c8c8fed87705811ca93271fd28c68e1fa77b88b67b97015d7b63d269fa46ed05
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@octokit/types@npm:^14.0.0, @octokit/types@npm:^14.1.0":
|
||||
version: 14.1.0
|
||||
resolution: "@octokit/types@npm:14.1.0"
|
||||
|
|
@ -2284,6 +2300,8 @@ __metadata:
|
|||
"@langchain/core": ^0.3.56
|
||||
"@langchain/langgraph": ^0.3.3
|
||||
"@langchain/langgraph-sdk": ^0.0.83
|
||||
"@octokit/rest": ^22.0.0
|
||||
"@octokit/types": ^12.0.0
|
||||
"@tsconfig/recommended": ^1.0.8
|
||||
"@types/node": ^22.13.5
|
||||
dotenv: ^16.4.7
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue