Refactor: Centralized Supabase Auth and implemented Auth Guards to prevent 401 errors
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import BottleDetails from '@/components/BottleDetails';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { validateSession } from '@/services/validate-session';
|
||||
import OfflineIndicator from '@/components/OfflineIndicator';
|
||||
import { useParams, useSearchParams } from 'next/navigation';
|
||||
@@ -12,12 +13,15 @@ export default function BottlePage() {
|
||||
const searchParams = useSearchParams();
|
||||
const [sessionId, setSessionId] = useState<string | undefined>(undefined);
|
||||
const [userId, setUserId] = useState<string | undefined>(undefined);
|
||||
const { user, isLoading: isAuthLoading } = useAuth();
|
||||
const supabase = createClient();
|
||||
|
||||
const bottleId = params?.id as string;
|
||||
const rawSessionId = searchParams?.get('session_id');
|
||||
|
||||
useEffect(() => {
|
||||
if (isAuthLoading) return;
|
||||
|
||||
const checkSession = async () => {
|
||||
if (rawSessionId) {
|
||||
const isValid = await validateSession(rawSessionId);
|
||||
@@ -27,16 +31,11 @@ export default function BottlePage() {
|
||||
}
|
||||
};
|
||||
|
||||
const getAuth = async () => {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (user) {
|
||||
setUserId(user.id);
|
||||
}
|
||||
};
|
||||
|
||||
checkSession();
|
||||
getAuth();
|
||||
}, [rawSessionId, supabase]);
|
||||
if (user) {
|
||||
setUserId(user.id);
|
||||
checkSession();
|
||||
}
|
||||
}, [rawSessionId, user, isAuthLoading]);
|
||||
|
||||
if (!bottleId) return null;
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import OfflineIndicator from "@/components/OfflineIndicator";
|
||||
import UploadQueue from "@/components/UploadQueue";
|
||||
import { I18nProvider } from "@/i18n/I18nContext";
|
||||
import { SessionProvider } from "@/context/SessionContext";
|
||||
import { AuthProvider } from "@/context/AuthContext";
|
||||
import ActiveSessionBanner from "@/components/ActiveSessionBanner";
|
||||
import MainContentWrapper from "@/components/MainContentWrapper";
|
||||
import AuthListener from "@/components/AuthListener";
|
||||
import SyncHandler from "@/components/SyncHandler";
|
||||
import CookieBanner from "@/components/CookieBanner";
|
||||
import OnboardingTutorial from "@/components/OnboardingTutorial";
|
||||
@@ -49,18 +49,19 @@ export default function RootLayout({
|
||||
<html lang="de" suppressHydrationWarning={true}>
|
||||
<body className={`${inter.variable} font-sans`}>
|
||||
<I18nProvider>
|
||||
<SessionProvider>
|
||||
<AuthListener />
|
||||
<ActiveSessionBanner />
|
||||
<MainContentWrapper>
|
||||
<SyncHandler />
|
||||
<PWARegistration />
|
||||
<UploadQueue />
|
||||
{children}
|
||||
</MainContentWrapper>
|
||||
<CookieBanner />
|
||||
<OnboardingTutorial />
|
||||
</SessionProvider>
|
||||
<AuthProvider>
|
||||
<SessionProvider>
|
||||
<ActiveSessionBanner />
|
||||
<MainContentWrapper>
|
||||
<SyncHandler />
|
||||
<PWARegistration />
|
||||
<UploadQueue />
|
||||
{children}
|
||||
</MainContentWrapper>
|
||||
<CookieBanner />
|
||||
<OnboardingTutorial />
|
||||
</SessionProvider>
|
||||
</AuthProvider>
|
||||
</I18nProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,6 +12,7 @@ import DramOfTheDay from "@/components/DramOfTheDay";
|
||||
import LanguageSwitcher from "@/components/LanguageSwitcher";
|
||||
import OfflineIndicator from "@/components/OfflineIndicator";
|
||||
import { useI18n } from "@/i18n/I18nContext";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useSession } from "@/context/SessionContext";
|
||||
import TastingHub from "@/components/TastingHub";
|
||||
import { Sparkles, X, Loader2 } from "lucide-react";
|
||||
@@ -25,8 +26,8 @@ export default function Home() {
|
||||
const supabase = createClient();
|
||||
const router = useRouter();
|
||||
const [bottles, setBottles] = useState<any[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [user, setUser] = useState<any>(null);
|
||||
const { user, isLoading: isAuthLoading } = useAuth();
|
||||
const [isInternalLoading, setIsInternalLoading] = useState(false);
|
||||
const [fetchError, setFetchError] = useState<string | null>(null);
|
||||
const { t } = useI18n();
|
||||
const { activeSession } = useSession();
|
||||
@@ -46,39 +47,15 @@ export default function Home() {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Check session
|
||||
const checkUser = async () => {
|
||||
try {
|
||||
// Proactively get session - this will trigger a refresh if needed
|
||||
const { data: { session }, error } = await supabase.auth.getSession();
|
||||
|
||||
if (session) {
|
||||
console.log('[Auth] Valid session found:', {
|
||||
userId: session.user.id,
|
||||
expiry: new Date(session.expires_at! * 1000).toLocaleString()
|
||||
});
|
||||
} else {
|
||||
console.log('[Auth] No active session found.');
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.error('[Auth] Session check error:', error);
|
||||
}
|
||||
|
||||
setUser(session?.user ?? null);
|
||||
if (session?.user) {
|
||||
fetchCollection();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[Auth] Unexpected error during session check:', err);
|
||||
setUser(null);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkUser();
|
||||
// Only fetch if auth is ready and user exists
|
||||
if (!isAuthLoading && user) {
|
||||
fetchCollection();
|
||||
} else if (!isAuthLoading && !user) {
|
||||
setBottles([]);
|
||||
}
|
||||
}, [user, isAuthLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch public splits if guest
|
||||
getActiveSplits().then(res => {
|
||||
if (res.success && res.splits) {
|
||||
@@ -86,33 +63,6 @@ export default function Home() {
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for visibility change (wake up from sleep)
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
console.log('[Auth] App became visible, refreshing session...');
|
||||
checkUser();
|
||||
}
|
||||
};
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
// Listen for auth changes
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((event: string, session: any) => {
|
||||
console.log('[Auth] State change event:', event, {
|
||||
hasSession: !!session,
|
||||
userId: session?.user?.id,
|
||||
email: session?.user?.email
|
||||
});
|
||||
|
||||
setUser(session?.user ?? null);
|
||||
if (session?.user) {
|
||||
if (event === 'SIGNED_IN' || event === 'INITIAL_SESSION' || event === 'TOKEN_REFRESHED') {
|
||||
fetchCollection();
|
||||
}
|
||||
} else {
|
||||
setBottles([]);
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for collection updates (e.g., after offline sync completes)
|
||||
const handleCollectionUpdated = () => {
|
||||
console.log('[Home] Collection update event received, refreshing...');
|
||||
@@ -121,14 +71,12 @@ export default function Home() {
|
||||
window.addEventListener('collection-updated', handleCollectionUpdated);
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
window.removeEventListener('collection-updated', handleCollectionUpdated);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const fetchCollection = async () => {
|
||||
setIsLoading(true);
|
||||
setIsInternalLoading(true);
|
||||
try {
|
||||
// Fetch bottles with their latest tasting date
|
||||
const { data, error } = await supabase
|
||||
@@ -194,7 +142,7 @@ export default function Home() {
|
||||
setFetchError(errorMessage);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsInternalLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -249,6 +197,8 @@ export default function Home() {
|
||||
);
|
||||
}
|
||||
|
||||
const isLoading = isAuthLoading || isInternalLoading;
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen flex-col items-center gap-6 md:gap-12 p-4 md:p-24 bg-[var(--background)] pb-32">
|
||||
<div className="z-10 max-w-5xl w-full flex flex-col items-center gap-12">
|
||||
|
||||
@@ -10,6 +10,7 @@ import { closeSession } from '@/services/close-session';
|
||||
import { useSession } from '@/context/SessionContext';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useI18n } from '@/i18n/I18nContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import SessionTimeline from '@/components/SessionTimeline';
|
||||
import SessionABVCurve from '@/components/SessionABVCurve';
|
||||
import OfflineIndicator from '@/components/OfflineIndicator';
|
||||
@@ -65,6 +66,7 @@ export default function SessionDetailPage() {
|
||||
const [tastings, setTastings] = useState<SessionTasting[]>([]);
|
||||
const [allBuddies, setAllBuddies] = useState<Buddy[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const { user, isLoading: isAuthLoading } = useAuth();
|
||||
const { activeSession, setActiveSession } = useSession();
|
||||
const [isAddingParticipant, setIsAddingParticipant] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
@@ -72,7 +74,9 @@ export default function SessionDetailPage() {
|
||||
const [isBulkScanOpen, setIsBulkScanOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSessionData();
|
||||
if (!isAuthLoading && user) {
|
||||
fetchSessionData();
|
||||
}
|
||||
|
||||
// Subscribe to bottle updates for realtime processing status
|
||||
const channel = supabase
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
|
||||
export default function AuthListener() {
|
||||
const supabase = createClient();
|
||||
|
||||
useEffect(() => {
|
||||
// Listener für Auth-Status Änderungen
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((event) => {
|
||||
if (event === 'SIGNED_OUT') {
|
||||
console.log(`[Auth] Event ${event} detected, forcing reload...`);
|
||||
// Zwinge den Browser zum kompletten Neuladen, um Caches zu leeren
|
||||
// Wir nutzen window.location.href statt router.push für einen harten Reload
|
||||
window.location.href = '/';
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [supabase]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
import { Users, UserPlus, Trash2, Loader2, ChevronDown, ChevronUp, Link2 } from 'lucide-react';
|
||||
import { useI18n } from '@/i18n/I18nContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { addBuddy, deleteBuddy } from '@/services/buddy';
|
||||
import BuddyHandshake from './BuddyHandshake';
|
||||
|
||||
@@ -27,10 +28,13 @@ export default function BuddyList() {
|
||||
return false;
|
||||
});
|
||||
const [isHandshakeOpen, setIsHandshakeOpen] = useState(false);
|
||||
const { user, isLoading: isAuthLoading } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
fetchBuddies();
|
||||
}, []);
|
||||
if (!isAuthLoading && user) {
|
||||
fetchBuddies();
|
||||
}
|
||||
}, [user, isAuthLoading]);
|
||||
|
||||
const fetchBuddies = async () => {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
|
||||
@@ -8,6 +8,7 @@ import AvatarStack from './AvatarStack';
|
||||
import { deleteSession } from '@/services/delete-session';
|
||||
import { useI18n } from '@/i18n/I18nContext';
|
||||
import { useSession } from '@/context/SessionContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
|
||||
interface Session {
|
||||
id: string;
|
||||
@@ -34,10 +35,13 @@ export default function SessionList() {
|
||||
});
|
||||
const [newName, setNewName] = useState('');
|
||||
const { activeSession, setActiveSession } = useSession();
|
||||
const { user, isLoading: isAuthLoading } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
fetchSessions();
|
||||
}, []);
|
||||
if (!isAuthLoading && user) {
|
||||
fetchSessions();
|
||||
}
|
||||
}, [user, isAuthLoading]);
|
||||
|
||||
const fetchSessions = async () => {
|
||||
const { data, error } = await supabase
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
import { useI18n } from '@/i18n/I18nContext';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useSession } from '@/context/SessionContext';
|
||||
import { getHostSplits, getParticipatingSplits } from '@/services/split-actions';
|
||||
import AvatarStack from './AvatarStack';
|
||||
@@ -49,6 +50,7 @@ export default function TastingHub({ isOpen, onClose }: TastingHubProps) {
|
||||
const { t, locale } = useI18n();
|
||||
const supabase = createClient();
|
||||
const { activeSession, setActiveSession } = useSession();
|
||||
const { user, isLoading: isAuthLoading } = useAuth();
|
||||
|
||||
const [activeTab, setActiveTab] = useState<'tastings' | 'splits'>('tastings');
|
||||
const [mySessions, setMySessions] = useState<Session[]>([]);
|
||||
@@ -62,10 +64,10 @@ export default function TastingHub({ isOpen, onClose }: TastingHubProps) {
|
||||
const [newName, setNewName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (isOpen && !isAuthLoading && user) {
|
||||
fetchAll();
|
||||
}
|
||||
}, [isOpen]);
|
||||
}, [isOpen, isAuthLoading, user]);
|
||||
|
||||
const fetchAll = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
@@ -7,8 +7,9 @@ import { createClient } from '@/lib/supabase/client';
|
||||
import { useI18n } from '@/i18n/I18nContext';
|
||||
import { useSession } from '@/context/SessionContext';
|
||||
import TagSelector from './TagSelector';
|
||||
import { useLiveQuery } from 'dexie-react-hooks';
|
||||
import { db } from '@/lib/db';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useLiveQuery } from 'dexie-react-hooks';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
import TastingFormBody from './TastingFormBody';
|
||||
|
||||
@@ -42,6 +43,7 @@ export default function TastingNoteForm({ bottleId, pendingBottleId, sessionId,
|
||||
const [suggestedTags, setSuggestedTags] = useState<string[]>([]);
|
||||
const [suggestedCustomTags, setSuggestedCustomTags] = useState<string[]>([]);
|
||||
const { activeSession } = useSession();
|
||||
const { user, isLoading: isAuthLoading } = useAuth();
|
||||
const [lastDramInSession, setLastDramInSession] = useState<{ name: string; isSmoky: boolean; timestamp: number } | null>(null);
|
||||
const [showPaletteWarning, setShowPaletteWarning] = useState(false);
|
||||
|
||||
@@ -56,16 +58,14 @@ export default function TastingNoteForm({ bottleId, pendingBottleId, sessionId,
|
||||
const effectiveSessionId = sessionId || activeSession?.id;
|
||||
|
||||
useEffect(() => {
|
||||
const getAuth = async () => {
|
||||
const { data: { user } } = await supabase.auth.getUser();
|
||||
if (user) setCurrentUserId(user.id);
|
||||
};
|
||||
getAuth();
|
||||
}, [supabase]);
|
||||
if (!isAuthLoading && user) {
|
||||
setCurrentUserId(user.id);
|
||||
}
|
||||
}, [user, isAuthLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
if (!bottleId) return;
|
||||
if (!bottleId || isAuthLoading || !user) return;
|
||||
|
||||
// Fetch Bottle Suggestions and Owner
|
||||
const { data: bottleData } = await supabase
|
||||
@@ -130,7 +130,7 @@ export default function TastingNoteForm({ bottleId, pendingBottleId, sessionId,
|
||||
}
|
||||
};
|
||||
fetchData();
|
||||
}, [supabase, effectiveSessionId, bottleId]);
|
||||
}, [supabase, effectiveSessionId, bottleId, user, isAuthLoading]);
|
||||
|
||||
// Live Palette Checker Logic
|
||||
useEffect(() => {
|
||||
|
||||
77
src/context/AuthContext.tsx
Normal file
77
src/context/AuthContext.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
'use client';
|
||||
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { Session, User } from '@supabase/supabase-js';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
|
||||
interface AuthContextType {
|
||||
user: User | null;
|
||||
session: Session | null;
|
||||
isLoading: boolean;
|
||||
signOut: () => Promise<void>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const supabase = createClient();
|
||||
|
||||
useEffect(() => {
|
||||
// Initial session check
|
||||
const initAuth = async () => {
|
||||
try {
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
setSession(session);
|
||||
setUser(session?.user ?? null);
|
||||
} catch (error) {
|
||||
console.error('[AuthContext] Error getting initial session:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
initAuth();
|
||||
|
||||
// Listen for auth changes (Magic Link, OAuth, Sign In/Out)
|
||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((event, currentSession) => {
|
||||
console.log(`[AuthContext] event: ${event}`, {
|
||||
userId: currentSession?.user?.id,
|
||||
email: currentSession?.user?.email
|
||||
});
|
||||
|
||||
setSession(currentSession);
|
||||
setUser(currentSession?.user ?? null);
|
||||
setIsLoading(false);
|
||||
|
||||
if (event === 'SIGNED_OUT') {
|
||||
// Hard reload to clear all state/cache on logout
|
||||
window.location.href = '/';
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, [supabase]);
|
||||
|
||||
const signOut = async () => {
|
||||
await supabase.auth.signOut();
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, session, isLoading, signOut }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -4,11 +4,13 @@ import { useState, useEffect, useCallback } from 'react';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
import { db, type CachedBottle, type CachedTasting } from '@/lib/db';
|
||||
import { useLiveQuery } from 'dexie-react-hooks';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
|
||||
export function useBottleData(bottleId: string) {
|
||||
const supabase = createClient();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { user, isLoading: isAuthLoading } = useAuth();
|
||||
|
||||
// Live queries from Dexie
|
||||
const cachedBottle = useLiveQuery(() => db.cache_bottles.get(bottleId), [bottleId]);
|
||||
@@ -18,8 +20,8 @@ export function useBottleData(bottleId: string) {
|
||||
);
|
||||
|
||||
const refreshData = useCallback(async () => {
|
||||
if (!navigator.onLine) {
|
||||
setLoading(false);
|
||||
if (!navigator.onLine || isAuthLoading || !user) {
|
||||
if (!isAuthLoading && !user) setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -70,7 +72,7 @@ export function useBottleData(bottleId: string) {
|
||||
|
||||
useEffect(() => {
|
||||
refreshData();
|
||||
}, [refreshData]);
|
||||
}, [refreshData, user, isAuthLoading]);
|
||||
|
||||
return {
|
||||
bottle: cachedBottle,
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
import { createBrowserClient } from '@supabase/ssr';
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
const globalForSupabase = globalThis as typeof globalThis & {
|
||||
supabaseBrowserClient?: SupabaseClient;
|
||||
};
|
||||
let supabaseClient: SupabaseClient | null = null;
|
||||
|
||||
export function createClient() {
|
||||
if (globalForSupabase.supabaseBrowserClient) {
|
||||
return globalForSupabase.supabaseBrowserClient;
|
||||
if (supabaseClient) return supabaseClient;
|
||||
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
||||
|
||||
if (!supabaseUrl || !supabaseAnonKey) {
|
||||
throw new Error('Supabase URL and Anon Key must be defined');
|
||||
}
|
||||
|
||||
globalForSupabase.supabaseBrowserClient = createBrowserClient(
|
||||
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
|
||||
);
|
||||
|
||||
return globalForSupabase.supabaseBrowserClient;
|
||||
supabaseClient = createBrowserClient(supabaseUrl, supabaseAnonKey);
|
||||
return supabaseClient;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user