FitAiid provides 8 powerful features to create a complete AI-powered fitness experience.
Feature Overview
User Authentication
Secure email/password and Google OAuth with JWT
AI Workout Generation
Personalized routines powered by OpenAI
Progress Tracking
Real-time statistics and workout history
Achievement System
Gamified rewards and streak tracking
Routine Management
Automatic day cycling and workout scheduling
Push Notifications
Real-time workout reminders and achievements
Email Verification
Secure account creation with code verification
User Profiles
Comprehensive fitness profiles and preferences
1. User Authentication & Authorization
Overview
Registration Flow
Google OAuth
Password Recovery
FitAiid implements enterprise-grade authentication with multiple methods:
- Email/Password: Secure registration with bcrypt hashing (12 salt rounds)
- Google OAuth: Firebase Admin SDK integration
- JWT Tokens: 30-day expiration with role-based access
- Rate Limiting: 5 login attempts before 30-minute lockout
- Email Verification: 6-digit codes with 15-minute expiration
All passwords must contain at least 8 characters, including uppercase, lowercase, and numbers.
// POST /api/auth/register-with-code
const response = await fetch('/api/auth/register-with-code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
firstName: 'John',
lastName: 'Doe',
email: '[email protected]',
password: 'SecurePass123',
phone: '1234567890'
})
});
// Response: { success: true, message: 'Código de verificación enviado' }
// POST /api/auth/verify-registration
const verifyResponse = await fetch('/api/auth/verify-registration', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: '[email protected]',
code: '123456' // Received via email
})
});
const { token, user } = await verifyResponse.json();
Users are NOT saved to the database until they verify their email code. This prevents spam registrations.
// Registration with Google
const registerResponse = await fetch('/api/auth/google-register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
firstName: 'John',
lastName: 'Doe',
email: '[email protected]',
uid: firebaseUser.uid // From Firebase Authentication
})
});
// Login with Google (for existing users)
const loginResponse = await fetch('/api/auth/google', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: '[email protected]',
uid: firebaseUser.uid
})
});
Source: backend/src/controllers/authController.js:380-514// Step 1: Request reset code
await fetch('/api/auth/forgot-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: '[email protected]' })
});
// Step 2: Verify the code
await fetch('/api/auth/verify-code', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: '[email protected]',
code: '123456'
})
});
// Step 3: Reset password
await fetch('/api/auth/reset-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: '[email protected]',
code: '123456',
password: 'NewSecurePass123'
})
});
Source: backend/src/controllers/authController.js:559-728
Security Features
// From backend/src/models/User.js:607-621
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) {
return next();
}
try {
const saltRounds = 12;
this.password = await bcrypt.hash(this.password, saltRounds);
console.log(`🔒 Contraseña encriptada para: ${this.email}`);
next();
} catch (error) {
console.error(`❌ Error encriptando contraseña:`, error.message);
next(error);
}
});
// From backend/src/models/User.js:653-669
userSchema.methods.incrementLoginAttempts = function() {
if (this.lockUntil && this.lockUntil < Date.now()) {
return this.updateOne({
$unset: { lockUntil: 1 },
$set: { loginAttempts: 1 }
});
}
const updates = { $inc: { loginAttempts: 1 } };
if (this.loginAttempts + 1 >= 5 && !this.isLocked) {
updates.$set = { lockUntil: Date.now() + 30 * 60 * 1000 }; // 30 minutes
console.log(`🔒 Cuenta bloqueada temporalmente: ${this.email}`);
}
return this.updateOne(updates);
};
2. AI-Powered Workout Generation
FitAiid uses OpenAI GPT models to generate personalized workout routines based on user profiles.
User Profile
Context Generation
Routine Structure
Users complete a comprehensive fitness questionnaire:// User fitness profile schema (from User.js:275-341)
const fitnessProfile = {
gender: 'hombre' | 'mujer',
age: 28, // Min: 14, Max: 100
height: 175, // cm, Min: 100, Max: 250
weight: 75, // kg, Min: 30, Max: 300
fitnessLevel: 'intermedio', // 'principiante' | 'intermedio' | 'avanzado'
mainGoal: 'tonificar', // 'tonificar' | 'ganar masa muscular' | 'bajar de peso'
medicalConditions: '', // Optional, max 500 chars
trainingLocation: 'gym', // 'casa' | 'gym'
trainingDaysPerWeek: 4, // 1-7 days
sessionDuration: '1 hr', // '30 min' | '45 min' | '1 hr' | '+1 hr'
questionnaireCompleted: true,
questionnaireCompletedAt: new Date()
};
The fitness profile is used to generate contextual prompts for OpenAI, ensuring workouts match the user’s capabilities and goals.
// From backend/src/models/User.js:891-914
userSchema.methods.getFitnessContext = function() {
const profile = this.fitnessProfile;
if (!profile || !profile.questionnaireCompleted) {
return "Usuario sin perfil fitness completado";
}
return `
**PERFIL FITNESS DEL USUARIO:**
- Nombre: ${this.firstName} ${this.lastName}
- Género: ${profile.gender || 'No especificado'}
- Edad: ${profile.age || 'No especificada'} años
- Altura: ${profile.height || 'No especificada'} cm
- Peso: ${profile.weight || 'No especificado'} kg
- IMC: ${this.bmi || 'No calculado'} (${this.bmiCategory || 'N/A'})
- Nivel: ${profile.fitnessLevel || 'No especificado'}
- Objetivo: ${profile.mainGoal || 'No especificado'}
- Condiciones médicas: ${profile.medicalConditions || 'Ninguna'}
- Entrena en: ${profile.trainingLocation || 'No especificado'}
- Días por semana: ${profile.trainingDaysPerWeek || 'No especificado'}
- Duración sesión: ${profile.sessionDuration || 'No especificado'}
Usa esta información para personalizar tus recomendaciones.`;
};
AI-generated routines follow this structure:// From backend/src/models/User.js:342-402
const rutinaSemanal = [
{
nombre: 'Día 1: Pecho y Tríceps',
esDescanso: false,
enfoque: 'Pecho y Tríceps',
duracion: '60 minutos',
duracionTotal: 60,
caloriasEstimadas: 450,
mensaje: 'Enfócate en la técnica y controla el peso',
ejercicios: [
{
nombre: 'Press de banca',
series: 4,
repeticiones: '8-10',
descanso: '90 segundos',
musculoObjetivo: 'Pecho',
tecnica: 'Mantén los codos a 45 grados',
completado: false
},
// ... more exercises
],
completado: false,
fechaCompletado: null
},
// ... 6 more days
];
Routines can include rest days by setting esDescanso: true. These are automatically skipped during day cycling.
BMI Calculation
// Automatic BMI calculation (User.js:565-581)
userSchema.virtual('bmi').get(function() {
if (!this.fitnessProfile?.weight || !this.fitnessProfile?.height) {
return null;
}
const heightInMeters = this.fitnessProfile.height / 100;
const bmi = this.fitnessProfile.weight / (heightInMeters * heightInMeters);
return Math.round(bmi * 10) / 10;
});
userSchema.virtual('bmiCategory').get(function() {
const bmi = this.bmi;
if (!bmi) return null;
if (bmi < 18.5) return 'Bajo peso';
if (bmi < 25) return 'Peso normal';
if (bmi < 30) return 'Sobrepeso';
return 'Obesidad';
});
3. Automatic Routine Cycling
FitAiid automatically cycles through workout days, skipping rest days and resetting when complete.
Get Current Day
Complete Day
Auto-Reset Logic
// GET /api/rutina/:userId/dia-actual
const response = await fetch(`/api/rutina/${userId}/dia-actual`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const data = await response.json();
Response:{
"success": true,
"data": {
"diaActual": {
"indice": 0,
"nombre": "lunes",
"enfoque": "Pecho y Tríceps",
"duracionTotal": 60,
"caloriasEstimadas": 450,
"ejercicios": [...],
"completado": false
},
"progreso": {
"cicloActual": 1,
"diaActualIndex": 0,
"diasCompletadosEsteCiclo": 0,
"porcentajeCiclo": 0
}
}
}
Source: backend/src/controllers/rutinaController.js:13-37// POST /api/rutina/:userId/completar-dia
const response = await fetch(`/api/rutina/${userId}/completar-dia`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
duracion: 60,
calorias: 450,
ejerciciosCompletados: 6
})
});
Response:{
"success": true,
"message": "Día lunes completado. Siguiente: martes.",
"data": {
"exito": true,
"diaCompletado": "lunes",
"indexCompletado": 0,
"siguienteDia": "martes",
"siguienteIndex": 1,
"nuevoCiclo": false,
"cicloActual": 1,
"estadisticas": {...}
}
}
Source: backend/src/controllers/rutinaController.js:43-90// From backend/src/models/User.js:993-1043
userSchema.methods.completarDiaYAvanzar = async function() {
if (!this.rutinaSemanal || this.rutinaSemanal.length === 0) {
throw new Error('No hay rutina asignada');
}
const totalDias = this.rutinaSemanal.length;
const diasSemana = ['lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado', 'domingo'];
// Mark current day as complete
this.rutinaSemanal[this.diaActualIndex].completado = true;
this.rutinaSemanal[this.diaActualIndex].fechaCompletado = new Date();
this.ultimoDiaCompletado = new Date();
this.diasCompletadosEsteCiclo += 1;
// Find next training day (skip rest days)
let siguienteIndex = (this.diaActualIndex + 1) % totalDias;
while (this.rutinaSemanal[siguienteIndex]?.esDescanso && siguienteIndex !== 0) {
siguienteIndex = (siguienteIndex + 1) % totalDias;
}
// Check if cycle is complete
let nuevoCiclo = false;
if (siguienteIndex <= this.diaActualIndex) {
nuevoCiclo = true;
this.cicloActual += 1;
this.diasCompletadosEsteCiclo = 0;
// Reset all days
this.rutinaSemanal.forEach(dia => {
dia.completado = false;
dia.fechaCompletado = null;
});
console.log(`🔄 ¡Ciclo ${this.cicloActual - 1} completado! Iniciando ciclo ${this.cicloActual}`);
}
this.diaActualIndex = siguienteIndex;
await this.save();
return {
exito: true,
diaCompletado: diasSemana[indexAnterior],
siguienteDia: diasSemana[siguienteIndex],
nuevoCiclo,
cicloActual: this.cicloActual
};
};
When a user completes the last day of their routine, the system automatically starts a new cycle and resets all progress flags.
4. Progress Tracking & Statistics
Workout History
Get Statistics
Streak Calculation
Every completed workout is automatically logged:// From backend/src/models/User.js:711-743
userSchema.methods.registrarEntrenamiento = async function(workoutData) {
console.log(`📝 Registrando entrenamiento para ${this.email}`);
const workout = {
date: new Date(),
nombre: workoutData.nombre,
enfoque: workoutData.enfoque,
duracionTotal: workoutData.duracionTotal,
caloriasEstimadas: workoutData.caloriasEstimadas || 0,
ejerciciosCompletados: workoutData.ejercicios?.length || 0,
ejerciciosDetalles: workoutData.ejercicios || []
};
// Add to history
this.fitnessStats.workoutHistory.push(workout);
// Update totals
this.fitnessStats.totalWorkouts += 1;
this.fitnessStats.totalExercises += workout.ejerciciosCompletados;
this.fitnessStats.totalMinutes += workout.duracionTotal;
this.fitnessStats.totalCalories += workout.caloriasEstimadas;
this.fitnessStats.lastWorkoutDate = workout.date;
this.fitnessStats.lastStatsUpdate = new Date();
// Calculate streak and check achievements
this.calcularRacha();
this.verificarLogros();
await this.save();
return workout;
};
// GET /api/estadisticas/:userId
const response = await fetch(`/api/estadisticas/${userId}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
Response:{
"success": true,
"data": {
"totalWorkouts": 15,
"totalExercises": 90,
"totalMinutes": 900,
"totalHours": "15.0",
"totalCalories": 6750,
"currentStreak": 7,
"maxStreak": 10,
"lastWorkoutDate": "2026-03-06T10:30:00.000Z",
"achievements": [
{
"achievementId": "first_workout",
"nombre": "Primera Rutina",
"unlockedAt": "2026-02-15T08:00:00.000Z"
},
{
"achievementId": "week_streak",
"nombre": "Racha de 7 días",
"unlockedAt": "2026-03-06T10:30:00.000Z"
}
],
"workoutHistory": [
{
"date": "2026-03-06T10:00:00.000Z",
"nombre": "Día 1: Pecho y Tríceps",
"enfoque": "Pecho y Tríceps",
"duracionTotal": 60,
"caloriasEstimadas": 450,
"ejerciciosCompletados": 6
}
]
}
}
Source: backend/src/routes/estadisticas_r.js:12-13// From backend/src/models/User.js:745-795
userSchema.methods.calcularRacha = function() {
const workouts = this.fitnessStats.workoutHistory;
if (workouts.length === 0) {
this.fitnessStats.currentStreak = 0;
this.fitnessStats.maxStreak = 0;
return;
}
// Sort workouts by date (newest first)
const sortedWorkouts = workouts
.map(w => new Date(w.date))
.sort((a, b) => b - a);
const today = new Date();
today.setHours(0, 0, 0, 0);
const lastWorkout = new Date(sortedWorkouts[0]);
lastWorkout.setHours(0, 0, 0, 0);
const daysSinceLastWorkout = Math.floor((today - lastWorkout) / (1000 * 60 * 60 * 24));
// Streak is broken if last workout was more than 1 day ago
if (daysSinceLastWorkout <= 1) {
let currentStreak = 1;
// Count consecutive days
for (let i = 1; i < sortedWorkouts.length; i++) {
const currentDate = new Date(sortedWorkouts[i]);
currentDate.setHours(0, 0, 0, 0);
const prevDate = new Date(sortedWorkouts[i - 1]);
prevDate.setHours(0, 0, 0, 0);
const daysDiff = Math.floor((prevDate - currentDate) / (1000 * 60 * 60 * 24));
if (daysDiff === 1) {
currentStreak++;
} else if (daysDiff === 0) {
continue; // Same day, multiple workouts
} else {
break; // Streak broken
}
}
this.fitnessStats.currentStreak = currentStreak;
this.fitnessStats.maxStreak = Math.max(
this.fitnessStats.maxStreak,
currentStreak
);
} else {
this.fitnessStats.currentStreak = 0;
}
};
5. Achievement System
Gamified achievements unlock automatically based on user progress.
// From backend/src/models/User.js:797-848
userSchema.methods.verificarLogros = function() {
const stats = this.fitnessStats;
const logros = [
{
id: 'first_workout',
nombre: 'Primera Rutina',
condicion: stats.totalWorkouts >= 1
},
{
id: 'week_streak',
nombre: 'Racha de 7 días',
condicion: stats.currentStreak >= 7
},
{
id: 'ten_workouts',
nombre: 'Dedicación',
condicion: stats.totalWorkouts >= 10
},
{
id: 'fifty_workouts',
nombre: 'Guerrero',
condicion: stats.totalWorkouts >= 50
},
{
id: 'month_streak',
nombre: 'Leyenda',
condicion: stats.currentStreak >= 30
},
{
id: 'hundred_exercises',
nombre: 'Incansable',
condicion: stats.totalExercises >= 100
}
];
logros.forEach(logro => {
if (logro.condicion) {
const yaDesbloqueado = stats.achievements.some(
a => a.achievementId === logro.id
);
if (!yaDesbloqueado) {
stats.achievements.push({
achievementId: logro.id,
nombre: logro.nombre,
unlockedAt: new Date()
});
console.log(`🏆 ¡Logro desbloqueado! ${logro.nombre}`);
}
}
});
};
Available Achievements
Primera Rutina
Complete your first workout
Racha de 7 días
Workout 7 consecutive days
Dedicación
Complete 10 total workouts
Guerrero
Complete 50 total workouts
Leyenda
Maintain a 30-day streak
Incansable
Complete 100 total exercises
Achievements are checked automatically every time registrarEntrenamiento() is called.
6. Push Notifications
Real-time Web Push notifications using VAPID protocol.
Subscribe
Send Notification
Auto Triggers
// Step 1: Get VAPID public key
const keyResponse = await fetch('/api/notifications/vapid-public-key');
const { publicKey } = await keyResponse.json();
// Step 2: Request permission and subscribe
const registration = await navigator.serviceWorker.ready;
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: publicKey
});
// Step 3: Save subscription to backend
await fetch('/api/notifications/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: currentUser.id,
subscription
})
});
Source: backend/src/routes/notifications.js:33-79// Manual notification
await fetch('/api/notifications/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: '507f1f77bcf86cd799439011',
title: '🎉 ¡Felicitaciones!',
body: 'Has completado tu entrenamiento de hoy',
url: '/dashboard'
})
});
Source: backend/src/routes/notifications.js:82-112Notifications are automatically sent for key events:// Achievement unlocked
POST /api/notifications/logro-desbloqueado
{
"userId": "507f1f77bcf86cd799439011",
"logroId": "week_streak",
"nombre": "Racha de 7 días",
"descripcion": "¡Has entrenado 7 días seguidos!"
}
// Workout completed
POST /api/notifications/entrenamiento-completado
{
"userId": "507f1f77bcf86cd799439011",
"nombre": "Día 1: Pecho y Tríceps",
"duracion": 60,
"calorias": 450
}
// Goal reached
POST /api/notifications/objetivo-completado
{
"userId": "507f1f77bcf86cd799439011",
"objetivoId": "weight_loss",
"nombre": "Meta de peso alcanzada"
}
Source: backend/src/routes/notifications.js:122-191
VAPID keys must be generated and configured in your .env file. See the quickstart guide for setup instructions.
7. Email System
Secure email delivery using Nodemailer with Gmail SMTP.
Verification Codes
Password Recovery
// From backend/src/controllers/authController.js:525-547
const sendVerificationCodeEmail = async (email, firstName, code) => {
const mailOptions = {
from: `"FitAiid 💪" <${process.env.EMAIL_USER}>`,
to: email,
subject: "Código de Verificación - FitAiid",
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<h2 style="color: #333;">¡Bienvenido a FitAiid!</h2>
<p>Hola ${firstName},</p>
<p>Gracias por registrarte. Tu código de verificación es:</p>
<div style="background-color: #f4f4f4; padding: 20px; text-align: center; border-radius: 5px; margin: 20px 0;">
<h1 style="color: #667eea; font-size: 36px; letter-spacing: 5px; margin: 0;">
${code}
</h1>
</div>
<p>Este código expira en <strong>15 minutos</strong>.</p>
<p style="color: #999; font-size: 14px;">Si no te registraste, ignora este correo.</p>
</div>
`
};
await transporter.sendMail(mailOptions);
};
// From backend/src/controllers/authController.js:596-614
const mailOptions = {
from: `"FitAiid 💪" <${process.env.EMAIL_USER}>`,
to: user.email,
subject: 'Código de Recuperación de Contraseña',
html: `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<h2 style="color: #333;">Código de Recuperación de Contraseña</h2>
<p>Hola ${user.firstName},</p>
<p>Has solicitado restablecer tu contraseña. Tu código de verificación es:</p>
<div style="background-color: #f4f4f4; padding: 20px; text-align: center; border-radius: 5px; margin: 20px 0;">
<h1 style="color: #667eea; font-size: 36px; letter-spacing: 5px; margin: 0;">
${resetCode}
</h1>
</div>
<p>Este código expirará en <strong>15 minutos</strong>.</p>
<p style="color: #999; font-size: 14px;">Si no solicitaste este cambio, ignora este correo.</p>
</div>
`
};
Email configuration requires Gmail App Passwords. Regular Gmail passwords won’t work with SMTP authentication.
8. User Profile Management
Get Profile
Update Profile
Virtual Properties
// GET /api/auth/profile
const response = await fetch('/api/auth/profile', {
headers: { 'Authorization': `Bearer ${token}` }
});
Response includes:
- Basic info (name, email, phone)
- Fitness profile (age, weight, height, goals)
- Statistics (workouts, calories, streak)
- BMI calculation
- Achievements
- Customer level (bronze/silver/gold/platinum)
Source: backend/src/controllers/authController.js:265-296// PUT /api/auth/profile
await fetch('/api/auth/profile', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
firstName: 'John',
lastName: 'Doe',
phone: '9876543210'
})
});
Allowed fields:
firstName
lastName
phone
dateOfBirth
gender
avatar
address
Source: backend/src/controllers/authController.js:307-366User model includes computed properties:// From backend/src/models/User.js:561-601
// Full name
userSchema.virtual('fullName').get(function() {
return `${this.firstName} ${this.lastName}`;
});
// BMI
userSchema.virtual('bmi').get(function() {
if (!this.fitnessProfile?.weight || !this.fitnessProfile?.height) {
return null;
}
const heightInMeters = this.fitnessProfile.height / 100;
const bmi = this.fitnessProfile.weight / (heightInMeters * heightInMeters);
return Math.round(bmi * 10) / 10;
});
// BMI Category
userSchema.virtual('bmiCategory').get(function() {
const bmi = this.bmi;
if (!bmi) return null;
if (bmi < 18.5) return 'Bajo peso';
if (bmi < 25) return 'Peso normal';
if (bmi < 30) return 'Sobrepeso';
return 'Obesidad';
});
// Customer Level (based on spending)
userSchema.virtual('customerLevel').get(function() {
if (this.totalSpent >= 5000000) return 'platinum';
if (this.totalSpent >= 2000000) return 'gold';
if (this.totalSpent >= 500000) return 'silver';
return 'bronze';
});
Complete Feature Integration
Here’s how all features work together:
User Registration
User signs up → Email verification → JWT token issued
Profile Setup
User completes fitness questionnaire → AI generates personalized routine
Daily Workout
User gets current day workout → Completes exercises → Marks day complete
Progress Tracking
System logs workout → Updates statistics → Calculates streak
Achievement Unlock
System checks achievements → Unlocks new badges → Sends push notification
Automatic Cycling
Next day is set → Rest days skipped → Routine resets when complete
API Reference
Complete endpoint documentation
Quickstart
Get started in 5 minutes