Skip to main content
The authentication system provides secure user login, registration, and session management using Better Auth, a modern authentication library for React applications.

Overview

Authentication features include:
  • Email/password authentication
  • User registration with email verification
  • Persistent sessions with cookies
  • Password reset functionality
  • Protected routes and components
  • Real-time session refresh
  • User favorites management

Authentication Context

The AuthContext provides authentication state and methods throughout the application. Location: src/contexts/AuthContext.tsx

Context Structure

AuthContext Interface
interface AuthContextType {
  // Current user state
  user: User | null;
  loading: boolean;
  error: string | null;
  favoritePropertyIds: string[];
  refreshingSession: boolean;

  // Authentication methods
  signIn: (email: string, password: string) => Promise<void>;
  signUp: (email: string, password: string, name: string) => Promise<void>;
  signOut: () => Promise<void>;
  refreshSession: () => Promise<User | null>;

  // Favorites management
  addToFavorites: (propertyId: string) => Promise<void>;
  removeFromFavorites: (propertyId: string) => Promise<void>;
  isFavorite: (propertyId: string) => boolean;

  // Password reset
  requestPasswordReset: (email: string) => Promise<void>;
  resetPassword: (token: string, newPassword: string) => Promise<void>;
}

Using the Auth Context

Using Auth in Components
import { useAuth } from "../contexts/AuthContext";

const MyComponent = () => {
  const { user, signIn, signOut, loading } = useAuth();

  if (loading) {
    return <div>Loading...</div>;
  }

  if (!user) {
    return (
      <button onClick={() => signIn("[email protected]", "password")}>
        Sign In
      </button>
    );
  }

  return (
    <div>
      <p>Welcome, {user.name}!</p>
      <button onClick={signOut}>Sign Out</button>
    </div>
  );
};

Authentication Methods

Sign In

Authenticate users with email and password.
1

Call signIn Method

Sign In Implementation
const signIn = async (email: string, password: string) => {
  setAuthState((prev) => ({
    ...prev,
    loading: true,
    error: null,
  }));

  try {
    const response = await authClient.signIn.email({
      email,
      password,
    });

    const { error } = response;

    if (error) {
      throw error;
    }

    // Refresh session to load user data
    await refreshSession();
  } catch (error: unknown) {
    const errorMessage = error instanceof Error 
      ? error.message 
      : "Error al iniciar sesión";
    const translatedError = translateAuthError(errorMessage);

    setAuthState((prev) => ({
      ...prev,
      loading: false,
      error: translatedError,
    }));

    throw new Error(translatedError);
  }
};
2

Handle Response

On successful authentication, the session is refreshed to load user data and favorites.
// Refresh session after sign in
await refreshSession();
3

Error Handling

Errors are translated to user-friendly Spanish messages.
const translatedError = translateAuthError(errorMessage);
// "Invalid credentials" → "Credenciales inválidas"
// "User not found" → "Usuario no encontrado"

Sign Up

Register new users with email, password, and name.
Sign Up Implementation
const signUp = async (email: string, password: string, name: string) => {
  setAuthState((prev) => ({
    ...prev,
    loading: true,
    error: null,
  }));

  try {
    const response = await authClient.signUp.email({
      email,
      password,
      name,
      callbackURL: "/",
    });

    const { error } = response;

    if (error) {
      throw error;
    }

    // Note: Session is NOT refreshed here
    // User must verify email first
  } catch (error: unknown) {
    const errorMessage = error instanceof Error 
      ? error.message 
      : "Error al crear cuenta";
    const translatedError = translateAuthError(errorMessage);

    setAuthState((prev) => ({
      ...prev,
      loading: false,
      error: translatedError,
    }));

    throw new Error(translatedError);
  }
};
After registration, users may need to verify their email before signing in, depending on your Better Auth configuration.

Sign Out

End the user session and clear authentication state.
Sign Out Implementation
const signOut = async () => {
  setAuthState((prev) => ({ ...prev, loading: true }));

  try {
    await authClient.signOut();
    
    setAuthState({
      user: null,
      loading: false,
      error: null,
      favoritePropertyIds: [],
      refreshingSession: false,
    });
  } catch {
    setAuthState((prev) => ({
      ...prev,
      loading: false,
      error: "Error al cerrar sesión",
    }));
  }
};

Session Refresh

Reload user data and verify session validity.
Refresh Session
const refreshSession = useCallback(async () => {
  // Prevent multiple simultaneous refresh calls
  if (authState.refreshingSession) {
    return authState.user;
  }

  setAuthState((prev) => ({ ...prev, refreshingSession: true }));

  try {
    const { data } = await authClient.getSession();

    if (data?.user) {
      const user = data.user as unknown as User;
      
      // Fetch user favorites
      try {
        const favoritesResponse = await api.users.getFavorites();
        const favoriteIds = favoritesResponse.data?.map(
          (property: Property) => property.id
        ) || [];

        setAuthState({
          user: user,
          loading: false,
          error: null,
          favoritePropertyIds: favoriteIds,
          refreshingSession: false,
        });
      } catch {
        // Set user even if favorites fail
        setAuthState({
          user: user,
          loading: false,
          error: null,
          favoritePropertyIds: [],
          refreshingSession: false,
        });
      }

      return user;
    } else {
      setAuthState({
        user: null,
        loading: false,
        error: null,
        favoritePropertyIds: [],
        refreshingSession: false,
      });
      return null;
    }
  } catch {
    setAuthState({
      user: null,
      loading: false,
      error: null,
      favoritePropertyIds: [],
      refreshingSession: false,
    });
    return null;
  }
}, [authState.refreshingSession, authState.user]);

Password Reset

Allow users to reset forgotten passwords.
1

Request Password Reset

User provides their email to receive a reset link.
Request Reset
const requestPasswordReset = async (email: string) => {
  try {
    const response = await authClient.forgetPassword({
      email,
      redirectTo: `${window.location.origin}/reset-password`,
    });

    const { error } = response;

    if (error) {
      throw error;
    }
  } catch (error: unknown) {
    const errorMessage = error instanceof Error 
      ? error.message 
      : "Error al solicitar restablecimiento de contraseña";
    const translatedError = translateAuthError(errorMessage);
    throw new Error(translatedError);
  }
};
2

Reset Password

User clicks the link in their email and provides a new password.
Reset Password
const resetPassword = async (token: string, newPassword: string) => {
  setAuthState((prev) => ({
    ...prev,
    loading: true,
    error: null,
  }));

  try {
    const { error } = await authClient.resetPassword({
      newPassword,
      token,
    });

    if (error) {
      throw error;
    }

    // User needs to sign in manually after reset
  } catch (error: unknown) {
    const errorMessage = error instanceof Error 
      ? error.message 
      : "Error al restablecer contraseña";
    const translatedError = translateAuthError(errorMessage);

    setAuthState((prev) => ({
      ...prev,
      loading: false,
      error: translatedError,
    }));

    throw new Error(translatedError);
  }
};
After resetting their password, users must sign in manually. Better Auth does not automatically create a session after password reset.

API Integration

Authentication uses both Better Auth client and custom API endpoints.

Better Auth Client

Better Auth Setup
// src/lib/auth-client.ts
import { createAuthClient } from "better-auth/react";

export const authClient = createAuthClient({
  baseURL: API_BASE,
});

User API Endpoints

User API Methods
// src/lib/api.ts
users: {
  getProfile: () => apiCall("/users/profile"),

  updateProfile: (data: any) =>
    apiCall("/users/profile", {
      method: "PUT",
      body: JSON.stringify(data),
    }),

  getFavorites: () => apiCall("/users/favorites"),
}

Protected Routes

Protect routes that require authentication.
Protected Route Component
import { useAuth } from "../contexts/AuthContext";
import { Navigate } from "@tanstack/react-router";

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

  if (loading) {
    return <div>Loading...</div>;
  }

  if (!user) {
    return <Navigate to="/auth" />;
  }

  return children;
};

// Usage
<Route path="/favorites" component={ProtectedRoute}>
  <FavoritesPage />
</Route>

User Roles

The system supports different user roles for access control.
Default role for all registered users.Permissions:
  • Browse properties
  • Save favorites
  • Contact property owners
  • Update profile
Role Check
const isAdmin = user?.role === "admin";

if (isAdmin) {
  // Show admin controls
}

Database Schema

The authentication system uses the following database tables:
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    name VARCHAR(255),
    image TEXT,
    email_verified TIMESTAMP,
    role VARCHAR(50) DEFAULT 'user' CHECK (role IN ('user', 'admin')),
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

Session Management

1

Session Initialization

On app load, verify if a valid session exists.
useEffect(() => {
  const initSession = async () => {
    try {
      const timeoutPromise = new Promise((_, reject) => {
        setTimeout(() => reject(new Error("Session timeout")), 5000);
      });

      await Promise.race([refreshSession(), timeoutPromise]);
    } catch {
      setAuthState((prev) => ({
        ...prev,
        loading: false,
        user: null,
      }));
    }
  };

  initSession();
}, []);
2

Session Persistence

Sessions are stored in HTTP-only cookies for security.
const apiCall = async (endpoint: string, options: RequestInit = {}) => {
  const response = await fetch(url, {
    credentials: "include", // Send cookies
    ...options,
  });
  
  return response.json();
};
3

Session Expiration

Sessions automatically expire based on the configured timeout. Users must sign in again after expiration.

Error Handling

Authentication errors are translated to user-friendly messages.
Error Translation
// src/utils/authErrorTranslator.ts
export const translateAuthError = (error: string): string => {
  const errorMap: Record<string, string> = {
    "Invalid credentials": "Credenciales inválidas",
    "User not found": "Usuario no encontrado",
    "Email already exists": "El correo ya está registrado",
    "Invalid token": "Token inválido o expirado",
    "Session expired": "Sesión expirada",
  };

  return errorMap[error] || "Error de autenticación";
};

Best Practices

  • Store sessions in HTTP-only cookies
  • Use HTTPS in production
  • Implement CSRF protection
  • Validate email addresses
  • Enforce strong passwords
  • Rate limit authentication attempts
  • Show loading states during authentication
  • Provide clear error messages
  • Auto-clear errors after timeout
  • Remember user preferences
  • Smooth transitions between auth states
  • Prevent multiple simultaneous session refreshes
  • Cache user data appropriately
  • Lazy load protected components
  • Optimize session validation

Favorites

Manage favorite properties (requires authentication)

Property Management

Create and edit properties (admin only)

Build docs developers (and LLMs) love