fix auth redirects and API env naming
This commit is contained in:
parent
0db53bfd9c
commit
4b6600e01f
@ -18,7 +18,6 @@ VITE_APP_HMR_URL=http://192.168.0.94:5173
|
||||
VITE_SUPABASE_URL=http://192.168.0.155:8000
|
||||
|
||||
# API should use localhost for local development
|
||||
VITE_API_URL=http://192.168.0.94:8000
|
||||
VITE_API_BASE=http://192.168.0.94:8000
|
||||
|
||||
# Neo4j
|
||||
|
||||
@ -14,7 +14,6 @@ VITE_APP_HMR_URL=http://localhost:24678
|
||||
# =============================================================================
|
||||
# API Configuration
|
||||
# =============================================================================
|
||||
VITE_API_URL=https://api.classroomcopilot.ai
|
||||
VITE_API_BASE=https://api.classroomcopilot.ai
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@ -121,3 +121,24 @@ describe('/admin route authorization', () => {
|
||||
expect(screen.getByText('Platform Admin Page')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('authenticated route redirects', () => {
|
||||
beforeEach(() => {
|
||||
mockUseAuth.mockReset();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['/calendar'],
|
||||
['/settings'],
|
||||
['/single-player'],
|
||||
['/lesson-plans'],
|
||||
])('redirects anonymous users from %s to login', (path) => {
|
||||
mockUseAuth.mockReturnValue(authState({ user: null, user_role: null }));
|
||||
|
||||
renderAt(path);
|
||||
|
||||
expect(screen.getByText('Login Page')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Public Not Found')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@ -11,7 +11,6 @@ import { CCExamMarker } from './pages/tldraw/CCExamMarker/CCExamMarker';
|
||||
import CalendarPage from './pages/user/calendarPage';
|
||||
import SettingsPage from './pages/user/settingsPage';
|
||||
import TLDrawCanvas from './pages/tldraw/TLDrawCanvas';
|
||||
import AdminDashboard from './pages/auth/adminPage';
|
||||
import PlatformAdminPage from './pages/auth/PlatformAdminPage';
|
||||
import TLDrawDevPage from './pages/tldraw/devPlayerPage';
|
||||
import DevPage from './pages/tldraw/devPage';
|
||||
@ -44,6 +43,17 @@ import {
|
||||
LessonPlanDetailPage,
|
||||
} from './pages/timetable';
|
||||
|
||||
const ProtectedRoutes: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
const location = useLocation();
|
||||
|
||||
if (!user) {
|
||||
return <Navigate to="/login" replace state={{ from: location }} />;
|
||||
}
|
||||
|
||||
return <Outlet />;
|
||||
};
|
||||
|
||||
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.
|
||||
@ -118,7 +128,7 @@ const AppRoutes: React.FC = () => {
|
||||
{/* Lightweight authenticated routes */}
|
||||
<Route
|
||||
path="/dashboard"
|
||||
element={user ? <DashboardPage /> : <Navigate to="/login" replace />}
|
||||
element={user ? <DashboardPage /> : <Navigate to="/login" replace state={{ from: location }} />}
|
||||
/>
|
||||
|
||||
{/* Super Admin only routes */}
|
||||
@ -135,8 +145,8 @@ const AppRoutes: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Authentication only routes - only render if all contexts are initialized */}
|
||||
{user && (
|
||||
{/* Authentication only routes */}
|
||||
<Route element={<ProtectedRoutes />}>
|
||||
<Route element={<FullContextRoutes />}>
|
||||
{/* Timetable Module Routes */}
|
||||
<Route path="/timetable" element={<TimetablePage />} />
|
||||
@ -167,7 +177,7 @@ const AppRoutes: React.FC = () => {
|
||||
<Route path="/calendar" element={<CalendarPage />} />
|
||||
<Route path="/settings" element={<SettingsPage />} />
|
||||
</Route>
|
||||
)}
|
||||
</Route>
|
||||
|
||||
{/* Fallback route - use different NotFound pages based on auth state */}
|
||||
<Route path="*" element={user ? <NotFound /> : <NotFoundPublic />} />
|
||||
|
||||
@ -1,12 +1,8 @@
|
||||
import axios from 'axios';
|
||||
import { logger } from './debugConfig';
|
||||
import { API_BASE } from './config/api';
|
||||
|
||||
// Use development backend URL if no custom URL is provided
|
||||
const baseURL = import.meta.env.VITE_API_BASE || 'http://localhost:8080';
|
||||
|
||||
if (!import.meta.env.VITE_API_BASE) {
|
||||
logger.warn('axios', '⚠️ VITE_API_BASE not set, defaulting to http://localhost:8080');
|
||||
}
|
||||
const baseURL = API_BASE;
|
||||
|
||||
const instance = axios.create({
|
||||
baseURL,
|
||||
|
||||
14
src/config/api.ts
Normal file
14
src/config/api.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { logger } from '../debugConfig';
|
||||
|
||||
const DEFAULT_API_BASE = 'http://localhost:8080';
|
||||
|
||||
const normalizeApiBase = (value: string | undefined): string => {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed ? trimmed.replace(/\/$/, '') : DEFAULT_API_BASE;
|
||||
};
|
||||
|
||||
export const API_BASE = normalizeApiBase(import.meta.env.VITE_API_BASE);
|
||||
|
||||
if (!import.meta.env.VITE_API_BASE) {
|
||||
logger.warn('api-config', `⚠️ VITE_API_BASE not set, defaulting to ${DEFAULT_API_BASE}`);
|
||||
}
|
||||
@ -34,11 +34,12 @@ import Lessons from '@mui/icons-material/EventNote';
|
||||
import LessonPlans from '@mui/icons-material/LibraryBooks';
|
||||
import People from '@mui/icons-material/People';
|
||||
import SchoolSettings from '@mui/icons-material/Tune';
|
||||
import AddBusiness from '@mui/icons-material/AddBusiness';
|
||||
import { HEADER_HEIGHT } from './Layout';
|
||||
import { logger } from '../debugConfig';
|
||||
import { GraphNavigator } from '../components/navigation/GraphNavigator';
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
||||
import { SchoolOnboardingWizard } from '../utils/tldraw/ui-overrides/components/shared/navigation/SchoolOnboardingWizard';
|
||||
import { API_BASE } from '../config/api';
|
||||
|
||||
const Header: React.FC = () => {
|
||||
const theme = useTheme();
|
||||
@ -48,6 +49,8 @@ const Header: React.FC = () => {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [isPlatformAdmin, setIsPlatformAdmin] = useState(false);
|
||||
const [schoolRole, setSchoolRole] = useState<string | null>(null);
|
||||
const [schoolStatus, setSchoolStatus] = useState<string | null>(null);
|
||||
const [onboardingOpen, setOnboardingOpen] = useState(false);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(!!user);
|
||||
const showGraphNavigation = location.pathname === '/single-player';
|
||||
const isSchoolAdmin = schoolRole === 'school_admin' || schoolRole === 'department_head';
|
||||
@ -76,15 +79,17 @@ const Header: React.FC = () => {
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setSchoolRole(data.user_role || null);
|
||||
setSchoolStatus(data.status || null);
|
||||
}
|
||||
} catch {
|
||||
setSchoolRole(null);
|
||||
setSchoolStatus(null);
|
||||
}
|
||||
}, [accessToken]);
|
||||
|
||||
useEffect(() => {
|
||||
if (accessToken) checkAdminStatus();
|
||||
else { setIsPlatformAdmin(false); setSchoolRole(null); }
|
||||
else { setIsPlatformAdmin(false); setSchoolRole(null); setSchoolStatus(null); }
|
||||
}, [accessToken, checkAdminStatus]);
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
|
||||
@ -100,6 +105,16 @@ const Header: React.FC = () => {
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleOnboardingOpen = () => {
|
||||
setOnboardingOpen(true);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
const handleOnboardingComplete = () => {
|
||||
setOnboardingOpen(false);
|
||||
void checkAdminStatus();
|
||||
};
|
||||
|
||||
const handleSignupNavigation = (role: 'teacher' | 'student') => {
|
||||
navigate('/signup', { state: { role } });
|
||||
handleMenuClose();
|
||||
@ -119,6 +134,7 @@ const Header: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppBar
|
||||
position="fixed"
|
||||
sx={{
|
||||
@ -262,6 +278,15 @@ const Header: React.FC = () => {
|
||||
secondary="Plan and manage lessons"
|
||||
/>
|
||||
</MenuItem>,
|
||||
<MenuItem key="school-onboarding" onClick={handleOnboardingOpen}>
|
||||
<ListItemIcon>
|
||||
<AddBusiness />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={schoolStatus === 'no_school' ? 'Set up your school' : 'School setup'}
|
||||
secondary="Find your school and create a calendar"
|
||||
/>
|
||||
</MenuItem>,
|
||||
<Divider key="work-divider" />,
|
||||
|
||||
// School Admin Section
|
||||
@ -357,6 +382,13 @@ const Header: React.FC = () => {
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<SchoolOnboardingWizard
|
||||
open={onboardingOpen}
|
||||
onClose={() => setOnboardingOpen(false)}
|
||||
onComplete={handleOnboardingComplete}
|
||||
apiBase={API_BASE}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import { Refresh, School, People, EventNote, HourglassEmpty } from '@mui/icons-m
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
||||
import { API_BASE } from '../../config/api';
|
||||
|
||||
interface SchoolEntry {
|
||||
id: string;
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
} from '@mui/icons-material';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
||||
import { API_BASE } from '../../config/api';
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
} from '@mui/icons-material';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
||||
import { API_BASE } from '../../config/api';
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
import { Add, Search, LibraryBooks, Edit, Delete, FilterList } from '@mui/icons-material';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
||||
import { API_BASE } from '../../config/api';
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
||||
import { API_BASE } from '../../config/api';
|
||||
|
||||
const DAY_TYPE_OPTIONS = ['Academic', 'Holiday', 'Staff', 'OffTimetable'];
|
||||
const DAY_TYPE_COLORS: Record<string, 'default' | 'primary' | 'success' | 'warning' | 'error'> = {
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
} from '@mui/icons-material';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
||||
import { API_BASE } from '../../config/api';
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
import { ChevronLeft, ChevronRight, Today } from '@mui/icons-material';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
||||
import { API_BASE } from '../../config/api';
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
} from '@mui/icons-material';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
||||
import { API_BASE } from '../../config/api';
|
||||
|
||||
interface Student {
|
||||
profile_id: string;
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
} from '@mui/icons-material';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
|
||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
||||
import { API_BASE } from '../../config/api';
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Box,
|
||||
@ -6,16 +6,21 @@ import {
|
||||
Container,
|
||||
Grid,
|
||||
Paper,
|
||||
Alert,
|
||||
Stack,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useUser } from '../../contexts/UserContext';
|
||||
import { API_BASE } from '../../config/api';
|
||||
import { SchoolOnboardingWizard } from '../../utils/tldraw/ui-overrides/components/shared/navigation/SchoolOnboardingWizard';
|
||||
|
||||
const DashboardPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user: authUser } = useAuth();
|
||||
const { profile, loading } = useUser();
|
||||
const [schoolOnboardingOpen, setSchoolOnboardingOpen] = useState(false);
|
||||
const [schoolSetupComplete, setSchoolSetupComplete] = useState(false);
|
||||
|
||||
const displayName = profile?.display_name || authUser?.display_name || authUser?.username || 'Member';
|
||||
const emailAddress = profile?.email || authUser?.email || '';
|
||||
@ -87,8 +92,39 @@ const DashboardPage: React.FC = () => {
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Paper elevation={2} sx={{ p: 3 }}>
|
||||
<Stack spacing={2} direction={{ xs: 'column', md: 'row' }} alignItems={{ md: 'center' }} justifyContent="space-between">
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
School onboarding
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ maxWidth: 680 }}>
|
||||
Link your account to a school, confirm school details, and set up an academic calendar without opening the canvas navigation panel first.
|
||||
</Typography>
|
||||
{schoolSetupComplete && (
|
||||
<Alert severity="success" sx={{ mt: 2 }}>
|
||||
School setup completed. Refreshing navigation will pick up the new school context.
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
<Button variant="contained" color="secondary" onClick={() => setSchoolOnboardingOpen(true)}>
|
||||
Set up school
|
||||
</Button>
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Stack>
|
||||
<SchoolOnboardingWizard
|
||||
open={schoolOnboardingOpen}
|
||||
onClose={() => setSchoolOnboardingOpen(false)}
|
||||
onComplete={() => {
|
||||
setSchoolOnboardingOpen(false);
|
||||
setSchoolSetupComplete(true);
|
||||
}}
|
||||
apiBase={API_BASE}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
@ -15,7 +15,7 @@ import {
|
||||
} from '../types/timetable.types';
|
||||
|
||||
// API base URL
|
||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
||||
import { API_BASE } from '../config/api';
|
||||
|
||||
// ============================================================================
|
||||
// Helper: Get auth headers
|
||||
|
||||
@ -4,7 +4,6 @@ import { RegularToolbar } from './regular/toolbar';
|
||||
import { RegularHelperButtons } from './regular/helperButton';
|
||||
// import { RegularHelpMenu } from './regular/helpMenu';
|
||||
// import { RegularMainMenu } from './regular/mainMenu';
|
||||
import { RegularNavigationPanel } from './regular/navigationPanel';
|
||||
// import { RegularPageMenu } from './regular/pageMenu';
|
||||
// import { RegularQuickActions } from './regular/quickActions';
|
||||
import { RegularStylePanel } from './regular/stylePanel';
|
||||
@ -18,7 +17,6 @@ import { PresentationToolbar } from './presentation/toolbar';
|
||||
import { PresentationHelperButtons } from './presentation/helperButton';
|
||||
// import { PresentationHelpMenu } from './presentation/helpMenu';
|
||||
// import { PresentationMainMenu } from './presentation/mainMenu';
|
||||
import { PresentationNavigationPanel } from './presentation/navigationPanel';
|
||||
// import { PresentationPageMenu } from './presentation/pageMenu';
|
||||
import { PresentationQuickActions } from './presentation/quickActions';
|
||||
import { PresentationStylePanel } from './presentation/stylePanel';
|
||||
@ -35,7 +33,7 @@ export const regularComponentsIndex: TLComponents = {
|
||||
HelperButtons: RegularHelperButtons,
|
||||
// HelpMenu: RegularHelpMenu,
|
||||
// MainMenu: RegularMainMenu,
|
||||
NavigationPanel: RegularNavigationPanel,
|
||||
// NavigationPanel intentionally omitted; CCPanel/CCGraphNavPanel is the supported navigation surface.
|
||||
// PageMenu: RegularPageMenu,
|
||||
// QuickActions: RegularQuickActions,
|
||||
StylePanel: RegularStylePanel,
|
||||
@ -52,7 +50,7 @@ export const presentationComponentsIndex: TLComponents = {
|
||||
HelperButtons: PresentationHelperButtons,
|
||||
// HelpMenu: PresentationHelpMenu,
|
||||
// MainMenu: PresentationMainMenu,
|
||||
NavigationPanel: PresentationNavigationPanel,
|
||||
// NavigationPanel intentionally omitted; CCPanel/CCGraphNavPanel is the supported navigation surface.
|
||||
// PageMenu: PresentationPageMenu,
|
||||
QuickActions: PresentationQuickActions,
|
||||
StylePanel: PresentationStylePanel,
|
||||
|
||||
@ -119,7 +119,7 @@ export const CCTranscriptionPanel: React.FC = () => {
|
||||
useEffect(() => {
|
||||
const detectTimetable = async () => {
|
||||
try {
|
||||
const apiBase = import.meta.env.VITE_API_URL || 'https://api.classroomcopilot.ai';
|
||||
const apiBase = API_BASE;
|
||||
const response = await fetch(`${apiBase}/database/timetables/current-period`);
|
||||
const data = await response.json();
|
||||
if (data.period_id) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user