Initial commit
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export default function AuthCallback() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
// Redirect to login page since we're no longer supporting external authentication
|
||||
navigate('/login');
|
||||
}, [navigate]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-gray-50">
|
||||
<div className="w-full max-w-md space-y-8 rounded-lg bg-white p-6 shadow-lg">
|
||||
<div className="text-center">
|
||||
<h2 className="text-3xl font-bold text-gray-900">Redirecting...</h2>
|
||||
<p className="mt-2 text-sm text-gray-600">Please wait while we redirect you to the login page...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import React, { useState } from 'react';
|
||||
import { TextField, Button, Box, Alert } from '@mui/material';
|
||||
import { EmailCredentials } from '../../services/auth/authService';
|
||||
|
||||
interface EmailLoginFormProps {
|
||||
role: 'email_teacher' | 'email_student';
|
||||
onSubmit: (credentials: EmailCredentials) => Promise<void>;
|
||||
}
|
||||
|
||||
export const EmailLoginForm: React.FC<EmailLoginFormProps> = ({ role, onSubmit }) => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await onSubmit({ email, password, role });
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to login');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box component="form" onSubmit={handleSubmit} sx={{ width: '100%' }}>
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
margin="normal"
|
||||
required
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
margin="normal"
|
||||
required
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={isLoading}
|
||||
sx={{ mt: 3 }}
|
||||
>
|
||||
{isLoading ? 'Logging in...' : 'Login'}
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,139 @@
|
||||
import React, { useState } from 'react';
|
||||
import { TextField, Button, Box, Alert, Stack } from '@mui/material';
|
||||
import { EmailCredentials } from '../../services/auth/authService';
|
||||
import { logger } from '../../debugConfig';
|
||||
|
||||
interface EmailSignupFormProps {
|
||||
role: 'email_teacher' | 'email_student';
|
||||
onSubmit: (
|
||||
credentials: EmailCredentials,
|
||||
displayName: string
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export const EmailSignupForm: React.FC<EmailSignupFormProps> = ({
|
||||
role,
|
||||
onSubmit,
|
||||
}) => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const validateForm = () => {
|
||||
if (!email || !password || !confirmPassword || !displayName) {
|
||||
return 'All fields are required';
|
||||
}
|
||||
if (password !== confirmPassword) {
|
||||
return 'Passwords do not match';
|
||||
}
|
||||
if (password.length < 6) {
|
||||
return 'Password must be at least 6 characters';
|
||||
}
|
||||
if (!email.includes('@')) {
|
||||
return 'Please enter a valid email address';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const validationError = validateForm();
|
||||
if (validationError) {
|
||||
setError(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
logger.debug('email-signup-form', '🔄 Submitting signup form', {
|
||||
email,
|
||||
role,
|
||||
hasDisplayName: !!displayName,
|
||||
});
|
||||
|
||||
await onSubmit({ email, password, role }, displayName);
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error ? err.message : 'An error occurred during signup'
|
||||
);
|
||||
logger.error(
|
||||
'email-signup-form',
|
||||
'❌ Signup form submission failed',
|
||||
err
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box component="form" onSubmit={handleSubmit} noValidate>
|
||||
<Stack spacing={2}>
|
||||
<TextField
|
||||
required
|
||||
fullWidth
|
||||
id="displayName"
|
||||
label="Display Name"
|
||||
name="displayName"
|
||||
autoComplete="name"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<TextField
|
||||
required
|
||||
fullWidth
|
||||
id="email"
|
||||
label="Email Address"
|
||||
name="email"
|
||||
autoComplete="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
required
|
||||
fullWidth
|
||||
name="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
id="password"
|
||||
autoComplete="new-password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
required
|
||||
fullWidth
|
||||
name="confirmPassword"
|
||||
label="Confirm Password"
|
||||
type="password"
|
||||
id="confirmPassword"
|
||||
autoComplete="new-password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
/>
|
||||
|
||||
{error && <Alert severity="error">{error}</Alert>}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
fullWidth
|
||||
variant="contained"
|
||||
disabled={
|
||||
isLoading || !email || !password || !confirmPassword || !displayName
|
||||
}
|
||||
>
|
||||
{isLoading ? 'Signing up...' : 'Sign Up'}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Container, Box, Typography, Tabs, Tab, Paper, Button } from '@mui/material';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { SchoolUploadSection } from '../components/admin/SchoolUploadSection';
|
||||
import { TimetableUploadSection } from '../components/admin/TimetableUploadSection';
|
||||
import { logger } from '../../debugConfig';
|
||||
|
||||
const SUPER_ADMIN_EMAIL = import.meta.env.VITE_SUPER_ADMIN_EMAIL;
|
||||
|
||||
interface TabPanelProps {
|
||||
children?: React.ReactNode;
|
||||
index: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
function TabPanel(props: TabPanelProps) {
|
||||
const { children, value, index, ...other } = props;
|
||||
return (
|
||||
<div hidden={value !== index} {...other}>
|
||||
{value === index && <Box sx={{ p: 3 }}>{children}</Box>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AdminDashboard() {
|
||||
const { user } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [tabValue, setTabValue] = useState(0);
|
||||
|
||||
const isSuperAdmin = user?.email === SUPER_ADMIN_EMAIL;
|
||||
|
||||
logger.debug('admin-page', '🔍 Super admin check', {
|
||||
userEmail: user?.email,
|
||||
superAdminEmail: SUPER_ADMIN_EMAIL,
|
||||
isMatch: isSuperAdmin
|
||||
});
|
||||
|
||||
const handleReturn = () => {
|
||||
logger.info('admin-page', '🏠 Returning to single player page');
|
||||
navigate('/single-player');
|
||||
};
|
||||
|
||||
if (!isSuperAdmin) {
|
||||
logger.error('admin-page', '🚫 Unauthorized access attempt', {
|
||||
userEmail: user?.email,
|
||||
requiredEmail: SUPER_ADMIN_EMAIL
|
||||
});
|
||||
return (
|
||||
<Container>
|
||||
<Typography variant="h4" color="error">Unauthorized Access</Typography>
|
||||
<Button
|
||||
onClick={handleReturn}
|
||||
variant="contained"
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
Return to User Page
|
||||
</Button>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
|
||||
setTabValue(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{ mt: 4 }}>
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
mb: 2
|
||||
}}>
|
||||
<Typography variant="h4">Admin Dashboard</Typography>
|
||||
<Button
|
||||
onClick={handleReturn}
|
||||
variant="outlined"
|
||||
>
|
||||
Return to User Page
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Paper sx={{ width: '100%', mb: 2 }}>
|
||||
<Tabs value={tabValue} onChange={handleTabChange}>
|
||||
<Tab label="System Settings" />
|
||||
<Tab label="Database Management" />
|
||||
<Tab label="User Management" />
|
||||
<Tab label="Schools" />
|
||||
<Tab label="Timetables" />
|
||||
</Tabs>
|
||||
<TabPanel value={tabValue} index={0}>
|
||||
<Typography variant="h6">System Settings</Typography>
|
||||
{/* Add system settings components here */}
|
||||
</TabPanel>
|
||||
<TabPanel value={tabValue} index={1}>
|
||||
<Typography variant="h6">Database Management</Typography>
|
||||
{/* Add database management components here */}
|
||||
</TabPanel>
|
||||
<TabPanel value={tabValue} index={2}>
|
||||
<Typography variant="h6">User Management</Typography>
|
||||
{/* Add user management components here */}
|
||||
</TabPanel>
|
||||
<TabPanel value={tabValue} index={3}>
|
||||
<SchoolUploadSection />
|
||||
</TabPanel>
|
||||
<TabPanel value={tabValue} index={4}>
|
||||
<TimetableUploadSection />
|
||||
</TabPanel>
|
||||
</Paper>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Container, Typography, Box, Alert } from '@mui/material';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { EmailLoginForm } from './EmailLoginForm';
|
||||
import { EmailCredentials } from '../../services/auth/authService';
|
||||
import { logger } from '../../debugConfig';
|
||||
|
||||
const LoginPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { user, signIn } = useAuth();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
logger.debug('login-page', '🔍 Login page loaded', {
|
||||
hasUser: !!user
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
navigate('/single-player');
|
||||
}
|
||||
}, [user, navigate]);
|
||||
|
||||
const handleLogin = async (credentials: EmailCredentials) => {
|
||||
try {
|
||||
setError(null);
|
||||
await signIn(credentials.email, credentials.password);
|
||||
navigate('/single-player');
|
||||
} catch (error) {
|
||||
logger.error('login-page', '❌ Login failed', error);
|
||||
setError(error instanceof Error ? error.message : 'Login failed');
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
if (user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: '100vh',
|
||||
gap: 4
|
||||
}}
|
||||
>
|
||||
<Typography variant="h2" component="h1" gutterBottom>
|
||||
ClassroomCopilot.ai
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h4" gutterBottom>
|
||||
Login
|
||||
</Typography>
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ width: '100%', maxWidth: 400 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Box sx={{ width: '100%', maxWidth: 400 }}>
|
||||
<EmailLoginForm
|
||||
role="email_teacher"
|
||||
onSubmit={handleLogin}
|
||||
/>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginPage;
|
||||
@@ -0,0 +1,120 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Container,
|
||||
Typography,
|
||||
Box,
|
||||
Button,
|
||||
Stack,
|
||||
Divider,
|
||||
} from '@mui/material';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { EmailSignupForm } from './EmailSignupForm';
|
||||
import { EmailCredentials } from '../../services/auth/authService';
|
||||
import { RegistrationService } from '../../services/auth/registrationService';
|
||||
import { logger } from '../../debugConfig';
|
||||
import MicrosoftIcon from '@mui/icons-material/Microsoft';
|
||||
|
||||
const SignupPage: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { user } = useAuth();
|
||||
const registrationService = RegistrationService.getInstance();
|
||||
|
||||
// Get role from location state, default to teacher
|
||||
const { role = 'teacher' } = location.state || {};
|
||||
const roleDisplay = role === 'teacher' ? 'Teacher' : 'Student';
|
||||
|
||||
logger.debug('signup-page', '🔍 Signup page loaded', {
|
||||
role,
|
||||
hasUser: !!user,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
navigate('/single-player');
|
||||
}
|
||||
}, [user, navigate]);
|
||||
|
||||
const handleSignup = async (
|
||||
credentials: EmailCredentials,
|
||||
displayName: string
|
||||
) => {
|
||||
try {
|
||||
const result = await registrationService.register(
|
||||
credentials,
|
||||
displayName
|
||||
);
|
||||
if (result.user) {
|
||||
navigate('/single-player');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('signup-page', '❌ Registration failed', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const switchRole = () => {
|
||||
navigate('/signup', {
|
||||
state: { role: role === 'teacher' ? 'student' : 'teacher' },
|
||||
});
|
||||
};
|
||||
|
||||
if (user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
minHeight: '100vh',
|
||||
gap: 4,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h2" component="h1" gutterBottom>
|
||||
ClassroomCopilot.ai
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h4" gutterBottom>
|
||||
{roleDisplay} Sign Up
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ width: '100%', maxWidth: 400 }}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
startIcon={<MicrosoftIcon />}
|
||||
onClick={() => {}}
|
||||
sx={{ mb: 3 }}
|
||||
>
|
||||
Sign up with Microsoft
|
||||
</Button>
|
||||
|
||||
<Divider sx={{ my: 2 }}>OR</Divider>
|
||||
|
||||
<EmailSignupForm
|
||||
role={`email_${role}` as 'email_teacher' | 'email_student'}
|
||||
onSubmit={handleSignup}
|
||||
/>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
justifyContent="center"
|
||||
sx={{ mt: 3 }}
|
||||
>
|
||||
<Button variant="text" onClick={switchRole}>
|
||||
Switch to {role === 'teacher' ? 'Student' : 'Teacher'} Sign Up
|
||||
</Button>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default SignupPage;
|
||||
|
||||
Reference in New Issue
Block a user