From 1a39fa69c985ab4410b9ce56e6960c4db8bea55a Mon Sep 17 00:00:00 2001 From: AngelMlt03 Date: Thu, 9 Apr 2026 18:02:12 +0200 Subject: [PATCH 01/19] ft: Roles para Usuarios --- .../dto/request/UsuarioRequestDTO.java | 2 + .../dto/response/UsuarioResponseDTO.java | 2 + .../business/mapper/UsuarioMapper.java | 2 + .../backend/business/model/Usuario.java | 5 ++ .../backend/business/model/enums/Rol.java | 6 +++ .../main/resources/db/migration/V2__fixes.sql | 44 +--------------- .../db/migration/V3__add_rol_usuario.sql | 50 +++++++++++++++++++ .../backend/mapper/UsuarioMapperTest.java | 3 ++ .../backend/service/CarritoServiceTest.java | 3 +- .../service/CompraVentaServiceTest.java | 3 ++ .../service/IntercambioServiceTest.java | 5 +- .../service/PostIntercambioServiceTest.java | 3 +- .../backend/service/PostVentaServiceTest.java | 3 +- .../backend/service/ReviewServiceTest.java | 5 +- .../backend/service/UsuarioServiceTest.java | 2 + 15 files changed, 88 insertions(+), 50 deletions(-) create mode 100644 backend/src/main/java/com/tfg/angel/gameswap/backend/business/model/enums/Rol.java create mode 100644 backend/src/main/resources/db/migration/V3__add_rol_usuario.sql diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/business/dto/request/UsuarioRequestDTO.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/business/dto/request/UsuarioRequestDTO.java index 5d4d3f8..9c039e3 100644 --- a/backend/src/main/java/com/tfg/angel/gameswap/backend/business/dto/request/UsuarioRequestDTO.java +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/business/dto/request/UsuarioRequestDTO.java @@ -1,5 +1,6 @@ package com.tfg.angel.gameswap.backend.business.dto.request; +import com.tfg.angel.gameswap.backend.business.model.enums.Rol; import lombok.*; import java.time.LocalDate; @@ -15,4 +16,5 @@ public class UsuarioRequestDTO { private String nUsuario; private LocalDate fechaNacimiento; private String correo; + private Rol rol; } \ No newline at end of file diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/business/dto/response/UsuarioResponseDTO.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/business/dto/response/UsuarioResponseDTO.java index d61a304..8a0aca1 100644 --- a/backend/src/main/java/com/tfg/angel/gameswap/backend/business/dto/response/UsuarioResponseDTO.java +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/business/dto/response/UsuarioResponseDTO.java @@ -1,5 +1,6 @@ package com.tfg.angel.gameswap.backend.business.dto.response; +import com.tfg.angel.gameswap.backend.business.model.enums.Rol; import lombok.*; import java.time.LocalDate; @@ -18,4 +19,5 @@ public class UsuarioResponseDTO { private Double saldo; private String correo; private Double estrellas; + private Rol rol; } diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/business/mapper/UsuarioMapper.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/business/mapper/UsuarioMapper.java index 4b7c0de..0314b80 100644 --- a/backend/src/main/java/com/tfg/angel/gameswap/backend/business/mapper/UsuarioMapper.java +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/business/mapper/UsuarioMapper.java @@ -15,6 +15,7 @@ public static Usuario toEntity(UsuarioRequestDTO dto) { .correo(dto.getCorreo()) .saldo(0.0) .estrellas(0.0) + .rol(dto.getRol()) .build(); } @@ -27,6 +28,7 @@ public static UsuarioResponseDTO toDTO(Usuario usuario) { .saldo(usuario.getSaldo()) .correo(usuario.getCorreo()) .estrellas(usuario.getEstrellas()) + .rol(usuario.getRol()) .build(); } } diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/business/model/Usuario.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/business/model/Usuario.java index f389197..51f0b69 100644 --- a/backend/src/main/java/com/tfg/angel/gameswap/backend/business/model/Usuario.java +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/business/model/Usuario.java @@ -1,5 +1,6 @@ package com.tfg.angel.gameswap.backend.business.model; +import com.tfg.angel.gameswap.backend.business.model.enums.Rol; import jakarta.persistence.*; import lombok.*; @@ -32,6 +33,10 @@ public class Usuario { private Double estrellas = 0.0; + @Enumerated(EnumType.STRING) + @Column(name = "rol") + private Rol rol; + // Relaciones @OneToMany(mappedBy = "comprador", fetch = FetchType.LAZY) diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/business/model/enums/Rol.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/business/model/enums/Rol.java new file mode 100644 index 0000000..01ecf16 --- /dev/null +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/business/model/enums/Rol.java @@ -0,0 +1,6 @@ +package com.tfg.angel.gameswap.backend.business.model.enums; + +public enum Rol { + ADMIN, + CLIENTE +} diff --git a/backend/src/main/resources/db/migration/V2__fixes.sql b/backend/src/main/resources/db/migration/V2__fixes.sql index 4b3ba33..a3568aa 100644 --- a/backend/src/main/resources/db/migration/V2__fixes.sql +++ b/backend/src/main/resources/db/migration/V2__fixes.sql @@ -46,46 +46,4 @@ ADD COLUMN id_post_intercambio INTEGER REFERENCES PostIntercambio(id); ALTER TABLE ProductoCarrito DROP COLUMN id_producto; ALTER TABLE ProductoCarrito -ADD COLUMN id_post_venta INTEGER REFERENCES PostVenta(id); - - --- 6. DATOS DE PRUEBA (opcional pero OK) - -INSERT INTO usuario (id, correo, estrellas, fecha_nacimiento, nombre, n_usuario, saldo) -VALUES -(1, 'victor@gmail.com', 2, CURRENT_DATE, 'Victor', 'Victor02', 90), -(2, 'andres@gmail.com', 3, CURRENT_DATE, 'Andrés', 'Andrés03', 110); - -INSERT INTO producto (id, estado, id_api, nombre) -VALUES -(1, 'NUEVO', 1, 'Tablet'), -(2, 'USADO', 2, 'Mesa'); - -INSERT INTO postventa (id, estado, precio, id_producto, id_vendedor) -VALUES -(1, 'ACTIVO', 30, 1, 2), -(2, 'FINALIZADO', 15, 1, 2); - -INSERT INTO postintercambio (id, estado, id_producto, id_producto_cambio, id_usuario) -VALUES -(1, 'ACTIVO', 1, 2, 2); - -INSERT INTO intercambio (id, fecha, id_post_intercambio, id_usuario_cambio) -VALUES -(1, CURRENT_DATE, 1, 2); - -INSERT INTO compraventa (id, fecha, precio, id_comprador, id_post_venta) -VALUES -(1, CURRENT_DATE, 100, 1, 1); - -INSERT INTO review (id, contenido, estrellas, id_reviewed, id_reviewer) -VALUES -(1, 'Muy bien', 3, 1, 2); - -INSERT INTO carrito (id, coste, id_usuario) -VALUES -(1, 40, 1); - -INSERT INTO productocarrito (id, id_carrito, id_post_venta) -VALUES -(1, 1, 2); \ No newline at end of file +ADD COLUMN id_post_venta INTEGER REFERENCES PostVenta(id); \ No newline at end of file diff --git a/backend/src/main/resources/db/migration/V3__add_rol_usuario.sql b/backend/src/main/resources/db/migration/V3__add_rol_usuario.sql new file mode 100644 index 0000000..5f94976 --- /dev/null +++ b/backend/src/main/resources/db/migration/V3__add_rol_usuario.sql @@ -0,0 +1,50 @@ +-- V3: Se añade la columna rol_usuario a la tabla Usuario + +ALTER TABLE Usuario +ADD COLUMN rol VARCHAR(20) DEFAULT 'CLIENTE'; + +ALTER TABLE Usuario +ADD CONSTRAINT chk_usuario_rol CHECK (rol IN ('ADMIN', 'CLIENTE')); + +-- Datos de prueba + +INSERT INTO usuario (id, correo, estrellas, fecha_nacimiento, nombre, n_usuario, saldo, rol) +VALUES +(1, 'victor@gmail.com', 2, CURRENT_DATE, 'Victor', 'Victor02', 90, 'CLIENTE'), +(2, 'andres@gmail.com', 3, CURRENT_DATE, 'Andrés', 'Andrés03', 110, 'CLIENTE'), +(3, 'admin@gmail.com', 5, CURRENT_DATE, 'Admin', 'Admin01', 9999, 'ADMIN'); + + +INSERT INTO producto (id, estado, id_api, nombre) +VALUES +(1, 'NUEVO', 1, 'Tablet'), +(2, 'USADO', 2, 'Mesa'); + +INSERT INTO postventa (id, estado, precio, id_producto, id_vendedor) +VALUES +(1, 'ACTIVO', 30, 1, 2), +(2, 'FINALIZADO', 15, 1, 2); + +INSERT INTO postintercambio (id, estado, id_producto, id_producto_cambio, id_usuario) +VALUES +(1, 'ACTIVO', 1, 2, 2); + +INSERT INTO intercambio (id, fecha, id_post_intercambio, id_usuario_cambio) +VALUES +(1, CURRENT_DATE, 1, 2); + +INSERT INTO compraventa (id, fecha, precio, id_comprador, id_post_venta) +VALUES +(1, CURRENT_DATE, 100, 1, 1); + +INSERT INTO review (id, contenido, estrellas, id_reviewed, id_reviewer) +VALUES +(1, 'Muy bien', 3, 1, 2); + +INSERT INTO carrito (id, coste, id_usuario) +VALUES +(1, 40, 1); + +INSERT INTO productocarrito (id, id_carrito, id_post_venta) +VALUES +(1, 1, 2); \ No newline at end of file diff --git a/backend/src/test/java/com/tfg/angel/gameswap/backend/mapper/UsuarioMapperTest.java b/backend/src/test/java/com/tfg/angel/gameswap/backend/mapper/UsuarioMapperTest.java index c644375..8045178 100644 --- a/backend/src/test/java/com/tfg/angel/gameswap/backend/mapper/UsuarioMapperTest.java +++ b/backend/src/test/java/com/tfg/angel/gameswap/backend/mapper/UsuarioMapperTest.java @@ -2,6 +2,7 @@ import com.tfg.angel.gameswap.backend.business.mapper.UsuarioMapper; import com.tfg.angel.gameswap.backend.business.model.Usuario; +import com.tfg.angel.gameswap.backend.business.model.enums.Rol; import org.junit.jupiter.api.Test; import java.time.LocalDate; @@ -20,6 +21,7 @@ void toDTO_ok() { .fechaNacimiento(LocalDate.now()) .saldo(100.0) .estrellas(5.0) + .rol(Rol.CLIENTE) .build(); var dto = UsuarioMapper.toDTO(usuario); @@ -31,5 +33,6 @@ void toDTO_ok() { assertEquals("a@gmail.com", dto.getCorreo()); assertEquals(100.0, dto.getSaldo()); assertEquals(5.0, dto.getEstrellas()); + assertEquals(Rol.CLIENTE, dto.getRol()); } } diff --git a/backend/src/test/java/com/tfg/angel/gameswap/backend/service/CarritoServiceTest.java b/backend/src/test/java/com/tfg/angel/gameswap/backend/service/CarritoServiceTest.java index f122998..4fe4e5f 100644 --- a/backend/src/test/java/com/tfg/angel/gameswap/backend/service/CarritoServiceTest.java +++ b/backend/src/test/java/com/tfg/angel/gameswap/backend/service/CarritoServiceTest.java @@ -3,6 +3,7 @@ import com.tfg.angel.gameswap.backend.business.dto.request.CarritoRequestDTO; import com.tfg.angel.gameswap.backend.business.dto.response.CarritoResponseDTO; import com.tfg.angel.gameswap.backend.business.model.*; +import com.tfg.angel.gameswap.backend.business.model.enums.Rol; import com.tfg.angel.gameswap.backend.business.repository.*; import com.tfg.angel.gameswap.backend.business.service.impl.CarritoServiceImpl; import com.tfg.angel.gameswap.backend.exception.GSBadRequestException; @@ -44,7 +45,7 @@ class CarritoServiceTest { @BeforeEach void setUp() { - usuario = Usuario.builder().id(1L).nombre("Angel").build(); + usuario = Usuario.builder().id(1L).nombre("Angel").rol(Rol.CLIENTE).build(); carrito = Carrito.builder() .id(10L) diff --git a/backend/src/test/java/com/tfg/angel/gameswap/backend/service/CompraVentaServiceTest.java b/backend/src/test/java/com/tfg/angel/gameswap/backend/service/CompraVentaServiceTest.java index f950f30..f33d36b 100644 --- a/backend/src/test/java/com/tfg/angel/gameswap/backend/service/CompraVentaServiceTest.java +++ b/backend/src/test/java/com/tfg/angel/gameswap/backend/service/CompraVentaServiceTest.java @@ -7,6 +7,7 @@ import com.tfg.angel.gameswap.backend.business.model.Usuario; import com.tfg.angel.gameswap.backend.business.model.enums.EstadoPost; import com.tfg.angel.gameswap.backend.business.model.enums.EstadoProducto; +import com.tfg.angel.gameswap.backend.business.model.enums.Rol; import com.tfg.angel.gameswap.backend.business.repository.CompraVentaRepository; import com.tfg.angel.gameswap.backend.business.repository.PostVentaRepository; import com.tfg.angel.gameswap.backend.business.repository.UsuarioRepository; @@ -58,12 +59,14 @@ void setUp() { comprador = Usuario.builder() .id(1L) .nombre("Comprador") + .rol(Rol.CLIENTE) .saldo(100.0) .build(); vendedor = Usuario.builder() .id(2L) .nombre("Vendedor") + .rol(Rol.CLIENTE) .saldo(50.0) .build(); diff --git a/backend/src/test/java/com/tfg/angel/gameswap/backend/service/IntercambioServiceTest.java b/backend/src/test/java/com/tfg/angel/gameswap/backend/service/IntercambioServiceTest.java index 862f07a..d3eeb75 100644 --- a/backend/src/test/java/com/tfg/angel/gameswap/backend/service/IntercambioServiceTest.java +++ b/backend/src/test/java/com/tfg/angel/gameswap/backend/service/IntercambioServiceTest.java @@ -4,6 +4,7 @@ import com.tfg.angel.gameswap.backend.business.model.*; import com.tfg.angel.gameswap.backend.business.model.enums.EstadoPost; import com.tfg.angel.gameswap.backend.business.model.enums.EstadoProducto; +import com.tfg.angel.gameswap.backend.business.model.enums.Rol; import com.tfg.angel.gameswap.backend.business.repository.*; import com.tfg.angel.gameswap.backend.business.service.impl.IntercambioServiceImpl; import com.tfg.angel.gameswap.backend.exception.GSBadRequestException; @@ -43,8 +44,8 @@ class IntercambioServiceTest { @BeforeEach void setUp() { - usuarioProducto = Usuario.builder().id(1L).nombre("Angel").build(); - usuarioIntercambio = Usuario.builder().id(2L).nombre("Juan").build(); + usuarioProducto = Usuario.builder().id(1L).nombre("Angel").rol(Rol.CLIENTE).build(); + usuarioIntercambio = Usuario.builder().id(2L).nombre("Juan").rol(Rol.CLIENTE).build(); Producto producto = Producto.builder() .id(1L) diff --git a/backend/src/test/java/com/tfg/angel/gameswap/backend/service/PostIntercambioServiceTest.java b/backend/src/test/java/com/tfg/angel/gameswap/backend/service/PostIntercambioServiceTest.java index a3aa8c7..328c5e6 100644 --- a/backend/src/test/java/com/tfg/angel/gameswap/backend/service/PostIntercambioServiceTest.java +++ b/backend/src/test/java/com/tfg/angel/gameswap/backend/service/PostIntercambioServiceTest.java @@ -6,6 +6,7 @@ import com.tfg.angel.gameswap.backend.business.model.Producto; import com.tfg.angel.gameswap.backend.business.model.Usuario; import com.tfg.angel.gameswap.backend.business.model.enums.EstadoProducto; +import com.tfg.angel.gameswap.backend.business.model.enums.Rol; import com.tfg.angel.gameswap.backend.business.repository.PostIntercambioRepository; import com.tfg.angel.gameswap.backend.business.repository.ProductoRepository; import com.tfg.angel.gameswap.backend.business.repository.UsuarioRepository; @@ -45,7 +46,7 @@ class PostIntercambioServiceTest { @BeforeEach void setUp() { - usuario = Usuario.builder().id(1L).nombre("Angel").build(); + usuario = Usuario.builder().id(1L).nombre("Angel").rol(Rol.CLIENTE).build(); producto = Producto.builder() .id(1L) diff --git a/backend/src/test/java/com/tfg/angel/gameswap/backend/service/PostVentaServiceTest.java b/backend/src/test/java/com/tfg/angel/gameswap/backend/service/PostVentaServiceTest.java index 8d29700..f49067e 100644 --- a/backend/src/test/java/com/tfg/angel/gameswap/backend/service/PostVentaServiceTest.java +++ b/backend/src/test/java/com/tfg/angel/gameswap/backend/service/PostVentaServiceTest.java @@ -7,6 +7,7 @@ import com.tfg.angel.gameswap.backend.business.model.Usuario; import com.tfg.angel.gameswap.backend.business.model.enums.EstadoPost; import com.tfg.angel.gameswap.backend.business.model.enums.EstadoProducto; +import com.tfg.angel.gameswap.backend.business.model.enums.Rol; import com.tfg.angel.gameswap.backend.business.repository.PostVentaRepository; import com.tfg.angel.gameswap.backend.business.repository.ProductoRepository; import com.tfg.angel.gameswap.backend.business.repository.UsuarioRepository; @@ -48,7 +49,7 @@ class PostVentaServiceTest { @BeforeEach void setUp() { - vendedor = Usuario.builder().id(1L).nombre("Angel").build(); + vendedor = Usuario.builder().id(1L).nombre("Angel").rol(Rol.CLIENTE).build(); producto = Producto.builder() .id(1L) diff --git a/backend/src/test/java/com/tfg/angel/gameswap/backend/service/ReviewServiceTest.java b/backend/src/test/java/com/tfg/angel/gameswap/backend/service/ReviewServiceTest.java index 9a84aea..dd7a9c9 100644 --- a/backend/src/test/java/com/tfg/angel/gameswap/backend/service/ReviewServiceTest.java +++ b/backend/src/test/java/com/tfg/angel/gameswap/backend/service/ReviewServiceTest.java @@ -4,6 +4,7 @@ import com.tfg.angel.gameswap.backend.business.dto.response.ReviewResponseDTO; import com.tfg.angel.gameswap.backend.business.model.Review; import com.tfg.angel.gameswap.backend.business.model.Usuario; +import com.tfg.angel.gameswap.backend.business.model.enums.Rol; import com.tfg.angel.gameswap.backend.business.repository.ReviewRepository; import com.tfg.angel.gameswap.backend.business.repository.UsuarioRepository; import com.tfg.angel.gameswap.backend.business.service.impl.ReviewServiceImpl; @@ -42,8 +43,8 @@ class ReviewServiceTest { @BeforeEach void setUp() { - reviewer = Usuario.builder().id(1L).nombre("Angel").build(); - reviewed = Usuario.builder().id(2L).nombre("Juan").build(); + reviewer = Usuario.builder().id(1L).nombre("Angel").rol(Rol.CLIENTE).build(); + reviewed = Usuario.builder().id(2L).nombre("Juan").rol(Rol.CLIENTE).build(); validDto = ReviewRequestDTO.builder() .idReviewer(1L) diff --git a/backend/src/test/java/com/tfg/angel/gameswap/backend/service/UsuarioServiceTest.java b/backend/src/test/java/com/tfg/angel/gameswap/backend/service/UsuarioServiceTest.java index 211ccaf..95c4808 100644 --- a/backend/src/test/java/com/tfg/angel/gameswap/backend/service/UsuarioServiceTest.java +++ b/backend/src/test/java/com/tfg/angel/gameswap/backend/service/UsuarioServiceTest.java @@ -3,6 +3,7 @@ import com.tfg.angel.gameswap.backend.business.dto.request.UsuarioRequestDTO; import com.tfg.angel.gameswap.backend.business.dto.response.UsuarioResponseDTO; import com.tfg.angel.gameswap.backend.business.model.Usuario; +import com.tfg.angel.gameswap.backend.business.model.enums.Rol; import com.tfg.angel.gameswap.backend.business.repository.UsuarioRepository; import com.tfg.angel.gameswap.backend.business.service.impl.UsuarioServiceImpl; import com.tfg.angel.gameswap.backend.exception.GSBadRequestException; @@ -49,6 +50,7 @@ void setUp() { .nombre("Angel") .nombreUsuario("angel_dev") .correo("angel@example.com") + .rol(Rol.CLIENTE) .build(); } From ce9764042c3d21fff5d9a370897ac488530d0e29 Mon Sep 17 00:00:00 2001 From: AngelMlt03 Date: Thu, 9 Apr 2026 23:07:30 +0200 Subject: [PATCH 02/19] =?UTF-8?q?ft:=20Configuraci=C3=B3n=20roles,=20acces?= =?UTF-8?q?os=20a=20endpoints=20e=20inicio=20de=20sesi=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/pom.xml | 16 ++++ .../backend/business/model/Usuario.java | 2 + .../backend/config/SpringSecurityConfig.java | 29 ------- .../backend/security/AuthController.java | 75 +++++++++++++++++++ .../backend/security/AuthRequest.java | 3 + .../backend/security/AuthResponse.java | 3 + .../{config => security}/CorsConfig.java | 4 +- .../gameswap/backend/security/JwtFilter.java | 53 +++++++++++++ .../gameswap/backend/security/JwtService.java | 63 ++++++++++++++++ .../backend/security/PasswordConfig.java | 15 ++++ .../security/SpringSecurityConfig.java | 52 +++++++++++++ .../security/TokenBlacklistService.java | 20 +++++ .../backend/security/UsuarioDetails.java | 44 +++++++++++ .../security/UsuarioDetailsService.java | 21 ++++++ .../db/migration/V3__add_rol_usuario.sql | 15 ++-- 15 files changed, 378 insertions(+), 37 deletions(-) delete mode 100644 backend/src/main/java/com/tfg/angel/gameswap/backend/config/SpringSecurityConfig.java create mode 100644 backend/src/main/java/com/tfg/angel/gameswap/backend/security/AuthController.java create mode 100644 backend/src/main/java/com/tfg/angel/gameswap/backend/security/AuthRequest.java create mode 100644 backend/src/main/java/com/tfg/angel/gameswap/backend/security/AuthResponse.java rename backend/src/main/java/com/tfg/angel/gameswap/backend/{config => security}/CorsConfig.java (90%) create mode 100644 backend/src/main/java/com/tfg/angel/gameswap/backend/security/JwtFilter.java create mode 100644 backend/src/main/java/com/tfg/angel/gameswap/backend/security/JwtService.java create mode 100644 backend/src/main/java/com/tfg/angel/gameswap/backend/security/PasswordConfig.java create mode 100644 backend/src/main/java/com/tfg/angel/gameswap/backend/security/SpringSecurityConfig.java create mode 100644 backend/src/main/java/com/tfg/angel/gameswap/backend/security/TokenBlacklistService.java create mode 100644 backend/src/main/java/com/tfg/angel/gameswap/backend/security/UsuarioDetails.java create mode 100644 backend/src/main/java/com/tfg/angel/gameswap/backend/security/UsuarioDetailsService.java diff --git a/backend/pom.xml b/backend/pom.xml index 99decbe..5605717 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -103,6 +103,22 @@ runtime + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + + diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/business/model/Usuario.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/business/model/Usuario.java index 51f0b69..e405a02 100644 --- a/backend/src/main/java/com/tfg/angel/gameswap/backend/business/model/Usuario.java +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/business/model/Usuario.java @@ -37,6 +37,8 @@ public class Usuario { @Column(name = "rol") private Rol rol; + private String password; + // Relaciones @OneToMany(mappedBy = "comprador", fetch = FetchType.LAZY) diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/config/SpringSecurityConfig.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/config/SpringSecurityConfig.java deleted file mode 100644 index e0c2fe8..0000000 --- a/backend/src/main/java/com/tfg/angel/gameswap/backend/config/SpringSecurityConfig.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.tfg.angel.gameswap.backend.config; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.web.SecurityFilterChain; - -@Configuration -public class SpringSecurityConfig { - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .csrf(csrf -> csrf.disable()) - .authorizeHttpRequests(auth -> auth - .requestMatchers( - "/swagger-ui/**", - "/v3/api-docs/**", - "/v3/api-docs.yaml" - ).permitAll() - .anyRequest().permitAll() - ) - .httpBasic(httpBasic -> httpBasic.disable()) - .formLogin(form -> form.disable()); - - return http.build(); - } - -} diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/AuthController.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/AuthController.java new file mode 100644 index 0000000..40e5cfa --- /dev/null +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/AuthController.java @@ -0,0 +1,75 @@ +package com.tfg.angel.gameswap.backend.security; + +import com.tfg.angel.gameswap.backend.exception.GSBadRequestException; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import com.tfg.angel.gameswap.backend.business.repository.UsuarioRepository; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/auth") +@RequiredArgsConstructor +public class AuthController { + + private final UsuarioRepository usuarioRepository; + private final JwtService jwtService; + private final PasswordEncoder passwordEncoder; + private final TokenBlacklistService tokenBlacklistService; + + @PostMapping("/login") + public AuthResponse login(@RequestBody AuthRequest request) { + + var usuario = usuarioRepository.findByNombreUsuario(request.username()) + .orElseThrow(() -> new RuntimeException("Usuario no encontrado")); + + if (!passwordEncoder.matches(request.password(), usuario.getPassword())) { + throw new GSBadRequestException("Constraseña incorrecta"); + } + + String accessToken = jwtService.generateToken( + usuario.getNombreUsuario(), + usuario.getRol().name() + ); + + String refreshToken = jwtService.generateRefreshToken(usuario.getNombreUsuario()); + + return new AuthResponse(accessToken, refreshToken); + } + + @PostMapping("/refresh") + public String refresh(@RequestParam String refreshToken) { + + if (!jwtService.isValid(refreshToken)) { + throw new GSBadRequestException("Refresh token inválido"); + } + + String username = jwtService.extractUsername(refreshToken); + + var usuario = usuarioRepository.findByNombreUsuario(username) + .orElseThrow(); + + return jwtService.generateToken( + usuario.getNombreUsuario(), + usuario.getRol().name() + ); + } + + @PostMapping("/logout") + public void logout(HttpServletRequest request) { + + String header = request.getHeader("Authorization"); + + if (header != null && header.startsWith("Bearer ")) { + String token = header.substring(7); + tokenBlacklistService.blacklist(token); + } + } + + @GetMapping("/admin/test") + @PreAuthorize("hasRole('ADMIN')") + public String adminOnly() { + return "ok"; + } +} diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/AuthRequest.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/AuthRequest.java new file mode 100644 index 0000000..5475049 --- /dev/null +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/AuthRequest.java @@ -0,0 +1,3 @@ +package com.tfg.angel.gameswap.backend.security; + +public record AuthRequest(String username, String password) {} diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/AuthResponse.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/AuthResponse.java new file mode 100644 index 0000000..883deb9 --- /dev/null +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/AuthResponse.java @@ -0,0 +1,3 @@ +package com.tfg.angel.gameswap.backend.security; + +public record AuthResponse(String accessToken, String refreshToken) {} diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/config/CorsConfig.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/CorsConfig.java similarity index 90% rename from backend/src/main/java/com/tfg/angel/gameswap/backend/config/CorsConfig.java rename to backend/src/main/java/com/tfg/angel/gameswap/backend/security/CorsConfig.java index 8da7d85..f824406 100644 --- a/backend/src/main/java/com/tfg/angel/gameswap/backend/config/CorsConfig.java +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/CorsConfig.java @@ -1,4 +1,4 @@ -package com.tfg.angel.gameswap.backend.config; +package com.tfg.angel.gameswap.backend.security; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -35,7 +35,7 @@ public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/api/**", config); + source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/JwtFilter.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/JwtFilter.java new file mode 100644 index 0000000..75cdc21 --- /dev/null +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/JwtFilter.java @@ -0,0 +1,53 @@ +package com.tfg.angel.gameswap.backend.security; + +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; + +import java.io.IOException; + +@Component +@RequiredArgsConstructor +public class JwtFilter extends GenericFilter { + + private final JwtService jwtService; + private final UsuarioDetailsService userDetailsService; + private final TokenBlacklistService tokenBlacklistService; + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + HttpServletRequest req = (HttpServletRequest) request; + String header = req.getHeader("Authorization"); + + if (header != null && header.startsWith("Bearer ")) { + + String token = header.substring(7); + + if (tokenBlacklistService.isBlacklisted(token)) { + chain.doFilter(request, response); + return; + } + + if (jwtService.isValid(token)) { + + String username = jwtService.extractUsername(token); + var userDetails = userDetailsService.loadUserByUsername(username); + + var auth = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + + SecurityContextHolder.getContext().setAuthentication(auth); + } + } + + chain.doFilter(request, response); + } +} diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/JwtService.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/JwtService.java new file mode 100644 index 0000000..14f387e --- /dev/null +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/JwtService.java @@ -0,0 +1,63 @@ +package com.tfg.angel.gameswap.backend.security; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import org.springframework.stereotype.Service; + +import java.security.Key; +import java.util.Date; + +@Service +public class JwtService { + + private final String SECRET = "esto_es_una_clave_super_segura_123456"; + private final long EXPIRATION = 1000 * 60 * 60; // 1 hora + + private Key getKey() { + return Keys.hmacShaKeyFor(SECRET.getBytes()); + } + + public String generateToken(String username, String rol) { + return Jwts.builder() + .setSubject(username) + .claim("rol", rol) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION)) + .signWith(getKey(), SignatureAlgorithm.HS256) + .compact(); + } + + public String generateRefreshToken(String username) { + return Jwts.builder() + .setSubject(username) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + (1000L * 60 * 60 * 24))) // 24 horas + .signWith(getKey(), SignatureAlgorithm.HS256) + .compact(); + } + + public String extractUsername(String token) { + return getClaims(token).getSubject(); + } + + public String extractRol(String token) { + return getClaims(token).get("rol", String.class); + } + + public boolean isValid(String token) { + try { + getClaims(token); + return true; + } catch (Exception e) { + return false; + } + } + + private Claims getClaims(String token) { + return Jwts.parserBuilder() + .setSigningKey(getKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/PasswordConfig.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/PasswordConfig.java new file mode 100644 index 0000000..cf28f4c --- /dev/null +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/PasswordConfig.java @@ -0,0 +1,15 @@ +package com.tfg.angel.gameswap.backend.security; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class PasswordConfig { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/SpringSecurityConfig.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/SpringSecurityConfig.java new file mode 100644 index 0000000..a1abc81 --- /dev/null +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/SpringSecurityConfig.java @@ -0,0 +1,52 @@ +package com.tfg.angel.gameswap.backend.security; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.*; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.web.*; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@RequiredArgsConstructor +@EnableMethodSecurity +public class SpringSecurityConfig { + + private final JwtFilter jwtFilter; + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + + return http + .csrf(csrf -> csrf.disable()) + .cors(cors -> {}) + + .authorizeHttpRequests(auth -> auth + + // públicas + .requestMatchers( + "/auth/**", + "/swagger-ui/**", + "/v3/api-docs/**", + "/v3/api-docs.yaml" + ).permitAll() + + // admin + .requestMatchers("/admin/**").hasRole("ADMIN") + + // cliente + .requestMatchers("/cliente/**").hasAnyRole("CLIENTE", "ADMIN") + + // resto protegido + .anyRequest().authenticated() + ) + + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) + + .httpBasic(httpBasic -> httpBasic.disable()) + .formLogin(form -> form.disable()) + + .build(); + } + +} diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/TokenBlacklistService.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/TokenBlacklistService.java new file mode 100644 index 0000000..0dc85ed --- /dev/null +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/TokenBlacklistService.java @@ -0,0 +1,20 @@ +package com.tfg.angel.gameswap.backend.security; + +import org.springframework.stereotype.Service; + +import java.util.HashSet; +import java.util.Set; + +@Service +public class TokenBlacklistService { + + private final Set blacklist = new HashSet<>(); + + public void blacklist(String token) { + blacklist.add(token); + } + + public boolean isBlacklisted(String token) { + return blacklist.contains(token); + } +} diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/UsuarioDetails.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/UsuarioDetails.java new file mode 100644 index 0000000..15d62a2 --- /dev/null +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/UsuarioDetails.java @@ -0,0 +1,44 @@ +package com.tfg.angel.gameswap.backend.security; + +import com.tfg.angel.gameswap.backend.business.model.Usuario; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.List; + +public class UsuarioDetails implements UserDetails { + + private final Usuario usuario; + + public UsuarioDetails(Usuario usuario) { + this.usuario = usuario; + } + + @Override + public List getAuthorities() { + return List.of(new SimpleGrantedAuthority("ROLE_" + usuario.getRol().name())); + } + + @Override + public String getPassword() { + return usuario.getPassword(); + } + + @Override + public String getUsername() { + return usuario.getNombreUsuario(); + } + + public Long getId() { + return usuario.getId(); + } + + @Override public boolean isAccountNonExpired() { return true; } + + @Override public boolean isAccountNonLocked() { return true; } + + @Override public boolean isCredentialsNonExpired() { return true; } + + @Override public boolean isEnabled() { return true; } + +} diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/UsuarioDetailsService.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/UsuarioDetailsService.java new file mode 100644 index 0000000..d6d1d47 --- /dev/null +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/UsuarioDetailsService.java @@ -0,0 +1,21 @@ +package com.tfg.angel.gameswap.backend.security; + +import com.tfg.angel.gameswap.backend.business.repository.UsuarioRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.userdetails.*; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class UsuarioDetailsService implements UserDetailsService { + + private final UsuarioRepository usuarioRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + var usuario = usuarioRepository.findByNombreUsuario(username) + .orElseThrow(() -> new UsernameNotFoundException("Usuario no encontrado")); + + return new UsuarioDetails(usuario); + } +} diff --git a/backend/src/main/resources/db/migration/V3__add_rol_usuario.sql b/backend/src/main/resources/db/migration/V3__add_rol_usuario.sql index 5f94976..e5d7b06 100644 --- a/backend/src/main/resources/db/migration/V3__add_rol_usuario.sql +++ b/backend/src/main/resources/db/migration/V3__add_rol_usuario.sql @@ -1,4 +1,4 @@ --- V3: Se añade la columna rol_usuario a la tabla Usuario +-- V3: Se añade la columna rol_usuario y password a la tabla Usuario ALTER TABLE Usuario ADD COLUMN rol VARCHAR(20) DEFAULT 'CLIENTE'; @@ -6,14 +6,17 @@ ADD COLUMN rol VARCHAR(20) DEFAULT 'CLIENTE'; ALTER TABLE Usuario ADD CONSTRAINT chk_usuario_rol CHECK (rol IN ('ADMIN', 'CLIENTE')); +ALTER TABLE Usuario +ADD COLUMN password VARCHAR(255); +-- '$2a$10$bKGDwSWcxsXf4tngOy3X3uHgp9xR4d4lpkcd2o15XVjIIWQ6bUeia'; -> "1234" + -- Datos de prueba -INSERT INTO usuario (id, correo, estrellas, fecha_nacimiento, nombre, n_usuario, saldo, rol) +INSERT INTO usuario (id, correo, estrellas, fecha_nacimiento, nombre, n_usuario, saldo, rol, password) VALUES -(1, 'victor@gmail.com', 2, CURRENT_DATE, 'Victor', 'Victor02', 90, 'CLIENTE'), -(2, 'andres@gmail.com', 3, CURRENT_DATE, 'Andrés', 'Andrés03', 110, 'CLIENTE'), -(3, 'admin@gmail.com', 5, CURRENT_DATE, 'Admin', 'Admin01', 9999, 'ADMIN'); - +(1, 'victor@gmail.com', 2, CURRENT_DATE, 'Victor', 'Victor02', 90, 'CLIENTE', '$2a$10$bKGDwSWcxsXf4tngOy3X3uHgp9xR4d4lpkcd2o15XVjIIWQ6bUeia'), +(2, 'andres@gmail.com', 3, CURRENT_DATE, 'Andrés', 'Andrés03', 110, 'CLIENTE', '$2a$10$bKGDwSWcxsXf4tngOy3X3uHgp9xR4d4lpkcd2o15XVjIIWQ6bUeia'), +(3, 'admin@gmail.com', 5, CURRENT_DATE, 'Admin', 'Admin01', 9999, 'ADMIN', '$2a$10$bKGDwSWcxsXf4tngOy3X3uHgp9xR4d4lpkcd2o15XVjIIWQ6bUeia'); INSERT INTO producto (id, estado, id_api, nombre) VALUES From cd158ac421b764f22a39f7b258f6d1d70a972167 Mon Sep 17 00:00:00 2001 From: AngelMlt03 Date: Sat, 11 Apr 2026 20:31:24 +0200 Subject: [PATCH 03/19] =?UTF-8?q?ft:=20Autorizaci=C3=B3n,=20contexto=20y?= =?UTF-8?q?=20sesi=C3=B3n=20de=20usuario?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/CarritoController.java | 4 +- .../business/service/CarritoService.java | 2 +- .../service/impl/CarritoServiceImpl.java | 10 ++++- .../gameswap/backend/security/JwtFilter.java | 31 +++++++------ .../backend/security/SecurityUtils.java | 12 +++++ .../security/SpringSecurityConfig.java | 20 ++++++--- .../controller/CarritoControllerTest.java | 2 +- .../backend/service/CarritoServiceTest.java | 44 ++++++++++++++----- 8 files changed, 87 insertions(+), 38 deletions(-) create mode 100644 backend/src/main/java/com/tfg/angel/gameswap/backend/security/SecurityUtils.java diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/business/controller/CarritoController.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/business/controller/CarritoController.java index 885eb78..9a12354 100644 --- a/backend/src/main/java/com/tfg/angel/gameswap/backend/business/controller/CarritoController.java +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/business/controller/CarritoController.java @@ -26,9 +26,7 @@ public CarritoResponseDTO findByUser(@PathVariable Long id) { @PostMapping("/add/{idPostVenta}") public CarritoResponseDTO addProduct(@PathVariable Long idPostVenta) { - - Long idUsuario = 1L; - return service.addProduct(idPostVenta, idUsuario); + return service.addProduct(idPostVenta); } @PostMapping("/remove") diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/business/service/CarritoService.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/business/service/CarritoService.java index ede6a39..b7299ef 100644 --- a/backend/src/main/java/com/tfg/angel/gameswap/backend/business/service/CarritoService.java +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/business/service/CarritoService.java @@ -9,7 +9,7 @@ public interface CarritoService { CarritoResponseDTO findByUser(Long idUsuario); - CarritoResponseDTO addProduct(Long idPostVenta, Long idUsuario); + CarritoResponseDTO addProduct(Long idPostVenta); CarritoResponseDTO removeProduct(Long idProductoCarrito); diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/business/service/impl/CarritoServiceImpl.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/business/service/impl/CarritoServiceImpl.java index c986fe3..745d87c 100644 --- a/backend/src/main/java/com/tfg/angel/gameswap/backend/business/service/impl/CarritoServiceImpl.java +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/business/service/impl/CarritoServiceImpl.java @@ -9,6 +9,7 @@ import com.tfg.angel.gameswap.backend.business.service.CarritoService; import com.tfg.angel.gameswap.backend.exception.GSBadRequestException; import com.tfg.angel.gameswap.backend.exception.GSNotFoundException; +import com.tfg.angel.gameswap.backend.security.SecurityUtils; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -46,9 +47,14 @@ public CarritoResponseDTO findByUser(Long idUsuario) { } @Override - public CarritoResponseDTO addProduct(Long idPostVenta, Long idUsuario) { + public CarritoResponseDTO addProduct(Long idPostVenta) { - Carrito carrito = carritoRepository.findByUsuarioId(idUsuario) + String username = SecurityUtils.getUsername(); + + Usuario usuario = usuarioRepository.findByNombreUsuario(username) + .orElseThrow(() -> new GSNotFoundException("Usuario no encontrado")); + + Carrito carrito = carritoRepository.findByUsuarioId(usuario.getId()) .orElseThrow(() -> new GSNotFoundException("Carrito no encontrado")); PostVenta postVenta = postVentaRepository.findById(idPostVenta) diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/JwtFilter.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/JwtFilter.java index 75cdc21..fa6cf9a 100644 --- a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/JwtFilter.java +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/JwtFilter.java @@ -1,28 +1,30 @@ package com.tfg.angel.gameswap.backend.security; -import jakarta.servlet.*; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @Component @RequiredArgsConstructor -public class JwtFilter extends GenericFilter { +public class JwtFilter extends OncePerRequestFilter { private final JwtService jwtService; private final UsuarioDetailsService userDetailsService; private final TokenBlacklistService tokenBlacklistService; @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain + ) throws ServletException, IOException { - HttpServletRequest req = (HttpServletRequest) request; - String header = req.getHeader("Authorization"); + String header = request.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { @@ -36,18 +38,19 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha if (jwtService.isValid(token)) { String username = jwtService.extractUsername(token); - var userDetails = userDetailsService.loadUserByUsername(username); - var auth = new UsernamePasswordAuthenticationToken( - userDetails, - null, - userDetails.getAuthorities() - ); + if (SecurityContextHolder.getContext().getAuthentication() == null) { - SecurityContextHolder.getContext().setAuthentication(auth); + var userDetails = userDetailsService.loadUserByUsername(username); + var auth = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + SecurityContextHolder.getContext().setAuthentication(auth); + } } } - chain.doFilter(request, response); } } diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/SecurityUtils.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/SecurityUtils.java new file mode 100644 index 0000000..0a76e89 --- /dev/null +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/SecurityUtils.java @@ -0,0 +1,12 @@ +package com.tfg.angel.gameswap.backend.security; + +import org.springframework.security.core.context.SecurityContextHolder; + +public class SecurityUtils { + + public static String getUsername() { + return SecurityContextHolder.getContext() + .getAuthentication() + .getName(); + } +} diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/SpringSecurityConfig.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/SpringSecurityConfig.java index a1abc81..55180c0 100644 --- a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/SpringSecurityConfig.java +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/SpringSecurityConfig.java @@ -4,6 +4,7 @@ import org.springframework.context.annotation.*; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.*; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @@ -23,26 +24,31 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .authorizeHttpRequests(auth -> auth - // públicas + // Públicos .requestMatchers( - "/auth/**", + "/auth/login", + "/auth/logout", "/swagger-ui/**", "/v3/api-docs/**", "/v3/api-docs.yaml" ).permitAll() - // admin - .requestMatchers("/admin/**").hasRole("ADMIN") + // Admin + .requestMatchers("/auth/admin/**").hasRole("ADMIN") - // cliente - .requestMatchers("/cliente/**").hasAnyRole("CLIENTE", "ADMIN") + // API - Usuarios logueados + .requestMatchers("/api/**").authenticated() - // resto protegido + // Lo demás .anyRequest().authenticated() ) .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) + .sessionManagement(sess -> + sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .httpBasic(httpBasic -> httpBasic.disable()) .formLogin(form -> form.disable()) diff --git a/backend/src/test/java/com/tfg/angel/gameswap/backend/controller/CarritoControllerTest.java b/backend/src/test/java/com/tfg/angel/gameswap/backend/controller/CarritoControllerTest.java index 56c7553..6478859 100644 --- a/backend/src/test/java/com/tfg/angel/gameswap/backend/controller/CarritoControllerTest.java +++ b/backend/src/test/java/com/tfg/angel/gameswap/backend/controller/CarritoControllerTest.java @@ -24,7 +24,7 @@ void testAllMethods() { controller.delete(1L); verify(service).findByUser(1L); - verify(service).addProduct(1L, 1L); + verify(service).addProduct(1L); verify(service).removeProduct(1L); verify(service).create(carritoRequestDTO); verify(service).delete(1L); diff --git a/backend/src/test/java/com/tfg/angel/gameswap/backend/service/CarritoServiceTest.java b/backend/src/test/java/com/tfg/angel/gameswap/backend/service/CarritoServiceTest.java index 4fe4e5f..ad11a36 100644 --- a/backend/src/test/java/com/tfg/angel/gameswap/backend/service/CarritoServiceTest.java +++ b/backend/src/test/java/com/tfg/angel/gameswap/backend/service/CarritoServiceTest.java @@ -8,13 +8,13 @@ import com.tfg.angel.gameswap.backend.business.service.impl.CarritoServiceImpl; import com.tfg.angel.gameswap.backend.exception.GSBadRequestException; import com.tfg.angel.gameswap.backend.exception.GSNotFoundException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Mock; +import org.mockito.*; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; import java.util.List; import java.util.Optional; @@ -45,7 +45,18 @@ class CarritoServiceTest { @BeforeEach void setUp() { - usuario = Usuario.builder().id(1L).nombre("Angel").rol(Rol.CLIENTE).build(); + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication( + new UsernamePasswordAuthenticationToken("angelUser", null, List.of()) + ); + SecurityContextHolder.setContext(context); + + usuario = Usuario.builder() + .id(1L) + .nombre("Angel") + .nombreUsuario("angelUser") + .rol(Rol.CLIENTE) + .build(); carrito = Carrito.builder() .id(10L) @@ -60,11 +71,17 @@ void setUp() { .build(); } + @AfterEach + void cleanUp() { + SecurityContextHolder.clearContext(); + } + @Test @DisplayName("Debe crear un carrito vacío para un usuario") void create_Success() { CarritoRequestDTO dto = new CarritoRequestDTO(1L); + when(usuarioRepository.findById(1L)).thenReturn(Optional.of(usuario)); when(carritoRepository.save(any(Carrito.class))).thenReturn(carrito); @@ -78,11 +95,12 @@ void create_Success() { @DisplayName("Debe añadir un producto al carrito y actualizar el coste") void addProduct_Success() { + when(usuarioRepository.findByNombreUsuario("angelUser")).thenReturn(Optional.of(usuario)); when(carritoRepository.findByUsuarioId(1L)).thenReturn(Optional.of(carrito)); when(postVentaRepository.findById(100L)).thenReturn(Optional.of(postVenta)); when(productoCarritoRepository.existsByCarritoIdAndPostVentaId(10L, 100L)).thenReturn(false); - CarritoResponseDTO result = carritoService.addProduct(100L, 1L); + CarritoResponseDTO result = carritoService.addProduct(100L); assertNotNull(result); assertEquals(30.0, carrito.getCoste()); @@ -94,13 +112,15 @@ void addProduct_Success() { @DisplayName("Debe lanzar GSBadRequestException si el producto ya está en el carrito") void addProduct_ThrowsBadRequest_WhenAlreadyInCart() { + when(usuarioRepository.findByNombreUsuario("angelUser")).thenReturn(Optional.of(usuario)); when(carritoRepository.findByUsuarioId(1L)).thenReturn(Optional.of(carrito)); when(postVentaRepository.findById(100L)).thenReturn(Optional.of(postVenta)); when(productoCarritoRepository.existsByCarritoIdAndPostVentaId(10L, 100L)).thenReturn(true); assertThrows(GSBadRequestException.class, () -> - carritoService.addProduct(100L, 1L) + carritoService.addProduct(100L) ); + verify(carritoRepository, never()).save(any()); } @@ -109,6 +129,7 @@ void addProduct_ThrowsBadRequest_WhenAlreadyInCart() { void removeProduct_Success() { carrito.setCoste(50.0); + ProductoCarrito pc = ProductoCarrito.builder() .id(500L) .carrito(carrito) @@ -120,7 +141,7 @@ void removeProduct_Success() { CarritoResponseDTO result = carritoService.removeProduct(500L); assertNotNull(result); - assertEquals(20.0, carrito.getCoste()); // 50.0 - 30.0 + assertEquals(20.0, carrito.getCoste()); verify(productoCarritoRepository).delete(pc); verify(carritoRepository).save(carrito); } @@ -130,6 +151,7 @@ void removeProduct_Success() { void removeProduct_CostNotNegative() { carrito.setCoste(10.0); + ProductoCarrito pc = ProductoCarrito.builder() .id(500L) .carrito(carrito) @@ -149,6 +171,8 @@ void findByUser_ThrowsNotFound() { when(carritoRepository.findByUsuarioId(1L)).thenReturn(Optional.empty()); - assertThrows(GSNotFoundException.class, () -> carritoService.findByUser(1L)); + assertThrows(GSNotFoundException.class, () -> + carritoService.findByUser(1L) + ); } } \ No newline at end of file From 87b9b5d5aa599f5f67b0a4bce4e6eae8a339c221 Mon Sep 17 00:00:00 2001 From: AngelMlt03 Date: Sat, 11 Apr 2026 20:55:38 +0200 Subject: [PATCH 04/19] =?UTF-8?q?fix:=20Reestructuraci=C3=B3n=20de=20fiche?= =?UTF-8?q?ros=20del=20proyecto=20frontend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/app/app.routes.ts | 2 +- frontend/src/app/{ => core}/models/usuario.model.ts | 0 frontend/src/app/{ => core}/services/usuario.service.ts | 0 .../src/app/{ => features}/usuarios/usuarios.component.css | 0 .../src/app/{ => features}/usuarios/usuarios.component.html | 0 .../app/{ => features}/usuarios/usuarios.component.spec.ts | 0 .../src/app/{ => features}/usuarios/usuarios.component.ts | 4 ++-- 7 files changed, 3 insertions(+), 3 deletions(-) rename frontend/src/app/{ => core}/models/usuario.model.ts (100%) rename frontend/src/app/{ => core}/services/usuario.service.ts (100%) rename frontend/src/app/{ => features}/usuarios/usuarios.component.css (100%) rename frontend/src/app/{ => features}/usuarios/usuarios.component.html (100%) rename frontend/src/app/{ => features}/usuarios/usuarios.component.spec.ts (100%) rename frontend/src/app/{ => features}/usuarios/usuarios.component.ts (86%) diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index cd82dcc..ab8e861 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,5 +1,5 @@ import { Routes } from '@angular/router'; -import { UsuariosComponent } from './usuarios/usuarios.component'; +import { UsuariosComponent } from './features/usuarios/usuarios.component'; export const routes: Routes = [ { path: 'usuarios', component: UsuariosComponent } diff --git a/frontend/src/app/models/usuario.model.ts b/frontend/src/app/core/models/usuario.model.ts similarity index 100% rename from frontend/src/app/models/usuario.model.ts rename to frontend/src/app/core/models/usuario.model.ts diff --git a/frontend/src/app/services/usuario.service.ts b/frontend/src/app/core/services/usuario.service.ts similarity index 100% rename from frontend/src/app/services/usuario.service.ts rename to frontend/src/app/core/services/usuario.service.ts diff --git a/frontend/src/app/usuarios/usuarios.component.css b/frontend/src/app/features/usuarios/usuarios.component.css similarity index 100% rename from frontend/src/app/usuarios/usuarios.component.css rename to frontend/src/app/features/usuarios/usuarios.component.css diff --git a/frontend/src/app/usuarios/usuarios.component.html b/frontend/src/app/features/usuarios/usuarios.component.html similarity index 100% rename from frontend/src/app/usuarios/usuarios.component.html rename to frontend/src/app/features/usuarios/usuarios.component.html diff --git a/frontend/src/app/usuarios/usuarios.component.spec.ts b/frontend/src/app/features/usuarios/usuarios.component.spec.ts similarity index 100% rename from frontend/src/app/usuarios/usuarios.component.spec.ts rename to frontend/src/app/features/usuarios/usuarios.component.spec.ts diff --git a/frontend/src/app/usuarios/usuarios.component.ts b/frontend/src/app/features/usuarios/usuarios.component.ts similarity index 86% rename from frontend/src/app/usuarios/usuarios.component.ts rename to frontend/src/app/features/usuarios/usuarios.component.ts index 80924e9..b17ed9b 100644 --- a/frontend/src/app/usuarios/usuarios.component.ts +++ b/frontend/src/app/features/usuarios/usuarios.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { UsuarioService } from '../services/usuario.service'; -import { Usuario } from '../models/usuario.model'; +import { UsuarioService } from '../../core/services/usuario.service'; +import { Usuario } from '../../core/models/usuario.model'; @Component({ selector: 'app-usuarios', From 42fc1ef898c1fbcffa8554b56c2ee0d4a9c6ef53 Mon Sep 17 00:00:00 2001 From: AngelMlt03 Date: Tue, 14 Apr 2026 22:38:17 +0200 Subject: [PATCH 05/19] =?UTF-8?q?ft:=20Inicio=20y=20registro=20de=20sesi?= =?UTF-8?q?=C3=B3n=20y=20UI=20b=C3=A1sica?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/security/AuthController.java | 34 ++++++++++ .../security/SpringSecurityConfig.java | 3 +- .../security/{ => records}/AuthRequest.java | 2 +- .../security/{ => records}/AuthResponse.java | 2 +- .../security/records/RegisterRequest.java | 8 +++ .../db/migration/V3__add_rol_usuario.sql | 6 +- frontend/src/app/app.component.ts | 10 +-- frontend/src/app/app.config.ts | 11 +++- frontend/src/app/app.routes.ts | 33 +++++++++- frontend/src/app/core/guards/auth.guard.ts | 16 +++++ .../app/core/interceptors/auth.interceptor.ts | 19 ++++++ .../src/app/core/services/auth.service.ts | 34 ++++++++++ .../src/app/core/services/token.service.ts | 23 +++++++ .../src/app/core/services/usuario.service.ts | 2 +- .../features/auth/login/login.component.ts | 56 +++++++++++++++++ .../auth/register/register.component.ts | 62 +++++++++++++++++++ .../src/app/features/home/home.component.ts | 58 +++++++++++++++++ .../src/app/layout/main-layout.component.ts | 26 ++++++++ .../components/footer/footer.component.ts | 21 +++++++ .../components/navbar/navbar.component.ts | 62 +++++++++++++++++++ frontend/src/environments/environment.ts | 3 +- 21 files changed, 476 insertions(+), 15 deletions(-) rename backend/src/main/java/com/tfg/angel/gameswap/backend/security/{ => records}/AuthRequest.java (52%) rename backend/src/main/java/com/tfg/angel/gameswap/backend/security/{ => records}/AuthResponse.java (55%) create mode 100644 backend/src/main/java/com/tfg/angel/gameswap/backend/security/records/RegisterRequest.java create mode 100644 frontend/src/app/core/guards/auth.guard.ts create mode 100644 frontend/src/app/core/interceptors/auth.interceptor.ts create mode 100644 frontend/src/app/core/services/auth.service.ts create mode 100644 frontend/src/app/core/services/token.service.ts create mode 100644 frontend/src/app/features/auth/login/login.component.ts create mode 100644 frontend/src/app/features/auth/register/register.component.ts create mode 100644 frontend/src/app/features/home/home.component.ts create mode 100644 frontend/src/app/layout/main-layout.component.ts create mode 100644 frontend/src/app/shared/components/footer/footer.component.ts create mode 100644 frontend/src/app/shared/components/navbar/navbar.component.ts diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/AuthController.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/AuthController.java index 40e5cfa..38b0bae 100644 --- a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/AuthController.java +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/AuthController.java @@ -1,6 +1,11 @@ package com.tfg.angel.gameswap.backend.security; +import com.tfg.angel.gameswap.backend.business.model.Usuario; +import com.tfg.angel.gameswap.backend.business.model.enums.Rol; import com.tfg.angel.gameswap.backend.exception.GSBadRequestException; +import com.tfg.angel.gameswap.backend.security.records.AuthRequest; +import com.tfg.angel.gameswap.backend.security.records.AuthResponse; +import com.tfg.angel.gameswap.backend.security.records.RegisterRequest; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import com.tfg.angel.gameswap.backend.business.repository.UsuarioRepository; @@ -38,6 +43,35 @@ public AuthResponse login(@RequestBody AuthRequest request) { return new AuthResponse(accessToken, refreshToken); } + @PostMapping("/register") + public AuthResponse register(@RequestBody RegisterRequest request) { + + if (usuarioRepository.existsByNombreUsuario(request.username())) { + throw new GSBadRequestException("El usuario ya existe"); + } + + var usuario = Usuario.builder() + .nombre(request.name()) + .nombreUsuario(request.username()) + .correo(request.correo()) + .rol(Rol.CLIENTE) + .saldo(0.0) + .estrellas(0.0) + .password(passwordEncoder.encode(request.password())) + .build(); + + usuarioRepository.save(usuario); + + String accessToken = jwtService.generateToken( + usuario.getNombreUsuario(), + usuario.getRol().name() + ); + + String refreshToken = jwtService.generateRefreshToken(usuario.getNombreUsuario()); + + return new AuthResponse(accessToken, refreshToken); + } + @PostMapping("/refresh") public String refresh(@RequestParam String refreshToken) { diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/SpringSecurityConfig.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/SpringSecurityConfig.java index 55180c0..083a36f 100644 --- a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/SpringSecurityConfig.java +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/SpringSecurityConfig.java @@ -26,8 +26,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // Públicos .requestMatchers( - "/auth/login", - "/auth/logout", + "/auth/**", "/swagger-ui/**", "/v3/api-docs/**", "/v3/api-docs.yaml" diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/AuthRequest.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/records/AuthRequest.java similarity index 52% rename from backend/src/main/java/com/tfg/angel/gameswap/backend/security/AuthRequest.java rename to backend/src/main/java/com/tfg/angel/gameswap/backend/security/records/AuthRequest.java index 5475049..1b90817 100644 --- a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/AuthRequest.java +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/records/AuthRequest.java @@ -1,3 +1,3 @@ -package com.tfg.angel.gameswap.backend.security; +package com.tfg.angel.gameswap.backend.security.records; public record AuthRequest(String username, String password) {} diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/AuthResponse.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/records/AuthResponse.java similarity index 55% rename from backend/src/main/java/com/tfg/angel/gameswap/backend/security/AuthResponse.java rename to backend/src/main/java/com/tfg/angel/gameswap/backend/security/records/AuthResponse.java index 883deb9..fc23711 100644 --- a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/AuthResponse.java +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/records/AuthResponse.java @@ -1,3 +1,3 @@ -package com.tfg.angel.gameswap.backend.security; +package com.tfg.angel.gameswap.backend.security.records; public record AuthResponse(String accessToken, String refreshToken) {} diff --git a/backend/src/main/java/com/tfg/angel/gameswap/backend/security/records/RegisterRequest.java b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/records/RegisterRequest.java new file mode 100644 index 0000000..a141285 --- /dev/null +++ b/backend/src/main/java/com/tfg/angel/gameswap/backend/security/records/RegisterRequest.java @@ -0,0 +1,8 @@ +package com.tfg.angel.gameswap.backend.security.records; + +public record RegisterRequest( + String name, + String username, + String password, + String correo +) {} diff --git a/backend/src/main/resources/db/migration/V3__add_rol_usuario.sql b/backend/src/main/resources/db/migration/V3__add_rol_usuario.sql index e5d7b06..3903317 100644 --- a/backend/src/main/resources/db/migration/V3__add_rol_usuario.sql +++ b/backend/src/main/resources/db/migration/V3__add_rol_usuario.sql @@ -50,4 +50,8 @@ VALUES INSERT INTO productocarrito (id, id_carrito, id_post_venta) VALUES -(1, 1, 2); \ No newline at end of file +(1, 1, 2); + +-- Sincronizar la secuencia del ID con los datos insertados + +SELECT setval('usuario_id_seq', (SELECT MAX(id) FROM usuario)); \ No newline at end of file diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index b412f0c..1614b69 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -2,11 +2,11 @@ import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; @Component({ + standalone: true, selector: 'app-root', imports: [RouterOutlet], - templateUrl: './app.component.html', - styleUrl: './app.component.css' + template: ` + + ` }) -export class AppComponent { - title = 'frontend'; -} +export class AppComponent {} diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 72a148e..609041c 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -1,9 +1,16 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'; import { provideRouter } from '@angular/router'; -import { provideHttpClient } from '@angular/common/http'; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; +import { authInterceptor } from './core/interceptors/auth.interceptor'; import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { - providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideHttpClient()] + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(routes), + provideHttpClient( + withInterceptors([authInterceptor]) + ) + ] }; diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index ab8e861..1edd26f 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,6 +1,37 @@ import { Routes } from '@angular/router'; import { UsuariosComponent } from './features/usuarios/usuarios.component'; +import { authGuard } from './core/guards/auth.guard'; export const routes: Routes = [ - { path: 'usuarios', component: UsuariosComponent } + { path: 'usuarios', component: UsuariosComponent }, + + { + path: '', + loadComponent: () => + import('./layout/main-layout.component') + .then(m => m.MainLayoutComponent), + canActivate: [authGuard], + children: [ + { + path: '', + loadComponent: () => + import('./features/home/home.component') + .then(m => m.HomeComponent) + } + ] + }, + + { + path: 'login', + loadComponent: () => + import('./features/auth/login/login.component') + .then(m => m.LoginComponent) + }, + + { + path: 'register', + loadComponent: () => + import('./features/auth/register/register.component') + .then(m => m.RegisterComponent) + } ]; diff --git a/frontend/src/app/core/guards/auth.guard.ts b/frontend/src/app/core/guards/auth.guard.ts new file mode 100644 index 0000000..c5f378c --- /dev/null +++ b/frontend/src/app/core/guards/auth.guard.ts @@ -0,0 +1,16 @@ +import { inject } from '@angular/core'; +import { CanActivateFn, Router } from '@angular/router'; +import { TokenService } from '../services/token.service'; + +export const authGuard: CanActivateFn = () => { + + const tokenService = inject(TokenService); + const router = inject(Router); + + if (tokenService.isLogged()) { + return true; + } + + router.navigate(['/login']); + return false; +}; \ No newline at end of file diff --git a/frontend/src/app/core/interceptors/auth.interceptor.ts b/frontend/src/app/core/interceptors/auth.interceptor.ts new file mode 100644 index 0000000..a17923c --- /dev/null +++ b/frontend/src/app/core/interceptors/auth.interceptor.ts @@ -0,0 +1,19 @@ +import { HttpInterceptorFn } from '@angular/common/http'; +import { inject } from '@angular/core'; +import { TokenService } from '../services/token.service'; + +export const authInterceptor: HttpInterceptorFn = (req, next) => { + + const tokenService = inject(TokenService); + const token = tokenService.getToken(); + + if (token) { + req = req.clone({ + setHeaders: { + Authorization: `Bearer ${token}` + } + }); + } + + return next(req); +}; \ No newline at end of file diff --git a/frontend/src/app/core/services/auth.service.ts b/frontend/src/app/core/services/auth.service.ts new file mode 100644 index 0000000..53f0a10 --- /dev/null +++ b/frontend/src/app/core/services/auth.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { TokenService } from './token.service'; +import { tap } from 'rxjs'; +import { environment } from '../../../environments/environment'; + +@Injectable({ providedIn: 'root' }) +export class AuthService { + + private API = `${environment.authUrl}`; + + constructor( + private http: HttpClient, + private tokenService: TokenService + ) {} + + login(data: any) { + return this.http.post(`${this.API}/login`, data) + .pipe( + tap(res => this.tokenService.setToken(res.accessToken)) + ); + } + + register(data: any) { + return this.http.post(`${this.API}/register`, data) + .pipe( + tap(res => this.tokenService.setToken(res.accessToken)) + ); + } + + logout() { + this.tokenService.removeToken(); + } +} \ No newline at end of file diff --git a/frontend/src/app/core/services/token.service.ts b/frontend/src/app/core/services/token.service.ts new file mode 100644 index 0000000..a8782d9 --- /dev/null +++ b/frontend/src/app/core/services/token.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class TokenService { + + private KEY = 'token'; + + setToken(token: string) { + localStorage.setItem(this.KEY, token); + } + + getToken(): string | null { + return localStorage.getItem(this.KEY); + } + + removeToken() { + localStorage.removeItem(this.KEY); + } + + isLogged(): boolean { + return !!this.getToken(); + } +} \ No newline at end of file diff --git a/frontend/src/app/core/services/usuario.service.ts b/frontend/src/app/core/services/usuario.service.ts index 70fc214..38ffd7a 100644 --- a/frontend/src/app/core/services/usuario.service.ts +++ b/frontend/src/app/core/services/usuario.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { Usuario } from '../models/usuario.model'; -import { environment } from '../../environments/environment'; +import { environment } from '../../../environments/environment'; @Injectable({ providedIn: 'root' diff --git a/frontend/src/app/features/auth/login/login.component.ts b/frontend/src/app/features/auth/login/login.component.ts new file mode 100644 index 0000000..eb339af --- /dev/null +++ b/frontend/src/app/features/auth/login/login.component.ts @@ -0,0 +1,56 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { AuthService } from '../../../core/services/auth.service'; +import { Router } from '@angular/router'; + +@Component({ + standalone: true, + imports: [FormsModule], + template: ` +
+

Login

+ +
+ + + + +
+ +

Crear cuenta

+
+ `, + styles: [` + .auth-container { + max-width: 400px; + margin: auto; + padding: 2rem; + } + + input, button { + width: 100%; + margin: 10px 0; + padding: 10px; + } + `] +}) +export class LoginComponent { + + username = ''; + password = ''; + + constructor(private auth: AuthService, private router: Router) {} + + login() { + this.auth.login({ + username: this.username, + password: this.password + }).subscribe(() => { + this.router.navigate(['/']); + }); + } + + goRegister() { + this.router.navigate(['/register']); + } +} \ No newline at end of file diff --git a/frontend/src/app/features/auth/register/register.component.ts b/frontend/src/app/features/auth/register/register.component.ts new file mode 100644 index 0000000..9341209 --- /dev/null +++ b/frontend/src/app/features/auth/register/register.component.ts @@ -0,0 +1,62 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { AuthService } from '../../../core/services/auth.service'; +import { Router } from '@angular/router'; + +@Component({ + standalone: true, + imports: [FormsModule], + template: ` +
+

Registro

+ +
+ + + + + + +
+ +

Ya tengo una cuenta

+
+ `, + styles: [` + .auth-container { + max-width: 400px; + margin: auto; + padding: 2rem; + } + + input, button { + width: 100%; + margin: 10px 0; + padding: 10px; + } + `] +}) +export class RegisterComponent { + + name = ''; + username = ''; + email = ''; + password = ''; + + constructor(private auth: AuthService, private router: Router) {} + + register() { + this.auth.register({ + name: this.name, + username: this.username, + correo: this.email, + password: this.password + }).subscribe(() => { + this.router.navigate(['/']); + }); + } + + goLogin() { + this.router.navigate(['/login']); + } +} \ No newline at end of file diff --git a/frontend/src/app/features/home/home.component.ts b/frontend/src/app/features/home/home.component.ts new file mode 100644 index 0000000..55716d1 --- /dev/null +++ b/frontend/src/app/features/home/home.component.ts @@ -0,0 +1,58 @@ +import { Component } from '@angular/core'; + +@Component({ + standalone: true, + selector: 'app-home', + template: ` +
+ +

Bienvenido a GameSwap 🎮

+ +

Compra, vende e intercambia videojuegos fácilmente.

+ +
+
+

Explorar juegos

+

Descubre lo que otros usuarios ofrecen

+
+ +
+

Publicar

+

Vende o intercambia tus juegos

+
+ +
+

Intercambios

+

Gestiona tus propuestas

+
+
+ +
+ `, + styles: [` + .home { + text-align: center; + padding: 1rem; + } + + .cards { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + margin-top: 2rem; + } + + .card { + padding: 1rem; + border-radius: 10px; + background: #f5f5f5; + } + + @media (max-width: 768px) { + .cards { + grid-template-columns: 1fr; + } + } + `] +}) +export class HomeComponent {} \ No newline at end of file diff --git a/frontend/src/app/layout/main-layout.component.ts b/frontend/src/app/layout/main-layout.component.ts new file mode 100644 index 0000000..966ab35 --- /dev/null +++ b/frontend/src/app/layout/main-layout.component.ts @@ -0,0 +1,26 @@ +import { Component } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; +import { FooterComponent } from '../shared/components/footer/footer.component'; +import { NavbarComponent } from '../shared/components/navbar/navbar.component'; + +@Component({ + standalone: true, + selector: 'app-main-layout', + imports: [RouterOutlet, NavbarComponent, FooterComponent], + template: ` + + +
+ +
+ + + `, + styles: [` + .content { + min-height: 80vh; + padding: 1rem; + } + `] +}) +export class MainLayoutComponent {} \ No newline at end of file diff --git a/frontend/src/app/shared/components/footer/footer.component.ts b/frontend/src/app/shared/components/footer/footer.component.ts new file mode 100644 index 0000000..e137999 --- /dev/null +++ b/frontend/src/app/shared/components/footer/footer.component.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; + +@Component({ + standalone: true, + selector: 'app-footer', + template: ` +
+

©2026 GameSwap

+

Contacto: gameswap@email.com

+
+ `, + styles: [` + .footer { + background: #111; + color: white; + text-align: center; + padding: 1rem; + } + `] +}) +export class FooterComponent {} \ No newline at end of file diff --git a/frontend/src/app/shared/components/navbar/navbar.component.ts b/frontend/src/app/shared/components/navbar/navbar.component.ts new file mode 100644 index 0000000..ea41b3c --- /dev/null +++ b/frontend/src/app/shared/components/navbar/navbar.component.ts @@ -0,0 +1,62 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; +import { AuthService } from '../../../core/services/auth.service'; + +@Component({ + standalone: true, + selector: 'app-navbar', + template: ` + + `, + styles: [` + .navbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + background: #222; + color: white; + flex-wrap: wrap; + } + + .menu { + display: flex; + gap: 1rem; + } + + a { + color: white; + text-decoration: none; + } + + @media (max-width: 600px) { + .menu { + width: 100%; + justify-content: center; + margin: 10px 0; + } + } + `] +}) +export class NavbarComponent { + + constructor(private auth: AuthService, private router: Router) {} + + logout() { + this.auth.logout(); + this.router.navigate(['/login']); + } +} \ No newline at end of file diff --git a/frontend/src/environments/environment.ts b/frontend/src/environments/environment.ts index 211e09a..77f465f 100644 --- a/frontend/src/environments/environment.ts +++ b/frontend/src/environments/environment.ts @@ -1,4 +1,5 @@ export const environment = { production: false, - apiUrl: 'http://localhost:8080/api' + apiUrl: 'http://localhost:8080/api', + authUrl: 'http://localhost:8080/auth' }; \ No newline at end of file From 6f64b1b84c8b684a9c367ec01c6d83960ecf3bb8 Mon Sep 17 00:00:00 2001 From: AngelMlt03 Date: Tue, 14 Apr 2026 23:34:44 +0200 Subject: [PATCH 06/19] fix: Arreglo ids bd en dev --- backend/src/main/resources/db/migration/V3__add_rol_usuario.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/resources/db/migration/V3__add_rol_usuario.sql b/backend/src/main/resources/db/migration/V3__add_rol_usuario.sql index 3903317..5b88804 100644 --- a/backend/src/main/resources/db/migration/V3__add_rol_usuario.sql +++ b/backend/src/main/resources/db/migration/V3__add_rol_usuario.sql @@ -54,4 +54,4 @@ VALUES -- Sincronizar la secuencia del ID con los datos insertados -SELECT setval('usuario_id_seq', (SELECT MAX(id) FROM usuario)); \ No newline at end of file +ALTER SEQUENCE usuario_id_seq RESTART WITH 4; \ No newline at end of file From c2d5b2de35173a84094ba0fe19fdac05701fc860 Mon Sep 17 00:00:00 2001 From: AngelMlt03 Date: Tue, 14 Apr 2026 23:51:15 +0200 Subject: [PATCH 07/19] fix: Nueva estructura de ficheros de componentes --- frontend/src/app/app.component.spec.ts | 6 +- frontend/src/app/app.component.ts | 4 +- .../features/auth/login/login.component.css | 17 ++++++ .../features/auth/login/login.component.html | 22 +++++++ .../auth/login/login.component.spec.ts | 55 ++++++++++++++++++ .../features/auth/login/login.component.ts | 33 ++--------- .../auth/register/register.component.css | 18 ++++++ .../auth/register/register.component.html | 14 +++++ .../auth/register/register.component.spec.ts | 58 +++++++++++++++++++ .../auth/register/register.component.ts | 35 ++--------- .../src/app/features/home/home.component.css | 24 ++++++++ .../src/app/features/home/home.component.html | 21 +++++++ .../app/features/home/home.component.spec.ts | 38 ++++++++++++ .../src/app/features/home/home.component.ts | 55 +----------------- .../src/app/layout/main-layout.component.css | 11 ++++ .../src/app/layout/main-layout.component.html | 7 +++ .../app/layout/main-layout.component.spec.ts | 43 ++++++++++++++ .../src/app/layout/main-layout.component.ts | 19 +----- .../components/footer/footer.component.css | 7 +++ .../components/footer/footer.component.html | 4 ++ .../footer/footer.component.spec.ts | 33 +++++++++++ .../components/footer/footer.component.ts | 18 +----- .../components/navbar/navbar.component.css | 37 ++++++++++++ .../components/navbar/navbar.component.html | 14 +++++ .../navbar/navbar.component.spec.ts | 55 ++++++++++++++++++ .../components/navbar/navbar.component.ts | 52 ++--------------- 26 files changed, 506 insertions(+), 194 deletions(-) create mode 100644 frontend/src/app/features/auth/login/login.component.css create mode 100644 frontend/src/app/features/auth/login/login.component.html create mode 100644 frontend/src/app/features/auth/login/login.component.spec.ts create mode 100644 frontend/src/app/features/auth/register/register.component.css create mode 100644 frontend/src/app/features/auth/register/register.component.html create mode 100644 frontend/src/app/features/auth/register/register.component.spec.ts create mode 100644 frontend/src/app/features/home/home.component.css create mode 100644 frontend/src/app/features/home/home.component.html create mode 100644 frontend/src/app/features/home/home.component.spec.ts create mode 100644 frontend/src/app/layout/main-layout.component.css create mode 100644 frontend/src/app/layout/main-layout.component.html create mode 100644 frontend/src/app/layout/main-layout.component.spec.ts create mode 100644 frontend/src/app/shared/components/footer/footer.component.css create mode 100644 frontend/src/app/shared/components/footer/footer.component.html create mode 100644 frontend/src/app/shared/components/footer/footer.component.spec.ts create mode 100644 frontend/src/app/shared/components/navbar/navbar.component.css create mode 100644 frontend/src/app/shared/components/navbar/navbar.component.html create mode 100644 frontend/src/app/shared/components/navbar/navbar.component.spec.ts diff --git a/frontend/src/app/app.component.spec.ts b/frontend/src/app/app.component.spec.ts index a6b0ab9..0c6d2c1 100644 --- a/frontend/src/app/app.component.spec.ts +++ b/frontend/src/app/app.component.spec.ts @@ -14,16 +14,16 @@ describe('AppComponent', () => { expect(app).toBeTruthy(); }); - it(`should have the 'frontend' title`, () => { + it(`should have the title`, () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.componentInstance; - expect(app.title).toEqual('frontend'); + expect(app.title).toEqual('GameSwap'); }); it('should render title', () => { const fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('h1')?.textContent).toContain('Hello, frontend'); + expect(compiled.querySelector('h1')?.textContent).toContain('Hello, GameSwap'); }); }); diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 1614b69..7735bdb 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -9,4 +9,6 @@ import { RouterOutlet } from '@angular/router'; ` }) -export class AppComponent {} +export class AppComponent { + title = 'GameSwap'; +} diff --git a/frontend/src/app/features/auth/login/login.component.css b/frontend/src/app/features/auth/login/login.component.css new file mode 100644 index 0000000..5155aee --- /dev/null +++ b/frontend/src/app/features/auth/login/login.component.css @@ -0,0 +1,17 @@ +.auth-container { + max-width: 400px; + margin: auto; + padding: 2rem; +} + +input, button { + width: 100%; + margin: 10px 0; + padding: 10px; +} + +.link { + cursor: pointer; + color: blue; + text-decoration: underline; +} \ No newline at end of file diff --git a/frontend/src/app/features/auth/login/login.component.html b/frontend/src/app/features/auth/login/login.component.html new file mode 100644 index 0000000..8cf2075 --- /dev/null +++ b/frontend/src/app/features/auth/login/login.component.html @@ -0,0 +1,22 @@ +
+

Login

+ +
+ + + + + +
+ + +
\ No newline at end of file diff --git a/frontend/src/app/features/auth/login/login.component.spec.ts b/frontend/src/app/features/auth/login/login.component.spec.ts new file mode 100644 index 0000000..2f3e8a1 --- /dev/null +++ b/frontend/src/app/features/auth/login/login.component.spec.ts @@ -0,0 +1,55 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { LoginComponent } from './login.component'; +import { AuthService } from '../../../core/services/auth.service'; +import { Router } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { of } from 'rxjs'; + +describe('LoginComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + // Mocks + const authServiceMock = { + login: jasmine.createSpy('login').and.returnValue(of({})) + }; + const routerMock = { + navigate: jasmine.createSpy('navigate') + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [LoginComponent, FormsModule], + providers: [ + { provide: AuthService, useValue: authServiceMock }, + { provide: Router, useValue: routerMock } + ] + }).compileComponents(); + + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('debería crear el componente', () => { + expect(component).toBeTruthy(); + }); + + it('debería llamar a auth.login y navegar al inicio al hacer login', () => { + component.username = 'testuser'; + component.password = '123456'; + + component.login(); + + expect(authServiceMock.login).toHaveBeenCalledWith({ + username: 'testuser', + password: '123456' + }); + expect(routerMock.navigate).toHaveBeenCalledWith(['/']); + }); + + it('debería navegar a /register al llamar a goRegister', () => { + component.goRegister(); + expect(routerMock.navigate).toHaveBeenCalledWith(['/register']); + }); +}); \ No newline at end of file diff --git a/frontend/src/app/features/auth/login/login.component.ts b/frontend/src/app/features/auth/login/login.component.ts index eb339af..e200a12 100644 --- a/frontend/src/app/features/auth/login/login.component.ts +++ b/frontend/src/app/features/auth/login/login.component.ts @@ -1,41 +1,16 @@ import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { AuthService } from '../../../core/services/auth.service'; import { Router } from '@angular/router'; +import { AuthService } from '../../../core/services/auth.service'; @Component({ + selector: 'app-login', standalone: true, imports: [FormsModule], - template: ` -
-

Login

- -
- - - - -
- -

Crear cuenta

-
- `, - styles: [` - .auth-container { - max-width: 400px; - margin: auto; - padding: 2rem; - } - - input, button { - width: 100%; - margin: 10px 0; - padding: 10px; - } - `] + templateUrl: './login.component.html', + styleUrls: ['./login.component.css'] }) export class LoginComponent { - username = ''; password = ''; diff --git a/frontend/src/app/features/auth/register/register.component.css b/frontend/src/app/features/auth/register/register.component.css new file mode 100644 index 0000000..63d16a0 --- /dev/null +++ b/frontend/src/app/features/auth/register/register.component.css @@ -0,0 +1,18 @@ +.auth-container { + max-width: 400px; + margin: auto; + padding: 2rem; +} + +input, button { + width: 100%; + margin: 10px 0; + padding: 10px; +} + +.link { + cursor: pointer; + color: #007bff; + text-decoration: underline; + text-align: center; +} \ No newline at end of file diff --git a/frontend/src/app/features/auth/register/register.component.html b/frontend/src/app/features/auth/register/register.component.html new file mode 100644 index 0000000..0931773 --- /dev/null +++ b/frontend/src/app/features/auth/register/register.component.html @@ -0,0 +1,14 @@ +
+

Registro

+ +
+ + + + + + +
+ + +
\ No newline at end of file diff --git a/frontend/src/app/features/auth/register/register.component.spec.ts b/frontend/src/app/features/auth/register/register.component.spec.ts new file mode 100644 index 0000000..6afbd4a --- /dev/null +++ b/frontend/src/app/features/auth/register/register.component.spec.ts @@ -0,0 +1,58 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RegisterComponent } from './register.component'; +import { AuthService } from '../../../core/services/auth.service'; +import { Router } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { of } from 'rxjs'; + +describe('RegisterComponent', () => { + let component: RegisterComponent; + let fixture: ComponentFixture; + + const authServiceMock = { + register: jasmine.createSpy('register').and.returnValue(of({})) + }; + const routerMock = { + navigate: jasmine.createSpy('navigate') + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [RegisterComponent, FormsModule], + providers: [ + { provide: AuthService, useValue: authServiceMock }, + { provide: Router, useValue: routerMock } + ] + }).compileComponents(); + + fixture = TestBed.createComponent(RegisterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('debería crear el componente', () => { + expect(component).toBeTruthy(); + }); + + it('debería llamar a auth.register con los datos correctos y navegar', () => { + component.name = 'Juan'; + component.username = 'juanito'; + component.email = 'juan@test.com'; + component.password = 'password123'; + + component.register(); + + expect(authServiceMock.register).toHaveBeenCalledWith({ + name: 'Juan', + username: 'juanito', + correo: 'juan@test.com', + password: 'password123' + }); + expect(routerMock.navigate).toHaveBeenCalledWith(['/']); + }); + + it('debería navegar a login al llamar a goLogin', () => { + component.goLogin(); + expect(routerMock.navigate).toHaveBeenCalledWith(['/login']); + }); +}); \ No newline at end of file diff --git a/frontend/src/app/features/auth/register/register.component.ts b/frontend/src/app/features/auth/register/register.component.ts index 9341209..a1f9670 100644 --- a/frontend/src/app/features/auth/register/register.component.ts +++ b/frontend/src/app/features/auth/register/register.component.ts @@ -1,43 +1,16 @@ import { Component } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { AuthService } from '../../../core/services/auth.service'; import { Router } from '@angular/router'; +import { AuthService } from '../../../core/services/auth.service'; @Component({ + selector: 'app-register', standalone: true, imports: [FormsModule], - template: ` -
-

Registro

- -
- - - - - - -
- -

Ya tengo una cuenta

-
- `, - styles: [` - .auth-container { - max-width: 400px; - margin: auto; - padding: 2rem; - } - - input, button { - width: 100%; - margin: 10px 0; - padding: 10px; - } - `] + templateUrl: './register.component.html', + styleUrls: ['./register.component.css'] }) export class RegisterComponent { - name = ''; username = ''; email = ''; diff --git a/frontend/src/app/features/home/home.component.css b/frontend/src/app/features/home/home.component.css new file mode 100644 index 0000000..2268751 --- /dev/null +++ b/frontend/src/app/features/home/home.component.css @@ -0,0 +1,24 @@ +.home { + text-align: center; + padding: 1rem; +} + +.cards { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + margin-top: 2rem; +} + +.card { + padding: 1rem; + border-radius: 10px; + background: #f5f5f5; + box-shadow: 0 2px 5px rgba(0,0,0,0.1); +} + +@media (max-width: 768px) { + .cards { + grid-template-columns: 1fr; + } +} \ No newline at end of file diff --git a/frontend/src/app/features/home/home.component.html b/frontend/src/app/features/home/home.component.html new file mode 100644 index 0000000..843f845 --- /dev/null +++ b/frontend/src/app/features/home/home.component.html @@ -0,0 +1,21 @@ +
+

Bienvenido a GameSwap 🎮

+

Compra, vende e intercambia videojuegos fácilmente.

+ +
+
+

Explorar juegos

+

Descubre lo que otros usuarios ofrecen

+
+ +
+

Publicar

+

Vende o intercambia tus juegos

+
+ +
+

Intercambios

+

Gestiona tus propuestas

+
+
+
\ No newline at end of file diff --git a/frontend/src/app/features/home/home.component.spec.ts b/frontend/src/app/features/home/home.component.spec.ts new file mode 100644 index 0000000..5f9a76b --- /dev/null +++ b/frontend/src/app/features/home/home.component.spec.ts @@ -0,0 +1,38 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HomeComponent } from './home.component'; + +describe('HomeComponent', () => { + let component: HomeComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HomeComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('debería crear el componente', () => { + expect(component).toBeTruthy(); + }); + + it('debería mostrar el título de bienvenida', () => { + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('h1')?.textContent).toContain('Bienvenido a GameSwap'); + }); + + it('debería renderizar las tres tarjetas de opciones', () => { + const compiled = fixture.nativeElement as HTMLElement; + const cards = compiled.querySelectorAll('.card'); + expect(cards.length).toBe(3); + }); + + it('debería tener la primera tarjeta con el texto Explorar juegos', () => { + const compiled = fixture.nativeElement as HTMLElement; + const firstCardTitle = compiled.querySelectorAll('.card h3')[0]; + expect(firstCardTitle.textContent).toBe('Explorar juegos'); + }); +}); \ No newline at end of file diff --git a/frontend/src/app/features/home/home.component.ts b/frontend/src/app/features/home/home.component.ts index 55716d1..0b2816a 100644 --- a/frontend/src/app/features/home/home.component.ts +++ b/frontend/src/app/features/home/home.component.ts @@ -1,58 +1,9 @@ import { Component } from '@angular/core'; @Component({ - standalone: true, selector: 'app-home', - template: ` -
- -

Bienvenido a GameSwap 🎮

- -

Compra, vende e intercambia videojuegos fácilmente.

- -
-
-

Explorar juegos

-

Descubre lo que otros usuarios ofrecen

-
- -
-

Publicar

-

Vende o intercambia tus juegos

-
- -
-

Intercambios

-

Gestiona tus propuestas

-
-
- -
- `, - styles: [` - .home { - text-align: center; - padding: 1rem; - } - - .cards { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 1rem; - margin-top: 2rem; - } - - .card { - padding: 1rem; - border-radius: 10px; - background: #f5f5f5; - } - - @media (max-width: 768px) { - .cards { - grid-template-columns: 1fr; - } - } - `] + standalone: true, + templateUrl: './home.component.html', + styleUrls: ['./home.component.css'] }) export class HomeComponent {} \ No newline at end of file diff --git a/frontend/src/app/layout/main-layout.component.css b/frontend/src/app/layout/main-layout.component.css new file mode 100644 index 0000000..3d6e8ac --- /dev/null +++ b/frontend/src/app/layout/main-layout.component.css @@ -0,0 +1,11 @@ +.content { + min-height: calc(100vh - 160px); + padding: 1rem; + display: block; +} + +:host { + display: flex; + flex-direction: column; + min-height: 100vh; +} \ No newline at end of file diff --git a/frontend/src/app/layout/main-layout.component.html b/frontend/src/app/layout/main-layout.component.html new file mode 100644 index 0000000..deedfdb --- /dev/null +++ b/frontend/src/app/layout/main-layout.component.html @@ -0,0 +1,7 @@ + + +
+ +
+ + \ No newline at end of file diff --git a/frontend/src/app/layout/main-layout.component.spec.ts b/frontend/src/app/layout/main-layout.component.spec.ts new file mode 100644 index 0000000..067688e --- /dev/null +++ b/frontend/src/app/layout/main-layout.component.spec.ts @@ -0,0 +1,43 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { MainLayoutComponent } from './main-layout.component'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NavbarComponent } from '../shared/components/navbar/navbar.component'; +import { FooterComponent } from '../shared/components/footer/footer.component'; +import { AuthService } from '../core/services/auth.service'; + +describe('MainLayoutComponent', () => { + let component: MainLayoutComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + + const authServiceMock = { logout: jasmine.createSpy('logout') }; + + await TestBed.configureTestingModule({ + imports: [ + MainLayoutComponent, + RouterTestingModule, + NavbarComponent, + FooterComponent + ], + providers: [ + { provide: AuthService, useValue: authServiceMock } + ] + }).compileComponents(); + + fixture = TestBed.createComponent(MainLayoutComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('debería crear el layout', () => { + expect(component).toBeTruthy(); + }); + + it('debería contener el navbar, el footer y el outlet', () => { + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('app-navbar')).not.toBeNull(); + expect(compiled.querySelector('app-footer')).not.toBeNull(); + expect(compiled.querySelector('router-outlet')).not.toBeNull(); + }); +}); \ No newline at end of file diff --git a/frontend/src/app/layout/main-layout.component.ts b/frontend/src/app/layout/main-layout.component.ts index 966ab35..a578d71 100644 --- a/frontend/src/app/layout/main-layout.component.ts +++ b/frontend/src/app/layout/main-layout.component.ts @@ -4,23 +4,10 @@ import { FooterComponent } from '../shared/components/footer/footer.component'; import { NavbarComponent } from '../shared/components/navbar/navbar.component'; @Component({ - standalone: true, selector: 'app-main-layout', + standalone: true, imports: [RouterOutlet, NavbarComponent, FooterComponent], - template: ` - - -
- -
- - - `, - styles: [` - .content { - min-height: 80vh; - padding: 1rem; - } - `] + templateUrl: './main-layout.component.html', + styleUrls: ['./main-layout.component.css'] }) export class MainLayoutComponent {} \ No newline at end of file diff --git a/frontend/src/app/shared/components/footer/footer.component.css b/frontend/src/app/shared/components/footer/footer.component.css new file mode 100644 index 0000000..1a5b2c0 --- /dev/null +++ b/frontend/src/app/shared/components/footer/footer.component.css @@ -0,0 +1,7 @@ +.footer { + background: #111; + color: white; + text-align: center; + padding: 1rem; + width: 100%; +} \ No newline at end of file diff --git a/frontend/src/app/shared/components/footer/footer.component.html b/frontend/src/app/shared/components/footer/footer.component.html new file mode 100644 index 0000000..add75db --- /dev/null +++ b/frontend/src/app/shared/components/footer/footer.component.html @@ -0,0 +1,4 @@ +
+

©2026 GameSwap

+

Contacto: gameswap@email.com

+
\ No newline at end of file diff --git a/frontend/src/app/shared/components/footer/footer.component.spec.ts b/frontend/src/app/shared/components/footer/footer.component.spec.ts new file mode 100644 index 0000000..0d1019b --- /dev/null +++ b/frontend/src/app/shared/components/footer/footer.component.spec.ts @@ -0,0 +1,33 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FooterComponent } from './footer.component'; + +describe('FooterComponent', () => { + let component: FooterComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [FooterComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(FooterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('debería crear el componente', () => { + expect(component).toBeTruthy(); + }); + + it('debería mostrar el texto del copyright con el año 2026', () => { + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('p')?.textContent).toContain('©2026 GameSwap'); + }); + + it('debería mostrar el email de contacto correctamente', () => { + const compiled = fixture.nativeElement as HTMLElement; + const paragraphs = compiled.querySelectorAll('p'); + // El segundo párrafo debería contener el email + expect(paragraphs[1].textContent).toContain('gameswap@email.com'); + }); +}); \ No newline at end of file diff --git a/frontend/src/app/shared/components/footer/footer.component.ts b/frontend/src/app/shared/components/footer/footer.component.ts index e137999..02f39ae 100644 --- a/frontend/src/app/shared/components/footer/footer.component.ts +++ b/frontend/src/app/shared/components/footer/footer.component.ts @@ -1,21 +1,9 @@ import { Component } from '@angular/core'; @Component({ - standalone: true, selector: 'app-footer', - template: ` -
-

©2026 GameSwap

-

Contacto: gameswap@email.com

-
- `, - styles: [` - .footer { - background: #111; - color: white; - text-align: center; - padding: 1rem; - } - `] + standalone: true, + templateUrl: './footer.component.html', + styleUrls: ['./footer.component.css'] }) export class FooterComponent {} \ No newline at end of file diff --git a/frontend/src/app/shared/components/navbar/navbar.component.css b/frontend/src/app/shared/components/navbar/navbar.component.css new file mode 100644 index 0000000..20bef1f --- /dev/null +++ b/frontend/src/app/shared/components/navbar/navbar.component.css @@ -0,0 +1,37 @@ +.navbar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + background: #222; + color: white; + flex-wrap: wrap; +} + +.menu { + display: flex; + gap: 1rem; +} + +a { + color: white; + text-decoration: none; +} + +.user { + display: flex; + align-items: center; + gap: 0.5rem; +} + +button { + cursor: pointer; +} + +@media (max-width: 600px) { + .menu { + width: 100%; + justify-content: center; + margin: 10px 0; + } +} \ No newline at end of file diff --git a/frontend/src/app/shared/components/navbar/navbar.component.html b/frontend/src/app/shared/components/navbar/navbar.component.html new file mode 100644 index 0000000..3540617 --- /dev/null +++ b/frontend/src/app/shared/components/navbar/navbar.component.html @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/frontend/src/app/shared/components/navbar/navbar.component.spec.ts b/frontend/src/app/shared/components/navbar/navbar.component.spec.ts new file mode 100644 index 0000000..f8776c4 --- /dev/null +++ b/frontend/src/app/shared/components/navbar/navbar.component.spec.ts @@ -0,0 +1,55 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NavbarComponent } from './navbar.component'; +import { AuthService } from '../../../core/services/auth.service'; +import { Router } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; + +describe('NavbarComponent', () => { + let component: NavbarComponent; + let fixture: ComponentFixture; + + const authServiceMock = { + logout: jasmine.createSpy('logout') + }; + const routerMock = { + navigate: jasmine.createSpy('navigate') + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [NavbarComponent, RouterTestingModule], + providers: [ + { provide: AuthService, useValue: authServiceMock }, + { provide: Router, useValue: routerMock } + ] + }).compileComponents(); + + fixture = TestBed.createComponent(NavbarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('debería crear el componente', () => { + expect(component).toBeTruthy(); + }); + + it('debería mostrar el logo "GameSwap"', () => { + const compiled = fixture.nativeElement as HTMLElement; + expect(compiled.querySelector('.logo')?.textContent).toContain('GameSwap'); + }); + + it('debería llamar a auth.logout y navegar a login al pulsar Salir', () => { + component.logout(); + + expect(authServiceMock.logout).toHaveBeenCalled(); + expect(routerMock.navigate).toHaveBeenCalledWith(['/login']); + }); + + it('debería tener los enlaces de navegación correctos', () => { + const compiled = fixture.nativeElement as HTMLElement; + const links = compiled.querySelectorAll('a'); + expect(links[0].getAttribute('routerLink')).toBe('/'); + expect(links[1].getAttribute('routerLink')).toBe('/explorar'); + expect(links[2].getAttribute('routerLink')).toBe('/perfil'); + }); +}); \ No newline at end of file diff --git a/frontend/src/app/shared/components/navbar/navbar.component.ts b/frontend/src/app/shared/components/navbar/navbar.component.ts index ea41b3c..f652718 100644 --- a/frontend/src/app/shared/components/navbar/navbar.component.ts +++ b/frontend/src/app/shared/components/navbar/navbar.component.ts @@ -1,55 +1,13 @@ import { Component } from '@angular/core'; -import { Router } from '@angular/router'; +import { Router, RouterLink } from '@angular/router'; import { AuthService } from '../../../core/services/auth.service'; @Component({ - standalone: true, selector: 'app-navbar', - template: ` - - `, - styles: [` - .navbar { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem; - background: #222; - color: white; - flex-wrap: wrap; - } - - .menu { - display: flex; - gap: 1rem; - } - - a { - color: white; - text-decoration: none; - } - - @media (max-width: 600px) { - .menu { - width: 100%; - justify-content: center; - margin: 10px 0; - } - } - `] + standalone: true, + imports: [RouterLink], + templateUrl: './navbar.component.html', + styleUrls: ['./navbar.component.css'] }) export class NavbarComponent { From 8a3a68fd4c9a0df0ef5c70cb1c2b9b262b357233 Mon Sep 17 00:00:00 2001 From: AngelMlt03 Date: Fri, 17 Apr 2026 20:13:40 +0200 Subject: [PATCH 08/19] ft: Login y primera interfaz principal --- frontend/src/app/app.component.html | 336 ------------------ frontend/src/app/app.component.ts | 11 +- frontend/src/app/app.routes.ts | 7 + .../src/app/core/services/theme.service.ts | 25 ++ .../src/app/core/services/token.service.ts | 8 + .../features/auth/login/login.component.css | 12 +- .../auth/register/register.component.css | 15 +- .../src/app/features/home/home.component.css | 1 - .../app/features/perfil/perfil.component.css | 33 ++ .../app/features/perfil/perfil.component.html | 44 +++ .../app/features/perfil/perfil.component.ts | 29 ++ .../src/app/layout/main-layout.component.css | 5 - .../components/footer/footer.component.css | 2 +- .../components/navbar/navbar.component.css | 128 ++++++- .../components/navbar/navbar.component.html | 30 +- .../components/navbar/navbar.component.ts | 65 +++- frontend/src/index.html | 2 +- frontend/src/styles.css | 38 +- 18 files changed, 424 insertions(+), 367 deletions(-) delete mode 100644 frontend/src/app/app.component.html create mode 100644 frontend/src/app/core/services/theme.service.ts create mode 100644 frontend/src/app/features/perfil/perfil.component.css create mode 100644 frontend/src/app/features/perfil/perfil.component.html create mode 100644 frontend/src/app/features/perfil/perfil.component.ts diff --git a/frontend/src/app/app.component.html b/frontend/src/app/app.component.html deleted file mode 100644 index 36093e1..0000000 --- a/frontend/src/app/app.component.html +++ /dev/null @@ -1,336 +0,0 @@ - - - - - - - - - - - -
-
-
- -

Hello, {{ title }}

-

Congratulations! Your app is running. 🎉

-
- -
-
- @for (item of [ - { title: 'Explore the Docs', link: 'https://angular.dev' }, - { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' }, - { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' }, - { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' }, - { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' }, - ]; track item.title) { - - {{ item.title }} - - - - - } -
- -
-
-
- - - - - - - - - - - diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 7735bdb..0951ca5 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,5 +1,6 @@ import { Component } from '@angular/core'; import { RouterOutlet } from '@angular/router'; +import { ThemeService } from './core/services/theme.service'; @Component({ standalone: true, @@ -7,8 +8,16 @@ import { RouterOutlet } from '@angular/router'; imports: [RouterOutlet], template: ` - ` + `, + styleUrls: ['./app.component.css'] }) export class AppComponent { + + constructor(private theme: ThemeService) {} + title = 'GameSwap'; + + ngOnInit() { + this.theme.initTheme(); + } } diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 1edd26f..1eff926 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -33,5 +33,12 @@ export const routes: Routes = [ loadComponent: () => import('./features/auth/register/register.component') .then(m => m.RegisterComponent) + }, + + { + path: 'perfil', + loadComponent: () => + import('./features/perfil/perfil.component') + .then(m => m.PerfilComponent) } ]; diff --git a/frontend/src/app/core/services/theme.service.ts b/frontend/src/app/core/services/theme.service.ts new file mode 100644 index 0000000..d28a067 --- /dev/null +++ b/frontend/src/app/core/services/theme.service.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class ThemeService { + + private THEME_KEY = 'theme'; + + initTheme() { + const theme = localStorage.getItem(this.THEME_KEY) || 'light'; + document.body.className = theme; + } + + toggleTheme() { + const current = document.body.classList.contains('dark') ? 'dark' : 'light'; + const newTheme = current === 'dark' ? 'light' : 'dark'; + + document.body.classList.remove('light', 'dark'); + document.body.classList.add(newTheme); + localStorage.setItem(this.THEME_KEY, newTheme); + } + + isDark() { + return document.body.classList.contains('dark'); + } +} \ No newline at end of file diff --git a/frontend/src/app/core/services/token.service.ts b/frontend/src/app/core/services/token.service.ts index a8782d9..7ed45c0 100644 --- a/frontend/src/app/core/services/token.service.ts +++ b/frontend/src/app/core/services/token.service.ts @@ -20,4 +20,12 @@ export class TokenService { isLogged(): boolean { return !!this.getToken(); } + + getUsername(): string | null { + const token = this.getToken(); + if (!token) return null; + + const payload = JSON.parse(atob(token.split('.')[1])); + return payload.sub; + } } \ No newline at end of file diff --git a/frontend/src/app/features/auth/login/login.component.css b/frontend/src/app/features/auth/login/login.component.css index 5155aee..5a82dcf 100644 --- a/frontend/src/app/features/auth/login/login.component.css +++ b/frontend/src/app/features/auth/login/login.component.css @@ -4,10 +4,18 @@ padding: 2rem; } -input, button { +input { width: 100%; margin: 10px 0; - padding: 10px; + padding: 12px; + border-radius: 8px; + border: 1px solid #ccc; +} + +@media (max-width: 600px) { + .auth-container { + padding: 1rem; + } } .link { diff --git a/frontend/src/app/features/auth/register/register.component.css b/frontend/src/app/features/auth/register/register.component.css index 63d16a0..5a82dcf 100644 --- a/frontend/src/app/features/auth/register/register.component.css +++ b/frontend/src/app/features/auth/register/register.component.css @@ -4,15 +4,22 @@ padding: 2rem; } -input, button { +input { width: 100%; margin: 10px 0; - padding: 10px; + padding: 12px; + border-radius: 8px; + border: 1px solid #ccc; +} + +@media (max-width: 600px) { + .auth-container { + padding: 1rem; + } } .link { cursor: pointer; - color: #007bff; + color: blue; text-decoration: underline; - text-align: center; } \ No newline at end of file diff --git a/frontend/src/app/features/home/home.component.css b/frontend/src/app/features/home/home.component.css index 2268751..2fe9e2d 100644 --- a/frontend/src/app/features/home/home.component.css +++ b/frontend/src/app/features/home/home.component.css @@ -13,7 +13,6 @@ .card { padding: 1rem; border-radius: 10px; - background: #f5f5f5; box-shadow: 0 2px 5px rgba(0,0,0,0.1); } diff --git a/frontend/src/app/features/perfil/perfil.component.css b/frontend/src/app/features/perfil/perfil.component.css new file mode 100644 index 0000000..d2f798e --- /dev/null +++ b/frontend/src/app/features/perfil/perfil.component.css @@ -0,0 +1,33 @@ +.perfil-container { + max-width: 500px; + margin: auto; + padding: 1rem; +} + +.card { + background: var(--card); + padding: 1.5rem; + border-radius: 10px; +} + +.field { + margin-bottom: 1rem; +} + +label { + display: block; + font-weight: bold; + margin-bottom: 5px; +} + +input { + width: 100%; + padding: 10px; + border-radius: 8px; + border: 1px solid #ccc; +} + +.actions { + display: flex; + justify-content: flex-end; +} \ No newline at end of file diff --git a/frontend/src/app/features/perfil/perfil.component.html b/frontend/src/app/features/perfil/perfil.component.html new file mode 100644 index 0000000..6f4bf34 --- /dev/null +++ b/frontend/src/app/features/perfil/perfil.component.html @@ -0,0 +1,44 @@ +
+ +
+

Mi Perfil

+ Volver +
+ +
+ +
+ + + + + + {{ usuario.nombre }} + +
+ +
+ + + + + + {{ usuario.email }} + +
+ +
+ + + +
+ +
+ +
\ No newline at end of file diff --git a/frontend/src/app/features/perfil/perfil.component.ts b/frontend/src/app/features/perfil/perfil.component.ts new file mode 100644 index 0000000..0bef7c6 --- /dev/null +++ b/frontend/src/app/features/perfil/perfil.component.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgIf } from '@angular/common'; +import { RouterLink } from '@angular/router'; + +@Component({ + standalone: true, + imports: [FormsModule, NgIf, RouterLink], + templateUrl: './perfil.component.html', + styleUrls: ['./perfil.component.css'] +}) +export class PerfilComponent { + + usuario = { + nombre: 'Angel', + email: 'angel@gmail.com' + }; + + editMode = false; + + toggleEdit() { + this.editMode = !this.editMode; + } + + guardar() { + console.log('Guardar usuario:', this.usuario); + this.editMode = false; + } +} \ No newline at end of file diff --git a/frontend/src/app/layout/main-layout.component.css b/frontend/src/app/layout/main-layout.component.css index 3d6e8ac..e1429bc 100644 --- a/frontend/src/app/layout/main-layout.component.css +++ b/frontend/src/app/layout/main-layout.component.css @@ -4,8 +4,3 @@ display: block; } -:host { - display: flex; - flex-direction: column; - min-height: 100vh; -} \ No newline at end of file diff --git a/frontend/src/app/shared/components/footer/footer.component.css b/frontend/src/app/shared/components/footer/footer.component.css index 1a5b2c0..16fd98d 100644 --- a/frontend/src/app/shared/components/footer/footer.component.css +++ b/frontend/src/app/shared/components/footer/footer.component.css @@ -2,6 +2,6 @@ background: #111; color: white; text-align: center; - padding: 1rem; + padding: 0.5rem 0; width: 100%; } \ No newline at end of file diff --git a/frontend/src/app/shared/components/navbar/navbar.component.css b/frontend/src/app/shared/components/navbar/navbar.component.css index 20bef1f..41adc5b 100644 --- a/frontend/src/app/shared/components/navbar/navbar.component.css +++ b/frontend/src/app/shared/components/navbar/navbar.component.css @@ -3,35 +3,145 @@ justify-content: space-between; align-items: center; padding: 1rem; - background: #222; - color: white; - flex-wrap: wrap; + background: var(--card); + position: sticky; + top: 0; + z-index: 1000; + transition: box-shadow 0.3s ease, background 0.3s ease; +} + +/* Al hacer scroll */ +.navbar.scrolled { + box-shadow: 0 4px 12px rgba(0,0,0,0.15); } +/* LOGO */ +.logo { + font-weight: bold; + font-size: 1.2rem; +} + +/* MENU */ .menu { display: flex; - gap: 1rem; + gap: 1.5rem; } -a { - color: white; +.menu a { text-decoration: none; + color: var(--text); + font-weight: 500; + transition: 0.2s; +} + +.menu a:hover { + opacity: 0.7; } .user { display: flex; align-items: center; - gap: 0.5rem; + gap: 10px; +} + +.username { + font-size: 0.9rem; } +/* BOTONES */ button { + padding: 6px 10px; + border-radius: 6px; + border: none; + background: var(--primary); + color: white; cursor: pointer; + transition: 0.2s; } -@media (max-width: 600px) { +button:hover { + opacity: 0.75; +} + +.theme-btn { + background: transparent; + border: 1px solid var(--text); + color: var(--text); +} + +/* DROPDOWN */ +.dropdown { + position: relative; +} + +.dropdown-toggle { + cursor: pointer; + font-size: 0.9rem; + display: flex; + align-items: center; + gap: 8px; +} + +.dropdown-menu { + position: absolute; + right: 0; + top: 45px; + background: var(--card); + border-radius: 10px; + box-shadow: 0 10px 25px rgba(0,0,0,0.2); + display: flex; + flex-direction: column; + min-width: 180px; +} + +.dropdown-menu a, +.dropdown-menu button { + padding: 10px; + font-size: 14px; + text-align: left; + background: none; + border: none; + color: var(--text); + cursor: pointer; + text-decoration: none; +} + +.dropdown-menu a:hover, +.dropdown-menu button:hover { + background: rgba(0,0,0,0.1); +} + +/* AVATAR */ +.avatar { + width: 35px; + height: 35px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: bold; + font-size: 0.9rem; +} + + +/* Mobile */ +@media (max-width: 768px) { + + .navbar { + flex-wrap: wrap; + gap: 10px; + } + .menu { width: 100%; justify-content: center; - margin: 10px 0; + flex-wrap: wrap; } + + .user { + width: 100%; + justify-content: center; + } + } \ No newline at end of file diff --git a/frontend/src/app/shared/components/navbar/navbar.component.html b/frontend/src/app/shared/components/navbar/navbar.component.html index 3540617..3505b6d 100644 --- a/frontend/src/app/shared/components/navbar/navbar.component.html +++ b/frontend/src/app/shared/components/navbar/navbar.component.html @@ -1,4 +1,5 @@ -