Skip to content

Commit

Permalink
refactor(frontend): Update Supabase and backend API management (#9036)
Browse files Browse the repository at this point in the history
Currently there are random issues (logout, auth desync) and
inconveniences with how Supabase and backend API works.
Resolves:
- #9006
- #8912

### Changes 🏗️

This PR streamlines how the Supabase and backend API is used to fix
current errors with auth, remove unnecessary code and make it easier to
use Supabase and backend API.

- Add `getServerSupabase` for server side that returns `SupabaseClient`.
- Add `Spinner` component that is used for loading animation.
- Remove redundant `useUser`, user is fetched in `useSupabase` already.
- Replace most Supabase `create*Client` to `getSupabaseServer` and
`useSupabase`.
- Remove redundant `AutoGPTServerAPI` class and rename
`BaseAutoGPTServerAPI` to `BackendAPI` and use it instead.
- Remove `SupabaseProvider` context; supabase caches internally what's
possible already.
- Move `useSupabase` hook to its own file and update it.

### Helpful table
| Next.js usage | Server | Client |
|---|---|---|
| API | `new BackendAPI();` | `new BackendAPI();`* or `useBackendAPI()`
|
| Supabase | `getServerSupabase();` | `useSupabase();` |
| user, user.role | `getServerUser();`** | `useSupabase();` |

\* `BackendAPI` automatically chooses correct Supabase client, so while
it's recommended to use `useBackendAPI()`, it's ok to use `new
BackendAPI();` in client components and even memoize it: `useMemo(() =>
new BackendAPI(), [])`.

** The reason user isn't returned in `getServerSupabase` is because it
forces async fetch but creating supabase doesn't, so it'd force
`getServerSupabase` to be async or return `{ supabase: SupabaseClient,
user: Promise<User> | null }`. For the same reason `useSupabase`
provides access to `supabase` immediately but `user` *may* be loading,
so there's `isUserLoading` provided as well.

### Checklist 📋

#### For code changes:
- [ ] I have clearly listed my changes in the PR description
- [ ] I have made a test plan
- [ ] I have tested my changes according to the test plan:
  <!-- Put your test plan here: -->
  - [ ] ...

<details>
  <summary>Example test plan</summary>
  
  - [ ] Create from scratch and execute an agent with at least 3 blocks
- [ ] Import an agent from file upload, and confirm it executes
correctly
  - [ ] Upload agent to marketplace
- [ ] Import an agent from marketplace and confirm it executes correctly
  - [ ] Edit an agent from monitor, and confirm it executes correctly
</details>

#### For configuration changes:
- [ ] `.env.example` is updated or already compatible with my changes
- [ ] `docker-compose.yml` is updated or already compatible with my
changes
- [ ] I have included a list of my configuration changes in the PR
description (under **Changes**)

<details>
  <summary>Examples of configuration changes</summary>

  - Changing ports
  - Adding new services that need to communicate with each other
  - Secrets or environment variable changes
  - New or infrastructure changes such as databases
</details>

---------

Co-authored-by: Aarushi <50577581+aarushik93@users.noreply.github.com>
Co-authored-by: Nicholas Tindle <nicholas.tindle@agpt.co>
  • Loading branch information
3 people authored Dec 18, 2024
1 parent 95bd268 commit 6ec2bac
Show file tree
Hide file tree
Showing 49 changed files with 889 additions and 1,160 deletions.
2 changes: 1 addition & 1 deletion autogpt_platform/frontend/public/mockServiceWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* - Please do NOT serve this file on production.
*/

const PACKAGE_VERSION = '2.6.8'
const PACKAGE_VERSION = '2.7.0'
const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
Expand Down
4 changes: 2 additions & 2 deletions autogpt_platform/frontend/src/app/auth/callback/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import getServerSupabase from "@/lib/supabase/getServerSupabase";
import { NextResponse } from "next/server";
import { createServerClient } from "@/lib/supabase/server";

// Handle the callback to complete the user session login
export async function GET(request: Request) {
Expand All @@ -9,7 +9,7 @@ export async function GET(request: Request) {
const next = searchParams.get("next") ?? "/";

if (code) {
const supabase = createServerClient();
const supabase = getServerSupabase();

if (!supabase) {
return NextResponse.redirect(`${origin}/error`);
Expand Down
4 changes: 2 additions & 2 deletions autogpt_platform/frontend/src/app/auth/confirm/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { type EmailOtpType } from "@supabase/supabase-js";
import { type NextRequest } from "next/server";

import { redirect } from "next/navigation";
import { createServerClient } from "@/lib/supabase/server";
import getServerSupabase from "@/lib/supabase/getServerSupabase";

// Email confirmation route
export async function GET(request: NextRequest) {
Expand All @@ -12,7 +12,7 @@ export async function GET(request: NextRequest) {
const next = searchParams.get("next") ?? "/";

if (token_hash && type) {
const supabase = createServerClient();
const supabase = getServerSupabase();

if (!supabase) {
redirect("/error");
Expand Down
10 changes: 0 additions & 10 deletions autogpt_platform/frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import TallyPopupSimple from "@/components/TallyPopup";
import { GoogleAnalytics } from "@next/third-parties/google";
import { Toaster } from "@/components/ui/toaster";
import { IconType } from "@/components/ui/icons";
import { createServerClient } from "@/lib/supabase/server";

const inter = Inter({ subsets: ["latin"] });

Expand All @@ -24,17 +23,10 @@ export default async function RootLayout({
}: Readonly<{
children: React.ReactNode;
}>) {
const supabase = createServerClient();

const {
data: { user },
} = await supabase.auth.getUser();

return (
<html lang="en">
<body className={cn("antialiased transition-colors", inter.className)}>
<Providers
initialUser={user}
attribute="class"
defaultTheme="light"
// Feel free to remove this line if you want to use the system theme by default
Expand All @@ -43,8 +35,6 @@ export default async function RootLayout({
>
<div className="flex min-h-screen flex-col items-center justify-center">
<Navbar
user={user}
isLoggedIn={!!user}
links={[
{
name: "Agent Store",
Expand Down
12 changes: 8 additions & 4 deletions autogpt_platform/frontend/src/app/login/actions.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { createServerClient } from "@/lib/supabase/server";
import { z } from "zod";
import * as Sentry from "@sentry/nextjs";
import getServerSupabase from "@/lib/supabase/getServerSupabase";
import BackendAPI from "@/lib/autogpt-server-api";

const loginFormSchema = z.object({
email: z.string().email().min(2).max(64),
Expand All @@ -15,7 +16,7 @@ export async function logout() {
"logout",
{},
async () => {
const supabase = createServerClient();
const supabase = getServerSupabase();

if (!supabase) {
redirect("/error");
Expand All @@ -36,7 +37,8 @@ export async function logout() {

export async function login(values: z.infer<typeof loginFormSchema>) {
return await Sentry.withServerActionInstrumentation("login", {}, async () => {
const supabase = createServerClient();
const supabase = getServerSupabase();
const api = new BackendAPI();

if (!supabase) {
redirect("/error");
Expand All @@ -45,6 +47,8 @@ export async function login(values: z.infer<typeof loginFormSchema>) {
// We are sure that the values are of the correct type because zod validates the form
const { data, error } = await supabase.auth.signInWithPassword(values);

await api.createUser();

if (error) {
console.log("Error logging in", error);
if (error.status == 400) {
Expand All @@ -70,7 +74,7 @@ export async function signup(values: z.infer<typeof loginFormSchema>) {
"signup",
{},
async () => {
const supabase = createServerClient();
const supabase = getServerSupabase();

if (!supabase) {
redirect("/error");
Expand Down
21 changes: 10 additions & 11 deletions autogpt_platform/frontend/src/app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"use client";
import useUser from "@/hooks/useUser";
import { login, signup } from "./actions";
import { Button } from "@/components/ui/button";
import {
Expand All @@ -18,10 +17,12 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { PasswordInput } from "@/components/PasswordInput";
import { FaGoogle, FaGithub, FaDiscord, FaSpinner } from "react-icons/fa";
import { useState } from "react";
import { useSupabase } from "@/components/providers/SupabaseProvider";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { Checkbox } from "@/components/ui/checkbox";
import useSupabase from "@/hooks/useSupabase";
import Spinner from "@/components/Spinner";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";

const loginFormSchema = z.object({
email: z.string().email().min(2).max(64),
Expand All @@ -32,11 +33,11 @@ const loginFormSchema = z.object({
});

export default function LoginPage() {
const { supabase, isLoading: isSupabaseLoading } = useSupabase();
const { user, isLoading: isUserLoading } = useUser();
const { supabase, user, isUserLoading } = useSupabase();
const [feedback, setFeedback] = useState<string | null>(null);
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const api = useBackendAPI();

const form = useForm<z.infer<typeof loginFormSchema>>({
resolver: zodResolver(loginFormSchema),
Expand All @@ -48,16 +49,12 @@ export default function LoginPage() {
});

if (user) {
console.log("User exists, redirecting to home");
console.debug("User exists, redirecting to /");
router.push("/");
}

if (isUserLoading || isSupabaseLoading || user) {
return (
<div className="flex h-[80vh] items-center justify-center">
<FaSpinner className="mr-2 h-16 w-16 animate-spin" />
</div>
);
if (isUserLoading || user) {
return <Spinner />;
}

if (!supabase) {
Expand All @@ -80,6 +77,8 @@ export default function LoginPage() {
},
});

await api.createUser();

if (!error) {
setFeedback(null);
return;
Expand Down
12 changes: 4 additions & 8 deletions autogpt_platform/frontend/src/app/monitoring/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
"use client";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import React, { useCallback, useEffect, useState } from "react";

import AutoGPTServerAPI, {
GraphExecution,
Schedule,
GraphMeta,
} from "@/lib/autogpt-server-api";
import { GraphExecution, Schedule, GraphMeta } from "@/lib/autogpt-server-api";

import { Card } from "@/components/ui/card";
import {
Expand All @@ -16,6 +12,7 @@ import {
FlowRunsStats,
} from "@/components/monitor";
import { SchedulesTable } from "@/components/monitor/scheduleTable";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";

const Monitor = () => {
const [flows, setFlows] = useState<GraphMeta[]>([]);
Expand All @@ -25,8 +22,7 @@ const Monitor = () => {
const [selectedRun, setSelectedRun] = useState<GraphExecution | null>(null);
const [sortColumn, setSortColumn] = useState<keyof Schedule>("id");
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");

const api = useMemo(() => new AutoGPTServerAPI(), []);
const api = useBackendAPI();

const fetchSchedules = useCallback(async () => {
setSchedules(await api.listSchedules());
Expand Down
43 changes: 19 additions & 24 deletions autogpt_platform/frontend/src/app/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
"use client";

import { useSupabase } from "@/components/providers/SupabaseProvider";
import { Button } from "@/components/ui/button";
import useUser from "@/hooks/useUser";
import { useRouter } from "next/navigation";
import { useCallback, useContext, useMemo, useState } from "react";
import { FaSpinner } from "react-icons/fa";
import { Separator } from "@/components/ui/separator";
import { useToast } from "@/components/ui/use-toast";
import { IconKey, IconUser } from "@/components/ui/icons";
Expand All @@ -31,10 +27,11 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import useSupabase from "@/hooks/useSupabase";
import Spinner from "@/components/Spinner";

export default function PrivatePage() {
const { user, isLoading, error } = useUser();
const { supabase } = useSupabase();
const { supabase, user, isUserLoading } = useSupabase();
const router = useRouter();
const providers = useContext(CredentialsProvidersContext);
const { toast } = useToast();
Expand Down Expand Up @@ -115,30 +112,28 @@ export default function PrivatePage() {
[],
);

if (isLoading || !providers) {
return (
<div className="flex h-[80vh] items-center justify-center">
<FaSpinner className="mr-2 h-16 w-16 animate-spin" />
</div>
);
if (isUserLoading) {
return <Spinner />;
}

if (error || !user || !supabase) {
if (!user || !supabase) {
router.push("/login");
return null;
}

const allCredentials = Object.values(providers).flatMap((provider) =>
[...provider.savedOAuthCredentials, ...provider.savedApiKeys]
.filter((cred) => !hiddenCredentials.includes(cred.id))
.map((credentials) => ({
...credentials,
provider: provider.provider,
providerName: provider.providerName,
ProviderIcon: providerIcons[provider.provider],
TypeIcon: { oauth2: IconUser, api_key: IconKey }[credentials.type],
})),
);
const allCredentials = providers
? Object.values(providers).flatMap((provider) =>
[...provider.savedOAuthCredentials, ...provider.savedApiKeys]
.filter((cred) => !hiddenCredentials.includes(cred.id))
.map((credentials) => ({
...credentials,
provider: provider.provider,
providerName: provider.providerName,
ProviderIcon: providerIcons[provider.provider],
TypeIcon: { oauth2: IconUser, api_key: IconKey }[credentials.type],
})),
)
: [];

return (
<div className="mx-auto max-w-3xl md:py-8">
Expand Down
24 changes: 8 additions & 16 deletions autogpt_platform/frontend/src/app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,19 @@ import { ThemeProvider as NextThemesProvider } from "next-themes";
import { ThemeProviderProps } from "next-themes";
import { BackendAPIProvider } from "@/lib/autogpt-server-api/context";
import { TooltipProvider } from "@/components/ui/tooltip";
import SupabaseProvider from "@/components/providers/SupabaseProvider";
import CredentialsProvider from "@/components/integrations/credentials-provider";
import { User } from "@supabase/supabase-js";
import { LaunchDarklyProvider } from "@/components/feature-flag/feature-flag-provider";

export function Providers({
children,
initialUser,
...props
}: ThemeProviderProps & { initialUser: User | null }) {
export function Providers({ children, ...props }: ThemeProviderProps) {
return (
<NextThemesProvider {...props}>
<SupabaseProvider initialUser={initialUser}>
<BackendAPIProvider>
<CredentialsProvider>
<LaunchDarklyProvider>
<TooltipProvider>{children}</TooltipProvider>
</LaunchDarklyProvider>
</CredentialsProvider>
</BackendAPIProvider>
</SupabaseProvider>
<BackendAPIProvider>
<CredentialsProvider>
<LaunchDarklyProvider>
<TooltipProvider>{children}</TooltipProvider>
</LaunchDarklyProvider>
</CredentialsProvider>
</BackendAPIProvider>
</NextThemesProvider>
);
}
8 changes: 3 additions & 5 deletions autogpt_platform/frontend/src/app/reset_password/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"use client";
import { useSupabase } from "@/components/providers/SupabaseProvider";
import { Button } from "@/components/ui/button";
import {
Form,
Expand All @@ -10,7 +9,7 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import useUser from "@/hooks/useUser";
import useSupabase from "@/hooks/useSupabase";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { useState } from "react";
Expand All @@ -33,8 +32,7 @@ const resetPasswordFormSchema = z
});

export default function ResetPasswordPage() {
const { supabase, isLoading: isSupabaseLoading } = useSupabase();
const { user, isLoading: isUserLoading } = useUser();
const { supabase, user, isUserLoading } = useSupabase();
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [feedback, setFeedback] = useState<string | null>(null);
Expand All @@ -54,7 +52,7 @@ export default function ResetPasswordPage() {
},
});

if (isUserLoading || isSupabaseLoading) {
if (isUserLoading) {
return (
<div className="flex h-[80vh] items-center justify-center">
<FaSpinner className="mr-2 h-16 w-16 animate-spin" />
Expand Down
Loading

0 comments on commit 6ec2bac

Please sign in to comment.