diff --git a/src/AppRoutes.tsx b/src/AppRoutes.tsx
index 0b2edda..10746fa 100644
--- a/src/AppRoutes.tsx
+++ b/src/AppRoutes.tsx
@@ -45,8 +45,6 @@ import {
} from './pages/timetable';
const FullContextRoutes: React.FC = () => {
- // Only block on Supabase profile being ready — Neo4j contexts initialize in the background.
- // Individual pages handle their own Neo4j loading states.
const { isInitialized: isUserInitialized } = useUser();
if (!isUserInitialized) {
@@ -68,6 +66,31 @@ const FullContextRoutes: React.FC = () => {
return ;
};
+const RequireAuth: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const { user, isAuthResolving } = useAuth();
+
+ if (isAuthResolving) {
+ return (
+
+
+
+ );
+ }
+
+ if (!user) {
+ return ;
+ }
+
+ return <>{children}>;
+};
+
const AppRoutes: React.FC = () => {
const { user, user_role, loading: isAuthLoading } = useAuth();
const location = useLocation();
@@ -136,7 +159,7 @@ const AppRoutes: React.FC = () => {
/>
{/* Authentication only routes - only render if all contexts are initialized */}
- {user && (
+
}>
{/* Timetable Module Routes */}
} />
@@ -167,7 +190,7 @@ const AppRoutes: React.FC = () => {
} />
} />
- )}
+
{/* Fallback route - authenticated users go to dashboard, unauthenticated see public 404 */}
: } />
diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx
index 971fe65..49002ab 100644
--- a/src/contexts/AuthContext.tsx
+++ b/src/contexts/AuthContext.tsx
@@ -18,6 +18,7 @@ export interface AuthContextType {
signOut: () => Promise;
clearError: () => void;
bootstrapData: BootstrapResponse | null;
+ isAuthResolving: boolean;
}
export const AuthContext = createContext({
@@ -29,7 +30,8 @@ export const AuthContext = createContext({
signIn: async () => {},
signOut: async () => {},
clearError: () => {},
- bootstrapData: null
+ bootstrapData: null,
+ isAuthResolving: false
});
export function AuthProvider({ children }: { children: React.ReactNode }) {
@@ -40,6 +42,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [bootstrapData, setBootstrapData] = useState(null);
+ const [isAuthResolving, setIsAuthResolving] = useState(false);
const persistSession = useCallback((session: Session | null) => {
@@ -92,6 +95,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
if (event === 'SIGNED_IN') {
persistSession(session ?? null);
+ setIsAuthResolving(!!session?.user);
if (session?.user) {
try {
const { user: resolvedUser, role } = await buildUserFromSupabase(session.user);
@@ -112,11 +116,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setAccessToken(null);
}
setLoading(false);
+ setIsAuthResolving(false);
return;
}
if (event === 'INITIAL_SESSION' || event === 'TOKEN_REFRESHED') {
persistSession(session ?? null);
+ setIsAuthResolving(!!session?.user);
if (session?.user) {
try {
const { user: resolvedUser, role } = await buildUserFromSupabase(session.user);
@@ -137,11 +143,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setAccessToken(null);
}
setLoading(false);
+ setIsAuthResolving(false);
return;
}
if (event === 'SIGNED_OUT') {
persistSession(null);
+ setIsAuthResolving(false);
setUser(null);
setUserRole(null);
setAccessToken(null);
@@ -214,7 +222,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
signIn,
signOut,
clearError,
- bootstrapData
+ bootstrapData,
+ isAuthResolving
}}
>
{children}
diff --git a/src/pages/auth/loginPage.tsx b/src/pages/auth/loginPage.tsx
index 4aa7a82..723544e 100644
--- a/src/pages/auth/loginPage.tsx
+++ b/src/pages/auth/loginPage.tsx
@@ -6,34 +6,50 @@ import { EmailLoginForm } from './EmailLoginForm';
import { EmailCredentials } from '../../services/auth/authService';
import { logger } from '../../debugConfig';
+const getLoginErrorMessage = (error: unknown): string => {
+ const status = (error as { status?: number } | null)?.status;
+ const code = (error as { code?: string } | null)?.code;
+
+ if (code === 'email_not_confirmed' || status === 401 && code === 'invalid_grant') {
+ return 'Please confirm your email before logging in';
+ }
+
+ if (status === 401 || code === 'invalid_grant') {
+ return 'Invalid email or password';
+ }
+
+ return error instanceof Error ? error.message : 'Login failed';
+};
+
const LoginPage: React.FC = () => {
const navigate = useNavigate();
- const { user, signIn } = useAuth();
+ const { user, signIn, isAuthResolving } = useAuth();
const [error, setError] = useState(null);
logger.debug('login-page', '🔍 Login page loaded', {
- hasUser: !!user
+ hasUser: !!user,
+ isAuthResolving
});
useEffect(() => {
- if (user) {
+ if (!isAuthResolving && user) {
navigate('/dashboard');
}
- }, [user, navigate]);
+ }, [user, isAuthResolving, navigate]);
const handleLogin = async (credentials: EmailCredentials) => {
try {
setError(null);
await signIn(credentials.email, credentials.password);
- navigate('/dashboard');
} catch (error) {
logger.error('login-page', '❌ Login failed', error);
- setError(error instanceof Error ? error.message : 'Login failed');
+ const message = getLoginErrorMessage(error);
+ setError(message);
throw error;
}
};
- if (user) {
+ if (user && !isAuthResolving) {
return null;
}