Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 50 additions & 7 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,12 +1,55 @@
FROM eclipse-temurin:21

RUN apt-get update && apt-get -y install \
openjdk-21-jdk \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
# -----------------------------------------------------------
# Etapa 1: Builder - Usamos el JDK para compilar la aplicación con Gradle
# -----------------------------------------------------------
FROM eclipse-temurin:21-jdk-jammy AS builder

# Establece el directorio de trabajo
WORKDIR /app

COPY build/libs/*.jar /app/*.jar
# Copia los archivos del build de Gradle
# Copia el wrapper de Gradle
COPY gradle ./gradle
# Copia los archivos de configuración de Gradle
COPY gradlew build.gradle settings.gradle ./

ENTRYPOINT ["java", "-jar", "articulosapi.jar"]
# Copia los archivos de código fuente
COPY src ./src

# Otorga permisos de ejecución al wrapper de Gradle
RUN chmod +x ./gradlew

# Compila y empaqueta la aplicación usando el wrapper de Gradle
# El task 'bootJar' es el estándar de Spring Boot para crear el JAR ejecutable
RUN ./gradlew bootJar

# -----------------------------------------------------------
# Etapa 2: Desarrollo - Usamos el JDK para la ejecución
# -----------------------------------------------------------
FROM eclipse-temurin:21-jdk-jammy AS development

ARG APP_VERSION
# Metadatos OCI
LABEL org.opencontainers.image.title="articulos-api" \
org.opencontainers.image.description="API de artículos para desarrollo en Java 21" \
org.opencontainers.image.source="https://github.com/LuisDev18/SecurityTask" \
org.opencontainers.image.version="${APP_VERSION}"

# Crea un usuario no-root por seguridad
RUN groupadd -g 10001 app && useradd -u 10000 -g app -s /usr/sbin/nologin -m app

# Establece el directorio de trabajo para la aplicación
WORKDIR /app

# Copia el archivo JAR compilado desde la etapa 'builder'
# El JAR se encuentra en build/libs por defecto
COPY --from=builder /app/build/libs/*.jar /app/app.jar

# Expone el puerto por defecto de Spring Boot
EXPOSE 8080

# Cambia a usuario no-root para ejecutar la aplicación
USER app

# Comando para ejecutar la aplicación
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
23 changes: 14 additions & 9 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ plugins {
id 'java'
id 'org.springframework.boot' version '3.5.3' // Actualizado a 3.5.3
id 'io.spring.dependency-management' version '1.1.0'
id 'com.diffplug.spotless' version '7.1.0' // Generalmente compatible con Spring Boot 3.x
id 'com.diffplug.spotless' version '7.1.0'
}

group = 'pe.edu.utp'
Expand All @@ -15,6 +15,7 @@ configurations {
}
}


spotless {
java {
// Configuración de Prettier para Java
Expand Down Expand Up @@ -44,22 +45,26 @@ repositories {
}

dependencies {
// Update the MySQL connector to the latest version
implementation 'com.mysql:mysql-connector-j:8.0.33'
runtimeOnly 'com.mysql:mysql-connector-j:8.0.33'

// Keep your existing Flyway dependencies
implementation 'org.flywaydb:flyway-core:9.16.1'
implementation 'org.flywaydb:flyway-mysql:9.16.1'

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'mysql:mysql-connector-java:8.0.30' // Actualizado a una versión más reciente
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
implementation 'com.github.ben-manes.caffeine:caffeine:3.1.8'
implementation 'org.springframework.boot:spring-boot-starter-cache'

//Dependencia para migraciones de base de datos
implementation 'org.flywaydb:flyway-core:9.16.1' // Actualizado a una versión más reciente
implementation 'org.flywaydb:flyway-mysql:9.16.1' // Actualizado a una versión más reciente

implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'

// Spring Batch dependencies
implementation 'org.springframework.boot:spring-boot-starter-batch'

// Dependencias de Lombok y MapStruct para el procesamiento de anotaciones
compileOnly 'org.projectlombok:lombok'
Expand Down
36 changes: 36 additions & 0 deletions buildspec.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
version: 0.2

phases:
pre_build:
commands:
- echo Running Gradle build...
- ./gradlew clean build

- echo Logging in to Amazon ECR...
- aws --version
- REPOSITORY_URI=235763262026.dkr.ecr.us-east-1.amazonaws.com/palcos-images-repository
- aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin $REPOSITORY_URI
- COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
- IMAGE_TAG=build-$(echo $CODEBUILD_BUILD_ID | awk -F":" '{print $2}')
build:
commands:
- echo Build started on `date`
- echo Building the Docker image...
- docker build --build-arg APP_VERSION=$IMAGE_VERSION -t $IMAGE_TAG .
post_build:
commands:
- echo Build completed on `date`
- echo Pushing the Docker images...
- docker push $REPOSITORY_URI:$IMAGE_TAG
- echo Writing image definitions file...
# This part is for CodeDeploy to know which container to update.
- DOCKER_CONTAINER_NAME=palcos-images-repository
- printf '[{"name":"%s","imageUri":"%s"}]' $DOCKER_CONTAINER_NAME $REPOSITORY_URI:$IMAGE_TAG > imagedefinitions.json
- echo printing imagedefinitions.json
- cat imagedefinitions.json

artifacts:
files:
- imagedefinitions.json
# The path to your JAR file needs to be updated for Gradle.
- build/libs/articulosapi.jar
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ services:
- DATASOURCE_USERNAME=root
- DATASOURCE_PASSWORD=admin
env_file:
- .env.keypair
- .env.development
depends_on:
- mysqldb

Expand Down
26 changes: 24 additions & 2 deletions src/main/java/pe/edu/utp/controller/ArticuloController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.List;
import java.util.Map;

import io.swagger.v3.oas.annotations.Parameter;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -16,6 +17,7 @@
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -24,6 +26,7 @@
import pe.edu.utp.dto.ArticuloResponseDto;
import pe.edu.utp.entity.Articulo;
import pe.edu.utp.exception.NoDataFoundException;
import pe.edu.utp.security.JwtService;
import pe.edu.utp.service.ArticuloService;
import pe.edu.utp.util.ApiResponse;
import pe.edu.utp.util.ConvertUtil;
Expand All @@ -35,6 +38,7 @@
public class ArticuloController {

private final ArticuloService articuloService;
private final JwtService jwtService;

@GetMapping
public ResponseEntity<ApiResponse<List<ArticuloResponseDto>>> getAll(
Expand Down Expand Up @@ -70,8 +74,12 @@ public ResponseEntity<ApiResponse<ArticuloResponseDto>> findById(@PathVariable("
}

@PostMapping
public ResponseEntity<ApiResponse<Articulo>> create(@Valid @RequestBody ArticuloDto articuloDto) {
Articulo registro = articuloService.save(articuloDto);
public ResponseEntity<ApiResponse<Articulo>> create(
@Valid @RequestBody ArticuloDto articuloDto,
@Parameter(hidden = true) @RequestHeader(value = "Authorization") String bearerToken
) {
var email = jwtService.extractUsername(bearerToken);
Articulo registro = articuloService.save(articuloDto, email);
return ApiResponse.created("Articulo creado exitosamente", registro).toResponseEntity();
}

Expand Down Expand Up @@ -100,4 +108,18 @@ public ResponseEntity<ArticuloDto> delete(@PathVariable("id") Integer id)
articuloService.delete(id);
return ResponseEntity.ok(null);
}

@PutMapping(value = "/new-stock/{productId}")
public ResponseEntity<Articulo> updateStokSp(
@PathVariable("productId") Integer productId,
@RequestParam Integer quantity
) {
var articuloUpdate = articuloService.discountStoock(productId, quantity);
return ResponseEntity.ok(articuloUpdate);
}

@GetMapping("/total-price/{productId}")
public Double getTotalPrice(@PathVariable("productId") Integer productId) {
return articuloService.calculateTotalPrice(productId);
}
}
17 changes: 17 additions & 0 deletions src/main/java/pe/edu/utp/entity/Articulo.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.NamedStoredProcedureQuery;
import jakarta.persistence.ParameterMode;
import jakarta.persistence.StoredProcedureParameter;
import jakarta.persistence.Table;
import jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType;
Expand All @@ -20,6 +25,14 @@
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

@NamedStoredProcedureQuery(
name = "updateStockProcedure",
procedureName = "update_stock",
parameters = {
@StoredProcedureParameter(mode = ParameterMode.IN, name = "productId", type = Integer.class),
@StoredProcedureParameter(mode = ParameterMode.IN, name = "quantity", type = Integer.class),
}
)
@Entity
@Table(name = "articulos")
@Getter
Expand All @@ -45,6 +58,10 @@ public class Articulo {

private Integer stock;

@ManyToOne
@JoinColumn(name = "usuario_id")
private Usuario usuario;

@Column(name = "create_at", nullable = false, updatable = false)
@Temporal(TemporalType.TIMESTAMP)
@CreatedDate
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/pe/edu/utp/entity/Usuario.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ public class Usuario implements UserDetails {
@Column(name = "rol", length = 20, nullable = false)
private Rol rol;

private String country;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
Expand Down
7 changes: 7 additions & 0 deletions src/main/java/pe/edu/utp/repository/ArticuloRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.QueryHints;
import org.springframework.data.jpa.repository.query.Procedure;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

Expand Down Expand Up @@ -39,4 +40,10 @@ List<Articulo> findByCategoriaAndMarcaAndPrecioBetween(
@Param("precioMax") Double precioMax,
Pageable pageable
);

@Procedure(name = "updateStockProcedure")
void updateStook(@Param("productId") Integer productId, @Param("quantity") Integer quantity);

@Query(value = "Select get_total_price(:productId)", nativeQuery = true)
Double getTotalPrice(Integer productId);
}
48 changes: 34 additions & 14 deletions src/main/java/pe/edu/utp/security/JwtService.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,17 @@ public class JwtService {
private static final String TOKEN_HEADER = "Authorization";
private static final String TOKEN_PREFIX = "Bearer ";

public String extractUsername(String token) {
return extractClaim(token, claims -> claims.get("username", String.class));
public String extractUsername(String tokenOrHeader) {
return extractClaim(tokenOrHeader, claims -> claims.get("username", String.class));
}

public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
public <T> T extractClaim(String tokenOrHeader, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(tokenOrHeader);
return claimsResolver.apply(claims);
}

private Claims extractAllClaims(String token) {
private Claims extractAllClaims(String tokenOrHeader) {
String token = sanitizeToken(tokenOrHeader);
Claims claims = Jwts
.parserBuilder()
.setSigningKey(pemReader.getPublicKey())
Expand All @@ -45,14 +46,33 @@ private Claims extractAllClaims(String token) {
return claims;
}

// Quita "Bearer " (con espacio) usando substring y maneja espacios extra
private String sanitizeToken(String tokenOrHeader) {
if (tokenOrHeader == null) {
throw new IllegalArgumentException("JWT no puede ser null");
}
String t = tokenOrHeader.trim();
// Case-insensitive por si viene "bearer ..."
if (
t.length() >= TOKEN_PREFIX.length() &&
t.regionMatches(true, 0, TOKEN_PREFIX, 0, TOKEN_PREFIX.length())
) {
t = t.substring(TOKEN_PREFIX.length()).trim();
}
if (t.isEmpty()) {
throw new IllegalArgumentException("JWT está vacío después de quitar el prefijo Bearer");
}
return t;
}

public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}

public String extractToken(HttpServletRequest request) {
String bearerToken = request.getHeader(TOKEN_HEADER);
if (bearerToken != null && bearerToken.startsWith(TOKEN_PREFIX)) {
return bearerToken.substring(TOKEN_PREFIX.length());
return bearerToken.substring(TOKEN_PREFIX.length()).trim();
}
return null;
}
Expand All @@ -77,7 +97,7 @@ public String generateToken(Map<String, Object> extraClaims, UserDetails userDet
Claims claims = Jwts.claims();
claims.put("username", userDetails.getUsername());
claims.put("roles", userDetails.getAuthorities().toString());
claims.putAll(extraClaims); // Agregar campos adicionales
claims.putAll(extraClaims);

return Jwts
.builder()
Expand All @@ -89,20 +109,20 @@ public String generateToken(Map<String, Object> extraClaims, UserDetails userDet
.compact();
}

public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
public boolean isTokenValid(String tokenOrHeader, UserDetails userDetails) {
final String username = extractUsername(tokenOrHeader);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(tokenOrHeader);
}

public boolean validateClaims(Claims claims) {
return claims.getExpiration().after(new Date());
}

private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
private boolean isTokenExpired(String tokenOrHeader) {
return extractExpiration(tokenOrHeader).before(new Date());
}

private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
private Date extractExpiration(String tokenOrHeader) {
return extractClaim(tokenOrHeader, Claims::getExpiration);
}
}
Loading