Spaces:
Sleeping
Sleeping
'use client'; | |
import './NexusAuth.css'; | |
import { useState, useEffect } from 'react'; | |
import NexusAuthApi from '@lib/Nexus_Auth_API'; | |
import SplashScreen from '@components/SplashScreen'; | |
import { useToast } from '@lib/ToastContext'; | |
import { CheckCircleIcon } from '@heroicons/react/20/solid'; | |
const SignupForm = ({ onSignup }) => { | |
const [username, setUsername] = useState(''); | |
const [password, setPassword] = useState(''); | |
const [confirmPassword, setConfirmPassword] = useState(''); | |
const [email, setEmail] = useState(''); | |
const [usernameAvailable, setUsernameAvailable] = useState(null); // null for initial state | |
const [doesUsernameContainInvalidChars, setDoesUsernameContainInvalidChars] = useState(false); | |
const [doesUsernameExceedMinLength, setDoesUsernameExceedMinLength] = useState(false); | |
const [passwordValid, setPasswordValid] = useState(false); // Initially invalid | |
const [formValid, setFormValid] = useState(false); | |
const [debounceTimeout, setDebounceTimeout] = useState(null); // Store timeout ID | |
const minUsernameLength = 3; | |
const validatePassword = (password) => { | |
return password.length >= 8; | |
}; | |
const handleUsernameChange = (e) => { | |
const newUsername = e.target.value; | |
setUsername(newUsername); | |
// Reset username availability while typing | |
setUsernameAvailable(null); | |
// Clear any existing debounce timeout | |
if (debounceTimeout) { | |
clearTimeout(debounceTimeout); | |
} | |
// Check for invalid characters | |
const invalidChars = /[^a-zA-Z0-9_]/g; | |
if (invalidChars.test(newUsername)) { | |
setDoesUsernameContainInvalidChars(true); | |
setTimeout(() => { | |
setDoesUsernameContainInvalidChars(false); | |
}, 2000); // Show error for 2 seconds | |
} | |
// Basic sanitization to prevent SQL injection | |
const sanitizedUsername = newUsername.replace(invalidChars, ''); | |
if (sanitizedUsername.length < minUsernameLength) { | |
setDoesUsernameExceedMinLength(false); | |
return; | |
} else { | |
setDoesUsernameExceedMinLength(true);} | |
if (sanitizedUsername.trim().length > 0) { | |
// Set a new timeout to check availability | |
const newTimeout = setTimeout(async () => { | |
try { | |
const response = await NexusAuthApi.isUsernameAvailable(sanitizedUsername); | |
setUsernameAvailable(response?.is_available === true); | |
} catch (error) { | |
console.error('Error checking username availability:', error); | |
setUsernameAvailable(null); // Fallback state | |
} | |
}, 1000); // 1-second debounce delay | |
setDebounceTimeout(newTimeout); | |
} else { | |
setUsernameAvailable(null); // Reset availability check when input is empty | |
} | |
// Set sanitized username | |
setUsername(sanitizedUsername); | |
}; | |
const handlePasswordChange = (e) => { | |
const newPassword = e.target.value; | |
setPassword(newPassword); | |
setPasswordValid(validatePassword(newPassword)); | |
}; | |
const handleConfirmPasswordChange = (e) => { | |
setConfirmPassword(e.target.value); | |
}; | |
const handleSubmit = (e) => { | |
e.preventDefault(); | |
// Set email to null if it's empty | |
const emailValue = email.trim() === '' ? null : email; | |
if (password === confirmPassword && passwordValid) { | |
onSignup({ username, password, email: emailValue }); | |
} | |
}; | |
useEffect(() => { | |
setFormValid( | |
usernameAvailable === true && | |
password === confirmPassword && | |
passwordValid && | |
username.length >= minUsernameLength && | |
!doesUsernameContainInvalidChars | |
); | |
}, [username, password, confirmPassword, usernameAvailable, passwordValid]); | |
return ( | |
<form onSubmit={handleSubmit} className="nexus-auth-form"> | |
<h2>Signup</h2> | |
<div className="form-group"> | |
<label>Username:</label> | |
<input | |
type="text" | |
value={username} | |
onChange={handleUsernameChange} | |
required | |
className={usernameAvailable === false ? 'error' : ''} /> | |
{usernameAvailable === true && username.length > 0 && ( | |
<CheckCircleIcon className="h-5 w-5 text-green-500" /> | |
)} | |
{doesUsernameExceedMinLength === false && ( | |
<p className="error-message text-red-500">Username must have more than {minUsernameLength} characters.</p> | |
)} | |
{doesUsernameContainInvalidChars === true && ( | |
<p className="error-message text-red-500">Username cannot contain invalid characters.</p> | |
)} | |
{usernameAvailable === false && ( | |
<p className="error-message text-red-500">Username is already taken</p> | |
)} | |
{usernameAvailable === null && username.length > 0 && ( | |
<p className="typing-message text-green-500">Checking username availability...</p> | |
)} | |
</div> | |
<div className="form-group"> | |
<label>Password:</label> | |
<input | |
type="password" | |
value={password} | |
onChange={handlePasswordChange} | |
required | |
className={passwordValid ? '' : 'error'} /> | |
{passwordValid && ( | |
<CheckCircleIcon className="h-5 w-5 text-green-500" /> | |
)} | |
{!passwordValid && ( | |
<p className="error-message text-yellow-500">Password must be at least 8 characters long</p> | |
)} | |
</div> | |
<div className="form-group"> | |
<label>Confirm Password:</label> | |
<input | |
type="password" | |
value={confirmPassword} | |
onChange={handleConfirmPasswordChange} | |
required | |
className={password === confirmPassword ? '' : 'error'} /> | |
{password === confirmPassword && confirmPassword.length > 0 && ( | |
<CheckCircleIcon className="h-5 w-5 text-green-500" /> | |
)} | |
{password !== confirmPassword && confirmPassword.length > 0 && ( | |
<p className="error-message text-red-500">Passwords do not match</p> | |
)} | |
</div> | |
<div className="form-group"> | |
<label>Email (optional):</label> | |
<input | |
type="email" | |
value={email} | |
onChange={(e) => setEmail(e.target.value)} /> | |
</div> | |
<button type="submit" className="submit-button" disabled={!formValid}> | |
Signup | |
</button> | |
</form> | |
); | |
}; | |
const LoginForm = ({ onLogin }) => { | |
const [username, setUsername] = useState(''); | |
const [password, setPassword] = useState(''); | |
const handleSubmit = (e) => { | |
e.preventDefault(); | |
onLogin({ username, password }); | |
}; | |
return ( | |
<form onSubmit={handleSubmit} className="nexus-auth-form"> | |
<h2>Login</h2> | |
<div className="form-group"> | |
<label>Username:</label> | |
<input | |
type="text" | |
value={username} | |
onChange={(e) => setUsername(e.target.value)} | |
required /> | |
</div> | |
<div className="form-group"> | |
<label>Password:</label> | |
<input | |
type="password" | |
value={password} | |
onChange={(e) => setPassword(e.target.value)} | |
required /> | |
</div> | |
<button type="submit" className="submit-button"> | |
Login | |
</button> | |
</form> | |
); | |
}; | |
export const NexusAuthWrapper = ({ children }) => { | |
const [isLoggedIn, setIsLoggedIn] = useState(false); | |
const [isSignup, setIsSignup] = useState(false); | |
const [isLoading, setIsLoading] = useState(true); | |
const toast = useToast(); | |
useEffect(() => { | |
const validateUserSession = async () => { | |
const storedUsername = localStorage.getItem("me"); | |
const storedToken = localStorage.getItem("s_tkn"); | |
const storedUserID = localStorage.getItem("u_id"); | |
if (storedUsername && storedToken && storedUserID) { | |
try { | |
// Validate the token with the NexusAuthApi | |
const response = await NexusAuthApi.validateToken(storedUserID, storedToken); | |
if (response.data && response.data.user_id) { | |
// Token is valid; response contains user details | |
console.log("User is already logged in."); | |
toast.info("Welcome back, " + response.data.username + "!"); | |
setIsLoggedIn(true); | |
// Optionally, update localStorage with new details if needed | |
localStorage.setItem("me", response.data.username); | |
localStorage.setItem("s_tkn", response.data.access_token); | |
localStorage.setItem("u_id", response.data.user_id); | |
localStorage.setItem("a_l", response.data.access_level); | |
} else if (response.status === 401) { | |
// Token is invalid; clear local storage | |
console.error("Token validation failed with status 401:", response.data); | |
clearLocalStorage(); | |
} else { | |
// Handle other errors (e.g., network issues) | |
console.error("Token validation failed due to an unexpected error:", response.data); | |
toast.error("Unable to validate token. Please check your connection."); | |
} | |
} catch (error) { | |
// Handle other errors (e.g., network issues) | |
console.error("Token validation failed due to an unexpected error:", error); | |
toast.error("Unable to validate token. Please check your connection."); | |
} | |
} | |
setIsLoading(false); | |
}; | |
const clearLocalStorage = () => { | |
localStorage.removeItem("me"); | |
localStorage.removeItem("s_tkn"); | |
localStorage.removeItem("u_id"); | |
localStorage.removeItem("a_l"); | |
setIsLoggedIn(false); | |
toast.error("Session expired. Please login again."); | |
}; | |
validateUserSession(); | |
}, []); | |
const handleSignup = async (data) => { | |
setIsLoading(true); | |
try { | |
const response = await NexusAuthApi.signup(data.username, data.password, data.email); | |
console.log("Signup successful:", response); | |
setIsLoading(false); | |
toast.success('Signup successful. Please login to continue'); | |
setIsSignup(false); | |
} catch (error) { | |
setIsLoading(false); | |
console.error("Signup failed:", error); | |
toast.error("Signup failed"); | |
} | |
}; | |
const handleLogin = async (data) => { | |
setIsLoading(true); | |
try { | |
const response = await NexusAuthApi.login(data.username, data.password); | |
console.log("Login successful:", response); | |
toast.success('Login successful.'); | |
// Save username and token to localStorage | |
localStorage.setItem("me", response.username); | |
localStorage.setItem("s_tkn", response.access_token); | |
localStorage.setItem("u_id", response.user_id); | |
localStorage.setItem("a_l", response.access_level); | |
setIsLoggedIn(true); | |
setIsLoading(false); | |
} catch (error) { | |
setIsLoading(false); | |
console.error("Login failed:", error); | |
toast.error("Login failed"); | |
} | |
}; | |
if (isLoading) { | |
return <SplashScreen />; | |
} | |
return ( | |
<div> | |
{isLoggedIn ? ( | |
children | |
) : ( | |
<div className="nexus-auth-signup-login"> | |
<h1>Nexus Accounts</h1> | |
<button onClick={() => setIsSignup(!isSignup)}> | |
{isSignup ? "Already have an Account? Login" : "Don't have an Account? Signup"} | |
</button> | |
{isSignup ? ( | |
<SignupForm onSignup={handleSignup} /> | |
) : ( | |
<LoginForm onLogin={handleLogin} /> | |
)} | |
</div> | |
)} | |
</div> | |
); | |
}; | |