mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-11 16:18:07 -05:00
Compare commits
1 Commits
master
...
fix/accoun
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc85a37305 |
@@ -3,15 +3,16 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/__legacy__/ui/popover";
|
||||
import Link from "next/link";
|
||||
import * as React from "react";
|
||||
import { getAccountMenuOptionIcon, MenuItemGroup } from "../../helpers";
|
||||
import { AccountLogoutOption } from "./components/AccountLogoutOption";
|
||||
import { PublishAgentModal } from "@/components/contextual/PublishAgentModal/PublishAgentModal";
|
||||
import { Skeleton } from "@/components/__legacy__/ui/skeleton";
|
||||
import Avatar, {
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/components/atoms/Avatar/Avatar";
|
||||
import { PublishAgentModal } from "@/components/contextual/PublishAgentModal/PublishAgentModal";
|
||||
import Link from "next/link";
|
||||
import * as React from "react";
|
||||
import { getAccountMenuOptionIcon, MenuItemGroup } from "../../helpers";
|
||||
import { AccountLogoutOption } from "./components/AccountLogoutOption";
|
||||
|
||||
interface Props {
|
||||
userName?: string;
|
||||
@@ -19,6 +20,7 @@ interface Props {
|
||||
avatarSrc?: string;
|
||||
hideNavBarUsername?: boolean;
|
||||
menuItemGroups: MenuItemGroup[];
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function AccountMenu({
|
||||
@@ -26,6 +28,7 @@ export function AccountMenu({
|
||||
userEmail,
|
||||
avatarSrc,
|
||||
menuItemGroups,
|
||||
isLoading = false,
|
||||
}: Props) {
|
||||
const popupId = React.useId();
|
||||
|
||||
@@ -63,15 +66,24 @@ export function AccountMenu({
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="relative flex h-[47px] w-[173px] flex-col items-start justify-center gap-1">
|
||||
<div className="max-w-[10.5rem] truncate font-sans text-base font-semibold leading-none text-white dark:text-neutral-200">
|
||||
{userName}
|
||||
</div>
|
||||
<div
|
||||
data-testid="account-menu-user-email"
|
||||
className="max-w-[10.5rem] truncate font-sans text-base font-normal leading-none text-white dark:text-neutral-400"
|
||||
>
|
||||
{userEmail}
|
||||
</div>
|
||||
{isLoading || !userName || !userEmail ? (
|
||||
<>
|
||||
<Skeleton className="h-4 w-24 bg-white/40" />
|
||||
<Skeleton className="h-4 w-32 bg-white/40" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="max-w-[10.5rem] truncate font-sans text-base font-semibold leading-none text-white dark:text-neutral-200">
|
||||
{userName}
|
||||
</div>
|
||||
<div
|
||||
data-testid="account-menu-user-email"
|
||||
className="max-w-[10.5rem] truncate font-sans text-base font-normal leading-none text-white dark:text-neutral-400"
|
||||
>
|
||||
{userEmail}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -28,7 +28,9 @@ export function AccountLogoutOption() {
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setIsLoggingOut(false);
|
||||
setTimeout(() => {
|
||||
setIsLoggingOut(false);
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,12 +26,19 @@ export function NavbarView({ isLoggedIn, previewBranchName }: NavbarViewProps) {
|
||||
const dynamicMenuItems = getAccountMenuItems(user?.role);
|
||||
const isChatEnabled = useGetFlag(Flag.CHAT);
|
||||
|
||||
const { data: profile } = useGetV2GetUserProfile({
|
||||
query: {
|
||||
select: (res) => (res.status === 200 ? res.data : null),
|
||||
enabled: isLoggedIn,
|
||||
const { data: profile, isLoading: isProfileLoading } = useGetV2GetUserProfile(
|
||||
{
|
||||
query: {
|
||||
select: (res) => (res.status === 200 ? res.data : null),
|
||||
enabled: isLoggedIn && !!user,
|
||||
// Include user ID in query key to ensure cache invalidation when user changes
|
||||
queryKey: ["/api/store/profile", user?.id],
|
||||
},
|
||||
},
|
||||
});
|
||||
);
|
||||
|
||||
const { isUserLoading } = useSupabase();
|
||||
const isLoadingProfile = isProfileLoading || isUserLoading;
|
||||
|
||||
const linksWithChat = useMemo(() => {
|
||||
const chatLink = { name: "Chat", href: "/chat" };
|
||||
@@ -84,6 +91,7 @@ export function NavbarView({ isLoggedIn, previewBranchName }: NavbarViewProps) {
|
||||
userEmail={profile?.name}
|
||||
avatarSrc={profile?.avatar_url ?? ""}
|
||||
menuItemGroups={dynamicMenuItems}
|
||||
isLoading={isLoadingProfile}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,9 @@ import {
|
||||
RefundRequest,
|
||||
TransactionHistory,
|
||||
} from "@/lib/autogpt-server-api/types";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
|
||||
export default function useCredits({
|
||||
fetchInitialCredits = false,
|
||||
@@ -30,6 +31,7 @@ export default function useCredits({
|
||||
fetchRefundRequests: () => void;
|
||||
formatCredits: (credit: number | null) => string;
|
||||
} {
|
||||
const { isLoggedIn } = useSupabase();
|
||||
const [credits, setCredits] = useState<number | null>(null);
|
||||
const [autoTopUpConfig, setAutoTopUpConfig] = useState<{
|
||||
amount: number;
|
||||
@@ -40,49 +42,90 @@ export default function useCredits({
|
||||
const router = useRouter();
|
||||
|
||||
const fetchCredits = useCallback(async () => {
|
||||
const response = await api.getUserCredit();
|
||||
setCredits(response.credits);
|
||||
}, [api]);
|
||||
if (!isLoggedIn) return;
|
||||
try {
|
||||
const response = await api.getUserCredit();
|
||||
if (response) {
|
||||
setCredits(response.credits);
|
||||
} else {
|
||||
setCredits(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching credits:", error);
|
||||
setCredits(null);
|
||||
}
|
||||
}, [api, isLoggedIn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!fetchInitialCredits) return;
|
||||
if (!fetchInitialCredits || !isLoggedIn) return;
|
||||
fetchCredits();
|
||||
}, [fetchCredits, fetchInitialCredits]);
|
||||
}, [fetchCredits, fetchInitialCredits, isLoggedIn]);
|
||||
|
||||
// Clear credits when user logs out
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
setCredits(null);
|
||||
}
|
||||
}, [isLoggedIn]);
|
||||
|
||||
const fetchAutoTopUpConfig = useCallback(async () => {
|
||||
const response = await api.getAutoTopUpConfig();
|
||||
setAutoTopUpConfig(response);
|
||||
}, [api]);
|
||||
if (!isLoggedIn) return;
|
||||
try {
|
||||
const response = await api.getAutoTopUpConfig();
|
||||
setAutoTopUpConfig(response || null);
|
||||
} catch (error) {
|
||||
console.error("Error fetching auto top-up config:", error);
|
||||
setAutoTopUpConfig(null);
|
||||
}
|
||||
}, [api, isLoggedIn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!fetchInitialAutoTopUpConfig) return;
|
||||
if (!fetchInitialAutoTopUpConfig || !isLoggedIn) return;
|
||||
fetchAutoTopUpConfig();
|
||||
}, [fetchAutoTopUpConfig, fetchInitialAutoTopUpConfig]);
|
||||
}, [fetchAutoTopUpConfig, fetchInitialAutoTopUpConfig, isLoggedIn]);
|
||||
|
||||
// Clear auto top-up config when user logs out
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
setAutoTopUpConfig(null);
|
||||
}
|
||||
}, [isLoggedIn]);
|
||||
|
||||
const updateAutoTopUpConfig = useCallback(
|
||||
async (amount: number, threshold: number) => {
|
||||
if (!isLoggedIn) return;
|
||||
await api.setAutoTopUpConfig({ amount, threshold });
|
||||
fetchAutoTopUpConfig();
|
||||
},
|
||||
[api, fetchAutoTopUpConfig],
|
||||
[api, fetchAutoTopUpConfig, isLoggedIn],
|
||||
);
|
||||
|
||||
const requestTopUp = useCallback(
|
||||
async (credit_amount: number) => {
|
||||
if (!isLoggedIn) return;
|
||||
const response = await api.requestTopUp(credit_amount);
|
||||
router.push(response.checkout_url);
|
||||
},
|
||||
[api, router],
|
||||
[api, router, isLoggedIn],
|
||||
);
|
||||
|
||||
const refundTopUp = useCallback(
|
||||
async (transaction_key: string, reason: string) => {
|
||||
const refunded_amount = await api.refundTopUp(transaction_key, reason);
|
||||
await fetchCredits();
|
||||
setTransactionHistory(await api.getTransactionHistory());
|
||||
return refunded_amount;
|
||||
if (!isLoggedIn) return 0;
|
||||
try {
|
||||
const refunded_amount = await api.refundTopUp(transaction_key, reason);
|
||||
await fetchCredits();
|
||||
const history = await api.getTransactionHistory();
|
||||
if (history) {
|
||||
setTransactionHistory(history);
|
||||
}
|
||||
return refunded_amount;
|
||||
} catch (error) {
|
||||
console.error("Error refunding top-up:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[api, fetchCredits],
|
||||
[api, fetchCredits, isLoggedIn],
|
||||
);
|
||||
|
||||
const [transactionHistory, setTransactionHistory] =
|
||||
@@ -92,38 +135,68 @@ export default function useCredits({
|
||||
});
|
||||
|
||||
const fetchTransactionHistory = useCallback(async () => {
|
||||
const response = await api.getTransactionHistory(
|
||||
transactionHistory.next_transaction_time,
|
||||
20,
|
||||
);
|
||||
setTransactionHistory({
|
||||
transactions: [
|
||||
...transactionHistory.transactions,
|
||||
...response.transactions,
|
||||
],
|
||||
next_transaction_time: response.next_transaction_time,
|
||||
});
|
||||
}, [api, transactionHistory]);
|
||||
if (!isLoggedIn) return;
|
||||
try {
|
||||
const response = await api.getTransactionHistory(
|
||||
transactionHistory.next_transaction_time,
|
||||
20,
|
||||
);
|
||||
if (response) {
|
||||
setTransactionHistory({
|
||||
transactions: [
|
||||
...transactionHistory.transactions,
|
||||
...response.transactions,
|
||||
],
|
||||
next_transaction_time: response.next_transaction_time,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching transaction history:", error);
|
||||
}
|
||||
}, [api, transactionHistory, isLoggedIn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!fetchInitialTransactionHistory) return;
|
||||
if (!fetchInitialTransactionHistory || !isLoggedIn) return;
|
||||
fetchTransactionHistory();
|
||||
// Note: We only need to fetch transaction history once.
|
||||
// Hence, we should avoid `fetchTransactionHistory` to the dependency array.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [fetchInitialTransactionHistory]);
|
||||
}, [fetchInitialTransactionHistory, isLoggedIn]);
|
||||
|
||||
// Clear transaction history when user logs out
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
setTransactionHistory({
|
||||
transactions: [],
|
||||
next_transaction_time: null,
|
||||
});
|
||||
}
|
||||
}, [isLoggedIn]);
|
||||
|
||||
const [refundRequests, setRefundRequests] = useState<RefundRequest[]>([]);
|
||||
|
||||
const fetchRefundRequests = useCallback(async () => {
|
||||
const response = await api.getRefundRequests();
|
||||
setRefundRequests(response);
|
||||
}, [api]);
|
||||
if (!isLoggedIn) return;
|
||||
try {
|
||||
const response = await api.getRefundRequests();
|
||||
setRefundRequests(response || []);
|
||||
} catch (error) {
|
||||
console.error("Error fetching refund requests:", error);
|
||||
setRefundRequests([]);
|
||||
}
|
||||
}, [api, isLoggedIn]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!fetchInitialRefundRequests) return;
|
||||
if (!fetchInitialRefundRequests || !isLoggedIn) return;
|
||||
fetchRefundRequests();
|
||||
}, [fetchRefundRequests, fetchInitialRefundRequests]);
|
||||
}, [fetchRefundRequests, fetchInitialRefundRequests, isLoggedIn]);
|
||||
|
||||
// Clear refund requests when user logs out
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn) {
|
||||
setRefundRequests([]);
|
||||
}
|
||||
}, [isLoggedIn]);
|
||||
|
||||
const formatCredits = useCallback((credit: number | null) => {
|
||||
if (credit === null) {
|
||||
|
||||
@@ -51,9 +51,11 @@ export async function fetchUser(): Promise<FetchUserResult> {
|
||||
const { user, error } = await getCurrentUser();
|
||||
|
||||
if (error || !user) {
|
||||
// Only mark as loaded if we got an explicit error (not just no user)
|
||||
// This allows retrying when cookies aren't ready yet after login
|
||||
return {
|
||||
user: null,
|
||||
hasLoadedUser: true,
|
||||
hasLoadedUser: !!error, // Only true if there was an error, not just no user
|
||||
isUserLoading: false,
|
||||
};
|
||||
}
|
||||
@@ -68,7 +70,7 @@ export async function fetchUser(): Promise<FetchUserResult> {
|
||||
console.error("Get user error:", error);
|
||||
return {
|
||||
user: null,
|
||||
hasLoadedUser: true,
|
||||
hasLoadedUser: true, // Error means we tried and failed, so mark as loaded
|
||||
isUserLoading: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { create } from "zustand";
|
||||
import { serverLogout, type ServerLogoutOptions } from "../actions";
|
||||
import {
|
||||
broadcastLogout,
|
||||
isProtectedPage,
|
||||
setWebSocketDisconnectIntent,
|
||||
setupSessionEventListeners,
|
||||
} from "../helpers";
|
||||
@@ -77,11 +78,14 @@ export const useSupabaseStore = create<SupabaseStoreState>((set, get) => {
|
||||
if (!get().hasLoadedUser || !get().user) {
|
||||
set({ isUserLoading: true });
|
||||
const result = await fetchUser();
|
||||
|
||||
// Always update state with fetch result
|
||||
set(result);
|
||||
|
||||
// If fetchUser didn't return a user, validate the session to ensure we have the latest state
|
||||
// This handles race conditions after login where cookies might not be immediately available
|
||||
if (!result.user) {
|
||||
if (!result.user && !result.hasLoadedUser) {
|
||||
// Cookies might not be ready yet, retry validation
|
||||
const validationResult = await validateSessionHelper({
|
||||
pathname: params.pathname,
|
||||
currentUser: null,
|
||||
@@ -93,10 +97,69 @@ export const useSupabaseStore = create<SupabaseStoreState>((set, get) => {
|
||||
hasLoadedUser: true,
|
||||
isUserLoading: false,
|
||||
});
|
||||
} else if (!validationResult.isValid) {
|
||||
// Session is invalid, mark as loaded so we don't keep retrying
|
||||
set({
|
||||
hasLoadedUser: true,
|
||||
isUserLoading: false,
|
||||
});
|
||||
} else {
|
||||
// Validation succeeded but no user - might be cookies not ready
|
||||
// If we're on a protected page, schedule a retry since we should have a user
|
||||
const isProtected = isProtectedPage(params.pathname);
|
||||
if (isProtected && params.router) {
|
||||
// Retry after a short delay to allow cookies to propagate
|
||||
// Use router.refresh() to trigger a re-initialization
|
||||
setTimeout(() => {
|
||||
const currentState = get();
|
||||
if (
|
||||
!currentState.user &&
|
||||
isProtectedPage(currentState.currentPathname)
|
||||
) {
|
||||
// Trigger router refresh to cause re-initialization
|
||||
params.router.refresh();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
// Don't mark as loaded yet, allow retry on next initialization
|
||||
set({
|
||||
isUserLoading: false,
|
||||
});
|
||||
}
|
||||
} else if (!result.user && result.hasLoadedUser) {
|
||||
// Explicit error or already marked as loaded - don't retry
|
||||
set({
|
||||
isUserLoading: false,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
set({ isUserLoading: false });
|
||||
// Even if we have a user, validate session to catch account switches
|
||||
// This ensures that if user logged out and logged in with different account,
|
||||
// we detect the change immediately
|
||||
const currentUser = get().user;
|
||||
if (currentUser) {
|
||||
const validationResult = await validateSessionHelper({
|
||||
pathname: params.pathname,
|
||||
currentUser,
|
||||
});
|
||||
|
||||
// Update user if IDs differ (account switch detected)
|
||||
if (
|
||||
validationResult.user &&
|
||||
validationResult.isValid &&
|
||||
validationResult.user.id !== currentUser.id
|
||||
) {
|
||||
set({
|
||||
user: validationResult.user,
|
||||
hasLoadedUser: true,
|
||||
isUserLoading: false,
|
||||
});
|
||||
} else {
|
||||
set({ isUserLoading: false });
|
||||
}
|
||||
} else {
|
||||
set({ isUserLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
const existingCleanup = get().listenersCleanup;
|
||||
@@ -140,10 +203,20 @@ export const useSupabaseStore = create<SupabaseStoreState>((set, get) => {
|
||||
|
||||
broadcastLogout();
|
||||
|
||||
// Clear React Query cache to prevent stale data from old user
|
||||
if (typeof window !== "undefined") {
|
||||
const { getQueryClient } = await import("@/lib/react-query/queryClient");
|
||||
const queryClient = getQueryClient();
|
||||
queryClient.clear();
|
||||
}
|
||||
|
||||
// Reset all state to ensure fresh initialization on next login
|
||||
set({
|
||||
user: null,
|
||||
hasLoadedUser: false,
|
||||
isUserLoading: false,
|
||||
initializationPromise: null, // Force fresh initialization
|
||||
lastValidation: 0, // Reset validation timestamp
|
||||
});
|
||||
|
||||
await serverLogout(options);
|
||||
@@ -186,15 +259,47 @@ export const useSupabaseStore = create<SupabaseStoreState>((set, get) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.user && result.shouldUpdateUser) {
|
||||
set({ user: result.user });
|
||||
}
|
||||
|
||||
// Always update user if:
|
||||
// 1. We got a user and current user is null (login scenario)
|
||||
// 2. We got a user and IDs differ (account switch scenario)
|
||||
// 3. shouldUpdateUser is true (session validation detected change)
|
||||
if (result.user) {
|
||||
set({
|
||||
hasLoadedUser: true,
|
||||
isUserLoading: false,
|
||||
});
|
||||
const currentUser = get().user;
|
||||
const shouldUpdate =
|
||||
!currentUser ||
|
||||
currentUser.id !== result.user.id ||
|
||||
result.shouldUpdateUser;
|
||||
|
||||
if (shouldUpdate) {
|
||||
// Invalidate profile query when user changes to ensure fresh data
|
||||
if (
|
||||
typeof window !== "undefined" &&
|
||||
currentUser?.id !== result.user.id
|
||||
) {
|
||||
const { getQueryClient } = await import(
|
||||
"@/lib/react-query/queryClient"
|
||||
);
|
||||
const { getGetV2GetUserProfileQueryKey } = await import(
|
||||
"@/app/api/__generated__/endpoints/store/store"
|
||||
);
|
||||
const queryClient = getQueryClient();
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: getGetV2GetUserProfileQueryKey(),
|
||||
});
|
||||
}
|
||||
|
||||
set({
|
||||
user: result.user,
|
||||
hasLoadedUser: true,
|
||||
isUserLoading: false,
|
||||
});
|
||||
} else {
|
||||
// Even if user didn't change, ensure loading state is cleared
|
||||
set({
|
||||
hasLoadedUser: true,
|
||||
isUserLoading: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user