The T1 Component Library features a complete authentication system built with JWT tokens, Zustand for state management, and server-side validation middleware.
Overview
The authentication system provides:
- JWT-based authentication with configurable expiration
- User registration and login with email/password
- Persistent sessions using localStorage
- Protected routes with authentication middleware
- Automatic token management with Zustand
- Password hashing with bcrypt
Architecture
The authentication system consists of three main components:
- Client State Management (
authStore.ts) - Zustand store with persistence
- Server Authentication Service (
auth.service.ts) - User registration and login logic
- Authentication Middleware (
auth.middleware.ts) - Token verification and route protection
Getting Started
Setup
The authentication store is automatically available throughout your app. No provider wrapper is needed thanks to Zustand:
import { useAuthStore } from './store/authStore';
function App() {
const { isAuthenticated, user } = useAuthStore();
return (
<div>
{isAuthenticated ? (
<p>Welcome, {user?.nombre}!</p>
) : (
<p>Please log in</p>
)}
</div>
);
}
Authentication Flow
User Registration
User submits registration form with name, email, and password. Server validates data, hashes password, and creates user account.
JWT Generation
Server generates a JWT token containing user ID and email, signed with a secret key and set to expire in 7 days (configurable).
Client Storage
Client receives user data and token, stores them in Zustand state, which automatically persists to localStorage.
Authenticated Requests
For protected routes, client includes token in Authorization header as “Bearer ”.
Token Verification
Server middleware verifies token signature and expiration, fetches user from database, and attaches user to request object.
Auth Store API
The useAuthStore hook provides the following:
| Property | Type | Description |
|---|
user | User | null | Currently authenticated user |
token | string | null | JWT authentication token |
isAuthenticated | boolean | Whether user is logged in |
isLoading | boolean | Loading state during auth operations |
error | string | null | Error message from last failed operation |
login | (email, password) => Promise<boolean> | Login with credentials |
register | (name, email, password) => Promise<boolean> | Register new account |
logout | () => void | Clear authentication state |
clearError | () => void | Clear error message |
User Object
interface User {
id: string;
nombre: string;
email: string;
}
Implementation Examples
import { useState } from 'react';
import { useAuthStore } from './store/authStore';
import { useRouter } from 'next/navigation';
function RegisterForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { register, isLoading, error } = useAuthStore();
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const success = await register(name, email, password);
if (success) {
router.push('/dashboard');
}
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
required
/>
<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
/>
{error && <p className="text-destructive">{error}</p>}
<button type="submit" disabled={isLoading}>
{isLoading ? 'Registering...' : 'Register'}
</button>
</form>
);
}
import { useState } from 'react';
import { useAuthStore } from './store/authStore';
import { useRouter } from 'next/navigation';
function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { login, isLoading, error, clearError } = useAuthStore();
const router = useRouter();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
clearError();
const success = await login(email, password);
if (success) {
router.push('/dashboard');
}
};
return (
<form onSubmit={handleSubmit}>
<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
/>
{error && <p className="text-destructive">{error}</p>}
<button type="submit" disabled={isLoading}>
{isLoading ? 'Logging in...' : 'Login'}
</button>
</form>
);
}
User Profile Display
import { useAuthStore } from './store/authStore';
import { useRouter } from 'next/navigation';
function UserProfile() {
const { user, logout } = useAuthStore();
const router = useRouter();
const handleLogout = () => {
logout();
router.push('/login');
};
if (!user) {
return null;
}
return (
<div>
<h2>Profile</h2>
<p>Name: {user.nombre}</p>
<p>Email: {user.email}</p>
<button onClick={handleLogout}>Logout</button>
</div>
);
}
Protected Route Component
import { useEffect } from 'react';
import { useAuthStore } from './store/authStore';
import { useRouter } from 'next/navigation';
function ProtectedRoute({ children }: { children: React.ReactNode }) {
const { isAuthenticated, isLoading } = useAuthStore();
const router = useRouter();
useEffect(() => {
if (!isLoading && !isAuthenticated) {
router.push('/login');
}
}, [isAuthenticated, isLoading, router]);
if (isLoading) {
return <div>Loading...</div>;
}
if (!isAuthenticated) {
return null;
}
return <>{children}</>;
}
export default ProtectedRoute;
Server-Side Implementation
JWT Token Generation
The server generates JWT tokens with user information:
// From auth.service.ts:25-38
const generateToken = (user: IUser): string => {
const jwtSecret = process.env.JWT_SECRET;
const jwtExpiresIn = process.env.JWT_EXPIRES_IN || '7d';
if (!jwtSecret) {
throw new Error('JWT_SECRET no está configurado');
}
return jwt.sign(
{ id: user._id, email: user.email },
jwtSecret,
{ expiresIn: jwtExpiresIn }
);
};
User Registration
// From auth.service.ts:40-61
register: async (data: RegisterData): Promise<AuthResponse> => {
const existingUser = await User.findOne({ email: data.email });
if (existingUser) {
const error = new Error('El email ya está registrado');
error.statusCode = 400;
throw error;
}
const user = await User.create(data);
const token = generateToken(user);
return {
user: {
id: user._id.toString(),
nombre: user.nombre,
email: user.email
},
token
};
}
User Login
// From auth.service.ts:63-90
login: async (data: LoginData): Promise<AuthResponse> => {
const user = await User.findOne({ email: data.email }).select('+password');
if (!user) {
const error = new Error('Credenciales inválidas');
error.statusCode = 401;
throw error;
}
const isMatch = await user.comparePassword(data.password);
if (!isMatch) {
const error = new Error('Credenciales inválidas');
error.statusCode = 401;
throw error;
}
const token = generateToken(user);
return {
user: {
id: user._id.toString(),
nombre: user.nombre,
email: user.email
},
token
};
}
Authentication Middleware
Protect server routes with the authentication middleware:
// From auth.middleware.ts:7-50
export const authMiddleware = async (
req: AuthRequest,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
res.status(401).json({
success: false,
message: 'Token de autenticación no proporcionado'
});
return;
}
const token = authHeader.split(' ')[1];
const jwtSecret = process.env.JWT_SECRET;
if (!jwtSecret) {
throw new Error('JWT_SECRET no está configurado');
}
const decoded = jwt.verify(token, jwtSecret) as JwtPayload;
const user = await User.findById(decoded.id);
if (!user) {
res.status(401).json({
success: false,
message: 'Usuario no encontrado'
});
return;
}
req.user = user;
next();
} catch (error) {
logger.error('Error de autenticación:', error);
res.status(401).json({
success: false,
message: 'Token inválido o expirado'
});
}
};
Using Middleware in Routes
import { Router } from 'express';
import { authMiddleware } from '../middlewares/auth.middleware';
const router = Router();
// Public route
router.get('/public', (req, res) => {
res.json({ message: 'This is public' });
});
// Protected route
router.get('/protected', authMiddleware, (req, res) => {
res.json({
message: 'This is protected',
user: req.user
});
});
export default router;
Making Authenticated Requests
API Client Setup
Create an API client that automatically includes the token:
import { useAuthStore } from './store/authStore';
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api';
export const api = {
get: async (endpoint: string) => {
const { token } = useAuthStore.getState();
const response = await fetch(`${API_URL}${endpoint}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
return response.json();
},
post: async (endpoint: string, data: any) => {
const { token } = useAuthStore.getState();
const response = await fetch(`${API_URL}${endpoint}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
return response.json();
}
};
Using the API Client
import { useState, useEffect } from 'react';
import { api } from './lib/api';
function UserDashboard() {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
const result = await api.get('/dashboard');
setData(result);
};
fetchData();
}, []);
return <div>{/* Render dashboard data */}</div>;
}
State Persistence
The auth store uses Zustand’s persistence middleware to save state to localStorage:
// From authStore.ts:127-136
persist(
(set) => ({ /* store implementation */ }),
{
name: 'auth-storage',
storage: createJSONStorage(() => localStorage),
partialize: (state) => ({
user: state.user,
token: state.token,
isAuthenticated: state.isAuthenticated,
}),
}
)
Only essential auth data (user, token, isAuthenticated) is persisted. Loading and error states are not saved to avoid showing stale states on page reload.
Environment Configuration
Set these environment variables for the authentication system:
Server (.env)
JWT_SECRET=your-secret-key-here
JWT_EXPIRES_IN=7d # Optional, defaults to 7 days
Client (.env.local)
NEXT_PUBLIC_API_URL=http://localhost:3001/api
Security Best Practices
Never expose JWT_SECRET - Keep it secure and use a strong, randomly generated value in production.
- Use HTTPS in production - Always transmit tokens over secure connections
- Set appropriate token expiration - Balance security with user experience (7 days is common)
- Validate user input - Use proper validation middleware on all auth endpoints
- Hash passwords - The User model automatically hashes passwords with bcrypt
- Handle token expiration - Implement token refresh or require re-login when tokens expire
- Clear tokens on logout - Always clear localStorage on explicit logout
- Don’t store sensitive data in tokens - JWT payloads are readable, only store user ID and email
Error Handling
Client-Side Errors
const { error, clearError } = useAuthStore();
// Display error
{error && (
<div className="alert alert-error">
{error}
<button onClick={clearError}>Dismiss</button>
</div>
)}
Server-Side Errors
The auth service throws errors with status codes:
// Email already exists
if (existingUser) {
const error = new Error('El email ya está registrado');
error.statusCode = 400;
throw error;
}
// Invalid credentials
if (!isMatch) {
const error = new Error('Credenciales inválidas');
error.statusCode = 401;
throw error;
}
Advanced Patterns
Role-Based Access Control
Extend the system with user roles:
interface User {
id: string;
nombre: string;
email: string;
role: 'user' | 'admin';
}
function AdminRoute({ children }: { children: React.ReactNode }) {
const { user, isAuthenticated } = useAuthStore();
const router = useRouter();
useEffect(() => {
if (!isAuthenticated || user?.role !== 'admin') {
router.push('/');
}
}, [isAuthenticated, user, router]);
return isAuthenticated && user?.role === 'admin' ? <>{children}</> : null;
}
Auto-Logout on Token Expiration
import { useEffect } from 'react';
import { useAuthStore } from './store/authStore';
import { jwtDecode } from 'jwt-decode';
function AuthMonitor() {
const { token, logout } = useAuthStore();
useEffect(() => {
if (!token) return;
const decoded = jwtDecode(token);
const expiresAt = decoded.exp * 1000; // Convert to milliseconds
const timeout = expiresAt - Date.now();
if (timeout <= 0) {
logout();
return;
}
const timer = setTimeout(() => {
logout();
alert('Your session has expired. Please log in again.');
}, timeout);
return () => clearTimeout(timer);
}, [token, logout]);
return null;
}
Source Code References
- Auth Store:
client/app/store/authStore.ts
- Auth Service:
server/src/services/auth.service.ts
- Auth Middleware:
server/src/middlewares/auth.middleware.ts
- User Model:
server/src/models/User.model.ts