Fix jwt-token

This commit is contained in:
2025-09-14 10:10:04 +02:00
parent bcaa48e5de
commit de220bb040
7 changed files with 586 additions and 20 deletions

View File

@@ -13,6 +13,7 @@ import Alerts from './pages/Alerts';
import Debug from './pages/Debug';
import Settings from './pages/Settings';
import Login from './pages/Login';
import Register from './pages/Register';
import ProtectedRoute from './components/ProtectedRoute';
function App() {
@@ -60,6 +61,7 @@ function App() {
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/" element={
<ProtectedRoute>

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { Navigate } from 'react-router-dom';
import { Navigate, Link } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
import toast from 'react-hot-toast';
@@ -205,13 +205,13 @@ const Login = () => {
)}
{/* Registration link for local auth if enabled */}
{tenantConfig?.auth_provider === 'local' && tenantConfig?.local?.allow_registration && (
{tenantConfig?.features?.registration && (
<div className="text-center">
<p className="text-sm text-gray-600">
Don't have an account?{' '}
<a href="/register" className="font-medium text-primary-600 hover:text-primary-500">
<Link to="/register" className="font-medium text-primary-600 hover:text-primary-500">
Sign up
</a>
</Link>
</p>
</div>
)}

View File

@@ -0,0 +1,395 @@
import React, { useState, useEffect } from 'react';
import { Navigate, Link } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
import toast from 'react-hot-toast';
import api from '../services/api';
const Register = () => {
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
confirmPassword: '',
first_name: '',
last_name: '',
phone_number: ''
});
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [tenantConfig, setTenantConfig] = useState(null);
const [configLoading, setConfigLoading] = useState(true);
const [registering, setRegistering] = useState(false);
const { isAuthenticated } = useAuth();
// Fetch tenant configuration on mount
useEffect(() => {
const fetchTenantConfig = async () => {
try {
const response = await api.get('/auth/config');
setTenantConfig(response.data.data);
// Security check: If registration is not enabled, show error
if (!response.data.data?.features?.registration) {
toast.error('Registration is not enabled for this tenant');
}
} catch (error) {
console.error('Failed to fetch tenant config:', error);
toast.error('Failed to load authentication configuration');
} finally {
setConfigLoading(false);
}
};
fetchTenantConfig();
}, []);
if (isAuthenticated) {
return <Navigate to="/" replace />;
}
// Show loading while fetching config
if (configLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading...</p>
</div>
</div>
);
}
// Block access if registration is not enabled
if (!tenantConfig?.features?.registration) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full text-center">
<div className="mx-auto h-12 w-12 bg-red-600 rounded-lg flex items-center justify-center">
<svg className="h-8 w-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Registration Not Available
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Registration is not enabled for this tenant. Please contact your administrator.
</p>
<div className="mt-6">
<Link
to="/login"
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
Back to Login
</Link>
</div>
</div>
</div>
);
}
// Block if not local authentication
if (tenantConfig?.provider !== 'local') {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="max-w-md w-full text-center">
<div className="mx-auto h-12 w-12 bg-blue-600 rounded-lg flex items-center justify-center">
<svg className="h-8 w-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
External Authentication
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
This tenant uses {tenantConfig.provider.toUpperCase()} authentication.
Registration is handled through your organization's authentication system.
</p>
<div className="mt-6">
<Link
to="/login"
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
>
Go to Login
</Link>
</div>
</div>
</div>
);
}
const handleSubmit = async (e) => {
e.preventDefault();
// Validation
if (!formData.username || !formData.email || !formData.password) {
toast.error('Please fill in all required fields');
return;
}
if (formData.password !== formData.confirmPassword) {
toast.error('Passwords do not match');
return;
}
if (formData.password.length < 8) {
toast.error('Password must be at least 8 characters long');
return;
}
// Strong password validation
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/;
if (!passwordRegex.test(formData.password)) {
toast.error('Password must contain at least one lowercase letter, one uppercase letter, and one number');
return;
}
// Username validation
const usernameRegex = /^[a-zA-Z0-9._-]+$/;
if (!usernameRegex.test(formData.username)) {
toast.error('Username can only contain letters, numbers, dots, underscores, and hyphens');
return;
}
// Email validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(formData.email)) {
toast.error('Please enter a valid email address');
return;
}
// Phone number validation (if provided)
if (formData.phone_number) {
const phoneRegex = /^[\+]?[1-9][\d]{0,15}$/;
if (!phoneRegex.test(formData.phone_number.replace(/[\s\-\(\)]/g, ''))) {
toast.error('Please enter a valid phone number');
return;
}
}
setRegistering(true);
try {
const { confirmPassword, ...registrationData } = formData;
const response = await api.post('/users/register', registrationData);
if (response.data.success) {
toast.success('Registration successful! Please log in with your new account.');
// Redirect to login page
window.location.href = '/login';
}
} catch (error) {
console.error('Registration error:', error);
const errorMessage = error.response?.data?.message || 'Registration failed';
toast.error(errorMessage);
} finally {
setRegistering(false);
}
};
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8">
<div>
<div className="mx-auto h-12 w-12 bg-primary-600 rounded-lg flex items-center justify-center">
<svg className="h-8 w-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z" />
</svg>
</div>
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
Create your account
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Register for {tenantConfig?.tenant_name || 'Drone Detection System'}
</p>
</div>
<form className="mt-8 space-y-6" onSubmit={handleSubmit}>
<div className="space-y-4">
{/* Username */}
<div>
<label htmlFor="username" className="block text-sm font-medium text-gray-700">
Username <span className="text-red-500">*</span>
</label>
<input
id="username"
name="username"
type="text"
required
className="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
placeholder="Choose a username"
value={formData.username}
onChange={handleChange}
disabled={registering}
/>
</div>
{/* Email */}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
Email Address <span className="text-red-500">*</span>
</label>
<input
id="email"
name="email"
type="email"
required
className="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
placeholder="your@email.com"
value={formData.email}
onChange={handleChange}
disabled={registering}
/>
</div>
{/* First Name */}
<div>
<label htmlFor="first_name" className="block text-sm font-medium text-gray-700">
First Name
</label>
<input
id="first_name"
name="first_name"
type="text"
className="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
placeholder="First name"
value={formData.first_name}
onChange={handleChange}
disabled={registering}
/>
</div>
{/* Last Name */}
<div>
<label htmlFor="last_name" className="block text-sm font-medium text-gray-700">
Last Name
</label>
<input
id="last_name"
name="last_name"
type="text"
className="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
placeholder="Last name"
value={formData.last_name}
onChange={handleChange}
disabled={registering}
/>
</div>
{/* Phone Number */}
<div>
<label htmlFor="phone_number" className="block text-sm font-medium text-gray-700">
Phone Number
</label>
<input
id="phone_number"
name="phone_number"
type="tel"
className="mt-1 appearance-none relative block w-full px-3 py-2 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
placeholder="+1 (555) 123-4567"
value={formData.phone_number}
onChange={handleChange}
disabled={registering}
/>
</div>
{/* Password */}
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
Password <span className="text-red-500">*</span>
</label>
<div className="mt-1 relative">
<input
id="password"
name="password"
type={showPassword ? 'text' : 'password'}
required
className="appearance-none relative block w-full px-3 py-2 pr-10 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
placeholder="Minimum 8 characters with uppercase, lowercase, and number"
value={formData.password}
onChange={handleChange}
disabled={registering}
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeSlashIcon className="h-5 w-5 text-gray-400" />
) : (
<EyeIcon className="h-5 w-5 text-gray-400" />
)}
</button>
</div>
</div>
{/* Confirm Password */}
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700">
Confirm Password <span className="text-red-500">*</span>
</label>
<div className="mt-1 relative">
<input
id="confirmPassword"
name="confirmPassword"
type={showConfirmPassword ? 'text' : 'password'}
required
className="appearance-none relative block w-full px-3 py-2 pr-10 border border-gray-300 placeholder-gray-500 text-gray-900 rounded-md focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
placeholder="Confirm your password"
value={formData.confirmPassword}
onChange={handleChange}
disabled={registering}
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
>
{showConfirmPassword ? (
<EyeSlashIcon className="h-5 w-5 text-gray-400" />
) : (
<EyeIcon className="h-5 w-5 text-gray-400" />
)}
</button>
</div>
</div>
</div>
<div>
<button
type="submit"
disabled={registering}
className="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{registering ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
) : (
'Create Account'
)}
</button>
</div>
<div className="text-center">
<p className="text-sm text-gray-600">
Already have an account?{' '}
<Link to="/login" className="font-medium text-primary-600 hover:text-primary-500">
Sign in
</Link>
</p>
</div>
</form>
</div>
</div>
);
};
export default Register;