diff --git a/mvnw b/mvnw old mode 100644 new mode 100755 diff --git a/src/main/java/es/tatvil/taiageweb/TaiageApplication.java b/src/main/java/es/tatvil/taiageweb/TaiageApplication.java index bd77542..6d2a61e 100644 --- a/src/main/java/es/tatvil/taiageweb/TaiageApplication.java +++ b/src/main/java/es/tatvil/taiageweb/TaiageApplication.java @@ -3,9 +3,22 @@ package es.tatvil.taiageweb; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +/** + * Punto de entrada de la aplicación TAIage. + * + *
Plataforma web para la preparación de las oposiciones TAI (Técnico Auxiliar + * de Informática) de la Administración General del Estado (AGE). Ofrece acceso + * al temario completo, legislación y noticias relevantes, con control de acceso + * por roles y pasarela de pago integrada con Stripe.
+ */ @SpringBootApplication public class TaiageApplication { + /** + * Arranca el contexto Spring Boot. + * + * @param args argumentos de línea de comandos (no se utilizan) + */ public static void main(String[] args) { SpringApplication.run(TaiageApplication.class, args); } diff --git a/src/main/java/es/tatvil/taiageweb/config/DataInitializer.java b/src/main/java/es/tatvil/taiageweb/config/DataInitializer.java index 14cc6b2..c2e7958 100644 --- a/src/main/java/es/tatvil/taiageweb/config/DataInitializer.java +++ b/src/main/java/es/tatvil/taiageweb/config/DataInitializer.java @@ -10,8 +10,13 @@ import java.util.HashSet; import java.util.Set; /** - * Crea el usuario admin la primera vez que arranca la aplicación. - * IMPORTANTE: cambia la contraseña en cuanto puedas desde el panel /admin/usuarios. + * Inicializa datos mínimos en base de datos al arrancar la aplicación. + * + *Si no existe ningún usuario con el email {@code admin@taiage.es}, crea + * automáticamente la cuenta de administrador con credenciales predeterminadas.
+ * + *IMPORTANTE: cambia la contraseña por defecto en cuanto + * sea posible desde el panel {@code /admin/usuarios}.
*/ @Component public class DataInitializer implements CommandLineRunner { @@ -19,11 +24,22 @@ public class DataInitializer implements CommandLineRunner { private final UsuarioRepository repo; private final PasswordEncoder encoder; + /** + * Constructor con inyección de dependencias. + * + * @param repo repositorio de usuarios + * @param encoder encoder de contraseñas BCrypt + */ public DataInitializer(UsuarioRepository repo, PasswordEncoder encoder) { this.repo = repo; this.encoder = encoder; } + /** + * Crea el usuario administrador si todavía no existe en la base de datos. + * + * @param args argumentos de línea de comandos (no se utilizan) + */ @Override public void run(String... args) { if (repo.findByEmail("admin@taiage.es").isEmpty()) { diff --git a/src/main/java/es/tatvil/taiageweb/config/SecurityConfig.java b/src/main/java/es/tatvil/taiageweb/config/SecurityConfig.java index fe3a70a..29eb94b 100644 --- a/src/main/java/es/tatvil/taiageweb/config/SecurityConfig.java +++ b/src/main/java/es/tatvil/taiageweb/config/SecurityConfig.java @@ -10,15 +10,41 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +/** + * Configuración de seguridad de la aplicación. + * + *Define las reglas de acceso HTTP, el proveedor de autenticación basado en + * base de datos y la configuración del formulario de login/logout.
+ * + *La clave se obtiene de la propiedad {@code stripe.secret-key}, que debe + * definirse como variable de entorno {@code STRIPE_SECRET_KEY} en producción.
+ */ @Configuration public class StripeConfig { + /** Clave secreta de la cuenta Stripe (inyectada desde propiedades). */ @Value("${stripe.secret-key}") private String secretKey; + /** + * Asigna la clave secreta al SDK de Stripe tras la construcción del bean. + * Se ejecuta una sola vez al arrancar el contexto de Spring. + */ @PostConstruct public void init() { Stripe.apiKey = secretKey; diff --git a/src/main/java/es/tatvil/taiageweb/controlador/AdminController.java b/src/main/java/es/tatvil/taiageweb/controlador/AdminController.java index 990088e..2587eb2 100644 --- a/src/main/java/es/tatvil/taiageweb/controlador/AdminController.java +++ b/src/main/java/es/tatvil/taiageweb/controlador/AdminController.java @@ -6,37 +6,74 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.support.RedirectAttributes; +/** + * Controlador MVC para el panel de administración de usuarios. + * + *Todas las rutas bajo {@code /admin} requieren {@code ROLE_ADMIN} + * (configurado en {@link es.tatvil.taiageweb.config.SecurityConfig}). + * Permite listar, habilitar/deshabilitar, asignar acceso de pago, + * crear y eliminar usuarios.
+ */ @Controller @RequestMapping("/admin") public class AdminController { private final UsuarioService service; + /** + * Constructor con inyección de dependencias. + * + * @param service servicio de gestión de usuarios + */ public AdminController(UsuarioService service) { this.service = service; } + /** + * Lista todos los usuarios registrados. + * + * @param model modelo Thymeleaf al que se añade el atributo {@code usuarios} + * @return nombre de la plantilla {@code admin/usuarios} + */ @GetMapping({"", "/", "/usuarios"}) public String listar(Model model) { model.addAttribute("usuarios", service.listarTodos()); return "admin/usuarios"; } - /** Activa o desactiva la cuenta */ + /** + * Activa o desactiva la cuenta del usuario indicado. + * + * @param id identificador del usuario + * @return redirección a {@code /admin/usuarios} + */ @PostMapping("/usuarios/{id}/toggle-habilitado") public String toggleHabilitado(@PathVariable Long id) { service.toggleHabilitado(id); return "redirect:/admin/usuarios"; } - /** Concede o revoca el acceso al curso */ + /** + * Concede o revoca el acceso al curso ({@code ROLE_PAGADO}) del usuario indicado. + * + * @param id identificador del usuario + * @return redirección a {@code /admin/usuarios} + */ @PostMapping("/usuarios/{id}/toggle-pagado") public String togglePagado(@PathVariable Long id) { service.toggleRolPagado(id); return "redirect:/admin/usuarios"; } - /** El admin crea un usuario directamente activo */ + /** + * Crea un usuario directamente activo desde el panel de administración. + * + * @param email email del nuevo usuario + * @param password contraseña en texto plano + * @param pagado si {@code true}, se asigna {@code ROLE_PAGADO} además de {@code ROLE_USER} + * @param ra atributos flash para mensajes de error entre redirecciones + * @return redirección a {@code /admin/usuarios} + */ @PostMapping("/usuarios/nuevo") public String crear( @RequestParam String email, @@ -51,7 +88,12 @@ public class AdminController { return "redirect:/admin/usuarios"; } - /** Elimina un usuario */ + /** + * Elimina un usuario de la base de datos. + * + * @param id identificador del usuario a eliminar + * @return redirección a {@code /admin/usuarios} + */ @PostMapping("/usuarios/{id}/eliminar") public String eliminar(@PathVariable Long id) { service.eliminar(id); diff --git a/src/main/java/es/tatvil/taiageweb/controlador/PagoController.java b/src/main/java/es/tatvil/taiageweb/controlador/PagoController.java index 42fa961..c058a4b 100644 --- a/src/main/java/es/tatvil/taiageweb/controlador/PagoController.java +++ b/src/main/java/es/tatvil/taiageweb/controlador/PagoController.java @@ -13,6 +13,20 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +/** + * Controlador MVC para el flujo de pago con Stripe. + * + *Gestiona tres pasos del proceso de compra:
+ *La concesión real del acceso ocurre vía webhook en + * {@link WebhookController}, no en las páginas de retorno de Stripe.
+ */ @Controller public class PagoController { @@ -24,11 +38,25 @@ public class PagoController { private final UsuarioRepository usuarioRepo; + /** + * Constructor con inyección de dependencias. + * + * @param usuarioRepo repositorio de usuarios + */ public PagoController(UsuarioRepository usuarioRepo) { this.usuarioRepo = usuarioRepo; } - /** Página informativa con precio y botón "Pagar ahora" */ + /** + * Muestra la página informativa de pago con el precio actual. + * + *Si el usuario ya tiene {@code ROLE_PAGADO}, se le redirige directamente + * al curso sin pasar por la pasarela.
+ * + * @param auth autenticación del usuario en sesión (puede ser {@code null} si no está autenticado) + * @param model modelo Thymeleaf para pasar el precio formateado + * @return vista {@code pagar} o redirección a {@code /curso} + */ @GetMapping("/pagar") public String mostrarPagina(Authentication auth, Model model) { // Si ya tiene acceso, redirigir directo al curso @@ -40,7 +68,17 @@ public class PagoController { return "pagar"; } - /** Crea la sesión de pago en Stripe y redirige a su pasarela */ + /** + * Crea una sesión de pago en Stripe y redirige al usuario a la pasarela. + * + *Utiliza el id del usuario como {@code clientReferenceId} para poder + * identificarlo cuando llegue el webhook de confirmación.
+ * + * @param auth autenticación del usuario en sesión + * @param request petición HTTP para construir las URLs de retorno + * @return redirección a la URL de checkout de Stripe + * @throws StripeException si falla la comunicación con la API de Stripe + */ @PostMapping("/pagar/iniciar") public String iniciarPago(Authentication auth, HttpServletRequest request) throws StripeException { String baseUrl = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort(); @@ -71,11 +109,24 @@ public class PagoController { return "redirect:" + session.getUrl(); } + /** + * Página de confirmación mostrada tras un pago exitoso. + * + *El acceso real al curso se concede mediante el webhook de Stripe, + * no en esta página.
+ * + * @return nombre de la plantilla {@code pago-exito} + */ @GetMapping("/pagar/exito") public String exitoPago() { return "pago-exito"; } + /** + * Página mostrada cuando el usuario cancela el pago en Stripe. + * + * @return nombre de la plantilla {@code pago-cancelado} + */ @GetMapping("/pagar/cancelado") public String canceladoPago() { return "pago-cancelado"; diff --git a/src/main/java/es/tatvil/taiageweb/controlador/RegistroController.java b/src/main/java/es/tatvil/taiageweb/controlador/RegistroController.java index 3be5f69..48c9818 100644 --- a/src/main/java/es/tatvil/taiageweb/controlador/RegistroController.java +++ b/src/main/java/es/tatvil/taiageweb/controlador/RegistroController.java @@ -7,20 +7,49 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; +/** + * Controlador MVC para el auto-registro de nuevos usuarios. + * + *Procesa el formulario de registro en {@code /registro}. La cuenta creada + * queda deshabilitada hasta que el administrador la active desde + * {@code /admin/usuarios}.
+ */ @Controller public class RegistroController { private final UsuarioService service; + /** + * Constructor con inyección de dependencias. + * + * @param service servicio de gestión de usuarios + */ public RegistroController(UsuarioService service) { this.service = service; } + /** + * Muestra el formulario de registro. + * + * @return nombre de la plantilla {@code registro} + */ @GetMapping("/registro") public String mostrarFormulario() { return "registro"; } + /** + * Procesa el formulario de registro. + * + *Valida que las contraseñas coincidan y delega en {@link UsuarioService#registrar}. + * Si el registro tiene éxito, redirige al login con el parámetro {@code ?registrado}.
+ * + * @param email email del nuevo usuario + * @param password contraseña elegida + * @param passwordConfirm confirmación de la contraseña + * @param model modelo Thymeleaf para enviar mensajes de error + * @return redirección a {@code /login?registrado} si éxito, o la vista {@code registro} con error + */ @PostMapping("/registro") public String registrar( @RequestParam String email, diff --git a/src/main/java/es/tatvil/taiageweb/controlador/TemaController.java b/src/main/java/es/tatvil/taiageweb/controlador/TemaController.java index 3bee047..a0b7b78 100644 --- a/src/main/java/es/tatvil/taiageweb/controlador/TemaController.java +++ b/src/main/java/es/tatvil/taiageweb/controlador/TemaController.java @@ -11,10 +11,32 @@ import org.springframework.web.bind.annotation.RestController; import java.io.IOException; import java.nio.charset.StandardCharsets; +/** + * API REST que sirve los ficheros Markdown del temario. + * + *Todos los recursos están bajo {@code /api/temas/**} y requieren + * autenticación con {@code ROLE_PAGADO} o {@code ROLE_ADMIN} (configurado en + * {@link es.tatvil.taiageweb.config.SecurityConfig}).
+ * + *Los ficheros se leen desde el classpath en {@code temas/} y se devuelven + * como texto plano UTF-8. Se protege contra ataques de path traversal + * rechazando rutas que contengan {@code ..} o {@code //}.
+ */ @RestController @RequestMapping("/api/temas") public class TemaController { + /** + * Devuelve el contenido Markdown de un tema dado su ruta relativa. + * + *Ejemplo de petición: {@code GET /api/temas/bloque1/tema1.md}
+ * + * @param request petición HTTP de la que se extrae la ruta del recurso + * @return {@code 200 OK} con el contenido Markdown como texto plano, + * {@code 400 Bad Request} si la ruta es inválida, + * {@code 404 Not Found} si el fichero no existe + * @throws IOException si ocurre un error leyendo el recurso del classpath + */ @GetMapping("/**") public ResponseEntityTodas las rutas devuelven el nombre de la plantilla correspondiente en + * {@code src/main/resources/templates/}. Spring Security controla qué rutas + * son accesibles sin autenticación.
*/ @Controller public class WebController { + /** + * Página de inicio. + * + * @return nombre de la plantilla {@code index} + */ @GetMapping({"/", "/inicio"}) public String inicio() { return "index"; } + /** + * Página de inicio de sesión. + * + * @return nombre de la plantilla {@code login} + */ @GetMapping("/login") public String login() { return "login"; } + /** + * Visor del temario del curso (requiere {@code ROLE_PAGADO} o {@code ROLE_ADMIN}). + * + * @return nombre de la plantilla {@code curso} + */ @GetMapping("/curso") public String curso() { return "curso"; } + /** + * Navegador de legislación; acceso público. + * + * @return nombre de la plantilla {@code leyes} + */ @GetMapping("/leyes") public String leyes() { return "leyes"; } + /** + * Agregador de noticias INAP/BOE; acceso público. + * + * @return nombre de la plantilla {@code noticias} + */ @GetMapping("/noticias") public String noticias() { return "noticias"; } + /** + * Página de acceso denegado (HTTP 403). + * + * @return nombre de la plantilla {@code acceso-denegado} + */ @GetMapping("/acceso-denegado") public String accesoDenegado() { return "acceso-denegado"; } } diff --git a/src/main/java/es/tatvil/taiageweb/controlador/WebhookController.java b/src/main/java/es/tatvil/taiageweb/controlador/WebhookController.java index 67f1d81..efcd047 100644 --- a/src/main/java/es/tatvil/taiageweb/controlador/WebhookController.java +++ b/src/main/java/es/tatvil/taiageweb/controlador/WebhookController.java @@ -9,18 +9,49 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +/** + * Controlador REST que recibe y procesa webhooks de Stripe. + * + *Escucha en {@code POST /webhook/stripe} (ruta excluida de CSRF en + * {@link es.tatvil.taiageweb.config.SecurityConfig}). Verifica la firma + * del payload con el secreto de webhook antes de procesar ningún evento.
+ * + *Evento gestionado:
+ *Verifica la firma {@code Stripe-Signature} del payload. Si es válida + * y el evento es {@code checkout.session.completed}, concede + * {@code ROLE_PAGADO} al usuario referenciado.
+ * + * @param payload cuerpo crudo de la petición HTTP (necesario para verificar la firma) + * @param sigHeader valor del header {@code Stripe-Signature} + * @return {@code 200 OK} si el evento se procesa correctamente, + * {@code 400 Bad Request} si la firma es inválida + */ @PostMapping("/webhook/stripe") public ResponseEntityLos usuarios se almacenan en la tabla {@code usuarios}. Sus roles se guardan + * en la tabla auxiliar {@code usuario_roles} mediante una relación + * {@code @ElementCollection}.
+ * + *Las cuentas nuevas arrancan con {@code false}; el administrador o un pago + * completado la activan automáticamente.
+ */ @Column(nullable = false) private boolean habilitado = false; /** - * Roles posibles: - * ROLE_USER – registrado (sin acceso al curso) - * ROLE_PAGADO – ha pagado (acceso al curso) - * ROLE_ADMIN – administrador (acceso total) + * Conjunto de roles asignados al usuario. + * Los valores posibles son {@code ROLE_USER}, {@code ROLE_PAGADO} y {@code ROLE_ADMIN}. */ @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "usuario_roles", joinColumns = @JoinColumn(name = "usuario_id")) @Column(name = "rol") private SetExtiende {@link JpaRepository} con métodos de búsqueda adicionales + * necesarios para la autenticación y el registro de usuarios.
+ */ public interface UsuarioRepository extends JpaRepositoryImplementa {@link UserDetailsService} para la integración con Spring Security + * y centraliza todas las operaciones sobre la entidad {@link Usuario}: registro, + * activación, asignación de roles y eliminación.
+ */ @Service public class UsuarioService implements UserDetailsService { private final UsuarioRepository repo; private final PasswordEncoder encoder; + /** + * Constructor con inyección de dependencias. + * + * @param repo repositorio de usuarios + * @param encoder encoder de contraseñas BCrypt + */ public UsuarioService(UsuarioRepository repo, PasswordEncoder encoder) { this.repo = repo; this.encoder = encoder; } - // ── Spring Security ────────────────────────────────────── + // ── Spring Security ────────────────────────────────── + /** + * Carga los detalles de un usuario por su email para Spring Security. + * + * @param email email del usuario (campo {@code username} del formulario de login) + * @return detalles del usuario listos para la autenticación + * @throws UsernameNotFoundException si no existe ningún usuario con ese email + */ @Override public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { Usuario u = repo.findByEmail(email) @@ -48,8 +68,13 @@ public class UsuarioService implements UserDetailsService { } // ── Auto-registro (pendiente de activación) ────────────── - - @Transactional + /** + * Registra un nuevo usuario con la cuenta deshabilitada hasta que el admin la active. + * + * @param email email del nuevo usuario + * @param passwordPlana contraseña en texto plano (mínimo 8 caracteres) + * @throws IllegalArgumentException si el email ya está registrado o la contraseña es demasiado corta + */ @Transactional public void registrar(String email, String passwordPlana) { if (repo.existsByEmail(email)) { throw new IllegalArgumentException("El email ya está registrado"); @@ -66,19 +91,32 @@ public class UsuarioService implements UserDetailsService { } // ── Consultas ──────────────────────────────────────────── - - public ListAñade {@code ROLE_PAGADO} y activa la cuenta si estuviera deshabilitada. + * Este método nunca quita el rol una vez concedido.
+ * + * @param id identificador del usuario + * @throws java.util.NoSuchElementException si no existe un usuario con ese id + */ @Transactional public void darAccesoPagado(Long id) { Usuario u = repo.findById(id).orElseThrow(); @@ -97,7 +143,14 @@ public class UsuarioService implements UserDetailsService { u.setHabilitado(true); } - /** El admin crea directamente un usuario ya activo */ + /** + * Crea un usuario directamente activo desde el panel de administración. + * + * @param email email del nuevo usuario + * @param passwordPlana contraseña en texto plano + * @param pagado si {@code true}, se añade {@code ROLE_PAGADO} además de {@code ROLE_USER} + * @throws IllegalArgumentException si el email ya está registrado + */ @Transactional public void crearPorAdmin(String email, String passwordPlana, boolean pagado) { if (repo.existsByEmail(email)) { @@ -114,6 +167,11 @@ public class UsuarioService implements UserDetailsService { repo.save(u); } + /** + * Elimina un usuario de la base de datos. + * + * @param id identificador del usuario a eliminar + */ @Transactional public void eliminar(Long id) { repo.deleteById(id);