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