feat: Initial MAPS Connect backend setup

- Implement Spring Boot REST API foundation - Add JWT authentication and security configuration - Configure CORS for frontend integration - Add user authentication (login/register) - Implement global exception handling - Add DTOs and entity models - Include MySQL database configuration - Add multiple environment profiles (dev/prod) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Daniel Silva committed Aug 17, 2026 at 18:03 UTC 9d61a893d679de09f460c9dd22a4a804a8caabd6
23 files changed +981 -2
.gitignore new
+42
@@ -0,0 +1,42 @@
1 +# IDE
2 +.idea/
3 +.vscode/
4 +*.swp
5 +*.swo
6 +*~
7 +.DS_Store
8 +*.iml
9 +out/
10 +
11 +# Maven
12 +target/
13 +.mvn/wrapper/maven-wrapper.jar
14 +.mvn/wrapper/maven-wrapper.properties
15 +
16 +# Build
17 +*.class
18 +*.jar
19 +*.war
20 +*.nar
21 +*.ear
22 +*.zip
23 +*.tar.gz
24 +*.rar
25 +
26 +# Gradle
27 +.gradle/
28 +build/
29 +
30 +# Environment
31 +.env
32 +.env.local
33 +.env.*.local
34 +application-local.yml
35 +
36 +# Logs
37 +*.log
38 +logs/
39 +
40 +# Temp files
41 +*.tmp
42 +temp/
README.md
+95 -2
@@ -1,2 +1,95 @@
1 -# maps-conect
2 -proyecto final de base de datos
1 +# MAPS Connect - Backend API
2 +
3 +Plataforma académica y de mentoría MAPS de Universidad Tecmilenio.
4 +
5 +## Requisitos Previos
6 +
7 +- Java 17+
8 +- Maven 3.8+
9 +- MySQL 8.0+
10 +
11 +## Configuración de Base de Datos
12 +
13 +Crear base de datos antes de ejecutar:
14 +
15 +```sql
16 +CREATE DATABASE maps_conect;
17 +CREATE DATABASE maps_conect_dev;
18 +```
19 +
20 +Actualizar credenciales en `application.yml`:
21 +
22 +```yaml
23 +spring:
24 + datasource:
25 + url: jdbc:mysql://localhost:3306/maps_conect
26 + username: root
27 + password: tu_contraseña
28 +```
29 +
30 +## Compilación
31 +
32 +```bash
33 +cd C:\Users\danie\IdeaProjects\maps-conect
34 +mvn clean install
35 +```
36 +
37 +## Ejecución
38 +
39 +### Desarrollo
40 +```bash
41 +mvn spring-boot:run -Dspring-boot.run.arguments="--spring.profiles.active=dev"
42 +```
43 +
44 +### Producción
45 +```bash
46 +mvn spring-boot:run -Dspring-boot.run.arguments="--spring.profiles.active=prod"
47 +```
48 +
49 +## API Health Check
50 +
51 +```bash
52 +curl http://localhost:8080/api/health
53 +```
54 +
55 +## Estructura del Proyecto
56 +
57 +```
58 +src/main/java/com/tecmilenio/mapsconect/
59 +├── config/ # Configuración (CORS, Seguridad, JWT)
60 +├── controller/ # Endpoints REST
61 +├── service/ # Lógica de negocio
62 +├── repository/ # Acceso a datos
63 +├── entity/ # Entidades JPA
64 +├── dto/ # Transfer Objects
65 +├── exception/ # Manejo de excepciones
66 +├── security/ # JWT y autenticación
67 +└── util/ # Utilidades
68 +```
69 +
70 +## Configuración de Seguridad
71 +
72 +- JWT Token: Cambiar `jwt.secret` en `application.yml` en producción
73 +- Expiración: 24 horas por defecto
74 +- CORS: Configurado para localhost:3000 y 5173
75 +
76 +## Módulos Implementados
77 +
78 +✅ Autenticación con JWT
79 +✅ Registro de usuarios
80 +✅ Login
81 +✅ CORS configurado
82 +✅ Manejo global de excepciones
83 +✅ DTOs y validaciones
84 +
85 +## Próximos Pasos
86 +
87 +1. Crear entidades adicionales (Carrera, Materia, Foro, etc.)
88 +2. Implementar repositorios y servicios
89 +3. Crear controllers para cada módulo
90 +4. Implementar lógica de foros y comunidades
91 +5. Agregar tests
92 +
93 +## Contacto
94 +
95 +Universidad Tecmilenio - Proyecto MAPS Connect
\ No newline at end of file
pom.xml new
+143
@@ -0,0 +1,143 @@
1 +<?xml version="1.0" encoding="UTF-8"?>
2 +<project xmlns="http://maven.apache.org/POM/4.0.0"
3 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5 + <modelVersion>4.0.0</modelVersion>
6 +
7 + <parent>
8 + <groupId>org.springframework.boot</groupId>
9 + <artifactId>spring-boot-starter-parent</artifactId>
10 + <version>3.3.0</version>
11 + <relativePath/>
12 + </parent>
13 +
14 + <groupId>com.tecmilenio</groupId>
15 + <artifactId>maps-conect</artifactId>
16 + <version>1.0.0</version>
17 + <name>MAPS Connect</name>
18 + <description>Plataforma académica y de mentoría MAPS</description>
19 +
20 + <properties>
21 + <java.version>25</java.version>
22 + <maven.compiler.source>25</maven.compiler.source>
23 + <maven.compiler.target>25</maven.compiler.target>
24 + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
25 + </properties>
26 +
27 + <dependencies>
28 + <!-- Spring Boot Web -->
29 + <dependency>
30 + <groupId>org.springframework.boot</groupId>
31 + <artifactId>spring-boot-starter-web</artifactId>
32 + </dependency>
33 +
34 + <!-- Spring Boot JPA/Hibernate -->
35 + <dependency>
36 + <groupId>org.springframework.boot</groupId>
37 + <artifactId>spring-boot-starter-data-jpa</artifactId>
38 + </dependency>
39 +
40 + <!-- Spring Boot Security -->
41 + <dependency>
42 + <groupId>org.springframework.boot</groupId>
43 + <artifactId>spring-boot-starter-security</artifactId>
44 + </dependency>
45 +
46 + <!-- Spring Boot Validation -->
47 + <dependency>
48 + <groupId>org.springframework.boot</groupId>
49 + <artifactId>spring-boot-starter-validation</artifactId>
50 + </dependency>
51 +
52 + <!-- MySQL Driver -->
53 + <dependency>
54 + <groupId>com.mysql</groupId>
55 + <artifactId>mysql-connector-j</artifactId>
56 + <version>8.1.0</version>
57 + <scope>runtime</scope>
58 + </dependency>
59 +
60 + <!-- Lombok -->
61 + <dependency>
62 + <groupId>org.projectlombok</groupId>
63 + <artifactId>lombok</artifactId>
64 + <optional>true</optional>
65 + <scope>provided</scope>
66 + </dependency>
67 +
68 + <!-- JWT (JSON Web Token) -->
69 + <dependency>
70 + <groupId>io.jsonwebtoken</groupId>
71 + <artifactId>jjwt-api</artifactId>
72 + <version>0.12.3</version>
73 + </dependency>
74 + <dependency>
75 + <groupId>io.jsonwebtoken</groupId>
76 + <artifactId>jjwt-impl</artifactId>
77 + <version>0.12.3</version>
78 + <scope>runtime</scope>
79 + </dependency>
80 + <dependency>
81 + <groupId>io.jsonwebtoken</groupId>
82 + <artifactId>jjwt-jackson</artifactId>
83 + <version>0.12.3</version>
84 + <scope>runtime</scope>
85 + </dependency>
86 +
87 + <!-- Gson -->
88 + <dependency>
89 + <groupId>com.google.code.gson</groupId>
90 + <artifactId>gson</artifactId>
91 + <version>2.10.1</version>
92 + </dependency>
93 +
94 + <!-- Testing -->
95 + <dependency>
96 + <groupId>org.springframework.boot</groupId>
97 + <artifactId>spring-boot-starter-test</artifactId>
98 + <scope>test</scope>
99 + </dependency>
100 +
101 + <!-- Spring Security Test -->
102 + <dependency>
103 + <groupId>org.springframework.security</groupId>
104 + <artifactId>spring-security-test</artifactId>
105 + <scope>test</scope>
106 + </dependency>
107 + </dependencies>
108 +
109 + <build>
110 + <plugins>
111 + <plugin>
112 + <groupId>org.springframework.boot</groupId>
113 + <artifactId>spring-boot-maven-plugin</artifactId>
114 + <configuration>
115 + <excludes>
116 + <exclude>
117 + <groupId>org.projectlombok</groupId>
118 + <artifactId>lombok</artifactId>
119 + </exclude>
120 + </excludes>
121 + </configuration>
122 + </plugin>
123 +
124 + <plugin>
125 + <groupId>org.apache.maven.plugins</groupId>
126 + <artifactId>maven-compiler-plugin</artifactId>
127 + <version>3.13.0</version>
128 + <configuration>
129 + <source>25</source>
130 + <target>25</target>
131 + <annotationProcessorPaths>
132 + <path>
133 + <groupId>org.projectlombok</groupId>
134 + <artifactId>lombok</artifactId>
135 + <version>1.18.30</version>
136 + </path>
137 + </annotationProcessorPaths>
138 + </configuration>
139 + </plugin>
140 + </plugins>
141 + </build>
142 +
143 +</project>
\ No newline at end of file
src/main/java/com/tecmilenio/mapsconect/MapsConectApplication.java new
+13
@@ -0,0 +1,13 @@
1 +package com.tecmilenio.mapsconect;
2 +
3 +import org.springframework.boot.SpringApplication;
4 +import org.springframework.boot.autoconfigure.SpringBootApplication;
5 +
6 +@SpringBootApplication
7 +public class MapsConectApplication {
8 +
9 + public static void main(String[] args) {
10 + SpringApplication.run(MapsConectApplication.class, args);
11 + }
12 +
13 +}
src/main/java/com/tecmilenio/mapsconect/config/CorsConfig.java new
+20
@@ -0,0 +1,20 @@
1 +package com.tecmilenio.mapsconect.config;
2 +
3 +import org.springframework.context.annotation.Configuration;
4 +import org.springframework.web.servlet.config.annotation.CorsRegistry;
5 +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
6 +
7 +@Configuration
8 +public class CorsConfig implements WebMvcConfigurer {
9 +
10 + @Override
11 + public void addCorsMappings(CorsRegistry registry) {
12 + registry.addMapping("/**")
13 + .allowedOrigins("http://localhost:3000", "http://localhost:5173")
14 + .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
15 + .allowedHeaders("*")
16 + .allowCredentials(true)
17 + .maxAge(3600);
18 + }
19 +
20 +}
src/main/java/com/tecmilenio/mapsconect/config/SecurityConfig.java new
+44
@@ -0,0 +1,44 @@
1 +package com.tecmilenio.mapsconect.config;
2 +
3 +import org.springframework.context.annotation.Bean;
4 +import org.springframework.context.annotation.Configuration;
5 +import org.springframework.security.authentication.AuthenticationManager;
6 +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
7 +import org.springframework.security.config.annotation.web.builders.HttpSecurity;
8 +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
9 +import org.springframework.security.config.http.SessionCreationPolicy;
10 +import org.springframework.security.web.SecurityFilterChain;
11 +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
12 +
13 +import com.tecmilenio.mapsconect.security.JwtAuthenticationFilter;
14 +
15 +@Configuration
16 +@EnableWebSecurity
17 +public class SecurityConfig {
18 +
19 + @Bean
20 + public JwtAuthenticationFilter jwtAuthenticationFilter() {
21 + return new JwtAuthenticationFilter();
22 + }
23 +
24 + @Bean
25 + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
26 + return config.getAuthenticationManager();
27 + }
28 +
29 + @Bean
30 + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
31 + http
32 + .csrf(csrf -> csrf.disable())
33 + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
34 + .authorizeHttpRequests(authz -> authz
35 + .requestMatchers("/api/auth/**").permitAll()
36 + .requestMatchers("/api/health").permitAll()
37 + .anyRequest().authenticated()
38 + )
39 + .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
40 +
41 + return http.build();
42 + }
43 +
44 +}
src/main/java/com/tecmilenio/mapsconect/controller/AuthController.java new
+35
@@ -0,0 +1,35 @@
1 +package com.tecmilenio.mapsconect.controller;
2 +
3 +import com.tecmilenio.mapsconect.dto.ApiResponse;
4 +import com.tecmilenio.mapsconect.dto.LoginDTO;
5 +import com.tecmilenio.mapsconect.dto.RegistroDTO;
6 +import com.tecmilenio.mapsconect.dto.TokenDTO;
7 +import com.tecmilenio.mapsconect.service.UsuarioService;
8 +import org.springframework.beans.factory.annotation.Autowired;
9 +import org.springframework.http.HttpStatus;
10 +import org.springframework.http.ResponseEntity;
11 +import org.springframework.web.bind.annotation.*;
12 +
13 +import jakarta.validation.Valid;
14 +
15 +@RestController
16 +@RequestMapping("/auth")
17 +public class AuthController {
18 +
19 + @Autowired
20 + private UsuarioService usuarioService;
21 +
22 + @PostMapping("/registrar")
23 + public ResponseEntity<ApiResponse<TokenDTO>> registrar(@Valid @RequestBody RegistroDTO registroDTO) {
24 + TokenDTO token = usuarioService.registrar(registroDTO);
25 + return ResponseEntity.status(HttpStatus.CREATED)
26 + .body(ApiResponse.success(token, "Usuario registrado correctamente"));
27 + }
28 +
29 + @PostMapping("/login")
30 + public ResponseEntity<ApiResponse<TokenDTO>> login(@Valid @RequestBody LoginDTO loginDTO) {
31 + TokenDTO token = usuarioService.login(loginDTO);
32 + return ResponseEntity.ok(ApiResponse.success(token, "Sesión iniciada correctamente"));
33 + }
34 +
35 +}
src/main/java/com/tecmilenio/mapsconect/controller/HealthController.java new
+20
@@ -0,0 +1,20 @@
1 +package com.tecmilenio.mapsconect.controller;
2 +
3 +import com.tecmilenio.mapsconect.dto.ApiResponse;
4 +import org.springframework.http.ResponseEntity;
5 +import org.springframework.web.bind.annotation.GetMapping;
6 +import org.springframework.web.bind.annotation.RequestMapping;
7 +import org.springframework.web.bind.annotation.RestController;
8 +
9 +@RestController
10 +@RequestMapping("/health")
11 +public class HealthController {
12 +
13 + @GetMapping
14 + public ResponseEntity<ApiResponse<String>> health() {
15 + return ResponseEntity.ok(
16 + ApiResponse.success("MAPS Connect Backend is running", "OK")
17 + );
18 + }
19 +
20 +}
src/main/java/com/tecmilenio/mapsconect/dto/ApiResponse.java new
+36
@@ -0,0 +1,36 @@
1 +package com.tecmilenio.mapsconect.dto;
2 +
3 +import lombok.AllArgsConstructor;
4 +import lombok.Builder;
5 +import lombok.Data;
6 +import lombok.NoArgsConstructor;
7 +
8 +@Data
9 +@NoArgsConstructor
10 +@AllArgsConstructor
11 +@Builder
12 +public class ApiResponse<T> {
13 +
14 + private int status;
15 + private String message;
16 + private T data;
17 + private String timestamp;
18 +
19 + public static <T> ApiResponse<T> success(T data, String message) {
20 + return ApiResponse.<T>builder()
21 + .status(200)
22 + .message(message)
23 + .data(data)
24 + .timestamp(java.time.LocalDateTime.now().toString())
25 + .build();
26 + }
27 +
28 + public static <T> ApiResponse<T> error(int status, String message) {
29 + return ApiResponse.<T>builder()
30 + .status(status)
31 + .message(message)
32 + .timestamp(java.time.LocalDateTime.now().toString())
33 + .build();
34 + }
35 +
36 +}
src/main/java/com/tecmilenio/mapsconect/dto/LoginDTO.java new
+24
@@ -0,0 +1,24 @@
1 +package com.tecmilenio.mapsconect.dto;
2 +
3 +import lombok.AllArgsConstructor;
4 +import lombok.Builder;
5 +import lombok.Data;
6 +import lombok.NoArgsConstructor;
7 +
8 +import jakarta.validation.constraints.Email;
9 +import jakarta.validation.constraints.NotBlank;
10 +
11 +@Data
12 +@NoArgsConstructor
13 +@AllArgsConstructor
14 +@Builder
15 +public class LoginDTO {
16 +
17 + @NotBlank(message = "El email es obligatorio")
18 + @Email(message = "El email debe ser válido")
19 + private String email;
20 +
21 + @NotBlank(message = "La contraseña es obligatoria")
22 + private String contrasena;
23 +
24 +}
src/main/java/com/tecmilenio/mapsconect/dto/RegistroDTO.java new
+36
@@ -0,0 +1,36 @@
1 +package com.tecmilenio.mapsconect.dto;
2 +
3 +import lombok.AllArgsConstructor;
4 +import lombok.Builder;
5 +import lombok.Data;
6 +import lombok.NoArgsConstructor;
7 +
8 +import jakarta.validation.constraints.Email;
9 +import jakarta.validation.constraints.NotBlank;
10 +import jakarta.validation.constraints.Size;
11 +
12 +@Data
13 +@NoArgsConstructor
14 +@AllArgsConstructor
15 +@Builder
16 +public class RegistroDTO {
17 +
18 + @NotBlank(message = "El email es obligatorio")
19 + @Email(message = "El email debe ser válido")
20 + private String email;
21 +
22 + @NotBlank(message = "El nombre es obligatorio")
23 + @Size(min = 2, max = 100, message = "El nombre debe tener entre 2 y 100 caracteres")
24 + private String nombre;
25 +
26 + @NotBlank(message = "El apellido es obligatorio")
27 + @Size(min = 2, max = 100, message = "El apellido debe tener entre 2 y 100 caracteres")
28 + private String apellido;
29 +
30 + @NotBlank(message = "La contraseña es obligatoria")
31 + @Size(min = 6, max = 100, message = "La contraseña debe tener entre 6 y 100 caracteres")
32 + private String contrasena;
33 +
34 + private String rol;
35 +
36 +}
src/main/java/com/tecmilenio/mapsconect/dto/TokenDTO.java new
+19
@@ -0,0 +1,19 @@
1 +package com.tecmilenio.mapsconect.dto;
2 +
3 +import lombok.AllArgsConstructor;
4 +import lombok.Builder;
5 +import lombok.Data;
6 +import lombok.NoArgsConstructor;
7 +
8 +@Data
9 +@NoArgsConstructor
10 +@AllArgsConstructor
11 +@Builder
12 +public class TokenDTO {
13 +
14 + private String token;
15 + private String tipo = "Bearer";
16 + private Long expiresIn;
17 + private UsuarioDTO usuario;
18 +
19 +}
src/main/java/com/tecmilenio/mapsconect/dto/UsuarioDTO.java new
+21
@@ -0,0 +1,21 @@
1 +package com.tecmilenio.mapsconect.dto;
2 +
3 +import lombok.AllArgsConstructor;
4 +import lombok.Builder;
5 +import lombok.Data;
6 +import lombok.NoArgsConstructor;
7 +
8 +@Data
9 +@NoArgsConstructor
10 +@AllArgsConstructor
11 +@Builder
12 +public class UsuarioDTO {
13 +
14 + private Long id;
15 + private String email;
16 + private String nombre;
17 + private String apellido;
18 + private String rol;
19 + private Boolean activo;
20 +
21 +}
src/main/java/com/tecmilenio/mapsconect/entity/Usuario.java new
+51
@@ -0,0 +1,51 @@
1 +package com.tecmilenio.mapsconect.entity;
2 +
3 +import jakarta.persistence.*;
4 +import lombok.AllArgsConstructor;
5 +import lombok.Builder;
6 +import lombok.Data;
7 +import lombok.NoArgsConstructor;
8 +
9 +import java.time.LocalDateTime;
10 +
11 +@Entity
12 +@Table(name = "usuarios")
13 +@Data
14 +@NoArgsConstructor
15 +@AllArgsConstructor
16 +@Builder
17 +public class Usuario {
18 +
19 + @Id
20 + @GeneratedValue(strategy = GenerationType.IDENTITY)
21 + private Long id;
22 +
23 + @Column(nullable = false, unique = true, length = 100)
24 + private String email;
25 +
26 + @Column(nullable = false, length = 100)
27 + private String nombre;
28 +
29 + @Column(nullable = false, length = 100)
30 + private String apellido;
31 +
32 + @Column(nullable = false)
33 + private String contrasena;
34 +
35 + @Enumerated(EnumType.STRING)
36 + private Rol rol;
37 +
38 + @Column(nullable = false)
39 + private Boolean activo = true;
40 +
41 + @Column(nullable = false)
42 + private LocalDateTime fechaCreacion = LocalDateTime.now();
43 +
44 + @Column
45 + private LocalDateTime fechaActualizacion;
46 +
47 + public enum Rol {
48 + ESTUDIANTE, DOCENTE, EGRESADO, ADMIN
49 + }
50 +
51 +}
src/main/java/com/tecmilenio/mapsconect/exception/GlobalExceptionHandler.java new
+48
@@ -0,0 +1,48 @@
1 +package com.tecmilenio.mapsconect.exception;
2 +
3 +import com.tecmilenio.mapsconect.dto.ApiResponse;
4 +import org.springframework.http.HttpStatus;
5 +import org.springframework.http.ResponseEntity;
6 +import org.springframework.security.access.AccessDeniedException;
7 +import org.springframework.validation.FieldError;
8 +import org.springframework.web.bind.MethodArgumentNotValidException;
9 +import org.springframework.web.bind.annotation.ExceptionHandler;
10 +import org.springframework.web.bind.annotation.RestControllerAdvice;
11 +
12 +import java.util.HashMap;
13 +import java.util.Map;
14 +
15 +@RestControllerAdvice
16 +public class GlobalExceptionHandler {
17 +
18 + @ExceptionHandler(ResourceNotFoundException.class)
19 + public ResponseEntity<ApiResponse<?>> handleResourceNotFound(ResourceNotFoundException ex) {
20 + return ResponseEntity.status(HttpStatus.NOT_FOUND)
21 + .body(ApiResponse.error(404, ex.getMessage()));
22 + }
23 +
24 + @ExceptionHandler(AccessDeniedException.class)
25 + public ResponseEntity<ApiResponse<?>> handleAccessDenied(AccessDeniedException ex) {
26 + return ResponseEntity.status(HttpStatus.FORBIDDEN)
27 + .body(ApiResponse.error(403, "Acceso denegado"));
28 + }
29 +
30 + @ExceptionHandler(MethodArgumentNotValidException.class)
31 + public ResponseEntity<ApiResponse<?>> handleValidationExceptions(MethodArgumentNotValidException ex) {
32 + Map<String, String> errors = new HashMap<>();
33 + ex.getBindingResult().getAllErrors().forEach(error -> {
34 + String fieldName = ((FieldError) error).getField();
35 + String errorMessage = error.getDefaultMessage();
36 + errors.put(fieldName, errorMessage);
37 + });
38 + return ResponseEntity.status(HttpStatus.BAD_REQUEST)
39 + .body(ApiResponse.error(400, errors.toString()));
40 + }
41 +
42 + @ExceptionHandler(Exception.class)
43 + public ResponseEntity<ApiResponse<?>> handleGenericException(Exception ex) {
44 + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
45 + .body(ApiResponse.error(500, "Error interno del servidor"));
46 + }
47 +
48 +}
src/main/java/com/tecmilenio/mapsconect/exception/ResourceNotFoundException.java new
+13
@@ -0,0 +1,13 @@
1 +package com.tecmilenio.mapsconect.exception;
2 +
3 +public class ResourceNotFoundException extends RuntimeException {
4 +
5 + public ResourceNotFoundException(String message) {
6 + super(message);
7 + }
8 +
9 + public ResourceNotFoundException(String message, Throwable cause) {
10 + super(message, cause);
11 + }
12 +
13 +}
src/main/java/com/tecmilenio/mapsconect/repository/UsuarioRepository.java new
+16
@@ -0,0 +1,16 @@
1 +package com.tecmilenio.mapsconect.repository;
2 +
3 +import com.tecmilenio.mapsconect.entity.Usuario;
4 +import org.springframework.data.jpa.repository.JpaRepository;
5 +import org.springframework.stereotype.Repository;
6 +
7 +import java.util.Optional;
8 +
9 +@Repository
10 +public interface UsuarioRepository extends JpaRepository<Usuario, Long> {
11 +
12 + Optional<Usuario> findByEmail(String email);
13 +
14 + boolean existsByEmail(String email);
15 +
16 +}
src/main/java/com/tecmilenio/mapsconect/security/JwtAuthenticationFilter.java new
+57
@@ -0,0 +1,57 @@
1 +package com.tecmilenio.mapsconect.security;
2 +
3 +import org.springframework.beans.factory.annotation.Autowired;
4 +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
5 +import org.springframework.security.core.context.SecurityContextHolder;
6 +import org.springframework.security.core.userdetails.UserDetails;
7 +import org.springframework.security.core.userdetails.UserDetailsService;
8 +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
9 +import org.springframework.util.StringUtils;
10 +import org.springframework.web.filter.OncePerRequestFilter;
11 +
12 +import jakarta.servlet.FilterChain;
13 +import jakarta.servlet.ServletException;
14 +import jakarta.servlet.http.HttpServletRequest;
15 +import jakarta.servlet.http.HttpServletResponse;
16 +import java.io.IOException;
17 +
18 +public class JwtAuthenticationFilter extends OncePerRequestFilter {
19 +
20 + @Autowired
21 + private JwtTokenProvider tokenProvider;
22 +
23 + @Autowired
24 + private UserDetailsService userDetailsService;
25 +
26 + @Override
27 + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
28 + throws ServletException, IOException {
29 + try {
30 + String jwt = getJwtFromRequest(request);
31 +
32 + if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
33 + String username = tokenProvider.getUsernameFromToken(jwt);
34 + UserDetails userDetails = userDetailsService.loadUserByUsername(username);
35 +
36 + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
37 + userDetails, null, userDetails.getAuthorities());
38 + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
39 +
40 + SecurityContextHolder.getContext().setAuthentication(authentication);
41 + }
42 + } catch (Exception ex) {
43 + logger.error("Fallo al autenticar con JWT", ex);
44 + }
45 +
46 + filterChain.doFilter(request, response);
47 + }
48 +
49 + private String getJwtFromRequest(HttpServletRequest request) {
50 + String bearerToken = request.getHeader("Authorization");
51 + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
52 + return bearerToken.substring(7);
53 + }
54 + return null;
55 + }
56 +
57 +}
src/main/java/com/tecmilenio/mapsconect/security/JwtTokenProvider.java new
+75
@@ -0,0 +1,75 @@
1 +package com.tecmilenio.mapsconect.security;
2 +
3 +import io.jsonwebtoken.Claims;
4 +import io.jsonwebtoken.Jwts;
5 +import io.jsonwebtoken.SignatureAlgorithm;
6 +import io.jsonwebtoken.security.Keys;
7 +import org.springframework.beans.factory.annotation.Value;
8 +import org.springframework.security.core.userdetails.UserDetails;
9 +import org.springframework.stereotype.Component;
10 +
11 +import javax.crypto.SecretKey;
12 +import java.util.Date;
13 +import java.util.HashMap;
14 +import java.util.Map;
15 +
16 +@Component
17 +public class JwtTokenProvider {
18 +
19 + @Value("${jwt.secret}")
20 + private String jwtSecret;
21 +
22 + @Value("${jwt.expiration}")
23 + private long jwtExpirationMs;
24 +
25 + private SecretKey getSigningKey() {
26 + return Keys.hmacShaKeyFor(jwtSecret.getBytes());
27 + }
28 +
29 + public String generateToken(String username) {
30 + Map<String, Object> claims = new HashMap<>();
31 + return createToken(claims, username);
32 + }
33 +
34 + public String generateTokenWithClaims(String username, Map<String, Object> claims) {
35 + return createToken(claims, username);
36 + }
37 +
38 + private String createToken(Map<String, Object> claims, String subject) {
39 + Date now = new Date();
40 + Date expiryDate = new Date(now.getTime() + jwtExpirationMs);
41 +
42 + return Jwts.builder()
43 + .setClaims(claims)
44 + .setSubject(subject)
45 + .setIssuedAt(now)
46 + .setExpiration(expiryDate)
47 + .signWith(getSigningKey(), SignatureAlgorithm.HS512)
48 + .compact();
49 + }
50 +
51 + public String getUsernameFromToken(String token) {
52 + return getClaimsFromToken(token).getSubject();
53 + }
54 +
55 + public boolean validateToken(String token) {
56 + try {
57 + Jwts.parserBuilder()
58 + .setSigningKey(getSigningKey())
59 + .build()
60 + .parseClaimsJws(token);
61 + return true;
62 + } catch (Exception e) {
63 + return false;
64 + }
65 + }
66 +
67 + private Claims getClaimsFromToken(String token) {
68 + return Jwts.parserBuilder()
69 + .setSigningKey(getSigningKey())
70 + .build()
71 + .parseClaimsJws(token)
72 + .getBody();
73 + }
74 +
75 +}
src/main/java/com/tecmilenio/mapsconect/service/UsuarioService.java new
+87
@@ -0,0 +1,87 @@
1 +package com.tecmilenio.mapsconect.service;
2 +
3 +import com.tecmilenio.mapsconect.dto.LoginDTO;
4 +import com.tecmilenio.mapsconect.dto.RegistroDTO;
5 +import com.tecmilenio.mapsconect.dto.TokenDTO;
6 +import com.tecmilenio.mapsconect.dto.UsuarioDTO;
7 +import com.tecmilenio.mapsconect.entity.Usuario;
8 +import com.tecmilenio.mapsconect.exception.ResourceNotFoundException;
9 +import com.tecmilenio.mapsconect.repository.UsuarioRepository;
10 +import com.tecmilenio.mapsconect.security.JwtTokenProvider;
11 +import org.springframework.beans.factory.annotation.Autowired;
12 +import org.springframework.security.crypto.password.PasswordEncoder;
13 +import org.springframework.stereotype.Service;
14 +
15 +@Service
16 +public class UsuarioService {
17 +
18 + @Autowired
19 + private UsuarioRepository usuarioRepository;
20 +
21 + @Autowired
22 + private PasswordEncoder passwordEncoder;
23 +
24 + @Autowired
25 + private JwtTokenProvider jwtTokenProvider;
26 +
27 + public TokenDTO registrar(RegistroDTO registroDTO) {
28 + if (usuarioRepository.existsByEmail(registroDTO.getEmail())) {
29 + throw new RuntimeException("El email ya está registrado");
30 + }
31 +
32 + Usuario usuario = Usuario.builder()
33 + .email(registroDTO.getEmail())
34 + .nombre(registroDTO.getNombre())
35 + .apellido(registroDTO.getApellido())
36 + .contrasena(passwordEncoder.encode(registroDTO.getContrasena()))
37 + .rol(Usuario.Rol.ESTUDIANTE)
38 + .activo(true)
39 + .build();
40 +
41 + Usuario usuarioGuardado = usuarioRepository.save(usuario);
42 + String token = jwtTokenProvider.generateToken(usuarioGuardado.getEmail());
43 +
44 + return TokenDTO.builder()
45 + .token(token)
46 + .tipo("Bearer")
47 + .expiresIn(86400L)
48 + .usuario(mapearADTO(usuarioGuardado))
49 + .build();
50 + }
51 +
52 + public TokenDTO login(LoginDTO loginDTO) {
53 + Usuario usuario = usuarioRepository.findByEmail(loginDTO.getEmail())
54 + .orElseThrow(() -> new ResourceNotFoundException("Usuario no encontrado"));
55 +
56 + if (!passwordEncoder.matches(loginDTO.getContrasena(), usuario.getContrasena())) {
57 + throw new RuntimeException("Contraseña incorrecta");
58 + }
59 +
60 + String token = jwtTokenProvider.generateToken(usuario.getEmail());
61 +
62 + return TokenDTO.builder()
63 + .token(token)
64 + .tipo("Bearer")
65 + .expiresIn(86400L)
66 + .usuario(mapearADTO(usuario))
67 + .build();
68 + }
69 +
70 + public UsuarioDTO obtenerPorEmail(String email) {
71 + Usuario usuario = usuarioRepository.findByEmail(email)
72 + .orElseThrow(() -> new ResourceNotFoundException("Usuario no encontrado"));
73 + return mapearADTO(usuario);
74 + }
75 +
76 + private UsuarioDTO mapearADTO(Usuario usuario) {
77 + return UsuarioDTO.builder()
78 + .id(usuario.getId())
79 + .email(usuario.getEmail())
80 + .nombre(usuario.getNombre())
81 + .apellido(usuario.getApellido())
82 + .rol(usuario.getRol().toString())
83 + .activo(usuario.getActivo())
84 + .build();
85 + }
86 +
87 +}
src/main/resources/application-dev.yml new
+22
@@ -0,0 +1,22 @@
1 +spring:
2 + datasource:
3 + url: jdbc:mysql://localhost:3306/maps_conect_dev
4 + username: root
5 + password:
6 +
7 + jpa:
8 + hibernate:
9 + ddl-auto: create-drop
10 + show-sql: true
11 + properties:
12 + hibernate:
13 + format_sql: true
14 + generate_statistics: false
15 +
16 +logging:
17 + level:
18 + root: INFO
19 + com.tecmilenio.mapsconect: DEBUG
20 + org.springframework.web: DEBUG
21 + org.hibernate.SQL: DEBUG
22 + org.hibernate.type.descriptor.sql.BasicBinder: TRACE
src/main/resources/application-prod.yml new
+19
@@ -0,0 +1,19 @@
1 +spring:
2 + datasource:
3 + url: jdbc:mysql://${DB_HOST}:${DB_PORT}/${DB_NAME}
4 + username: ${DB_USER}
5 + password: ${DB_PASSWORD}
6 +
7 + jpa:
8 + hibernate:
9 + ddl-auto: validate
10 + show-sql: false
11 +
12 +logging:
13 + level:
14 + root: WARN
15 + com.tecmilenio.mapsconect: INFO
16 +
17 +jwt:
18 + secret: ${JWT_SECRET}
19 + expiration: ${JWT_EXPIRATION:86400000}
src/main/resources/application.yml new
+45
@@ -0,0 +1,45 @@
1 +spring:
2 + application:
3 + name: maps-conect
4 +
5 + datasource:
6 + url: jdbc:mysql://localhost:3306/maps_conect
7 + username: root
8 + password:
9 + driver-class-name: com.mysql.cj.jdbc.Driver
10 +
11 + jpa:
12 + hibernate:
13 + ddl-auto: validate
14 + show-sql: false
15 + properties:
16 + hibernate:
17 + dialect: org.hibernate.dialect.MySQLDialect
18 + format_sql: true
19 +
20 + mvc:
21 + throw-exception-if-no-handler-found: true
22 +
23 + web:
24 + resources:
25 + add-mappings: false
26 +
27 +server:
28 + port: 8080
29 + servlet:
30 + context-path: /api
31 +
32 +logging:
33 + level:
34 + root: INFO
35 + com.tecmilenio.mapsconect: DEBUG
36 + pattern:
37 + console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
38 +
39 +jwt:
40 + secret: your-secret-key-change-in-production
41 + expiration: 86400000
42 +
43 +app:
44 + name: MAPS Connect
45 + version: 1.0.0