@mycuppa/auth
The @mycuppa/auth package provides a complete authentication system with token management, automatic token refresh, role-based access control (RBAC), and session persistence.
Installation
npm install @mycuppa/auth react react-dom
# or
pnpm add @mycuppa/auth react react-dom
Quick Start
import { createAuthManager, useAuth } from '@mycuppa/auth'
import { useState } from 'react'
// Create auth manager once at app initialization
const authManager = createAuthManager({
storageType: 'localStorage',
autoRefresh: true,
refreshThreshold: 5 * 60 * 1000, // 5 minutes
})
function LoginForm() {
const { login, isLoading, isAuthenticated } = useAuth(authManager)
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleLogin = async (e) => {
e.preventDefault()
// Call your API
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
})
const { user, tokens } = await response.json()
// Set user and tokens in auth manager
await login(user, tokens)
}
if (isAuthenticated) {
return <div>Welcome back!</div>
}
return (
<form onSubmit={handleLogin}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
/>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
required
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Login'}
</button>
</form>
)
}
Core Concepts
AuthManager
The AuthManager is the core of the authentication system. It manages user state, tokens, and authentication lifecycle.
import { createAuthManager } from '@mycuppa/auth'
const authManager = createAuthManager({
// Storage key for tokens (default: 'cuppa_auth_tokens')
storageKey: 'my_app_tokens',
// Storage type (default: 'localStorage')
storageType: 'localStorage', // or 'sessionStorage'
// Enable automatic token refresh (default: false)
autoRefresh: true,
// Time before token expiry to trigger refresh (default: 5 minutes)
refreshThreshold: 5 * 60 * 1000,
// Token refresh handler
onTokenRefresh: async (tokens) => {
const response = await fetch('/api/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: tokens.refreshToken }),
})
return response.json() // Should return new { accessToken, refreshToken }
},
// Logout handler
onLogout: async () => {
await fetch('/api/logout', { method: 'POST' })
},
// Error handler
onError: (error) => {
console.error('Auth error:', error)
},
})
User Object
The user object represents the authenticated user:
interface User {
id: string
email: string
name?: string
roles?: string[] // For RBAC
permissions?: string[] // For RBAC
metadata?: Record<string, unknown> // Custom fields
}
Token Pair
Authentication tokens consist of an access token and refresh token:
interface TokenPair {
accessToken: string // Short-lived token for API requests
refreshToken: string // Long-lived token for getting new access tokens
}
useAuth Hook
The useAuth hook provides access to authentication state and methods in your React components.
Basic Usage
import { useAuth } from '@mycuppa/auth'
function UserProfile() {
const { user, isAuthenticated, logout } = useAuth(authManager)
if (!isAuthenticated) {
return <div>Please log in</div>
}
return (
<div>
<h1>Welcome, {user.name}!</h1>
<p>Email: {user.email}</p>
<button onClick={logout}>Logout</button>
</div>
)
}
Authentication State
function AppHeader() {
const {
user,
isAuthenticated,
isLoading,
error,
} = useAuth(authManager)
return (
<header>
{isLoading && <span>Loading...</span>}
{error && <span>Error: {error.message}</span>}
{isAuthenticated ? (
<span>Hello, {user.name}</span>
) : (
<a href="/login">Login</a>
)}
</header>
)
}
Login & Logout
Login
function LoginPage() {
const { login, isLoading, error } = useAuth(authManager)
const handleLogin = async (credentials) => {
try {
// Call your authentication API
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials),
})
if (!response.ok) {
throw new Error('Login failed')
}
const data = await response.json()
// Login with user data and tokens
await login(
{
id: data.user.id,
email: data.user.email,
name: data.user.name,
roles: data.user.roles,
},
{
accessToken: data.accessToken,
refreshToken: data.refreshToken,
}
)
// Navigate to dashboard or home
} catch (error) {
console.error('Login error:', error)
}
}
return (
<form onSubmit={(e) => {
e.preventDefault()
const formData = new FormData(e.target)
handleLogin({
email: formData.get('email'),
password: formData.get('password'),
})
}}>
<input name="email" type="email" placeholder="Email" required />
<input name="password" type="password" placeholder="Password" required />
<button type="submit" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Login'}
</button>
{error && <div className="error">{error.message}</div>}
</form>
)
}
Logout
function LogoutButton() {
const { logout, isLoading } = useAuth(authManager)
const handleLogout = async () => {
try {
await logout()
// Navigate to login page
} catch (error) {
console.error('Logout error:', error)
}
}
return (
<button onClick={handleLogout} disabled={isLoading}>
{isLoading ? 'Logging out...' : 'Logout'}
</button>
)
}
Token Management
Access Tokens
Use access tokens for authenticated API requests:
function Dashboard() {
const { getAccessToken } = useAuth(authManager)
const fetchData = async () => {
const token = getAccessToken()
const response = await fetch('/api/dashboard', {
headers: {
'Authorization': `Bearer ${token}`,
},
})
return response.json()
}
// Use with @mycuppa/data
const { data } = useQuery({
queryKey: 'dashboard',
queryFn: fetchData,
})
return <div>{/* Render dashboard */}</div>
}
Automatic Token Refresh
When autoRefresh is enabled, tokens are automatically refreshed before expiration:
const authManager = createAuthManager({
autoRefresh: true,
refreshThreshold: 5 * 60 * 1000, // 5 minutes before expiry
onTokenRefresh: async (currentTokens) => {
// Call your token refresh endpoint
const response = await fetch('/api/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${currentTokens.refreshToken}`,
},
})
if (!response.ok) {
throw new Error('Token refresh failed')
}
const { accessToken, refreshToken } = await response.json()
return { accessToken, refreshToken }
},
})
Manual Token Refresh
function RefreshButton() {
const { refreshTokens, isLoading } = useAuth(authManager)
const handleRefresh = async () => {
try {
const newTokens = await refreshTokens()
console.log('Tokens refreshed:', newTokens)
} catch (error) {
console.error('Refresh failed:', error)
}
}
return (
<button onClick={handleRefresh} disabled={isLoading}>
{isLoading ? 'Refreshing...' : 'Refresh Tokens'}
</button>
)
}
Role-Based Access Control (RBAC)
Check Roles
function AdminPanel() {
const { hasRole, hasAnyRole, hasAllRoles } = useAuth(authManager)
// Check single role
if (!hasRole('admin')) {
return <div>Access denied</div>
}
// Check if user has any of the roles
const canView = hasAnyRole(['admin', 'moderator'])
// Check if user has all roles
const isSuperAdmin = hasAllRoles(['admin', 'super'])
return (
<div>
<h1>Admin Panel</h1>
{canView && <section>Moderation Tools</section>}
{isSuperAdmin && <section>Super Admin Tools</section>}
</div>
)
}
Check Permissions
function DocumentEditor() {
const { hasPermission, hasAnyPermission, hasAllPermissions } = useAuth(authManager)
// Check single permission
const canEdit = hasPermission('documents:edit')
// Check if user has any permission
const canView = hasAnyPermission(['documents:view', 'documents:edit'])
// Check if user has all permissions
const canPublish = hasAllPermissions(['documents:edit', 'documents:publish'])
return (
<div>
{canView && <DocumentViewer />}
{canEdit && <DocumentEditor />}
{canPublish && <button>Publish</button>}
</div>
)
}
Conditional Rendering
function FeatureList() {
const { user, hasRole } = useAuth(authManager)
return (
<div>
<h2>Features</h2>
<ul>
<li>Dashboard (All users)</li>
{hasRole('admin') && <li>Admin Panel</li>}
{hasRole('moderator') && <li>Content Moderation</li>}
{user?.roles?.includes('super') && <li>System Settings</li>}
</ul>
</div>
)
}
Protected Routes
Route Protection with useAuth
import { useAuth } from '@mycuppa/auth'
import { useRouter } from '@mycuppa/router'
function ProtectedRoute({ children, requiredRoles, requiredPermissions }) {
const { isAuthenticated, canAccessRoute } = useAuth(authManager)
const { navigate } = useRouter()
const canAccess = canAccessRoute({
requireAuth: true,
requiredRoles,
requiredPermissions,
})
if (!isAuthenticated) {
navigate('/login')
return null
}
if (!canAccess) {
return <div>Access denied. Insufficient permissions.</div>
}
return <>{children}</>
}
// Usage
function App() {
return (
<div>
{/* Public route */}
<Route path="/" component={Home} />
{/* Protected route - requires authentication */}
<Route
path="/dashboard"
component={() => (
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
)}
/>
{/* Protected route - requires admin role */}
<Route
path="/admin"
component={() => (
<ProtectedRoute requiredRoles={['admin']}>
<AdminPanel />
</ProtectedRoute>
)}
/>
{/* Protected route - requires specific permissions */}
<Route
path="/editor"
component={() => (
<ProtectedRoute requiredPermissions={['documents:edit']}>
<Editor />
</ProtectedRoute>
)}
/>
</div>
)
}
Route Guards
function RouteGuard({ config, children }) {
const { isAuthenticated, canAccessRoute } = useAuth(authManager)
const { navigate } = useRouter()
useEffect(() => {
if (!isAuthenticated && config.requireAuth) {
navigate(config.redirectTo || '/login')
}
}, [isAuthenticated])
const canAccess = canAccessRoute(config)
if (!canAccess) {
return <div>Access denied</div>
}
return <>{children}</>
}
// Usage
<RouteGuard
config={{
requireAuth: true,
requiredRoles: ['admin'],
redirectTo: '/login',
}}
>
<AdminPanel />
</RouteGuard>
Session Persistence
Sessions are automatically persisted to storage (localStorage or sessionStorage):
// Configure storage
const authManager = createAuthManager({
storageKey: 'my_app_auth',
// localStorage - persists across browser sessions
storageType: 'localStorage',
// sessionStorage - cleared when tab is closed
// storageType: 'sessionStorage',
})
// Session is automatically restored on page reload
// If tokens exist, user is marked as authenticated
Session Restoration
function App() {
const { isAuthenticated, user } = useAuth(authManager)
const [isCheckingSession, setIsCheckingSession] = useState(true)
useEffect(() => {
const restoreSession = async () => {
if (isAuthenticated && !user) {
// Tokens exist but no user data
// Fetch user data from server
try {
const token = authManager.getAccessToken()
const response = await fetch('/api/me', {
headers: { 'Authorization': `Bearer ${token}` },
})
const userData = await response.json()
// Update user in auth manager
authManager.updateUser(userData)
} catch (error) {
// Token is invalid, logout
await authManager.logout()
}
}
setIsCheckingSession(false)
}
restoreSession()
}, [isAuthenticated, user])
if (isCheckingSession) {
return <div>Loading...</div>
}
return <div>{/* Your app */}</div>
}
Update User Data
function ProfileEditor() {
const { user, updateUser } = useAuth(authManager)
const [name, setName] = useState(user?.name || '')
const handleSave = async () => {
// Update on server
await fetch('/api/profile', {
method: 'PATCH',
body: JSON.stringify({ name }),
})
// Update in auth manager
updateUser({ name })
}
return (
<div>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
/>
<button onClick={handleSave}>Save</button>
</div>
)
}
Integration with API Requests
Create an HTTP Client
import { createAuthManager } from '@mycuppa/auth'
class ApiClient {
constructor(authManager) {
this.authManager = authManager
}
async request(url, options = {}) {
const token = this.authManager.getAccessToken()
const response = await fetch(url, {
...options,
headers: {
...options.headers,
'Authorization': token ? `Bearer ${token}` : '',
'Content-Type': 'application/json',
},
})
// Handle 401 Unauthorized
if (response.status === 401) {
// Try to refresh token
const newTokens = await this.authManager.refreshTokens()
if (newTokens) {
// Retry request with new token
return this.request(url, options)
}
// Refresh failed, logout
await this.authManager.logout()
throw new Error('Authentication failed')
}
return response
}
get(url) {
return this.request(url, { method: 'GET' })
}
post(url, data) {
return this.request(url, {
method: 'POST',
body: JSON.stringify(data),
})
}
put(url, data) {
return this.request(url, {
method: 'PUT',
body: JSON.stringify(data),
})
}
delete(url) {
return this.request(url, { method: 'DELETE' })
}
}
// Create API client
const api = new ApiClient(authManager)
// Use it
await api.get('/api/users')
await api.post('/api/users', { name: 'John' })
Integration with @mycuppa/data
import { useQuery } from '@mycuppa/data'
import { useAuth } from '@mycuppa/auth'
function UserList() {
const { getAccessToken, isAuthenticated } = useAuth(authManager)
const { data, isLoading, error } = useQuery({
queryKey: 'users',
queryFn: async () => {
const token = getAccessToken()
const response = await fetch('/api/users', {
headers: {
'Authorization': `Bearer ${token}`,
},
})
return response.json()
},
enabled: isAuthenticated, // Only fetch when authenticated
})
if (!isAuthenticated) return <div>Please log in</div>
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}
Best Practices
-
Create AuthManager once - Initialize at app root, share across components
// In your app.tsx or index.tsx const authManager = createAuthManager({ /* config */ }) -
Enable auto-refresh - Keep users logged in seamlessly
autoRefresh: true -
Use RBAC for authorization - Check roles and permissions, not user properties
// Good if (hasRole('admin')) { /* ... */ } // Avoid if (user.email === 'admin@example.com') { /* ... */ } -
Handle token expiration - Implement proper error handling
onError: (error) => { if (error.message.includes('token')) { navigate('/login') } } -
Protect sensitive routes - Use route guards for admin pages
<ProtectedRoute requiredRoles={['admin']}> <AdminPanel /> </ProtectedRoute> -
Store minimal data in tokens - Keep tokens small and fast
// Good: ID and basic info { id: '123', email: 'user@example.com', roles: ['user'] } // Avoid: Large objects { id: '123', settings: { /* huge object */ }, history: [...] } -
Use TypeScript - Get full type safety
interface MyUser extends User { customField: string } const { user } = useAuth<MyUser>(authManager)
API Reference
createAuthManager(config?)
Creates a new AuthManager instance.
Parameters:
storageKey?: string- Storage key (default: 'cuppa_auth_tokens')storageType?: 'localStorage' | 'sessionStorage'- Storage type (default: 'localStorage')autoRefresh?: boolean- Enable auto-refresh (default: false)refreshThreshold?: number- Refresh threshold in ms (default: 300000)onTokenRefresh?: (tokens) => Promise<TokenPair>- Refresh handleronLogout?: () => void | Promise<void>- Logout handleronError?: (error) => void- Error handler
Returns: AuthManager
useAuth(authManager)
Hook for accessing auth state and methods.
Parameters:
authManager: AuthManager- The auth manager instance
Returns:
user: User | null- Current userisAuthenticated: boolean- Authentication statusisLoading: boolean- Loading stateerror: Error | null- Error statelogin: (user, tokens) => Promise<void>- Login functionlogout: () => Promise<void>- Logout functionrefreshTokens: () => Promise<TokenPair | null>- Refresh functionupdateUser: (updates) => void- Update user functiongetAccessToken: () => string | null- Get access tokenhasRole: (role) => boolean- Check rolehasAnyRole: (roles) => boolean- Check any rolehasAllRoles: (roles) => boolean- Check all roleshasPermission: (permission) => boolean- Check permissionhasAnyPermission: (permissions) => boolean- Check any permissionhasAllPermissions: (permissions) => boolean- Check all permissionscanAccessRoute: (config) => boolean- Check route access
AuthManager Methods
login(user, tokens)- Set user and tokenslogout()- Clear user and tokensrefreshTokens()- Refresh access tokengetTokens()- Get current tokensgetAccessToken()- Get access tokenupdateUser(updates)- Update user datahasRole(role)- Check single rolehasAnyRole(roles)- Check any rolehasAllRoles(roles)- Check all roleshasPermission(permission)- Check single permissionhasAnyPermission(permissions)- Check any permissionhasAllPermissions(permissions)- Check all permissionscanAccessRoute(config)- Check route accessdestroy()- Clean up manager
Comparison to Traditional Approach
Traditional approach (PHP sessions):
// Server-side session management
session_start();
$_SESSION['user_id'] = $user->id;
// Check authentication
if (!isset($_SESSION['user_id'])) {
header('Location: /login');
exit;
}
// Check roles
if (!in_array('admin', $_SESSION['roles'])) {
die('Access denied');
}
With @mycuppa/auth:
// Client-side token management, automatic persistence
const { isAuthenticated, hasRole } = useAuth(authManager)
// Declarative authentication check
if (!isAuthenticated) return <Navigate to="/login" />
// Declarative authorization check
if (!hasRole('admin')) return <AccessDenied />
The Cuppa approach provides a modern, client-side authentication system with automatic token management, seamless refresh, and declarative access control that integrates perfectly with React.