Sistema de gestión de préstamos de dispositivos para laboratorio. Permite registrar equipos, categorizarlos, controlar su estado y llevar un historial completo de préstamos y devoluciones.
- 📦 CRUD completo de dispositivos y categorías
- 🔄 Control de estado de dispositivos (
disponible/prestado) - 📋 Historial de préstamos por dispositivo
- 🖼️ Gestión de imágenes con Cloudinary
- 🔐 Autenticación con registro y login (contraseñas encriptadas con BCrypt)
- 🌐 CORS habilitado para cualquier origen
- 🔌 Connection pooling con HikariCP
- 📄 Documentación interactiva con Swagger UI (solo Ktor)
Este proyecto cuenta con dos versiones del backend. Ambas comparten la misma base de datos PostgreSQL y la misma funcionalidad, pero difieren en tecnología y arquitectura.
| Javalin (v1) | Ktor (v2) | |
|---|---|---|
| Lenguaje | Java 17+ | Kotlin 2.1 |
| Framework | Javalin 6 | Ktor 3.x |
| ORM | JDBC puro | Exposed ORM |
| Arquitectura | Capas (MVC) | Hexagonal |
| Puerto | 7001 |
8080 |
| Swagger | ❌ | ✅ /swagger |
| Build | Gradle | Gradle Kotlin DSL |
⚠️ Importante para la app Android: Si usas el backend Javalin, los endpoints de autenticación son/loginy/registro. Si usas Ktor, son/usuarios/loginy/usuarios/registro. ActualizaRetrofitClient.ktsegún cuál uses. Ver sección Configuración Android.
HTTP Request
│
▼
Router (Routes*) → Define los endpoints
│
▼
Controller (*Controller) → Maneja la request/response HTTP
│
▼
Service (*Service) → Lógica de negocio y validaciones
│
▼
Repository (*Repository) → Acceso a base de datos (JDBC puro)
│
▼
PostgreSQL
| Tecnología | Uso |
|---|---|
| Java 17+ | Lenguaje principal |
| Javalin 6 | Framework HTTP |
| PostgreSQL | Base de datos relacional |
| HikariCP | Pool de conexiones |
| Cloudinary | Almacenamiento de imágenes |
| BCrypt | Encriptación de contraseñas |
| dotenv-java | Variables de entorno |
src/main/java/williamsel/
├── Main.java
├── config/
│ ├── DBconfig.java
│ └── Inicio.java
├── model/
│ ├── Usuario.java
│ ├── Categoria.java
│ ├── Dispositivo.java
│ └── Prestamo.java
├── repository/
│ ├── UsuarioRepository.java
│ ├── CategoriaRepository.java
│ ├── DispositivoRepository.java
│ └── PrestamoRepository.java
├── service/
│ ├── UsuarioService.java
│ ├── CategoriaService.java
│ ├── DispositivoService.java
│ ├── PrestamoService.java
│ └── ClouudinaryService.java
├── controller/
│ ├── UsuarioController.java
│ ├── CategoriaController.java
│ ├── DispositivoController.java
│ └── PrestamoController.java
└── routers/
├── RoutesUsuario.java
├── RouteCategoria.java
├── RoutesDispositivo.java
└── RoutesPrestamos.java
./gradlew runAPI disponible en: http://localhost:7001
Base URL: http://localhost:7001
| Método | Endpoint | Body |
|---|---|---|
POST |
/registro |
{ "correo": "", "contrasena": "" } |
POST |
/login |
{ "correo": "", "contrasena": "" } |
| Método | Endpoint | Body |
|---|---|---|
GET |
/categorias |
— |
GET |
/categorias/{id} |
— |
POST |
/categorias |
{ "nombre": "" } |
PUT |
/categorias/{id} |
{ "nombre": "" } |
DELETE |
/categorias/{id} |
— |
| Método | Endpoint | Body |
|---|---|---|
GET |
/dispositivos |
— |
GET |
/dispositivos?q=texto |
— |
GET |
/dispositivos/{id} |
— |
POST |
/dispositivos |
form-data: nombre, categoria_id, imagen |
PUT |
/dispositivos/{id} |
{ "nombre": "", "categoriaId": 1, "estado": "" } |
PUT |
/dispositivos/{id}/imagen |
form-data: imagen |
PATCH |
/dispositivos/{id}/estado |
{ "estado": "disponible" } |
DELETE |
/dispositivos/{id} |
— |
| Método | Endpoint | Body |
|---|---|---|
GET |
/prestamos |
— |
GET |
/prestamos/dispositivo/{id} |
— |
POST |
/prestamos/prestar |
{ "dispositivo_id": 1 } |
PUT |
/prestamos/devolver/{dispositivoId} |
— |
src/main/kotlin/com/labkeep/
│
├── domain/ ← NÚCLEO (sin dependencias externas)
│ ├── model/
│ │ ├── Models.kt ← Entidades de dominio
│ │ └── Exceptions.kt ← Excepciones de dominio
│ └── port/
│ ├── input/ ← Puertos de entrada (interfaces de casos de uso)
│ └── output/ ← Puertos de salida (interfaces de repos y storage)
│
├── application/ ← CASOS DE USO
│ ├── usecase/
│ │ ├── UsuarioUseCases.kt
│ │ ├── CategoriaUseCases.kt
│ │ ├── DispositivoUseCases.kt
│ │ └── PrestamoUseCases.kt
│ └── dto/
│ └── Dtos.kt
│
└── infrastructure/ ← ADAPTADORES
├── adapter/
│ ├── http/
│ │ ├── routes/ ← Rutas Ktor por módulo
│ │ └── plugins/ ← Serialización, CORS, StatusPages, Swagger
│ ├── persistence/
│ │ └── repository/ ← Repositorios con Exposed ORM
│ └── cloudinary/ ← Adaptador Cloudinary
└── config/
├── DatabaseConfig.kt
└── AppContainer.kt ← Wiring de dependencias
| Tecnología | Uso |
|---|---|
| Kotlin 2.1 | Lenguaje principal |
| Ktor 3.x | Framework HTTP |
| Exposed ORM | Acceso a base de datos |
| PostgreSQL | Base de datos relacional |
| HikariCP | Pool de conexiones |
| Cloudinary | Almacenamiento de imágenes |
| BCrypt | Encriptación de contraseñas |
| Swagger UI | Documentación en /swagger |
| Kotlinx Serialization | Serialización JSON |
./gradlew runAPI disponible en: http://localhost:8080
Swagger UI: http://localhost:8080/swagger
Base URL: http://localhost:8080
| Método | Endpoint | Body |
|---|---|---|
POST |
/usuarios/registro |
{ "correo": "", "contrasena": "" } |
POST |
/usuarios/login |
{ "correo": "", "contrasena": "" } |
| Método | Endpoint | Body |
|---|---|---|
GET |
/categorias |
— |
GET |
/categorias/{id} |
— |
POST |
/categorias |
{ "nombre": "" } |
PUT |
/categorias/{id} |
{ "nombre": "" } |
DELETE |
/categorias/{id} |
— |
| Método | Endpoint | Body |
|---|---|---|
GET |
/dispositivos?q=... |
— |
GET |
/dispositivos/{id} |
— |
POST |
/dispositivos |
form-data: nombre, categoria_id, imagen |
PUT |
/dispositivos/{id} |
{ "nombre": "", "categoriaId": 1, "estado": "" } |
PUT |
/dispositivos/{id}/imagen |
form-data: imagen |
PATCH |
/dispositivos/{id}/estado |
{ "estado": "disponible" } |
DELETE |
/dispositivos/{id} |
— |
| Método | Endpoint | Body |
|---|---|---|
GET |
/prestamos |
— |
GET |
/prestamos/dispositivo/{id} |
— |
POST |
/prestamos/prestar |
{ "dispositivoId": 1, "usuarioId": 2 } |
PUT |
/prestamos/devolver/{dispositivoId} |
— |
- Dominio → modelo en
Models.kt+ excepciones si aplica - Puertos → interfaces en
InputPorts.ktyOutputPorts.kt - Aplicación → casos de uso en
XxxUseCases.kt - Infraestructura → repositorio + tabla Exposed + rutas Ktor
- Config → registrar en
AppContainer.ktyApplication.kt
Compartida por ambos backends.
usuario
├── id SERIAL PK
├── correo VARCHAR(255) UNIQUE
└── contrasena VARCHAR(255)
categoria
├── id SERIAL PK
└── nombre VARCHAR(100)
dispositivo
├── id SERIAL PK
├── nombre VARCHAR(200)
├── categoria_id FK → categoria.id
├── estado VARCHAR(50) DEFAULT 'disponible'
├── imagen_url VARCHAR(500)
├── imagen_public_id VARCHAR(200)
└── fecha_creacion TIMESTAMP DEFAULT NOW()
prestamo
├── id SERIAL PK
├── dispositivo_id FK → dispositivo.id
├── usuario_id FK → usuario.id
├── fecha_prestamo TIMESTAMP DEFAULT NOW()
└── fecha_devolucion TIMESTAMP (NULL = activo)Ambos backends usan el mismo .env:
DB_HOST=localhost
DB_PORT=5432
DB_NAME=labkeep
DB_USER=tu_usuario
DB_PASS=tu_contraseña
CLOUDINARY_CLOUD_NAME=tu_cloud_name
CLOUDINARY_API_KEY=tu_api_key
CLOUDINARY_API_SECRET=tu_api_secretCliente móvil desarrollado en Kotlin con Jetpack Compose y arquitectura Clean Architecture por features.
| Tecnología | Uso |
|---|---|
| Kotlin | Lenguaje principal |
| Jetpack Compose | UI declarativa |
| ViewModel + UIState | Gestión de estado por pantalla |
| Retrofit | Cliente HTTP para consumir la API REST |
| Navigation Compose | Navegación entre pantallas |
| Clean Architecture | Separación en capas: data, domain, presentacion |
feature/
├── data/
│ ├── datasource/
│ │ ├── api/ → Interface Retrofit
│ │ ├── mapper/ → DTO → Entidad de dominio
│ │ └── models/ → DTOs (respuesta de la API)
│ └── repositories/ → Implementación del repositorio
├── di/
│ └── *Provider.kt → Inyección de dependencias manual
├── domain/
│ ├── entities/ → Modelos de negocio
│ ├── repositories/ → Interface del repositorio
│ └── usescases/ → Casos de uso
└── presentacion/
├── screens/ → Pantalla Composable + UIState
├── components/ → Componentes reutilizables
└── viewmodels/ → ViewModel + ViewModelFactory
app/src/main/java/com/williamsel/labkeep/
├── MainActivity.kt
├── core/
│ ├── navigation/
│ │ └── NavigationWrapper.kt
│ └── network/
│ └── RetrofitClient.kt
├── features/
│ ├── login/
│ ├── inventario/
│ ├── descripciondispositivo/
│ ├── nuevodispositivo/
│ ├── editardispositivo/
│ └── eliminardispositivo/
└── ui/theme/
├── Color.kt
├── Theme.kt
└── Type.kt
| Pantalla | Descripción |
|---|---|
LoginScreen |
Inicio de sesión con correo y contraseña |
InventarioScreen |
Lista todos los dispositivos disponibles |
DescripcionDispositivoScreen |
Detalle completo de un dispositivo |
NuevoDispositivoScreen |
Formulario para registrar un nuevo dispositivo |
EditarDispositivoScreen |
Editar nombre, categoría o estado |
EliminarDispositivoScreen |
Confirmación y eliminación de dispositivo |
En RetrofitClient.kt configura la BASE_URL según el backend que uses:
// ✅ Si usas Javalin (v1) — sin cambios en endpoints de auth
private const val BASE_URL = "http://TU_IP:7001/"
// ✅ Si usas Ktor (v2) — debes actualizar los endpoints de auth en la app
private const val BASE_URL = "http://TU_IP:8080/"
⚠️ Si cambias a Ktor, actualiza en tu feature de login los endpoints:
/login→/usuarios/login/registro→/usuarios/registroEl resto de endpoints (categorías, dispositivos, préstamos) son iguales en ambos backends.
⚠️ Si pruebas en emulador usa10.0.2.2como IP. Si usas dispositivo físico, ambos deben estar en la misma red WiFi.
Todo el contenido de este repositorio, incluyendo el código fuente, diseño gráfico, está protegido por las leyes de propiedad intelectual.
- Uso permitido: Consulta, aprendizaje y exhibición personal.
- Prohibiciones: Se prohíbe estrictamente la copia parcial o total para uso comercial, la reventa del software.
© 2026 Williams-Espinosa. Todos los derechos reservados.