From 108ac3d6b341074c61561dbccffd9def0f4b81a1 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Thu, 22 May 2025 18:20:03 +0200 Subject: [PATCH 01/19] fix(frontend): refresh credentials on user change --- .../components/integrations/credentials-provider.tsx | 8 ++++++++ autogpt_platform/frontend/src/hooks/useSupabase.ts | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx b/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx index d31cda8530e0..bd1842ed227c 100644 --- a/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx +++ b/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx @@ -8,6 +8,7 @@ import { UserPasswordCredentials, } from "@/lib/autogpt-server-api"; import { useBackendAPI } from "@/lib/autogpt-server-api/context"; +import useSupabase from "@/hooks/useSupabase"; import { createContext, useCallback, useEffect, useState } from "react"; // Get keys from CredentialsProviderName type @@ -103,6 +104,7 @@ export default function CredentialsProvider({ const [providers, setProviders] = useState(null); const api = useBackendAPI(); + const { user } = useSupabase(); const addCredentials = useCallback( ( @@ -202,6 +204,11 @@ export default function CredentialsProvider({ ); useEffect(() => { + if (!user) { + setProviders(null); + return; + } + api.isAuthenticated().then((isAuthenticated) => { if (!isAuthenticated) return; @@ -248,6 +255,7 @@ export default function CredentialsProvider({ createUserPasswordCredentials, deleteCredentials, oAuthCallback, + user?.id, ]); return ( diff --git a/autogpt_platform/frontend/src/hooks/useSupabase.ts b/autogpt_platform/frontend/src/hooks/useSupabase.ts index c04c12fa8f11..37501de065ed 100644 --- a/autogpt_platform/frontend/src/hooks/useSupabase.ts +++ b/autogpt_platform/frontend/src/hooks/useSupabase.ts @@ -40,6 +40,16 @@ export default function useSupabase() { }; fetchUser(); + + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange((_event, session) => { + setUser(session?.user ?? null); + }); + + return () => { + subscription.unsubscribe(); + }; }, [supabase]); return { supabase, user, isUserLoading }; From eb33948ea9f674fdbc18021f88683c38efa62cd5 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Thu, 22 May 2025 21:45:09 +0100 Subject: [PATCH 02/19] reorder imports --- .../src/components/integrations/credentials-provider.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx b/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx index bd1842ed227c..8925a31ec559 100644 --- a/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx +++ b/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx @@ -1,3 +1,5 @@ +import { createContext, useCallback, useEffect, useState } from "react"; +import useSupabase from "@/hooks/useSupabase"; import { APIKeyCredentials, CredentialsDeleteNeedConfirmationResponse, @@ -8,8 +10,6 @@ import { UserPasswordCredentials, } from "@/lib/autogpt-server-api"; import { useBackendAPI } from "@/lib/autogpt-server-api/context"; -import useSupabase from "@/hooks/useSupabase"; -import { createContext, useCallback, useEffect, useState } from "react"; // Get keys from CredentialsProviderName type const CREDENTIALS_PROVIDER_NAMES = Object.values( @@ -103,8 +103,8 @@ export default function CredentialsProvider({ }) { const [providers, setProviders] = useState(null); - const api = useBackendAPI(); const { user } = useSupabase(); + const api = useBackendAPI(); const addCredentials = useCallback( ( From 07c1fd07737f42d2c899361f0582a229bf9b837b Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Sat, 24 May 2025 18:11:49 +0100 Subject: [PATCH 03/19] refactor --- .../integrations/credentials-provider.tsx | 2 +- .../frontend/src/hooks/useSupabase.ts | 16 ++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx b/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx index 8925a31ec559..ba760666bfc8 100644 --- a/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx +++ b/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx @@ -255,7 +255,7 @@ export default function CredentialsProvider({ createUserPasswordCredentials, deleteCredentials, oAuthCallback, - user?.id, + user, ]); return ( diff --git a/autogpt_platform/frontend/src/hooks/useSupabase.ts b/autogpt_platform/frontend/src/hooks/useSupabase.ts index 37501de065ed..bd1ca076795c 100644 --- a/autogpt_platform/frontend/src/hooks/useSupabase.ts +++ b/autogpt_platform/frontend/src/hooks/useSupabase.ts @@ -24,9 +24,13 @@ export default function useSupabase() { return; } - const fetchUser = async () => { - const response = await supabase.auth.getUser(); + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange((_event, session) => { + setUser(session?.user ?? null); + }); + supabase.auth.getUser().then((response) => { if (response.error) { // Display error only if it's not about missing auth session (user is not logged in) if (response.error.message !== "Auth session missing!") { @@ -37,14 +41,6 @@ export default function useSupabase() { setUser(response.data.user); } setIsUserLoading(false); - }; - - fetchUser(); - - const { - data: { subscription }, - } = supabase.auth.onAuthStateChange((_event, session) => { - setUser(session?.user ?? null); }); return () => { From d81d7a0342c3c9232b2f51b17a58f7ed42b4d1c0 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Sat, 24 May 2025 19:52:47 +0100 Subject: [PATCH 04/19] fix(frontend): Also log out on client side --- .../src/app/(platform)/login/actions.ts | 24 --------------- .../src/app/(platform)/login/page.tsx | 2 +- .../profile/(user)/dashboard/page.tsx | 2 +- .../profile/(user)/integrations/page.tsx | 16 +++++----- .../app/(platform)/reset_password/page.tsx | 2 +- .../src/app/(platform)/signup/page.tsx | 2 +- .../src/components/ProfileDropdown.tsx | 2 +- .../src/components/RoleBasedAccess.tsx | 2 +- .../src/components/agptui/ProfileInfoForm.tsx | 2 +- .../agptui/ProfilePopoutMenuLogoutButton.tsx | 6 ++-- .../integrations/credentials-provider.tsx | 2 +- .../onboarding/onboarding-provider.tsx | 2 +- .../frontend/src/lib/supabase/actions.ts | 30 +++++++++++++++++++ .../{hooks => lib/supabase}/useSupabase.ts | 11 +++++-- 14 files changed, 59 insertions(+), 46 deletions(-) create mode 100644 autogpt_platform/frontend/src/lib/supabase/actions.ts rename autogpt_platform/frontend/src/{hooks => lib/supabase}/useSupabase.ts (80%) diff --git a/autogpt_platform/frontend/src/app/(platform)/login/actions.ts b/autogpt_platform/frontend/src/app/(platform)/login/actions.ts index cd3ca3e7508e..8fa5899c1363 100644 --- a/autogpt_platform/frontend/src/app/(platform)/login/actions.ts +++ b/autogpt_platform/frontend/src/app/(platform)/login/actions.ts @@ -8,30 +8,6 @@ import BackendAPI from "@/lib/autogpt-server-api"; import { loginFormSchema, LoginProvider } from "@/types/auth"; import { verifyTurnstileToken } from "@/lib/turnstile"; -export async function logout() { - return await Sentry.withServerActionInstrumentation( - "logout", - {}, - async () => { - const supabase = getServerSupabase(); - - if (!supabase) { - redirect("/error"); - } - - const { error } = await supabase.auth.signOut(); - - if (error) { - console.error("Error logging out", error); - return error.message; - } - - revalidatePath("/", "layout"); - redirect("/login"); - }, - ); -} - async function shouldShowOnboarding() { const api = new BackendAPI(); return ( diff --git a/autogpt_platform/frontend/src/app/(platform)/login/page.tsx b/autogpt_platform/frontend/src/app/(platform)/login/page.tsx index b13cfc8acdc7..553f558cc4ec 100644 --- a/autogpt_platform/frontend/src/app/(platform)/login/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/login/page.tsx @@ -15,7 +15,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useCallback, useState } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; -import useSupabase from "@/hooks/useSupabase"; +import useSupabase from "@/lib/supabase/useSupabase"; import LoadingBox from "@/components/ui/loading"; import { AuthCard, diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/page.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/page.tsx index 60ab478e9082..309a41cb7ea1 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/dashboard/page.tsx @@ -11,7 +11,7 @@ import { StoreSubmissionsResponse, StoreSubmissionRequest, } from "@/lib/autogpt-server-api/types"; -import useSupabase from "@/hooks/useSupabase"; +import useSupabase from "@/lib/supabase/useSupabase"; import { useBackendAPI } from "@/lib/autogpt-server-api/context"; export default function Page({}: {}) { diff --git a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/integrations/page.tsx b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/integrations/page.tsx index 710ee1059234..80e34a134c2e 100644 --- a/autogpt_platform/frontend/src/app/(platform)/profile/(user)/integrations/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/profile/(user)/integrations/page.tsx @@ -1,7 +1,7 @@ "use client"; import { Button } from "@/components/ui/button"; import { useRouter } from "next/navigation"; -import { useCallback, useContext, useMemo, useState } from "react"; +import { useCallback, useContext, useEffect, useMemo, useState } from "react"; import { useToast } from "@/components/ui/use-toast"; import { IconKey, IconUser } from "@/components/ui/icons"; import { Trash2Icon } from "lucide-react"; @@ -26,10 +26,10 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; -import useSupabase from "@/hooks/useSupabase"; +import useSupabase from "@/lib/supabase/useSupabase"; import LoadingBox from "@/components/ui/loading"; -export default function PrivatePage() { +export default function UserIntegrationsPage() { const { supabase, user, isUserLoading } = useSupabase(); const router = useRouter(); const providers = useContext(CredentialsProvidersContext); @@ -122,15 +122,15 @@ export default function PrivatePage() { [], ); + useEffect(() => { + if (isUserLoading) return; + if (!user || !supabase) router.push("/login"); + }, [isUserLoading, user, supabase, router]); + if (isUserLoading) { return ; } - if (!user || !supabase) { - router.push("/login"); - return null; - } - const allCredentials = providers ? Object.values(providers).flatMap((provider) => provider.savedCredentials diff --git a/autogpt_platform/frontend/src/app/(platform)/reset_password/page.tsx b/autogpt_platform/frontend/src/app/(platform)/reset_password/page.tsx index 116dbfdff53a..d945607d6fc2 100644 --- a/autogpt_platform/frontend/src/app/(platform)/reset_password/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/reset_password/page.tsx @@ -17,7 +17,7 @@ import { FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import useSupabase from "@/hooks/useSupabase"; +import useSupabase from "@/lib/supabase/useSupabase"; import { sendEmailFormSchema, changePasswordFormSchema } from "@/types/auth"; import { zodResolver } from "@hookform/resolvers/zod"; import { useCallback, useState } from "react"; diff --git a/autogpt_platform/frontend/src/app/(platform)/signup/page.tsx b/autogpt_platform/frontend/src/app/(platform)/signup/page.tsx index 68871c77f2d1..ee1af90d2e84 100644 --- a/autogpt_platform/frontend/src/app/(platform)/signup/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/signup/page.tsx @@ -17,7 +17,7 @@ import { useCallback, useState } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { Checkbox } from "@/components/ui/checkbox"; -import useSupabase from "@/hooks/useSupabase"; +import useSupabase from "@/lib/supabase/useSupabase"; import LoadingBox from "@/components/ui/loading"; import { AuthCard, diff --git a/autogpt_platform/frontend/src/components/ProfileDropdown.tsx b/autogpt_platform/frontend/src/components/ProfileDropdown.tsx index 0c8517e4e550..d9ed96c37b6a 100644 --- a/autogpt_platform/frontend/src/components/ProfileDropdown.tsx +++ b/autogpt_platform/frontend/src/components/ProfileDropdown.tsx @@ -8,7 +8,7 @@ import { } from "@/components/ui/dropdown-menu"; import { Button } from "./ui/button"; import { useRouter } from "next/navigation"; -import useSupabase from "@/hooks/useSupabase"; +import useSupabase from "@/lib/supabase/useSupabase"; const ProfileDropdown = () => { const { supabase, user, isUserLoading } = useSupabase(); diff --git a/autogpt_platform/frontend/src/components/RoleBasedAccess.tsx b/autogpt_platform/frontend/src/components/RoleBasedAccess.tsx index 176c4761c178..e5a4b98aa2bb 100644 --- a/autogpt_platform/frontend/src/components/RoleBasedAccess.tsx +++ b/autogpt_platform/frontend/src/components/RoleBasedAccess.tsx @@ -1,5 +1,5 @@ // components/RoleBasedAccess.tsx -import useSupabase from "@/hooks/useSupabase"; +import useSupabase from "@/lib/supabase/useSupabase"; import React from "react"; interface RoleBasedAccessProps { diff --git a/autogpt_platform/frontend/src/components/agptui/ProfileInfoForm.tsx b/autogpt_platform/frontend/src/components/agptui/ProfileInfoForm.tsx index eebe428f56db..6624498c3be5 100644 --- a/autogpt_platform/frontend/src/components/agptui/ProfileInfoForm.tsx +++ b/autogpt_platform/frontend/src/components/agptui/ProfileInfoForm.tsx @@ -9,7 +9,7 @@ import { Button } from "./Button"; import { IconPersonFill } from "@/components/ui/icons"; import { CreatorDetails, ProfileDetails } from "@/lib/autogpt-server-api/types"; import { Separator } from "@/components/ui/separator"; -import useSupabase from "@/hooks/useSupabase"; +import useSupabase from "@/lib/supabase/useSupabase"; import { useBackendAPI } from "@/lib/autogpt-server-api/context"; export const ProfileInfoForm = ({ profile }: { profile: CreatorDetails }) => { diff --git a/autogpt_platform/frontend/src/components/agptui/ProfilePopoutMenuLogoutButton.tsx b/autogpt_platform/frontend/src/components/agptui/ProfilePopoutMenuLogoutButton.tsx index 954cfc202480..428c5a598d27 100644 --- a/autogpt_platform/frontend/src/components/agptui/ProfilePopoutMenuLogoutButton.tsx +++ b/autogpt_platform/frontend/src/components/agptui/ProfilePopoutMenuLogoutButton.tsx @@ -1,13 +1,13 @@ "use client"; - -import { logout } from "@/app/(platform)/login/actions"; +import useSupabase from "@/lib/supabase/useSupabase"; import { IconLogOut } from "@/components/ui/icons"; export const ProfilePopoutMenuLogoutButton = () => { + const supabase = useSupabase(); return (
logout()} + onClick={() => supabase.logOut()} role="button" tabIndex={0} > diff --git a/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx b/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx index ba760666bfc8..5746f161e494 100644 --- a/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx +++ b/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx @@ -1,5 +1,5 @@ import { createContext, useCallback, useEffect, useState } from "react"; -import useSupabase from "@/hooks/useSupabase"; +import useSupabase from "@/lib/supabase/useSupabase"; import { APIKeyCredentials, CredentialsDeleteNeedConfirmationResponse, diff --git a/autogpt_platform/frontend/src/components/onboarding/onboarding-provider.tsx b/autogpt_platform/frontend/src/components/onboarding/onboarding-provider.tsx index f98c17b8edb5..e4a9ed8d9cca 100644 --- a/autogpt_platform/frontend/src/components/onboarding/onboarding-provider.tsx +++ b/autogpt_platform/frontend/src/components/onboarding/onboarding-provider.tsx @@ -1,5 +1,5 @@ "use client"; -import useSupabase from "@/hooks/useSupabase"; +import useSupabase from "@/lib/supabase/useSupabase"; import { OnboardingStep, UserOnboarding } from "@/lib/autogpt-server-api"; import { useBackendAPI } from "@/lib/autogpt-server-api/context"; import { usePathname, useRouter } from "next/navigation"; diff --git a/autogpt_platform/frontend/src/lib/supabase/actions.ts b/autogpt_platform/frontend/src/lib/supabase/actions.ts new file mode 100644 index 000000000000..5e5973d5c112 --- /dev/null +++ b/autogpt_platform/frontend/src/lib/supabase/actions.ts @@ -0,0 +1,30 @@ +"use server"; +import { revalidatePath } from "next/cache"; +import { redirect } from "next/navigation"; +import * as Sentry from "@sentry/nextjs"; + +import getServerSupabase from "@/lib/supabase/getServerSupabase"; + +export async function _logoutServer() { + return await Sentry.withServerActionInstrumentation( + "logout", + {}, + async () => { + const supabase = getServerSupabase(); + + if (!supabase) { + redirect("/error"); + } + + const { error } = await supabase.auth.signOut(); + + if (error) { + console.error("Error logging out", error); + return error.message; + } + + revalidatePath("/", "layout"); + redirect("/login"); + }, + ); +} diff --git a/autogpt_platform/frontend/src/hooks/useSupabase.ts b/autogpt_platform/frontend/src/lib/supabase/useSupabase.ts similarity index 80% rename from autogpt_platform/frontend/src/hooks/useSupabase.ts rename to autogpt_platform/frontend/src/lib/supabase/useSupabase.ts index bd1ca076795c..f7cd387c60fe 100644 --- a/autogpt_platform/frontend/src/hooks/useSupabase.ts +++ b/autogpt_platform/frontend/src/lib/supabase/useSupabase.ts @@ -1,6 +1,9 @@ +"use client"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { createBrowserClient } from "@supabase/ssr"; import { User } from "@supabase/supabase-js"; -import { useEffect, useMemo, useState } from "react"; + +import { _logoutServer } from "./actions"; export default function useSupabase() { const [user, setUser] = useState(null); @@ -48,5 +51,9 @@ export default function useSupabase() { }; }, [supabase]); - return { supabase, user, isUserLoading }; + const logOut = useCallback(() => { + supabase?.auth.signOut().then(() => _logoutServer()); + }, [supabase]); + + return { supabase, user, isLoggedIn: !!user, isUserLoading, logOut }; } From 355548f97c85e6ecdc86814a2b69e597c8fcca38 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Sat, 24 May 2025 22:17:18 +0100 Subject: [PATCH 05/19] make supabase state logic better --- .../src/lib/autogpt-server-api/client.ts | 7 ++++--- .../frontend/src/lib/supabase/useSupabase.ts | 21 ++++++------------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/autogpt_platform/frontend/src/lib/autogpt-server-api/client.ts b/autogpt_platform/frontend/src/lib/autogpt-server-api/client.ts index 889bab56d2e7..b2882255effd 100644 --- a/autogpt_platform/frontend/src/lib/autogpt-server-api/client.ts +++ b/autogpt_platform/frontend/src/lib/autogpt-server-api/client.ts @@ -91,6 +91,7 @@ export default class BackendAPI { ? createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { isSingleton: true }, ) : getServerSupabase(); } @@ -98,9 +99,9 @@ export default class BackendAPI { async isAuthenticated(): Promise { if (!this.supabaseClient) return false; const { - data: { user }, - } = await this.supabaseClient?.auth.getUser(); - return user != null; + data: { session }, + } = await this.supabaseClient.auth.getSession(); + return session != null; } createUser(): Promise { diff --git a/autogpt_platform/frontend/src/lib/supabase/useSupabase.ts b/autogpt_platform/frontend/src/lib/supabase/useSupabase.ts index f7cd387c60fe..b40a81405a40 100644 --- a/autogpt_platform/frontend/src/lib/supabase/useSupabase.ts +++ b/autogpt_platform/frontend/src/lib/supabase/useSupabase.ts @@ -14,6 +14,7 @@ export default function useSupabase() { return createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, + { isSingleton: true }, ); } catch (error) { console.error("Error creating Supabase client", error); @@ -27,22 +28,11 @@ export default function useSupabase() { return; } + // Sync up the current state and listen for changes const { data: { subscription }, } = supabase.auth.onAuthStateChange((_event, session) => { setUser(session?.user ?? null); - }); - - supabase.auth.getUser().then((response) => { - if (response.error) { - // Display error only if it's not about missing auth session (user is not logged in) - if (response.error.message !== "Auth session missing!") { - console.error("Error fetching user", response.error); - } - setUser(null); - } else { - setUser(response.data.user); - } setIsUserLoading(false); }); @@ -51,9 +41,10 @@ export default function useSupabase() { }; }, [supabase]); - const logOut = useCallback(() => { - supabase?.auth.signOut().then(() => _logoutServer()); - }, [supabase]); + const logOut = useCallback( + () => Promise.all([_logoutServer(), supabase?.auth.signOut()]), + [supabase], + ); return { supabase, user, isLoggedIn: !!user, isUserLoading, logOut }; } From 63255793bd3b14275b3588f0f72bfe73a0d1d061 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Sat, 24 May 2025 22:21:43 +0100 Subject: [PATCH 06/19] remove redundant check --- .../integrations/credentials-provider.tsx | 76 +++++++++---------- 1 file changed, 36 insertions(+), 40 deletions(-) diff --git a/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx b/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx index 5746f161e494..af531d831fe6 100644 --- a/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx +++ b/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx @@ -103,7 +103,7 @@ export default function CredentialsProvider({ }) { const [providers, setProviders] = useState(null); - const { user } = useSupabase(); + const { isLoggedIn } = useSupabase(); const api = useBackendAPI(); const addCredentials = useCallback( @@ -204,58 +204,54 @@ export default function CredentialsProvider({ ); useEffect(() => { - if (!user) { + if (!isLoggedIn) { setProviders(null); return; } - api.isAuthenticated().then((isAuthenticated) => { - if (!isAuthenticated) return; + api.listCredentials().then((response) => { + const credentialsByProvider = response.reduce( + (acc, cred) => { + if (!acc[cred.provider]) { + acc[cred.provider] = []; + } + acc[cred.provider].push(cred); + return acc; + }, + {} as Record, + ); - api.listCredentials().then((response) => { - const credentialsByProvider = response.reduce( - (acc, cred) => { - if (!acc[cred.provider]) { - acc[cred.provider] = []; - } - acc[cred.provider].push(cred); - return acc; - }, - {} as Record, - ); - - setProviders((prev) => ({ - ...prev, - ...Object.fromEntries( - CREDENTIALS_PROVIDER_NAMES.map((provider) => [ + setProviders((prev) => ({ + ...prev, + ...Object.fromEntries( + CREDENTIALS_PROVIDER_NAMES.map((provider) => [ + provider, + { provider, - { - provider, - providerName: providerDisplayNames[provider], - savedCredentials: credentialsByProvider[provider] ?? [], - oAuthCallback: (code: string, state_token: string) => - oAuthCallback(provider, code, state_token), - createAPIKeyCredentials: ( - credentials: APIKeyCredentialsCreatable, - ) => createAPIKeyCredentials(provider, credentials), - createUserPasswordCredentials: ( - credentials: UserPasswordCredentialsCreatable, - ) => createUserPasswordCredentials(provider, credentials), - deleteCredentials: (id: string, force: boolean = false) => - deleteCredentials(provider, id, force), - } satisfies CredentialsProviderData, - ]), - ), - })); - }); + providerName: providerDisplayNames[provider], + savedCredentials: credentialsByProvider[provider] ?? [], + oAuthCallback: (code: string, state_token: string) => + oAuthCallback(provider, code, state_token), + createAPIKeyCredentials: ( + credentials: APIKeyCredentialsCreatable, + ) => createAPIKeyCredentials(provider, credentials), + createUserPasswordCredentials: ( + credentials: UserPasswordCredentialsCreatable, + ) => createUserPasswordCredentials(provider, credentials), + deleteCredentials: (id: string, force: boolean = false) => + deleteCredentials(provider, id, force), + } satisfies CredentialsProviderData, + ]), + ), + })); }); }, [ api, + isLoggedIn, createAPIKeyCredentials, createUserPasswordCredentials, deleteCredentials, oAuthCallback, - user, ]); return ( From f345688ca6647c291f80cd95cea7576b136875ed Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Sat, 24 May 2025 22:38:43 +0100 Subject: [PATCH 07/19] improve useSupabase hook --- .../src/components/integrations/credentials-provider.tsx | 2 +- autogpt_platform/frontend/src/lib/supabase/useSupabase.ts | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx b/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx index af531d831fe6..91bdc40f46b9 100644 --- a/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx +++ b/autogpt_platform/frontend/src/components/integrations/credentials-provider.tsx @@ -205,7 +205,7 @@ export default function CredentialsProvider({ useEffect(() => { if (!isLoggedIn) { - setProviders(null); + if (isLoggedIn == false) setProviders(null); return; } diff --git a/autogpt_platform/frontend/src/lib/supabase/useSupabase.ts b/autogpt_platform/frontend/src/lib/supabase/useSupabase.ts index b40a81405a40..5324c9699120 100644 --- a/autogpt_platform/frontend/src/lib/supabase/useSupabase.ts +++ b/autogpt_platform/frontend/src/lib/supabase/useSupabase.ts @@ -46,5 +46,11 @@ export default function useSupabase() { [supabase], ); - return { supabase, user, isLoggedIn: !!user, isUserLoading, logOut }; + if (!supabase || isUserLoading) { + return { supabase, user: null, isLoggedIn: null, isUserLoading, logOut }; + } + if (!user) { + return { supabase, user, isLoggedIn: false, isUserLoading, logOut }; + } + return { supabase, user, isLoggedIn: true, isUserLoading, logOut }; } From 62f5730341342ad5d493ca6f1b9b277de564b4be Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Sat, 24 May 2025 22:57:13 +0100 Subject: [PATCH 08/19] fix credentials refresh on login --- autogpt_platform/frontend/src/app/(platform)/login/page.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/autogpt_platform/frontend/src/app/(platform)/login/page.tsx b/autogpt_platform/frontend/src/app/(platform)/login/page.tsx index 553f558cc4ec..f5c52eeb50d3 100644 --- a/autogpt_platform/frontend/src/app/(platform)/login/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/login/page.tsx @@ -80,6 +80,7 @@ export default function LoginPage() { } const error = await login(data, turnstile.token as string); + await supabase?.auth.refreshSession(); setIsLoading(false); if (error) { setFeedback(error); @@ -89,7 +90,7 @@ export default function LoginPage() { } setFeedback(null); }, - [form, turnstile], + [form, turnstile, supabase], ); if (user) { From 8a4db8ad6bd4ed7a90e7e08ec40719e56dd61433 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Mon, 26 May 2025 13:20:09 +0100 Subject: [PATCH 09/19] remove unused `ProfileDropdown` component --- .../src/components/ProfileDropdown.tsx | 55 ------------------- 1 file changed, 55 deletions(-) delete mode 100644 autogpt_platform/frontend/src/components/ProfileDropdown.tsx diff --git a/autogpt_platform/frontend/src/components/ProfileDropdown.tsx b/autogpt_platform/frontend/src/components/ProfileDropdown.tsx deleted file mode 100644 index d9ed96c37b6a..000000000000 --- a/autogpt_platform/frontend/src/components/ProfileDropdown.tsx +++ /dev/null @@ -1,55 +0,0 @@ -"use client"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Button } from "./ui/button"; -import { useRouter } from "next/navigation"; -import useSupabase from "@/lib/supabase/useSupabase"; - -const ProfileDropdown = () => { - const { supabase, user, isUserLoading } = useSupabase(); - const router = useRouter(); - - if (isUserLoading) { - return null; - } - - return ( - - - - - - router.push("/profile")}> - Profile - - {user!.role === "admin" && ( - router.push("/admin/dashboard")}> - Admin Dashboard - - )} - - supabase?.auth.signOut().then(() => router.replace("/login")) - } - > - Log out - - - - ); -}; - -export default ProfileDropdown; From 1aa60ec2305f925a5c3304705ea1579e8619e890 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Mon, 26 May 2025 13:50:48 +0100 Subject: [PATCH 10/19] improve logging in login.page.ts for debug purposes --- .../frontend/src/tests/pages/login.page.ts | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/autogpt_platform/frontend/src/tests/pages/login.page.ts b/autogpt_platform/frontend/src/tests/pages/login.page.ts index 41f9ea1b7743..2cdfda119048 100644 --- a/autogpt_platform/frontend/src/tests/pages/login.page.ts +++ b/autogpt_platform/frontend/src/tests/pages/login.page.ts @@ -4,7 +4,10 @@ export class LoginPage { constructor(private page: Page) {} async login(email: string, password: string) { - console.log("Attempting login with:", { email, password }); // Debug log + console.log(`Attempting login on ${this.page.url()} with`, { + email, + password, + }); // Debug log // Fill email const emailInput = this.page.getByPlaceholder("m@example.com"); @@ -35,15 +38,20 @@ export class LoginPage { // Start waiting for navigation before clicking const navigationPromise = Promise.race([ - this.page.waitForURL("/", { timeout: 10_000 }), // Wait for home page - this.page.waitForURL("/marketplace", { timeout: 10_000 }), // Wait for home page - this.page.waitForURL("/onboarding/**", { timeout: 10_000 }), // Wait for onboarding page + this.page + .waitForURL(/^\/(marketplace|onboarding(\/.*)?)?$/, { timeout: 10_000 }) + .catch((reason) => { + console.warn( + `Navigation away from /login timed out: ${reason}. Current URL: ${this.page.url()}`, + ); + throw reason; + }), // Wait for home page ]); - console.log("About to click login button"); // Debug log + console.log(`About to click login button on ${this.page.url()}`); // Debug log await loginButton.click(); - console.log("Waiting for navigation"); // Debug log + console.log("Waiting for navigation away from /login"); // Debug log await navigationPromise; await this.page.goto("/marketplace"); From 8701ee162f81f74e146e521ae8a08d180dc8173d Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Mon, 26 May 2025 14:19:20 +0100 Subject: [PATCH 11/19] fix nav check regex? --- autogpt_platform/frontend/src/tests/pages/login.page.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/autogpt_platform/frontend/src/tests/pages/login.page.ts b/autogpt_platform/frontend/src/tests/pages/login.page.ts index 2cdfda119048..97c0227ae76a 100644 --- a/autogpt_platform/frontend/src/tests/pages/login.page.ts +++ b/autogpt_platform/frontend/src/tests/pages/login.page.ts @@ -39,7 +39,10 @@ export class LoginPage { // Start waiting for navigation before clicking const navigationPromise = Promise.race([ this.page - .waitForURL(/^\/(marketplace|onboarding(\/.*)?)?$/, { timeout: 10_000 }) + .waitForURL( + (url) => /^\/(marketplace|onboarding(\/.*)?)?$/.test(url.pathname), + { timeout: 10_000 }, + ) .catch((reason) => { console.warn( `Navigation away from /login timed out: ${reason}. Current URL: ${this.page.url()}`, From 2bae95c063ee5b8bf998023d1a0d9e543ede9c20 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Mon, 26 May 2025 14:21:09 +0100 Subject: [PATCH 12/19] improve nav failure warning --- autogpt_platform/frontend/src/tests/pages/login.page.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/autogpt_platform/frontend/src/tests/pages/login.page.ts b/autogpt_platform/frontend/src/tests/pages/login.page.ts index 97c0227ae76a..5195e67faae8 100644 --- a/autogpt_platform/frontend/src/tests/pages/login.page.ts +++ b/autogpt_platform/frontend/src/tests/pages/login.page.ts @@ -45,7 +45,8 @@ export class LoginPage { ) .catch((reason) => { console.warn( - `Navigation away from /login timed out: ${reason}. Current URL: ${this.page.url()}`, + `Navigation away from /login timed out (current URL: ${this.page.url()}):`, + reason, ); throw reason; }), // Wait for home page From e4bb368ef4eb6503b1ffb3fa06200045f9b8e4f4 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Mon, 26 May 2025 20:23:32 +0100 Subject: [PATCH 13/19] more debug logging --- autogpt_platform/frontend/src/tests/pages/login.page.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/autogpt_platform/frontend/src/tests/pages/login.page.ts b/autogpt_platform/frontend/src/tests/pages/login.page.ts index 5195e67faae8..bb54dcd857a9 100644 --- a/autogpt_platform/frontend/src/tests/pages/login.page.ts +++ b/autogpt_platform/frontend/src/tests/pages/login.page.ts @@ -40,7 +40,10 @@ export class LoginPage { const navigationPromise = Promise.race([ this.page .waitForURL( - (url) => /^\/(marketplace|onboarding(\/.*)?)?$/.test(url.pathname), + (url) => { + console.log(`Navigation update: ${url}`); + return /^\/(marketplace|onboarding(\/.*)?)?$/.test(url.pathname); + }, { timeout: 10_000 }, ) .catch((reason) => { From 1762f5c63d1bc1cf9226f19cc304bbac312d85c6 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Tue, 27 May 2025 15:42:34 +0100 Subject: [PATCH 14/19] make logout a client-side action --- .../frontend/src/lib/supabase/actions.ts | 30 ------------------- .../frontend/src/lib/supabase/useSupabase.ts | 19 ++++++++---- 2 files changed, 14 insertions(+), 35 deletions(-) delete mode 100644 autogpt_platform/frontend/src/lib/supabase/actions.ts diff --git a/autogpt_platform/frontend/src/lib/supabase/actions.ts b/autogpt_platform/frontend/src/lib/supabase/actions.ts deleted file mode 100644 index 5e5973d5c112..000000000000 --- a/autogpt_platform/frontend/src/lib/supabase/actions.ts +++ /dev/null @@ -1,30 +0,0 @@ -"use server"; -import { revalidatePath } from "next/cache"; -import { redirect } from "next/navigation"; -import * as Sentry from "@sentry/nextjs"; - -import getServerSupabase from "@/lib/supabase/getServerSupabase"; - -export async function _logoutServer() { - return await Sentry.withServerActionInstrumentation( - "logout", - {}, - async () => { - const supabase = getServerSupabase(); - - if (!supabase) { - redirect("/error"); - } - - const { error } = await supabase.auth.signOut(); - - if (error) { - console.error("Error logging out", error); - return error.message; - } - - revalidatePath("/", "layout"); - redirect("/login"); - }, - ); -} diff --git a/autogpt_platform/frontend/src/lib/supabase/useSupabase.ts b/autogpt_platform/frontend/src/lib/supabase/useSupabase.ts index 5324c9699120..f45000726781 100644 --- a/autogpt_platform/frontend/src/lib/supabase/useSupabase.ts +++ b/autogpt_platform/frontend/src/lib/supabase/useSupabase.ts @@ -1,11 +1,11 @@ "use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { createBrowserClient } from "@supabase/ssr"; -import { User } from "@supabase/supabase-js"; - -import { _logoutServer } from "./actions"; +import { SignOut, User } from "@supabase/supabase-js"; +import { useRouter } from "next/navigation"; export default function useSupabase() { + const router = useRouter(); const [user, setUser] = useState(null); const [isUserLoading, setIsUserLoading] = useState(true); @@ -42,8 +42,17 @@ export default function useSupabase() { }, [supabase]); const logOut = useCallback( - () => Promise.all([_logoutServer(), supabase?.auth.signOut()]), - [supabase], + async (options?: SignOut) => { + if (!supabase) return; + + const { error } = await supabase.auth.signOut({ + scope: options?.scope ?? "local", + }); + if (error) console.error("Error logging out:", error); + + router.push("/login"); + }, + [router, supabase], ); if (!supabase || isUserLoading) { From 04340dc78a41f3680508541a7fe7e1744e52098f Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Tue, 27 May 2025 16:26:52 +0100 Subject: [PATCH 15/19] clean up and improve logging in `login.page.ts` --- .../frontend/src/tests/pages/login.page.ts | 51 +++++++++---------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/autogpt_platform/frontend/src/tests/pages/login.page.ts b/autogpt_platform/frontend/src/tests/pages/login.page.ts index bb54dcd857a9..4e78602b61a8 100644 --- a/autogpt_platform/frontend/src/tests/pages/login.page.ts +++ b/autogpt_platform/frontend/src/tests/pages/login.page.ts @@ -4,10 +4,10 @@ export class LoginPage { constructor(private page: Page) {} async login(email: string, password: string) { - console.log(`Attempting login on ${this.page.url()} with`, { + console.log(`ℹ️ Attempting login on ${this.page.url()} with`, { email, password, - }); // Debug log + }); // Fill email const emailInput = this.page.getByPlaceholder("m@example.com"); @@ -37,34 +37,31 @@ export class LoginPage { await loginButton.waitFor({ state: "visible" }); // Start waiting for navigation before clicking - const navigationPromise = Promise.race([ - this.page - .waitForURL( - (url) => { - console.log(`Navigation update: ${url}`); - return /^\/(marketplace|onboarding(\/.*)?)?$/.test(url.pathname); - }, - { timeout: 10_000 }, - ) - .catch((reason) => { - console.warn( - `Navigation away from /login timed out (current URL: ${this.page.url()}):`, - reason, - ); - throw reason; - }), // Wait for home page - ]); + const leaveLoginPage = this.page + .waitForURL( + (url) => { + console.log(`ℹ️ Now at URL: ${url}`); + return /^\/(marketplace|onboarding(\/.*)?)?$/.test(url.pathname); + }, + { timeout: 10_000 }, + ) + .catch((reason) => { + console.error( + `🚨 Navigation away from /login timed out (current URL: ${this.page.url()}):`, + reason, + ); + throw reason; + }); - console.log(`About to click login button on ${this.page.url()}`); // Debug log + console.log(`🖱️ Clicking login button...`); await loginButton.click(); - console.log("Waiting for navigation away from /login"); // Debug log - await navigationPromise; - - await this.page.goto("/marketplace"); + console.log("⏳ Waiting for navigation away from /login ..."); + await leaveLoginPage; + console.log(`⌛ Post-login redirected to ${this.page.url()}`); - console.log("Navigation complete, waiting for network idle"); // Debug log - await this.page.waitForLoadState("load", { timeout: 10_000 }); - console.log("Login process complete"); // Debug log + console.log("➡️ Navigating to /marketplace ..."); + await this.page.goto("/marketplace", { timeout: 10_000 }); + console.log("✅ Login process complete"); } } From c523390ae47bd18369bb504325908842d6159873 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Tue, 27 May 2025 16:43:43 +0100 Subject: [PATCH 16/19] improve output of frontend tests --- autogpt_platform/frontend/playwright.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autogpt_platform/frontend/playwright.config.ts b/autogpt_platform/frontend/playwright.config.ts index ed0bdc445c45..9cb2f8cef3fe 100644 --- a/autogpt_platform/frontend/playwright.config.ts +++ b/autogpt_platform/frontend/playwright.config.ts @@ -22,7 +22,7 @@ export default defineConfig({ /* Opt out of parallel tests on CI. */ workers: process.env.CI ? 1 : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: "html", + reporter: [["html"], ["line"]], /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ From 0505b7819bae51834275b499439fc3059fb5baad Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Tue, 27 May 2025 16:45:57 +0100 Subject: [PATCH 17/19] clean up login action --- .../frontend/src/app/(platform)/login/actions.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/autogpt_platform/frontend/src/app/(platform)/login/actions.ts b/autogpt_platform/frontend/src/app/(platform)/login/actions.ts index 8fa5899c1363..a01be75fd6a3 100644 --- a/autogpt_platform/frontend/src/app/(platform)/login/actions.ts +++ b/autogpt_platform/frontend/src/app/(platform)/login/actions.ts @@ -35,23 +35,21 @@ export async function login( } // We are sure that the values are of the correct type because zod validates the form - const { data, error } = await supabase.auth.signInWithPassword(values); + const { error } = await supabase.auth.signInWithPassword(values); if (error) { - console.error("Error logging in", error); + console.error("Error logging in:", error); return error.message; } await api.createUser(); + // Don't onboard if disabled or already onboarded if (await shouldShowOnboarding()) { revalidatePath("/onboarding", "layout"); redirect("/onboarding"); } - if (data.session) { - await supabase.auth.setSession(data.session); - } revalidatePath("/", "layout"); redirect("/"); }); From ef0a614fc617740960e30ff5d790a7b2a8b346f5 Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Tue, 27 May 2025 17:13:27 +0100 Subject: [PATCH 18/19] fix navigation redirect(?) --- autogpt_platform/frontend/src/tests/pages/login.page.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/autogpt_platform/frontend/src/tests/pages/login.page.ts b/autogpt_platform/frontend/src/tests/pages/login.page.ts index 4e78602b61a8..f8b50b772b86 100644 --- a/autogpt_platform/frontend/src/tests/pages/login.page.ts +++ b/autogpt_platform/frontend/src/tests/pages/login.page.ts @@ -60,6 +60,9 @@ export class LoginPage { await leaveLoginPage; console.log(`⌛ Post-login redirected to ${this.page.url()}`); + await new Promise((resolve) => setTimeout(resolve, 200)); // allow time for client-side redirect + await this.page.waitForLoadState("load", { timeout: 10_000 }); + console.log("➡️ Navigating to /marketplace ..."); await this.page.goto("/marketplace", { timeout: 10_000 }); console.log("✅ Login process complete"); From 43998f27d272381cbc2e82820281f9665381145a Mon Sep 17 00:00:00 2001 From: Reinier van der Leer Date: Tue, 27 May 2025 17:15:43 +0100 Subject: [PATCH 19/19] improve logging in `login.page.ts` --- autogpt_platform/frontend/src/tests/pages/login.page.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/autogpt_platform/frontend/src/tests/pages/login.page.ts b/autogpt_platform/frontend/src/tests/pages/login.page.ts index f8b50b772b86..a2fcabddf4eb 100644 --- a/autogpt_platform/frontend/src/tests/pages/login.page.ts +++ b/autogpt_platform/frontend/src/tests/pages/login.page.ts @@ -36,13 +36,13 @@ export class LoginPage { }); await loginButton.waitFor({ state: "visible" }); + // Attach navigation logger for debug purposes + this.page.on("load", (page) => console.log(`ℹ️ Now at URL: ${page.url()}`)); + // Start waiting for navigation before clicking const leaveLoginPage = this.page .waitForURL( - (url) => { - console.log(`ℹ️ Now at URL: ${url}`); - return /^\/(marketplace|onboarding(\/.*)?)?$/.test(url.pathname); - }, + (url) => /^\/(marketplace|onboarding(\/.*)?)?$/.test(url.pathname), { timeout: 10_000 }, ) .catch((reason) => {