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");
}
}