Merge branch 'dev' into redesigning-block-menu

This commit is contained in:
Abhimanyu Yadav
2025-06-09 21:29:30 +05:30
committed by GitHub
10 changed files with 364 additions and 188 deletions

View File

@@ -1,12 +1,23 @@
import getServerSupabase from "@/lib/supabase/getServerSupabase";
import BackendAPI from "@/lib/autogpt-server-api";
import { NextResponse } from "next/server";
import { revalidatePath } from "next/cache";
async function shouldShowOnboarding() {
const api = new BackendAPI();
return (
(await api.isOnboardingEnabled()) &&
!(await api.getUserOnboarding()).completedSteps.includes("CONGRATS")
);
}
// Handle the callback to complete the user session login
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get("code");
// if "next" is in param, use it as the redirect URL
const next = searchParams.get("next") ?? "/";
let next = searchParams.get("next") ?? "/";
if (code) {
const supabase = await getServerSupabase();
@@ -18,6 +29,21 @@ export async function GET(request: Request) {
const { data, error } = await supabase.auth.exchangeCodeForSession(code);
// data.session?.refresh_token is available if you need to store it for later use
if (!error) {
try {
const api = new BackendAPI();
await api.createUser();
if (await shouldShowOnboarding()) {
next = "/onboarding";
revalidatePath("/onboarding", "layout");
} else {
revalidatePath("/", "layout");
}
} catch (createUserError) {
console.error("Error creating user:", createUserError);
// Continue with redirect even if createUser fails
}
const forwardedHost = request.headers.get("x-forwarded-host"); // original origin before load balancer
const isLocalEnv = process.env.NODE_ENV === "development";
if (isLocalEnv) {

View File

@@ -61,13 +61,12 @@ export async function providerLogin(provider: LoginProvider) {
{},
async () => {
const supabase = await getServerSupabase();
const api = new BackendAPI();
if (!supabase) {
redirect("/error");
}
const { error } = await supabase!.auth.signInWithOAuth({
const { data, error } = await supabase!.auth.signInWithOAuth({
provider: provider,
options: {
redirectTo:
@@ -81,12 +80,13 @@ export async function providerLogin(provider: LoginProvider) {
return error.message;
}
await api.createUser();
// Don't onboard if disabled or already onboarded
if (await shouldShowOnboarding()) {
revalidatePath("/onboarding", "layout");
redirect("/onboarding");
// Redirect to the OAuth provider's URL
if (data?.url) {
redirect(data.url);
}
// Note: api.createUser() and onboarding check happen in the callback handler
// after the session is established. See `auth/callback/route.ts`.
},
);
}

View File

@@ -1,5 +1,4 @@
"use client";
import { login, providerLogin } from "./actions";
import {
Form,
FormControl,
@@ -8,14 +7,8 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { useForm } from "react-hook-form";
import { Input } from "@/components/ui/input";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useCallback, useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import useSupabase from "@/lib/supabase/useSupabase";
import LoadingBox from "@/components/ui/loading";
import {
AuthCard,
@@ -23,92 +16,34 @@ import {
AuthButton,
AuthFeedback,
AuthBottomText,
GoogleOAuthButton,
PasswordInput,
Turnstile,
} from "@/components/auth";
import { loginFormSchema } from "@/types/auth";
import { getBehaveAs } from "@/lib/utils";
import { useTurnstile } from "@/hooks/useTurnstile";
import { useLoginPage } from "./useLoginPage";
export default function LoginPage() {
const { supabase, user, isUserLoading } = useSupabase();
const [feedback, setFeedback] = useState<string | null>(null);
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [captchaKey, setCaptchaKey] = useState(0);
const {
form,
feedback,
turnstile,
captchaKey,
isLoading,
isCloudEnv,
isLoggedIn,
isUserLoading,
isGoogleLoading,
isSupabaseAvailable,
handleSubmit,
handleProviderLogin,
} = useLoginPage();
const turnstile = useTurnstile({
action: "login",
autoVerify: false,
resetOnError: true,
});
const form = useForm<z.infer<typeof loginFormSchema>>({
resolver: zodResolver(loginFormSchema),
defaultValues: {
email: "",
password: "",
},
});
// TODO: uncomment when we enable social login
// const onProviderLogin = useCallback(async (
// provider: LoginProvider,
// ) => {
// setIsLoading(true);
// const error = await providerLogin(provider);
// setIsLoading(false);
// if (error) {
// setFeedback(error);
// return;
// }
// setFeedback(null);
// }, [supabase]);
const resetCaptcha = useCallback(() => {
setCaptchaKey((k) => k + 1);
turnstile.reset();
}, [turnstile]);
const onLogin = useCallback(
async (data: z.infer<typeof loginFormSchema>) => {
setIsLoading(true);
if (!(await form.trigger())) {
setIsLoading(false);
return;
}
if (!turnstile.verified) {
setFeedback("Please complete the CAPTCHA challenge.");
setIsLoading(false);
resetCaptcha();
return;
}
const error = await login(data, turnstile.token as string);
await supabase?.auth.refreshSession();
setIsLoading(false);
if (error) {
setFeedback(error);
resetCaptcha();
return;
}
setFeedback(null);
},
[form, turnstile, supabase],
);
if (user) {
console.debug("User exists, redirecting to /");
router.push("/");
}
if (isUserLoading || user) {
if (isUserLoading || isLoggedIn) {
return <LoadingBox className="h-[80vh]" />;
}
if (!supabase) {
if (!isSupabaseAvailable) {
return (
<div>
User accounts are disabled because Supabase client is unavailable
@@ -119,8 +54,26 @@ export default function LoginPage() {
return (
<AuthCard className="mx-auto">
<AuthHeader>Login to your account</AuthHeader>
{isCloudEnv ? (
<>
<div className="mb-6">
<GoogleOAuthButton
onClick={() => handleProviderLogin("google")}
isLoading={isGoogleLoading}
disabled={isLoading}
/>
</div>
<div className="mb-6 flex items-center">
<div className="flex-1 border-t border-gray-300"></div>
<span className="mx-3 text-sm text-gray-500">or</span>
<div className="flex-1 border-t border-gray-300"></div>
</div>
</>
) : null}
<Form {...form}>
<form onSubmit={form.handleSubmit(onLogin)}>
<form onSubmit={handleSubmit}>
<FormField
control={form.control}
name="email"
@@ -176,11 +129,7 @@ export default function LoginPage() {
shouldRender={turnstile.shouldRender}
/>
<AuthButton
onClick={() => onLogin(form.getValues())}
isLoading={isLoading}
type="submit"
>
<AuthButton isLoading={isLoading} type="submit">
Login
</AuthButton>
</form>

View File

@@ -0,0 +1,102 @@
import { useTurnstile } from "@/hooks/useTurnstile";
import useSupabase from "@/lib/supabase/useSupabase";
import { loginFormSchema, LoginProvider } from "@/types/auth";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { login, providerLogin } from "./actions";
import z from "zod";
import { BehaveAs } from "@/lib/utils";
import { getBehaveAs } from "@/lib/utils";
export function useLoginPage() {
const { supabase, user, isUserLoading } = useSupabase();
const [feedback, setFeedback] = useState<string | null>(null);
const [captchaKey, setCaptchaKey] = useState(0);
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
const isCloudEnv = getBehaveAs() === BehaveAs.CLOUD;
const turnstile = useTurnstile({
action: "login",
autoVerify: false,
resetOnError: true,
});
const form = useForm<z.infer<typeof loginFormSchema>>({
resolver: zodResolver(loginFormSchema),
defaultValues: {
email: "",
password: "",
},
});
const resetCaptcha = useCallback(() => {
setCaptchaKey((k) => k + 1);
turnstile.reset();
}, [turnstile]);
useEffect(() => {
if (user) router.push("/");
}, [user]);
async function handleProviderLogin(provider: LoginProvider) {
setIsGoogleLoading(true);
try {
const error = await providerLogin(provider);
if (error) throw error;
setFeedback(null);
} catch (error) {
resetCaptcha();
setFeedback(JSON.stringify(error));
} finally {
setIsGoogleLoading(false);
}
}
async function handleLogin(data: z.infer<typeof loginFormSchema>) {
setIsLoading(true);
if (!turnstile.verified) {
setFeedback("Please complete the CAPTCHA challenge.");
setIsLoading(false);
resetCaptcha();
return;
}
if (data.email.includes("@agpt.co")) {
setFeedback("Please use Google SSO to login using an AutoGPT email.");
setIsLoading(false);
resetCaptcha();
return;
}
const error = await login(data, turnstile.token as string);
await supabase?.auth.refreshSession();
setIsLoading(false);
if (error) {
setFeedback(error);
resetCaptcha();
// Always reset the turnstile on any error
turnstile.reset();
return;
}
setFeedback(null);
}
return {
form,
feedback,
turnstile,
captchaKey,
isLoggedIn: !!user,
isLoading,
isCloudEnv,
isUserLoading,
isGoogleLoading,
isSupabaseAvailable: !!supabase,
handleSubmit: form.handleSubmit(handleLogin),
handleProviderLogin,
};
}

View File

@@ -1,5 +1,4 @@
"use client";
import { signup } from "./actions";
import {
Form,
FormControl,
@@ -9,102 +8,44 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { useForm } from "react-hook-form";
import { Input } from "@/components/ui/input";
import type { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useCallback, useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { Checkbox } from "@/components/ui/checkbox";
import useSupabase from "@/lib/supabase/useSupabase";
import LoadingBox from "@/components/ui/loading";
import {
AuthCard,
AuthHeader,
AuthButton,
AuthBottomText,
GoogleOAuthButton,
PasswordInput,
Turnstile,
} from "@/components/auth";
import AuthFeedback from "@/components/auth/AuthFeedback";
import { signupFormSchema } from "@/types/auth";
import { getBehaveAs } from "@/lib/utils";
import { useTurnstile } from "@/hooks/useTurnstile";
import { useSignupPage } from "./useSignupPage";
export default function SignupPage() {
const { supabase, user, isUserLoading } = useSupabase();
const [feedback, setFeedback] = useState<string | null>(null);
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [captchaKey, setCaptchaKey] = useState(0);
//TODO: Remove after closed beta
const {
form,
feedback,
turnstile,
captchaKey,
isLoggedIn,
isLoading,
isCloudEnv,
isUserLoading,
isGoogleLoading,
isSupabaseAvailable,
handleSubmit,
handleProviderSignup,
} = useSignupPage();
const turnstile = useTurnstile({
action: "signup",
autoVerify: false,
resetOnError: true,
});
const form = useForm<z.infer<typeof signupFormSchema>>({
resolver: zodResolver(signupFormSchema),
defaultValues: {
email: "",
password: "",
confirmPassword: "",
agreeToTerms: false,
},
});
const resetCaptcha = useCallback(() => {
setCaptchaKey((k) => k + 1);
turnstile.reset();
}, [turnstile]);
const onSignup = useCallback(
async (data: z.infer<typeof signupFormSchema>) => {
setIsLoading(true);
if (!(await form.trigger())) {
setIsLoading(false);
return;
}
if (!turnstile.verified) {
setFeedback("Please complete the CAPTCHA challenge.");
setIsLoading(false);
resetCaptcha();
return;
}
const error = await signup(data, turnstile.token as string);
setIsLoading(false);
if (error) {
if (error === "user_already_exists") {
setFeedback("User with this email already exists");
resetCaptcha();
return;
} else {
setFeedback(error);
resetCaptcha();
}
return;
}
setFeedback(null);
},
[form, turnstile],
);
if (user) {
console.debug("User exists, redirecting to /");
router.push("/");
}
if (isUserLoading || user) {
if (isUserLoading || isLoggedIn) {
return <LoadingBox className="h-[80vh]" />;
}
if (!supabase) {
if (!isSupabaseAvailable) {
return (
<div>
User accounts are disabled because Supabase client is unavailable
@@ -115,8 +56,26 @@ export default function SignupPage() {
return (
<AuthCard className="mx-auto mt-12">
<AuthHeader>Create a new account</AuthHeader>
{isCloudEnv ? (
<>
<div className="mb-6">
<GoogleOAuthButton
onClick={() => handleProviderSignup("google")}
isLoading={isGoogleLoading}
disabled={isLoading}
/>
</div>
<div className="mb-6 flex items-center">
<div className="flex-1 border-t border-gray-300"></div>
<span className="mx-3 text-sm text-gray-500">or</span>
<div className="flex-1 border-t border-gray-300"></div>
</div>
</>
) : null}
<Form {...form}>
<form onSubmit={form.handleSubmit(onSignup)}>
<form onSubmit={handleSubmit}>
<FormField
control={form.control}
name="email"
@@ -177,11 +136,7 @@ export default function SignupPage() {
shouldRender={turnstile.shouldRender}
/>
<AuthButton
onClick={() => onSignup(form.getValues())}
isLoading={isLoading}
type="submit"
>
<AuthButton isLoading={isLoading} type="submit">
Sign up
</AuthButton>
<FormField

View File

@@ -0,0 +1,110 @@
import { useTurnstile } from "@/hooks/useTurnstile";
import useSupabase from "@/lib/supabase/useSupabase";
import { signupFormSchema, LoginProvider } from "@/types/auth";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { signup } from "./actions";
import { providerLogin } from "../login/actions";
import z from "zod";
import { BehaveAs, getBehaveAs } from "@/lib/utils";
export function useSignupPage() {
const { supabase, user, isUserLoading } = useSupabase();
const [feedback, setFeedback] = useState<string | null>(null);
const [captchaKey, setCaptchaKey] = useState(0);
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
const isCloudEnv = getBehaveAs() === BehaveAs.CLOUD;
const turnstile = useTurnstile({
action: "signup",
autoVerify: false,
resetOnError: true,
});
const resetCaptcha = useCallback(() => {
setCaptchaKey((k) => k + 1);
turnstile.reset();
}, [turnstile]);
const form = useForm<z.infer<typeof signupFormSchema>>({
resolver: zodResolver(signupFormSchema),
defaultValues: {
email: "",
password: "",
confirmPassword: "",
agreeToTerms: false,
},
});
useEffect(() => {
if (user) router.push("/");
}, [user]);
async function handleProviderSignup(provider: LoginProvider) {
setIsGoogleLoading(true);
const error = await providerLogin(provider);
setIsGoogleLoading(false);
if (error) {
resetCaptcha();
setFeedback(error);
return;
}
setFeedback(null);
}
async function handleSignup(data: z.infer<typeof signupFormSchema>) {
setIsLoading(true);
if (!turnstile.verified) {
setFeedback("Please complete the CAPTCHA challenge.");
setIsLoading(false);
resetCaptcha();
return;
}
if (data.email.includes("@agpt.co")) {
setFeedback(
"Please use Google SSO to create an account using an AutoGPT email.",
);
setIsLoading(false);
resetCaptcha();
return;
}
const error = await signup(data, turnstile.token as string);
setIsLoading(false);
if (error) {
if (error === "user_already_exists") {
setFeedback("User with this email already exists");
turnstile.reset();
return;
} else {
setFeedback(error);
resetCaptcha();
turnstile.reset();
}
return;
}
setFeedback(null);
}
return {
form,
feedback,
turnstile,
captchaKey,
isLoggedIn: !!user,
isLoading,
isCloudEnv,
isUserLoading,
isGoogleLoading,
isSupabaseAvailable: !!supabase,
handleSubmit: form.handleSubmit(handleSignup),
handleProviderSignup,
};
}

View File

@@ -14,7 +14,7 @@ const buttonVariants = cva(
destructive:
"bg-red-600 text-neutral-50 border border-red-500/50 hover:bg-red-500/90 dark:bg-red-700 dark:text-neutral-50 dark:hover:bg-red-600",
accent: "bg-accent text-accent-foreground hover:bg-violet-500",
primary: "bg-neutral-800 text-white hover:bg-black/60",
primary: "bg-zinc-700 text-white hover:bg-zinc-800 text-white",
outline:
"border border-black/50 text-neutral-800 hover:bg-neutral-100 dark:bg-neutral-800 dark:text-neutral-100 dark:hover:bg-neutral-700",
secondary:

View File

@@ -1,25 +1,26 @@
import { ReactNode } from "react";
import { Button } from "../ui/button";
import { FaSpinner } from "react-icons/fa";
import { Button } from "../ui/button";
interface Props {
children?: ReactNode;
onClick: () => void;
isLoading?: boolean;
disabled?: boolean;
type?: "button" | "submit" | "reset";
onClick?: () => void;
}
export default function AuthButton({
children,
onClick,
isLoading = false,
disabled = false,
type = "button",
onClick,
}: Props) {
return (
<Button
className="mt-2 w-full self-stretch rounded-md bg-slate-900 px-4 py-2"
className="mt-2 w-full px-4 py-2 text-zinc-800"
variant="outline"
type={type}
disabled={isLoading || disabled}
onClick={onClick}
@@ -27,9 +28,7 @@ export default function AuthButton({
{isLoading ? (
<FaSpinner className="animate-spin" />
) : (
<div className="text-sm font-medium leading-normal text-slate-50">
{children}
</div>
<div className="text-sm font-medium">{children}</div>
)}
</Button>
);

View File

@@ -0,0 +1,33 @@
import { useState } from "react";
import { FaGoogle, FaSpinner } from "react-icons/fa";
import { Button } from "../ui/button";
interface GoogleOAuthButtonProps {
onClick: () => void;
isLoading?: boolean;
disabled?: boolean;
}
export default function GoogleOAuthButton({
onClick,
isLoading = false,
disabled = false,
}: GoogleOAuthButtonProps) {
return (
<Button
type="button"
className="w-full border bg-zinc-700 py-2 text-white disabled:opacity-50"
disabled={isLoading || disabled}
onClick={onClick}
>
{isLoading ? (
<FaSpinner className="mr-2 h-4 w-4 animate-spin" />
) : (
<FaGoogle className="mr-2 h-4 w-4" />
)}
<span className="text-sm font-medium">
{isLoading ? "Signing in..." : "Continue with Google"}
</span>
</Button>
);
}

View File

@@ -3,6 +3,7 @@ import AuthButton from "./AuthButton";
import AuthCard from "./AuthCard";
import AuthFeedback from "./AuthFeedback";
import AuthHeader from "./AuthHeader";
import GoogleOAuthButton from "./GoogleOAuthButton";
import { PasswordInput } from "./PasswordInput";
import Turnstile from "./Turnstile";
@@ -12,6 +13,7 @@ export {
AuthCard,
AuthFeedback,
AuthHeader,
GoogleOAuthButton,
PasswordInput,
Turnstile,
};