Update login (#8743)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
chuckbutkus
2025-05-28 13:53:35 -04:00
committed by GitHub
parent 6fe5da810b
commit 9f86f731a7
8 changed files with 103 additions and 21 deletions

View File

@@ -129,7 +129,7 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
isDisabled={isPending}
>
{isPending
? t(I18nKey.FEEDBACK$SUBMITTING_LABEL) || "Submitting..."
? t(I18nKey.FEEDBACK$SUBMITTING_LABEL)
: t(I18nKey.FEEDBACK$SHARE_LABEL)}
</BrandButton>
<BrandButton
@@ -144,8 +144,7 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
</div>
{isPending && (
<p className="text-sm text-center text-neutral-400">
{t(I18nKey.FEEDBACK$SUBMITTING_MESSAGE) ||
"Submitting your feedback, please wait..."}
{t(I18nKey.FEEDBACK$SUBMITTING_MESSAGE)}
</p>
)}
</form>

View File

@@ -9,7 +9,6 @@ import GitHubLogo from "#/assets/branding/github-logo.svg?react";
import GitLabLogo from "#/assets/branding/gitlab-logo.svg?react";
import { useAuthUrl } from "#/hooks/use-auth-url";
import { GetConfigResponse } from "#/api/open-hands.types";
import { LoginMethod, setLoginMethod } from "#/utils/local-storage";
interface AuthModalProps {
githubAuthUrl: string | null;
@@ -26,10 +25,6 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
const handleGitHubAuth = () => {
if (githubAuthUrl) {
// Store the login method in local storage (only in SAAS mode)
if (appMode === "saas") {
setLoginMethod(LoginMethod.GITHUB);
}
// Always start the OIDC flow, let the backend handle TOS check
window.location.href = githubAuthUrl;
}
@@ -37,10 +32,6 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
const handleGitLabAuth = () => {
if (gitlabAuthUrl) {
// Store the login method in local storage (only in SAAS mode)
if (appMode === "saas") {
setLoginMethod(LoginMethod.GITLAB);
}
// Always start the OIDC flow, let the backend handle TOS check
window.location.href = gitlabAuthUrl;
}

View File

@@ -1,5 +1,4 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "react-router";
import posthog from "posthog-js";
import OpenHands from "#/api/open-hands";
import { useConfig } from "../query/use-config";
@@ -8,7 +7,6 @@ import { clearLoginData } from "#/utils/local-storage";
export const useLogout = () => {
const queryClient = useQueryClient();
const { data: config } = useConfig();
const navigate = useNavigate();
return useMutation({
mutationFn: () => OpenHands.logout(config?.APP_MODE ?? "oss"),
@@ -24,7 +22,6 @@ export const useLogout = () => {
}
posthog.reset();
await navigate("/");
// Refresh the page after all logout logic is completed
window.location.reload();

View File

@@ -0,0 +1,49 @@
import { useEffect } from "react";
import { useLocation, useNavigate } from "react-router";
import { useIsAuthed } from "./query/use-is-authed";
import { LoginMethod, setLoginMethod } from "#/utils/local-storage";
import { useConfig } from "./query/use-config";
/**
* Hook to handle authentication callback and set login method after successful authentication
*/
export const useAuthCallback = () => {
const location = useLocation();
const { data: isAuthed, isLoading: isAuthLoading } = useIsAuthed();
const { data: config } = useConfig();
const navigate = useNavigate();
useEffect(() => {
// Only run in SAAS mode
if (config?.APP_MODE !== "saas") {
return;
}
// Wait for auth to load
if (isAuthLoading) {
return;
}
// Only set login method if authentication was successful
if (!isAuthed) {
return;
}
// Check if we have a login_method query parameter
const searchParams = new URLSearchParams(location.search);
const loginMethod = searchParams.get("login_method");
// Set the login method if it's valid
if (
loginMethod === LoginMethod.GITHUB ||
loginMethod === LoginMethod.GITLAB
) {
setLoginMethod(loginMethod as LoginMethod);
// Clean up the URL by removing the login_method parameter
searchParams.delete("login_method");
const newUrl = `${location.pathname}${searchParams.toString() ? `?${searchParams.toString()}` : ""}`;
navigate(newUrl, { replace: true });
}
}, [isAuthed, isAuthLoading, location.search, config?.APP_MODE]);
};

View File

@@ -53,8 +53,12 @@ export const useAutoLogin = () => {
// If we have an auth URL, redirect to it
if (authUrl) {
// Add the login method as a query parameter
const url = new URL(authUrl);
url.searchParams.append("login_method", loginMethod);
// After successful login, the user will be redirected back and can navigate to the last page
window.location.href = authUrl;
window.location.href = url.toString();
}
}, [
config?.APP_MODE,

View File

@@ -36,6 +36,7 @@ import { useDocumentTitleFromState } from "#/hooks/use-document-title-from-state
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
import OpenHands from "#/api/open-hands";
import { TabContent } from "#/components/layout/tab-content";
import { useIsAuthed } from "#/hooks/query/use-is-authed";
function AppContent() {
useConversationConfig();
@@ -43,6 +44,7 @@ function AppContent() {
const { data: settings } = useSettings();
const { conversationId } = useConversationId();
const { data: conversation, isFetched } = useActiveConversation();
const { data: isAuthed } = useIsAuthed();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const dispatch = useDispatch();
@@ -54,13 +56,13 @@ function AppContent() {
const [width, setWidth] = React.useState(window.innerWidth);
React.useEffect(() => {
if (isFetched && !conversation) {
if (isFetched && !conversation && isAuthed) {
displayErrorToast(
"This conversation does not exist, or you do not have permission to access it.",
);
navigate("/");
}
}, [conversation, isFetched]);
}, [conversation, isFetched, isAuthed]);
React.useEffect(() => {
dispatch(clearTerminal());

View File

@@ -23,6 +23,7 @@ import { SetupPaymentModal } from "#/components/features/payment/setup-payment-m
import { displaySuccessToast } from "#/utils/custom-toast-handlers";
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
import { useAutoLogin } from "#/hooks/use-auto-login";
import { useAuthCallback } from "#/hooks/use-auth-callback";
import { LOCAL_STORAGE_KEYS } from "#/utils/local-storage";
export function ErrorBoundary() {
@@ -88,6 +89,9 @@ export default function MainApp() {
// Auto-login if login method is stored in local storage
useAutoLogin();
// Handle authentication callback and set login method after successful authentication
useAuthCallback();
React.useEffect(() => {
// Don't change language when on TOS page
if (!isOnTosPage && settings?.LANGUAGE) {
@@ -131,8 +135,8 @@ export default function MainApp() {
}
}, [error?.status, pathname, isOnTosPage]);
// Check if login method exists in local storage
const loginMethodExists = React.useMemo(() => {
// Function to check if login method exists in local storage
const checkLoginMethodExists = React.useCallback(() => {
// Only check localStorage if we're in a browser environment
if (typeof window !== "undefined" && window.localStorage) {
return localStorage.getItem(LOCAL_STORAGE_KEYS.LOGIN_METHOD) !== null;
@@ -140,6 +144,39 @@ export default function MainApp() {
return false;
}, []);
// State to track if login method exists
const [loginMethodExists, setLoginMethodExists] = React.useState(
checkLoginMethodExists(),
);
// Listen for storage events to update loginMethodExists when logout happens
React.useEffect(() => {
const handleStorageChange = (event: StorageEvent) => {
if (event.key === LOCAL_STORAGE_KEYS.LOGIN_METHOD) {
setLoginMethodExists(checkLoginMethodExists());
}
};
// Also check on window focus, as logout might happen in another tab
const handleWindowFocus = () => {
setLoginMethodExists(checkLoginMethodExists());
};
window.addEventListener("storage", handleStorageChange);
window.addEventListener("focus", handleWindowFocus);
return () => {
window.removeEventListener("storage", handleStorageChange);
window.removeEventListener("focus", handleWindowFocus);
};
}, [checkLoginMethodExists]);
// Check login method status when auth status changes
React.useEffect(() => {
// When auth status changes (especially on logout), recheck login method
setLoginMethodExists(checkLoginMethodExists());
}, [isAuthed, checkLoginMethodExists]);
const renderAuthModal =
!isAuthed &&
!isAuthError &&

View File

@@ -16,5 +16,8 @@ export const generateAuthUrl = (identityProvider: string, requestUrl: URL) => {
authUrl = `auth.${requestUrl.hostname}`;
}
const scope = "openid email profile"; // OAuth scope - not user-facing
return `https://${authUrl}/realms/allhands/protocol/openid-connect/auth?client_id=allhands&kc_idp_hint=${identityProvider}&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}&state=${encodeURIComponent(requestUrl.href)}`;
const separator = requestUrl.search ? "&" : "?";
const cleanHref = requestUrl.href.replace(/\/$/, "");
const state = `${cleanHref}${separator}login_method=${identityProvider}`;
return `https://${authUrl}/realms/allhands/protocol/openid-connect/auth?client_id=allhands&kc_idp_hint=${identityProvider}&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}&state=${encodeURIComponent(state)}`;
};