From c385aa1996bbe7009d75f0d39eec82b3ab8980e1 Mon Sep 17 00:00:00 2001 From: Maulana Sodiqin Date: Fri, 28 Nov 2025 15:57:30 +0700 Subject: [PATCH] fix(hackathon): remove Supabase dependencies completely MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove Supabase export from service library - Update forgot-password and reset-password pages to use backend API - Update layout.tsx to use useAuthStore instead of Supabase session - Update sidebar logout to not use Supabase - Update middleware to use SessionUser instead of Supabase - Update use-session hook to remove Supabase dependency 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/app/auth/forgot-password/page.tsx | 39 +++------ .../src/app/auth/reset-password/page.tsx | 85 +++++++++---------- apps/hackathon/src/app/layout.tsx | 52 +++--------- apps/hackathon/src/components/sidebar.tsx | 19 ++--- apps/hackathon/src/middleware.ts | 56 ++++++------ libs/service/src/index.ts | 2 +- libs/utils/src/hooks/use-session.ts | 6 +- 7 files changed, 106 insertions(+), 153 deletions(-) diff --git a/apps/hackathon/src/app/auth/forgot-password/page.tsx b/apps/hackathon/src/app/auth/forgot-password/page.tsx index b28aacc..5a6c4d3 100644 --- a/apps/hackathon/src/app/auth/forgot-password/page.tsx +++ b/apps/hackathon/src/app/auth/forgot-password/page.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { supabase } from '@imphnen-frontend-service/service'; +import { useForgotPassword } from '@imphnen-frontend-service/service'; import { Link, useNavigate } from 'react-router'; import { toast } from 'sonner'; import { Icon } from '@iconify/react'; @@ -7,9 +7,9 @@ import ThemeToggle from '../../../components/theme-toggle'; export default function ForgotPasswordPage() { const [email, setEmail] = useState(''); - const [isLoading, setIsLoading] = useState(false); const [emailSent, setEmailSent] = useState(false); const navigate = useNavigate(); + const forgotPasswordMutation = useForgotPassword(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -20,44 +20,33 @@ export default function ForgotPasswordPage() { } try { - setIsLoading(true); - - const { error } = await supabase.auth.resetPasswordForEmail(email, { - redirectTo: `${window.location.origin}/auth/reset-password`, - }); - - if (error) { - throw error; - } + await forgotPasswordMutation.mutateAsync({ email }); setEmailSent(true); toast.success('Password reset email sent! Check your inbox.'); } catch (err) { - // console.error('Failed to send reset email:', err); toast.error((err as Error).message || 'Failed to send reset email'); - } finally { - setIsLoading(false); } }; if (emailSent) { return ( -
-
+
+
-
+
✓
-

+

Check Your Email

-

+

We've sent a password reset link to {email}

-

+

Click the link in the email to reset your password. The link will expire in 1 hour.

@@ -70,7 +59,7 @@ export default function ForgotPasswordPage() { @@ -116,18 +105,18 @@ export default function ForgotPasswordPage() { value={email} onChange={(e) => setEmail(e.target.value)} placeholder="your@email.com" - disabled={isLoading} - className="bg-white dark:bg-gray-800 w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed" + disabled={forgotPasswordMutation.isPending} + className="bg-white dark:bg-gray-800 text-gray-900 dark:text-white w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:cursor-not-allowed" required />
diff --git a/apps/hackathon/src/app/auth/reset-password/page.tsx b/apps/hackathon/src/app/auth/reset-password/page.tsx index 905917b..fd88412 100644 --- a/apps/hackathon/src/app/auth/reset-password/page.tsx +++ b/apps/hackathon/src/app/auth/reset-password/page.tsx @@ -1,26 +1,28 @@ import { useState, useEffect } from 'react'; -import { supabase } from '@imphnen-frontend-service/service'; +import { useResetPassword, useAuthStore } from '@imphnen-frontend-service/service'; import { useNavigate } from 'react-router'; import { toast } from 'sonner'; export default function ResetPasswordPage() { const navigate = useNavigate(); + const { clearSession } = useAuthStore(); + const resetPasswordMutation = useResetPassword(); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); - const [isLoading, setIsLoading] = useState(false); - const [isValidToken, setIsValidToken] = useState(false); + const [accessToken, setAccessToken] = useState(null); - // useEffect(() => { - // // Check if we have a valid session (from the reset link) - // supabase.auth.getSession().then(({ data: { session } }) => { - // if (session) { - // setIsValidToken(true); - // } else { - // toast.error('Invalid or expired reset link'); - // setTimeout(() => navigate('/auth/forgot-password'), 2000); - // } - // }); - // }, [navigate]); + useEffect(() => { + // Get the access_token from URL hash (Supabase sends it as hash fragment) + const hashParams = new URLSearchParams(globalThis.location.hash.substring(1)); + const token = hashParams.get('access_token'); + + if (token) { + setAccessToken(token); + } else { + toast.error('Invalid or expired reset link'); + setTimeout(() => navigate('/auth/forgot-password'), 2000); + } + }, [navigate]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -35,42 +37,39 @@ export default function ResetPasswordPage() { return; } + if (!accessToken) { + toast.error('Invalid reset token'); + return; + } + try { - setIsLoading(true); - - const { error } = await supabase.auth.updateUser({ - password: password, + await resetPasswordMutation.mutateAsync({ + access_token: accessToken, + new_password: password, }); - if (error) { - throw error; - } - toast.success('Password updated successfully!'); - // Sign out and redirect to login - await supabase.auth.signOut(); + // Clear session and redirect to login + clearSession(); navigate('/auth/login'); } catch (err) { - // console.error('Failed to reset password:', err); toast.error((err as Error).message || 'Failed to reset password'); - } finally { - setIsLoading(false); } }; - // if (!isValidToken) { - // return ( - //
- //
- //
- //

- // Verifying reset link... - //

- //
- //
- // ); - // } + if (!accessToken) { + return ( +
+
+
+

+ Verifying reset link... +

+
+
+ ); + } return (
@@ -98,7 +97,7 @@ export default function ResetPasswordPage() { value={password} onChange={(e) => setPassword(e.target.value)} placeholder="••••••••" - disabled={isLoading} + disabled={resetPasswordMutation.isPending} className="w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed bg-white dark:bg-gray-800 text-gray-900 dark:text-white" required minLength={6} @@ -118,7 +117,7 @@ export default function ResetPasswordPage() { value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} placeholder="••••••••" - disabled={isLoading} + disabled={resetPasswordMutation.isPending} className="w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-transparent disabled:bg-gray-100 disabled:cursor-not-allowed bg-white dark:bg-gray-800 text-gray-900 dark:text-white" required minLength={6} @@ -127,10 +126,10 @@ export default function ResetPasswordPage() {
diff --git a/apps/hackathon/src/app/layout.tsx b/apps/hackathon/src/app/layout.tsx index c17b781..d6af127 100644 --- a/apps/hackathon/src/app/layout.tsx +++ b/apps/hackathon/src/app/layout.tsx @@ -5,7 +5,7 @@ import { useNavigate, } from 'react-router-dom'; import { useEffect, useState } from 'react'; -import { supabase } from '@imphnen-frontend-service/service'; +import { useAuthStore, useUserMe } from '@imphnen-frontend-service/service'; // Define onboarding routes const ONBOARDING_ROUTES = new Set(['/onboarding/user']); @@ -13,6 +13,8 @@ const ONBOARDING_ROUTES = new Set(['/onboarding/user']); export default function RootLayout() { const location = useLocation(); const navigate = useNavigate(); + const { session } = useAuthStore(); + const { data: userData, isLoading: isUserLoading } = useUserMe(); const [isChecking, setIsChecking] = useState(true); useEffect(() => { @@ -31,22 +33,8 @@ export default function RootLayout() { return; } - // Check Supabase session - const { - data: { session }, - error, - } = await supabase.auth.getSession(); - - if (error) { - console.error('[Layout] Session error:', error); - } - // Public auth pages (login, signup, forgot-password, reset-password) - allow unauthenticated access const isPublicAuthPage = pathname === '/maintenance'; - // pathname === '/auth/login' || - // pathname === '/auth/signup' || - // pathname === '/auth/forgot-password' || - // pathname === '/auth/reset-password'; if (isPublicAuthPage) { // If already authenticated and not on password reset pages, redirect to dashboard @@ -69,35 +57,23 @@ export default function RootLayout() { // Require authentication for all other routes if (!session) { navigate('/maintenance', { replace: true }); - // navigate('/auth/login', { replace: true }); setIsChecking(false); return; } + // Wait for user data to load before checking onboarding + if (isUserLoading) { + return; + } + // Check if user has completed onboarding (skip for onboarding routes) if (!ONBOARDING_ROUTES.has(pathname)) { - try { - const { data: userData, error: userError } = await supabase - .from('users') - .select('location') - .eq('id', session.user.id) - .single(); + const hasLocation = !!userData?.data?.location || !!session?.user?.location; - if (userError) { - console.error('[Layout] Failed to fetch user data:', userError); - setIsChecking(false); - return; - } - - const hasLocation = !!userData?.location; - - if (!hasLocation) { - navigate('/onboarding/user', { replace: true }); - setIsChecking(false); - return; - } - } catch (error) { - // Silently handle error + if (!hasLocation) { + navigate('/onboarding/user', { replace: true }); + setIsChecking(false); + return; } } @@ -105,7 +81,7 @@ export default function RootLayout() { }; checkAuth(); - }, [location.pathname, navigate]); + }, [location.pathname, navigate, session, userData, isUserLoading]); // Show loading state while checking auth if (isChecking) { diff --git a/apps/hackathon/src/components/sidebar.tsx b/apps/hackathon/src/components/sidebar.tsx index 7b0eff1..34fe193 100644 --- a/apps/hackathon/src/components/sidebar.tsx +++ b/apps/hackathon/src/components/sidebar.tsx @@ -3,7 +3,6 @@ import { Link, useLocation } from 'react-router'; import { useMyTeams, useAuthStore, - supabase, } from '@imphnen-frontend-service/service'; import { useNavigate } from 'react-router'; import { toast } from 'sonner'; @@ -40,19 +39,11 @@ export const Sidebar: FC = ({ isOpen = true, onClose }) => { } }, [location.pathname]); - const handleLogout = async () => { - try { - await supabase.auth.signOut(); - clearSession(); - localStorage.clear(); - toast.success('Logged out successfully'); - navigate('/auth/login'); - } catch (error) { - console.error('Logout error:', error); - clearSession(); - localStorage.clear(); - navigate('/auth/login'); - } + const handleLogout = () => { + clearSession(); + localStorage.clear(); + toast.success('Logged out successfully'); + navigate('/auth/login'); }; const navItems: NavItem[] = [ diff --git a/apps/hackathon/src/middleware.ts b/apps/hackathon/src/middleware.ts index d5001ae..010770d 100644 --- a/apps/hackathon/src/middleware.ts +++ b/apps/hackathon/src/middleware.ts @@ -1,5 +1,5 @@ import { SessionUser } from '@imphnen-frontend-service/utils'; -import { supabase } from '@imphnen-frontend-service/service'; +import { hackathonApi } from '@imphnen-frontend-service/service'; import { LoaderFunctionArgs, redirect } from 'react-router'; const mappingPublicRoutes = [ @@ -37,14 +37,9 @@ export const middleware = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); const pathname = url.pathname; - // Get session from Supabase (authoritative source) - const { data: { session: supabaseSession }, error: sessionError } = await supabase.auth.getSession(); - - // Handle session errors - if (sessionError) { - console.error('[Middleware] Session error:', sessionError); - // Don't redirect on session errors, let the app handle it - } + // Get session from local storage (via SessionUser) + const session = SessionUser.get(); + const isAuthenticated = !!session?.token?.access_token; // Allow to access the hackathon pages without authentication if (mappingPublicPrefixRoutes.some((prefix) => pathname.startsWith(prefix))) { @@ -63,20 +58,24 @@ export const middleware = async ({ request }: LoaderFunctionArgs) => { // Auth routes (all /auth/* paths) - redirect to dashboard if already authenticated if (pathname.startsWith('/auth')) { - if (supabaseSession) return redirect('/dashboard'); + if (isAuthenticated) return redirect('/dashboard'); return null; } - // Require authentication for all other routes - ONLY check Supabase session - if (!supabaseSession) { + // Require authentication for all other routes + if (!isAuthenticated) { return redirect('/auth/login'); } - // Check if user has completed onboarding by querying database (not localStorage!) + // Check if user has completed onboarding // Skip onboarding check for onboarding routes themselves if (!mappingOnboardingRoutes.includes(pathname)) { try { - const userId = supabaseSession.user.id; + const userId = session?.user?.id; + if (!userId) { + return redirect('/auth/login'); + } + const now = Date.now(); // Check cache first @@ -86,20 +85,20 @@ export const middleware = async ({ request }: LoaderFunctionArgs) => { if (cached && (now - cached.timestamp) < CACHE_DURATION) { hasLocation = cached.hasLocation; } else { - const { data: userData, error: userError } = await supabase - .from('users') - .select('location') - .eq('id', userId) - .single(); - - if (userError) { - console.error('[Middleware] Failed to fetch user data:', userError); - // If we can't fetch user data, allow access (don't break the app) - return null; + // First check session data (faster) + if (session?.user?.location) { + hasLocation = true; + } else { + // Fetch from backend API + try { + const response = await hackathonApi.get('/users/me'); + hasLocation = !!response.data?.data?.location; + } catch { + // If API fails, check session data as fallback + hasLocation = !!session?.user?.location; + } } - hasLocation = !!userData?.location; - // Update cache onboardingCache.set(userId, { hasLocation, timestamp: now }); } @@ -114,10 +113,9 @@ export const middleware = async ({ request }: LoaderFunctionArgs) => { } } - // Check route permissions using fresh user data from Zustand (for UI metadata) - const session = SessionUser.get(); + // Check route permissions using user data from session const userPermissions = - session?.role?.permissions?.map?.((perm) => perm?.name) ?? []; + session?.user?.role?.permissions?.map?.((perm) => perm?.name) ?? []; const matchedRoute = mappingRoutePermissions.find( (route) => route.path === pathname diff --git a/libs/service/src/index.ts b/libs/service/src/index.ts index 981a008..d0aa0f5 100644 --- a/libs/service/src/index.ts +++ b/libs/service/src/index.ts @@ -2,5 +2,5 @@ export * from './api'; export * from './hooks'; export * from './types'; export * from './schemas'; -export * from './supabase'; export * from './storage'; +// Note: Supabase export removed - using backend API instead diff --git a/libs/utils/src/hooks/use-session.ts b/libs/utils/src/hooks/use-session.ts index 86bdc75..c2c5176 100644 --- a/libs/utils/src/hooks/use-session.ts +++ b/libs/utils/src/hooks/use-session.ts @@ -1,4 +1,4 @@ -import { supabase, useAuthStore } from '@imphnen-frontend-service/service'; +import { useAuthStore } from '@imphnen-frontend-service/service'; import { useNavigate } from 'react-router'; export const useSession = () => { @@ -6,9 +6,9 @@ export const useSession = () => { const { clearSession, session, status } = useAuthStore(); const isAuthenticated = status === 'authenticated'; - const signOut = async () => { - await supabase.auth.signOut(); + const signOut = () => { clearSession(); + localStorage.clear(); navigate('/auth/login'); };