# Capítulo 6: Estructura del Backend y Despliegue de la Aplicación

En este capítulo, exploraremos la estructura del backend que hemos implementado para nuestra aplicación de reservas de gimnasio y el proceso de despliegue para dispositivos móviles reales.

## 1. Arquitectura del Backend

Nuestra aplicación ahora cuenta con un backend real basado en Node.js, Express y PostgreSQL que gestiona todas las operaciones de datos. Este enfoque tiene varias ventajas sobre el almacenamiento local:

- Datos centralizados y accesibles desde cualquier dispositivo
- Mayor seguridad y control de acceso
- Escalabilidad mejorada para manejar más usuarios
- Sincronización de datos en tiempo real

### Estructura de Carpetas del Backend

```
backend/
├── config/
│   └── db.js                  # Configuración de la base de datos
├── controllers/
│   ├── authController.js      # Controlador de autenticación
│   ├── bookingController.js   # Controlador de reservas
│   ├── gymClassController.js  # Controlador de clases
│   ├── uploadController.js    # Controlador para subida de archivos
│   └── userController.js      # Controlador de usuarios
├── models/
│   ├── Booking.js             # Modelo de datos para reservas
│   ├── GymClass.js            # Modelo de datos para clases
│   ├── User.js                # Modelo de datos para usuarios
│   └── index.js               # Exporta todos los modelos
├── routes/
│   ├── authRoutes.js          # Rutas de autenticación
│   ├── bookingRoutes.js       # Rutas de reservas
│   ├── gymClassRoutes.js      # Rutas de clases
│   ├── uploadRoutes.js        # Rutas para subida de archivos
│   └── userRoutes.js          # Rutas de usuarios
├── uploads/                   # Directorio para archivos subidos
├── server.js                  # Punto de entrada principal
└── package.json               # Dependencias y scripts
```

### Tecnologías Utilizadas en el Backend

- **Node.js**: Entorno de ejecución JavaScript del lado del servidor
- **Express**: Framework web para crear APIs REST
- **PostgreSQL**: Base de datos relacional robusta para almacenar los datos
- **Sequelize**: ORM para modelar los datos y conectar con PostgreSQL
- **JWT (JSON Web Tokens)**: Para gestionar la autenticación de usuarios
- **Multer**: Para manejar la subida de archivos

## 2. Implementación del Backend

### Configuración de la Base de Datos

**config/db.js**:

```javascript
const { Sequelize } = require('sequelize');
const dotenv = require('dotenv');

dotenv.config();

// Create Sequelize instance
let sequelize;

if (process.env.DATABASE_URL) {
  // Use connection string if available
  sequelize = new Sequelize(process.env.DATABASE_URL, {
    logging: process.env.NODE_ENV === 'development' ? console.log : false,
    define: {
      freezeTableName: true // Prevent Sequelize from pluralizing table names
    },
    dialectOptions: {
      ssl: {
        require: true,
        rejectUnauthorized: false
      }
    },
    pool: {
      max: 5,
      min: 0,
      acquire: 30000,
      idle: 10000
    }
  });
} else {
  // Fall back to individual parameters
  sequelize = new Sequelize(
    process.env.DB_NAME || 'postgres',
    process.env.DB_USER || 'postgres',
    process.env.DB_PASSWORD || 'postgres',
    {
      host: process.env.DB_HOST || 'localhost',
      port: process.env.DB_PORT || 5432,
      dialect: 'postgres',
      logging: process.env.NODE_ENV === 'development' ? console.log : false,
      define: {
        freezeTableName: true // Prevent Sequelize from pluralizing table names
      },
      dialectOptions: {
        ssl: process.env.NODE_ENV === 'production' ? {
          require: true,
          rejectUnauthorized: false
        } : false
      },
      pool: {
        max: 5,
        min: 0,
        acquire: 30000,
        idle: 10000
      }
    }
  );
}

// Test the connection
const connectDB = async () => {
  try {
    await sequelize.authenticate();
    console.log('Database connection established successfully.');
  } catch (error) {
    console.error('Unable to connect to the database:', error);
    process.exit(1);
  }
};

module.exports = { sequelize, connectDB };
```

### Modelos de Datos

**models/User.js**:

```javascript
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/db');
const bcrypt = require('bcryptjs');

const User = sequelize.define('user', {
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  name: {
    type: DataTypes.STRING,
    allowNull: false
  },
  email: {
    type: DataTypes.STRING,
    allowNull: false,
    unique: true,
    validate: {
      isEmail: true
    }
  },
  password: {
    type: DataTypes.STRING,
    allowNull: false
  },
  profilePicUrl: {
    type: DataTypes.STRING,
    defaultValue: '/uploads/default-avatar.jpg'
  },
  notificationsEnabled: {
    type: DataTypes.BOOLEAN,
    defaultValue: true
  }
}, {
  hooks: {
    beforeCreate: async (user) => {
      if (user.password) {
        const salt = await bcrypt.genSalt(10);
        user.password = await bcrypt.hash(user.password, salt);
      }
    },
    beforeUpdate: async (user) => {
      if (user.changed('password')) {
        const salt = await bcrypt.genSalt(10);
        user.password = await bcrypt.hash(user.password, salt);
      }
    }
  },
  tableName: 'users' // Explicitly set lowercase table name
});

// Method to check if password matches
User.prototype.matchPassword = async function(enteredPassword) {
  return await bcrypt.compare(enteredPassword, this.password);
};

module.exports = User;
```

**models/GymClass.js**:

```javascript
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/db');

const GymClass = sequelize.define('gymClass', {
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  name: {
    type: DataTypes.STRING,
    allowNull: false
  },
  description: {
    type: DataTypes.TEXT,
    allowNull: false
  },
  instructor: {
    type: DataTypes.STRING,
    allowNull: false
  },
  startTime: {
    type: DataTypes.DATE,
    allowNull: false
  },
  endTime: {
    type: DataTypes.DATE,
    allowNull: false
  },
  maxCapacity: {
    type: DataTypes.INTEGER,
    allowNull: false
  },
  currentBookings: {
    type: DataTypes.INTEGER,
    defaultValue: 0
  },
  category: {
    type: DataTypes.STRING,
    defaultValue: 'Otros'
  },
  imageUrl: {
    type: DataTypes.STRING,
    defaultValue: '/uploads/default-class.jpg'
  }
}, {
  tableName: 'classes' // Explicitly set lowercase table name
});

module.exports = GymClass;
```

**models/Booking.js**:

```javascript
const { DataTypes } = require('sequelize');
const { sequelize } = require('../config/db');

const Booking = sequelize.define('booking', {
  id: {
    type: DataTypes.INTEGER,
    primaryKey: true,
    autoIncrement: true
  },
  userId: {
    type: DataTypes.INTEGER,
    allowNull: false,
    references: {
      model: 'users',
      key: 'id'
    }
  },
  classId: {
    type: DataTypes.INTEGER,
    allowNull: false,
    references: {
      model: 'classes',
      key: 'id'
    }
  },
  className: {
    type: DataTypes.STRING,
    allowNull: false
  },
  date: {
    type: DataTypes.DATE,
    defaultValue: DataTypes.NOW
  },
  status: {
    type: DataTypes.ENUM('confirmed', 'cancelled', 'pending'),
    defaultValue: 'confirmed'
  }
}, {
  tableName: 'bookings' // Explicitly set lowercase table name
});

module.exports = Booking;
```

**models/index.js**:

```javascript
const User = require('./User');
const GymClass = require('./GymClass');
const Booking = require('./Booking');

// Define associations
User.hasMany(Booking, { foreignKey: 'userId' });
Booking.belongsTo(User, { foreignKey: 'userId' });

GymClass.hasMany(Booking, { foreignKey: 'classId' });
Booking.belongsTo(GymClass, { foreignKey: 'classId' });

module.exports = {
  User,
  GymClass,
  Booking
};
```

### Controladores 

**controllers/authController.js**:

```javascript
const jwt = require('jsonwebtoken');
const { User } = require('../models');

// Generate JWT Token
const generateToken = (id) => {
  return jwt.sign({ id }, process.env.JWT_SECRET || 'secret_key_12345', {
    expiresIn: '30d'
  });
};

// @desc    Register a new user
// @route   POST /api/auth/register
// @access  Public
exports.registerUser = async (req, res) => {
  try {
    const { name, email, password } = req.body;

    // Check if user already exists
    const existingUser = await User.findOne({ where: { email } });
    if (existingUser) {
      return res.status(400).json({
        success: false,
        message: 'Email is already registered'
      });
    }

    // Create user
    const user = await User.create({
      name,
      email,
      password
    });

    // Generate token
    const token = generateToken(user.id);

    res.status(201).json({
      success: true,
      token,
      user: {
        id: user.id,
        name: user.name,
        email: user.email,
        profilePicUrl: user.profilePicUrl,
        notificationsEnabled: user.notificationsEnabled
      }
    });
  } catch (error) {
    console.error('Error registering user:', error);
    res.status(500).json({
      success: false,
      message: 'Server error',
      error: error.message
    });
  }
};

// @desc    Login user
// @route   POST /api/auth/login
// @access  Public
exports.loginUser = async (req, res) => {
  try {
    const { email, password } = req.body;

    // Check for user email
    const user = await User.findOne({ where: { email } });
    if (!user) {
      return res.status(401).json({
        success: false,
        message: 'Invalid credentials'
      });
    }

    // Check if password matches
    const isMatch = await user.matchPassword(password);
    if (!isMatch) {
      return res.status(401).json({
        success: false,
        message: 'Invalid credentials'
      });
    }

    // Generate token
    const token = generateToken(user.id);

    res.json({
      success: true,
      token,
      user: {
        id: user.id,
        name: user.name,
        email: user.email,
        profilePicUrl: user.profilePicUrl,
        notificationsEnabled: user.notificationsEnabled
      }
    });
  } catch (error) {
    console.error('Error logging in:', error);
    res.status(500).json({
      success: false,
      message: 'Server error',
      error: error.message
    });
  }
};
```

### Rutas API

**routes/authRoutes.js**:

```javascript
const express = require('express');
const { registerUser, loginUser } = require('../controllers/authController');

const router = express.Router();

router.post('/register', registerUser);
router.post('/login', loginUser);

module.exports = router;
```

### Punto de Entrada del Servidor

**server.js**:

```javascript
const path = require('path');
const express = require('express');
const dotenv = require('dotenv');
const cors = require('cors');
const morgan = require('morgan');
const fs = require('fs');
const { connectDB, sequelize } = require('./config/db');

// Load env variables
dotenv.config();

// Import route files
const userRoutes = require('./routes/userRoutes');
const gymClassRoutes = require('./routes/gymClassRoutes');
const bookingRoutes = require('./routes/bookingRoutes');
const uploadRoutes = require('./routes/uploadRoutes');
const authRoutes = require('./routes/authRoutes');

// Initialize express app
const app = express();

// Middleware
app.use(express.json());
app.use(cors());

// Dev logging middleware
if (process.env.NODE_ENV === 'development') {
  app.use(morgan('dev'));
}

// Create uploads directory if it doesn't exist
const uploadsDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadsDir)) {
  fs.mkdirSync(uploadsDir, { recursive: true });
}

// Set static folder for uploads
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));

// Mount routers
app.use('/api/users', userRoutes);
app.use('/api/classes', gymClassRoutes);
app.use('/api/bookings', bookingRoutes);
app.use('/api/upload', uploadRoutes);
app.use('/api/auth', authRoutes);

// Root route
app.get('/', (req, res) => {
  res.json({ message: 'Welcome to Gym API' });
});

// Error handling middleware
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({
    message: 'Server Error',
    error: err.message
  });
});

// Define port
const PORT = process.env.PORT || 3000;

// Connect to database and start server
const startServer = async () => {
  try {
    // Connect to database
    await connectDB();
    
    await sequelize.sync();
    console.log('Database synchronized');
    
    // Start server
    app.listen(PORT, () => {
      console.log(`Server running in ${process.env.NODE_ENV} mode on port ${PORT}`);
    });
  } catch (error) {
    console.error('Unable to start server:', error);
    process.exit(1);
  }
};

startServer();

// Handle unhandled promise rejections
process.on('unhandledRejection', (err) => {
  console.log(`Error: ${err.message}`);
  // Close server & exit process
  process.exit(1);
});
```

## 3. Despliegue en Plataformas Móviles

Una de las grandes ventajas de usar Ionic con Capacitor es la posibilidad de desplegar la misma base de código en múltiples plataformas. Vamos a revisar el proceso para Android.

### Preparación para Android

1. **Instalar las Dependencias de Android**:
   - Android Studio
   - JDK (Java Development Kit)
   - Configurar variables de entorno (ANDROID_HOME, JAVA_HOME)

2. **Añadir la Plataforma Android a nuestro Proyecto**:

```bash
# Instalar @capacitor/android si no está instalado
npm install @capacitor/android

# Añadir Android al proyecto
npx cap add android
```

3. **Sincronizar el Proyecto**:

Después de hacer cambios en el código web, necesitamos construir el proyecto y sincronizarlo con las plataformas nativas:

```bash
# Construir la aplicación
ionic build --prod

# Sincronizar con Android
npx cap sync android
```

4. **Abrir el Proyecto en Android Studio**:

```bash
npx cap open android
```

### Configuración del AndroidManifest.xml

El archivo `AndroidManifest.xml` es crucial para definir los permisos y configuraciones de la aplicación Android. Ya cubrimos algunos permisos esenciales:

```xml
<!-- Permisos para cámara -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />

<!-- Permisos para notificaciones -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
```

### Personalización de Iconos y Splash Screen

Para personalizar los iconos y la pantalla de inicio:

1. **Reemplazar los Archivos de Iconos**: 
   - Crear iconos para diferentes resoluciones
   - Reemplazar los archivos en `android/app/src/main/res/mipmap-*/`

2. **Reemplazar la Pantalla de Inicio (Splash Screen)**:
   - Crear imágenes para diferentes orientaciones y resoluciones
   - Reemplazar los archivos en `android/app/src/main/res/drawable-*/splash.png`

3. **Personalizar el Nombre de la Aplicación**:
   - Editar `android/app/src/main/res/values/strings.xml`:
   ```xml
   <resources>
       <string name="app_name">Gym Reservation</string>
       <string name="title_activity_main">Gym Reservation</string>
       <string name="package_name">com.example.gymreservation</string>
       <string name="custom_url_scheme">com.example.gymreservation</string>
   </resources>
   ```

### Construir un APK de Prueba

Para generar un APK que puedas compartir con probadores:

1. En Android Studio, selecciona **Build > Build Bundle(s) / APK(s) > Build APK(s)**
2. Cuando termine, haz clic en **locate** para encontrar el archivo APK
3. Este archivo se puede compartir directamente para instalarlo en dispositivos Android (se debe habilitar la instalación desde fuentes desconocidas)

### Construir un Bundle para Google Play

Para publicar en la tienda de Google Play:

1. En Android Studio, selecciona **Build > Generate Signed Bundle / APK**
2. Elige **Android App Bundle**
3. Crea o selecciona una clave de firma (keystore)
4. Completa la información necesaria y genera el bundle (.aab)
5. Este archivo es el que se sube a la Google Play Console

## 4. Consideraciones de Seguridad

Al desplegar una aplicación móvil que se comunica con un backend, es crucial considerar aspectos de seguridad:

### Autenticación Segura

Nuestra aplicación implementa JWT (JSON Web Tokens) para autenticar usuarios, pero aquí hay algunas mejoras adicionales:

- **Tokens de Actualización**: Implementar tokens de corta duración con tokens de actualización
- **Almacenamiento Seguro**: Utilizar almacenamiento nativo seguro para tokens 
- **HTTPS**: Garantizar que todas las comunicaciones API usen HTTPS

### Manejo de Datos Sensibles

- **No almacenar contraseñas en texto plano**: Ya estamos usando bcrypt para hash de contraseñas
- **Minimizar los datos almacenados localmente**: Solo guardar lo necesario en el dispositivo
- **Eliminar datos al cerrar sesión**: Limpiar datos sensibles cuando el usuario cierre sesión

### Permisos de Aplicación

- **Solicitar solo los permisos necesarios**: No solicitar permisos que no sean esenciales
- **Explicar por qué se necesitan permisos**: Proporcionar contexto cuando se solicitan permisos
- **Manejar denegaciones graciosamente**: La aplicación debe funcionar aunque se denieguen permisos no críticos

## 5. Optimizaciones de Rendimiento

Para garantizar que nuestra aplicación funcione de manera óptima en dispositivos reales:

### Optimización de Imágenes

- **Redimensionar imágenes**: Utilizar tamaños apropiados para dispositivos móviles
- **Compresión de imágenes**: Reducir el tamaño de archivo sin perder calidad visible
- **Carga progresiva**: Implementar carga progresiva para imágenes grandes

### Optimización de Red

- **Implementar caché**: Almacenar en caché respuestas de API para reducir solicitudes de red
- **Carga perezosa (lazy loading)**: Cargar datos solo cuando sean necesarios
- **Compresión de datos**: Utilizar gzip en el servidor para respuestas API

### Optimización de la UI

- **Virtualización de listas**: Para listas largas, solo renderizar elementos visibles
- **Animaciones eficientes**: Usar animaciones CSS en lugar de JavaScript cuando sea posible
- **Evitar work blocking**: No bloquear el hilo principal con operaciones pesadas

## 6. Pruebas y Depuración

Para identificar y resolver problemas en dispositivos reales:

### Herramientas de Depuración

- **Chrome Remote Debugging**: Para depurar WebView en dispositivos Android
- **Safari Web Inspector**: Para depurar WebView en dispositivos iOS
- **Capacitor Logs**: Usar `npx cap logs android` para ver logs en tiempo real

### Pruebas en Dispositivos Reales

- **Probar en diferentes tamaños de pantalla**: Asegurar que la UI sea responsiva
- **Probar con diferentes velocidades de red**: Simular conexiones lentas
- **Pruebas de batería**: Verificar que la aplicación no consuma demasiada batería

## Resumen

En este capítulo, hemos explorado:

1. La arquitectura de nuestro backend Node.js/Express/PostgreSQL
2. Los detalles de implementación de los modelos, controladores y rutas del backend
3. El proceso de despliegue para dispositivos Android
4. Consideraciones de seguridad para aplicaciones móviles
5. Técnicas de optimización de rendimiento
6. Herramientas y enfoques para pruebas y depuración

Con esto, nuestra aplicación Gym Reservation está lista para ser utilizada por usuarios reales, con un backend robusto y una experiencia móvil optimizada.