| '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); |
| const [doesUsernameContainInvalidChars, setDoesUsernameContainInvalidChars] = useState(false); |
| const [doesUsernameExceedMinLength, setDoesUsernameExceedMinLength] = useState(false); |
| const [passwordValid, setPasswordValid] = useState(false); |
| const [formValid, setFormValid] = useState(false); |
| const [debounceTimeout, setDebounceTimeout] = useState(null); |
|
|
| const minUsernameLength = 3; |
|
|
| const validatePassword = (password) => { |
| return password.length >= 8; |
| }; |
|
|
| const handleUsernameChange = (e) => { |
| const newUsername = e.target.value; |
| setUsername(newUsername); |
|
|
| |
| setUsernameAvailable(null); |
|
|
| |
| if (debounceTimeout) { |
| clearTimeout(debounceTimeout); |
| } |
|
|
| |
| const invalidChars = /[^a-zA-Z0-9_]/g; |
| if (invalidChars.test(newUsername)) { |
| setDoesUsernameContainInvalidChars(true); |
| setTimeout(() => { |
| setDoesUsernameContainInvalidChars(false); |
| }, 2000); |
| } |
|
|
| |
| const sanitizedUsername = newUsername.replace(invalidChars, ''); |
|
|
| if (sanitizedUsername.length < minUsernameLength) { |
| setDoesUsernameExceedMinLength(false); |
| return; |
| } else { |
| setDoesUsernameExceedMinLength(true);} |
|
|
| if (sanitizedUsername.trim().length > 0) { |
| |
| 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); |
| } |
| }, 1000); |
|
|
| setDebounceTimeout(newTimeout); |
| } else { |
| setUsernameAvailable(null); |
| } |
|
|
| |
| 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(); |
|
|
| |
| 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 { |
| |
| const response = await NexusAuthApi.validateToken(storedUserID, storedToken); |
|
|
| if (response.data && response.data.user_id) { |
| |
| console.log("User is already logged in."); |
| toast.info("Welcome back, " + response.data.username + "!"); |
| setIsLoggedIn(true); |
|
|
| |
| 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) { |
| |
| console.info("Token validation failed with status 401:"); |
| clearLocalStorage(); |
| } else { |
| |
| console.debug("Token validation failed due to an unexpected error:", response.data); |
| toast.error("Unable to validate token. Please check your connection."); |
| } |
| } catch (error) { |
| |
| console.debug("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.debug("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.'); |
| |
| 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.debug("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> |
| ); |
| }; |
|
|