Skip to content

Commit 5fa962b

Browse files
committed
release: merge develop into main for v3.5.0
2 parents fff1ea1 + e394e6d commit 5fa962b

File tree

61 files changed

+3028
-80
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+3028
-80
lines changed

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Mantener y evolucionar una plataforma educativa que transforma libros en aventur
1515
- Arquitectura por capas backend: `domain`, `engine`, `persistence`, `service`, `api`.
1616
- Estructura monorepo: `apps/backend`, `apps/frontend`, `docs`, `scripts`.
1717
- No mezclar entrada/salida con reglas de dominio.
18+
- Priorizacion vigente y criterios de revision interna: `docs/PRIORITY_REQUIREMENTS.md`.
1819

1920
## Flujo de ramas
2021
- `main`: produccion.
@@ -24,6 +25,7 @@ Mantener y evolucionar una plataforma educativa que transforma libros en aventur
2425

2526
## Definicion de terminado
2627
- Backend `mvn test` en verde.
28+
- Frontend `npm run test` en verde.
2729
- Frontend `npm run build` en verde.
2830
- CI sin fallos.
2931
- Documentacion actualizada para cambios de UX/arquitectura.

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,11 @@ Abrir: `http://localhost:5173`
5050
4. Para continuar luego, usa `Cargar sesion` con el `sessionId` (tambien se recuerda automaticamente en el navegador).
5151

5252
## Release desktop
53-
- Release publico actual: `v3.4.0`
53+
- Release publico actual: `v3.5.0`
5454
- Incluye `AutoBookQuest-win64.zip` (portable con `AutoBookQuest.exe`).
5555
- Para instalador `.exe` tipo setup con `jpackage`, se requiere WiX v3 instalado.
5656
- El workflow `release` publica tambien `latest.json` para auto-update.
57+
- Notas del release: `docs/RELEASE_NOTES_v3.5.0.md`.
5758

5859
## Calidad
5960
- Backend: `mvn test`

apps/backend/README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ mvn spring-boot:run
1111

1212
## API
1313
- `GET /api/health`
14+
- `POST /api/auth/login` body: `{ "username": "...", "password": "..." }`
1415
- `GET /api/books`
1516
- `POST /api/books/import` body: `{ "path": "file:///C:/.../libro.pdf" }`
1617
- `POST /api/game/start` body: `{ "playerName": "Juan", "bookPath": "C:/.../libro.pdf" }`
@@ -28,7 +29,29 @@ mvn spring-boot:run
2829
- `GET /api/teacher/classrooms/{classroomId}/assignments`
2930
- `POST /api/teacher/attempts/link`
3031
- `GET /api/teacher/classrooms/{classroomId}/dashboard`
31-
- `GET /api/teacher/classrooms/{classroomId}/report.csv`
32+
- `GET /api/teacher/classrooms/{classroomId}/dashboard?from=YYYY-MM-DD&to=YYYY-MM-DD`
33+
- `GET /api/teacher/classrooms/{classroomId}/report.csv?from=YYYY-MM-DD&to=YYYY-MM-DD`
34+
35+
### Seguridad API (P0)
36+
- Recomendado: `Authorization: Bearer <accessToken>` emitido por `/api/auth/login`.
37+
- Compatibilidad legacy opcional: `X-Api-Token` (controlado por `app.security.allow-legacy-token`).
38+
- Tokens por rol configurables en `application.properties`:
39+
- `app.security.student-token`
40+
- `app.security.teacher-token`
41+
- `app.security.admin-token`
42+
- Credenciales por rol para login:
43+
- `app.security.student-username` / `app.security.student-password`
44+
- `app.security.teacher-username` / `app.security.teacher-password`
45+
- `app.security.admin-username` / `app.security.admin-password`
46+
- Firma y expiracion del token bearer:
47+
- `app.security.jwt-secret`
48+
- `app.security.jwt-previous-secret` (ventana de rotacion)
49+
- `app.security.jwt-ttl-seconds`
50+
- `app.security.allow-legacy-token`
51+
- Rate limit basico configurable:
52+
- `app.rate-limit.window-seconds`
53+
- `app.rate-limit.max-requests`
54+
- `app.rate-limit.teacher-max-requests`
3255

3356
## Arquitectura
3457
- `domain`: entidades de juego.
@@ -44,4 +67,8 @@ mvn spring-boot:run
4467
- El frontend empaquetado se copia a `src/main/resources/static` solo durante build desktop.
4568
- El pipeline narrativo incluye normalizacion de texto, memoria de entidades, grafo de relaciones y nivel cognitivo por escena.
4669
- Persistencia docente sobre JDBC + Flyway (`classrooms`, `students`, `assignments`, `attempts`).
70+
- Persistencia runtime de sesiones de juego sobre JDBC + Flyway (`game_sessions`).
71+
- Vinculacion de intentos docente protegida contra duplicados (`student_id + assignment_id + session_id`).
72+
- Dashboard docente incluye tiempo efectivo y abandono por actividad real.
4773
- Default local con H2 file DB; PostgreSQL habilitado por variables de entorno Spring datasource.
74+
- Importacion de libros restringida a `.txt` y `.pdf` con limite configurable (`app.import.max-bytes`, default 25MB).
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.juegodefinitivo.autobook.api;
2+
3+
import com.juegodefinitivo.autobook.security.ApiAuthInterceptor;
4+
import com.juegodefinitivo.autobook.security.RateLimitInterceptor;
5+
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
7+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
8+
9+
@Configuration
10+
public class ApiInterceptorsConfig implements WebMvcConfigurer {
11+
12+
private final ApiAuthInterceptor apiAuthInterceptor;
13+
private final RateLimitInterceptor rateLimitInterceptor;
14+
15+
public ApiInterceptorsConfig(ApiAuthInterceptor apiAuthInterceptor, RateLimitInterceptor rateLimitInterceptor) {
16+
this.apiAuthInterceptor = apiAuthInterceptor;
17+
this.rateLimitInterceptor = rateLimitInterceptor;
18+
}
19+
20+
@Override
21+
public void addInterceptors(InterceptorRegistry registry) {
22+
registry.addInterceptor(apiAuthInterceptor).addPathPatterns("/api/**");
23+
registry.addInterceptor(rateLimitInterceptor).addPathPatterns("/api/**");
24+
}
25+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.juegodefinitivo.autobook.api;
2+
3+
import com.juegodefinitivo.autobook.api.dto.LoginRequest;
4+
import com.juegodefinitivo.autobook.api.dto.LoginResponse;
5+
import com.juegodefinitivo.autobook.security.AuthService;
6+
import org.springframework.http.HttpStatus;
7+
import org.springframework.web.bind.annotation.ExceptionHandler;
8+
import org.springframework.web.bind.annotation.PostMapping;
9+
import org.springframework.web.bind.annotation.RequestBody;
10+
import org.springframework.web.bind.annotation.RequestMapping;
11+
import org.springframework.web.bind.annotation.ResponseStatus;
12+
import org.springframework.web.bind.annotation.RestController;
13+
import org.springframework.web.server.ResponseStatusException;
14+
15+
import java.util.Map;
16+
17+
@RestController
18+
@RequestMapping("/api/auth")
19+
public class AuthController {
20+
21+
private final AuthService authService;
22+
23+
public AuthController(AuthService authService) {
24+
this.authService = authService;
25+
}
26+
27+
@PostMapping("/login")
28+
public LoginResponse login(@RequestBody LoginRequest request) {
29+
return authService.login(request.username(), request.password())
30+
.map(result -> new LoginResponse(
31+
result.token(),
32+
"Bearer",
33+
result.role().name(),
34+
result.expiresAtEpochSeconds()
35+
))
36+
.orElseThrow(() -> new IllegalArgumentException("Credenciales invalidas."));
37+
}
38+
39+
@ExceptionHandler(IllegalArgumentException.class)
40+
@ResponseStatus(HttpStatus.UNAUTHORIZED)
41+
public Map<String, String> onUnauthorized(IllegalArgumentException ex) {
42+
return Map.of("error", ex.getMessage());
43+
}
44+
45+
@ExceptionHandler(Exception.class)
46+
public void onError(Exception ex) {
47+
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Error interno", ex);
48+
}
49+
}

apps/backend/src/main/java/com/juegodefinitivo/autobook/api/CorsConfig.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public class CorsConfig implements WebMvcConfigurer {
1010
public void addCorsMappings(CorsRegistry registry) {
1111
registry.addMapping("/api/**")
1212
.allowedOrigins("http://localhost:5173", "http://127.0.0.1:5173")
13-
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS");
13+
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
14+
.allowedHeaders("*");
1415
}
1516
}

apps/backend/src/main/java/com/juegodefinitivo/autobook/api/TeacherController.java

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,22 @@
1212
import org.springframework.http.HttpHeaders;
1313
import org.springframework.http.MediaType;
1414
import org.springframework.http.ResponseEntity;
15+
import org.springframework.http.HttpStatus;
16+
import org.springframework.web.bind.annotation.ExceptionHandler;
1517
import org.springframework.web.bind.annotation.GetMapping;
1618
import org.springframework.web.bind.annotation.PathVariable;
1719
import org.springframework.web.bind.annotation.PostMapping;
1820
import org.springframework.web.bind.annotation.RequestBody;
1921
import org.springframework.web.bind.annotation.RequestMapping;
22+
import org.springframework.web.bind.annotation.RequestParam;
23+
import org.springframework.web.bind.annotation.ResponseStatus;
2024
import org.springframework.web.bind.annotation.RestController;
25+
import org.springframework.web.server.ResponseStatusException;
2126

2227
import java.nio.charset.StandardCharsets;
28+
import java.time.LocalDate;
2329
import java.util.List;
30+
import java.util.Map;
2431

2532
@RestController
2633
@RequestMapping("/api/teacher")
@@ -68,16 +75,35 @@ public void linkAttempt(@RequestBody LinkAttemptRequest request) {
6875
}
6976

7077
@GetMapping("/classrooms/{classroomId}/dashboard")
71-
public ClassroomDashboardResponse getDashboard(@PathVariable String classroomId) {
72-
return workspaceService.getDashboard(classroomId);
78+
public ClassroomDashboardResponse getDashboard(
79+
@PathVariable String classroomId,
80+
@RequestParam(required = false) LocalDate from,
81+
@RequestParam(required = false) LocalDate to
82+
) {
83+
return workspaceService.getDashboard(classroomId, from, to);
7384
}
7485

7586
@GetMapping(value = "/classrooms/{classroomId}/report.csv", produces = "text/csv")
76-
public ResponseEntity<byte[]> exportCsv(@PathVariable String classroomId) {
77-
String csv = workspaceService.exportClassroomCsv(classroomId);
87+
public ResponseEntity<byte[]> exportCsv(
88+
@PathVariable String classroomId,
89+
@RequestParam(required = false) LocalDate from,
90+
@RequestParam(required = false) LocalDate to
91+
) {
92+
String csv = workspaceService.exportClassroomCsv(classroomId, from, to);
7893
return ResponseEntity.ok()
7994
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"classroom-" + classroomId + "-report.csv\"")
8095
.contentType(new MediaType("text", "csv", StandardCharsets.UTF_8))
8196
.body(csv.getBytes(StandardCharsets.UTF_8));
8297
}
98+
99+
@ExceptionHandler(IllegalArgumentException.class)
100+
@ResponseStatus(HttpStatus.BAD_REQUEST)
101+
public Map<String, String> onBadRequest(IllegalArgumentException ex) {
102+
return Map.of("error", ex.getMessage());
103+
}
104+
105+
@ExceptionHandler(Exception.class)
106+
public void onError(Exception ex) {
107+
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Error interno", ex);
108+
}
83109
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.juegodefinitivo.autobook.api.dto;
2+
3+
public record ActivityAbandonmentView(
4+
String eventType,
5+
int activeAttempts,
6+
int activeRatePercent
7+
) {
8+
}

apps/backend/src/main/java/com/juegodefinitivo/autobook/api/dto/ClassroomDashboardResponse.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ public record ClassroomDashboardResponse(
88
String teacherName,
99
int students,
1010
int assignments,
11+
int activeAttempts,
12+
int completedAttempts,
13+
int abandonmentRatePercent,
14+
int totalEffectiveReadingMinutes,
15+
int averageEffectiveMinutesPerAttempt,
16+
List<ActivityAbandonmentView> abandonmentByActivity,
1117
List<StudentProgressView> studentProgress
1218
) {
1319
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.juegodefinitivo.autobook.api.dto;
2+
3+
public record LoginRequest(
4+
String username,
5+
String password
6+
) {
7+
}

0 commit comments

Comments
 (0)