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> | |
| ); | |
| }; | |