UTN - FRC - 2025 Tecnicatura Universitaria en Programación
Agustin Nieto Legajo: 405660
Este proyecto esta bajo la licencia Apache License 2.0.
El proyecto fue inspirado por principios UNIX, KISS, DRY, el paradigma declarativo.
Agradecimiento al software o servicios que use y sus versiones:
- Java 17.0.16 (Azul Zulu JDK)
- Maven 3.9.11
- Node.js 22.18.0
- npm 10.9.3
- Angular 20.1.4
- Docker 28.3.3
- Git 2.50.1
- LazyGit 0.54.2
- Vim 9.1
- IntelliJ IDEA Ultimate 2025.2-1 (AUR)
- Visual Studio Code 1.103.1-1 (AUR)
- Curl 8.15.0
- GNU bash 5.3.3
- Arch Linux
- ffmpeg 7.1.1 (Grabar video)
- GitHub
Nota: Despues de inicializar los contenedores, dejo un set de datos de muestra
en el archivo datos-de-prueba.sql
.
Las tablas soporte del proyecto tienen entidades de catalogo/referencia. Estan divididas en dos patrones, el A y el B enb ase a su necesidad:
Cada implementacion de servicio tiene un comentario de que tipo de tabla soporte es
Necesidad:
- Alto volumen de datos (100+ registros al menos)
- Necesita paginacion si o si
- No tiene PUT (Update) para proteger los datos historicos - Reutiliza entrys que fueron "desactivadas" (borrado logico)
Metodos:
Page<Entity> findAll(Pageable, String nombre, Boolean activo)
findById()
save()
delete()
Usado en: Marca, Categoria, Trabajo.
Necesidad:
- Bajo volumen de datos (5-10 registros normalmente)
- Lista completa, sin necesitar paginacion
- Si tiene PUT (Update) para datos que si se pueden cambiar
Metodos a diferencia de A:
List<Entity> findAll(String nombre, Boolean activo)
update()
Usado en: FormaPago, Rol, Sede.
Entidades que manejan las tablas soportes (A y B), pueden tener relaciones varios a varios, con borrado logico, y creacion automatica de dependencias. Esto significa, basicamente, que el mismo endpoint de creacion o actualizacion de Prod/Repa crea sus derivados soportes (A/B por igual), como tambien, maneja automaticamente las relaciones varios a varios (entidades ProdXSoporte).
Funciones:
- Gestionar relaciones (uno muchos / muchos muchos)
- Creacion o "reactivacion" automatica de soportes (A/B) en la creacion de un prod/reparacion
- "desactivacion" y "reactivacion" de relaciones muchos a muchos, y soportes (A/B).
- Reparaciones no te permite modificarlo si el campo de fecha de entrega esta utilizado.
Subquery de varios a varios para endpoints declarativos:
// COUNT() verifica que tenga TODAS las categorías (seria un AND)
subquery.select(builder.count(relacion.get("id")))
.where(categoria.get("nombre").in(nombresCategorias));
return builder.equal(subquery, (long) nombresCategorias.size());
Ejemplo: ["Guitarras", "Eléctricas"] -> filtra por productos con ambas categorias
Esto lo manda como una sola query, que dentro tiene una subquery cuando se manejan las especificaciones.
La facturacion orquesta metodos de productos y reparaciones, que estos a su vez, en base a sus estados actualizan stock y/o fechas.
Reparacion seria en si, como una especie de "Detalle" de los trabajos realizados a un servicio de instrumentos, entonces para aclarar la diferencia tenemos Detalle vs Reparacion:
Reparacion:
- Ciclo de vida dinamico
- Se puede actualizar constantemente hasta su entrega
- Existe sin una factura
- Se "sella" cuando se factura esa reparacion
- Si una facturacion con reparacion se cancela, se revierte el "sello" y vuelve a su ciclo de vida anterior
Detalle de factura:
- Solo existe como parte de una factura
- Imposible de modificar una vez creado
- Puede tener precios diferentes (ofertas) a las que vienen por defecto en el producto
- Necesarios para descontar stock
- No te deja facturar si no llegas con el stock
Como se puede "sellar" con una factura, se sobrecarga el metodo de save para contemplar ambas situaciones (ciclos de vida)
@Service
public interface ReparacionService {
// para usar de forma independiente
Reparacion save(ReparacionDTO dto);
// para orquestacion desde service de factura
Reparacion save(ReparacionDTO dto, FacturaEntity factura);
}
@Override
public Reparacion save(ReparacionDTO dto) {
return save(dto, null); // manda a sobrecarga
}
@Override
public Reparacion save(ReparacionDTO dto, FacturaEntity factura) {
// logica del save
}
Los usuarios son utilizados por spring security para generar las sesiones que se guardan en los navegadores.
Los emails son unique en la base de datos, y se maneja con cuidado el cambio de email para la gestion de administradores. Porque solamente los admin pueden gestionarlos. Se necesita un rol de administrador para gestionar usuarios. Cada logeo o creacion queda registrado.
Se genera la sesion y la cookie si el usuario esta activo, registra fecha de logeo. Usa el servicio de usuarios.
Matriz de permisos en la config: (permitAll) -> Publico (ADMINISTRADOR || EMPLEADO || LUTHIER) -> Operativo (ADMINISTRADOR) -> Gerencial
- user intenta entrar a un endpoint protegido
- spring sec intercepta
- redirecciona a login (302 Redirect)
- user ingresa credenciales
- spring sec usa CustomUserDetailsService
- valida activo
- actualiza fecha de login
- crea la sesion (JSESSIONID)
- user queda autentificado mediante la cookie
tuve que cambiar algunas cosas del backend, especificamente manejar cors y la config de spring security en el mismo lugar, porque los dos interferian, tambien cambie como spring security te redirigia en los logins, y hice que devuelva un json para los login/logout, porque si no era imposible hacerlo funcionar con angular, porque te redirigia a el backend.
Sessions vs JWT: Elegi las sesiones basadas en cookies para que el backend tenga el control para esta aplicacion.
"Guards Async": Habia un problema a la hora de chequear el guard, como el que lo maneja es el back, y javascript es single-threaded, la sesion y el guard eran null antes de que el back nos diera la sesion.
- Usuario -> POST /login (form-data)
- Spring Security -> CustomUserDetailsService
- Validación -> Usuario activo + credenciales correctas
- Session cookie -> HttpOnly JSESSIONID
- Frontend -> GET /auth/me (datos del usuario)
- Estado de Angular -> actualizo BehaviorSubject
- Guards -> Verifica el rol
@GetMapping("/me")
public ResponseEntity<UsuarioDTO> getCurrentUser(Authentication auth) {
String email = auth.getName(); // spring security
UsuarioEntity usuario = usuarioService.findByEmail(email);
return ResponseEntity.ok(new UsuarioDTO(...)); // dto sin la password
}
Auth != User CRUD El endpoint auth no tiene que ver o es un crud de users.
private currentUserSubject = new BehaviorSubject<Usuario | null>(null);
public currentUser$ = this.currentUserSubject.asObservable();
- Estado inicial:
null
= no autenticado - Subscribers reciben el estado actual
getCurrentUser()
en el mismo momento
// FormData = multipart/form-data
const formData = new FormData();
formData.append('username', email);
// URLSearchParams = application/x-www-form-urlencoded
const body = new URLSearchParams();
body.set('username', credentials.username);
- form-encoded, no multipart
- Content-Type header tiene que estar igual
- withCredentials: true lo mas importante para las cookies
1. Guard se ejecuta -> currentUser$ = null
2. checkSession() termina -> currentUser$ = usuario
3. Guard ya te mando a el login -> Race condition basicamente
canActivate(): Observable<boolean> {
return this.authService.currentUser$.pipe(
take(1), // solamente el primer valor
switchMap(user => {
if (!user) {
// verificar sesion activa antes
return this.authService.verifySession().pipe(
map(sessionActive => sessionActive ? this.checkUserAfterSession() : false)
);
}
return of(this.hasPermission(user));
})
);
}
- take(1): No te hace que te subscribas infinitamente
- switchMap: Logica async adentro del guard
- verifySession(): Tira un request real para confirmar
- Fallback: (no hay sesion = redirect) || (hay sesion = permitir)
// ADMINISTRADOR (Gestion de usuarios)
private hasPermission(user: Usuario): boolean {
return user.rol === 'ADMINISTRADOR';
}
// ADMINISTRADOR + EMPLEADO + LUTHIER (Cualquier cosa que no sea para el publico)
private hasEmployeeAccess(rol: string): boolean {
return ['ADMINISTRADOR', 'EMPLEADO', 'LUTHIER'].includes(rol);
}
El front esta dividido en dos partes, la publica y la administrativa.
El usuario no sabe que podes entrar y autentificarte con /login
en la url de la pagina.
Como se compone basicamente todo: Frontend Publico
- E-commerce: catalogo, carrito, checkout
- Servicios: solicitud de reparaciones
- Marketing: newsletter, contacto
- Informacion: preguntas frecuentes (FAQ)
Frontend Admin (Autenticado)
- Dashboard: multiples graficos
- Gestion: CRUD completo con filtros y paginacion (Productos/Reparaciones/Usuarios)
- Facturacion: caja (facturacion presencial), historial de ventas (+ cancelacion)
.
├── public ------------------------- imagenes, logos, texturas
└── src
├── app
│ ├── admin ------------------ paginas protegidas
│ │ ├── components
│ │ │ ├── dashboard ------ charts y estadisticas
│ │ │ ├── facturacion ---- caja y historial
│ │ │ ├── newsletter ----- para mandar emails
│ │ │ ├── productos
│ │ │ ├── reparaciones
│ │ │ └── usuarios
│ │ ├── services
│ │ ├── shared ------------- cosas que tienen todas las paginas admin (nav y sidebar)
│ │ │ ├── admin-layout
│ │ │ └── admin-sidebar
│ │ └── test-admin --------- para probar conexion al back
│ ├── auth
│ │ ├── guards
│ │ ├── login -------------- pagina para logearse
│ │ └── services
│ ├── core
│ │ ├── models
│ │ └── services
│ ├── features --------------- paginas publicas que ve cualquier usuario
│ │ ├── cart
│ │ ├── catalog
│ │ ├── checkout
│ │ │ ├── payment-modal
│ │ │ └── status-modal
│ │ ├── contacto
│ │ ├── faq
│ │ ├── landing
│ │ ├── mercado-pago
│ │ └── repair-request
│ └── shared
│ ├── components
│ │ └── generic-crud --- fabrica de paginas CRUD generica
│ ├── header
│ ├── interfaces
│ └── product-card
└── environments
Basicamente, las paginas de gestion de productos, usuarios, y reparaciones, tienen una estructura similar, lo que me llevo a declarar una pagina, interfaces, y servicios genericos, lo que nos lleva a que en vez de crear por separado 3 paginas de 2000 lineas cada una, tengas que implementar el crud y "configurarlo" con unas 100-200 lineas a lo sumo.
src/main/
├── java
│ └── tup
│ └── pps
│ ├── configs ------ spring sec, cors, autentificacion, matriz de permisos
│ ├── controllers -- aclaro que todos los endpoints get son declarativos y solo hay uno para todo
│ ├── dtos
│ │ └── usuarios
│ ├── entities
│ ├── exceptions --- exepciones custom
│ ├── models
│ ├── repositories
│ │ └── specs ---- specificaciones para declarar querys al repositorio
│ └── services
│ └── impl
└── resources ---------------- config de spring, api keys, archivos sql para inicializar datos si se usa h2
Los endpoints estan pensados para usarse siempre por una sola forma de entrar a pedir informacion. "Construis" la query o la declaras. Basicamente en vez de tener muchos endpoints distintos para diferentes cosas, tenes uno, declaras busquedas opcionales, y lo armas para lo que necesitas.
Dashboard -> Pagina con 10000 items por ejemplo Query nomal -> Traer todo default Query especifica -> Traer con especificaciones Query especifica multiple -> Traer con arreglo de especificaciones
Si bien es mas dificil implementarlo al principio, despues es mucho mas flexible meter nuevas features.
Esto se usa con todos los controllers, da igual si son o no paginados (para soportes A/B).