fix auth redirects and API env naming

This commit is contained in:
kcar 2026-05-28 12:44:10 +01:00
parent 0db53bfd9c
commit 4b6600e01f
20 changed files with 137 additions and 32 deletions

View File

@ -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

View File

@ -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
# =============================================================================

View File

@ -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();
});
});

View File

@ -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 />} />

View File

@ -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
View 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}`);
}

View File

@ -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}
/>
</>
);
};

View File

@ -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;

View File

@ -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 ────────────────────────────────────────────────────────────────────

View File

@ -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 ────────────────────────────────────────────────────────────────────

View File

@ -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 ────────────────────────────────────────────────────────────────────

View File

@ -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'> = {

View File

@ -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 ────────────────────────────────────────────────────────────────────

View File

@ -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 ────────────────────────────────────────────────────────────────────

View File

@ -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;

View File

@ -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 ────────────────────────────────────────────────────────────────────

View File

@ -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>
);
};

View File

@ -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

View File

@ -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,

View File

@ -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) {