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