UNPKG

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