mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
5 Commits
fix-github
...
feature/im
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac239db4e1 | ||
|
|
97cb1103a5 | ||
|
|
129399bb50 | ||
|
|
74e140a3a2 | ||
|
|
01a3e98bb9 |
@@ -1,7 +1,20 @@
|
||||
import axios from "axios";
|
||||
import { saveLastPage } from "#/utils/last-page";
|
||||
|
||||
export const openHands = axios.create();
|
||||
|
||||
// Add response interceptor to handle 401 errors
|
||||
openHands.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// Save the last page before redirecting
|
||||
saveLastPage();
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export const setAuthTokenHeader = (token: string) => {
|
||||
openHands.defaults.headers.common.Authorization = `Bearer ${token}`;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from "react";
|
||||
import { FaListUl } from "react-icons/fa";
|
||||
import { useDispatch } from "react-redux";
|
||||
import posthog from "posthog-js";
|
||||
import { NavLink, useLocation } from "react-router";
|
||||
import { NavLink, useLocation, useNavigate } from "react-router";
|
||||
import { useGitHubUser } from "#/hooks/query/use-github-user";
|
||||
import { UserActions } from "./user-actions";
|
||||
import { AllHandsLogoButton } from "#/components/shared/buttons/all-hands-logo-button";
|
||||
@@ -17,14 +16,15 @@ import { setCurrentAgentState } from "#/state/agent-slice";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { TooltipButton } from "#/components/shared/buttons/tooltip-button";
|
||||
import { ConversationPanelWrapper } from "../conversation-panel/conversation-panel-wrapper";
|
||||
import { useLogout } from "#/hooks/mutation/use-logout";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
|
||||
import { saveLastPage } from "#/utils/last-page";
|
||||
import { useAppLogout } from "#/hooks/use-app-logout";
|
||||
|
||||
export function Sidebar() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const endSession = useEndSession();
|
||||
const user = useGitHubUser();
|
||||
@@ -35,8 +35,6 @@ export function Sidebar() {
|
||||
isError: settingsIsError,
|
||||
isFetching: isFetchingSettings,
|
||||
} = useSettings();
|
||||
const { mutateAsync: logout } = useLogout();
|
||||
const { mutate: saveUserSettings } = useSaveSettings();
|
||||
|
||||
const [settingsModalIsOpen, setSettingsModalIsOpen] = React.useState(false);
|
||||
|
||||
@@ -77,10 +75,12 @@ export function Sidebar() {
|
||||
endSession();
|
||||
};
|
||||
|
||||
const handleLogout = async () => {
|
||||
if (config?.APP_MODE === "saas") await logout();
|
||||
else saveUserSettings({ unset_github_token: true });
|
||||
posthog.reset();
|
||||
const { handleLogout: appLogout } = useAppLogout();
|
||||
|
||||
const handleLogout = () => {
|
||||
// Save the current page before logout
|
||||
saveLastPage();
|
||||
appLogout();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from "react";
|
||||
interface AuthContextType {
|
||||
githubTokenIsSet: boolean;
|
||||
setGitHubTokenIsSet: (value: boolean) => void;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
interface AuthContextProps extends React.PropsWithChildren {
|
||||
@@ -16,12 +17,22 @@ function AuthProvider({ children, initialGithubTokenIsSet }: AuthContextProps) {
|
||||
!!initialGithubTokenIsSet,
|
||||
);
|
||||
|
||||
const logout = React.useCallback(() => {
|
||||
setGitHubTokenIsSet(false);
|
||||
// Save the last page before logging out
|
||||
const { saveLastPage } = require('../utils/last-page');
|
||||
saveLastPage();
|
||||
// Clear any auth-related data from localStorage
|
||||
localStorage.removeItem("gh_token");
|
||||
}, [setGitHubTokenIsSet]);
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
githubTokenIsSet,
|
||||
setGitHubTokenIsSet,
|
||||
logout,
|
||||
}),
|
||||
[githubTokenIsSet, setGitHubTokenIsSet],
|
||||
[githubTokenIsSet, setGitHubTokenIsSet, logout],
|
||||
);
|
||||
|
||||
return <AuthContext value={value}>{children}</AuthContext>;
|
||||
|
||||
@@ -1,15 +1,42 @@
|
||||
import { useLogout } from "./mutation/use-logout";
|
||||
import { useSaveSettings } from "./mutation/use-save-settings";
|
||||
import { useConfig } from "./query/use-config";
|
||||
import { useNavigate } from "react-router";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import posthog from "posthog-js";
|
||||
|
||||
export const useAppLogout = () => {
|
||||
const { data: config } = useConfig();
|
||||
const { mutateAsync: logout } = useLogout();
|
||||
const { mutate: saveUserSettings } = useSaveSettings();
|
||||
const { setGitHubTokenIsSet } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogout = async () => {
|
||||
if (config?.APP_MODE === "saas") await logout();
|
||||
else saveUserSettings({ unset_github_token: true });
|
||||
try {
|
||||
if (config?.APP_MODE === "saas") {
|
||||
// First perform the logout
|
||||
await logout();
|
||||
// Then clear local state
|
||||
setGitHubTokenIsSet(false);
|
||||
localStorage.removeItem("gh_token");
|
||||
posthog.reset();
|
||||
// Finally navigate to logout page
|
||||
navigate("/logout");
|
||||
} else {
|
||||
saveUserSettings({ unset_github_token: true });
|
||||
setGitHubTokenIsSet(false);
|
||||
localStorage.removeItem("gh_token");
|
||||
posthog.reset();
|
||||
navigate("/");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Logout failed:", error);
|
||||
// Still navigate to logout page in case of error
|
||||
if (config?.APP_MODE === "saas") {
|
||||
navigate("/logout");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { handleLogout };
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
setUrl,
|
||||
} from "#/state/browser-slice";
|
||||
import { clearSelectedRepository } from "#/state/initial-query-slice";
|
||||
import { saveLastPage } from "#/utils/last-page";
|
||||
|
||||
export const useEndSession = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -15,6 +16,9 @@ export const useEndSession = () => {
|
||||
* End the current session by clearing the token and redirecting to the home page.
|
||||
*/
|
||||
const endSession = () => {
|
||||
// Save the last page before ending session
|
||||
//saveLastPage();
|
||||
|
||||
dispatch(clearSelectedRepository());
|
||||
|
||||
// Reset browser state to initial values
|
||||
|
||||
17
frontend/src/hooks/use-post-login-redirect.ts
Normal file
17
frontend/src/hooks/use-post-login-redirect.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { getLastPage, clearLastPage } from '../utils/last-page';
|
||||
|
||||
export const usePostLoginRedirect = (isLoggedIn: boolean) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoggedIn) {
|
||||
const lastPage = getLastPage();
|
||||
if (lastPage) {
|
||||
navigate(lastPage);
|
||||
clearLastPage();
|
||||
}
|
||||
}
|
||||
}, [isLoggedIn, navigate]);
|
||||
};
|
||||
@@ -1,5 +1,10 @@
|
||||
// this file generate by script, don't modify it manually!!!
|
||||
export enum I18nKey {
|
||||
AUTH$TRY_AGAIN = "AUTH$TRY_AGAIN",
|
||||
AUTH$LOGOUT_ERROR = "AUTH$LOGOUT_ERROR",
|
||||
AUTH$LOGGING_OUT = "AUTH$LOGGING_OUT",
|
||||
AUTH$LOGGED_OUT = "AUTH$LOGGED_OUT",
|
||||
AUTH$LOG_IN_WITH_GITHUB = "AUTH$LOG_IN_WITH_GITHUB",
|
||||
APP$TITLE = "APP$TITLE",
|
||||
BROWSER$TITLE = "BROWSER$TITLE",
|
||||
BROWSER$EMPTY_MESSAGE = "BROWSER$EMPTY_MESSAGE",
|
||||
|
||||
@@ -1,4 +1,79 @@
|
||||
{
|
||||
"AUTH$TRY_AGAIN": {
|
||||
"en": "Try Again",
|
||||
"ja": "再試行",
|
||||
"zh-CN": "重试",
|
||||
"zh-TW": "重試",
|
||||
"ko-KR": "다시 시도",
|
||||
"no": "Prøv igjen",
|
||||
"it": "Riprova",
|
||||
"pt": "Tentar novamente",
|
||||
"es": "Intentar de nuevo",
|
||||
"ar": "حاول مرة أخرى",
|
||||
"fr": "Réessayer",
|
||||
"tr": "Tekrar dene",
|
||||
"de": "Erneut versuchen"
|
||||
},
|
||||
"AUTH$LOGOUT_ERROR": {
|
||||
"en": "An error occurred while logging out. Please try again.",
|
||||
"ja": "ログアウト中にエラーが発生しました。もう一度お試しください。",
|
||||
"zh-CN": "退出登录时发生错误。请重试。",
|
||||
"zh-TW": "登出時發生錯誤。請重試。",
|
||||
"ko-KR": "로그아웃 중 오류가 발생했습니다. 다시 시도해 주세요.",
|
||||
"no": "Det oppstod en feil under utlogging. Vennligst prøv igjen.",
|
||||
"it": "Si è verificato un errore durante la disconnessione. Per favore riprova.",
|
||||
"pt": "Ocorreu um erro ao sair. Por favor, tente novamente.",
|
||||
"es": "Ocurrió un error al cerrar sesión. Por favor, inténtelo de nuevo.",
|
||||
"ar": "حدث خطأ أثناء تسجيل الخروج. يرجى المحاولة مرة أخرى.",
|
||||
"fr": "Une erreur s'est produite lors de la déconnexion. Veuillez réessayer.",
|
||||
"tr": "Oturum kapatılırken bir hata oluştu. Lütfen tekrar deneyin.",
|
||||
"de": "Beim Abmelden ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut."
|
||||
},
|
||||
"AUTH$LOGGING_OUT": {
|
||||
"en": "Logging out...",
|
||||
"ja": "ログアウト中...",
|
||||
"zh-CN": "正在退出登录...",
|
||||
"zh-TW": "正在登出...",
|
||||
"ko-KR": "로그아웃 중...",
|
||||
"no": "Logger ut...",
|
||||
"it": "Disconnessione in corso...",
|
||||
"pt": "Saindo...",
|
||||
"es": "Cerrando sesión...",
|
||||
"ar": "جاري تسجيل الخروج...",
|
||||
"fr": "Déconnexion en cours...",
|
||||
"tr": "Oturum kapatılıyor...",
|
||||
"de": "Abmeldung läuft..."
|
||||
},
|
||||
"AUTH$LOGGED_OUT": {
|
||||
"en": "You have been logged out",
|
||||
"ja": "ログアウトしました",
|
||||
"zh-CN": "您已退出登录",
|
||||
"zh-TW": "您已登出",
|
||||
"ko-KR": "로그아웃되었습니다",
|
||||
"no": "Du har blitt logget ut",
|
||||
"it": "Sei stato disconnesso",
|
||||
"pt": "Você foi desconectado",
|
||||
"es": "Has cerrado sesión",
|
||||
"ar": "لقد تم تسجيل خروجك",
|
||||
"fr": "Vous avez été déconnecté",
|
||||
"tr": "Oturumunuz kapatıldı",
|
||||
"de": "Sie wurden abgemeldet"
|
||||
},
|
||||
"AUTH$LOG_IN_WITH_GITHUB": {
|
||||
"en": "Log in with GitHub",
|
||||
"ja": "GitHubでログイン",
|
||||
"zh-CN": "使用GitHub登录",
|
||||
"zh-TW": "使用GitHub登入",
|
||||
"ko-KR": "GitHub로 로그인",
|
||||
"no": "Logg inn med GitHub",
|
||||
"it": "Accedi con GitHub",
|
||||
"pt": "Entrar com GitHub",
|
||||
"es": "Iniciar sesión con GitHub",
|
||||
"ar": "تسجيل الدخول باستخدام GitHub",
|
||||
"fr": "Se connecter avec GitHub",
|
||||
"tr": "GitHub ile giriş yap",
|
||||
"de": "Mit GitHub anmelden"
|
||||
},
|
||||
"APP$TITLE": {
|
||||
"en": "App",
|
||||
"ja": "アプリ",
|
||||
|
||||
@@ -6,6 +6,9 @@ import {
|
||||
} from "@react-router/dev/routes";
|
||||
|
||||
export default [
|
||||
layout("routes/_no-sidebar/route.tsx", [
|
||||
route("logout", "routes/logout.tsx"),
|
||||
]),
|
||||
layout("routes/_oh/route.tsx", [
|
||||
index("routes/_oh._index/route.tsx"),
|
||||
route("settings", "routes/settings.tsx", [
|
||||
|
||||
10
frontend/src/routes/_no-sidebar/route.tsx
Normal file
10
frontend/src/routes/_no-sidebar/route.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react";
|
||||
import { Outlet } from "react-router";
|
||||
|
||||
export default function NoSidebarLayout() {
|
||||
return (
|
||||
<div className="h-screen w-screen">
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { AgentState } from "#/types/agent-state";
|
||||
import { ErrorObservation } from "#/types/core/observations";
|
||||
import { useEndSession } from "../../../hooks/use-end-session";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { saveLastPage } from "#/utils/last-page";
|
||||
|
||||
interface ServerError {
|
||||
error: boolean | string;
|
||||
@@ -32,6 +33,8 @@ export const useHandleWSEvents = () => {
|
||||
|
||||
if (isServerError(event)) {
|
||||
if (event.error_code === 401) {
|
||||
// Save the last page before ending session
|
||||
saveLastPage();
|
||||
displayErrorToast("Session expired.");
|
||||
endSession();
|
||||
return;
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useMigrateUserConsent } from "#/hooks/use-migrate-user-consent";
|
||||
import { useBalance } from "#/hooks/query/use-balance";
|
||||
import { SetupPaymentModal } from "#/components/features/payment/setup-payment-modal";
|
||||
import { displaySuccessToast } from "#/utils/custom-toast-handlers";
|
||||
import { usePostLoginRedirect } from "#/hooks/use-post-login-redirect";
|
||||
|
||||
export function ErrorBoundary() {
|
||||
const error = useRouteError();
|
||||
@@ -112,8 +113,27 @@ export default function MainApp() {
|
||||
}, [error?.status, pathname, isFetching]);
|
||||
|
||||
const userIsAuthed = !!isAuthed && !authError;
|
||||
const isOnLogoutPage = pathname === "/logout";
|
||||
|
||||
// In SaaS mode, when not authenticated and not on logout page, redirect to GitHub auth
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
!isFetchingAuth &&
|
||||
!userIsAuthed &&
|
||||
config.data?.APP_MODE === "saas" &&
|
||||
!isOnLogoutPage &&
|
||||
gitHubAuthUrl
|
||||
) {
|
||||
window.location.href = gitHubAuthUrl;
|
||||
}
|
||||
}, [isFetchingAuth, userIsAuthed, config.data?.APP_MODE, isOnLogoutPage, gitHubAuthUrl]);
|
||||
|
||||
// Only show waitlist modal in non-SaaS mode
|
||||
const renderWaitlistModal =
|
||||
!isFetchingAuth && !userIsAuthed && config.data?.APP_MODE === "saas";
|
||||
!isFetchingAuth && !userIsAuthed && config.data?.APP_MODE !== "saas";
|
||||
|
||||
// Handle redirection to last page after login
|
||||
usePostLoginRedirect(userIsAuthed);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
104
frontend/src/routes/logout.tsx
Normal file
104
frontend/src/routes/logout.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { generateGitHubAuthUrl } from "#/utils/generate-github-auth-url";
|
||||
import { AllHandsLogoButton } from "#/components/shared/buttons/all-hands-logo-button";
|
||||
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
|
||||
// Hardcoded translations since we don't want to load i18n
|
||||
const translations = {
|
||||
LOGGING_OUT: "Logging out...",
|
||||
LOGGED_OUT: "You have been logged out",
|
||||
LOG_IN_WITH_GITHUB: "Log in with GitHub",
|
||||
LOGOUT_ERROR: "An error occurred while logging out. Please try again.",
|
||||
TRY_AGAIN: "Try Again",
|
||||
};
|
||||
|
||||
export default function LogoutPage() {
|
||||
const navigate = useNavigate();
|
||||
const [isLoggingOut, setIsLoggingOut] = React.useState(true);
|
||||
const [hasLogoutError, setHasLogoutError] = React.useState(false);
|
||||
const hasAttemptedLogout = React.useRef(false);
|
||||
const config = useConfig();
|
||||
|
||||
// Generate GitHub auth URL once on mount
|
||||
const gitHubAuthUrl = React.useMemo(
|
||||
() => generateGitHubAuthUrl("github", new URL(window.location.href)),
|
||||
[],
|
||||
);
|
||||
|
||||
const handleLogoClick = React.useCallback(() => {
|
||||
// In SaaS mode, redirect directly to GitHub auth
|
||||
if (config.data?.APP_MODE === "saas" && gitHubAuthUrl) {
|
||||
window.location.href = gitHubAuthUrl;
|
||||
} else {
|
||||
navigate("/");
|
||||
}
|
||||
}, [config.data?.APP_MODE, gitHubAuthUrl, navigate]);
|
||||
|
||||
const performLogout = React.useCallback(async () => {
|
||||
// Only attempt logout once
|
||||
if (hasAttemptedLogout.current) return;
|
||||
hasAttemptedLogout.current = true;
|
||||
|
||||
try {
|
||||
// Use the OpenHands API client for consistent headers and error handling
|
||||
await OpenHands.logout();
|
||||
|
||||
// Clear any auth-related data from localStorage
|
||||
localStorage.removeItem("gh_token");
|
||||
|
||||
setIsLoggingOut(false);
|
||||
} catch (error) {
|
||||
console.error('Logout error:', error);
|
||||
setHasLogoutError(true);
|
||||
setIsLoggingOut(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
performLogout();
|
||||
}, [performLogout]);
|
||||
|
||||
return (
|
||||
<div className="h-screen w-screen flex items-center justify-center bg-base">
|
||||
<div className="flex flex-col items-center gap-8 p-8 rounded-lg bg-neutral-800">
|
||||
<AllHandsLogoButton onClick={handleLogoClick} />
|
||||
{isLoggingOut ? (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<LoadingSpinner size="large" />
|
||||
<span className="text-neutral-200">{translations.LOGGING_OUT}</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<h1 className="text-2xl font-bold text-neutral-200">
|
||||
{hasLogoutError
|
||||
? translations.LOGOUT_ERROR
|
||||
: translations.LOGGED_OUT}
|
||||
</h1>
|
||||
<div className="flex flex-col gap-4">
|
||||
{hasLogoutError && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={performLogout}
|
||||
className="px-4 py-2 bg-primary text-white rounded hover:bg-primary/90 text-center"
|
||||
>
|
||||
{translations.TRY_AGAIN}
|
||||
</button>
|
||||
)}
|
||||
{!hasLogoutError && gitHubAuthUrl && (
|
||||
<a
|
||||
href={gitHubAuthUrl}
|
||||
className="px-4 py-2 bg-primary text-white rounded hover:bg-primary/90 text-center"
|
||||
>
|
||||
{translations.LOG_IN_WITH_GITHUB}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
frontend/src/utils/last-page.ts
Normal file
17
frontend/src/utils/last-page.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
const LAST_PAGE_KEY = 'openhands_last_page';
|
||||
|
||||
export const saveLastPage = () => {
|
||||
const currentPath = window.location.pathname;
|
||||
// Don't save login, logout, or settings pages
|
||||
if (!currentPath.includes('/settings') && currentPath !== '/' && currentPath !== '/logout') {
|
||||
localStorage.setItem(LAST_PAGE_KEY, currentPath);
|
||||
}
|
||||
};
|
||||
|
||||
export const getLastPage = (): string | null => {
|
||||
return localStorage.getItem(LAST_PAGE_KEY);
|
||||
};
|
||||
|
||||
export const clearLastPage = () => {
|
||||
localStorage.removeItem(LAST_PAGE_KEY);
|
||||
};
|
||||
Reference in New Issue
Block a user