diff --git a/autogpt_platform/frontend/src/app/(platform)/login/actions.ts b/autogpt_platform/frontend/src/app/(platform)/login/actions.ts index cd30c77e29c3..568a04b266b3 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 ( @@ -59,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("/"); }); diff --git a/autogpt_platform/frontend/src/app/(platform)/login/page.tsx b/autogpt_platform/frontend/src/app/(platform)/login/page.tsx index 78cdf49dd082..025ec7541b5f 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, @@ -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) { 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 5f70f34f82d3..c2c9f393d980 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 2974e281a208..b44083d28108 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 deleted file mode 100644 index 0c8517e4e550..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 "@/hooks/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; 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 d850c82a9a40..c83945becc72 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 { 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: ProfileDetails }) => { 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 d31cda8530e0..91bdc40f46b9 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 "@/lib/supabase/useSupabase"; import { APIKeyCredentials, CredentialsDeleteNeedConfirmationResponse, @@ -8,7 +10,6 @@ import { UserPasswordCredentials, } from "@/lib/autogpt-server-api"; import { useBackendAPI } from "@/lib/autogpt-server-api/context"; -import { createContext, useCallback, useEffect, useState } from "react"; // Get keys from CredentialsProviderName type const CREDENTIALS_PROVIDER_NAMES = Object.values( @@ -102,6 +103,7 @@ export default function CredentialsProvider({ }) { const [providers, setProviders] = useState(null); + const { isLoggedIn } = useSupabase(); const api = useBackendAPI(); const addCredentials = useCallback( @@ -202,48 +204,50 @@ export default function CredentialsProvider({ ); useEffect(() => { - api.isAuthenticated().then((isAuthenticated) => { - if (!isAuthenticated) return; + if (!isLoggedIn) { + if (isLoggedIn == false) setProviders(null); + 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, diff --git a/autogpt_platform/frontend/src/components/onboarding/onboarding-provider.tsx b/autogpt_platform/frontend/src/components/onboarding/onboarding-provider.tsx index 0d10e733b64e..61c2b64e8dde 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/hooks/useSupabase.ts b/autogpt_platform/frontend/src/hooks/useSupabase.ts deleted file mode 100644 index c04c12fa8f11..000000000000 --- a/autogpt_platform/frontend/src/hooks/useSupabase.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { createBrowserClient } from "@supabase/ssr"; -import { User } from "@supabase/supabase-js"; -import { useEffect, useMemo, useState } from "react"; - -export default function useSupabase() { - const [user, setUser] = useState(null); - const [isUserLoading, setIsUserLoading] = useState(true); - - const supabase = useMemo(() => { - try { - return createBrowserClient( - process.env.NEXT_PUBLIC_SUPABASE_URL!, - process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, - ); - } catch (error) { - console.error("Error creating Supabase client", error); - return null; - } - }, []); - - useEffect(() => { - if (!supabase) { - setIsUserLoading(false); - return; - } - - const fetchUser = async () => { - const response = await supabase.auth.getUser(); - - 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); - }; - - fetchUser(); - }, [supabase]); - - return { supabase, user, isUserLoading }; -} 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 new file mode 100644 index 000000000000..f45000726781 --- /dev/null +++ b/autogpt_platform/frontend/src/lib/supabase/useSupabase.ts @@ -0,0 +1,65 @@ +"use client"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { createBrowserClient } from "@supabase/ssr"; +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); + + const supabase = useMemo(() => { + try { + 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); + return null; + } + }, []); + + useEffect(() => { + if (!supabase) { + setIsUserLoading(false); + return; + } + + // Sync up the current state and listen for changes + const { + data: { subscription }, + } = supabase.auth.onAuthStateChange((_event, session) => { + setUser(session?.user ?? null); + setIsUserLoading(false); + }); + + return () => { + subscription.unsubscribe(); + }; + }, [supabase]); + + const logOut = useCallback( + 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) { + return { supabase, user: null, isLoggedIn: null, isUserLoading, logOut }; + } + if (!user) { + return { supabase, user, isLoggedIn: false, isUserLoading, logOut }; + } + return { supabase, user, isLoggedIn: true, isUserLoading, logOut }; +} diff --git a/autogpt_platform/frontend/src/tests/pages/login.page.ts b/autogpt_platform/frontend/src/tests/pages/login.page.ts index 41f9ea1b7743..a2fcabddf4eb 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, + }); // Fill email const emailInput = this.page.getByPlaceholder("m@example.com"); @@ -33,23 +36,35 @@ 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 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 - ]); + const leaveLoginPage = this.page + .waitForURL( + (url) => /^\/(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"); // Debug log + console.log(`🖱️ Clicking login button...`); await loginButton.click(); - console.log("Waiting for navigation"); // Debug log - await navigationPromise; + console.log("⏳ Waiting for navigation away from /login ..."); + await leaveLoginPage; + console.log(`⌛ Post-login redirected to ${this.page.url()}`); - await this.page.goto("/marketplace"); - - console.log("Navigation complete, waiting for network idle"); // Debug log + await new Promise((resolve) => setTimeout(resolve, 200)); // allow time for client-side redirect 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"); } }