Skip to main content
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:
  1. Client State Management (authStore.ts) - Zustand store with persistence
  2. Server Authentication Service (auth.service.ts) - User registration and login logic
  3. 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

1

User Registration

User submits registration form with name, email, and password. Server validates data, hashes password, and creates user account.
2

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).
3

Client Storage

Client receives user data and token, stores them in Zustand state, which automatically persists to localStorage.
4

Authenticated Requests

For protected routes, client includes token in Authorization header as “Bearer ”.
5

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:
PropertyTypeDescription
userUser | nullCurrently authenticated user
tokenstring | nullJWT authentication token
isAuthenticatedbooleanWhether user is logged in
isLoadingbooleanLoading state during auth operations
errorstring | nullError message from last failed operation
login(email, password) => Promise<boolean>Login with credentials
register(name, email, password) => Promise<boolean>Register new account
logout() => voidClear authentication state
clearError() => voidClear error message

User Object

interface User {
  id: string;
  nombre: string;
  email: string;
}

Implementation Examples

Registration Form

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

Login 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.
  1. Use HTTPS in production - Always transmit tokens over secure connections
  2. Set appropriate token expiration - Balance security with user experience (7 days is common)
  3. Validate user input - Use proper validation middleware on all auth endpoints
  4. Hash passwords - The User model automatically hashes passwords with bcrypt
  5. Handle token expiration - Implement token refresh or require re-login when tokens expire
  6. Clear tokens on logout - Always clear localStorage on explicit logout
  7. 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

Build docs developers (and LLMs) love