Skip to main content
Tambo360 uses React Context to manage authentication state globally across the application. The AuthContext provides user information, token management, and authentication status to all components.

Overview

The authentication system implements:

Session Management

Automatic session validation on app load

Token Storage

HTTP-only cookies for secure token storage

Global State

User data accessible throughout the app

Protected Routes

Automatic redirects for unauthenticated users

File Location

apps/frontend/src/context/AuthContext.tsx

Type Definitions

User Type

export interface User {
  id: string
  correo: string
  nombre: string
  apellido: string
  establecimientos: Establecimiento[]
  rol: string
  verificado: boolean
  createdAt: string
  updatedAt: string
}

interface Establecimiento {
  idEstablecimiento: string
  nombre: string
  direccion: string
  localidad: string
  provincia: string
}

AuthState Interface

interface AuthState {
  user: User | null
  token: string | null
  isAuthenticated: boolean
  loading: boolean
  error: string | null
}

AuthContext Type

interface AuthContextType extends AuthState {
  setToken: (token: string | null) => void
  login: ({ user, token }: { user: User; token: string }) => void
  setUser: (user: User | null) => void
  logout: () => void
  setLoading: (loading: boolean) => void
  setError: (error: string | null) => void
}

Implementation

AuthContext Provider

AuthContext.tsx
import React, { createContext, useContext, useEffect, useState } from 'react'
import { AuthState, User } from '../types'
import Cookies from 'js-cookie'
import { api } from '@/src/services/api'
import { useLogout } from '@/src/hooks/auth/useLogout'

interface AuthContextType extends AuthState {
  setToken: (token: string | null) => void
  login: ({ user, token }: { user: User; token: string }) => void
  setUser: (user: User | null) => void
  logout: () => void
  setLoading: (loading: boolean) => void
  setError: (error: string | null) => void
}

const AuthContext = createContext<AuthContextType | undefined>(undefined)

export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({
  children,
}) => {
  const [user, setUser] = useState<User | null>(null)
  const [token, setToken] = useState<string | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)
  const { mutateAsync } = useLogout()

  const isAuthenticated = !!user

  // Fetch current user session on mount
  const fetchSession = async () => {
    setLoading(true)
    try {
      const res = await api.get('/auth/me')
      if (res?.data.data) {
        setUser(res.data.data)
      } else {
        setUser(null)
      }
    } catch {
      setUser(null)
    } finally {
      setLoading(false)
    }
  }

  useEffect(() => {
    fetchSession()
  }, [])

  const login = async ({ user, token }: { user: User; token: string }) => {
    setUser(user)
    setToken(token)
    setError(null)
  }

  const logout = async () => {
    try {
      setUser(null)
      setToken(null)
      setError(null)
      await mutateAsync()
      Cookies.remove('token')
    } catch (error) {
      console.error(error)
    }
  }

  return (
    <AuthContext.Provider
      value={{
        user,
        setUser,
        token,
        setToken,
        isAuthenticated,
        loading,
        error,
        login,
        logout,
        setLoading,
        setError,
      }}
    >
      {children}
    </AuthContext.Provider>
  )
}

export const useAuth = (): AuthContextType => {
  const context = useContext(AuthContext)
  if (!context) {
    throw new Error('useAuth must be used within an AuthProvider')
  }
  return context
}

Usage

Wrapping the App

The AuthProvider wraps the entire application in App.tsx:
App.tsx
import { AuthProvider } from './src/context/AuthContext'
import { QueryClientProvider } from '@tanstack/react-query'

export const App: React.FC = () => {
  return (
    <QueryClientProvider client={queryClient}>
      <AuthProvider>
        <Router>
          <AppRoutes />
        </Router>
      </AuthProvider>
    </QueryClientProvider>
  )
}
Location: apps/frontend/App.tsx:27

Using the useAuth Hook

Access authentication state and methods in any component:
import { useAuth } from '@/src/context/AuthContext'

const MyComponent = () => {
  const { user, isAuthenticated, loading, login, logout } = useAuth()

  if (loading) {
    return <LoadingSpinner />
  }

  if (!isAuthenticated) {
    return <Navigate to="/login" />
  }

  return (
    <div>
      <p>Welcome, {user?.nombre}!</p>
      <button onClick={logout}>Logout</button>
    </div>
  )
}

Example: Login Flow

Login.tsx
import { useAuth } from '@/src/context/AuthContext'
import { useLogin } from '@/src/hooks/auth/useLogin'
import { useNavigate } from 'react-router-dom'

const Login: React.FC = () => {
  const { mutateAsync } = useLogin()
  const { login } = useAuth()
  const navigate = useNavigate()

  const onSubmit = handleSubmit(async (data) => {
    try {
      const response = await mutateAsync(data)
      
      // Update auth context
      login({ 
        token: response.data.token, 
        user: response.data.user 
      })
      
      // Redirect to dashboard
      navigate('/dashboard')
    } catch (err) {
      console.error('Login failed:', err)
    }
  })

  return <form onSubmit={onSubmit}>...</form>
}
Location: apps/frontend/src/pages/Login.tsx:69

Example: Accessing User Data

Dashboard.tsx
import { useAuth } from '@/src/context/AuthContext'

const Dashboard = () => {
  const { user } = useAuth()

  return (
    <div>
      <p className="text-muted-foreground text-xs sm:text-sm">
        Dashboard / {user.establecimientos[0].nombre}
      </p>
      <h1 className="text-2xl sm:text-3xl font-bold">
        Bienvenido, {user.nombre} {user.apellido}
      </h1>
    </div>
  )
}
Location: apps/frontend/src/pages/Dashboard.tsx:25

Example: Logout

Navbar.tsx
import { useAuth } from '@/src/context/AuthContext'
import { useNavigate } from 'react-router-dom'

const Navbar = () => {
  const { logout } = useAuth()
  const navigate = useNavigate()

  const handleLogout = async () => {
    await logout()
    navigate('/login')
  }

  return (
    <Button onClick={handleLogout}>
      Cerrar sesión
    </Button>
  )
}

Session Management

Automatic Session Restoration

On app load, AuthContext automatically attempts to restore the user session:
const fetchSession = async () => {
  setLoading(true)
  try {
    const res = await api.get('/auth/me')
    if (res?.data.data) {
      setUser(res.data.data)  // Session restored
    } else {
      setUser(null)  // No active session
    }
  } catch {
    setUser(null)  // Session invalid
  } finally {
    setLoading(false)
  }
}

useEffect(() => {
  fetchSession()
}, [])

Loading State

While checking authentication, the app shows a loading spinner:
AppRoutes.tsx
import { useAuth } from '../context/AuthContext'
import LoadingSpinner from '../components/layout/LoadingSpinner'

export const AppRoutes = () => {
  const { loading } = useAuth()

  if (loading) return <LoadingSpinner />

  return <Routes>...</Routes>
}
Location: apps/frontend/src/routes/AppRoutes.tsx:21

Protected Routes

The ProtectedRoute component uses useAuth to guard routes:
ProtectedRoute.tsx
import { Navigate } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
import Loading from '@/src/components/layout/Loading'

interface ProtectedRouteProps {
  children: React.ReactNode
}

const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
  const { user, loading } = useAuth()

  if (loading) {
    return <Loading />
  }

  if (!user) {
    return <Navigate to="/login" replace />
  }

  return <>{children}</>
}

export default ProtectedRoute
Location: apps/frontend/src/routes/ProtectedRoute.tsx

Usage in Routes

AppRoutes.tsx
<Route
  element={
    <ProtectedRoute>
      <Layout />
    </ProtectedRoute>
  }
>
  <Route path="/dashboard" element={<Dashboard />} />
  <Route path="/produccion" element={<Produccion />} />
</Route>

Authentication Hooks

Tambo360 provides custom hooks for auth operations:

useLogin

useLogin.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { loginUser } from '@/src/utils/api/auth.api'
import { queryKeys } from '@/src/utils/queryKeys'

export function useLogin() {
  const queryClient = useQueryClient()
  return useMutation<
    AxiosResponse<{ user: User; token: string }>,
    AxiosError<{ message: string }>,
    LoginData
  >({
    mutationFn: async (values: LoginData) => {
      const { data } = await loginUser(values)
      return data
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: queryKeys.auth.currentUser })
    },
  })
}
Location: apps/frontend/src/hooks/auth/useLogin.ts

useLogout

Location: apps/frontend/src/hooks/auth/useLogout.ts

Other Auth Hooks

  • useRegister - Register new users
  • useForgotPassword - Request password reset
  • useResetPassword - Reset password with token
  • useVerifyEmail - Verify email address
  • useResendEmail - Resend verification email
All located in: apps/frontend/src/hooks/auth/

API Configuration

The authentication API uses Axios with interceptors:
api.ts
import axios from 'axios'
import { API_ENDPOINTS } from '@/src/constants/routes'

export const api = axios.create({
  baseURL: API_ENDPOINTS.BASE,
  withCredentials: true,  // Send cookies with requests
})

// Redirect to login on 401 errors
api.interceptors.response.use(
  (response) => response,
  (error) => {
    const isAuthMe = error.config?.url?.includes('/auth/me')
    const isLogout = error.config?.url?.includes('/auth/logout')

    if (error.response?.status === 401) {
      if (!isAuthMe && !isLogout && typeof window !== 'undefined') {
        window.location.href = '/login'
      }
    }
    return Promise.reject(error)
  }
)
Location: apps/frontend/src/services/api.ts Authentication tokens are stored in HTTP-only cookies:
import Cookies from 'js-cookie'

// Set cookie (usually done by backend)
Cookies.set('token', tokenValue, { 
  secure: true,
  sameSite: 'strict' 
})

// Remove cookie on logout
Cookies.remove('token')

// Read cookie (usually automatic with withCredentials)
const token = Cookies.get('token')
Never manually set authentication tokens in cookies from the frontend. The backend should set HTTP-only cookies to prevent XSS attacks.

Best Practices

Always check loading state

Show loading UI while loading is true to prevent flashes of wrong content

Use useAuth hook

Never access AuthContext directly; always use the useAuth() hook

Handle errors gracefully

Check for errors and display user-friendly messages

Secure token storage

Use HTTP-only cookies, never localStorage for auth tokens

Troubleshooting

”useAuth must be used within an AuthProvider” Error

This error occurs when a component uses useAuth() outside of the AuthProvider:
// ❌ Wrong
const root = ReactDOM.createRoot(document.getElementById('root')!)
root.render(<MyComponent />)  // No AuthProvider!

// ✅ Correct
root.render(
  <AuthProvider>
    <MyComponent />
  </AuthProvider>
)

Session Not Persisting

Ensure cookies are being sent:
// In api.ts
export const api = axios.create({
  baseURL: API_ENDPOINTS.BASE,
  withCredentials: true,  // Required!
})
And ensure backend sends Set-Cookie headers with proper flags:
Set-Cookie: token=...; HttpOnly; Secure; SameSite=Strict

Protected Routes

Learn how routes integrate with auth

Auth API Endpoints

Backend authentication endpoints

React Query

How auth hooks use TanStack Query

Login Page

Complete login form implementation

Build docs developers (and LLMs) love