mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-09 23:28:07 -05:00
feat(frontend): handle cross-tab login/logout + auth architecture refactor (#10150)
## 🏗️ Changes ### 🧢 Authentication improvements - Updates for [CASA compliance](https://appdefensealliance.dev/casa) - implemented cross-tab login/logout - logout now triggers cross-device logout - forgot password triggers cross-device logout - we are already able to revoke sessions given Supabase stores sessions 🙌🏽 ### 📙 Cross-tab login/logout implementation I implemented some session validation debouncing ( _2-second cooldown_ ) to prevent excessive API calls when switching tabs fast ( _more of an edge-case but could happen_ ). Cross tab implementation is done via `localStorage` and `window.visibility` events. ### Refactor and cleanup Smol things to improve our auth logic on the Frontend: - created `helpers.ts` with utilities for protected page detection, admin page routing, and cross-tab communication - added `STORAGE_KEYS`, `PROTECTED_PAGES`, and `ADMIN_PAGES` constants for better organization - refactored server-side Supabase utilities and middleware - updated import paths to use named exports ## Checklist 📋 ### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Cross-tab logout synchronization works correctly - [x] Session validation debouncing prevents excessive API calls - [x] Protected page redirects function properly - [x] Authentication state persists correctly across tabs - [x] Role-based access controls work as expected - [x] Cross-device logout is performed after forgot password change ### Cross-tab login/logout https://github.com/user-attachments/assets/5dbdd204-faa2-419f-b989-e31f69ddabd6 ### Cross-device logout https://github.com/user-attachments/assets/aac9c97a-beec-4519-a391-f94f988dc7c8
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import getServerSupabase from "@/lib/supabase/getServerSupabase";
|
||||
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
|
||||
import BackendAPI from "@/lib/autogpt-server-api";
|
||||
import { NextResponse } from "next/server";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
@@ -2,7 +2,7 @@ import { type EmailOtpType } from "@supabase/supabase-js";
|
||||
import { type NextRequest } from "next/server";
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
import getServerSupabase from "@/lib/supabase/getServerSupabase";
|
||||
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
|
||||
|
||||
// Email confirmation route
|
||||
export async function GET(request: NextRequest) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import getServerSupabase from "@/lib/supabase/getServerSupabase";
|
||||
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
|
||||
import BackendAPI from "@/lib/autogpt-server-api";
|
||||
import { loginFormSchema, LoginProvider } from "@/types/auth";
|
||||
import { verifyTurnstileToken } from "@/lib/turnstile";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTurnstile } from "@/hooks/useTurnstile";
|
||||
import useSupabase from "@/lib/supabase/useSupabase";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { loginFormSchema, LoginProvider } from "@/types/auth";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { AgentsSection } from "@/components/agptui/composite/AgentsSection";
|
||||
import { BecomeACreator } from "@/components/agptui/BecomeACreator";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Metadata } from "next";
|
||||
import getServerUser from "@/lib/supabase/getServerUser";
|
||||
import { getServerUser } from "@/lib/supabase/server/getServerUser";
|
||||
|
||||
// Force dynamic rendering to avoid static generation issues with cookies
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
StoreSubmissionsResponse,
|
||||
StoreSubmissionRequest,
|
||||
} from "@/lib/autogpt-server-api/types";
|
||||
import useSupabase from "@/lib/supabase/useSupabase";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
|
||||
export default function Page({}: {}) {
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import useSupabase from "@/lib/supabase/useSupabase";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import LoadingBox from "@/components/ui/loading";
|
||||
|
||||
export default function UserIntegrationsPage() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import getServerSupabase from "@/lib/supabase/getServerSupabase";
|
||||
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
|
||||
import BackendApi from "@/lib/autogpt-server-api";
|
||||
import { NotificationPreferenceDTO } from "@/lib/autogpt-server-api/types";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { Metadata } from "next";
|
||||
import SettingsForm from "@/components/profile/settings/SettingsForm";
|
||||
import getServerUser from "@/lib/supabase/getServerUser";
|
||||
import { getServerUser } from "@/lib/supabase/server/getServerUser";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getUserPreferences } from "./actions";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use server";
|
||||
import getServerSupabase from "@/lib/supabase/getServerSupabase";
|
||||
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
|
||||
import { redirect } from "next/navigation";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { verifyTurnstileToken } from "@/lib/turnstile";
|
||||
@@ -64,7 +64,7 @@ export async function changePassword(password: string, turnstileToken: string) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
await supabase.auth.signOut();
|
||||
await supabase.auth.signOut({ scope: "global" });
|
||||
redirect("/login");
|
||||
},
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import useSupabase from "@/lib/supabase/useSupabase";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { sendEmailFormSchema, changePasswordFormSchema } from "@/types/auth";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import getServerSupabase from "@/lib/supabase/getServerSupabase";
|
||||
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
|
||||
import { signupFormSchema } from "@/types/auth";
|
||||
import BackendAPI from "@/lib/autogpt-server-api";
|
||||
import { verifyTurnstileToken } from "@/lib/turnstile";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTurnstile } from "@/hooks/useTurnstile";
|
||||
import useSupabase from "@/lib/supabase/useSupabase";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { signupFormSchema, LoginProvider } from "@/types/auth";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// components/RoleBasedAccess.tsx
|
||||
import useSupabase from "@/lib/supabase/useSupabase";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import React from "react";
|
||||
|
||||
interface RoleBasedAccessProps {
|
||||
|
||||
@@ -7,8 +7,9 @@ import { Button } from "./Button";
|
||||
import Wallet from "./Wallet";
|
||||
import { ProfileDetails } from "@/lib/autogpt-server-api/types";
|
||||
import { NavbarLink } from "./NavbarLink";
|
||||
import getServerUser from "@/lib/supabase/getServerUser";
|
||||
|
||||
import BackendAPI from "@/lib/autogpt-server-api";
|
||||
import { getServerUser } from "@/lib/supabase/server/getServerUser";
|
||||
|
||||
// Disable theme toggle for now
|
||||
// import { ThemeToggle } from "./ThemeToggle";
|
||||
|
||||
@@ -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 "@/lib/supabase/useSupabase";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
|
||||
export const ProfileInfoForm = ({ profile }: { profile: ProfileDetails }) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import useSupabase from "@/lib/supabase/useSupabase";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { IconLogOut } from "@/components/ui/icons";
|
||||
import { useTransition } from "react";
|
||||
import { LoadingSpinner } from "../ui/loading";
|
||||
@@ -16,7 +16,7 @@ export function ProfilePopoutMenuLogoutButton() {
|
||||
function handleLogout() {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
await supabase.logOut();
|
||||
await supabase.logOut({ scope: "global" });
|
||||
router.refresh();
|
||||
} catch (e) {
|
||||
Sentry.captureException(e);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { SearchBar } from "@/components/agptui/SearchBar";
|
||||
import { FilterChips } from "@/components/agptui/FilterChips";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
|
||||
export const HeroSection: React.FC = () => {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createContext, useCallback, useEffect, useState } from "react";
|
||||
import useSupabase from "@/lib/supabase/useSupabase";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import {
|
||||
APIKeyCredentials,
|
||||
CredentialsDeleteNeedConfirmationResponse,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import useSupabase from "@/lib/supabase/useSupabase";
|
||||
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
|
||||
import { OnboardingStep, UserOnboarding } from "@/lib/autogpt-server-api";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import getServerSupabase from "@/lib/supabase/getServerSupabase";
|
||||
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
|
||||
import { createBrowserClient } from "@supabase/ssr";
|
||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||
import type {
|
||||
|
||||
83
autogpt_platform/frontend/src/lib/supabase/helpers.ts
Normal file
83
autogpt_platform/frontend/src/lib/supabase/helpers.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
// Session management constants and utilities
|
||||
|
||||
export const PROTECTED_PAGES = [
|
||||
"/monitor",
|
||||
"/build",
|
||||
"/onboarding",
|
||||
"/profile",
|
||||
"/library",
|
||||
"/monitoring",
|
||||
] as const;
|
||||
|
||||
export const ADMIN_PAGES = ["/admin"] as const;
|
||||
|
||||
export const STORAGE_KEYS = {
|
||||
LOGOUT: "supabase-logout",
|
||||
} as const;
|
||||
|
||||
// Page protection utilities
|
||||
export function isProtectedPage(pathname: string): boolean {
|
||||
return PROTECTED_PAGES.some((page) => pathname.startsWith(page));
|
||||
}
|
||||
|
||||
export function isAdminPage(pathname: string): boolean {
|
||||
return ADMIN_PAGES.some((page) => pathname.startsWith(page));
|
||||
}
|
||||
|
||||
export function shouldRedirectOnLogout(pathname: string): boolean {
|
||||
return isProtectedPage(pathname) || isAdminPage(pathname);
|
||||
}
|
||||
|
||||
// Cross-tab logout utilities
|
||||
export function broadcastLogout(): void {
|
||||
if (typeof window !== "undefined") {
|
||||
window.localStorage.setItem(STORAGE_KEYS.LOGOUT, Date.now().toString());
|
||||
}
|
||||
}
|
||||
|
||||
export function isLogoutEvent(event: StorageEvent): boolean {
|
||||
return event.key === STORAGE_KEYS.LOGOUT;
|
||||
}
|
||||
|
||||
// Redirect utilities
|
||||
export function getRedirectPath(
|
||||
pathname: string,
|
||||
userRole?: string,
|
||||
): string | null {
|
||||
if (shouldRedirectOnLogout(pathname)) {
|
||||
return "/login";
|
||||
}
|
||||
|
||||
if (isAdminPage(pathname) && userRole !== "admin") {
|
||||
return "/marketplace";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Event listener management
|
||||
export interface EventListeners {
|
||||
cleanup: () => void;
|
||||
}
|
||||
|
||||
export function setupSessionEventListeners(
|
||||
onVisibilityChange: () => void,
|
||||
onFocus: () => void,
|
||||
onStorageChange: (e: StorageEvent) => void,
|
||||
): EventListeners {
|
||||
if (typeof window === "undefined" || typeof document === "undefined") {
|
||||
return { cleanup: () => {} };
|
||||
}
|
||||
|
||||
document.addEventListener("visibilitychange", onVisibilityChange);
|
||||
window.addEventListener("focus", onFocus);
|
||||
window.addEventListener("storage", onStorageChange);
|
||||
|
||||
return {
|
||||
cleanup: () => {
|
||||
document.removeEventListener("visibilitychange", onVisibilityChange);
|
||||
window.removeEventListener("focus", onFocus);
|
||||
window.removeEventListener("storage", onStorageChange);
|
||||
},
|
||||
};
|
||||
}
|
||||
158
autogpt_platform/frontend/src/lib/supabase/hooks/useSupabase.ts
Normal file
158
autogpt_platform/frontend/src/lib/supabase/hooks/useSupabase.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
"use client";
|
||||
import { useEffect, useMemo, useState, useRef } from "react";
|
||||
import { createBrowserClient } from "@supabase/ssr";
|
||||
import { SignOut, User } from "@supabase/supabase-js";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
broadcastLogout,
|
||||
getRedirectPath,
|
||||
isLogoutEvent,
|
||||
setupSessionEventListeners,
|
||||
} from "../helpers";
|
||||
|
||||
export function useSupabase() {
|
||||
const router = useRouter();
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [isUserLoading, setIsUserLoading] = useState(true);
|
||||
const lastValidationRef = useRef<number>(0);
|
||||
|
||||
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;
|
||||
}
|
||||
}, []);
|
||||
|
||||
async function logOut(options?: SignOut) {
|
||||
if (!supabase) return;
|
||||
|
||||
broadcastLogout();
|
||||
|
||||
const { error } = await supabase.auth.signOut({
|
||||
scope: "global",
|
||||
});
|
||||
if (error) console.error("Error logging out:", error);
|
||||
|
||||
router.push("/login");
|
||||
}
|
||||
|
||||
async function validateSession() {
|
||||
if (!supabase) return false;
|
||||
|
||||
// Simple debounce - only validate if 2 seconds have passed
|
||||
const now = Date.now();
|
||||
if (now - lastValidationRef.current < 2000) {
|
||||
return true;
|
||||
}
|
||||
lastValidationRef.current = now;
|
||||
|
||||
try {
|
||||
const {
|
||||
data: { user: apiUser },
|
||||
error,
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
if (error || !apiUser) {
|
||||
// Session is invalid, clear local state
|
||||
setUser(null);
|
||||
const redirectPath = getRedirectPath(window.location.pathname);
|
||||
if (redirectPath) {
|
||||
router.push(redirectPath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update local state if we have a valid user but no local user
|
||||
if (apiUser && !user) {
|
||||
setUser(apiUser);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Session validation error:", error);
|
||||
setUser(null);
|
||||
const redirectPath = getRedirectPath(window.location.pathname);
|
||||
if (redirectPath) {
|
||||
router.push(redirectPath);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleCrossTabLogout(e: StorageEvent) {
|
||||
if (!isLogoutEvent(e)) return;
|
||||
|
||||
// Clear the Supabase session first
|
||||
if (supabase) {
|
||||
supabase.auth.signOut({ scope: "global" }).catch(console.error);
|
||||
}
|
||||
|
||||
// Clear local state immediately
|
||||
setUser(null);
|
||||
router.refresh();
|
||||
|
||||
const redirectPath = getRedirectPath(window.location.pathname);
|
||||
if (redirectPath) {
|
||||
router.push(redirectPath);
|
||||
}
|
||||
}
|
||||
|
||||
function handleVisibilityChange() {
|
||||
if (document.visibilityState === "visible") {
|
||||
validateSession();
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocus() {
|
||||
validateSession();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!supabase) {
|
||||
setIsUserLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((event, session) => {
|
||||
const newUser = session?.user ?? null;
|
||||
|
||||
// Only update if user actually changed to prevent unnecessary re-renders
|
||||
setUser((currentUser) => {
|
||||
if (currentUser?.id !== newUser?.id) {
|
||||
return newUser;
|
||||
}
|
||||
return currentUser;
|
||||
});
|
||||
|
||||
setIsUserLoading(false);
|
||||
});
|
||||
|
||||
const eventListeners = setupSessionEventListeners(
|
||||
handleVisibilityChange,
|
||||
handleFocus,
|
||||
handleCrossTabLogout,
|
||||
);
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
eventListeners.cleanup();
|
||||
};
|
||||
}, [supabase]);
|
||||
|
||||
return {
|
||||
supabase,
|
||||
user,
|
||||
isLoggedIn: !isUserLoading ? !!user : null,
|
||||
isUserLoading,
|
||||
logOut,
|
||||
validateSession,
|
||||
};
|
||||
}
|
||||
@@ -1,16 +1,11 @@
|
||||
import { createServerClient } from "@supabase/ssr";
|
||||
import { NextResponse, type NextRequest } from "next/server";
|
||||
|
||||
// TODO: Update the protected pages list
|
||||
const PROTECTED_PAGES = [
|
||||
"/monitor",
|
||||
"/build",
|
||||
"/onboarding",
|
||||
"/profile",
|
||||
"/library",
|
||||
"/monitoring",
|
||||
];
|
||||
const ADMIN_PAGES = ["/admin"];
|
||||
import {
|
||||
PROTECTED_PAGES,
|
||||
ADMIN_PAGES,
|
||||
isProtectedPage,
|
||||
isAdminPage,
|
||||
} from "./helpers";
|
||||
|
||||
export async function updateSession(request: NextRequest) {
|
||||
let supabaseResponse = NextResponse.next({
|
||||
@@ -59,41 +54,26 @@ export async function updateSession(request: NextRequest) {
|
||||
error,
|
||||
} = await supabase.auth.getUser();
|
||||
|
||||
// Get the user role
|
||||
const userRole = user?.role;
|
||||
const url = request.nextUrl.clone();
|
||||
const pathname = request.nextUrl.pathname;
|
||||
|
||||
// AUTH REDIRECTS
|
||||
// 1. Check if user is not authenticated but trying to access protected content
|
||||
if (!user) {
|
||||
// Check if the user is trying to access either a protected page or an admin page
|
||||
const isAttemptingProtectedPage = PROTECTED_PAGES.some((page) =>
|
||||
request.nextUrl.pathname.startsWith(page),
|
||||
);
|
||||
const attemptingProtectedPage = isProtectedPage(pathname);
|
||||
const attemptingAdminPage = isAdminPage(pathname);
|
||||
|
||||
const isAttemptingAdminPage = ADMIN_PAGES.some((page) =>
|
||||
request.nextUrl.pathname.startsWith(page),
|
||||
);
|
||||
|
||||
// If trying to access any protected content without being logged in,
|
||||
// redirect to login page
|
||||
if (isAttemptingProtectedPage || isAttemptingAdminPage) {
|
||||
url.pathname = `/login`;
|
||||
if (attemptingProtectedPage || attemptingAdminPage) {
|
||||
url.pathname = "/login";
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Check if user is authenticated but lacks admin role when accessing admin pages
|
||||
if (user && userRole !== "admin") {
|
||||
const isAttemptingAdminPage = ADMIN_PAGES.some((page) =>
|
||||
request.nextUrl.pathname.startsWith(page),
|
||||
);
|
||||
|
||||
// If a non-admin user is trying to access admin pages,
|
||||
// redirect to marketplace
|
||||
if (isAttemptingAdminPage) {
|
||||
url.pathname = `/marketplace`;
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
if (user && userRole !== "admin" && isAdminPage(pathname)) {
|
||||
url.pathname = "/marketplace";
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
|
||||
// IMPORTANT: You *must* return the supabaseResponse object as it is. If you're
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { UnsafeUnwrappedCookies } from "next/headers";
|
||||
import { createServerClient } from "@supabase/ssr";
|
||||
|
||||
export default async function getServerSupabase() {
|
||||
export async function getServerSupabase() {
|
||||
// Need require here, so Next.js doesn't complain about importing this on client side
|
||||
const { cookies } = require("next/headers");
|
||||
const cookieStore = await cookies();
|
||||
@@ -1,6 +1,6 @@
|
||||
import getServerSupabase from "./getServerSupabase";
|
||||
import { getServerSupabase } from "./getServerSupabase";
|
||||
|
||||
const getServerUser = async () => {
|
||||
export async function getServerUser() {
|
||||
const supabase = await getServerSupabase();
|
||||
|
||||
if (!supabase) {
|
||||
@@ -10,7 +10,7 @@ const getServerUser = async () => {
|
||||
try {
|
||||
const {
|
||||
data: { user },
|
||||
error,
|
||||
error: _,
|
||||
} = await supabase.auth.getUser();
|
||||
// if (error) {
|
||||
// // FIX: Suppressing error for now. Need to stop the nav bar calling this all the time
|
||||
@@ -30,6 +30,4 @@ const getServerUser = async () => {
|
||||
error: `Unexpected error: ${(error as Error).message}`,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default getServerUser;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
"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<User | null>(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 };
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
import { redirect } from "next/navigation";
|
||||
import getServerUser from "./supabase/getServerUser";
|
||||
import { getServerUser } from "./supabase/server/getServerUser";
|
||||
|
||||
export async function withRoleAccess(allowedRoles: string[]) {
|
||||
"use server";
|
||||
|
||||
Reference in New Issue
Block a user