Fix jwt-token
This commit is contained in:
@@ -13,6 +13,7 @@ import Alerts from './pages/Alerts';
|
|||||||
import Debug from './pages/Debug';
|
import Debug from './pages/Debug';
|
||||||
import Settings from './pages/Settings';
|
import Settings from './pages/Settings';
|
||||||
import Login from './pages/Login';
|
import Login from './pages/Login';
|
||||||
|
import Register from './pages/Register';
|
||||||
import ProtectedRoute from './components/ProtectedRoute';
|
import ProtectedRoute from './components/ProtectedRoute';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
@@ -60,6 +61,7 @@ function App() {
|
|||||||
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route path="/register" element={<Register />} />
|
||||||
|
|
||||||
<Route path="/" element={
|
<Route path="/" element={
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
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 { useAuth } from '../contexts/AuthContext';
|
||||||
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
|
import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast';
|
||||||
@@ -205,13 +205,13 @@ const Login = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Registration link for local auth if enabled */}
|
{/* Registration link for local auth if enabled */}
|
||||||
{tenantConfig?.auth_provider === 'local' && tenantConfig?.local?.allow_registration && (
|
{tenantConfig?.features?.registration && (
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
Don't have an account?{' '}
|
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
|
Sign up
|
||||||
</a>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
395
client/src/pages/Register.jsx
Normal file
395
client/src/pages/Register.jsx
Normal 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;
|
||||||
30
server/migrations/20250914-add-allow-registration.js
Normal file
30
server/migrations/20250914-add-allow-registration.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Migration: Add allow_registration field to tenants table
|
||||||
|
* This field controls whether self-registration is allowed for local auth tenants
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.addColumn('tenants', 'allow_registration', {
|
||||||
|
type: Sequelize.BOOLEAN,
|
||||||
|
defaultValue: false, // Default to false for security
|
||||||
|
allowNull: false,
|
||||||
|
comment: 'Whether self-registration is allowed for local auth'
|
||||||
|
});
|
||||||
|
|
||||||
|
// For existing tenants, you might want to enable registration for specific tenants
|
||||||
|
// Uncomment the line below to enable registration for all existing tenants (NOT RECOMMENDED for production)
|
||||||
|
// await queryInterface.sequelize.query("UPDATE tenants SET allow_registration = true WHERE auth_provider = 'local'");
|
||||||
|
|
||||||
|
console.log('✅ Added allow_registration field to tenants table');
|
||||||
|
console.log('⚠️ Registration is disabled by default for all tenants for security');
|
||||||
|
console.log('💡 To enable registration for a tenant, update the allow_registration field to true');
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.removeColumn('tenants', 'allow_registration');
|
||||||
|
console.log('✅ Removed allow_registration field from tenants table');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -157,6 +157,11 @@ module.exports = (sequelize) => {
|
|||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
comment: 'Whether users can have multiple concurrent sessions'
|
comment: 'Whether users can have multiple concurrent sessions'
|
||||||
},
|
},
|
||||||
|
allow_registration: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
defaultValue: false, // Default to false for security
|
||||||
|
comment: 'Whether self-registration is allowed for local auth'
|
||||||
|
},
|
||||||
role_mappings: {
|
role_mappings: {
|
||||||
type: DataTypes.JSONB,
|
type: DataTypes.JSONB,
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ router.get('/config/:tenantId', async (req, res) => {
|
|||||||
features: {
|
features: {
|
||||||
local_login: tenant.auth_provider === 'local',
|
local_login: tenant.auth_provider === 'local',
|
||||||
sso_login: ['saml', 'oauth', 'ldap'].includes(tenant.auth_provider),
|
sso_login: ['saml', 'oauth', 'ldap'].includes(tenant.auth_provider),
|
||||||
registration: tenant.auth_provider === 'local'
|
registration: tenant.auth_provider === 'local' && tenant.allow_registration
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,21 +3,65 @@ const router = express.Router();
|
|||||||
const Joi = require('joi');
|
const Joi = require('joi');
|
||||||
const bcrypt = require('bcryptjs');
|
const bcrypt = require('bcryptjs');
|
||||||
const jwt = require('jsonwebtoken');
|
const jwt = require('jsonwebtoken');
|
||||||
|
const rateLimit = require('express-rate-limit');
|
||||||
const { User, Tenant } = require('../models');
|
const { User, Tenant } = require('../models');
|
||||||
const { Op } = require('sequelize');
|
const { Op } = require('sequelize');
|
||||||
const { validateRequest } = require('../middleware/validation');
|
const { validateRequest } = require('../middleware/validation');
|
||||||
const { authenticateToken, requireRole } = require('../middleware/auth');
|
const { authenticateToken, requireRole } = require('../middleware/auth');
|
||||||
const MultiTenantAuth = require('../middleware/multi-tenant-auth');
|
const MultiTenantAuth = require('../middleware/multi-tenant-auth');
|
||||||
|
|
||||||
// Validation schemas
|
// Rate limiting for registration endpoint - EXTRA SECURITY
|
||||||
|
const registrationLimiter = rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||||
|
max: 3, // Limit each IP to 3 registration attempts per windowMs
|
||||||
|
message: {
|
||||||
|
success: false,
|
||||||
|
message: 'Too many registration attempts. Please try again later.'
|
||||||
|
},
|
||||||
|
standardHeaders: true,
|
||||||
|
legacyHeaders: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enhanced validation schema with stronger requirements
|
||||||
const registerSchema = Joi.object({
|
const registerSchema = Joi.object({
|
||||||
username: Joi.string().min(3).max(50).required(),
|
username: Joi.string()
|
||||||
email: Joi.string().email().required(),
|
.min(3)
|
||||||
password: Joi.string().min(6).required(),
|
.max(50)
|
||||||
first_name: Joi.string().optional(),
|
.pattern(/^[a-zA-Z0-9._-]+$/)
|
||||||
last_name: Joi.string().optional(),
|
.required()
|
||||||
phone_number: Joi.string().optional(),
|
.messages({
|
||||||
role: Joi.string().valid('admin', 'operator', 'viewer').default('viewer')
|
'string.pattern.base': 'Username can only contain letters, numbers, dots, underscores, and hyphens'
|
||||||
|
}),
|
||||||
|
email: Joi.string()
|
||||||
|
.email()
|
||||||
|
.required()
|
||||||
|
.max(255),
|
||||||
|
password: Joi.string()
|
||||||
|
.min(8)
|
||||||
|
.max(100)
|
||||||
|
.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
|
||||||
|
.required()
|
||||||
|
.messages({
|
||||||
|
'string.pattern.base': 'Password must contain at least one lowercase letter, one uppercase letter, and one number'
|
||||||
|
}),
|
||||||
|
first_name: Joi.string()
|
||||||
|
.max(100)
|
||||||
|
.optional()
|
||||||
|
.allow(''),
|
||||||
|
last_name: Joi.string()
|
||||||
|
.max(100)
|
||||||
|
.optional()
|
||||||
|
.allow(''),
|
||||||
|
phone_number: Joi.string()
|
||||||
|
.pattern(/^[\+]?[1-9][\d]{0,15}$/)
|
||||||
|
.optional()
|
||||||
|
.allow('')
|
||||||
|
.messages({
|
||||||
|
'string.pattern.base': 'Please enter a valid phone number'
|
||||||
|
}),
|
||||||
|
role: Joi.string()
|
||||||
|
.valid('viewer') // Only allow viewer role for self-registration
|
||||||
|
.default('viewer')
|
||||||
});
|
});
|
||||||
|
|
||||||
const loginSchema = Joi.object({
|
const loginSchema = Joi.object({
|
||||||
@@ -34,21 +78,111 @@ const updateProfileSchema = Joi.object({
|
|||||||
timezone: Joi.string().optional()
|
timezone: Joi.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /api/users/register - Register new user
|
// POST /api/users/register - Register new user (ULTRA SECURE)
|
||||||
router.post('/register', validateRequest(registerSchema), async (req, res) => {
|
router.post('/register', registrationLimiter, validateRequest(registerSchema), async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
console.log('🔒 Registration attempt started');
|
||||||
|
|
||||||
|
// Step 1: Determine tenant context - CRITICAL SECURITY CHECK
|
||||||
|
const multiAuth = new MultiTenantAuth();
|
||||||
|
const tenantId = await multiAuth.determineTenant(req);
|
||||||
|
console.log('🔍 Registration - Determined tenant:', tenantId);
|
||||||
|
|
||||||
|
if (!tenantId) {
|
||||||
|
console.log('❌ Registration BLOCKED - No tenant determined');
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Unable to determine tenant context'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Get tenant from database - VERIFY TENANT EXISTS
|
||||||
|
const tenant = await Tenant.findOne({ where: { slug: tenantId } });
|
||||||
|
if (!tenant) {
|
||||||
|
console.log('❌ Registration BLOCKED - Tenant not found:', tenantId);
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Tenant not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 3: TRIPLE CHECK - Tenant must be active
|
||||||
|
if (!tenant.is_active) {
|
||||||
|
console.log('❌ Registration BLOCKED - Tenant inactive:', tenantId);
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Registration not available for this tenant'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: CRITICAL SECURITY CHECK - Only local auth tenants allow registration
|
||||||
|
if (tenant.auth_provider !== 'local') {
|
||||||
|
console.log('❌ Registration BLOCKED - Non-local auth provider:', tenant.auth_provider);
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Registration not available. This tenant uses external authentication.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 5: ULTIMATE SECURITY CHECK - Explicit registration permission
|
||||||
|
if (!tenant.allow_registration) {
|
||||||
|
console.log('❌ Registration BLOCKED - Registration disabled for tenant:', tenantId);
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Registration is not enabled for this tenant. Please contact your administrator.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 6: Additional security - Check if registration is explicitly enabled in auth config
|
||||||
|
const authConfig = await multiAuth.getTenantAuthConfig(tenantId);
|
||||||
|
if (!authConfig.enabled) {
|
||||||
|
console.log('❌ Registration BLOCKED - Auth config disabled');
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Authentication is disabled for this tenant'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Registration security checks passed for tenant:', tenantId);
|
||||||
|
|
||||||
|
// Step 7: Process registration - User data validation
|
||||||
const { password, ...userData } = req.body;
|
const { password, ...userData } = req.body;
|
||||||
|
|
||||||
// Hash password
|
// Check if user already exists in this tenant
|
||||||
|
const existingUser = await User.findOne({
|
||||||
|
where: {
|
||||||
|
[Op.or]: [
|
||||||
|
{ username: userData.username },
|
||||||
|
{ email: userData.email }
|
||||||
|
],
|
||||||
|
tenant_id: tenant.id // Scope to specific tenant
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingUser) {
|
||||||
|
console.log('❌ Registration BLOCKED - User already exists in tenant');
|
||||||
|
return res.status(409).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Username or email already exists'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 8: Hash password with high security
|
||||||
const saltRounds = 12;
|
const saltRounds = 12;
|
||||||
const password_hash = await bcrypt.hash(password, saltRounds);
|
const password_hash = await bcrypt.hash(password, saltRounds);
|
||||||
|
|
||||||
// Create user
|
// Step 9: Create user with tenant association
|
||||||
const user = await User.create({
|
const user = await User.create({
|
||||||
...userData,
|
...userData,
|
||||||
password_hash
|
password_hash,
|
||||||
|
tenant_id: tenant.id, // CRITICAL: Associate with specific tenant
|
||||||
|
is_active: true,
|
||||||
|
created_at: new Date(),
|
||||||
|
updated_at: new Date()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('✅ User registered successfully:', user.username, 'for tenant:', tenantId);
|
||||||
|
|
||||||
// Remove password hash from response
|
// Remove password hash from response
|
||||||
const { password_hash: _, ...userResponse } = user.toJSON();
|
const { password_hash: _, ...userResponse } = user.toJSON();
|
||||||
|
|
||||||
@@ -59,7 +193,7 @@ router.post('/register', validateRequest(registerSchema), async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error registering user:', error);
|
console.error('❌ Registration error:', error);
|
||||||
|
|
||||||
if (error.name === 'SequelizeUniqueConstraintError') {
|
if (error.name === 'SequelizeUniqueConstraintError') {
|
||||||
return res.status(409).json({
|
return res.status(409).json({
|
||||||
@@ -70,7 +204,7 @@ router.post('/register', validateRequest(registerSchema), async (req, res) => {
|
|||||||
|
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Failed to register user',
|
message: 'Registration failed',
|
||||||
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user