Skip to content
This repository was archived by the owner on Aug 17, 2025. It is now read-only.

agusnieto/pps

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Practica Profesional Supervisada

UTN - FRC - 2025 Tecnicatura Universitaria en Programación

Agustin Nieto Legajo: 405660

Este proyecto esta bajo la licencia Apache License 2.0.


General

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.


Documentacion


Parte 1: Tablas Soporte (A / B)

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

A

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.

B

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.


Parte 2: Productos y Reparaciones

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.


Parte 3: Facturacion

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

Sobrecarga de Metodos para guardar una Reparacion

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
}

Parte 4: Usuarios

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.


Parte 5: Spring Security

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

Flujo de una autenticacion desde el back

  1. user intenta entrar a un endpoint protegido
  2. spring sec intercepta
  3. redirecciona a login (302 Redirect)
  4. user ingresa credenciales
  5. spring sec usa CustomUserDetailsService
  6. valida activo
  7. actualiza fecha de login
  8. crea la sesion (JSESSIONID)
  9. user queda autentificado mediante la cookie

Parte 6: Guards Angular

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.

Flujo de una autenticacion desde el front

  1. Usuario -> POST /login (form-data)
  2. Spring Security -> CustomUserDetailsService
  3. Validación -> Usuario activo + credenciales correctas
  4. Session cookie -> HttpOnly JSESSIONID
  5. Frontend -> GET /auth/me (datos del usuario)
  6. Estado de Angular -> actualizo BehaviorSubject
  7. Guards -> Verifica el rol

AuthController.java: Back <-> Front

Endpoint /auth/me

@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.

AuthService en front

BehaviorSubject
private currentUserSubject = new BehaviorSubject<Usuario | null>(null);
public currentUser$ = this.currentUserSubject.asObservable();
  1. Estado inicial: null = no autenticado
  2. Subscribers reciben el estado actual
  3. getCurrentUser() en el mismo momento
Login: FormData() -> URLSearchParams()
// 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);
Cosas que espera el back
  • form-encoded, no multipart
  • Content-Type header tiene que estar igual
  • withCredentials: true lo mas importante para las cookies

Guards: Como maneje el race condition

Problema original
1. Guard se ejecuta -> currentUser$ = null
2. checkSession() termina -> currentUser$ = usuario
3. Guard ya te mando a el login -> Race condition basicamente
Solucion con take() + switchMap()
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)
AdminGuard vs EmployeeGuard
// 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);
}

Parte 7: Estructura del Frontend

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

Fabrica de paginas en front (generic-crud)

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.


Parte 8: Estructura del Backend

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

Endpoints declarativos

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).

About

Practica Profesional Supervisada - UTN - Tecnicatura Universitaria en Programación

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published