🎯 Changes: - Moved from gitea.cbcren.online to manga.cbcren.online - Updated Caddy proxy configuration with SSL auto-cert - Updated iOS app APIConfig to use HTTPS - Changed port from 3001 to standard HTTPS (443) - No interference with Gitea or other services 🔧 Technical Details: - DNS: manga.cbcren.online → 194.163.191.200 - Proxy: Caddy with automatic HTTPS - Backend: 172.17.0.1:3001 (Docker gateway) - SSL: Automatic Let's Encrypt certificate ✅ Tested: - Health check: https://manga.cbcren.online/api/health ✓ - Storage stats: https://manga.cbcren.online/api/storage/stats ✓ - HTTPS redirect working correctly ✓ 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
435 lines
14 KiB
Swift
435 lines
14 KiB
Swift
import Foundation
|
|
|
|
/// Configuración centralizada de la API del backend VPS.
|
|
///
|
|
/// `APIConfig` proporciona todos los endpoints y parámetros de configuración
|
|
/// necesarios para comunicarse con el backend VPS que gestiona el almacenamiento
|
|
/// y serving de capítulos de manga.
|
|
///
|
|
/// # Example
|
|
/// ```swift
|
|
/// let baseURL = APIConfig.baseURL
|
|
/// let downloadEndpoint = APIConfig.Endpoints.download(mangaSlug: "one-piece", chapterNumber: 1)
|
|
/// print(downloadEndpoint) // "https://api.example.com/api/v1/download/one-piece/1"
|
|
/// ```
|
|
enum APIConfig {
|
|
// MARK: - Base Configuration
|
|
|
|
/// URL base del backend VPS
|
|
///
|
|
/// Esta URL se usa para construir todos los endpoints de la API.
|
|
/// Configurar según el entorno (desarrollo, staging, producción).
|
|
///
|
|
/// # Configuración Actual
|
|
/// - Producción: `https://gitea.cbcren.online`
|
|
/// - Puerto: `3001` (se añade automáticamente)
|
|
///
|
|
/// # Notas Importantes
|
|
/// - Incluir el protocolo (`https://` o `http://`)
|
|
/// - NO incluir el número de puerto aquí (usar la propiedad `port`)
|
|
/// - NO incluir slash al final
|
|
/// - Asegurarse de que el servidor sea accesible desde el dispositivo iOS
|
|
///
|
|
/// # Ejemplos
|
|
/// - `https://manga.cbcren.online` (VPS de producción)
|
|
/// - `http://192.168.1.100` (desarrollo local)
|
|
/// - `http://localhost` (simulador con servidor local)
|
|
static let serverURL = "https://manga.cbcren.online"
|
|
|
|
/// Puerto donde corre el backend API
|
|
///
|
|
/// # Valor por Defecto
|
|
/// - `nil` - Usa el puerto estándar HTTPS (443)
|
|
///
|
|
/// # Notas
|
|
/// - Con HTTPS, se usa el puerto estándar 443
|
|
/// - Solo especificar un puerto si es diferente al estándar
|
|
static let port: Int? = nil
|
|
|
|
/// URL base completa para requests a la API
|
|
///
|
|
/// Construye automáticamente la URL base combinando la URL del servidor y el puerto.
|
|
/// Esta es la propiedad recomendada para usar al hacer requests a la API.
|
|
///
|
|
/// # Ejemplo
|
|
/// ```swift
|
|
/// let endpoint = "/api/v1/manga"
|
|
/// let url = URL(string: APIConfig.baseURL + endpoint)
|
|
/// ```
|
|
static var baseURL: String {
|
|
// Si no hay puerto específico o es el puerto estándar HTTPS, usar solo la URL
|
|
if port == nil || port == 443 {
|
|
return serverURL
|
|
}
|
|
return "\(serverURL):\(port!)"
|
|
}
|
|
|
|
/// Versión de la API
|
|
static var apiVersion: String {
|
|
return "v1"
|
|
}
|
|
|
|
/// Path base de la API
|
|
static var basePath: String {
|
|
return "\(baseURL)/api/\(apiVersion)"
|
|
}
|
|
|
|
/// Timeout por defecto para requests (en segundos)
|
|
static var defaultTimeout: TimeInterval {
|
|
return 30.0
|
|
}
|
|
|
|
/// Timeout para requests de descarga (en segundos)
|
|
static var downloadTimeout: TimeInterval {
|
|
return 300.0 // 5 minutos
|
|
}
|
|
|
|
// MARK: - HTTP Headers
|
|
|
|
/// Headers HTTP comunes para todas las requests
|
|
static var commonHeaders: [String: String] {
|
|
return [
|
|
"Content-Type": "application/json",
|
|
"Accept": "application/json"
|
|
]
|
|
}
|
|
|
|
/// Header de autenticación (si se requiere API key o token)
|
|
///
|
|
/// - Parameter token: Token de autenticación (API key, JWT, etc.)
|
|
/// - Returns: Dictionary con el header de autorización
|
|
static func authHeader(token: String) -> [String: String] {
|
|
return [
|
|
"Authorization": "Bearer \(token)"
|
|
]
|
|
}
|
|
|
|
// MARK: - Retry Configuration
|
|
|
|
/// Número máximo de intentos de retry para requests fallidas
|
|
///
|
|
/// # Valor por Defecto
|
|
/// - `3` intentos de retry
|
|
///
|
|
/// # Comportamiento
|
|
/// - Un valor de `0` significa sin reintentos
|
|
/// - Los reintentos usan backoff exponencial
|
|
/// - Solo se reintentan errores recuperables (fallos de red, timeouts, etc.)
|
|
/// - Errores de cliente (4xx) típicamente no se reintentan
|
|
static let maxRetries: Int = 3
|
|
|
|
/// Delay base entre intentos de retry en segundos
|
|
///
|
|
/// # Valor por Defecto
|
|
/// - `1.0` segundo
|
|
///
|
|
/// # Fórmula
|
|
/// El delay real usa backoff exponencial:
|
|
/// ```
|
|
/// delay = baseRetryDelay * (2 ^ numeroDeIntento)
|
|
/// ```
|
|
/// - Intento 1: 1 segundo de delay
|
|
/// - Intento 2: 2 segundos de delay
|
|
/// - Intento 3: 4 segundos de delay
|
|
static let baseRetryDelay: TimeInterval = 1.0
|
|
|
|
// MARK: - Cache Configuration
|
|
|
|
/// Número máximo de respuestas de API a cachear en memoria
|
|
///
|
|
/// # Valor por Defecto
|
|
/// - `100` respuestas cacheadas
|
|
///
|
|
/// # Notas
|
|
/// - Cachear ayuda a reducir requests de red y mejorar performance
|
|
/// - La caché se limpia automáticamente cuando se detecta presión de memoria
|
|
/// - Valores más grandes pueden aumentar el uso de memoria
|
|
static let cacheMaxMemoryUsage = 100
|
|
|
|
/// Tiempo de expiración de caché para respuestas de API en segundos
|
|
///
|
|
/// # Valor por Defecto
|
|
/// - `300.0` segundos (5 minutos)
|
|
///
|
|
/// # Uso
|
|
/// - Datos cacheados más viejos que esto se refrescarán del servidor
|
|
/// - Configurar en `0` para deshabilitar caché
|
|
/// - Aumentar para datos que cambian infrecuentemente
|
|
static let cacheExpiryTime: TimeInterval = 300.0
|
|
|
|
// MARK: - Logging Configuration
|
|
|
|
/// Habilitar logging de requests para debugging
|
|
///
|
|
/// # Valor por Defecto
|
|
/// - `false` (deshabilitado en producción)
|
|
///
|
|
/// # Comportamiento
|
|
/// - Cuando es `true`, todas las requests y respuestas se loguean a consola
|
|
/// - Útil para desarrollo y debugging
|
|
/// - Debe ser `false` en builds de producción por seguridad
|
|
///
|
|
/// # Recomendación
|
|
/// Usar configuraciones de build para habilitar solo en debug:
|
|
/// ```swift
|
|
/// #if DEBUG
|
|
/// static let loggingEnabled = true
|
|
/// #else
|
|
/// static let loggingEnabled = false
|
|
/// #endif
|
|
/// ```
|
|
static let loggingEnabled = false
|
|
|
|
// MARK: - Helper Methods
|
|
|
|
/// Construye una URL completa para un endpoint dado
|
|
///
|
|
/// - Parameter endpoint: El path del endpoint de la API (ej: "/manga/popular")
|
|
/// - Returns: Una URL completa combinando la base URL y el endpoint
|
|
///
|
|
/// # Ejemplo
|
|
/// ```swift
|
|
/// let url = APIConfig.url(for: "/manga/popular")
|
|
/// // Retorna: "https://gitea.cbcren.online:3001/api/v1/manga/popular"
|
|
/// ```
|
|
static func url(for endpoint: String) -> String {
|
|
// Remover slash inicial si está presente para evitar dobles slashes
|
|
let cleanEndpoint = endpoint.hasPrefix("/") ? String(endpoint.dropFirst()) : endpoint
|
|
|
|
// Añadir prefix de API si no está ya incluido
|
|
if cleanEndpoint.hasPrefix("api/") {
|
|
return baseURL + "/" + cleanEndpoint
|
|
} else {
|
|
return baseURL + "/" + cleanEndpoint
|
|
}
|
|
}
|
|
|
|
/// Crea un objeto URL para un endpoint dado
|
|
///
|
|
/// - Parameter endpoint: El path del endpoint de la API
|
|
/// - Returns: Un objeto URL, o nil si el string es inválido
|
|
///
|
|
/// # Ejemplo
|
|
/// ```swift
|
|
/// if let url = APIConfig.urlObject(for: "/manga/popular") {
|
|
/// var request = URLRequest(url: url)
|
|
/// // Hacer request...
|
|
/// }
|
|
/// ```
|
|
static func urlObject(for endpoint: String) -> URL? {
|
|
return URL(string: url(for: endpoint))
|
|
}
|
|
|
|
/// Retorna el timeout a usar para un tipo específico de request
|
|
///
|
|
/// - Parameter isResourceRequest: Si esta es una request intensiva de recursos (ej: descargar imágenes)
|
|
/// - Returns: El valor de timeout apropiado
|
|
///
|
|
/// # Ejemplo
|
|
/// ```swift
|
|
/// let timeout = APIConfig.timeoutFor(isResourceRequest: true)
|
|
/// // Retorna: 300.0 (downloadTimeout)
|
|
/// ```
|
|
static func timeoutFor(isResourceRequest: Bool = false) -> TimeInterval {
|
|
return isResourceRequest ? downloadTimeout : defaultTimeout
|
|
}
|
|
|
|
// MARK: - Validation
|
|
|
|
/// Valida que la configuración actual esté correctamente configurada
|
|
///
|
|
/// - Returns: `true` si la configuración parece válida, `false` en caso contrario
|
|
///
|
|
/// # Verificaciones Realizadas
|
|
/// - URL del servidor no está vacía
|
|
/// - Puerto está en rango válido (1-65535)
|
|
/// - Valores de timeout son positivos
|
|
/// - Cantidad de reintentos es no-negativa
|
|
///
|
|
/// # Uso
|
|
/// Llamar durante el inicio de la app para asegurar configuración válida:
|
|
/// ```swift
|
|
/// assert(APIConfig.isValid, "Configuración de API inválida")
|
|
/// ```
|
|
static var isValid: Bool {
|
|
// Verificar URL del servidor
|
|
guard !serverURL.isEmpty else { return false }
|
|
|
|
// Verificar rango de puerto
|
|
guard (1...65535).contains(port) else { return false }
|
|
|
|
// Verificar timeouts
|
|
guard defaultTimeout > 0 && downloadTimeout > 0 else { return false }
|
|
|
|
// Verificar cantidad de reintentos
|
|
guard maxRetries >= 0 else { return false }
|
|
|
|
return true
|
|
}
|
|
|
|
// MARK: - Endpoints
|
|
|
|
/// Estructura que contiene todos los endpoints de la API
|
|
enum Endpoints {
|
|
/// Endpoint para solicitar la descarga de un capítulo al VPS
|
|
///
|
|
/// El backend iniciará el proceso de descarga de las imágenes
|
|
/// y las almacenará en el VPS.
|
|
///
|
|
/// - Parameters:
|
|
/// - mangaSlug: Slug del manga a descargar
|
|
/// - chapterNumber: Número del capítulo
|
|
/// - Returns: URL completa del endpoint
|
|
static func download(mangaSlug: String, chapterNumber: Int) -> String {
|
|
return "\(basePath)/download/\(mangaSlug)/\(chapterNumber)"
|
|
}
|
|
|
|
/// Endpoint para verificar si un capítulo está descargado en el VPS
|
|
///
|
|
/// - Parameters:
|
|
/// - mangaSlug: Slug del manga
|
|
/// - chapterNumber: Número del capítulo
|
|
/// - Returns: URL completa del endpoint
|
|
static func checkDownloaded(mangaSlug: String, chapterNumber: Int) -> String {
|
|
return "\(basePath)/chapters/\(mangaSlug)/\(chapterNumber)"
|
|
}
|
|
|
|
/// Endpoint para listar todos los capítulos descargados de un manga
|
|
///
|
|
/// - Parameter mangaSlug: Slug del manga
|
|
/// - Returns: URL completa del endpoint
|
|
static func listChapters(mangaSlug: String) -> String {
|
|
return "\(basePath)/chapters/\(mangaSlug)"
|
|
}
|
|
|
|
/// Endpoint para obtener la URL de una imagen específica de un capítulo
|
|
///
|
|
/// - Parameters:
|
|
/// - mangaSlug: Slug del manga
|
|
/// - chapterNumber: Número del capítulo
|
|
/// - pageIndex: Índice de la página (0-based)
|
|
/// - Returns: URL completa del endpoint
|
|
static func getImage(mangaSlug: String, chapterNumber: Int, pageIndex: Int) -> String {
|
|
return "\(basePath)/images/\(mangaSlug)/\(chapterNumber)/\(pageIndex)"
|
|
}
|
|
|
|
/// Endpoint para eliminar un capítulo del VPS
|
|
///
|
|
/// - Parameters:
|
|
/// - mangaSlug: Slug del manga
|
|
/// - chapterNumber: Número del capítulo
|
|
/// - Returns: URL completa del endpoint
|
|
static func deleteChapter(mangaSlug: String, chapterNumber: Int) -> String {
|
|
return "\(basePath)/chapters/\(mangaSlug)/\(chapterNumber)"
|
|
}
|
|
|
|
/// Endpoint para obtener estadísticas de almacenamiento del VPS
|
|
///
|
|
/// - Returns: URL completa del endpoint
|
|
static func storageStats() -> String {
|
|
return "\(basePath)/storage/stats"
|
|
}
|
|
|
|
/// Endpoint para hacer ping al servidor (health check)
|
|
///
|
|
/// - Returns: URL completa del endpoint
|
|
static func health() -> String {
|
|
return "\(basePath)/health"
|
|
}
|
|
}
|
|
|
|
// MARK: - Error Codes
|
|
|
|
/// Códigos de error específicos de la API
|
|
enum ErrorCodes {
|
|
static let chapterNotFound = 40401
|
|
static let chapterAlreadyDownloaded = 40901
|
|
static let storageLimitExceeded = 50701
|
|
static let invalidImageFormat = 42201
|
|
static let downloadFailed = 50001
|
|
}
|
|
|
|
// MARK: - Environment Configuration
|
|
|
|
/// Configuración para entorno de desarrollo
|
|
///
|
|
/// # Uso
|
|
/// Para usar configuración de desarrollo, modificar en builds de desarrollo:
|
|
/// ```swift
|
|
/// #if DEBUG
|
|
/// static let serverURL = "http://192.168.1.100"
|
|
/// #else
|
|
/// static let serverURL = "https://gitea.cbcren.online"
|
|
/// #endif
|
|
/// ```
|
|
static var development: (serverURL: String, port: Int, timeout: TimeInterval, logging: Bool) {
|
|
return (
|
|
serverURL: "http://192.168.1.100",
|
|
port: 3001,
|
|
timeout: 60.0,
|
|
logging: true
|
|
)
|
|
}
|
|
|
|
/// Configuración para entorno de staging
|
|
static var staging: (serverURL: String, port: Int, timeout: TimeInterval, logging: Bool) {
|
|
return (
|
|
serverURL: "https://staging.cbcren.online",
|
|
port: 3001,
|
|
timeout: 30.0,
|
|
logging: true
|
|
)
|
|
}
|
|
|
|
/// Configuración para entorno de producción
|
|
static var production: (serverURL: String, port: Int, timeout: TimeInterval, logging: Bool) {
|
|
return (
|
|
serverURL: "https://gitea.cbcren.online",
|
|
port: 3001,
|
|
timeout: 30.0,
|
|
logging: false
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: - Debug Support
|
|
|
|
#if DEBUG
|
|
extension APIConfig {
|
|
/// Configuración de test para unit testing
|
|
///
|
|
/// # Uso
|
|
/// Usar esta configuración en unit tests para evitar hacer llamadas reales a la API:
|
|
/// ```swift
|
|
/// func testAPICall() {
|
|
/// let testConfig = APIConfig.testing
|
|
/// // Usar URL de servidor mock
|
|
/// }
|
|
/// ```
|
|
static var testing: (serverURL: String, port: Int, timeout: TimeInterval, logging: Bool) {
|
|
return (
|
|
serverURL: "http://localhost:3001",
|
|
port: 3001,
|
|
timeout: 5.0,
|
|
logging: true
|
|
)
|
|
}
|
|
|
|
/// Imprime la configuración actual a consola (solo debug)
|
|
static func printConfiguration() {
|
|
print("=== API Configuration ===")
|
|
print("Server URL: \(serverURL)")
|
|
print("Port: \(port)")
|
|
print("Base URL: \(baseURL)")
|
|
print("API Version: \(apiVersion)")
|
|
print("Default Timeout: \(defaultTimeout)s")
|
|
print("Download Timeout: \(downloadTimeout)s")
|
|
print("Max Retries: \(maxRetries)")
|
|
print("Logging Enabled: \(loggingEnabled)")
|
|
print("Cache Enabled: \(cacheExpiryTime > 0)")
|
|
print("=========================")
|
|
}
|
|
}
|
|
#endif
|
|
|