fix(hackathon): remove Supabase dependencies completely

- 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 <noreply@anthropic.com>
This commit is contained in:
Maulana Sodiqin
2025-11-28 15:57:30 +07:00
co-authored by Claude
parent 9a97eb3e8e
commit c385aa1996
7 changed files with 106 additions and 153 deletions
@@ -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 (
<div className="flex justify-center items-center min-h-screen bg-gray-50 p-4">
<div className="bg-white w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200 text-center">
<div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-gray-950 p-4">
<div className="bg-white dark:bg-gray-900 w-full max-w-md p-8 rounded-2xl shadow-lg border border-gray-200 dark:border-gray-700 text-center">
<div className="mb-6">
<div className="mx-auto w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mb-4">
<div className="mx-auto w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center mb-4">
<span className="text-3xl"></span>
</div>
<h2 className="text-3xl font-bold text-gray-900 mb-2">
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
Check Your Email
</h2>
<p className="text-gray-600">
<p className="text-gray-600 dark:text-gray-400">
We've sent a password reset link to <strong>{email}</strong>
</p>
</div>
<div className="space-y-4">
<p className="text-sm text-gray-600">
<p className="text-sm text-gray-600 dark:text-gray-400">
Click the link in the email to reset your password. The link will
expire in 1 hour.
</p>
@@ -70,7 +59,7 @@ export default function ForgotPasswordPage() {
<button
onClick={() => setEmailSent(false)}
className="w-full py-3 text-gray-600 hover:text-gray-900 transition-colors"
className="w-full py-3 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors"
>
Send another email
</button>
@@ -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
/>
</div>
<button
type="submit"
disabled={isLoading}
disabled={forgotPasswordMutation.isPending}
className="w-full py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
{isLoading ? 'Sending...' : 'Send Reset Link'}
{forgotPasswordMutation.isPending ? 'Sending...' : 'Send Reset Link'}
</button>
</form>
</div>
@@ -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<string | null>(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 (
// <div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-gray-950">
// <div className="text-center">
// <div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mb-4"></div>
// <p className="text-gray-600 dark:text-gray-400">
// Verifying reset link...
// </p>
// </div>
// </div>
// );
// }
if (!accessToken) {
return (
<div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-gray-950">
<div className="text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mb-4"></div>
<p className="text-gray-600 dark:text-gray-400">
Verifying reset link...
</p>
</div>
</div>
);
}
return (
<div className="flex justify-center items-center min-h-screen bg-gray-50 dark:bg-gray-950 p-4">
@@ -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() {
<button
type="submit"
disabled={isLoading}
disabled={resetPasswordMutation.isPending}
className="w-full py-3 bg-primary-600 text-white rounded-lg font-semibold hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
>
{isLoading ? 'Updating...' : 'Update Password'}
{resetPasswordMutation.isPending ? 'Updating...' : 'Update Password'}
</button>
</form>
</div>
+14 -38
View File
@@ -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) {
+5 -14
View File
@@ -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<SidebarProps> = ({ 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[] = [
+27 -29
View File
@@ -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
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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');
};