Backend Implementation (FastAPI + MongoDB): - JWT authentication with access/refresh tokens - User registration and login endpoints - Password hashing with bcrypt (fixed 72-byte limit) - Protected endpoints with JWT middleware - Token refresh mechanism - Role-Based Access Control (RBAC) structure - Pydantic v2 models and async MongoDB with Motor - API endpoints: /api/auth/register, /api/auth/login, /api/auth/me, /api/auth/refresh Frontend Implementation (React + TypeScript + Material-UI): - Login and Register pages with validation - AuthContext for global authentication state - API client with Axios interceptors for token refresh - Protected routes with automatic redirect - User profile display in navigation - Logout functionality Technical Achievements: - Resolved bcrypt 72-byte limit (replaced passlib with native bcrypt) - Fixed Pydantic v2 compatibility (PyObjectId, ConfigDict) - Implemented automatic token refresh on 401 errors - Created comprehensive test suite for all auth endpoints Docker & Kubernetes: - Backend image: yakenator/site11-console-backend:latest - Frontend image: yakenator/site11-console-frontend:latest - Deployed to site11-pipeline namespace - Nginx reverse proxy configuration Documentation: - CONSOLE_ARCHITECTURE.md - Complete system architecture - PHASE1_COMPLETION.md - Detailed completion report - PROGRESS.md - Updated with Phase 1 status All authentication endpoints tested and verified working. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
183 lines
4.7 KiB
TypeScript
183 lines
4.7 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { useNavigate, Link as RouterLink } from 'react-router-dom';
|
|
import {
|
|
Container,
|
|
Box,
|
|
Paper,
|
|
TextField,
|
|
Button,
|
|
Typography,
|
|
Alert,
|
|
Link,
|
|
} from '@mui/material';
|
|
import { useAuth } from '../contexts/AuthContext';
|
|
|
|
const Register: React.FC = () => {
|
|
const navigate = useNavigate();
|
|
const { register } = useAuth();
|
|
const [formData, setFormData] = useState({
|
|
email: '',
|
|
username: '',
|
|
password: '',
|
|
confirmPassword: '',
|
|
full_name: '',
|
|
});
|
|
const [error, setError] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
setFormData({
|
|
...formData,
|
|
[e.target.name]: e.target.value,
|
|
});
|
|
setError('');
|
|
};
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError('');
|
|
|
|
// Validate passwords match
|
|
if (formData.password !== formData.confirmPassword) {
|
|
setError('Passwords do not match');
|
|
return;
|
|
}
|
|
|
|
// Validate password length
|
|
if (formData.password.length < 6) {
|
|
setError('Password must be at least 6 characters');
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
|
|
try {
|
|
await register({
|
|
email: formData.email,
|
|
username: formData.username,
|
|
password: formData.password,
|
|
full_name: formData.full_name || undefined,
|
|
});
|
|
navigate('/');
|
|
} catch (err: any) {
|
|
setError(err.response?.data?.detail || 'Registration failed. Please try again.');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Container maxWidth="sm">
|
|
<Box
|
|
sx={{
|
|
minHeight: '100vh',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
}}
|
|
>
|
|
<Paper
|
|
elevation={3}
|
|
sx={{
|
|
p: 4,
|
|
width: '100%',
|
|
maxWidth: 400,
|
|
}}
|
|
>
|
|
<Typography variant="h4" component="h1" gutterBottom align="center">
|
|
Site11 Console
|
|
</Typography>
|
|
<Typography variant="h6" component="h2" gutterBottom align="center" color="text.secondary">
|
|
Create Account
|
|
</Typography>
|
|
|
|
{error && (
|
|
<Alert severity="error" sx={{ mb: 2 }}>
|
|
{error}
|
|
</Alert>
|
|
)}
|
|
|
|
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 2 }}>
|
|
<TextField
|
|
fullWidth
|
|
label="Email"
|
|
name="email"
|
|
type="email"
|
|
value={formData.email}
|
|
onChange={handleChange}
|
|
margin="normal"
|
|
required
|
|
autoFocus
|
|
disabled={loading}
|
|
/>
|
|
<TextField
|
|
fullWidth
|
|
label="Username"
|
|
name="username"
|
|
value={formData.username}
|
|
onChange={handleChange}
|
|
margin="normal"
|
|
required
|
|
disabled={loading}
|
|
inputProps={{ minLength: 3, maxLength: 50 }}
|
|
/>
|
|
<TextField
|
|
fullWidth
|
|
label="Full Name"
|
|
name="full_name"
|
|
value={formData.full_name}
|
|
onChange={handleChange}
|
|
margin="normal"
|
|
disabled={loading}
|
|
/>
|
|
<TextField
|
|
fullWidth
|
|
label="Password"
|
|
name="password"
|
|
type="password"
|
|
value={formData.password}
|
|
onChange={handleChange}
|
|
margin="normal"
|
|
required
|
|
disabled={loading}
|
|
inputProps={{ minLength: 6 }}
|
|
/>
|
|
<TextField
|
|
fullWidth
|
|
label="Confirm Password"
|
|
name="confirmPassword"
|
|
type="password"
|
|
value={formData.confirmPassword}
|
|
onChange={handleChange}
|
|
margin="normal"
|
|
required
|
|
disabled={loading}
|
|
/>
|
|
<Button
|
|
type="submit"
|
|
fullWidth
|
|
variant="contained"
|
|
size="large"
|
|
sx={{ mt: 3, mb: 2 }}
|
|
disabled={loading}
|
|
>
|
|
{loading ? 'Creating account...' : 'Sign Up'}
|
|
</Button>
|
|
|
|
<Box sx={{ textAlign: 'center' }}>
|
|
<Typography variant="body2">
|
|
Already have an account?{' '}
|
|
<Link component={RouterLink} to="/login" underline="hover">
|
|
Sign In
|
|
</Link>
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
</Paper>
|
|
</Box>
|
|
</Container>
|
|
);
|
|
};
|
|
|
|
export default Register;
|