Skip to main content

Overview

Horse Trust uses a secure authentication system built on JWT (JSON Web Tokens) and bcrypt password hashing. The platform supports two user roles: sellers and admins, with email and phone verification capabilities.

User Model

The authentication system is built around a comprehensive User model defined in server/src/models/User.ts:
const userSchema = new Schema<IUser>({
  email: {
    type: String,
    required: [true, "Email is required"],
    unique: true,
    lowercase: true,
    trim: true,
    match: [/^[^\s@]+@[^\s@]+\.[^\s@]+$/, "Invalid email format"],
  },
  password_hash: { type: String, required: true },
  role: { type: String, enum: ["admin", "seller"], required: true },
  full_name: { type: String, required: [true, "Full name is required"], trim: true },
  phone: {
    type: String,
    trim: true,
    match: [/^\+?[1-9][0-9]{7,14}$/, "Invalid phone format. Use international format: +5491112345678"],
  },
  is_email_verified: { type: Boolean, default: false },
  is_phone_verified: { type: Boolean, default: false },
  email_verification_token: { type: String },
  profile_picture_url: { type: String },
  seller_profile: { type: sellerProfileSchema, default: null },
  is_active: { type: Boolean, default: true },
  last_login: { type: Date },
});

Key Features

Password Security

Passwords are hashed using bcrypt with configurable salt rounds before storage

Email Validation

Built-in regex validation ensures proper email format

Phone Verification

International phone format validation with E.164 standard

Role-Based Access

Two-tier role system: sellers and admins

Password Hashing

Passwords are automatically hashed before saving using a pre-save middleware hook:
userSchema.pre("save", async function (next) {
  // Only hash when password_hash field is modified
  if (!this.isModified("password_hash")) return;
  const salt = await bcrypt.genSalt(Number(process.env.BCRYPT_SALT_ROUNDS) || 12);
  this.password_hash = await bcrypt.hash(this.password_hash, salt);
});
The system uses 12 salt rounds by default, configurable via the BCRYPT_SALT_ROUNDS environment variable.

Login Flow

Frontend Implementation

The login page (client/app/login/page.tsx) provides a clean, responsive interface:
const handleLogin = async (e: React.FormEvent) => {
  e.preventDefault();
  setIsLoading(true);
  setError(null);

  const response = await loginUser(email, password);

  if (!response.success) {
    setError(response.error);
    setIsLoading(false);
    return;
  }

  localStorage.setItem('horse_trust_token', response.data.token);
  localStorage.setItem('horse_trust_user', JSON.stringify(response.data.user));

  router.push('/dashboard');
};

Server Action

The loginUser server action (client/app/actions/auth.ts) handles authentication:
export async function loginUser(email: string, password: string) {
  try {
    const res = await apiFetch('/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    });

    const data = await res.json();

    if (!res.ok) {
      throw new Error(data.error || data.message || 'Error al validar credenciales');
    }
    
    const cookieStore = await cookies();
    cookieStore.set('horse_trust_token', data.token, { 
      httpOnly: true, 
      secure: process.env.NODE_ENV === 'production',
      maxAge: 60 * 60 * 24 * 7, // 7 days
      path: '/',
    });

    return { success: true, data };
  } catch (error: any) {
    return { success: false, error: error.message };
  }
}
Tokens are stored both in httpOnly cookies (server-side) and localStorage (client-side) for different use cases.

Registration Flow

Horse Trust implements a two-step registration process:

Step 1: Basic Account Creation

Users provide their basic information:
  • Full name
  • Email address
  • Phone number (international format)
  • Password (minimum 8 characters)
const handleRegister = async (e: React.FormEvent) => {
  e.preventDefault();
  setIsLoading(true);

  // 1. Create the account
  const response = await registerUser({
    full_name: formData.full_name,
    email: formData.email,
    phone: formData.phone,
    password: formData.password,
    role: 'seller'
  });

  if (!response.success) {
    throw new Error(response.error);
  }

  // 2. Auto-login after registration
  const loginResponse = await loginUser(formData.email, formData.password);
  
  if (loginResponse.success && loginResponse.data.token) {
    localStorage.setItem('horse_trust_token', loginResponse.data.token);
    localStorage.setItem('horse_trust_user', JSON.stringify(loginResponse.data.user));
  }
  
  setStep(2); // Move to verification step
};

Step 2: Identity Verification (Optional)

Sellers can verify their identity to gain trust badges:
  • Document number (DNI/Passport)
  • Selfie holding ID document
Users can skip verification and still access the marketplace, but verified sellers receive priority placement and trust badges.

Password Comparison

The User model includes an instance method for secure password verification:
userSchema.methods.comparePassword = async function (candidatePassword: string): Promise<boolean> {
  return bcrypt.compare(candidatePassword, this.password_hash);
};

Security Features

Sensitive Data Protection

Sensitive fields are automatically removed from JSON responses:
userSchema.methods.toJSON = function () {
  const obj = this.toObject();
  delete obj.password_hash;
  delete obj.email_verification_token;
  return obj;
};

Token Storage

1

Server-side Cookies

JWT tokens are stored in httpOnly cookies to prevent XSS attacks
2

Client-side Storage

Tokens are also stored in localStorage for client-side API calls
3

7-Day Expiration

Tokens automatically expire after 7 days for security

Protected Routes

The dashboard uses server-side authentication checking:
export default async function DashboardPage() {
  const cookieStore = await cookies();
  const tokenCookie = cookieStore.get('horse_trust_token');

  if (!tokenCookie?.value) redirect('/login');
  
  const data = await fetchDashboardData(tokenCookie.value);
  if (!data) redirect('/login');

  const { user, horses } = data;
  // Render dashboard...
}

Best Practices

The system requires minimum 8 characters. Encourage users to use passwords with mixed case, numbers, and symbols.
The secure cookie flag is automatically enabled in production to ensure tokens are only sent over HTTPS.
Add rate limiting to login endpoints to prevent brute force attacks.
Consider implementing refresh tokens for enhanced security in long-lived sessions.

Error Handling

The authentication system provides user-friendly error messages:
{error && (
  <div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
    <p className="text-sm font-medium text-red-600">
      {error.includes('Pool') 
        ? 'Error del servidor: La base de datos está en reposo. Por favor, intentá de nuevo en unos segundos.' 
        : error}
    </p>
  </div>
)}

Next Steps

Seller Verification

Learn how sellers verify their identity

Create Horse Listings

Start publishing horse listings

Build docs developers (and LLMs) love