ChandimaPrabath's picture
upload
050fd77
'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>
);
};