simple-express-react-auth
Version:
A lightweight authentication package for Express/React apps with single password protection using cookie-session
299 lines (264 loc) • 7.09 kB
JavaScript
const React = require('react');
const { useState, useEffect, createContext, useContext } = React;
// Auth Context
const AuthContext = createContext();
/**
* Auth Provider Component
* Manages authentication state and provides auth methods to children
*/
function AuthProvider({ children, apiBaseUrl = '', statusPath = '/auth/status', loginPath = '/auth/login', logoutPath = '/auth/logout' }) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
// Check authentication status
const checkAuthStatus = async () => {
try {
setIsLoading(true);
setError(null);
const response = await fetch(`${apiBaseUrl}${statusPath}`, {
method: 'GET',
credentials: 'include', // Include cookies/session
});
const data = await response.json();
setIsAuthenticated(data.authenticated || false);
} catch (err) {
console.error('Auth status check failed:', err);
setError('Failed to check authentication status');
setIsAuthenticated(false);
} finally {
setIsLoading(false);
}
};
// Login function
const login = async (password) => {
try {
setError(null);
const response = await fetch(`${apiBaseUrl}${loginPath}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({ password }),
});
const data = await response.json();
if (response.ok && data.authenticated) {
setIsAuthenticated(true);
return { success: true, message: data.message };
} else {
setIsAuthenticated(false);
return { success: false, error: data.error || 'Login failed' };
}
} catch (err) {
console.error('Login failed:', err);
const errorMessage = 'Login request failed';
setError(errorMessage);
return { success: false, error: errorMessage };
}
};
// Logout function
const logout = async () => {
try {
setError(null);
const response = await fetch(`${apiBaseUrl}${logoutPath}`, {
method: 'GET',
credentials: 'include',
});
if (response.ok) {
setIsAuthenticated(false);
return { success: true };
} else {
return { success: false, error: 'Logout failed' };
}
} catch (err) {
console.error('Logout failed:', err);
setError('Logout request failed');
return { success: false, error: 'Logout request failed' };
}
};
// Check auth status on mount
useEffect(() => {
checkAuthStatus();
}, []);
const value = {
isAuthenticated,
isLoading,
error,
login,
logout,
checkAuthStatus
};
return React.createElement(AuthContext.Provider, { value }, children);
}
/**
* Hook to use auth context
*/
function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}
/**
* Login Form Component
*/
function LoginForm({
onLoginSuccess,
className = '',
submitButtonText = 'Login',
passwordPlaceholder = 'Enter password',
showError = true
}) {
const [password, setPassword] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [localError, setLocalError] = useState('');
const { login } = useAuth();
const handleSubmit = async (e) => {
e.preventDefault();
if (!password.trim()) {
setLocalError('Password is required');
return;
}
setIsSubmitting(true);
setLocalError('');
const result = await login(password);
if (result.success) {
setPassword('');
if (onLoginSuccess) {
onLoginSuccess();
}
} else {
setLocalError(result.error);
}
setIsSubmitting(false);
};
return React.createElement(
'form',
{ onSubmit: handleSubmit, className },
React.createElement(
'div',
{ style: { marginBottom: '1rem' } },
React.createElement('input', {
type: 'password',
value: password,
onChange: (e) => setPassword(e.target.value),
placeholder: passwordPlaceholder,
disabled: isSubmitting,
style: {
width: '100%',
padding: '0.5rem',
fontSize: '1rem',
border: '1px solid #ccc',
borderRadius: '4px'
}
})
),
React.createElement(
'button',
{
type: 'submit',
disabled: isSubmitting,
style: {
width: '100%',
padding: '0.5rem',
fontSize: '1rem',
backgroundColor: '#007bff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: isSubmitting ? 'not-allowed' : 'pointer',
opacity: isSubmitting ? 0.6 : 1
}
},
isSubmitting ? 'Logging in...' : submitButtonText
),
showError && localError && React.createElement(
'div',
{
style: {
marginTop: '0.5rem',
color: '#dc3545',
fontSize: '0.875rem'
}
},
localError
)
);
}
/**
* Protected Route Component
* Only renders children if authenticated, otherwise shows login form
*/
function ProtectedRoute({
children,
fallback,
loadingComponent,
className = ''
}) {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return loadingComponent || React.createElement(
'div',
{ className },
'Loading...'
);
}
if (!isAuthenticated) {
return fallback || React.createElement(
'div',
{ className, style: { maxWidth: '400px', margin: '2rem auto', padding: '2rem' } },
React.createElement('h2', { style: { marginBottom: '1rem' } }, 'Please Login'),
React.createElement(LoginForm)
);
}
return children;
}
/**
* Logout Button Component
*/
function LogoutButton({
children = 'Logout',
onLogoutSuccess,
className = '',
...props
}) {
const [isLoggingOut, setIsLoggingOut] = useState(false);
const { logout } = useAuth();
const handleLogout = async () => {
setIsLoggingOut(true);
const result = await logout();
if (result.success && onLogoutSuccess) {
onLogoutSuccess();
}
setIsLoggingOut(false);
};
return React.createElement(
'button',
{
...props,
onClick: handleLogout,
disabled: isLoggingOut,
className,
style: {
padding: '0.5rem 1rem',
fontSize: '1rem',
backgroundColor: '#dc3545',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: isLoggingOut ? 'not-allowed' : 'pointer',
opacity: isLoggingOut ? 0.6 : 1,
...props.style
}
},
isLoggingOut ? 'Logging out...' : children
);
}
module.exports = {
AuthProvider,
useAuth,
LoginForm,
ProtectedRoute,
LogoutButton
};